|pipe|联合创始人/CTO Tim Panton希望构建一个轻量级的H.264 WebRTC堆栈。本文展示了Tim一步步努力实现视频播放的过程,以及从中取得的收获。LiveVideoStack对文章进行了翻译。
文 /Tim Panton
译 / 许海燕
原文:
https://webrtchacks.com/what-i-learned-about-h-264-for-webrtc-video-tim-panton/
自WebRTC编解码器战争以缓和告终以来,已经有几年时间了。H.264也已经存在了超过15年,因此很容易掩盖运用中的多种复杂问题。
|pipe| CTO Tim Panton正在研究一个无人机项目,他需要为WebRTC提供一个轻量级H.264堆栈,而他决定自己构建一个。这当然不是我推荐给大多数人的一个运用,但Tim表示,如果不是一个简单的运用,那么这可能是一种启发性的体验。在这篇文章中,Tim一步步地向我们展示了他在努力让视频播放时的发现。除了阅读H.264介绍的RFCs规范之外,还可以通过它获得一个有趣的替换方案!
在WebRTC和VoIP出现之前,我已经做了WebRTC好几年了,所以我知道如何处理RTP和实时媒体。或者我是这么想的……
在那段时间里,我从未真正研究过视频技术。身边总有其他人已经做过这件事,所以我很高兴的保持无知,并且花时间学习SCTP 和WebRTC数据通道。
然后出现了一个副业,通过WebRTC从无人机发送H.264视频。它会有多难?
TLDR;
RTP语音!= RTP视频
为什么支持H.264而不支持VP8?
这就是无人机产生的问题。转码成VP8远远超出了硬件(Beaglebone or Raspberry Pi)的能力。我们团队提出了在WebRTC中支持H.264和VP8的折中方案,由于我是团队中的一员,我认为我应该利用并使用所有优秀的WebRTC端点中可用的H.264解码器。
所以我把插入到| pipe |的WebRTC堆栈中的开源srtplight 库(很久以前作为Phono项目JavaScript的一部分由我编写的代码)删去。我写了一个读取RTP数据包的类,使用DTLS-SRTP对它们进行加密,然后通过ICE选择的路径转发它们。我知道ICE / DTLS-SRTP位有效,因为我已经使用它来提供来自我们的WebRTC门铃PoC的音频。
为什么使用Java?
构建100万行libWebRTC代码需要20GB的内存,这让我无法使用C / C 。这超出了我的小项目想要处理的范围。
此外,我拥有Java所需的所有位。事实上,对于这类事情,Java是一个很好的选择——可以说这正是OAK——Java的前身被发明的目的。
经过磨练的JVM使其在许多架构上都具有可移植性和高性能。在ARM上,DTLS-SRTP(AES)中使用的加密直接映射到硬件加速指令,这意味着即使最小的Raspberry Pi也可以加密多个视频流。
多线程是这类网络任务的理想选择。
最后但同样重要的是,JVM的内存管理和编译器的强类型检查意味着我的代码相对不受缓冲区溢出和来自入站数据包的其他内存攻击的影响。(但是对于Maven,这使得所有其他构建系统看起来非常糟糕)。
让视频播放
首先,我努力让SDP提供/回答 运作。它花了一段时间,但最终Chrome接受了我的SDP并显示数据包到达。
尽管没有视频
我又挖了一些,发现数据包比我想象的要小一些。
更深入的研究表明,我将RTP类上的缓冲区保持得很小,因为这些类最初设计用于一个受限的环境(J2ME)。对于20 ms G.711的数据包来说已经足够大了,但比最大传输单元(MTU)小得多 - 所以我们的H.264数据包被截断了。这里我在srtplight中解决了这个问题。
仍然没有视频
看看Chrome的chrome:// webrtc内部页面,我获得了大量字节,但没有一个解码帧。
我有一种预感,这可能是因为我没有回复Chrome正在发送的RTCP数据包(或者实际上是发起了我自己的RTCP数据包)。
RTCP用于控制RTP媒体通道并报告统计信息。Chrome还使用RTCP扩展来估算可用带宽。由于这是浏览器的单向视频,我认为不需要RTCP。现在我开始怀疑......
所以我编写了一些最小的RTCP类添加到SRTP实现中。
来自RFC3711的安全RTCP数据包格式。RTCP是我最不喜欢的协议 - 加密部分的长度是加密的。
仍然没有视频。
通过Wireshark逆向工程H.264
标记位
我启动了Wireshark 并捕获了入站和出站数据包以尝试查看哪里出错了。盯着屏幕好几个小时后,我终于注意到...... 标记位设置在某些入站数据包上,但没有设置在任何出站数据包上。
现在我本应该阅读关于H.264分组的RFC(特别是第5.1节)。这将可以节省我很多时间。但是我并没有阅读。我确实记得DTMF使用标记位来表示这是一组(冗余的)DTMF数据包的结束。
我调整了代码 以确保标记位如实地从内到外传递。
视频有时候有一个帧或者两帧,然后什么都没有。
RFC3711的 SRTP数据包格式
时间戳
回到Wireshark。我再次比较了入站和出站数据包。我注意到入站数据包的时间戳被分组。5到10个包将具有相同的时间戳,最后一个包具有标记位设置。出站包有当前发送时间的时间戳。即他们增加了。
如果我读过RFC,我就会知道....
所以这就是:H.264(或任何视频编解码器)创建的帧比UDP网络的MTU大得多。因此,RTP打包器将帧拆分为数据包,并为与帧关联的所有数据包提供相同的时间戳,但递增序列号,最后一个包使用标记位进行标记。
FU MTU
您可能想知道为什么编码器不仅发送大于MTU的数据包还让IP级别处理分片。当我最终阅读RFC时,我发现了以下有关分片单元(FUs)的部分:
来自RFC6184的分片单元(FU)描述
我最初编写srtplight代码是为了从本地麦克风发送音频。它在传出的数据包上生成了自己的时间戳。
所以我解决了这个问题,如实地从内到外复制时间戳......
Wireshark跟踪显示标记位
更多的视频,更好的视频,几乎是可用的视频,除非它不是。
关键帧
我查看了到达接收端的序列号,看看是否有数据包被丢弃。WebRTC-internals和Wireshark表明没有,但视频讲述了一个不同的故事。
此时我使用H.264编码器模式,我发现更频繁地发送关键帧可以恢复停滞的视频。
与音频编解码器不同,并非所有帧都与视频同等重要。大多数帧仅描述图像中的差异 - 除非所有先前的帧都已被解码,否则这些差异无法呈现。例外情况是关键帧 - 它们包含完整的(即使模糊的)图像和功能,作为后续数据包构建的基础。(这是对H.264实际功能的难以置信的粗略简化,但从数据包的角度来看,它确实可以做到这一点)。因此,获得一个关键帧可以让混乱的解码器重新开始。
这并没有解释为什么它一开始就被混淆了。再看看Wireshark,我意识到有些帧在入站端丢失了数据包,尽管在出站时没有丢失数据包。直到我记起srtplight默认创建序列号(因为这是麦克风所需要的),这才变得有意义。因此,如果来自无人机的入站数据包被丢弃或乱序了,srtplight将从那时发出错误的序列号。这导致重新组装的H.264帧包含丢失的或乱序的无意义的片段。
所以我解决了这个问题。
可用的视频!足以驱动机器人。
疯狂的SFU
是时候进行一些改进了。
如果不止一个用户可以观看给定的摄像机就好了 - 例如本地飞行员和远程观察者。通常浏览器只是打开一个新的相机实例,并假设操作系统会做正确的事情。此时我看到的平台是Raspberry Pi Zero。它有一个硬件H.264编码器,一次只能创建一个编码流。
所以我编写了一些代码,它接受一个入站数据包并通过多个WebRTC连接发送给多个查看器。
这是可行的,但是一个新的连接器在新的关键帧到达之前不会看到任何视频(可能是几秒钟)。所以我和一些真正的WebRTC专家讨论了这个问题 ,他们帮助我理解到此时我正在编写一些看起来像是疯狂的SFU。他们说真正的SFU会隐藏最新的关键帧,然后将它播放给一个新的连接器,以便他们立即获得一些视频。
例如,以下是Meetecho / Janus所做的:
代码语言:javascript复制/* H.264 depay */
int jump = 0;
uint8_t fragment = * buffer & 0x1F;
uint8_t nal = * (buffer 1) & 0x1F;
uint8_t start_bit = * (buffer 1) & 0x80;
if (fragment == 28 || fragment == 29)
JANUS_LOG(LOG_HUGE, "Fragment=%d, NAL=%d, Start=%d (len=%d, frameLen=%d)n", fragment, nal, start_bit, len, frameLen);
else
JANUS_LOG(LOG_HUGE, "Fragment=%d (len=%d, frameLen=%d)n", fragment, len, frameLen);
if (fragment == 5 ||
((fragment == 28 || fragment == 29) && nal == 5 && start_bit == 128)) {
JANUS_LOG(LOG_VERB, "(seq=%"
SCNu16 ", ts=%"
SCNu64 ") Key framen", tmp - > seq, tmp - > ts);
keyFrame = 1;
/* Is this the first keyframe we find? */
if (!keyframe_found) {
keyframe_found = TRUE;
JANUS_LOG(LOG_INFO, "First keyframe: %"
SCNu64 "n", tmp - > ts - list - > ts);
}
}
我实现了这个,它运行得很好。
最后的改进是响应Chrome在认为丢失或损坏关键帧时发送的一些RTCP消息。我用它来触发发送一个旧的(缓存的)关键帧。
考虑到Raspberry Pi上硬件编码器的限制,这是我能做的最好的事情了,虽然我仍然需要了解一些更奇怪的可选RTCP扩展,以便我可以要求编码器做一些事情,如重新生成帧等
一个可移植的,轻量级H.264 WebRTC堆栈
因此,现在我们有了一个可移植的,轻量级的WebRTC堆栈,它可以将H.264视频(和音频)从piZero的摄像机发送到多个WebRTC浏览器收件人。这是我多年来一直想要做的事情。该堆栈的大部分是开源的(参见上面的链接) - 但是身份验证和编排部分不是开源的源代码。您可以在https://github.com/pipe/webcam上试验这些成果 。
收获
- 不要仅仅因为您的SRTP堆栈携带了大量音频,就认为它适用于视频,视频是不同的。
- 在开始之前阅读相关的RFC!
- RTP不适合视频 - 特别是在有损介质上。这与音频形成鲜明对比,在Opus编解码器中,编解码器的前向纠错将掩盖单个数据包的丢失。不会出现任何故障,后续数据包也不会受到影响。
- 丢弃单个H.264视频数据包意味着整个帧(最多10个数据包)不可用并将导致可见的假像。
- 从关键帧中删除单个数据包意味着视频将会停止,直到重新发送帧或新帧到达为止。