本文来自《WebRTC Native开发实战》书籍作者许建林在LiveVideoStack线上分享中的内容,详细分析总结 WebRTC 的视频数据流程,并对大型项目如何快速上手:分析方法,如何在工作中按需进行定制开发或调试分析问题等内容进行了深入解读。
文 / 许建林
整理 / LiveVideoStack
直播回放:https://www.livevideostack.cn/video/online-piasy/
大家好,非常高兴能有机会和大家进行分享。我叫许建林,英文名Piasy,目前在PowerInfo从事高稳定、低延迟的音视频实时传输技术的研发工作。到目前为止已经有五年的工作经验,前两年主要是做视频直播的Android/ target=_blank class=infotextkey>安卓App开发,之后的三年在PowerInfo一直在做RTC SDK的开发。我是一个开源爱好者,在GitHub也有发过许多的开源项目,非常欢迎大家关注一起交流。
本次分享的主题是WebRTC视频流数据流程分析,主要内容可以分为以下几个部分:
- WebRTC 代码库简介
- 分析方法
- 视频流程介绍
- 实战:客户端视频录制
WebRTC 代码库简介
1.1 WebRTC简单介绍
关于什么是WebRTC,如何用一两句话简单说明?
WebRTC是一个Web端 RTC的互联网标准,同时我们也会用WebRTC来指代一个开源项目,是目前完成度最高、最流行的RTC框架,是由google开源的项目。
1.2 WebRTC版本说明
上图展示的是Chromium项目网站上公开的版本发布规划表,图中Milestone 81是Milestone的编号,简写为M81。表中其它列如Chromium、Skia以及WebRTC等都是其对应的版本分支,例如Skia有同名的m81分支,之前WebRTC也是有同名的m75、m76分支,只不过后来改变了分支的命名方式。
在《WebRTC Native开发实战》中有提到过书中对于WebRTC的代码分析是基于某次提交版本,例如“#30432提交”。如图所示为WebRTC的Git提交记录图,蓝色箭头所指位置:Cr-Commit-Position:refs/head/master@{#30432},“#30432”的含义是指该项目从开始到现在第几次提交。WebRTC的代码库有一个特点,其主干分支是一条直线,无其它分支(当然在发布新版本时,会开出发布分支,可能会同步一些需要带上去的提交,但是这个分支上的提交都不会再合并回来),这也就使得WebRTC的版本历史非常清晰,开发者在查询提交记录或变更历史时会非常简单方便。
上图是我在之前参与过的一个项目中截取的,是在Sourcetree中截取的。我们可以看到其分支非常复杂,但其实这是遵循GitFlow的版本控制模式运行所导致的结果,Gitflow被很多人诟病的地方就是提交的版本记录分支结构非常复杂,难以追溯历史。
1.3 WebRTC代码目录
首先是“api”,主要是C++代码的公开api,开发者在使用C++开发时就会用到其预先定义的接口程序,例如pPear_cConnection类。同时,在安卓或iOS上使用JAVA或者ObjectiveC接口的话,其实也都是对C++接口的bouninding。
“call、pc、media”:这三个目录在我理解,是WebRTC主要流程和业务逻辑的实现代码。
“audio、common_aduio、video、common_video”:这四个目录主要是音频和视频类相关的代码。
“modules”:很多公司可能不会直接使用整个WebRTC的代码库,而只是使用其中的一些常用模块,这些模块大都包含在“modules”中,例如回声抑制、噪音抑制等处理,视频编码、Jitterbuffer等。
“p2p”:与p2p连接相关的代码。
“sdk”:Android和iOS平台相关的代码,如视频采集、预览、渲染、编解码等需要调用系统接口的代码,对C++接口的bouninding。
“rtc_base”:Chromium项目中一些公用的基础代码,例如线程、锁相关代码。
“third_party”:包含许多Google的其它开源项目以及非Google开源的项目,被WebRTC用到的都放在third_party中,例如FFmpeg、libvpx等。
“system_wrappers”:包含另一个系统相关的代码的目录,如sleep函数。SDK主要涉及的是Androic和iOS平台相关的代码,system_wrappers则包含更多平台如windows等相关的代码。
“stats 、logging”:状态统计,日志打印相关的代码。
“examples”:包含有各个平台的demo,例如Android、iOS、Windows、linux、MacOS等。
目前就我的学习和了解,还没有触及其它的一些目录,不过它们应该也不是主干流程相关的内容。
本次分享的第一部分我们以《WebRTC Native开发实战》书中第一部分的标题Hello WebRTC来做一个结尾。
第一章:开发环境的搭建:书中有非常详细的一步步的教程,只要解决了科学上网的问题,按照教程基本上应该不会再遇到其它问题。
第二章:运行官方Demo:主要是刚才提到的examples目录中的各种Demo。
第三章:基本流程分析:这里的基本流程与我们此次分享的内容有些区别,这里的基本流程更多的是如何使用WebRTC的接口,实现简单的1V1的音视频通话,也就是Demo实现的一个功能。
分析方法-如何上手大型项目
对于个人来说如何尽快上手大型项目?例如WebRTC或者其它的开源项目像FFmpeg、GStreamer等。包括大家入职新公司,很可能会接手或参与到较大的项目中,虽然可能不都会像WebRTC那么巨大,但还是存在一定的挑战性。在这里分享一些我的经验,希望能为大家提供些帮助。
首先,第一步就是“跑起来”,只有把相关项目的demo运行起来,以此才能对项目有更加直观的了解,了解其相关功能,以功能实现的位置作为切入点,思考其实现方式、方法。
第二步,“从外部的API入手,顺藤摸瓜”。例如下图是iOS的代码,首先找到外部的API,如代码中RTCCameraVideoCapture是用来实现相机采集的,然后就可以看类中是如何调用接口和处理数据的。
第三步“基于基础知识(音频采播系统接口),搜索定位关键函数/类”,第二步例如在Android或者iOS下我们是先找到实现相应功能所需要调用的外部接口,可以根据这些关键的接口在代码中进行搜索发现关键的函数和类。但不是所有的逻辑都会有外部Web接口,例如WebRTC中音频相关的实现就是不需要调用任何接口的。下图是一个iOS的例子,对于音频播放最关键的函数是AudioOutputUnitStart,即开启一个Audio Unit。我们在搜索后可以找到voice_processing_audio_unit.m文件,其中包含的一个Start函数,我们就可以进一步观察函数以及头文件有哪些接口,例如初始化start、stop等,音频就可以从这里进行外扩或阅读源码。
第四步“静态阅读源码+单步调试”。静态阅读源码主要是利用IDE的代码跳转,但是gn其生成exXcode工程文件的方式有一些特殊,很多代码跳转会跳不过去或者跳转到错误地方。所以更多的时候我还是使用全局搜代码,尽管效率稍低,但目前没有其它更合适的办法。单步调试,在代码中的某些位置,我们希望了解其下一步是如何跳转的,而代码无法直接跳转,搜索的结果也并不知道是什么作用无法准确判断,这时我们可以通过加断点进行验证。
如图所示,是视频编码相关的一个类的函数,在加入断点后,我们可以观察到视频数据是如何从系统的回调接口到采集RTCCameraVideoCapture的类再一步步到编码的类,非常清晰。
在软件开发中,没有银弹,都是那些看似朴实无华但往往非常有效的办法,掌握这些方法后,再上手一些新的项目就会有一些帮助。
视频流程介绍
WebRTC的视频数据流程在各个平台基本上都是一致的。
视频数据首先由VideoCapturer采集,然后交给VideoSource,通过其中的VideoBroadcaster传输给接收对象,例如Encoder、Preview等。Preview负责进行本地预览,Encoder负责编码发送。从网络层接收到数据之后,首先会通过VideoDecoder进行解码,接下来同样会将其传输VideoBroadcaster,再分发给数据的接收方。VideoSource在上图中未体现,但也是一个比较关键的类,它位于VideoTrack和VideoBroadcaster中间,其实是对VideoBroadcaster接口的封装。
VideoTrack是WebRTC中比较重要的一个概念,音频、视频等媒体从概念上来说其实就是一个Track,我们通常会添加或从远端接收一个Track。另外,IOS的流程与上图中流程有一些区别,其视频预览不是从VideoBroadcaster接收每一帧的数据然后进行渲染,而是其系统存在接口可以将采集和预览两个系统类关联并自动实现渲染。但其实我们也可以像RemoteRenderer类一样,获取到一帧个数据后再进行渲染,用RemoteRenderer类添加到采集端的VideoBroadcaster中进行渲染。
在非iOS的平台上,本地预览以及远端视频的渲染其实都是通过一个类来实现的。
完整视频数据流程(调用栈)
图中详细的列出了视频数据的整体采集、处理、传输相关步骤。简单来看,就是从上到下到最底部网络层,再由下到上最终到渲染的整体流程。所有平台的视频数据流程基本上都是大同小异的,区别只在于采集、编解码和渲染的实现不同,其余的流程基本是一致的。
采集:
首先RTCCameraVideoCapture会从系统数据回调,接收到实际的视频数据,交给VideoSource通过_nativeVideoSource将数据传递到C++这一层,最后提交AdaptedVideoTrackSource进行一些如旋转、裁剪之类的操作。
编码:
视频数据经过AdaptedVideoTrackSource层之后,就可以通过broadcaster_进行分发。在安卓或者linux中可能会有多个分支,一个预览一个编码,这里我们以编码为主干进行分析。Sink实际上就是数据的消费者,通过VideoStreamEncoder来实现编码,但其只是概念上的编码,最终实际编码还是调用系统相关的类,因此最终会回到ObjectiveC层,通过一些调用到达RTCVideoEncoderH.264,再调用VideotoolbBox接口,实现H.264的硬件编码。编码完成之后会实现系统的回调,再将编码后的数据交回给C++层,即VideoStreamEncoder的OnEncodedImage回调函数中,表示一帧视频数据已经完成编码。
发送:
VideoSendStream表示要发送的视频流,通过rtp_vVideo_sSender_进行RTP打包处理,再接下来就是需要进行的RTP封包和网络传输。假设通过网络传输数据已经到达RtpVideoStreamReceiver,我们可以看到左右两边的sender和receiver在类以及函数的命名上会有一些对称的地方。RtpVideoStreamReceiver接收到RTP,并且已经完成解包以及其它的网络乱序、错误重传等处理,获得一帧完整可解码的帧,然后就会调用解码回调,送到VideoReceiveStream中进行解码操作,在这里会调用vVideo_rReceiver_的Decode函数。
解码:
vVideo_rReceiver_的Decode函数其实也是概念上的解码,叫VCMGenericDecoder,最终也会调到平台相关的ObjectiveC实现的视频硬解,即RTCVideoDecoderH.264,也是调用VideoToolBox进行解码,解码后通过DecodedFrameCallback交还给C++这一层。
解码后的数据最终还是到达了VideoStreamdDecoder,交给了incoming_vVideo_sStream_。这里会存在一个视频帧的队列,解码和编码不太一样,编码是采集到一帧视频帧,编码完成后立刻发送,但解码完成后却不会立刻进行渲染,而是需要一定的缓冲,以避免由于抖动而导致卡顿。所以视频数据解码完成后会首先放入队列中,等待渲染模块控制节奏,需要时再获取数据。
渲染:
获取到视频数据后,会通过Broadcaster将数据交给sink,sink在iOS上具体是通过RTCMTLVideoView对数据进行渲染,MTL是调用iOS的Metal接口进行视频渲染。
其实图中只是视频流程中调用栈的总结,书中有一章节的内容总结了视频数据流程的更多示例代码的分析以及讲解。
实战:客户端视频录制
首先要明确需求:1. 推流和收流都需要,即发送的数据需要录制成文件并且接收到的内容也要录制成文件;2. 其次是不希望做额外的编码,因为通常接收或者发送的视频都是已经处理(编码)好的,额外的编码会造成资源浪费。3.在不需要额外编码的情况下,我们只需要调用FFmpeg把编码后的数据存储到文件内即可。4.我们应该从哪里拿数据?
要回答从哪里拿数据这个问题,首先需要对视频数据流程有一定了解,也就是前面第三部分所介绍的内容。如上图红框所示,VideoSendStreamlmpl::OnEncodedimage中已经接收了编码后的视频数据,但其数据存在形式还是完整一帧,并没有拆分成一个一个的RTP包。接收端情况比较复杂,在网络传输时会出现乱序到达、丢包缺失等问题,造成网络数据的不可用。因此,我们需要找到一个已经对上述问题进行过处理的数据点,即解码之前的数据点,VidioReceiveStream的HandleEncodedFrame函数中。
当我们找到数据接入点后,需要进行的操作就是修改代码,增加API,实现相关功能。如在Android和iOS上希望有Java或Object C的接口暴露出来供APP层调用。想要修改iOS接口,就需要修改SDK目录下的代码。
举例如图所示 ,我们需要修改RTCPeerConnection文件,其中所定义为WebRTC的主类。增加Start/StopRecoder的接口,通过dir的参数表明想要录制视频的方向(发送或者接收)。
SDK仅为C++接口的boinunding,因此还需要修改API目录里面的C++接口,即修改peer_connection_interface.h,为C++的PC类增加接口。
API里面只是程序接口,我们需要修改程序的实现类,实现类主要在pc中,但这里有一点特殊的是,业务流程和实现逻辑,call中也是很重要的一部分。
如图所示,它是对api/peer_connection_interface的一个子类,一个具体集成的实现类,我们为其增加接口,但是在这里我们不在peer_connection_interface的类中调用录制相关的代码,而是在call里进行修改。
前面我们介绍到的VideoSendStream和VideoReceiveStream以及本次没有介绍到的VAideo相关的类,其实例的管理都在call对象里。在我理解,在以前WebRTC的概念模型中,主类其实是一个Call,而pPeercConnection是在后续标准化过程中所定义的接口。所以实际的视频录制调用功能被封装成一个Recorder类,Recorder类的管理都会放在call里面,修改pc peer connection以及call的头文件。
完成上述操作后,下一步就是截取数据。截取数据的操作其实就是VideoSendStream和VideoReciveStream的函数调用,Recorder的对象在call里面,两种Stream的对象也在call里面,那我们就只需要将Recorder设置给Stream,注入进去即可。
从call到VideoSendStream有如上图所示的文件需要修改,Call里面有一个Stream接口的定义,然后在video目录下会有call里面定义的Stream接口的子类、实现类,video_send_stream_impl,在OndecodedImage中,将完整的一帧给recorder,再调用FFmpeg的头文件接口即可。
数据收留端和发送端情况类似,ReceiveStream和SendStream在功能上非常对称,在call目录下也有一个接口定义,在video目录下也有一个接口的实现。
录制相关完整的代码在github上有一个完整的提交,大家可以作为参考。
(https://github.com/HackWebRTC/webrtc/commit/dfbcd2c75d27dafd24512d6ca3d24c6d86d63b82)