下载APP
关闭
讲堂
算法训练营
极客商城
客户端下载
兑换中心
企业服务
免费资讯
渠道合作
推荐作者

16 | WebRTC中的数据统计原来这么强大(下)

2019-08-20 李超
从0打造音视频直播系统
进入课程

讲述:李超

时长12:40大小11.60M

上一篇文章中我向你介绍了 WebRTC 可以获得哪些统计信息,以及如何使用 RTCPeerConntction 对象的 getStats 方法获取想要的统计信息。

那本文我们在上一篇文章的基础之上,继续对 WebRTC 中的统计信息做进一步的讨论,了解它更为详细的内容。

再论 getStats

现在你已经非常清楚,通过 RTCPeerConnection 对象的 getStats 方法可以很轻松地获取到各种统计信息,比如发了多少包、收了多少包、丢了多少包,等等。但实际上对于收发包这块儿的统计还可以从其他方法获取到,即通过 RTCRtpSender 的 getStats 方法和 RTCRtpReceiver 的 getStats 方法也能获取收发包的统计信息

也就是说,除了 RTCPeerConnection 对象有 getStats 方法外,RTCRtpSender 和 RTCRtpReceiver 对象也有 getStats 方法,只不过它们只能获取到与传输相关的统计信息,而 RTCPeerConnection 还可以获取到其他更多的统计信息。

下面我们就来看一下它们三者之间的区别:

  • RTCPeerConnection 对象的 getStats 方法获取的是所有的统计信息,除了收发包的统计信息外,还有候选者、证书、编解码器等其他类型的统计信息。
  • RTCRtpSender 对象的 getStats 方法只统计与发送相关的统计信息。
  • RTCRtpReceiver 对象的 getStats 方法则只统计与接收相关的统计信息。

通过上面的描述,我想你已经非常清楚 RTCPeerConnection 中的 getStats 方法是获取到所有的统计信息,而 RTCRtpSender 和 RTCRtpReceiver 对象中的 getStats 方法则分别统计的是发包、收包的统计信息。所以 RTCPeerConnection 对象中的统计信息与 RTCRtpSender 和 RTCRtpReceiver 对象中的统计信息是整体与局部的关系。

下面咱们通过一段示例代码来详细看看它们之间的不同:

