There is no excerpt because this is a protected post.
There is no excerpt because this is a protected post.
在Webrtc中存在两种不同的audio level定义,本篇文章我们将介绍下这两种不同的audio level。 AudioDeviceBuffer中的level 在WebRTC中通过AudioDeviceBuffer传递采集播放数据,这里我们将介绍AudioDeviceBuffer中的audio level相关情况。首先先简单介绍下AudioDeviceBuffer在采集播放过程的作用。 音频采集 音频采集模块通过调用AudioDeviceBuffer的SetRecordedBuffer与DeliverRecordedData传递采集数据。 SetRecordedBuffer:把采集数据存到AudioDeviceBuffer的缓存中,调用UpdateRecStats进行采集统计 DeliverRecordedData:将刚才缓存的数据传递给AudioTransport,从而送到3A以及发送模块 音频播放 音频播放模块通过调用AudioDeviceBuffer的RequestPlayoutData以及GetPlayoutData获取要播放的数据。 RequestPlayoutData:通过AudioTransport获取要播放的数据,并存到AudioDeviceBuffer的缓存中,调用UpdatePlayStats进行播放统计 GetPlayoutData:获取刚才缓存的播放数据 统计日志 前面提到调用接口都是由采集播放线程驱动的。通过调用接口会同时调用音频的统计。 一般排查音频采集播放问题,我们可以通过AudioDeviceBuffer中的统计日志判断,如下是一个示例日志: [crayon-69e941d0cc604408612867/] 该日志会打印10s内采集播放情况。 callbacks:表示接口调用次数,一般是默认10ms采集播放一次,所以10s内会调用1000次 samples:表示实际音频采集播放样本数,由于示例中采样率为48k,所以10ms样本数为480,所以10s采集播放样本数都为480000 rate:表示实际采集播放速率,也就是每秒的采集播放样本数 rate diff:表示实际采集播放速率跟采集播放设置的采样率差异,百分数表示,这个值不为0,可能是抖动以及丢音情况 level:这里的level是通过调用WebRtcSpl_MaxAbsValueW16计算的,WebRtcSpl_MaxAbsValueW16用于计算16bit音频样本数据的最大绝对值,这个level表示10s内采集播放数据的最大绝对值,也就是最大振幅 至此我们介绍完了AudioDeviceBuffer中的level,这个level表示音频数据的最大振幅,一般我们可以通过统计日志中这个level初步判断下音频数据情况,例如是否静音。 报头扩展中的level 这节我们介绍下音频报头扩展中的audio level。 audio level header extension 根据rfc6464,audio level报头扩展的作用是方便会议转发服务器根据携带的audio level信息进行音频选择转发处理,尤其在音频流非常多情况下。转发服务器可以根据audio level信息,转发最active的几路流,一般是选择3路,从而节省带宽等资源。 rfc中的定义 audio level扩展有两种不同的格式。 one-byte header格式: [crayon-69e941d0cc611564948110/] V:1bit,标识该音频包是否包含语音(voice activity),为1含有语音。这个字段是否用到由SDP中是否包含"vad="决定,后面SDP小节介绍 level:7bit,范围为0(最大声)~127(静音),单位-dBov,也就是0到-127dBov db表示分贝,用于度量声音强度,计算公式如下: 计算分贝需要参考值Pref,db后面带的后缀表示用到的参考值,dBov的ov后缀表示overload,参考系统的overload point,也就是当前payload格式音频所能忍受的最大声。 two-byte header格式: [crayon-69e941d0cc616323660748/] WebRTC中默认都是使用one-byte header格式。这里我们看下one-byte header解析以及构造代码: [crayon-69e941d0cc619260757506/] SDP中的audio level header extension 先看下一段音频示例sdp: [crayon-69e941d0cc61c492744735/] 可以看到其中audio level的描述如下: [crayon-69e941d0cc620776629825/] 如果该描述尾部带有:vad=on,也就是: [crayon-69e941d0cc623034673944/] 表示用到了header extension中的V字段,如果是”vad=off“,表示没有用到V字段。没有指定"vad=",默认表示"vad=on",WebRTC生成的SDP中没有指定"vad=",所以WebRTC中默认都是"vad=on"。 Wireshark解析的audio level 根据前面一节的SDP可知audio level扩展的ID为1,根据该ID,我们看下Wireshark中的解析: 根据解析,该音频包audio level扩展数据为0xa8,可知V字段为0xa8 & 0x80 = 1,说明有语音,level字段为:0xa8 & 0x7f = 0x28,也就是audio level为40-dbov。 代码中的audio level计算 audio level通过计算均方根(Root Mean Square,RMS)得到,计算代码位于RmsLevel类中。 均方根,也叫做平方平均数(quadratic mean),是指一组数据的平方的平均数的算术平方根。 首先需要计算所有幅值平方和: [crayon-69e941d0cc626399643365/] 接着将平方和除以样本数: [crayon-69e941d0cc62a258635337/] 最后根据平方和的平均数计算均方根: [crayon-69e941d0cc62d631084715/] 计算后的audio level通过如下代码流程写到header extension中: [crayon-69e941d0cc630360012109/] 总结 本文介绍了WebRTC中两种audio level,其中一种表示振幅,另一种表示均方根能量。通过本文可以对音频问题初步分析以及audio level这个报头扩展增加了解。 参考 [1] A Real-time Transport Protocol (RTP) Header Extension for Client-to-Mixer Audio Level Indication.https://datatracker.ietf.org/doc/rfc6464/
系统要求 磁盘空间:至少5.6 GB磁盘空间(带IOS支持) 准备工作 需要Xcode 9以及以上版本。 对于外网访问,我这里用的是v2rayu,在终端执行如下代理设置: [crayon-69e941d0cd9b4704096571/] 安装depot tools [crayon-69e941d0cd9be957106620/] 获取代码 [crayon-69e941d0cd9c2533188434/] 如果中间意外中断,执行gclient sync即可。 生成Ninja工程文件 WebRTC默认使用Ninja作为编译系统,Ninja工程文件通过GN生成。 使用如下命令生成默认配置工程(Debug编译,工程文件位于out\Default目录下): [crayon-69e941d0cd9c6515883264/] 如果需要Release编译,通过如下命令生成工程文件: [crayon-69e941d0cd9c9179000327/] 编译 [crayon-69e941d0cd9cc970868302/] 最后在src/out/Default/obj可以看到生成的静态库文件:libwebrtc.a,WebRTC.framework文件以及测试demo:AppRTCMobile。src/out/Default看到all.xcodeproj文件。 代码更新 后续如需要更新代码,按照如下步骤: [crayon-69e941d0cd9d0461598167/] 然后参考前面步骤重新生成工程文件,编译即可。 参考 [1] WebRTC Native code Development.https://webrtc.github.io/webrtc-org/native-code/development/. 本文已收录到大话WebRTC专栏,更多精彩请访问《大话WebRTC》。
RTP时间戳 RTP固定报头中,带有4字节大小的时间戳。 [crayon-69e941d0cebbf674903308/] RTP时间戳一般以采样频率为单位。相邻两帧的时间戳增量为: RTP时间戳分为绝对时间戳跟相对时间戳。本文主要介绍这个时间戳是如何产生的。 视频RTP时间戳产生 由于视频每帧间隔也就是时间差不是稳定的,无法使用固定的时间戳增量计算时间戳,所以视频使用当前帧采集的NTP时间作为时间戳计算基础,也就是使用绝对时间戳表示。 采集阶段 调用set_timestamp_ms(rtc::TimeMillis())设置当前时间。 [crayon-69e941d0cebc8258182005/] 编码阶段 这里将NTP时间戳转换为RTP时间戳,计算方法为: RTP时间:NTP时间(ms) * 90000(采样率) / 1000,对应代码如下: [crayon-69e941d0cebcc100893738/] 根据rfc3551,5.Video章节,之所以使用90kHz作为采样频率是因为,90kHz跟MPEG呈现时间戳频率相同。同时90kHz差不多是24(HDTV),25(PAL),29.97(NTSC)和30Hz(HDTV)这些电视信号制式帧率以及50,59.94和60帧率的整数倍。同时使用90kHz能够满足一些场景的时间精度需要。例如: 音视频同步,需要将RTP时间戳转为NTP时间戳 抖动计算,参考:WebRTC研究:统计参数之抖动 RTP打包发送阶段 这里会在之前设置的RTP时间戳基础上加上一个随机偏移,加上这个偏移是为了防止时间戳冲突,假如同一个时刻有好几个视频源同时产生视频,它们就可能有完全一样的时间戳,对应代码如下: [crayon-69e941d0cebd0992965690/] 至此得到了最后视频RTP包中的时间戳。 音频RTP时间戳产生 RTC中音频默认是10ms采集一次。由于采样间隔也就是时间差是稳定的,根据公式(1),相邻采集帧时间戳增量为: 10ms * 采样率 / 1000 = 采样率 / 100,也就是每帧单声道的样本数。所以音频时间戳计算无需获取当前NTP时间,从0开始,按固定时间戳增量不断增加即可,也就是使用相对时间戳表示。 采集阶段 直接取每帧单道样本数作为时间戳增量,计算代码如下: [crayon-69e941d0cebd3205010156/] 这样得到了每一个采集帧的时间戳。 编码阶段 Opus编码器默认设置20ms编码帧长,一个Opus编码帧包含20ms音频数据。也就是每两个10ms音频采集帧编码为一个opus帧,喂给编码器两帧10ms数据,最后编码出一帧,这时编码帧时间戳增量为:2 * 采样率 / 100,Opus编码器内部处理代码如下: [crayon-69e941d0cebd6451648771/] Opus编码完一帧后,会将此时的RTP时间戳通过EncodedInfo返回给上层,然后送到RTP打包发送模块,对应处理代码如下: [crayon-69e941d0cebd9551525045/] RTP打包发送阶段 跟视频RTP时间戳处理一样,也需要加上一个随机偏移: [crayon-69e941d0cebdc085686119/] 总结 RTP时间戳在组帧,抖动计算,音视频同步等起到了重要作用,通过本文相信对RTP时间戳有了更深刻认识。 参考 [1]RFC3550.https://www.rfc-editor.org/rfc/rfc3550. [2]RFC3551.https://www.rfc-editor.org/rfc/rfc3551.
今天我们来看下chrome://webrtc-internals/页面中的一个重要统计参数:丢包率(Fraction lost)。 什么是丢包率 丢包率这个概念我想大家再熟悉不过了,一般来说,丢包率是指所丢失数据包数量占所发送数据包的比例。丢包率也是衡量网络性能的一个指标,我们通常使用丢包率判断当前网络质量,不过丢包也分为拥塞丢包,随机丢包等,随机丢包情况下,我们不能通过得到的丢包率认为当前网络质量差,发生拥塞。这也是现在很多拥塞控制算法不使用丢包率作为主要衡量指标的原因。 ping命令是最常见的一种估计丢包率的方法。下面是ping Google的示例,底部packet loss就是这次ping的丢包率检测结果。 [crayon-69e941d0cf68f609482629/] RTCP中的丢包信息 在WebRTC中,一般是通过Receiver report(RR)反馈丢包信息。RR记录着丢包相关统计。首先看下RR报文格式: [crayon-69e941d0cf698443999238/] 跟丢包统计有关的几个字段如下: fraction lost,自上次发送RR后SSRC_n的丢包率 cumulative number of packets lost: 24 bits,记录SSRC_n从开始到现在总共丢失的包 extended highest sequence number received: 32 bits,将序列号扩展为32bit,用于标识当前收到的最大包序列号 如上3个字段中,fraction lost根据后两个字段即可算出。前后两个RR报文间隔内fraction lost计算公式入下: [crayon-69e941d0cf69c398954784/] 公式中: received_seq_max_:extended highest sequence number receive last_report_seq_max_:last extended highest sequence number received cumulative_loss_:cumulative number of packets lost last_report_cumulative_loss_:last cumulative number of packets lost 接下来我们结合接收端统计代码看下这几个参数如何计算。相关计算代码位于StreamStatisticianImpl中。 StreamStatisticianImpl代码导读 首先是根据RTP包记录相关信息: [crayon-69e941d0cf6a0867469845/] 接着构造RR时,根据记录的信息进行相关丢包统计参数计算 [crayon-69e941d0cf6a4051746966/] fraction lost 计算位于StreamStatisticianImpl::MaybeAppendReportBlockAndReset中,: [crayon-69e941d0cf6a7392937458/] cumulative number of packets lost 对应代码中的cumulative_loss_计算。 [crayon-69e941d0cf6aa004710110/] StreamStatisticianImpl::UpdateOutOfOrder中,如果是重传、乱序或者重复包,当前包sequence_number 会小于received_seq_max_。 [crayon-69e941d0cf6ad591828098/] 代码中提到得到的丢包率不是原始丢包率,是包括重传包后的丢包率,实际值会低于原始丢包率,这个会干扰我们的一些判断,假如每次丢包后面重传都收到,那么我们也许会得到一个丢包率为0的反馈,由于Sendside BWE中用到了这个丢包率,所以对带宽估计值有影响。那么有什么办法避免吗,如果要想得到原始丢包率,重传包就不能进入这里统计了,WebRTC提供了RTX机制,重传包用额外SSRC的包发送,这样重传包就不会干扰原始媒体包的统计。 extended highest sequence number received 收到的最新非乱序包序列号,前面提到这个字段用32bits表示,所以需要对16bits进行扩展处理。 StreamStatisticianImpl::UpdateCounters中得到64bits的序列号: [crayon-69e941d0cf6b0903005642/] 接着在StreamStatisticianImpl::MaybeAppendReportBlockAndReset中将该序列号传给ReportBlock: [crayon-69e941d0cf6b4348844546/] 传递给ReportBlock时会转为32bits。 [crayon-69e941d0cf6b7895791657/] 发送端中的丢包率 WebRTC中通过接收端统计包接收情况,反馈给发送端,然后发送端根据这些统计进行丢包率计算。发送端中丢包率主要用在Sendside BWE中。这里先看下主要代码流程: [crayon-69e941d0cf6ba455811086/] RtpTransportControllerSend::OnReceivedRtcpReceiverReportBlocks中根据RTCP报文中的信息提取相关信息。 [crayon-69e941d0cf6bc897097478/] SendSideBandwidthEstimation::UpdatePacketsLost中根据提取的信息进行丢包率计算。 [crayon-69e941d0cf6bf828196857/] 最后在SendSideBandwidthEstimation::UpdateEstimate中根据不同档位的丢包率进行相应的码率调整。 总结 本文主要介绍了WebRTC中丢包相关统计,以及使用非RTX方式进行丢包重传时引入的问题,最后简单介绍了下丢包统计在发送端的使用。 参考 [1] RFC3550.https://tools.ietf.org/html/rfc3550.
丢包检测与重传作为WebRTC中一个抗弱网机制,对提高QOS起了很大作用。WebRTC中目前主要靠NACK机制实现丢包检测与重传,接收端判断丢包情况并记录,然后通过NACK RTCP报文(RFC 4585)反馈丢包信息给发送端,发送端根据NACK报文信息进行包的重传。 NACK机制针对的是某个SSRC的单独流,对于某个SSRC流,对于一个包N,接收端要检测它是否丢了,需要前面收到的该流的包N-1,以及后面收到该流的包N+1的信息,通过检测N-1以及N+1间是否有空隙判断N是否丢了。如果因为一些特殊原因,包N+1到晚了些,那么我们就无法判断N是否丢失了,假如N真丢了,会造成包N的恢复时间变长。如果该流所在传输通道,还有其它SSRC流,那么可否借助其它流信息更早检测丢包? 目前WebRTC默认都是采用基于发送端的带宽估计(Send-side BWE),通过报头扩展携带的传输序列号Transport sequence number以及Transport Feedback RTCP报文携带的信息进行相关处理。对于单个传输通道,包含的所有流,携带的传输序列号采用统一的计数。Transport Feedback RTCP中基于传输序列号记录着包的详细信息,例如是否收到,到达时间等。既然Transport Feedback RTCP记录着接收端收到的所有流的包信息,那么就可以利用这个进行丢包检测。这样对于某个SSRC流,要检测丢包,就可以借助其他SSRC流的传输序列号了。对于单个传输通道多条流场景,例如单PC或者Simulcast,相比NACK机制,能够更早地检测到丢包情况。在WebRTC这个机制叫做早期丢包检测(Early loss detection)。 与NACK机制差异 我们通过某个例子解释下早期丢包检测机制为什么会快些。假设某个传输通道下有视频流A与B,它们的RTP包记为Ra(n,m),Rb(n,m),n表示RTP sequence number,m表示Transport sequence number,这样同一个传输通道(PeerConnection)下,视频流按如下形式传输: Ra(1,1),Ra(2,2),Rb(1,3),Rb(2,4),Ra(3,5),Ra(4,6),Rb(3,7),Rb(4,8) 假设只有Ra(2,2)丢了,在NACK机制中,只能处理单个SSRC的流,通过RTP序列号判断丢包。需要收到Ra(3,5)或者更靠后的Ra包,才能判断Ra(2,2)丢失。而在早期丢包检测机制中,可以直接通过Rb(1,3)的传输序列号判断Ra(2,2)丢失,所以能更早地检测到。 这里我们总结下跟NACK机制的差异: 1)NACK机制处理的是某个SSRC流,通过报头携带的RTP序列号判断 2)Early loss detection机制处理的是某个传输通道所有流,通过报头扩展携带的传输序列号判断。 接下来我们详细介绍下Early loss detection机制。 Transport Feedback RTCP 首先我们简单复习下Transport Feedback RTCP报文格式。Transport Feedback RTCP中记录着一系列包的到达状态以及时间。 记录的第一个RTP包传输序列号Transport sequence number为base sequence number,总共记录着packet status count个RTP包信息,这些RTP包Transport sequence number以base sequence number为基准,并依次递增。 通过packet chunk记录各个RTP包的到达状态,主要有:packet received,packet not received这两种状态。 以reference time为基准,通过recv delta记录各个"packet received"状态的包的到达时间信息。 [crayon-69e941d0d0994060705995/] 检测原理 前面我们简单回顾了Transport Feedback RTCP的格式,那么通过这个RTCP检测丢包就很简单了。"packet not received"状态的包就是我们所认为的丢包。只要我们解析Transport Feedback RTCP,获取这些"packet not received"状态的包,然后通过这些RTP包的Transport sequence number,从发送端记录的RTP包发送历史中获取对应RTP包的SSRC以及RTP sequence number,告知相应发送模块,即可实现重传。 接收端 接收端主要处理代码位于RemoteEstimatorProxy::MaybeBuildFeedbackPacket中,packet_arrival_times_记录着收到的RTP包的Transport Sequence Number以及到达时间。 [crayon-69e941d0d099e093090398/] 发送端 发送端处理代码位于RtpVideoSender::OnPacketFeedbackVector中。主要处理代码如下: [crayon-69e941d0d09a2139144434/] 通过SSRC找到对应的RTPSender,传入RTP序列号完成重传。 可能引起的问题 由于WebRTC中存在这种机制,那么我们自己编写接收端代码时需要注意一些事(一般是服务端),否则会引起问题。 举个例子,客户端上行推Simulcast流。大流B表示,小流S表示,后面跟着传输序列号。某次传输情况如下: [crayon-69e941d0d09a6024682923/] 假如我们服务端是单独处理大小流,构造大小流Transport Feecback RTCP报文也是单独处理,那么就会有个问题。 对大流的处理模块,收到的流为B1 B2 B3 B4 B7 B8 B9 B10,构造的Transport Feecback报文中记录的收到的传输序列号为:1 2 3 4 7 8 9 10,其中5,6由于属于小流导致记录为没收到。所以在发送端处理该Transport Feedback RTCP报文时,会将传输序列号5,6所属的RTP包重传,直到收到小流反馈的Transport Feecback报文。这样就会导致每收到反馈的Transport Feedback RTCP,就会重传RTP包,虽然并没有丢包。如果通过浏览器推流,那么在chrome://webrtc-internals/中会看到: 其中retransmittedPacketsSent一直在增加。 不过对于接收端而言,这些重传的包会被SRTP处理过滤掉,在libSRTP库中会报srtp_err_status_replay_fail错误,这是因为libSRTP的一个安全措施。libSRTP中有个窗口,记录着收到的包,如果后面收到的包在这个窗口内,会被认为是重复的包,从而报srtp_err_status_replay_fail错误,然后被丢弃。 所以编写服务端代码时,同一个传输通道的流,无论单PC,还是Simulcast场景,Transport Feecback RTCP构造得统一处理,这样就可以利用到早期丢包检测机制优势,同时避免增加不必要的带宽消耗。 总结 本文主要介绍了NACK机制与早期丢包检测机制差异,回顾了Transport Feedback RTCP报文格式,并结合代码简单介绍了早期丢包检测机制,最后举例讲了下这个机制可能引起的问题。WebRTC本身在不断演进,我们服务端开发的得时刻关注,做好兼容处理。
自从去年4月后,由于临时接手了离职音频算法工程师的工作,所以一直忙于音频处理的研究工作,同时解决产品上音频相关问题。由于要学的内容很多,很难有空余时间,不知不觉把博客给落下了。进入2022年,经过去年一段时间的潜心修炼,目前对WebRTC整个音频处理有了更深入的认识,当然,空余时间也多了些,所以准备继续更新博客,也把没回复的评论过下,找个时间回复。 这段消失的时间,自己总结起来有三方面收货: 1)加深了音频3A算法的熟悉,尤其是AEC3算法内部处理流程,以及特定场景下回声问题排查以及修复,例如采集丢音,采集播放抖动等场景的回声; 2)学了很多音频信号处理,同时解决了语音激励,共享桌面音频等场景下一系列问题; 3)进一步提升我们产品音频传输的带宽自适应能力。 回头想想,自己也许算半个音频工程师了 。 目前已经进入2022年了,虎年春节也要到来,后面准备保持之前的更新节奏,继续分享WebRTC以及音视频相关知识,加油!
本篇文章主要从自己前几个月遇到的一个小问题引出。 问题背景 之前使用Web浏览器播放做测试,发现升级Chrome 87后播放视频会卡,排查服务端下行处理日志发现,带宽估计模块异常,如下是部分异常日志: [crayon-69e941d0d1354199581902/] 由于下行带宽估计会作用到Pacing模块,带宽估计的异常触发Pacing模块发送异常,从而导致视频卡顿。明明最近没有优化服务端带宽估计处理,哪里出错了呢? 通过异常日志可以很容易发现是反馈的Transport Feecback报文异常,Transport Feecback报文记录的部分RTP包在发送端没有发送时间记录,所以导致“Failed to lookup send time for”错误。进一步分析发现是Chrome 87音频也有带宽估计RTCP反馈,Transport Feedback报文中同时记录着音视频的信息。由于我们服务端目前只支持视频的带宽估计处理,带宽估计模块没有记录音频的发送时间信息,所以导致报错。 源码分析 接收端 在RemoteEstimatorProxy中,如果收到的RTP包有TransportSequenceNumber报头扩展,就会将该RTP包信息记录到Transport Feecback报文中。Transport Feecback报文中记录着音频信息,说明发送端音频注册了TransportSequenceNumber报头扩展。 我们服务端在处理Web浏览器间的音频数据转发时,没做太多处理,毕竟浏览器上优化空间不大,除了重新构建序列号等报头信息,RTP报头扩展维持不动。所以问题出在上行的浏览器发送端。 发送端 M86及以前版本 AudioSendStream::AudioSendStream中: [crayon-69e941d0d135d597137442/] 默认没启用音频带宽估计特性。 在AudioSendStream::ConfigureStream中: [crayon-69e941d0d1361182850692/] 所以旧版WebRTC中音频默认没有注册TransportSequenceNumber扩展。 M87及以后版本 AudioSendStream::AudioSendStream中: [crayon-69e941d0d1364007499705/] allocate_audio_without_feedback_默认为false。 AudioSendStream::ConfigureStream中: [crayon-69e941d0d1367917013663/] 可知新版WebRTC中音频默认注册TransportSequenceNumber报头扩展,也就是开启带宽估计特性。 了解了区别后,问题修复就简单了。后面我们服务端增加了对音频带宽估计的兼容处理修复了该问题。 总结 本文在分析相关问题基础上,介绍了新旧WebRTC版本在音频带宽估计处理上的一个小区别。新版WebRTC默认启用音频带宽估计也是为了提高带宽估计准确性。
Unauthorized reproduction or plagiarism in any form is strictly prohibited. For reprint requests, please contact via email.