WebRTC Native 源码导读(十):视频数据 native 层之旅

Posted by Piasy on May 24, 2018
本文是 Piasy 原创,发表于 https://blog.piasy.com,请阅读原文支持原创 https://blog.piasy.com/2018/05/24/WebRTC-Video-Native-Journey/

分析完应用上层的视频采集、渲染、编码之后,原本我是打算把完整的 WebRTC 带到 Flutter 的世界里,形成 FlutterRTC 的,但后来仔细想想,这件事没多大意思,做出来了也不能产生多大价值,所以我决定调头深入底层。

本篇算是真正深入底层的第一篇,让我们深究一下之前没有深究的话题:视频数据 native 层之旅,以及 WebRTC 对视频数据的处理。最近对 iOS 上层的分析也不算白费,毕竟在 iOS 平台深入底层,无论是编译还是调试都更方便。

本文的分析基于 WebRTC 的 #23295 提交;部分内容更新于 #28513 提交

基本概念

扎到代码里之前,让我们先理清一些概念,下面这些内容都是总结自 WebRTC 标准

PC stream track source sink

一个 track 是一个音频或视频数据流(发送或接收)。它有输入和输出,对于要发送的 track 来说,输入是本地的采集,输出是远端,对于要接收的 track 来说,输入是远端,输出是本地的渲染。

source 则是数据源,可以作为 track 的输入,它的消费者叫 sink,一个 source 可以有多个 sink。给 track 加 sink 实际上都是给 track 的 source 加 sink。

发送方发送的一个 track,在接收端也一定表现为一个 track。一个 track 可以归属于一个或多个 stream,通过 stream id 区分,如果接收端尚不存在对应 stream,则会被创建。

加一个 stream 是为了更灵活的控制本地和远端的 track 组合,比如本地有音视频,但只发送音频。其实直接指定发送哪些 track 也能达到这个效果,但通过 stream 这个概念整合一下,会更清晰一些,况且总得有个数据结构容纳多个 track,那就索性给他一个名字(stream)吧。

PC 是 PeerConnection 的简称,它表示了终端之间的 P2P 连接,用来传输数据。PC 是 WebRTC 的门面,我们直接使用的都是 PC 的接口。

总结下这四个概念的关系:PC - 一到多个 stream - 零到多个 track - 一个 source。

sender receiver transceiver

sender 负责编码、发送,receiver 负责接收、解码,一个 sender 至多有一个要发送的 track,一个要接收的 track 刚好有一个 receiver。规范规定 transceiver 包含一个 sender 和一个 receiver,它们拥有相同的 mid,WebRTC 实现的 unified plan 遵循了这一规定。一个 PC 有多个 transceiver。

addTrack 时会为其分配 transceiver/sender(新建或复用已有 transceiver/sender),也可以通过 addTransceiver 来建立 track - sender/receiver - transceiver 的关联。

发送端视频数据的 native 层之旅

前面我们已经知道,在 AppRTCMobile/ARDAppClient.m startSignalingIfReady 函数中,我们会创建 RTCPeerConnection 对象,并创建 source, track, stream。

根据上面理清的 source track stream 之间的关系,我们可以推断,视频数据会从 capturer 到 source,再到 sender(track),而 sender 里又包括编码和发送。那接下来我们就利用 iOS 系统的优势,一「栈」到底,直捣黄龙。