...
var pc = new RTCPeerConnection(null);
...
pc.getStats()
.then( reports => { // 得到相关的报告
reports.forEach( report => { // 遍历每个报告
console.log(report);
});
}).catch( err=>{
console.error(err);
});
// 从 PC 上获得 sender 对象
var sender = pc.getSenders()[0];
...
// 调用 sender 的 getStats 方法
sender.getStats()
.then(reports => { // 得到相关的报告
reports.forEach(report =>{ // 遍历每个报告
if(report.type === 'outbound-rtp'){ // 如果是 rtp 输出流
....
}
}
);
...
复制代码

在上面的代码中生成了两段统计信息,一段是通过 RTCPeerConnection 对象的 getStats 方法获取到的,其结果如下:

另一段是通过 RTCRtpSender 对象的 getStats 方法获取到的,其结果如下:

通过对上面两幅图的对比你可以发现,RTCPeerConnection 对象的 getStats 方法获取到的统计信息明显要比 RTCRtpSender 对象的 getStats 方法获取到的信息多得多。这也证明了我们上面的结论,即 RTCPeerConnection 对象的 getStas 方法获取到的信息与 RTCRtpSender 对象的 getStats 方法获取的信息之间是整体与局部的关系。

RTCStatsReport

我们通过 getStats API 可以获取到 WebRTC 各个层面的统计信息,它的返回值的类型是 RTCStatsReport。

RTCStatsReport 的结构如下:

interface RTCStatsReport {
readonly maplike<DOMString, object>;
};
复制代码

即 RTCStatsReport 中有一个 Map,Map 中的 key 是一个字符串,object 是 RTCStats 的继承类。

RTCStats 作为基类,它包括以下三个字段。

  • id:对象的唯一标识,是一个字符串。
  • timestamp:时间戳,用来标识该条 Report 是什么时间产生的。
  • type:类型,是 RTCStatsType 类型,它是各种类型 Report 的基类。

而继承自 RTCStats 的子类就特别多了,下面我挑选其中的一些子类向你做下介绍。

第一种,编解码器相关的统计信息,即 RTCCodecStats。其类型定义如下:

dictionary RTCCodecStats : RTCStats {
unsigned long payloadType; // 数据负载类型
RTCCodecType codecType; // 编解码类型
DOMString transportId; // 传输 ID
DOMString mimeType;
unsigned long clockRate; // 采样时钟频率
unsigned long channels; // 声道数,主要用于音频
DOMString sdpFmtpLine;
DOMString implementation;
};
复制代码

通过 RTCCodecStats 类型的统计信息,你就可以知道现在直播过程中都支持哪些类型的编解码器,如 AAC、OPUS、H264、VP8/VP9 等等。

第二种,输入 RTP 流相关的统计信息,即 RTCInboundRtpStreamStats。其类型定义如下:

dictionary RTCInboundRtpStreamStats : RTCReceivedRtpStreamStats {
...
unsigned long frameWidth; // 帧宽度
unsigned long frameHeight; // 帧高度
double framesPerSecond;// 每秒帧数
...
unsigned long long bytesReceived; // 接收到的字节数
....
unsigned long packetsDuplicated; // 重复的包数
...
unsigned long nackCount; // 丢包数
....
double jitterBufferDelay; // 缓冲区延迟
....
unsigned long framesReceived; // 接收的帧数
unsigned long framesDropped; // 丢掉的帧数
...
};
复制代码

通过 RTCInboundRtpStreamStats 类型的统计信息,你就可以从中取出接收到字节数、包数、丢包数等信息了。

第三种,输出 RTP 流相关的统计信息,即 RTCOutboundRtpStreamStats。其类型定义如下:

dictionary RTCOutboundRtpStreamStats : RTCSentRtpStreamStats {
...
unsigned long long retransmittedPacketsSent; // 重传包数
unsigned long long retransmittedBytesSent; // 重传字节数
double targetBitrate; // 目标码率
...
.
unsigned long frameWidth; // 帧的宽度
unsigned long frameHeight; // 帧的高度
double framesPerSecond; // 每秒帧数
unsigned long framesSent; // 发送的总帧数
...
unsigned long nackCount; // 丢包数
....
};
复制代码

通过 RTCOutboundRtpStreamStats 类型的统计信息,你就可以从中得到目标码率、每秒发送的帧数、发送的总帧数等内容了。

在 WebRTC 1.0 规范中,一共定义了 17 种 RTCStats 类型的子类,这里我们就不一一进行说明了。关于这 17 种子类型,你可以到文末的参考中去查看。实际上,这个表格在上一篇文章中我已经向你做过介绍了,这里再重新温习一下。

若你对具体细节很感兴趣的话,可以通过《WebRTC1.0 规范》去查看每个 RTCStats 的详细定义,相关链接在这里

RTCP 交换统计信息

上一篇文章中,我给你留了一道思考题,不知你是否已经找到答案了?实际上在 WebRTC 中,上面介绍的输入 / 输出 RTP 流报告中的统计数据都是通过 RTCP 协议中的 SR、RR 消息计算而来的。

关于 RTCP 以及 RTCP 中的 SR、 RR 等相关协议内容记不清的同学可以再重新回顾一下《 06 | WebRTC 中的 RTP 及 RTCP 详解》一文的内容。

在 RTCP 协议中,SR 是发送方发的,记录的是 RTP 流从发送到现在一共发了多少包、发送了多少字节数据,以及丢包率是多少。RR 是接收方发的,记录的是 RTP 流从接收到现在一共收了多少包、多少字节的数据等。

通过 SR、RR 的不断交换,在通讯的双方就很容易计算出每秒钟的传输速率、丢包率等统计信息了。

在使用 RTCP 交换信息时有一个主要原则,就是 RTCP 信息包在整个数据流的传输中占带宽的百分比不应超过 5%。也就是说你的媒体包发送得越多,RTCP 信息包发送得也就越多。你的媒体包发得少,RTCP 包也会相应减少,它们是一个联动关系。

绘制图形

通过 getStats 方法我们现在可以获取到各种类型的统计数据了,而且在上面的 RTCP 交换统计信息中,我们也知道了 WebRTC 底层是如何获取到传输相关的统计数据的了,那么接下来我们再来看一下如何利用 RTCStatsReport 中的信息来绘制出各种分析图形,从而使监控的数据更加直观地展示出来。

在本文的例子中,我们以绘制每秒钟发送的比特率和每秒钟发送的包数为例,向你展示如何将 RTCStats 信息转化为图形。

要将 Report 转化为图形大体上分为以下几个步骤:

  • 引入第三方库 graph.js;
  • 启动一个定时器,每秒钟绘制一次图形;
  • 在定时器的回调函数中,读取 RTCStats 统计信息,转化为可量化参数,并将其传给 graph.js 进行绘制。

了解了上面的步骤后,下来我们就来实操一下吧!

第三方库 graph.js 是由 WebRTC 项目组开发的,是专门用于绘制各种图形的,它底层是通过 Canvas 来实现的。这个库非常短小,只有 600 多行代码,使用起来也非常方便,在下面的代码中会对它的使用做详细的介绍。

另外,该库的代码链接我已经放到了文章的末尾,供你参考。

1. 引入第三方库

在 JavaScript 中引入第三方库也非常简单,只要使用 <script> 就可以将第三方库引入进来了。具体代码如下:

<html>
...
<body>
...
<script src="js/client.js"></script>
// 引入第三方库 graph.js
<script src="js/third_party/graph.js"></script>
...
</body>
</html>
复制代码

2. client.js 代码的实现

client.js 是绘制图形的核心代码,具体代码如下所示:

...
var pc = null;
// 定义绘制比特率图形相关的变量
var bitrateGraph;
var bitrateSeries;
// 定义绘制发送包图形相关的变理
var packetGraph;
var packetSeries;
...
pc = new RTCPeerConnection(null);
...
//bitrateSeries 用于绘制点
bitrateSeries = new TimelineDataSeries();
//bitrateGraph 用于将 bitrateSeries 绘制的点展示出来
bitrateGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas');
bitrateGraph.updateEndDate(); // 绘制时间轴
// 与上面一样,只不是用于绘制包相关的图
packetSeries = new TimelineDataSeries();
packetGraph = new TimelineGraphView('packetGraph', 'packetCanvas');
packetGraph.updateEndDate();
...
// 每秒钟获取一次 Report,并更新图形
window.setInterval(() => {
if (!pc) { // 如果 pc 没有创建直接返回
return;
}
// 从 pc 中获取发送者对象
const sender = pc.getSenders()[0];
if (!sender) {
return;
}
sender.getStats().then(res => { // 获取到所有的 Report
res.forEach(report => { // 遍历每个 Report
let bytes;
let packets;
// 我们只对 outbound-rtp 型的 Report 做处理
if (report.type === 'outbound-rtp') {
if (report.isRemote) { // 只对本地的做处理
return;
}
const now = report.timestamp;
bytes = report.bytesSent; // 获取到发送的字节
packets = report.packetsSent; // 获取到发送的包数
// 因为计算的是每秒与上一秒的数据的对比,所以这里要做个判断
// 如果是第一次就不进行绘制
if (lastResult && lastResult.has(report.id)) {
// 计算这一秒与上一秒之间发送数据的差值
var mybytes= (bytes - lastResult.get(report.id).bytesSent);
// 计算走过的时间,因为定时器是秒级的,而时间戳是豪秒级的
var mytime = (now - lastResult.get(report.id).timestamp);
const bitrate = 8 * mybytes / mytime * 1000; // 将数据转成比特位
// 绘制点
bitrateSeries.addPoint(now, bitrate);
// 将会制的数据显示出来
bitrateGraph.setDataSeries([bitrateSeries]);
bitrateGraph.updateEndDate();// 更新时间
// 下面是与包相关的绘制
packetSeries.addPoint(now, packets -
lastResult.get(report.id).packetsSent);
packetGraph.setDataSeries([packetSeries]);
packetGraph.updateEndDate();
}
}
});
// 记录上一次的报告
lastResult = res;
});
}, 1000); // 每秒钟触发一次
...
复制代码

在该代码中,最重要的是 32~89 行的代码,因为这其中实现了一个定时器——每秒钟执行一次。每次定时器被触发时,都会调用 sender 的 getStats 方法获取与传输相关的统计信息。

然后对获取到的 RTCStats 类型做判断,只取 RTCStats 类型为 outbound-rtp 的统计信息。最后将本次统计信息的数据与上一次信息的数据做差值,从而得到它们之间的增量,并将增量绘制出来。

3. 最终的结果

当运行上面的代码时,会绘制出下面的结果,这样看起来就一目了然了。通过这张图你可以看到,当时发送端的码率为 1.5Mbps 的带宽,每秒差不多发送小 200 个数据包。

小结

在本文中,我首先向你介绍了除了可以通过 RTCPeerConnection 对象的 getStats 方法获取到各种统计信息之外,还可以通过 RTCRtpSender 或 RTCRtpReceiver 的 getStats 方法获得与传输相关的统计信息。WebRTC 对这些统计信息做了非常细致的分类,按类型可细分为 17 种,关于这 17 种类型你可以查看文末参考中的表格。

在文中我还向你重点介绍了编解码器、输入 RTP 流以及输出 RTP 流相关的统计信息。

除此之外,在文中我还向你介绍了网络传输相关的统计信息是如何获得的,即通过 RTCP 协议中的 SR 和 RR 消息进行交换而来的。实际上,对于 RTCP 的知识我在前面《06 | WebRTC 中的 RTP 及 RTCP 详解》一文中已经向你讲解过了,而本文所讲的内容则是 RTCP 协议的具体应用。

最后,我们通过使用第三方库 graph.js 与 getStats 方法结合,就可以将统计信息以图形的方式绘制出来,使你可以清晰地看出这些报告真正表达的意思。

思考时间

今天你要思考的问题是:当使用 RTCP 交换 SR/RR 信息时,如果 SR/RR 包丢失了,会不会影响数据的准确性呢?为什么呢?

欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

参考

例子代码地址,戳这里
第三方库地址,戳这里

© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
15 | WebRTC中的数据统计原来这么强大(上)
下一篇
17 | 如何使用Canvas绘制统计图表(上)?
 写留言

精选留言(3)

  • 2019-08-22
    老师还是昨天问题,在Frefix,IE浏览器上是可以播放的,只是Google Chrome上播放一点就报错,提示视频问题或浏览器某些特征不支持,如果真的是视频问题,这种性象,暂时无法理解!

    作者回复: 你用video 标签播的吗?如果是 video标签各浏览器的实现不一样,在chrome下,浏览器对vp8/vp9支技的更好。你可能通过第三方库来播MP4文件

  • 2019-08-21
    老师,这个问题你遇到吗,能否帮帮我
    Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'F:/mp4/convert/041.mp4': Metadata: major_brand : isom minor_version : 512 compatible_brands: isomiso2avc1mp41 encoder : Lavf57.76.100 Duration: 00:00:21.04, start: 0.033008, bitrate: 3656 kb/s Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p(tv, bt709), 1920x1080, 3653 kb/s, 29.99 fps, 50 tbr, 15360 tbn, 60 tbc (default) Metadata: handler_name : VideoHandler[h264 @ 00000000028a4d80] Invalid NAL unit 8, skipping. Last message repeated 3 times[h264 @ 00000000028a4d80] concealing 7569 DC, 7569 AC, 7569 MV errors in P frame[h264 @ 00000000028a4d80] illegal short term buffer state detected[h264 @ 00000000028a4d80] mmco: unref short failure
    展开

    作者回复: 提示上有这个信息“Invalid NAL unit 8” 说明你的视频数据有问题

  • 许童童
    2019-08-20
    思考题:
    不会影响准确性,因为每一次传输都是全量的,丢失只会丢失这一次的值,在下一次又会全量带过来。
    展开

    作者回复: 赞!