統計情報とは?

WebRTCでは、基盤となるネットワーク環境や送受信されるメディアの情報を監視することが出来るようにように、統計情報のAPIが規定されています。
最新の仕様書は https://w3c.github.io/webrtc-stats/ です。

このAPIで取得可能な情報は、WebRTCを使ってアプリケーションを開発したことがある方なら、一度はお世話になっているであろう、chrome://webrtc-internlsabout:webrtcで表示される情報と同じ物となります。

どのような情報を収集することが出来るか

W3Cの仕様書で解説します。

RTCStatsのWebIDL

WebIDL
dictionary RTCStats {
  DOMHighResTimeStamp timestamp;
  RTCStatsType type;
  DOMString id;
};

ポイントは、RTCStatsTypeです。

RTCStatsTypeで取得できる統計情報が規定されています。

WebIDL
enum RTCStatsType {
    "codec",
    "inbound-rtp",
    "outbound-rtp",
    "remote-inbound-rtp",
    "remote-outbound-rtp",
    "csrc",
    "peer-connection",
    "data-channel",
    "stream",
    "track",
    "transport",
    "candidate-pair",
    "local-candidate",
    "remote-candidate",
    "certificate"
};
タイプ 内容
codec 現在使用されている映像、音声のコーデックの種類
inbound-rtp 受信しているRTPストリームの統計
outbound-rtp 送信しているRTPストリームの統計
remote-inbound-rtp 送信したRTPストリームがリモートで受信された際の統計(RTCPプロトコルの受信レポートで収集)
remote-outbound-rtp 受信したRTPストリームがリモートで送信された際の統計(RTCPプロトコルの送信レポートで収集)
csrc 受信しているRTPストリームを識別するための情報
peer-connection PeerConnectionに関する統計
data-channel DataChannel IDに関する情報
stream メディアストリームの統計情報
track メディアストリームトラックの統計
transport 通信に利用しているトランスポートの統計
candidate-pair ペアになるCandidate情報
local-candidate 自身のCandidate情報
remote-candidate リモートのCandidate情報
certificate トランスポートの生成に使用された証明書の情報

アプリ開発時に活用できそうな情報

全てのStatstypeについて解説すると長くなりすぎるので、筆者の主観ですが、アプリ開発で使いそうな項目をピックアップしてみます。

定義 内容 備考
RTCCodecStats.codec 現在使用されているコーデックの種類(VP8,H264,OPUS…) 使用しているコーデックが知りたいときって結構有りますよね
RTCInboundRTPStreamStats.bytesReceived
RTCOutboundRTPStreamStats.bytesSent
送受信したデータ量(バイト) 上と同じく
RTCInboundRTPStreamStats.fractionLost パケットロス率 ネットワーク品質などを見極めたい時は便利
RTCMediaStreamTrackStats.frameWidth
RTCMediaStreamTrackStats.frameHeight
RTCMediaStreamTrackStats.framesPerSecond
ビデオストリームトラックにおけるフレームサイズとフレームレート WebRTCはネットワーク品質を考慮して自動的にこの辺りのパラメーターを調整します
RTCMediaStreamTrackStats.audioLevel オーディオストリームトラックにおけるオーディオレベル(dB) 音が出ているかどうかがわかるかな
RTCTransportStats.activeConnection トランスポートがアクティブかどうか P2Pの通信経路が確立しているかどうかがわかる
RTCIceCandidateStats.ip
RTCIceCandidateStats.port
RTCIceCandidateStats.protocol
該当するCandidateのIPアドレス、ポート番号、プロトコル種別(UDP/TCP) 利用者のIPアドレス、ポート番号を収集したいってニーズはあるはず
RTCIceCandidateStats.candidateType 該当するCandidateのタイプ(host/srflx/prflx/relay) 各タイプの説明は SkyWay FAQを参照
TURN経由しているかどうか知りたいことって多いですよね

実際に収集してみる

SkyWayのサンプルアプリを利用して統計情報を収集してみます。

動作イメージ

スクリーンショット 2017-12-01 5.08.10.png

