有人能够全面解释WebRTC统计API吗?

16

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

我需要使用WebRTC的stats API来显示每个RTCPeerConnection的相关性能统计数据(如每秒丢包数、抖动、重传等),这有助于观察随着更多的同行加入对性能的影响。

然而,API似乎尚未完全成熟。它显然经历了一些刷新,并不完全符合我看到的一些W3C规范(虽然可能已经过时或者我只是不理解阅读规范的微妙之处,两个都不会让我感到惊讶)。

我的API调用类似于这个,但解释数据并不容易。例如,在循环遍历RTCStatsReport::results()中的所有项目时,其中许多具有重复的名称和混淆的值。我似乎找不到任何关于它们含义的信息。如果有人可以帮助我理解一些重要的信息或将我指向黄金失落之城(例如适当的文档),我将不胜感激。


2
我理解你的痛苦。我也没有找到关于那个主题的文档。我一段时间以前写了一个简单的包装器,围绕着获取统计数据的API,或许它能对你有所帮助。我把它放在了GitHub上 - wpp
你好,主要问题是缺乏关于如何解释数据的文档。查看您的实现有助于将这些数据拼凑在一起并理解其含义。我一定会将其引入我的项目中,并感谢您的贡献。README文件相当简洁,但是当我转储数据时,组织结构应该足以理解所有数据的含义。再次感谢,这是没有文档的下一个最佳选择。 - Joey Carson
你也可以查看 https://github.com/webrtc/apprtc/tree/master/src/web_app/js,特别是 "infobox.js" 和 "stats.js" 这两个文件。抱歉我还没有时间更新/扩展 README。 - wpp
对于未来的任何人,此页面显示逐个API的浏览器兼容性:https://webrtc-stats.callstats.io/verify - AndrewJC
2个回答

18
您困惑的原因可能是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) { ... });

还有一个更新的 promise 版本。

返回的数据在 stats 中,它是一个大的 snowball 对象,并且每个记录都有唯一的 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语法,并更改了类型名称以包括破折号,以符合最新规范。


1
我无法感谢你花时间帮忙。是的,我在使用OS X上的Chrome,这就是我所指的API已经进行了一些更新的原因。当查看Mozilla API文档时,getStats有一个列表,说明它需要一个MediaStreamTrack,但是没有相关页面来讨论它。同样,Chrome的实现显然是不同的。W3C规范并没有提供太多关于如何解释它的信息,但明显更与Mozilla相符。我本以为Google的实现会更加更新,很多来源都这样建议。 - Joey Carson
抱歉,我没有更多关于谷歌的信息。这里有一些类似的数据转储演示,适用于ChromeFirefox。这两个浏览器还有内部页面,分别是chrome://webrtc-internals和about:webrtc,可能比JS暴露的信息更多。 - jib
@jib 你知道 getStat polyfill 是怎么回事吗? - zaxy78

1
实际上,有几个统计数据块涵盖了入站/出站视频/音频流和常见连接参数。我们将它们整合到一个易于使用的客户端库webrtc-issue-detector中,用于解析WebRTC统计数据并收集RTCPeerConnection的性能统计信息。
有许多参数,您应该针对不同的场景使用不同的参数集。 入站RTP流
- 每N秒收集所有入站流的统计数据 - 将当前的jitterjitterBufferDelayjitterBufferEmittedCountcurrentRoundTripTimepacketsLost值与之前的值进行比较 - 检查是否存在高平均往返时间(RTT)、抖动缓冲延迟(jitterBufferDelay)、抖动值的迹象 发送方的视频质量限制
每隔N秒收集所有出站视频流的统计数据 将当前的qualityLimitationReason值与之前的值进行比较 检查自上次检查以来是否发生了任何限制 - cpu或带宽 还有很多其他案例

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接