先占个坑,后面有空慢慢填。。。 拥塞控制 BBR
前面文章介绍了抖动,今天我们来看下chrome://webrtc-internals/页面中的一个重要统计参数:往返时延(RTT)。 什么是RTT 往返时延(round-trip time,RTT) 是网络请求从起点到目的地然后再回到起点所花费的时长(不包括接收端的处理时间)。RTT是分析网络性能的一个重要指标,我们通常使用RTT来诊断网络连接的速度,可靠性以及拥塞程度。 ping命令是最常见的一种估计往返时延的方法。下面是ping Google的示例,底部显示了rtt的不同表示:最小,平均,最大,平均偏差。 [crayon-69d0082e1079f845038441/] RTT计算方式 WebRTC中目前有两种方式计算RTT: 基于媒体流发送端的计算(默认开启)。通过Sender Report(SR)与Receiver Report(RR)携带的信息。 基于媒体流接收端的计算。通过RTCP Extended ReportsRTCP(XR)携带的信息。 这两种方式计算RTT的原理都一样。至于为什么需要接收端计算方式,这是因为在一些场景,媒体流传输是单向的,两个端点间一个只发媒体数据,一个只接收,例如客户端与SFU服务器间。假设客户端与SFU服务器上行推流场景,客户端推流,服务端收流,这种场景下作为接收端的服务端并不会发送SR,导致无法计算RTT。于是就有了通过RTCP XR在接收端计算这种方式。 媒体流发送端RTT计算 WebRTC中媒体流发送端RTT的计算是根据SR以及RR中携带的时间信息。这里我们回顾下Sender Report与Receiver Report RTCP两种报文。 Sender Report [crayon-69d0082e107ac637867310/] NTP timestamp:64bits。记录着发送该SR的NTP时间戳。 Receiver Report [crayon-69d0082e107b0263231601/] last SR timestamp (LSR): 32 bits。64位NTP时间戳中间的32bit(NTP时间戳指绝对时间,相对1900年1月1日00:00:00经历的时间,单位为秒。完整NTP时间戳用64bits表示,左半32bits表示整数,右半32bits表示小数,一般为了紧凑,取中间32bits表示即可,这时整数与小数分别16bits表示)。记录着上次源SSRC_n发送SR的NTP时间,从收到的SR记录的NTP时间戳获取。如果没有收到SR,值为0。 delay since last SR (DLSR): 32 bits。以1/65536(2^16)秒为单位。记录着上次接收到源SSRC_n发送的SR到当前发送RR的间隔时间。如果没有收到SR,值为0。 计算原理 前面简单介绍了RR中记录的时间信息,下面我们看下如何根据这些时间信息计算RTT。 上图中\(T_{0}\)时刻媒体流发送端发送SR,SR中记录着send_time_ntp。媒体流接收端\(t_{0}\)时刻收到SR,\(t_{1}\)时刻回复RR,RR中记录着:1)从收到的SR中获取的send_time_ntp信息;2)接收端回复RR与收到SR的间隔时间delay_since_last_sr。发送端在\(T_{1}\)时刻收到回复的RR。可知: 代码导读 WebRTC中,发送端首先需要调用ModuleRtpRtcpImpl::OnSendingRtpFrame,这样才能触发发送SR。 接着收到反馈的RR后才能进行RTT计算。WebRTC中发送端RTT计算代码位于RTCPReceiver::HandleReportBlock。 主要代码流程如下: [crayon-69d0082e107b4139161194/] RTCPReceiver::HandleReportBlock中RTT计算代码如下: [crayon-69d0082e107b7174949288/] 结合前面的公式,上面代码应该很好理解。最后得到一个ms表示的RTT,chrome://webrtc-internals/中RTT的单位是秒。 媒体流接收端RTT计算 RTCP XR报文 WebRTC中媒体流接收端RTT的计算是根据XR中携带的时间信息。这里我们看下XR报文格式: [crayon-69d0082e107ba001591261/] 时间信息存放在report blocks中。report blocks格式如下: [crayon-69d0082e107be909547258/] 接收端RTT计算主要用到了Receiver Reference Time Report Block与DLRR Report Block这两种report blocks。 Receiver Reference Time Report Block(RRTR) 由媒体流接收端发送。报文格式如下: [crayon-69d0082e107c1408832085/] NTP timestamp:64bits。记录着发送该Receiver Reference Time Report Block的NTP时间戳。 DLRR Report Block 由媒体流发送端发送。报文格式如下: [crayon-69d0082e107c4334485566/] last RR timestamp (LRR): 32 bits。记录着上次源SSRC_n发送Receiver Reference Time Report Block的NTP时间,从收到的Receiver Reference Time Report Block记录的NTP时间戳获取。如果没有收到,值为0。 delay since last RR (DLRR): 32 bits。以1/65536(2^16)秒为单位。记录着上次接收到源SSRC_n发送的Receiver Reference Time Report Block到当前发送DLRR Report Block的间隔时间。如果没有收到Receiver Reference Time Report Block,值为0。 计算原理 类似发送端RTT计算,只不过SR变为了Receiver Reference Time Report Block,由媒体流接收端发送。RR变为了DLRR Report Block,由媒体流发送端发送,最后在接收端计算。具体原理这里就不再啰嗦一次了。 代码导读 需要配置接收端ModuleRtpRtcpImpl中RtpRtcpInterface::Configuration的non_sender_rtt_measurement开启XR。 这里以视频为例,配置开启方式如下: [crayon-69d0082e107c7981426771/] 也就是检查SDP中某编码器"a=rtcp-fb"是否含有"rrtr"。例如: [crayon-69d0082e107ca947567283/] 最终这个receiver_reference_time_report配置会传给RtpRtcpInterface::Configuration中的non_sender_rtt_measurement配置项,最后传到RTCPSender中。 媒体流接收端首先发送RRTR,然后收到媒体流发送端反馈的DLRR Report Block进行RTT计算。WebRTC中接收端RTT计算代码位于RTCPReceiver::HandleXrDlrrReportBlock。 主要代码流程如下: [crayon-69d0082e107cd633285667/] RTCPReceiver::HandleXrDlrrReportBlock中RTT计算代码如下: [crayon-69d0082e107d0987618850/] 代码类似前面发送端RTT计算,就不分析了。 注意事项 WebRTC中RTCP XR总是附在SR或者RR后组成Compound RTCP包发送。 总结 本文简单介绍了RTT以及WebRTC中两种RTT的计算,RTT作为网络性能分析的一个重要指标,在WebRTC中扮演一个很重要的角色,至于具体用途,后面有空结合其它再分析。 参考 [1] RFC3550.https://tools.ietf.org/html/rfc3550. [2] RFC3611.https://tools.ietf.org/html/rfc3611.
当我们调试WebRTC web客户端时,经常会打开chrome://webrtc-internals/这个页面,在这里我们可以看到音视频流的各种统计。今天我们就来看下其中的一个统计参数:抖动(jitter)。 什么是抖动 在网络传输中,每个包从发送端到接收端的时延都是不相同的,而jitter就是用来衡量这种不同。一般在发送端,数据包发送时间间隔是相同的,也就是均匀发送数据,但是传输过程中由于各种情况,例如拥塞,丢包,网络错误等,接收端收到的数据包间隔就会不一样,可能一会大,一会小,导致时延发生变化,这个时延的变化程度就是所谓的抖动,接收端如果不做任何处理,就会影响用户体验。例如对于视频来说,会导致帧率变化,视频播放就不平滑,有卡顿感。如下图片是抖动的一个形象描述。 WebRTC内部通过NetEQ与JitterBuffer进行音视频的抖动处理,消除抖动造成的影响,这些会在我的后续文章介绍。 抖动计算 根据RFC3550,假设RTP包i,RTP报头记录的时间戳为Si(单位为采样率),到达接收端的时间为Ri(单位同样为采样率),对于包i,j,我们定义一个变量\(D\),描述两个包时间的差异,由接收时间差减去发送时间差: 抖动\(J\)按如下公式定义: 上述公式中i-1指的是前一个收到的包,不是按RTP序列号计数的。增益参数1/16是为了消除噪声影响,使抖动收敛在较合理范围内,避免突发数据的影响。 由公式可知,如果接受端收到的包间隔跟发送端发送间隔一样,那么抖动为0。 代码导读 WebRTC统计参数中的抖动计算代码位于StreamStatisticianImpl::UpdateJitter中,看下如何计算。 1)计算接收时间差,也就是\(Ri - R_{i-1}\): [crayon-69d0082e12884685696501/] 2)计算\(|D(i-1,i)|\),其中发送时间为RTP报头记录的时间戳: [crayon-69d0082e1288f721079420/] 3)由公式(2),使用jitter_diff表示\(|D(i-1,i)|-J(i-1)\): 所以相关代码如下: [crayon-69d0082e12893856351636/] 至此我们得到了jitter_diff_q4,单位为采样率,如果需要转换为毫秒时间单位,需要如下计算: 1)由于jitter_diff_q4放大了16倍,首先需要还原回去: [crayon-69d0082e12897529922120/] 2)转换为毫秒单位: [crayon-69d0082e1289a947951458/] 这样我们就得到了以毫秒表示的抖动统计参数,chrome://webrtc-internals/中抖动的单位是秒。 参考 [1] Jitter.https://en.wikipedia.org/wiki/Jitter. [2] RFC3550.https://tools.ietf.org/html/rfc3550.
最近更新了M89代码,看了下Release Notes,有几个需要关心的地方。 Plan B SDP语法后续处理计划 WebRTC 1.0已经正式成为W3C标准,推荐使用标准SDP格式:Unified Plan,主流浏览器基本都支持Unified Plan SDP。Plan B SDP后续将会不赞成使用,直到移除掉。官方后续时间计划如下: M89 (2021.02):在开发者控制台增加不赞成使用警告 M93 (2021.08): Plan B被移除掉, 但是增加了选项,可以延长移除的截止日期 M96 (2022.01): 不再允许延长截止日期,Plan B将彻底移除 rtp payload类型增加新范围 M89中RTP payload类型增加了新的值范围:35-65。 首先根据RFC3551,回顾下目前用于标准音视频编码器的RTP payload类型。 音频编码器的payload类型 [crayon-69d0082e13bb0865750873/] 视频编码器的payload类型 [crayon-69d0082e13bba623408825/] 原有动态RTP payload类型范围局限性 RFC 3551中定义的动态RTP payload类型范围[96,127]只能表示32种payload类型。由于Chrome支持的音视频编解码器越来越多,而且像DTMF,FEC,RTX机制,冗余编码,H264不同profile/打包模式等都需要占用一个RTP payload类型。原先的范围已经不够使用了。 这里我们看下某个默认生成的Offer SDP中的内容(有点长),a=rtpmap后面跟着就是某种RTP payload类型。 [crayon-69d0082e13bbf611719641/] 根据m-line,可知,音频用到的RTP payload类型有:111 103 104 9 0 8 106 105 13 110 112 113 126。视频用到的RTP payload类型有:96 97 98 99 100 101 102 121 127 120 125 107 108 109 124 119 123 118 114 115 116。 在动态RTP payload类型[96,127]范围内,用到了31个取值,如果稍微再复杂些,[96,127]确实不够用了。 新增RTP payload类型范围 RFC3551中,section-3中提到,当需要定义的动态payload类型超过32个时,推荐优先使用unassigned payload类型这个范围的值。 为了兼容旧版Chrome,所以M89中增加了新的RTP payload类型范围:35-65。由前面视频编码器的payload类型可知[35,65]位于[35,71]这个Unassigned payload类型范围内。 在webrtc_video_engine.cc中相关说明代码如下: [crayon-69d0082e13bc2702349179/] 总结 针对前面提到的两点重要更新,我觉得后续我们WebRTC服务器开发的得注意下,需要完全支持Unified Plan,同时需要能正确识别[35,65]这个范围表示的RTP payload类型。 参考 [1] WebRTC M89 Release Notes.https://groups.google.com/g/discuss-webrtc/c/Zrsn2hi8FV0/m/7Y4RLluHAQAJ. [2] RFC3551.https://tools.ietf.org/html/rfc3551. 本文已收录到大话WebRTC专栏,更多精彩请访问《大话WebRTC》。
系统要求 系统:Ubuntu 16.04及以上(本文Ubuntu 18.04) 磁盘空间:至少6.4 GB磁盘空间 安装工具 [crayon-69d0082e14dff906668641/] 安装depot tools [crayon-69d0082e14e08154022413/] 获取代码 [crayon-69d0082e14e0c815577835/] 安装依赖 [crayon-69d0082e14e0f349334543/] 生成Ninja工程文件 WebRTC默认使用Ninja作为编译系统,Ninja工程文件通过GN生成。 使用如下命令生成默认配置工程(Debug编译,工程文件位于out\Default目录下): [crayon-69d0082e14e12565799679/] 如果需要Release编译,通过如下命令生成工程文件: [crayon-69d0082e14e16678783173/] 编译 [crayon-69d0082e14e19960147507/] 最后在src/out/Default/obj可以看到生成的静态库文件:libwebrtc.a。 代码更新 后续如需要更新代码,按照如下步骤: [crayon-69d0082e14e1c417817831/] 然后参考前面步骤重新生成工程文件,编译即可。 参考 [1] WebRTC Native code Development.https://webrtc.github.io/webrtc-org/native-code/development/. 本文已收录到大话WebRTC专栏,更多精彩请访问《大话WebRTC》。
当我们上行推Simulcast流的时候,会发现Simulcast层数会时不时变化,本来是大小流(大小两个分辨率)两层,但是变为只有一层小流了,后面又恢复为两层了,如果我们服务端支持Simulcast的话,这就成为一个棘手问题。 例如我们团队的WebRTC产品,客户端默认会启用Simulcast,上行推送多个不同分辨率的流到服务端,服务端下行会依据带宽估计值,自动选择某个分辨率的流给订阅客户端。假设此时下行带宽足够,服务端会推送大流。但是对于上行客户端来说,因为某种原因,Simulcast层数减少为一层,只剩一路小流了,如果没有相应处理,此时订阅客户端就接收不到视频流了。 Simulcast层数为何变化 WebRTC中如果送到编码器的采集视频帧分辨率发生变化,会触发ReconfigureEncoder也就是重置编码器操作,然后Simulcast层数也会重新计算。在WebRTC中,Simulcast层数与采集视频帧分辨率的关系由一张表决定: [crayon-69d0082e15feb443701893/] 对于1920x1080的采集分辨率,允许的最大Simulcast层数为3层,对于640x360的采集分辨率,允许的最大层数为2层。所以当采集视频分辨率从1920x1080变为640x360时,Simulcast层数就会发生变化。 下面简单说下几种造成采集视频帧分辨率变化的几种情况: 1)带宽估计值不够; 2)编码视频的QP值在设定的阈值范围外; 3)通过编码延时计算得到的CPU负载发生变化。 发生如上情况时,WebRTC会调整采集的原始视频帧分辨率或者帧率,至于是调整采集分辨率还是帧率,依据我们采取的策略,WebRTC中目前存在四种策略: 1)BALANCED,平衡模式; 2)MAINTAIN_RESOLUTION,保分辨率模式,也就是清晰模式; 3)MAINTAIN_FRAMERATE,保帧率模式,也就是流畅模式; 4)DISABLED,禁用模式。 例如对于摄像头视频流,默认是流畅模式,我们举两个例子说明下。 1)如果分配的带宽估计值满足不了当前编码器分辨率的码率设置时,就会触发降低采集视频帧分辨率的操作; 2)如果检测到CPU负载较高,也会触发降低采集视频帧分辨率的操作。这种现象在移动端设备较为明显,会看到视频刚开始很清晰,后面就模糊了(假设带宽足够情况下)。 当然了,对于屏幕共享流,默认是清晰模式,只会触发降低帧率的操作,看起来视频会变卡。 如何防止Simulcast层数变化 这里我们先看下启用Simulcast时,采集分辨率改变后,重置编码器的相关代码流程: [crayon-69d0082e15ff4128043432/] 在LimitSimulcastLayerCount中会根据前面提到的表内容,改变Simulcast层数: [crayon-69d0082e15ff9053816604/] 可知,通过在webrtc::field_trial设置禁用kUseLegacySimulcastLayerLimitFieldTrial即可保持当前Simulcast层数,避免了大流消失的麻烦。 总结 本文分析了Simulcast层数变化的原因,以及如何防止变化。其中介绍了WebRTC的清晰模式,流畅模式等概念,后续文章会对这些进行详细分析。
RTP Header RTP协议中,RTP Header(报头)包括固定报头(Fixed Header)与报头扩展(Header extension,可选)。 RTP Fixed Header结构如下,其中前12字节内容必须包含的。 [crayon-69d0082e175bc090814919/] 但是这Fixed Header携带的信息满足不了更复杂的需求。所以引入了RTP Header Extension,可以携带更多的信息,同时方便各种扩展。 RTP Header Extension 如果RTP Fixed Header中,X字段为1,说明后面跟着RTP Header Extension。RTP Header Extension结构如下: [crayon-69d0082e175c6783172444/] defined by profile:决定使用哪种Header Extension:one-byte或者two-byte header length:表示Header Extension的长度:length x 4字节 根据Header Extension中ID+len字段所占字节,RTP Header Extension分为One-Byte Header和Two-Byte Header类型。 One-Byte Header 对于One-Byte Header,"defined by profile"字段为固定的0xBEDE。接着后面的结构如下,占用1个byte: [crayon-69d0082e175cb522671751/] ID:4-bit长度的ID表示本地标识符 len:表示extension data长度,范围:0~15,为0表示长度为1字节,15表示16字节 所以叫做One-Byte Header。 如下是一个One-Byte Header的示例: [crayon-69d0082e175ce493016262/] 首先是0xBEDE固定字段开头,接着length长度为3,说明后面跟着3x4字节长度的header extension 。对于第一个header extension:L=0,表示data长度为1字节。对于第二个header extension:L=1,表示data长度为2字节。由于按4字节对齐,所以接着是值为0的填充数据。最后一个header extension:L=3,表示data长度为4字节。 WireShark抓包分析 如下是WireShark抓包WebRTC视频流解析的一个RTP包结构: Defined by profile字段为0xBEDE,表示One-Byte Header,Extension length为1,表示Header Extension长度为1x4字节,对于Header Extension:ID为3,Lengh为2。 相关代码Review 构造相关代码位于RtpPacket::AllocateRawExtension中 解析相关代码位于RtpPacket::ParseBuffer中 大家可以对着WebRTC相关代码熟悉下协议。 Two-Byte Header "defined by profile"字段结构如下: [crayon-69d0082e175d1831151999/] 接着后面跟着的每个扩展元素结构如下,占用2个byte: [crayon-69d0082e175d4376576017/] ID:本地标识符 length:表示extension data长度,范围1~255 如下是一个Two-Byte Header示例: [crayon-69d0082e175d8594965615/] 首先"defined by profile"字段为0x1000,length为3,后面跟着3x4字节长度扩展,对于第一个header extension:L=0,数据长度为0,对于第二个header extension:L=1,data长度为1,接着是填充数据,对于第三个header extension:L=4,后面跟着4字节长度数据。 由于WebRTC中默认都是One-Byte Header,所以就不抓包分析了,具体构造解析代码跟One-Byte Header位于同一地方。 常见RTP Header Extension 在WebRTC中定义了很多RTP Header Extension,最常见的要数用于带宽估计的Transport-CC扩展,记录传输层序列号:TransportSequenceNumber,默认每个RTP包都带有此扩展。 还有记录音频能量大小的AudioLevel扩展,记录发送时间的AbsoluteSendTime扩展等等。 看下目前WebRTC支持的几种Header Extension: [crayon-69d0082e175db700847173/] SDP 如下是某个示例SDP内容: [crayon-69d0082e175df957842455/] SDP中,a=extmap开头即为RTP Header Extension, 对于RFC中定义的RTP Header Extension,SDP格式如下: [crayon-69d0082e175e3572370356/] 对于WebRTC中自定义的RTP Header Extension,SDP格式如下: [crayon-69d0082e175e6855949037/] value值就是Header extension的本地标识符(ID值),ID值中0是预留位,所以我们可以看到从1开始递增,对于One-Byte Header来说,15也是预留位。 下面结合该SDP看下某个视频包的Wireshark解析: 可以看到该RTP包共有4个Header extension,这四个Header extension ID分别为:2,3,4,10。根据示例SDP,可知分别是:abs-send-time,transport-wide-cc-extensions,mid,rtp-stream-id扩展。 相关代码 RTP header extension的解析以及构造代码位于rtp_header_extensions.cc中。这里以TransportSequenceNumber扩展为例,看下代码: [crayon-69d0082e175e9336073460/] 前面SDP章节提到了,Header extensin ID跟Header extensin类型的对应。它们的对应关系处理在RtpHeaderExtensionMap类中,当进行SetLocalSdp操作时候,会调用: [crayon-69d0082e175ec593433290/] 绑定它们的关系。 总结 本文介绍了RTP包中的Header Extension,通过本文可以了解下Header Extension的结构,以及如果根据SDP以及Wireshark分析。 参考 [1] RTP: A Transport Protocol for Real-Time Applications.https://tools.ietf.org/html/rfc3550. [2] A General Mechanism for RTP Header Extensions.https://tools.ietf.org/html/rfc5285.
在WebRTC中,对于音频丢包,目前有三种丢包恢复方案: 带内FEC 带外FEC NACK 本文介绍其中最简单的带内FEC。带内FEC属于WebRTC中默认启用的功能,由Opus编解码器实现,经过我们测试,30%随机丢包率下语音聊天,仍有不错的质量。不过单一靠带内FEC是无法实现更高的抗丢包要求,例如突发丢包环境或者30%以上随机丢包率,这也是各个厂商的优化点。 Opus编解码器 Opus编解码器其实是一种混合音频编解码器,融合了SILK与CELT两种编解码器。 SILK编解码器 由Skype开发 使用线性预测 适合语音 CELT编解码器 由Xiph.Org开发 使用改进的离散余弦变换 适合音乐等音质要求高的 支持特性 FEC(Forward Error Correction):前向纠错。使用低码率编码前面音频包数据,作为冗余信息。 DTX(Discontinuous Transmission):非连续传输。在安静的情况时自动降低编码码率。 PLC(Packet Loss Concealment):丢包隐藏。在解码端实现。通过前面音频数据包和后面音频数据包的相关性来预测当前丢失的音频数据包,对丢失的音频数据包进行补偿,隐藏当前的丢包错误。 关于Opus中SILK与CELT为什么在一起的故事,大家可以看下声网社区分享的这一篇文章: 编解码器杂谈:浅析Opus 带内FEC原理 使用低码率编码前面的历史音频包数据,作为冗余信息插入当前音频包中,同时结合解码端的PLC处理,从而实现不错的丢包恢复效果。 假设当前音频包采样时间为T,采样间隔为Δ,那么某音频编码后数据如下: [crayon-69d0082e18a3d899216316/] 携带T时刻编码数据,以及低码率编码的T - Δ时刻数据。 假设Δ为1,时间戳T从1开始,那么前6个包数据如下: [crayon-69d0082e18a46255706463/] 如果时刻4的包丢了,那么接收端可以从时刻5的数据包恢复时刻4数据。 如果出现突发连续丢包,例如时刻4,5的数据包都丢了,那么时刻4数据就无法还原了,此时就要通过PLC算法进行丢包恢复。当然,如果使用了带外FEC以及NACK,也可以配合带内FEC用于丢包恢复。 代码导读 带内FEC整个闭环处理比较简单: 从Receiver Report中获取丢包率 平滑处理丢包率 平滑后的丢包率反馈给Opus编码器 AudioEncoderOpusImpl::OnReceivedUplinkPacketLossFraction负责相关处理。 接收通过RTCP反馈的丢包率,传给Opus编码器。 [crayon-69d0082e18a4a795392053/] SDP处理 这里我们看下使用Opus编码器时默认生成的SDP: [crayon-69d0082e18a4d748634754/] 可以看到useinbandfec=1字段,表示开启带内FEC,最后通过WebRtcOpus_EnableFec接口启用带内FEC功能。 优势与不足 由于带内FEC由编解码器实现,所以我们的工作量很少,可以很方便地将音频FEC的能力集成到我们的应用中。不过带内FEC中,音频包的冗余信息用的是低码率编码,所以还原后的音质会降低。 参考 [1] RTP Payload Format for the Opus Speech and Audio Codec.https://tools.ietf.org/html/rfc7587.
Unauthorized reproduction or plagiarism in any form is strictly prohibited. For reprint requests, please contact via email.