问题 有人可以全面解释WebRTC统计API吗?


我正在为视频通信的研究生课程完成一个WebRTC项目,它本质上是一个视频会议聊天室。连接到服务器的每个人都会添加到会议中。

我需要使用WebRTC中的stats API来显示每个RTCPeerConnection的一些相关性能统计信息(每秒丢失的数据包,抖动,重传等)。这有助于观察性能成本,因为更多对等体被添加到对话中。

然而,API似乎还没有完全充实。它显然经历了一些刷新,并不完全符合我见过的一些W3C规范(虽然它可能已经过时或者我只是不明白阅读规范的细微差别,也不会让我感到惊讶)。

我调用API是 与此类似,但解释数据并不简单。例如,当循环遍历所有项目时 RTCStatsReport::results(),其中许多都有重复的名称和令人困惑的值。我似乎无法找到任何有关其含义的信息。如果有人能帮助我理解一些重要的东西或者指出我失去的黄金之城(例如适当的文件),我将不胜感激。


6396
2018-04-22 14:02


起源

我感觉到你的痛苦。我也没有找到关于该主题的任何文档。我在前一段时间围绕get Stats API编写了一个简单的Wrapper,也许它可以帮到你一点点, 我把它放在GitHub上。 - wpp
嗨,主要问题是缺乏有关如何解释数据的文档。查看您的实现有助于将这些数据组合在一起并理解它。我一定会把它拉进我的项目中,我感谢你。 README文件非常稀疏,但是我转储数据时的组织应该足以理解所有数据的含义。再次感谢,这是没有文档的下一个最好的事情。 - Joey Carson
你也可以看看 github.com/webrtc/apprtc/tree/master/src/web_app/js 特别是文件“infobox.js”和“stats.js”。我没有机会更新/扩展README sry。 - wpp


答案:


您混淆的根源可能是Google Chrome的实施 getStats() 标准之前的日期尚未更新(您链接到的示例是特定于Chrome的,因此我假设您使用的是Chrome)。

如果你要尝试Firefox,你会发现它实现了 getStats() 至 标准 (但它不支持标准中的所有统计数据,并且整体统计数据比Chrome的旧API更少)。

由于您没有指定浏览器,我将描述标准,并使用Firefox来显示示例。你可能知道 getStats() 已经,但标准版本允许您过滤,比如说特定的MediaStreamTrack,或传入 null 获取与连接关联的所有数据:

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

还有一个更新的承诺版本。

数据将被返回 stats,一个大雪球对象,每个记录都有唯一的ID。每条记录都有 下列 基类:

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

哪里 id 是重复用于访问记录的属性名称。描述了派生类型 这里

您通常会枚举记录,直到找到 RTCStatsType 感兴趣的,例如 "inbound-rtp" 看起来像这样:

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;
};

有一个对应的 RTCOutboundRTPStreamStats

您还可以跟踪对其他记录的交叉引用。任何以。结尾的成员 Id 是一个外键,可以用来查找另一条记录。例如, mediaTrackId 链接到 RTCMediaStreamTrackStats 对于该RTP数据所属的轨道。

一个特别松散的情况是RTCP数据,它存储在与上面相同的字典中,这意味着你必须检查 isRemote == false 知道你正在查看RTP数据而不是RTCP数据。使用 remoteId找到另一个(请注意,这是最近的名称更改,因此Firefox仍然使用较旧的 remoteId 这里)。出站RTP的关联RTCP统计信息存储在入站字典中,反之亦然(有意义)。

这里的 一个例子 在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>

要查看支持的内容,请执行此操作 stats.forEach(stat => console.log(JSON.stringify(stat))) 倾倒一切。很难读,但它就在那里。

我相信很快就会为adapter.js计划一个polyfill来缩小差距,直到Chrome更新它的实现。

更新: 我已经更新了示例以使用新的maplike语法,并更改了类型名称以包含破折号,以符合最新规范。


14
2018-04-24 07:07



我不能感谢你花时间。是的我在OS X上使用chrome,这就是我对API已经看到一些刷新这一事实的意思。在查看Mozilla API文档时,getStats有一个列表,表明它需要一个MediaStreamTrack,但是没有相关的页面来讨论它。同样,Chrome的实施也明显不同。 W3C规范没有提供有关如何解释它的更多信息,但显然与Mozilla更加一致。我预计谷歌的实施将更新,很多消息来源都表示。 - Joey Carson
对不起,我没有关于Google的更多信息。以下是可比较的数据转储演示 铬 和 火狐。这两个浏览器还分别有内部页面,chrome:// webrtc-internals和about:webrtc,它们可能比JS公开的信息更多。 - jib
@jib你知道getStat polyfill有什么用吗? - zaxy78