Joey Carson Joey Carson - 4 months ago 23
Javascript Question

Can someone comprehensively explain the WebRTC stats API?

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.

jib jib
Answer

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. "inboundrtp" which looks like this:

dictionary RTCRTPStreamStats : RTCStats {
         DOMString     ssrc;
         DOMString     associateStatsId;
         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 associateStatsId 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 == "outboundrtp" && !stat.isRemote) {
          s += "<h4>Sender side</h4>" + dumpStats(stat);
        }
      });
      s2.forEach(stat => {
        if (stat.type == "inboundrtp" && !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, to conform with the latest spec.

Comments