Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
264 views
in Technique[技术] by (71.8m points)

javascript - 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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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>

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...