Edited at
WebRTCDay 1

WebRTCで統計情報を収集する


統計情報とは?

WebRTCでは、基盤となるネットワーク環境や送受信されるメディアの情報を監視することが出来るようにように、統計情報のAPIが規定されています。

最新の仕様書は https://w3c.github.io/webrtc-stats/ です。

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

注意: 記事中で、SkyWayのJavaScript SDKを利用して統計情報を収集していますが、こちらは非公式な方法となります。そのため、この方法は将来変更される可能性があります

SkyWay JavaScript SDK v1.4.0からRTCPeerConnectionを取得するAPIが追加されたため、こちらを利用すると統計情報収集API getStats()にアクセスが可能です。


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

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のサンプルアプリを利用して統計情報を収集してみます。


動作イメージ

getstats.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.getPeerConnection().getStats();


受信側(例)


example

peer = new Peer({key:APIKEY});

peer.on('call', call => {
call.answer(localStream);
const stats = call.getPeerConnection().getStats();
});

注意: この方法は将来変更される可能性があります SkyWay JavaScript SDK v1.4.0からRTCPeerConnectionを取得するAPIが追加されました

注意: SkyWay Android/iOS/IoT SDKでは統計情報の収集はできません


デモアプリのコード解説

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


script.js

// `existingCall`には`call`オブジェクトが格納されており、getPeerConnection()を実行するとRTCPeerConnectionが取得できる

const _PC = existingCall.getPeerConnection();
// setInterval`で1000ms間隔でgetRTCStatsを実行する
timer = setInterval(() => {
// `getRTCStats`に`getStats`オブジェクトを引き数で渡す
getRTCStats(_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_sender') !== -1){
mediaStreamTrack_senderArray.push(stat);
}
if(stat.id.indexOf('RTCMediaStreamTrack_receiver') !== -1){
mediaStreamTrack_receiverArray.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;
}
});
});
// mediaStreamTrack_senderArrayには送信中のMediaStreamTrack(Audio/Video共に)が格納されているのでそれぞれ取り出す
mediaStreamTrack_senderArray.forEach(mediaStreamTrack => {
if(mediaStreamTrack.kind === 'audio'){
mediaStreamTrack_local_audioArray.push(mediaStreamTrack)
}else if(mediaStreamTrack.kind === 'video'){
mediaStreamTrack_local_videoArray.push(mediaStreamTrack)
}
});
// mediaStreamTrack_receiverArrayには受信中のMediaStreamTrack(Audio/Video共に)が格納されているのでそれぞれ取り出す
mediaStreamTrack_receiverArray.forEach(mediaStreamTrack => {
if(mediaStreamTrack.kind === 'audio'){
mediaStreamTrack_remote_audioArray.push(mediaStreamTrack)
}else if(mediaStreamTrack.kind === 'video'){
mediaStreamTrack_remote_videoArray.push(mediaStreamTrack)
}
});

// 収集した情報を画面に表示する
$('#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);

}



  • 2018/08/08 Chrome RTCStatsReportの仕様変更に伴いコードを修正


    • RTCMediaStreamTrack_sender/RTCMediaStreamTrack_receiverという項目が増え、RTCMediaStreamTrackStats相当の情報はその中に配列として格納されるようになった




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

WebRTCの統計情報収集に特化したSaaSとして、callstats.ioというサービスが有ります。

有償ですが、本格的に収集したい場合は活用してみるのも良いかもしれません。