今年上半年开始接触 OWT,当时用起来之后整理了 OWT Server 快速入门,其中初步涉及了音视频数据的转发流程。最近在考虑实现客户端的推收流录制,正好之前也遇见了 OWT 录制的一些问题,所以这次就先对 OWT 的音视频数据转发和录制流程探个究竟,一方面可以作为客户端实现的借鉴,另一方面也可以为向 OWT Server 提交 PR 做好准备。
注:本文分析的代码,是 OWT Server 的 4.2.x 分支,4.3.x 更新了 Licode 的版本,有了较大的变化。
音视频数据转发流程
Licode(OWT 使用了一些 Licode 的代码)定义了 Transport 接口,并提供了 DtlsTransport 实现;WebRtcConnection 类实现了 TransportListener 接口,故 DtlsTransport::onIceData 收到发布端的数据并解密后,会交给 WebRtcConnection::onTransportData。DtlsTransport 的数据从哪里来?来自 LibNiceConnection::onData,它是对 libnice/libnice 的封装。原本 Licode 使用的是 resiprocate/nICEr,对应的函数为 NicerConnection::onData,但 OWT 用 libnice 替换了 nICEr。
WebRtcConnection 之后的数据传递流程为:
WebRtcConnection::onTransportData ->
Pipeline::read ->
PacketReader::read ->
WebRtcConnection::read ->
MediaSink::deliverAudio/VideoData
WebRtcConnection 的 audio_sink_ 就是 AudioFrameConstructor,video_sink_ 就是 VideoFrameConstructor。
所以音频数据接下来的流程就是:
MediaSink::deliverAudioData ->
AudioFrameConstructor::deliverAudioData_ ->
FrameSource::deliverFrame -> // (1)
AudioFramePacketizer::onFrame -> // (2)
WebRtcConnection::deliverAudioData_ -> // (3)
WebRtcConnection::sendPacket ->
WebRtcConnection::write ->
DtlsTransport::write // (4)
要点如下:
- 有客户端订阅某个流时,就会给
FrameSource增加FrameDestination(具体流程可以查看 OWT Server 快速入门:建立数据转发关系的分析),而FrameSource::deliverFrame里会把帧转发给所有的FrameDestination; - 音频数据的
FrameDestination实现类为AudioFramePacketizer,故音频帧就送到了AudioFramePacketizer::onFrame; AudioFramePacketizer的audio_sink_是订阅端的WebRtcConnection,故欲发送的音频数据就到了WebRtcConnection::deliverAudioData_里;- 最后要转发的音频数据通过
DtlsTransport发送给订阅端;
视频数据的流程为:
MediaSink::deliverVideoData ->
VideoFrameConstructor::deliverVideoData_ -> // (1)
ViEReceiver::ReceivedRTPPacket -> // (2)
RtpReceiverImpl::IncomingRtpPacket -> // (3)
ViEReceiver::OnReceivedPayloadData -> // (4)
VideoReceiver::IncomingPacket ->
VCMReceiver::InsertPacket -> // (5)
VideoReceiver::Decode -> // (6)
VideoFrameConstructor::Decode -> // (7)
FrameSource::deliverFrame ->
VideoFramePacketizer::onFrame -> // (8)
VideoFramePacketizer::receiveRtpData -> // (9)
WebRtcConnection::deliverVideoData_ -> // (10)
WebRtcConnection::sendPacket ->
WebRtcConnection::write ->
DtlsTransport::write // (11)
要点如下:
VideoFrameConstructor有两个关键成员:webrtc::ViEReceiver m_videoReceiver和webrtc::vcm::VideoReceiver m_video_receiver;webrtc::ViEReceiver有两个关键成员:RtpReceiver rtp_receiver_和webrtc::vcm::VideoReceiver video_receiver_;m_video_receiver和video_receiver_指向同一个对象;VideoFrameConstructor::deliverVideoData_里调用ViEReceiver::ReceivedRTPPacket,进而把 RTP 包交给RtpReceiver,由其负责 RTP 解包逻辑;RtpReceiverImpl::IncomingRtpPacket调用RTPReceiverVideo::ParseRtpPacket(RTPReceiverStrategy接口的实现),其中会根据不同的 payload type,创建不同的RtpDepacketizer去提取载荷内容;- 提取到载荷数据后会回调到
ViEReceiver::OnReceivedPayloadData; - 之后载荷数据会交给
VideoReceiver::IncomingPacket,进而交给VCMReceiver::InsertPacket,进而交给了 jitter buffer; VideoFrameConstructor::deliverVideoData_里调用完m_videoReceiver->ReceivedRTPPacket后,还会调用m_video_receiver->Decode,以从 jitter buffer 中取出已完整收到的视频帧(未解码);VideoFrameConstructor还实现了webrtc::VideoDecoder接口,在VideoFrameConstructor::OnInitializeDecoder中会把自己注册给webrtc::vcm::VideoReceiver,所以调用VideoReceiver::Decode实际上会调用到VideoFrameConstructor::Decode;当然,VideoFrameConstructor只是做视频数据的转发,无需实现真正的解码逻辑;通过这一「伪解码」的过程,OWT 就利用 WebRTC 的 VCM 来实现了视频 RTP 的解包过程,拿到了完整的视频帧(未解码);- 视频数据的
FrameDestination实现类为VideoFramePacketizer,故视频帧就送到了VideoFramePacketizer::onFrame;其中需要实现视频帧的 RTP 封包逻辑,它是调用webrtc::RtpRtcp类来实现的,需要指出的是,调用m_rtpRtcp->SendOutgoingData时,第一个参数传的一律都是webrtc::kVideoFrameKey,这是因为这个参数在 WebRTC H.264 封包的实现代码里并未使用,所以这里图省事就这么传了(这一点是请教 OWT 研发人员后得到的解答); - 在
VideoFramePacketizer::init函数中创建webrtc::RtpRtcp时,设置了outgoing_transport为WebRTCTransport对象(core/owt_base/WebRTCTransport.h),故 RTP 封包完成后,会调用到WebRTCTransport<dataType>::SendRtcp,进而调用到RTPDataReceiver::receiveRtpData(即VideoFramePacketizer::receiveRtpData,VideoFramePacketizer实现了RTPDataReceiver接口,并在构造函数里创建WebRTCTransport时传入了自己); VideoFramePacketizer的video_sink_是订阅端的WebRtcConnection,故欲发送的视频数据就到了WebRtcConnection::deliverVideoData_里;- 最后要转发的视频数据通过
DtlsTransport发送给订阅端;
搞清楚了音视频数据的转发流程后,我们发现 OWT 对音视频 RTP 包做了解包和重新封包。其实音频倒也谈不上解包和封包,因为音频数据编码后都比较小,一个 RTP 包就能容纳,所以没有像视频那样复杂的封包和解包逻辑。
但实际上 SFU 是不需要做 RTP 解包的,收到发布端的 RTP 包后直接转发给订阅端即可,OWT 这里做解包,应该是给 MCU 功能用的。
经 OWT 官方指正,OWT 做 RTP 解包并非是为了 MCU 功能考虑,而是一个基础设计原则:
将传输层事务在接入节点终结掉,媒体在集群中流转以“媒体帧”为封装单元,所有操作均在“帧交互层”以上进行。
SFU 是否组帧,效果上并没有简单明确的好坏之分:
端到端的全程延迟取决于每一帧什么时候被完整拼出来,而不是第一个包什么时候到达。理论上看并不会因为中间组过一次帧而显著增加“最后一块拼图”的到达时间,增加的只是将收齐的rtp包序列拼装成帧及将帧打成包序列的CPU计算过程的时间,这个时间一般是毫秒以内。另一方面将传输拆成帧接力的两段的话,可以比盲转更早发现丢包并请求重传,实际上是帮助减小了“最后一块拼图”的延迟。但实际弱网环境时刻在变化,很难模拟,需要实际测试一下效果,并调整各阶段的对抗手段,来达到相对较好的抗丢包效果。
服务端录制
启用服务端录制需要调用 RESTful API,其处理流程为:
management_api/api.js -> // (1)
management_api/resource/recordingsResource.js exports.add ->
management_api/requestHandler.js exports.addServerSideSubscription -> // (2)
agent/conference/conference.js that.addServerSideSubscription ->
agent/conference/accessController.js that.initiate -> // (3)
agent/conference/rpcRequest.js that.initiate -> // (4)
agent/recording/index.js that.subscribe // (5)
要点如下:
- 定义 RESTful API 的处理函数;
agent/conference/conference.js that.rpcAPI注册了 RPC server;- 根据
sessionOptions.type找到 RPC server node 为 recording node; - direction 是 out,故 RPC 调用(recording node 的)subscribe;
- 创建 AVStreamOut;
AVStreamOut 是 agent/addons/avstreamLib 扩展里的类型,其 C++ 实现为 agent/addons/avstreamLib/AVStreamOutWrap.cc,在 AVStreamOutWrap::New 中, type 是 file,故实际创建的是 owt_base::MediaFileOut。另一种 type 是 streaming,用于转推 rtsp/rtmp/hls/dash。
agent/recording/index.js createFileOut 函数里,创建 AVStreamOut 时传入了一个状态回调函数(应该是由 AVStreamOut::notifyAsyncEvent 函数调用触发),如果没有发生错误,就会调用 notifyStatus 抛出 ready 消息,之后的处理流程为(在 OWT Server 快速入门:subscribe 建立数据转发关系曾分析过订阅端的流程,这里稍作更新即可得到录制的流程):
agent/recording/index.js notifyStatus ->
agent/conference.js onSessionProgress ->
agent/conference/accessController.js onSessionStatus ->
agent/conference.js onSessionEstablished
之后,服务端建立推流端的数据到录制的转发关系:
agent/conference.js onSessionEstablished ->
agent/conference.js addSubscription ->
agent/conference/roomController.js subscribe ->
agent/conference/roomController.js spreadStream ->
agent/conference/roomController.js linkup ->
agent/webrtc/index.js linkup ->
agent/connections.js linkupConnection -> // (1)
agent/webrtc/wrtcConnection.js addDestination ->
NAN_METHOD(AudioFrameConstructor::addDestination), NAN_METHOD(VideoFrameConstructor::addDestination) ->
FrameSource::addAudioDestination, FrameSource::addVideoDestination
要点如下:
linkupConnection里调用conn.connection.receiver('audio')和conn.connection.receiver('video')分别获取音频和视频的 dest,然后调用connection.addDestination;connection.addDestination里的connection毫无疑问就是发布端的wrtcConnection,但 dest 是什么呢?我们可以看到agent/recording/index.js里定义了connection.receiver函数,返回的就是this,因此音视频的 dest 也就都是AVStreamOut了,所以音视频数据最终会送到AVStreamOut::onFrame中;
owt_base::MediaFileOut 继承自 owt_base::AVStreamOut。在 AVStreamOut::onFrame 中,会把帧放入 m_frameQueue 中;在构造函数里,会创建一个线程,线程函数为 AVStreamOut::sendLoop,其中不停从 m_frameQueue 里取帧,取到后调用 AVStreamOut::writeFrame;其中就是调用 ffmpeg 的 av_interleaved_write_frame 写入音视频帧。
通过前文音视频数据转发流程的分析,我们知道数据到达 FrameDestination::onFrame 之前已经处理好了乱序、重复等问题,所以这里直接写入文件即可。至于丢包的情况,如果最终视频包也没能按时收到,那也只能任其处于缺失状态了。
增加日志
最后分享一下如何给 OWT Server 增加日志,以便分析问题或分析调用流程。
如果原本类中已经有过日志代码了,那直接使用 ELOG_INFO 等宏即可。
如果原本类中没有任何日志代码,比如 FrameSource 类,则需要做以下几处修改:
core/owt_base/MediaFramePipeline.h增加#include <logger.h>;class FrameSource {后增加DECLARE_LOGGER();;core/owt_base/MediaFramePipeline.cpp在所有FrameSource函数定义之前增加DEFINE_LOGGER(FrameSource, "FrameSource");;- 在需要打日志的地方使用
ELOG_INFO等宏即可;
编译时,如果报类似如下错误:
[FAIL] mediaFrameMulticaster.node Error:
/media/psf/Home/src/media/owt-server/source/agent/addons/mediaFrameMulticaster/
build/Release/mediaFrameMulticaster.node: undefined symbol: _ZN7log4cxx6Logger9getLoggerEPKc
这是因为该模块没有链接 log4cxx,在相应模块的 binding.gyp 的 libraries 中添加 '-llog4cxx', 即可。例如上面是 agent/addons/mediaFrameMulticaster 模块,那就修改 agent/addons/mediaFrameMulticaster/binding.gyp 即可。
此外,每个模块下都有 log4cxx.properties 和 log4js_configuration.json,用于控制日志输出的级别,需要注意如果开的日志级别不够,可能在日志文件里看不到日志。
欢迎大家加入 Hack WebRTC 星球,和我一起钻研 WebRTC。