16

I'm finishing a WebRTC project for a graduate course in video communications, it's essentially a video conference chat room. Everyone that connects to the server is added to the conference.

I need to use the stats API in WebRTC to show some relevant performance statistics for each RTCPeerConnection (packets lost per second, jitter, retransmission, etc). This helps to observe performance cost as more peers are added to the conversation.

However the API seems to not be fully fleshed out yet. It's apparently gone through some refreshes and doesn't quite match up to some W3C specs I've seen (though perhaps it's out of date or I just don't understand nuances of reading the spec, neither would surprise me).

My invocation of the API is similar to this one, but interpreting the data isn't straightforward. For instance when looping through all items in RTCStatsReport::results(), many of them have duplicate names and confusing values. I can't seem to find any information about their meaning. If anyone can assist me in understanding some of the important ones or point me to the lost city of gold (e.g. proper documentation), I'd be grateful.

Mogsdad
  • 44,709
  • 21
  • 151
  • 275
Joey Carson
  • 2,973
  • 7
  • 36
  • 60
  • 2
    I feel your pain. I haven't found any docs on that topic too. I wrote a simple Wrapper around the get Stats API a while back, perhaps it helps you out a little, [I just put it on GitHub](https://github.com/wpp/simplestats/blob/master/simpleStats.js). – wpp Apr 23 '15 at 07:45
  • Hi, the main problem is the lack of documentation on how to interpret the data. Looking over your implementation helps to piece some of this data together and make sense of it. I will definitely pull it into my project and I thank you for it. The README file is a pretty sparse, but the organization when i dump the data should be enough to understand the meaning of all data. Thanks again, this is the next best thing without documentation. – Joey Carson Apr 23 '15 at 15:11
  • You can also check out https://github.com/webrtc/apprtc/tree/master/src/web_app/js especially the files "infobox.js" and "stats.js". I haven't had a chance to update/expand the README sry. – wpp Apr 23 '15 at 15:51
  • For anyone from the future, this page shows API-by-API browser compatibility: https://webrtc-stats.callstats.io/verify – AndrewJC Dec 12 '19 at 23:15

1 Answers1

18

The source of your confusion is likely that Google Chrome's implementation of getStats() pre-dates the standard and has not been updated yet (the example you link to is Chrome-specific, so I presume you are using Chrome).

