WebRTC getStats详解 - 从标准、调用到实现

前言

getStats是WebRTC一个非常重要的API,用来向开发者和用户导出WebRTC运行时状态信息,包括网络数据接收和发送状态、P2P客户端媒体数据采集和渲染状态等[1]。这些信息对于监控WebRTC运行状态、排除程序错误等非常重要。<br />
本文首先描述W3C定义的getStats标准,然后展示如何在JS层调用getStats,最后深入分析WebRTC源代码中getStats的实现。全文从标准到实现,全方位透彻展示getStats的细节。<br />

一 getStats标准

getStats的标准由W3C定义,其接口很简单,但是却返回丰富的WebRTC运行时信息。其返回信息的主要内容如下[2]:<br />

  1. 发送端采集统计:对应于媒体数据的产生,包括帧率,帧大小,媒体数据源的时钟频率,编解码器名称,等等。<br />
  2. 发送端RTP统计:对应于媒体数据的发送,包括发送数据包数,发送字节数,往返时间RTT,等等。<br />
  3. 接收端RTP统计:对应于媒体数据的接收,包括接收数据包数,接收字节数,丢弃数据包数,丢失数据包数,网络抖动jitter,等等。<br />
  4. 接收端渲染统计:对应于媒体数据的渲染,包括丢弃帧数,丢失帧数,渲染帧数,渲染延迟,等等。<br />

另外还有一些杂项统计,如DataChannel度量,网络接口度量,证书统计等等。在众多信息中,有一些反映WebRTC运行状态的核心度量,包括往返时间RTT,丢包率和接收端延迟等,分别表述如下:<br />

  • 往返时间RTT:表示数据在网络上传输所用的时间,一般通过RTCP 的SR/RR数据包中的相关域进行计算。该度量直接反映网络状况的好坏。<br />
  • 丢包率影响接收端音视频质量,在严重的情况下可能导致声音跳变或者视频马赛克,从侧面反映网络状况的好坏。<br />
  • 音视频数据到达接收端之后,要经历收包、解码、渲染等过程,该过程会带来延迟。接收端延迟是数据从采集到渲染单向延迟的重要组成部分。<br />

通过以上分析可知,getStats的返回信息包含WebRTC数据管线的各个阶段的统计信息,从数据采集、编码到发送,再到数据接收、解码和渲染。这为监控WebRTC应用的运行状态提供第一手数据。<br />

二 使用JS调用getStats

getStats的JS API很简单, W3C规定getStats的JS API函数PTCPeerConnection.getStats需要三个参数:一个可为空的MediaStreamTrack对象,一个调用成功时的回调函数和一个调用失败时的回调函数。成功回调函数的参数为getStats得到的RTCStatsReport,主要工作就发生在解析RTCStatsReport上,拿到我们感兴趣的参数,进而分析应用的运行状态。<br />
下面我们选取W3C标准上给出的例子作简单讲解[1]。假设当前会话的通话质量很差,我们想知道是不是由于丢包率过大引起的。因此,我们可以通过getStats返回结果的outbound-rtp中的丢包数和收包数计算丢包率,然后进行判断。具体代码实现如下:<br />

var baselineReport, currentReport;
var selector = pc.getRemoteStreams()[0].getAudioTracks()[0];
pc.getStats(selector, function (report) {
    baselineReport = report;
}, logError);

setTimeout(function () {
    pc.getStats(selector, function (report) {
        currentReport = report;
        processStats();
    }, logError);
}, aBit);

function processStats() {
    for (var i in currentReport) {
        var now = currentReport[i];
        if (now.type != "outbund-rtp")
            continue;

        base = baselineReport[now.id];
        if (base) {
            remoteNow = currentReport[now.associateStatsId];
            remoteBase = baselineReport[base.associateStatsId];
            var packetsSent = now.packetsSent - base.packetsSent;
            var packetsReceived = remoteNow.packetsReceived – 
                   remoteBase.packetsReceived;

            // if fractionLost is > 0.3, we have probably found the culprit
            var fractionLost = (packetsSent - packetsReceived) / packetsSent;
        }
    }
}
function logError(error) {
log(error.name + ": " + error.message);
}

通过上述例子,我们可以体会到从JS层调用getStats分析应用运行状态的基本流程。值得注意的是,Chrome和Firefox两款浏览器在调用方面有稍微差别,具体请参考文档[3]。<br />

三 getStats在WebRTC内部的实现

JS层的getStats调用如何传递到到WebRTC内部的实现函数,涉及到浏览器的内部工作原理,具体到Chrome浏览器来讲,是由WebKit,V8,Content,libjingle等模块一起协同工作实现。本节我们不讨论这里面的细节,我们只关注getStats在WebRTC内部的实现。<br />

