2018-06-07 18:02 出处:清屏网 人气:192 评论(0)
http://www.qingpingshan.com/rjbc/ios/388455.html
分析完应用上层的视频采集、渲染、编码之后,原本我是打算把完整的 WebRTC 带到 Flutter 的世界里,形成 FlutterRTC 的,但后来仔细想想,这件事没多大意思,做出来了也不能产生多大价值,所以我决定调头深入底层。
本篇算是真正深入底层的第一篇,让我们深究一下之前没有深究的话题:视频数据 native 层之旅,以及 WebRTC 对视频数据的处理。最近对 iOS 上层的分析也不算白费,毕竟在 iOS 平台深入底层,无论是编译还是调试都更方便。
扎到代码里之前,让我们先理清一些概念,下面这些内容都是总结自 WebRTC 标准 。
一个 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 负责接收、解码,一个 sender 至多有一个要发送的 track,一个要接收的 track 刚好有一个 receiver。规范规定 transceiver 包含一个 sender 和一个 receiver,它们拥有相同的 mid,WebRTC 实现的 unified plan 遵循了这一规定。一个 PC 有多个 transceiver。
addTrack 时会为其分配 transceiver/sender(新建或复用已有 transceiver/sender),也可以通过 addTransceiver 来建立 track - sender/receiver - transceiver 的关联。
前面我们已经知道,在 AppRTCMobile/ARDAppClient.m
startSignalingIfReady
函数中,我们会创建 RTCPeerConnection
对象,并创建 source, track, stream。
根据上面理清的 source track stream 之间的关系,我们可以推断,视频数据会从 capturer 到 source,再到 sender(track),而 sender 里又包括编码和发送。那接下来我们就利用 iOS 系统的优势,一「栈」到底,直捣黄龙。
RTCVideoSource
实现了 RTCVideoCapturerDelegate
,所以视频数据从 RTCCameraVideoCapturer
送到了 RTCVideoSource
;ObjCVideoTrackSource
,它继承自 AdaptedVideoTrackSource
,其主要责任是对视频数据做「适配」,包括裁剪缩放,旋转,甚至丢弃操作;rtc_base/timestampaligner.h
完成,它背后还有一个数学模型,感兴趣的朋友可以仔细查看代码、注释、测例;RTCVideoFrame
变成了 rtc::VideoFrame
,并交给了 AdaptedVideoTrackSource
,由此正式开始 native 层之旅;之后如果视频需要旋转,那就做一个旋转操作(其他平台里此前可能未做旋转处理),然后交给 VideoBroadcaster
;VideoBroadcaster
负责把视频数据“广播”出去(交给它的每个 sink),这里 broadcaster 还会检查 sink 对视频方向的需求,如果不匹配则丢弃数据,如果需要黑屏数据则传黑屏数据;另外, AdaptedVideoTrackSource
作为一个 source,可以添加 sink,但其实最终都是添加到了 VideoBroadcaster
里;VideoStreamEncoder
,如果视频时间戳超过了当前时间,它会将其重置为当前时间,然后它会统计采集帧数、丢弃帧数(编码过慢导致),并定期输出日志(60s 一次),最后如果视频编码速度跟得上,就会把数据交给编码器,当然丢帧或编码的操作都是在编码线程( encoder_queue_
)完成的;( VideoStreamEncoder::OnFrame
)VideoStreamEncoder
支持每帧数据编码之前,先执行一个回调;开始编码之前我们要先启动编码,此外,如果视频尺寸发生变化,或者数据类型发生变化(是否 texture),那我们都要重启编码;最后如果视频数据量没有超标,编码器也没有暂停,就进入下一环节;( VideoStreamEncoder::MaybeEncodeVideoFrame
)VideoSender
;( VideoStreamEncoder::EncodeVideoFrame
)VideoSender
会根据帧率、分辨率和 buffer 类型决定是否需要丢弃视频帧,如果确定要编码,就再交给 VCMGenericEncoder
;VCMGenericEncoder
检查了帧类型,并发出编码前回调之后( 回调也有点多…… ),就把数据交给了 ObjCVideoEncoder
,并最终交到了 RTCVideoEncoderH264
的手里;这里简单小结一下:
RTCVideoEncoderH264
在收到系统的编码完成回调后,除了转换 NALU 存储格式外,还会顺带产生每帧视频数据的所有 RTP header,每个 NALU 作为一个 RTP fragment;解码器实际使用的大多是 Annex-B 格式,但中途处理时 AVCC 更方便(不用统计 unit 长度),把 H.264 数据封装进 RTP 报文之后,我们可以从包头里得知 unit 大小,这样就可以让数据保持 Annex-B 格式了;ObjCVideoEncoder
在 RegisterEncodeCompleteCallback
函数里为 RTCVideoEncoderH264
设置了编码回调,它收到编码后的数据之后,会把 ObjC 的数据结构转换为 C++ 的数据结构,然后交给 VCMEncodedFrameCallback
;VCMEncodedFrameCallback
是 VCMGenericEncoder
设置的编码回调,它会把传入的 EncodedImage
拷贝一份,因为传入的是 const &
,而这里却需要对这个结构内容做修改( 为什么不传普通引用呢,是有点作…… ),然后填充 timing info, experiment id, simulcast id,最后交给 VideoStreamEncoder
;交出数据之后,会再调用 media_optimization::MediaOptimization
的相关接口, 应该是做帧率限制用的,但我并未在项目中搜到对 drop_next_frame
的使用 ;VideoStreamEncoder
,接收编码完成回调的也是它,之后就该要封装 RTP 报文发送了,这个过程不涉及视频数据的处理,以后再做展开;上面我们了解了视频数据的流动过程,但这个数据流的 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。
VideoBroadcaster => VideoStreamEncoder:
PeerConnection::AddTrack
:创建 sender, receiver, transceiver,并关联 track 和 sender;
VideoTrack
,它的 video_source_
是 AdaptedVideoTrackSource
,此外它也实现了 VideoSourceInterface
接口;PeerConnection::setLocalDescription
:在 VideoRtpSender::SetVideoSend
里关联 media_channel_
和 track_
,不过 media_channel_
把 track 当做 VideoSourceInterface
使用;
WebRtcVideoChannel
把 source 交给了它的 send_streams_
,即 WebRtcVideoSendStream
;PeerConnection::SetRemoteDescription
:经由 VidelChannel(BaseChannel)、WebRtcVideoChannel(MediaChannel)、WebRtcVideoSendStream,创建 VideoSendStream,并关联 stream 和 source,最后给 source 绑定了 sink(VideoSendStream 的 VideoStreamEncoder);
VideoSourceInterface::AddOrUpdateSink
的调用发生在 worker_thread_
;source_
就是 setLocalDescription
里被绑定的 VideoTrack,给它添加 sink,实际上是给它的 source 添加 sink;那么我们终于把 VideoStreamEncoder 交给 AdaptedVideoTrackSource 了。
VideoStreamEncoder => VideoSender:
VideoSender 是 VideoStreamEncoder 自己构造的成员变量,构造 VideoSender 时,VideoStreamEncoder 还把自己作为编码数据的回调注册给了 VideoSender。
VideoSender => VCMGenericEncoder => ObjCVideoEncoder => RTCVideoEncoderH264:
VideoStreamEncoder 开始接收视频数据后,会在 VideoStreamEncoder::ReconfigureEncoder
里创建编码器。
ObjCVideoEncoderFactory::CreateVideoEncoder
创建编码器,创建出来的是包装了 RTCVideoEncoderH264
的 ObjCVideoEncoder
;VideoSender::RegisterExternalEncoder
把 ObjCVideoEncoder 注册给 VideoSender,进而注册给 VCMEncoderDataBase;VideoSender::RegisterSendCodec
,其中会调用 VCMEncoderDataBase::SetSendCodec
把 VideoSender 包装成 VCMGenericEncoder,并调用 VCMEncoderDataBase::GetEncoder
将其取出保存;那么我们终于建立起了完整的编码数据通道了,不过还不能高兴得太早,编码后数据抛出的通道是怎么建立的?让我们再接再厉。
RTCVideoEncoderH264 => VCMEncodedFrameCallback => VideoStreamEncoder:
在 VCMEncoderDataBase::SetSendCodec
中,我们会调用 InitEncode
初始化编码,之后就会注册编码回调。
ObjCVideoEncoder::RegisterEncodeCompleteCallback
里,我们调用 RTCVideoEncoderH264 setCallback
注册回调,这样编码数据就会被交给 ObjCVideoEncoder,进而交给 VCMEncodedFrameCallback;好了,到这里我们就搞清楚了发送端视频数据的流动过程,以及这个数据通道的建立过程了,确实不容易,让我们先歇一口气 :)
接收端的视频数据处理就很简单了,就是解码、渲染。在这里我们跳过 RTP 报文处理的逻辑,只看解码和渲染的过程。
待解码数据送入解码器:
从 VideoReceiveStream 到 VideoRecenver,再到 VCMGenericDecoder,最后到 ObjCVideoDecoder 和 RTCVideoDecoderH264,都没有视频数据的处理,只是简单的透传、解码,以及打上时间戳(VideoReceiveStream, VCMGenericDecoder)。
这条路径上的各个类名,我们都可以在发送端找到对应的类名:
解码后数据进行渲染:
在这个过程中最主要的逻辑就是时间戳的处理,限于篇幅我们这里不做展开,留待以后探究。
另外需要注意的是,发送端可能会选择不对视频做旋转操作,而是把旋转角度发过来,那么接收端就需要在渲染的时候进行旋转了。
通过这次的源码分析,我们可以看到 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