If you were to try Firefox, you would find that it implements getStats() to the standard (however it does not support all the stats in the standard yet, and fewer stats overall than Chrome's old API).

Since you didn't specify a browser, I'll describe the standard, and use Firefox to show an example. You probably know getStats() already, but the standard one lets you filter on, say, a specific MediaStreamTrack, or pass in null to get all the data associated with a connection:

var pc = new RTCPeerConnection(config)
...
pc.getStats(null, function(stats) { ...}, function(error) { ... });

There's a newer promise-version as well.

Data is returned in stats, a big snowball object with unique ids for each record. Each record has the following base class:

dictionary RTCStats {
    DOMHiResTimeStamp timestamp;
    RTCStatsType      type;
    DOMString         id;
};

where id is a repeat of the property name used to access the record. The derived types are described here.

You typically enumerate the records until you find an RTCStatsType of interest, e.g. "inbound-rtp" which looks like this:

dictionary RTCRTPStreamStats : RTCStats {
         DOMString     ssrc;
         DOMString     remoteId;
         boolean       isRemote = false;
         DOMString     mediaTrackId;
         DOMString     transportId;
         DOMString     codecId;
         unsigned long firCount;
         unsigned long pliCount;
         unsigned long nackCount;
         unsigned long sliCount;
};

dictionary RTCInboundRTPStreamStats : RTCRTPStreamStats {
         unsigned long      packetsReceived;
         unsigned long long bytesReceived;
         unsigned long      packetsLost;
         double             jitter;
         double             fractionLost;
};

There's a corresponding one for RTCOutboundRTPStreamStats.

You can also follow cross-references to other records. Any member ending with Id is a foreign-key you can use to look up another record. For instance, mediaTrackId links to RTCMediaStreamTrackStats for the track this RTP data belongs to.

A particularly squirrelly case is RTCP data, which is stored in the same dictionaries as above, which means you have to check isRemote == false to know you are looking at RTP data and not RTCP data. Use the remoteId to find the other one (Note that this is a recent name-change, so Firefox still uses an older remoteId here). The associated RTCP stats for outbound RTP is stored in an inbound dictionary, and vice versa (makes sense).

Here's an example that runs in Firefox:

var pc1 = new RTCPeerConnection(), pc2 = new RTCPeerConnection();

var add = (pc, can) => can && pc.addIceCandidate(can).catch(log);
pc1.onicecandidate = e => add(pc2, e.candidate);
pc2.onicecandidate = e => add(pc1, e.candidate);

pc2.oniceconnectionstatechange = () => update(statediv, pc2.iceConnectionState);
pc2.onaddstream = e => v2.srcObject = e.stream;

navigator.mediaDevices.getUserMedia({ video: true })
  .then(stream => pc1.addStream(v1.srcObject = stream))
  .then(() => pc1.createOffer())
  .then(offer => pc1.setLocalDescription(offer))
  .then(() => pc2.setRemoteDescription(pc1.localDescription))
  .then(() => pc2.createAnswer())
  .then(answer => pc2.setLocalDescription(answer))
  .then(() => pc1.setRemoteDescription(pc2.localDescription))
  .then(() => repeat(10, () => Promise.all([pc1.getStats(), pc2.getStats()])
    .then(([s1, s2]) => {
      var s = "";
      s1.forEach(stat => {
        if (stat.type == "outbound-rtp" && !stat.isRemote) {
          s += "<h4>Sender side</h4>" + dumpStats(stat);
        }
      });
      s2.forEach(stat => {
        if (stat.type == "inbound-rtp" && !stat.isRemote) {
          s += "<h4>Receiver side</h4>" + dumpStats(stat);
        }
      });
      update(statsdiv, "<small>"+ s +"</small>");
  })))
  .catch(failed);

function dumpStats(o) {
  var s = "";
  if (o.mozAvSyncDelay !== undefined || o.mozJitterBufferDelay !== undefined) {
    if (o.mozAvSyncDelay !== undefined) s += "A/V sync: " + o.mozAvSyncDelay + " ms";
    if (o.mozJitterBufferDelay !== undefined) {
      s += " Jitter buffer delay: " + o.mozJitterBufferDelay + " ms";
    }
    s += "<br>";
  }
  s += "Timestamp: "+ new Date(o.timestamp).toTimeString() +" Type: "+ o.type +"<br>";
  if (o.ssrc !== undefined) s += "SSRC: " + o.ssrc + " ";
  if (o.packetsReceived !== undefined) {
    s += "Recvd: " + o.packetsReceived + " packets";
    if (o.bytesReceived !== undefined) {
      s += " ("+ (o.bytesReceived/1024000).toFixed(2) +" MB)";
    }
    if (o.packetsLost !== undefined) s += " Lost: "+ o.packetsLost;
  } else if (o.packetsSent !== undefined) {
    s += "Sent: " + o.packetsSent + " packets";
    if (o.bytesSent !== undefined) s += " ("+ (o.bytesSent/1024000).toFixed(2) +" MB)";
  } else {
    s += "<br><br>";
  }
  s += "<br>";
  if (o.bitrateMean !== undefined) {
    s += " Avg. bitrate: "+ (o.bitrateMean/1000000).toFixed(2) +" Mbps";
    if (o.bitrateStdDev !== undefined) {
      s += " ("+ (o.bitrateStdDev/1000000).toFixed(2) +" StdDev)";
    }
    if (o.discardedPackets !== undefined) {
      s += " Discarded packts: "+ o.discardedPackets;
    }
  }
  s += "<br>";
  if (o.framerateMean !== undefined) {
    s += " Avg. framerate: "+ (o.framerateMean).toFixed(2) +" fps";
    if (o.framerateStdDev !== undefined) {
      s += " ("+ o.framerateStdDev.toFixed(2) +" StdDev)";
    }
  }
  if (o.droppedFrames !== undefined) s += " Dropped frames: "+ o.droppedFrames;
  if (o.jitter !== undefined) s += " Jitter: "+ o.jitter;
  return s;
}

var wait = ms => new Promise(r => setTimeout(r, ms));
var repeat = (ms, func) => new Promise(r => (setInterval(func, ms), wait(ms).then(r)));
var log = msg => div.innerHTML = div.innerHTML + msg +"<br>";
var update = (div, msg) => div.innerHTML = msg;
var failed = e => log(e.name +": "+ e.message +", line "+ e.lineNumber);
<table><tr><td>
  <video id="v1" width="124" height="75" autoplay></video><br>
  <video id="v2" width="124" height="75" autoplay></video><br>
  <div id="statediv"></div></td>
<td><div id="div"></div><br><div id="statsdiv"></div></td>
</tr></table>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

To see what's supported, do stats.forEach(stat => console.log(JSON.stringify(stat))) to dump everything. Hard to read but it's all there.

I believe a polyfill is planned shortly for adapter.js to bridge the gap until Chrome updates its implementation.

Update: I've updated the examples to use the new maplike syntax, and changed type-names to include dashes, to conform with the latest spec.

jib
  • 40,579
  • 17
  • 100
  • 158
  • 1
    I can't thank you enough for taking the time. Yes I am using chrome on OS X and that's kind of what I meant regarding the fact that the API has seen some refreshes. When looking at the Mozilla API docs, getStats has a listing that says it takes a MediaStreamTrack, however there isn't an associated page to discuss it. Likewise the Chrome implementation is obviously different. The W3C spec doesn't give much information on how to interpret it but clearly aligns more with the Mozilla. I expected the Google implementation to be more up-to-date, a lot sources suggest that. – Joey Carson Apr 24 '15 at 11:43
  • Sorry I don't have more information on the Google ones. Here are comparable data-dump demos for [Chrome](http://webrtc.github.io/samples/src/content/peerconnection/constraints) and [Firefox](https://bug951496.bugzilla.mozilla.org/attachment.cgi?id=8436385). Both browsers also have internal pages, chrome://webrtc-internals and about:webrtc respectively, which may have more information than what's exposed to the JS. – jib Apr 24 '15 at 13:23
  • @jib do u know what's with the getStat polyfill? – zaxy78 Mar 22 '18 at 17:31