作者:Stian Selnes(原文链接)
翻译:刘通
原标题:How to capture & replay WebRTC video streams with video_replay (Stian Selnes)
引言—Philipp Hancke
当有丢包存在的时候进行视频解码并不会一项工作。因为在Chrome 58版本中引入了一个新的视频jitter buffer,导致了新版本的Chrome浏览器深受视频损伤之苦。因为只有当特定的数据包丢失的时候这个bug才会出现,所以想要进行调试就很困难。为了解决这些问题,webrtc.org提供了一个很有用的工具来复制并且对它们进行分析,叫做video_replay。当我看到Stian Selnes提出了另外一个视频损伤问题时,我告诉他了这个工具。有了这个工具可以非常简单的复制视频流,而Google的WebRTC视频组正式借着这个工具的帮助缩短了debug的时间。坏消息是,这个工作并没有被很好的记录下来,所以我请Stian来带我们过一遍捕捉所需数据并且使用video_replay工具的流程。
如果你的WebRTC视频流是这个样子的,意味着出问题了。幸运的是有video_replay可以用。
WebRTC有一个非常有用但是少有人知的工具叫做video_replay,已经被证明对视频解码问题的debug工作非常有用。它的功能是为了非常简单地重放捕捉到的WebRTC通话来重现观察到的现象。video_replay将捕捉到的RTP视频流作为输入文件,使用WebRTC帧来解码视频流,然后在屏幕上播放输出结果。
为了详细说明,我最近在研究的一个问题是Chrome突然将输入视频播成了上图那样。最终,在使用video_replay进行debug之后,WebRTC工作组发现Chrome的jitter buffer重新实现会引发一个bug,会在一些特定情况下使视频流产生损伤。导致VP8解码器的内部状态发生错误,使得输出帧看起来就像随机数据一样。
(点击图片跳转观看视频)
视频编码问题是最难解决的问题之一。刚开始我还可以开发一个测试,来复制每20个通话中的错误。如此复制错误的做法是非常消耗时间,会让你很沮丧,而且还不会让WebRTC工作组寻找解决办法的工作变得轻松。为了坚持复制问题,我想办法用Wireshark捕捉到了其中一个失败的通话,然后将其输入给了video_replay工具。现在我就有了一个测试用例可以每次都复制这个汉奸的错误了。当这个问题变得可重复之后,就会吸引很多人来尝试解决它。一个典型的双赢局面。
在这篇文章中,我会通过一个例子来阐述如何使用video_replay。在这个例子中,我们捕获了一个WebRTC通话的RTP传输,找到并且提取出接收到的视频流,并且最终将其输入给video_replay来将这段捕捉到的视频播放到屏幕上。
捕捉未加密的RTP
video_replay把输入文件送入RTP栈,拆包器和解码器。但是,它目前还没有解密已加密通话中SRTP包的功能。通话加密对Chrome和Firefox来说都是标准。想解密一个WebRTC通话并不是一个简单的工作,特别是因为用DTLS来分享私钥,所以不容易得到。为了避开这些,最好是用Chromium或者Chrome Canary,因为它们会关闭SRTP加密功能。只需要使用命令行flag -disable-webrtc-encryption启动浏览器,然后就应该看到弹出了一个警示框提示你正在使用一个未受支持的命令行flag。需要注意的是通话的双方都需要在未加密的状态下进行通话;如果不这样做通话就会连接失败。
首先,使用Wireshark开始抓包。非常重要的一点是在通话开始发送媒体之前就要开始进行抓包,这样可以确保你将整个流完整的记录了下来。如果开头没有被抓到,那么就会发生无法解码的错误。
然后,打开一个标签页进到chrome://webrtc-internals。在通话进行之前完成这个工作已确保可以获得完整的信息,特别是协商的SDP。
最后,这个时候进行通话。我们会用appr.tc作为例子,但是任何使用WebRTC的通话都是可以的。在浏览器中再打开一个标签页,进入https://appr.tc/?ipv6=false。 在本示例中我将IPv6关掉是因为目前video_replay还有一些问题没有得到解决,但是我会想办法尽快修复的。
现在,加入到一个视频聊天室中。RTP会在第二个通话参与者加入的时候开始进行。谁先加入都没关系,只是chrome://webrtc-internals会看上去又细微的差别。下面的一张图是在进入房间是截下的。
收集信息
为了可以提取出RTP包,并且使用video_replay成功播放,我们需要收集关于RTP流的细节信息。有很多种方法可以达到这个目的,但是我会挑可以给出最明确说明的那个方法。我们需要:
# 视频编解码器
# RTP SSRC
# RTP负载类型
# IP地址和端口
用webrtc-internals收集数据
首先,点开接收视频流的数据表,数据表的名称与ssrc_4075734755_recv相似。有可能存在不止一个表,通常来说第二个流是音频流,并且有可能是一对有着_send下标的,他们与发送流有着同样的数据。接收流的数据很好辨认,只要有_recv下标,而且mediaType=video的就是接收流。需要注意的是ssrc,googCodecName和transportId这三个值分别是4075734755,VP9以及Channel-audio-1。
你可能会问了为什么视频流的transportId和音频轨道的一样?这说明使用了BUNDLE,使用BUNDLE就会让音视频共享一个通道。否则音频和视频将会使用不同的轨道。
接着,我们来看协商SDP以获得RTP负载类型(PT)。除了需要找到视频编解码器使用的PT之外,我们还必须找到RED的PT。RED是WebRTC用来封装视频包的。SDPSDP描述了视频客户端的接收能力,所以想要找到我们接收的负载类型,我们就必须查看我们浏览器提供给另一个通话方的SDP。通过展开setLocalDescription API调用可以找到这个SDP,找到m=video的部分,以及随后的rtpmap属性,它定义了每个支持的codec的PT。因为我们所使用的视频编解码器是VP9,我们需要注意这连个值a=rtpmap:98 VP9/90000以及a=rtpmap:102 red/9000,它们可以告诉我们VP9的负载类型是98,RED的负载类型是102.
如果你在找的是关于发送流的信息而不是接收流的,你应该通过展开另一端通话方的setRemoteDescription来寻找响应数据。实际上,负载类型应该是对称的,所以无论你看的是setLocalDescription还是setRemoteDescription都无所谓,但是你知道在实时视频通信的世界里没有什么打包票的话。
为了可以快速的在Wireshark中定位到正确的RTP流,得知使用的IP地址和端口是十分有用的。只要你设定了合适的Wireshark过滤器,不管你是在找远端还是本地地址都无所谓。对于本例来说,我们准备使用本地地址,因为我们想要提取的是接收流,所以本地地址是我们所感兴趣的数据包的目的地。chrome://webrtc-internals包括了以Conn-audio和Conn-video作为开头的连接数据。激活的项会以加粗的形式显示,并且基于上一步中的transportId,我们可以知道是要看音频还是视频通道。为了在我们的例子中找到本地地址,我们需要展开Conn-audio-1-0并且留意googLocalAddress这一项,它的值是10.47.4.245:52740。
Wireshark中的RTP标记
现在我们已经收集了可以快速找到并且提取通话中接收流的全部所需信息。Wireshark很有可能会将捕捉到的RTP包简单的显示成UDP包。我们需要告诉Wireshark这些是RTP包,这样我们才能将它们输出成rtpdump的格式。
首先,针对地址和端口设置一个显示过滤器,比如ip.dst == 10.47.4.245和udp.dstport == 52740。然后右键点击一个包,选择“Decode As”,然后选择“RTP”。
然后,通过从菜单选择Telephony -> RTP -> RTP Streams来列出所有RTP流。我们接收流的SSRC会与其他流列到一起。选择它然后提取成rtpdump格式。最终我们就得到了一个只包含接收视频包的文件,这个文件就可以传给video_replay了。
搭建WebRTC以及video_replay
在你能使用video_replay之前你需要从WebRTC资源中搭建它。你可以在https://webrtc.org/native-code/development这里找到如何设置环境,获取代码,以及编译的说明。要注意如果想要搭建video_replay,就必须在编译的时候将这个工具明确地列为目标。简而言之,在安装完所有必需软件之后,下面的指令会获取到代码并且搭建video_replay。
mkdir webrtc-checkout
cd webrtc-checkout/
fetch –nohooks webrtc
gclient sync
cd src
gn gen out/Default
ninja -C out/Default video_replay
使用video_replay来回放捕捉内容
终于,我们准备好来回放捕捉到的视频流了,希望可以完美的复制原本在appr.tc中的画面。在我们的示例中,完成这些最少的指令如下:
out/Default/video_replay -input_file received-video.rtpdump -codec VP9 -payload_type 98 -red_payload_type 102 -ssrc 4075734755
video_replay参数
如果我们的目标是复制WebRTC的问题并且提交bug,将rtpdump和重放问题的指令参数一起提供会极大的帮助问题解决。但是,如果你想要做的事情不只是debug的话,video_replay还有很多指令行你可能会感兴趣。
让我们来看看帮助里的文本,然后解释不通的选项是干什么的。
../../webrtc/video/replay.cc里的flag:
-abs_send_time_id (RTP extension ID for abs-send-time) type: int32 default: -1
-codec (Video codec) type: string default: "VP8"
-decoder_bitstream_filename (Decoder bitstream output file) type: string default: ""
-fec_payload_type (ULPFEC payload type) type: int32 default: -1
-input_file (input file) type: string default: ""
-out_base (Basename (excluding .yuv) for raw output) type: string default: ""
-payload_type (Payload type) type: int32 default: 123
-payload_type_rtx (RTX payload type) type: int32 default: 98
-red_payload_type (RED payload type) type: int32 default: -1
-ssrc (Incoming SSRC) type: uint64 default: 12648429
-ssrc_rtx (Incoming RTX SSRC) type: uint64 default: 195939069
-transmission_offset_id (RTP extension ID for transmission-offset) type: int32 default: -1
下面是解释:
Flag |
描述 |
是否必须 |
abs_send_time |
abs-send-time 扩展的RTP扩展ID |
非必须 |
codec |
视频codec与负载类型相关,并且定义了用来解码流的codec |
必须 |
decoder_bitstream_filename |
在depayload和解码之前存储接收比特流的文件。比特流不需要任何类型的容器或者帧分隔符就会被输出到文件中,并且不可以被解码。 |
非必须 |
fec_payload_type |
Forward Error Correction 包的RTP负载类型。FEC是当发生丢包时的增强质量机制。FEC的负载类型可以从SDP中分离出来,比如 a=rtpmap:127ulpfec/90000 |
非必须 |
input_file |
需要进行重放的文件。这项既可以是rtpdump也可以是pcap。目前为止pcap只支持LINKTYPE_NULL and LINKTYPE_ETHERNET ,这让它的使用范围就受到了些许限制。 |
必须 |
out_base |
以未压缩的格式(I420)将解码后的帧存入文件。 |
非必须 |
payload_type |
需要进行解码的视频包的负载类型。 |
必须 |
payload_type_rtx |
有丢包时RTP retransmission (RTX) 的负载类型。这类包的负载类型也可以在SDP中找到。注意RTX有多种负载类型。一定要使用与正确负载类型相关的一个,比如 a=rtpmap:99 rtx/90000a=fmtp:99 apt=98 ,表示负载类型99是负载类型98的重传。 |
非必须 |
red_payload_type |
RED的负载类型。 |
非必须 |
ssrc |
需要解码流的SSRC |
必须 |
ssrc_rtx |
RTX流的SSTC。实际上是在发送端提供的SDP中找到的。在特性ssrc-group中找到,比如:
a=ssrc-group:FID 40757347553957275412, |
非必须 |
transmission_offset_id |
transmission offset 扩展的RTP扩展ID |
非必须l |