capturer => source => encoder

  • RTCVideoSource 实现了 RTCVideoCapturerDelegate,所以视频数据从 RTCCameraVideoCapturer 送到了 RTCVideoSource
  • 紧接着数据被送到了 ObjCVideoTrackSource,它继承自 AdaptedVideoTrackSource,其主要责任是对视频数据做「适配」,包括裁剪缩放,旋转,甚至丢弃操作;
  • 此外,它还负责做一个时间戳的「翻译」工作:相机采集本身会提供一个时间戳,这个时间戳比系统时钟更精确(步调更稳),但它是相对时间,而后面实际使用的时间戳都是 unix 时间戳,所以需要「翻译」;这件「翻译」工作实际由 rtc_base/timestampaligner.h 完成,它背后还有一个数学模型,感兴趣的朋友可以仔细查看代码、注释、测例;
  • 经过上面的处理后,视频数据已经由 RTCVideoFrame 变成了 rtc::VideoFrame,并交给了 AdaptedVideoTrackSource,由此正式开始 native 层之旅;之后如果视频需要旋转,那就做一个旋转操作(其他平台里此前可能未做旋转处理),然后交给 VideoBroadcaster
  • 顾名思义,VideoBroadcaster 负责把视频数据“广播”出去(交给它的每个 sink),这里 broadcaster 还会检查 sink 对视频方向的需求,如果不匹配则丢弃数据,如果需要黑屏数据则传黑屏数据;另外,AdaptedVideoTrackSource 作为一个 source,可以添加 sink,但其实最终都是添加到了 VideoBroadcaster 里;
  • iOS 里只有一个 sink,那就是 VideoStreamEncoder,如果视频时间戳超过了当前时间,它会将其重置为当前时间,然后它会统计采集帧数、丢弃帧数(编码过慢导致),并定期输出日志(60s 一次),最后如果视频编码速度跟得上,就会把数据交给编码器,当然丢帧或编码的操作都是在编码线程(encoder_queue_)完成的;(VideoStreamEncoder::OnFrame
  • 开始编码之前我们要先启动编码,此外,如果视频尺寸发生变化,或者数据类型发生变化(是否 texture),那我们都要重启编码;然后会根据帧率、分辨率和 buffer 类型决定是否需要丢弃视频帧,如果确定要编码,则进入下一步;(VideoStreamEncoder::MaybeEncodeVideoFrame
  • 在进行最后一次裁剪缩放检查后(裁剪、缩放、旋转操作可能发生的位置确实有点多……),把帧交给 ObjCVideoEncoder,并最终交到了 RTCVideoEncoderH264 的手里;(VideoStreamEncoder::EncodeVideoFrame

这里简单小结一下:

  • 视频数据采集到之后,会在采集线程做裁剪缩放、旋转、黑屏、丢弃等处理;
  • 随后视频数据提交到编码线程,如果不丢帧,就进行编码;

encoder => sender

  • RTCVideoEncoderH264 在收到系统的编码完成回调后,除了转换 NALU 存储格式外,还会顺带产生每帧视频数据的所有 RTP header,每个 NALU 作为一个 RTP fragment;解码器实际使用的大多是 Annex-B 格式,但中途处理时 AVCC 更方便(不用统计 unit 长度),把 H.264 数据封装进 RTP 报文之后,我们可以从包头里得知 unit 大小,这样就可以让数据保持 Annex-B 格式了;
  • ObjCVideoEncoderRegisterEncodeCompleteCallback 函数里为 RTCVideoEncoderH264 设置了编码回调,它收到编码后的数据之后,会把 ObjC 的数据结构转换为 C++ 的数据结构,然后交给 VideoStreamEncoder
  • VideoStreamEncoder 会把传入的 EncodedImage 拷贝一份,因为传入的是 const &,而这里却需要对这个结构内容做修改(为什么不传普通引用呢,是有点作……),然后填充 timing info, experiment id, simulcast id,此外还会再调用 media_optimization::MediaOptimization 的相关接口,应该是做帧率限制用的,但我并未在项目中搜到对 drop_next_frame 的使用
  • 好了,接收视频数据并发起编码的是 VideoStreamEncoder,接收编码完成回调的也是它,之后就该要封装 RTP 报文发送了,这个过程不涉及视频数据的处理,以后再做展开;

数据 pipeline 建立过程

上面我们了解了视频数据的流动过程,但这个数据流的 pipeline 是怎么建立起来的呢?现在就来揭晓。

RTCCameraVideoCapturer => RTCVideoSource:

ARDAppClient createLocalVideoTrack 函数中,我们把 RTCVideoSource 作为 RTCCameraVideoCapturer 的 delegate,用来构造 capturer。

RTCVideoSource => ObjCVideoTrackSource:

RTCVideoSource 的 native source 就是 ObjCVideoTrackSource,这一关联在前者的 init 函数中建立;ObjCVideoTrackSourceAdaptedVideoTrackSource 的子类,所以它俩对应同一个对象。

AdaptedVideoTrackSource => VideoBroadcaster:

VideoBroadcaster 是 AdaptedVideoTrackSource 自己构造的成员变量,它的作用就是把视频数据分发给多个 sink,给 AdaptedVideoTrackSource 添加 sink,实际上是给 VideoBroadcaster 添加 sink。

在构造出 RTCVideoSource 和 RTCCameraVideoCapturer 后,我们就完成了上述对象的关联,数据从 iOS 系统 -> RTCCameraVideoCapturer -> RTCVideoSource -> ObjCVideoTrackSource -> AdaptedVideoTrackSource -> VideoBroadcaster 的通路就建立起来了。

VideoBroadcaster => VideoStreamEncoder:

  • PeerConnection::AddTrack:创建 sender, receiver, transceiver, 关联 track 和 sender;
    • track 和 sender 的关联,通过 transceiver->sender()->SetTrack(track)CreateSender(media_type, sender_id, track, stream_ids, {}) 完成;
    • track 是 VideoTrackRTCVideoTrack 的 native 层对象),创建 RTCVideoTrack 时,我们传入了 RTCVideoSource 对象,但数据并不会流经 RTCVideoTrackRTCVideoTrack 只是充当一个牵线搭桥的角色;
    • track 的 video_source_AdaptedVideoTrackSource,此外 track 也实现了 VideoSourceInterface 接口,因此可以添加 sink,但 sink 实际上是添加给了 source,因此数据就会从 source 流经 sink 了,牵线搭桥正是在此处;
  • PeerConnection::SetLocalDescription:把 track 交给 channel,但 channel 把 track 当 source 用;
    • PeerConnection::setLocalDescription ->
    • PeerConnection::ApplyLocalDescriptiontransceiver->internal()->sender_internal()->SetSsrc ->
    • RtpSenderBase::SetSsrc ->
    • VideoRtpSender::SetSend ->
    • WebRtcVideoChannel::SetVideoSend ->
    • WebRtcVideoChannel::WebRtcVideoSendStream::SetVideoSend,channel 把 source 存了起来,但此时 channel 的 stream(WebRtcVideoSendStream)还没有创建,需要等 SetRemoteDescription 才会创建;
  • PeerConnection::SetRemoteDescription:关联 stream 和 source;
    • PeerConnection::ApplyRemoteDescription ->
    • PeerConnection::UpdateSessionState ->
    • PeerConnection::PushdownMediaDescription ->
    • BaseChannel::SetRemoteContent ->
    • VideoChannel::SetRemoteContent_w ->
    • WebRtcVideoChannel::SetSendParameters ->
    • WebRtcVideoChannel::ApplyChangedParams ->
    • WebRtcVideoChannel::WebRtcVideoSendStream::SetSendParameters ->
    • WebRtcVideoChannel::WebRtcVideoSendStream::SetCodec ->
    • WebRtcVideoChannel::WebRtcVideoSendStream::RecreateWebRtcStream ->
    • VideoSendStream::SetSource ->
    • VideoStreamEncoder::SetSource ->
    • VideoStreamEncoder::VideoSourceProxySetSource,并在其中,把 video_stream_encoder_ 作为 sink 添加到了 source 里;

那么我们终于把 VideoStreamEncoder 交给 AdaptedVideoTrackSource 了,也就是添加到 VideoBroadcaster 中了,于是视频数据就能流到 encoder 了。

VideoStreamEncoder => RTCVideoEncoderH264:

VideoStreamEncoder 开始接收视频数据后,会在 VideoStreamEncoder::ReconfigureEncoder 里创建编码器。

  • 调用 ObjCVideoEncoderFactory::CreateVideoEncoder 创建编码器,创建出来的是包装了 RTCVideoEncoderH264ObjCVideoEncoder,之后收到帧后,就交给 RTCVideoEncoderH264 编码了;
  • 调用 InitEncode 初始化编码,之后调用 ObjCVideoEncoder::RegisterEncodeCompleteCallback 把自己注册给 ObjCVideoEncoder,所以编码完成后,数据也就会回调给 VideoStreamEncoder 了;

好了,到这里我们就搞清楚了发送端视频数据的流动过程,以及这个数据通道的建立过程了,确实不容易,让我们先歇一口气 :)


接收端视频数据的 native 层之旅

接收端的视频数据处理就很简单了,就是解码、渲染。在这里我们跳过 RTP 报文处理的逻辑,只看解码和渲染的过程。

待解码数据送入解码器:

从 VideoReceiveStream 到 VideoReceiver,再到 VCMGenericDecoder,最后到 ObjCVideoDecoder 和 RTCVideoDecoderH264,都没有视频数据的处理,只是简单的透传、解码,以及打上时间戳(VideoReceiveStream, VCMGenericDecoder)。

这条路径上的各个类名,我们都可以在发送端找到对应的类名:

  • VideoReceiveStream - VideoSendStream
  • VideoReceiver - VideoSender
  • VCMGenericDecoder - VCMGenericEncoder
  • ObjCVideoDecoder - ObjCVideoEncoder
  • RTCVideoDecoderH264 - RTCVideoEecoderH264

解码后数据进行渲染:

在这个过程中最主要的逻辑就是时间戳的处理,限于篇幅我们这里不做展开,留待以后探究。

另外需要注意的是,发送端可能会选择不对视频做旋转操作,而是把旋转角度发过来,那么接收端就需要在渲染的时候进行旋转了。

跨平台视频处理模块的架构

通过这次的源码分析,我们可以看到 WebRTC 是如何设计 VCM (Video Coding Module) 这个跨平台视频处理模块的结构的。

首先这个模块包括采集、编码、jitter buffer、解码、渲染等功能,还有它们的线程、队列管理,其中采集、(硬件)编解码、渲染都是平台相关的,其他模块以及对平台相关模块的调用和管理,都是平台无关的。

那我们就通过接口 + 回调的形式,将平台相关的模块抽离出去,对它们的操作,VCM 就调用接口,而它们需要把数据交给 VCM,或者需要调用 VCM 的功能时,就通过回调的形式把控制权还给 VCM。

至于怎么把它们关联起来,就需要在构造 VCM 的时候,把接口实现注入进去了,或者利用工厂模式,把 factory 注入进去,VCM 调用 factory 来创建接口的平台相关实现,显然 WebRTC 是用了工厂模式。

这个套路其实我在移动客户端跨平台开发方案探索中提到过,看,WebRTC 也是这样的套路 :)

总结

好了,WebRTC 真正 native 层的初体验到这里就结束了,其实并没有涵盖多少内容,主要是都要展开内容就太多了,别急,我们一步一步来。

另外,在这次的分析过程中,我还梳理了一下 WebRTC 一些核心类的关系,以及音视频数据、DataChannel 的流程,画了一张超大的 svg,也算是为其他模块的分析打下了一个底子,欢迎大家 star 支持 :)

附录:部分类实现代码路径

sdk/objc/Framework/Classes/PeerConnection/RTCVideoSource.mm
sdk/objc/Framework/Native/src/objc_video_track_source.mm
media/base/adaptedvideotracksource.cc
media/base/videobroadcaster.cc
media/engine/webrtcvideoengine.cc
call/video_send_stream.cc
video/video_stream_encoder.cc
modules/video_coding/video_coding_impl.cc
modules/video_coding/video_sender.cc
modules/video_coding/generic_encoder.cc

欢迎大家加入 Hack WebRTC 星球,和我一起钻研 WebRTC。

piasy-knowladge-planet