关于H.264我学到了什么(Tim Panton)

距离WebRTC编码战缓和结束已经有几年了。H.264已经存在了超过15年,因此很容易掩盖使它工作的错综复杂的问题。

Tim Panton正在进行一个无人机项目,他需要一个轻量级的H.264栈供WebRTC使用,因此他决定建造一个。这当然不是我最推荐的做法,但是Tim表示这可能是一个启发性的试验。在本文中,Tim一步一步向我们介绍他使视频工作的步骤。你还可以通过阅读介绍H.264的RFC规范来得到一种有趣的替代方案。

自从WebRTC和VoIP出现开始我就在研究它们,所以我有自己的关于RTP和实时媒体的处理方法。否则我想…

在那段时间里,我从未真正看过视频技术。手头上总有其它人早先做过,所以我还是无知,花时间学习SCTP还有WebRTC数据频道。

接着出现了一个项目,从无人机发送H.264视频。它会有多难?

为什么使用H.264而不是VP8?

这就是无人机产生的问题。转码到VP8远远超出了我的硬件能力。由于我是其中一个工作人员,促成了在WebRTC中支持H.264和VP8的折中方案,并指出,我们应该利用H.264解码器的优势。

所以我开始关注开源srtplight库,将它插入了WebRTC栈。我写了一个类,它可以读取RTP数据包,使用DTLS-SRTP加密,接着在ICE选择的路径上传输。我知道ICE/DTLS-SRTP位工作正常,因为我已经使用它们从WebRTC门铃PoC提供音频。

为什么使用Java?

构建100万行libWebRTC需要20GB,这一点使我脱离了使用C/C++的路线。如果是小项目的话,我会考虑C/C++;

除此之外,Java提供了我需要的所有功能。实际上,Java是一个好的选择,这也是OAK被发明的原因。

JVM使其在许多架构上都具有可移植性和高性能。ARM中,DTLS-SRTP中使用的加密,被直接映射到硬件的加速指令,意味着即使是最小的树莓派也可以加密多条视频流。

多线程对于这个网络任务是理想的。

最后一点,JVM的内存管理和编译器的强类型检查意味着我的代码相对不受缓冲区溢出和来自入站数据包的其它内存攻击的影响。

使视频工作

首先,我使SDP能够提供/应答工作。这花费了一段时间,但是最终Chrome接受了我的SDP并显示数据到达。

尽管没有视频。

我向下深挖,发现数据包比我想象的要小一些。

更多的挖掘表明我将RTP类上的缓冲区保持的很小,因为这些类最初被设计用于受限环境。足够大的20ms G.711数据包,但是,比最大传输单元小得多,所以我们的H.264数据包被截断了,我在这里修改了srtplight。

还是没有出现视频。

在Chrome的chrome:// webrtc-internals页面,我获得了大量字节,但是没有一个解码帧。

我有一个问题,这可能是因为我没有回复Chrome发送的RTCP数据包。

RTCP用于控制RTP媒体频道和报告统计信息。Chrome还使用RTCP扩展来估算可用带宽。由于这是浏览器的单向视频,我认为不需要RTCP。

我写了一些RTCP类加入了SRTP实现中。

仍然没有视频。

通过Wireshark逆向工程H.264

标记位

我启动了Wireshark并捕获了输入和输出数据包以试图查看错误。连续盯着屏幕几个小时之后,我终于注意到了,标记为在某些输入数据包上设置,但不在任何输出数据包上设置。

在这一点上,我应该已经开始阅读关于H.264分组化的RFC(特别是5.1章节)。这本来可以为我节省很多时间,但是我没有这样做。我确实记得DTMF使用标记为来表示这一组DTMF数据包的结束。

调整了代码以确保标记为从内到外忠实的进行。

好极了,视频。有时候,会产生一到两帧,接着就什么都没有了。

 时间戳

回到Wireshark。我再次比较了输入输出数据包。我注意到输入数据包的时间戳已经分组。5到10个包具有相同的时间戳,最后一个是标记位。输出的有当前发送时间的时间戳。换句话说,它们会增加。

如果我阅读了RFC我就该知道…

所以:H.264(或任何视频编解码器)创建的帧比UDP网络的MTU大得多。因此,RTP打包器将帧拆分为数据包,并为与帧相关联的所有数据包提供相同的时间戳,但序列号会递增,使用标记位标记最后一个。

FU MTU

你可能想知道为什么编码器不仅发送大于MTU的数据包而且让IP级别处理碎片。当我最终阅读RFC时,我发现以下有关碎片单元(FU)的部分:

我最初写的是srtplight代码,用于从本地麦克风发送音频。它在输出数据包上生成了自己的时间戳。

所以我修复了它,忠实的将时间戳从里面复制到外面。

更多视频,更好的视频,几乎可用的视频。

关键帧

我查看了到达接收端的序列号,看看是否有丢弃的数据包。WebRTC内部和Wireshark并没有,但视频讲述了一个不同的故事。

在这一点上,我走下了一系列H.264编码器模式,发现更频繁的发送关键帧会恢复陷入停滞的视频。

与音频编解码器不同,并非所有帧都与视频同等重要。大多数帧仅描述图像中的差异,除非所有先前的帧都已被解码,否则这些差异无法呈现。例外情况是关键帧,它们包括完整的图像和功能,作为后序数据包构建的基础。对于H.264实际上做的事,这是一个令人难以置信的粗略过度简化,但从数据包的角度来看,它会做到这一点。因此,获得一个关键帧可以让困惑的解码器重新开始。

这并没有解释为什么它首先被混淆了。再看看Wireshark,我意识到有些帧在输入端丢失了数据包,即使在输出时没有,我记得srtplight默认创建的序列号没有任何意义。因此,如果来自无人机的输入数据包被丢弃或未命令,srtplight将会从那时发出错误的序列号。这导致重新组装的H.264帧或者片段丢失,或者片段顺序错误。

修复了它

嘿!视频-可用视频!足以驾驭机器人。

疯狂的SFU

是时候进行一些改进了。

如果不止一个用户可以观看给定的摄像机,例如本地飞行员和远程观察者。通常浏览器只是打开一个新的相机实例,并假设操作系统会作出正确的事情。我在这一点上看到的平台是树莓派零。它有一个硬件H.264编码器,它一次只能创建一个编码流。

所以我写了一些代码,它接收一个输入数据包并通过多个WebRTC连接发送给多个viewers。

这工作正常,但是一个新的连接器在新的关键帧到达之前不会看到任何视频。所以我和一些真正的WebRTC大师讨论了这个问题,他们帮我理解,到目前为止,我正在写一些看起来像疯狂的SFU的东西。他们说一个真正的SFU会藏匿最新的关键帧,然后将它播放到一个新的参与者,以便他们立即获得一些视频。

例如,这是Meetecho/Janus所做的

/* 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 frame\n", 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消息。我用它来触发发送一个旧的(缓存的)关键帧。  考虑到树莓派上硬件编码器的限制,这是我能做的最好的,虽然我还需要了解一些更奇怪的可选RTCP扩展,这样我就可以要求编码器执行重新生成帧之类的操作。

便携式轻量级H.264 WebRTC堆栈

现在我们有一个便携,轻量级的WebRTC堆栈,可以将pitrero的摄像机发送H.264视频到多个WebRTC浏览器的接收者。这是我多年来想做的事。堆栈大部分是开源的,但是身份验证和编排部分是闭源代码。你可以在https://github.com/pipe/webcam上试验这些成果。

 

原文标题:What I learned about H.264 for WebRTC video
作者:“chad hart

填写常用邮箱,接收社区更新

WebRTC 中文社区由

运营