分析完应用上层的视频采集、渲染、编码之后,原本我是打算把完整的 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 格式了;ObjCVideoEncoder
在RegisterEncodeCompleteCallback
函数里为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 函数中建立;ObjCVideoTrackSource
是 AdaptedVideoTrackSource
的子类,所以它俩对应同一个对象。
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 是
VideoTrack
(RTCVideoTrack
的 native 层对象),创建RTCVideoTrack
时,我们传入了RTCVideoSource
对象,但数据并不会流经RTCVideoTrack
,RTCVideoTrack
只是充当一个牵线搭桥的角色; - track 的
video_source_
是AdaptedVideoTrackSource
,此外 track 也实现了VideoSourceInterface
接口,因此可以添加 sink,但 sink 实际上是添加给了 source,因此数据就会从 source 流经 sink 了,牵线搭桥正是在此处;
- track 和 sender 的关联,通过
PeerConnection::SetLocalDescription
:把 track 交给 channel,但 channel 把 track 当 source 用;PeerConnection::setLocalDescription
->PeerConnection::ApplyLocalDescription
的transceiver->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::VideoSourceProxy
的SetSource
,并在其中,把video_stream_encoder_
作为 sink 添加到了 source 里;
那么我们终于把 VideoStreamEncoder 交给 AdaptedVideoTrackSource 了,也就是添加到 VideoBroadcaster 中了,于是视频数据就能流到 encoder 了。
VideoStreamEncoder => RTCVideoEncoderH264:
VideoStreamEncoder 开始接收视频数据后,会在 VideoStreamEncoder::ReconfigureEncoder
里创建编码器。
- 调用
ObjCVideoEncoderFactory::CreateVideoEncoder
创建编码器,创建出来的是包装了RTCVideoEncoderH264
的ObjCVideoEncoder
,之后收到帧后,就交给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。