統計情報とは?
WebRTCでは、基盤となるネットワーク環境や送受信されるメディアの情報を監視することが出来るようにように、統計情報のAPIが規定されています。
最新の仕様書は https://w3c.github.io/webrtc-stats/ です。
このAPIで取得可能な情報は、WebRTCを使ってアプリケーションを開発したことがある方なら、一度はお世話になっているであろう、chrome://webrtc-internlsやabout:webrtcで表示される情報と同じ物となります。
注意: 記事中で、SkyWayのJavaScript SDKを利用して統計情報を収集していますが、こちらは非公式な方法となります。そのため、この方法は将来変更される可能性があります
SkyWay JavaScript SDK v1.4.0からRTCPeerConnectionを取得するAPIが追加されたため、こちらを利用すると統計情報収集API getStats()にアクセスが可能です。
どのような情報を収集することが出来るか
W3Cの仕様書で解説します。
RTCStatsのWebIDL
dictionary RTCStats {
DOMHighResTimeStamp timestamp;
RTCStatsType type;
DOMString id;
};
ポイントは、RTCStatsTypeです。
RTCStatsTypeで取得できる統計情報が規定されています。
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のサンプルアプリを利用して統計情報を収集してみます。
- ソースコード : https://github.com/yusuke84/skyway-stats-sample
- サンプルアプリ : https://yusuke84.github.io/skyway-stats-sample/index.html
- 動作確認済みブラウザ(2019/6/28現在)
- Chrome M75 for macOS
動作イメージ
注意: W3Cの仕様書と実際にブラウザで取得出来る情報には差分があります。また、APIの実装方法もブラウザごとに差分があります。今回のサンプルアプリはChromeで動作するように作成してあります。Fireofxでは正常に情報が収集できません。
skyway.jsで統計情報のAPIにアクセスする方法
skyway.jsは以下の方法でRTCPeerConnectionのgetStats API
にアクセスが可能です。
発信側(例)
peer = new Peer({key:APIKEY});
const call = peer.call(peerid,localStream);
const stats = call.getPeerConnection().getStats();
受信側(例)
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
は実行した時点の情報を収集するため、連続して収集するには定期実行する必要があります。
// `existingCall`には`call`オブジェクトが格納されており、getPeerConnection()を実行するとRTCPeerConnectionが取得できる
const _PC = existingCall.getPeerConnection();
// setInterval`で1000ms間隔でgetRTCStatsを実行する
timer = setInterval(() => {
// `getRTCStats`に`getStats`オブジェクトを引き数で渡す
getRTCStats(_PC.getStats());
}, 1000);
getStatsは
非同期型でpromiseベースとなります。サンプルアプリでは、async/await
で記述しています。
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というサービスが有ります。
有償ですが、本格的に収集したい場合は活用してみるのも良いかもしれません。