WebRTC模块对外提供两个重要对象:PeerConnectionFactory和PeerConnection,前者负责一系列重要对象的创建,如MediaStream,MediaSource,MediaTrack等等,后者则负责P2P连接的建立和维护,包括CreateOffer/Answer,AddStream等操作。监控P2P连接运行状态GetStats函数,自然在PeerConnection对象中实现,而该对象把任务委托给成员变量StatsCollector对象的UpdateStats函数来实现:<br />

void StatsCollector::UpdateStats(PeerConnectionInterface::StatsOutputLevel level) {
  RTC_DCHECK(pc_->session()->signaling_thread()->IsCurrent());  // 由signal线程调用;
  double time_now = GetTimeNow();
  const double kMinGatherStatsPeriod = 50;
  if (stats_gathering_started_ != 0 &&
      stats_gathering_started_ + kMinGatherStatsPeriod > time_now) {
    return;  // 调用间隔不低于50ms;
  }

  stats_gathering_started_ = time_now;
  if (pc_->session()) {
    ExtractSessionInfo();      // 收集传输信息;
    ExtractVoiceInfo();       // 收集VoiceChannel信息;
    ExtractVideoInfo(level);   // 收集VideoChannel信息;
    ExtractSenderInfo();      // 收集PeerConnection的sender信息;
    ExtractDataInfo();        // 收集DataChannel信息;
    UpdateTrackReports();    // 更新Track报告;
  }
}

由该函数我们可以看到,信息的收集是分模块进行的,其中最重要的是四个模块的信息:Transport,VoiceChannel,VideoChannel,DataChannel。顾名思义,Transport是和网络相关的统计信息,而其余三个是和各自MediaChannel相关的统计信息。<br />

Extract系列函数从相应模块收集到信息后,执行后处理操作,把不同类型的信息重新组织为类型相同的StatsReport对象,存储到StatsCollector的列表中。StatsReport对象结构基本定义如下:<br />

struct StatsReport {
  const Id id_;       // 包括类型,唯一标示符等信息;
  double timestamp_;  // 本次信息收集的开始时间;
  Values values_;     // 信息集合,可存储int, int64, string, bool, double等类型
};

下面以ExtractVideoInfo为例分析信息收集过程:<br />

void StatsCollector::ExtractVideoInfo(PeerConnectionInterface::StatsOutputLevel level) {
  cricket::VideoMediaInfo video_info;
  // 从video channel收集信息,包括发送端,接收端和带宽估计信息;
  if (!pc_->session()->video_channel()->GetStats(&video_info)) {
    return;
  }
  // 收集到的信息归一化为StatsReport对象;
  ExtractStatsFromList(video_info.receivers, transport_id, this,
      StatsReport::kReceive);
  ExtractStatsFromList(video_info.senders, transport_id, this,
      StatsReport::kSend);
    ExtractStats(video_info.bw_estimations[0], stats_gathering_started_, level, report);
}

从videochannel收集到的数据来自三个模块:VideoSendStream,VideoReceiveStream 和Call,这三个模块分别从自己的信息统计对象中获得统计数据,最后汇总为VideoMediaInfo对象,由ExtractStatsFromXX系列函数归一化为StatsReport对象。<br />

以上分析的即为getStats函数的内部实现细节。需要注意的是,getStats只负责拉取统计数据,而统计数据本身则由WebRTC内部各个模块周期性更新,这个过程是异步的。例如,传输层的RTT是由网络线程收到数据包后实时更新,而带宽估计信息则是在受到RTCP报文后解析计算得到。下面以VideoReceiveStream统计信息的更新过程为例,深入分析这部分是如何协同工作的:<br />

VideoReceiveStream的数据更新和拉取.png

在Video接收端,network/decoder/render三个线程在各自工作完成后,都会更新相应的统计数据到timing对象中。而module process线程则周期性更新Stats proxy对象,该对象从timing对象中拉取数据,保存在自己的stats成员变量中。最后,getstats线程调用流程到达stats proxy对象,获取stats数据而返。工作线程、更新线程和拉取线程共同协同工作完成统计数据的产生、更新和拉取。<br />

四 总结

本文从标准、使用和实现三个方面全方位分析了WebRTC的getStats API,这对WebRTC应用的运行时监控和状态分析排错具有重要意义,我们从另一角度对WebRTC有了更深入的理解。<br />
<br />

参考文献

[1] Identifiers for WebRTC's Statistics API:
   https://www.w3.org/TR/webrtc-stats/
[2] Basics of WebRTC getStats() API:
   https://www.callstats.io/2015/07/06/basics-webrtc-getstats-api/
[3] RTCPeerConnection.getStats: Chrome VS Firefox:
http://blog.telenor.io/webrtc/2015/06/11/getstats-chrome-vs-firefox.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容