注意: W3Cの仕様書と実際にブラウザで取得出来る情報には差分があります。また、APIの実装方法もブラウザごとに差分があります。今回のサンプルアプリはChromeで動作するように作成してあります。Fireofxでは正常に情報が収集できません。

skyway.jsで統計情報のAPIにアクセスする方法

skyway.jsは以下の方法でRTCPeerConnectionのgetStats APIにアクセスが可能です。

発信側(例)

example
peer = new Peer({key:APIKEY});
const call = peer.call(peerid,localStream);
const stats = call._negotiator._pc.getStats();

受信側(例)

example
peer = new Peer({key:APIKEY});
    peer.on('call', call => {
        call.answer(localStream);
        const stats = call._negotiator._pc.getStats();
    });

注意: この方法は将来変更されるかもしれません
注意: SkyWay Android/iOS/IoT SDKでは統計情報の収集はできません

デモアプリのコード解説

getStatsは実行した時点の情報を収集するため、連続して収集するには定期実行する必要があります。

script.js
// setInterval`で1000ms間隔でgetRTCStatsを実行する
timer = setInterval(() => {
    // `getRTCStats`に`getStats`オブジェクトを引き数で渡す
    // `existingCall`には`call`オブジェクトが格納されている
    getRTCStats(existingCall._negotiator._pc.getStats());
}, 1000);

getStatsは非同期型でpromiseベースとなります。サンプルアプリでは、async/awaitで記述しています。

script.js
    async function getRTCStats(statsObject){

            // 宣言文は省略

        // getStats()を実行し結果が取得できるまで待機
        let stats = await statsObject;

        // 取得できた統計情報オブジェクトのIDから必要な物を操作しやすいように配列に格納
        stats.forEach(stat => {
            if(stat.id.indexOf('RTCTransport') !== -1){
                trasportArray.push(stat);
            }                
            if(stat.id.indexOf('RTCIceCandidatePair') !== -1){
                candidatePairArray.push(stat);
            }
            if(stat.id.indexOf('RTCIceCandidate_') !== -1){
                candidateArray.push(stat);
            }
            if(stat.id.indexOf('RTCInboundRTPAudioStream') !== -1){
                inboundRTPAudioStreamArray.push(stat);
            }
            if(stat.id.indexOf('RTCInboundRTPVideoStream') !== -1){
                inboundRTPVideoStreamArray.push(stat);
            }
            if(stat.id.indexOf('RTCOutboundRTPAudioStream') !== -1){
                outboundRTPAudioStreamArray.push(stat);
            }
            if(stat.id.indexOf('RTCOutboundRTPVideoStream') !== -1){
                outboundRTPVideoStreamArray.push(stat);
            }
            if(stat.id.indexOf('RTCMediaStreamTrack_local_audio') !== -1){
                mediaStreamTrack_local_audioArray.push(stat);
            }
            if(stat.id.indexOf('RTCMediaStreamTrack_local_video') !== -1){
                mediaStreamTrack_local_videoArray.push(stat);
            }
            if(stat.id.indexOf('RTCMediaStreamTrack_remote_audio') !== -1){
                mediaStreamTrack_remote_audioArray.push(stat);
            }
            if(stat.id.indexOf('RTCMediaStreamTrack_remote_video') !== -1){
                mediaStreamTrack_remote_videoArray.push(stat);
            }
            if(stat.id.indexOf('RTCCodec') !== -1){
                codecArray.push(stat);
            }
        });

        // Transportの統計からselectedCandidatePairIdを取得
        trasportArray.forEach(transport => {
            if(transport.dtlsState === 'connected'){
                candidatePairId = transport.selectedCandidatePairId;
            }
        });
        // selectedCandidatePairIdをもとに通信に成功している、LocalCandidateID/RemoteCandidateIDを取り出す
        candidatePairArray.forEach(candidatePair => {
            if(candidatePair.state === 'succeeded' && candidatePair.id === candidatePairId){
                localCandidateId = candidatePair.localCandidateId;
                remoteCandidateId = candidatePair.remoteCandidateId;
            }
        });
        // LocalCandidateID/RemoteCandidateIDから、LocalCandidate/RemoteCandidateを取り出す
        candidateArray.forEach(candidate => {
            if(candidate.id === localCandidateId){
                localCandidate = candidate;
            }
            if(candidate.id === remoteCandidateId){
                remoteCandidate = candidate;
            }
        });
        // InboundRTPAudioStreamのcodecIdをもとに、codecArrayから利用されているCodec情報を取り出す
        inboundRTPAudioStreamArray.forEach(inboundRTPAudioStream => {
            codecArray.forEach(codec => {
                if(inboundRTPAudioStream.codecId === codec.id){
                    inboundAudioCodec = codec;
                }
            });
        });
        // inboundRTPVideoStreamArrayのcodecIdをもとに、codecArrayから利用されているCodec情報を取り出す
        inboundRTPVideoStreamArray.forEach(inboundRTPVideoStream => {
            codecArray.forEach(codec => {
                if(inboundRTPVideoStream.codecId === codec.id){
                    inboundVideoCodec = codec;
                }
            });
        });
        // outboundRTPAudioStreamArrayのcodecIdをもとに、codecArrayから利用されているCodec情報を取り出す     
        outboundRTPAudioStreamArray.forEach(outboundRTPAudioStream => {
            codecArray.forEach(codec => {
                if(outboundRTPAudioStream.codecId === codec.id){
                    outboundAudioCodec = codec;
                }
            });
        });
        // outboundRTPVideoStreamArrayのcodecIdをもとに、codecArrayから利用されているCodec情報を取り出す
        outboundRTPVideoStreamArray.forEach(outboundRTPVideo => {
            codecArray.forEach(codec => {
                if(outboundRTPVideo.codecId === codec.id){
                    outboundVideoCodec = codec;
                }
            });
        });        

        // 収集した情報を画面に表示する
        $('#local-candidate').html(localCandidate.ip + ':' + localCandidate.port + '(' +localCandidate.protocol + ')' + '<BR>type:' + localCandidate.candidateType);
        $('#remote-candidate').html(remoteCandidate.ip + ':' + remoteCandidate.port + '(' +remoteCandidate.protocol + ')' + '<BR>type:' + remoteCandidate.candidateType);        
        $('#inbound-codec').html(inboundVideoCodec.mimeType + '<BR>' + inboundAudioCodec.mimeType);
        $('#outbound-codec').html(outboundVideoCodec.mimeType + '<BR>' + outboundAudioCodec.mimeType)
        $('#inbound-audio').html('bytesReceived:' + inboundRTPAudioStreamArray[0].bytesReceived + '<BR>jitter:' + inboundRTPAudioStreamArray[0].jitter + '<BR>fractionLost:' + inboundRTPAudioStreamArray[0].fractionLost);
        $('#inbound-video').html('bytesReceived:' + inboundRTPVideoStreamArray[0].bytesReceived + '<BR>fractionLost:' + inboundRTPVideoStreamArray[0].fractionLost);        
        $('#outbound-audio').html('bytesReceived:' + outboundRTPAudioStreamArray[0].bytesSent);
        $('#outbound-video').html('bytesReceived:' + outboundRTPVideoStreamArray[0].bytesSent);
        $('#local-audio-video').html('audioLevel:' + mediaStreamTrack_local_audioArray[0].audioLevel + '<BR>frameHeight:' + mediaStreamTrack_local_videoArray[0].frameHeight + '<BR>frameWidth:' + mediaStreamTrack_local_videoArray[0].frameWidth + '<BR>framesSent:' + mediaStreamTrack_local_videoArray[0].framesSent);
        $('#remote-audio-video').html('audioLevel:' + mediaStreamTrack_remote_audioArray[0].audioLevel + '<BR>frameHeight:' + mediaStreamTrack_local_videoArray[0].frameHeight + '<BR>frameWidth:' + mediaStreamTrack_remote_videoArray[0].frameWidth);

    }

参考:統計情報を収集するSaaS

WebRTCの統計情報収集に特化したSaaSとして、callstats.ioというサービスが有ります。
有償ですが、本格的に収集したい場合は活用してみるのも良いかもしれません。