peer_connection中从视频采集到编码的流程
摘要:本篇文章主要讲述当我们通过peer_connection完成推流时,视频从采集到编码是如何衔接的。
既,视频采集后如何传送到编码器。重点分析传输的过程,方便我们对webrtc不同模块的拆分使用。
在webrtc源码中,modules目录下的video_capture目录中,有关于linux平台,windows平台视频采集的实现,
下面介绍 VideoCaptureModule 类,此类提供了如下几个主要方法:
virtual void RegisterCaptureDataCallback(
rtc::VideoSinkInterface<VideoFrame>* dataCallback) = 0;
virtual void DeRegisterCaptureDataCallback() = 0;
virtual int32_t StartCapture(const VideoCaptureCapability& capability) = 0;
virtual int32_t StopCapture() = 0;
这里的 RegisterCaptureDataCallback 是用来注册获取数据的回调,当我们打开 rtc::VideoSinkInterface
类的时候可以看到 OnFrame函数,如下,
virtual void OnFrame(const VideoFrameT& frame) = 0;
使用时,只需要实现 VideoSinkInterface 接口,再调用RegisterCaptureDataCallback注册即可。
下面介绍 VideoCaptureImpl 类,此类继承 VideoCaptureModule ,主要函数IncomingFrame
// `capture_time` must be specified in NTP time format in milliseconds.
int32_t IncomingFrame(uint8_t* videoFrame,
size_t videoFrameLength,
const VideoCaptureCapability& frameInfo,
int64_t captureTime = 0);
其实早些版本的webrtc,是有 VideoCaptureExternal 类,提供提供了 IncomingFrame的纯虚方法,
而后 VideoCaptureImpl 类继承VideoCaptureModule 和 VideoCaptureExternal 类,从而具有了
IncomingFrame方法, 但从最新的源码中看,VideoCaptureExternal 类已经删除 ,IncomingFrame
方法也直接放入 VideoCaptureImpl 中。VideoCaptureImpl 的主要功能,是提供了YUV格式的转换,
其实现放在 IncomingFrame 方法中。
那为什么需要yuv格式的转换呢?
不管我们是使用 android 的camera或者 linux中的V4L2,摄像头直接采集出来的都不是 I420(YUV420P),
但是x264 和 x265需要输入的YUV格式 通常 为 I420,所以采集出来的YUV数据在送入编码其之前都
需要做一次YUV之间的转换,转成编码器需要的格式,需要解释一下的是,编码器的输入源并非只是I420,
也可以是其他YUV格式。并且有时我们会选用硬编,根据硬件厂商的不同,可以支持的YUV格式也不同,所以,
是否需要转换需要根据实际情况而定。就 IncomingFrame 实现来看,webrtc提供了转换为I420的转换实现。
以下是 IncomingFrame 中的部分实现。
const int conversionResult = libyuv::ConvertToI420(
videoFrame, videoFrameLength, buffer.get()->MutableDataY(),
buffer.get()->StrideY(), buffer.get()->MutableDataU(),
buffer.get()->StrideU(), buffer.get()->MutableDataV(),
buffer.get()->StrideV(), 0, 0, // No Cropping
width, height, target_width, target_height, rotation_mode,
ConvertVideoType(frameInfo.videoType));
下面以linux中 的视频采集为例稍作说明,VideoCaptureModuleV4L2 类继承了 VideoCaptureImpl 类,实现
如下方法。
virtual int32_t StartCapture(const VideoCaptureCapability& capability) = 0;
virtual int32_t StopCapture() = 0;
VideoCaptureModuleV4L2 类中主要调用了 V4L2的接口去实现视频的采集, 主要流程是,通过open
打开video fd,调用ioctl接口,结合相应的宏,完成视频的采集。(采集的实现细节就不在此展开,非难点也非
本文重点)
下面说一下 rtc::VideoSourceInterface 和 rtc::VideoSinkInterface 接口类,分别摘取其中重要的接口方法,
如下:
rtc::VideoSourceInterface
virtual void AddOrUpdateSink(VideoSinkInterface<VideoFrameT>* sink,
const VideoSinkWants& wants) = 0;
virtual void RemoveSink(VideoSinkInterface<VideoFrameT>* sink) = 0;
rtc::VideoSinkInterface
virtual void OnFrame(const VideoFrameT& frame) = 0;
从接口方法中不难看出,source interface为视频的出口接口,sink interface 为视频入口接口。
所以我们只需要顺着该思路就能探索出视频音视频流向的原理。
当我们使用peer_connection方法推流时,首先使用 webrtc::CreatePeerConnectionFactory 创建
PeerConnectionFactory ,而后 PeerConnectionFactory 创建 PeerConnection , PeerConnection
通过AddTrack 添加视频源,随后通过 CreateOffer 创建 sdp,server端收到 sdp后解析成功,发送
answer sdp消息,推流端收到后解析answer sdp,完成信息收集,开始推流。以上就是ice 推流大
体过程,当然,这只是通过ice实现我们推流端到公网server的交互,而并非p2p形式的交互。
在上文中我们采集好视频后,是需要通过 PeerConnection 的AddTrack 方法将视频加入到
PeerConnection中,那我们先看两段源码。
源码一
rtc::scoped_refptr<PeerConnectionFactoryInterface> CreatePeerConnectionFactory(
rtc::Thread* network_thread,
rtc::Thread* worker_thread,
rtc::Thread* signaling_thread,
rtc::scoped_refptr<AudioDeviceModule> default_adm,
rtc::scoped_refptr<AudioEncoderFactory> audio_encoder_factory,
rtc::scoped_refptr<AudioDecoderFactory> audio_decoder_factory,
std::unique_ptr<VideoEncoderFactory> video_encoder_factory,
std::unique_ptr<VideoDecoderFactory> video_decoder_factory,
rtc::scoped_refptr<AudioMixer> audio_mixer,
rtc::scoped_refptr<AudioProcessing> audio_processing)
源码二
RTCErrorOr<rtc::scoped_refptr<RtpSenderInterface>>
PeerConnectionInterface::AddTrack(
rtc::scoped_refptr<MediaStreamTrackInterface> track,
const std::vector<std::string>& stream_ids) {
return RTCError(RTCErrorType::UNSUPPORTED_OPERATION, "Not implemented");
}
源码一种我们发现编解码器是在创建 PeerConnectionFactory 时给到PeerConnectionFactory
的,我们先不管编解码器最终交给了哪个对象, 源码二显示我们的 track 至少实现了 MediaStreamTrackInterface
接口,可是当我们查看 MediaStreamTrackInterface 接口时却发现 其中并没有视频传出的接口,
我们接着往下跟 AddTrack,下面展示一下方法的调用过程。
在AddTrack 方法中
RTCErrorOr<rtc::scoped_refptr<RtpSenderInterface>> PeerConnection::AddTrack(
rtc::scoped_refptr<MediaStreamTrackInterface> track,
const std::vector<std::string>& stream_ids)
进入到 AddTrackUnifiedPlan 方法 (还有 AddTrackPlanB 方法,根据sdp 格式不同做不同选择,
目前Plan B已经基本不用,所以我们只分析 AddTrackUnifiedPlan 方法)
RTCErrorOr<rtc::scoped_refptr<RtpSenderInterface>>
PeerConnection::AddTrackUnifiedPlan(
rtc::scoped_refptr<MediaStreamTrackInterface> track,
const std::vector<std::string>& stream_ids)
进入到 CreateSender
auto sender = CreateSender(media_type, sender_id, track, stream_ids, {});
调用到
bool set_track_succeeded = sender->SetTrack(track);
至此,AddTrack的调用完成,在SetTrack中 track 值被赋值给track_ ,我们内部变量
track_最终保留了track的值,我们接着看track_的使用。
在 VideoRtpSender 中(上面 CreateSender创建出的即为 VideoRtpSender)
rtc::scoped_refptr<VideoTrackInterface> video_track() const {
return rtc::scoped_refptr<VideoTrackInterface>(
static_cast<VideoTrackInterface*>(track_.get()));
}
当我们看到 video_track 中下面这行代码的时候,是否已经恍然大悟,为什么在AddTrack
的MediaStreamTrackInterface接口中没有视频数据出口,没有视频数据出口,采集好的视频
又是怎送到编码器呢,这些疑惑都可以用下面这行代码解释,也就是说,我们传入AddTrack中的
对象,实际是实现了 VideoTrackInterface 接口类的对象。
static_cast<VideoTrackInterface*>(track_.get()))
VideoTrackInterface 接口类的继承关系如下,
class VideoTrackInterface : public MediaStreamTrackInterface,
public rtc::VideoSourceInterface<VideoFrame>
有下面几个主要方法
void AddOrUpdateSink(rtc::VideoSinkInterface<VideoFrame>* sink,
const rtc::VideoSinkWants& wants) override {}
void RemoveSink(rtc::VideoSinkInterface<VideoFrame>* sink) override {}
virtual VideoTrackSourceInterface* GetSource() const = 0;
我们现在从 VideoRtpSender 中查找哪里调用了 video_track() ,如下
void VideoRtpSender::SetSend() {
bool success = worker_thread_->Invoke<bool>(RTC_FROM_HERE, [&] {
return video_media_channel()->SetVideoSend(ssrc_, &options, video_track());
}
我们再查找 SetVideoSend 的实现,当看到下面这段代码的时候,知道我们的视频源被送到了这里,赋值
给了 source_ 。
bool WebRtcVideoChannel::WebRtcVideoSendStream::SetVideoSend(
const VideoOptions* options,
rtc::VideoSourceInterface<webrtc::VideoFrame>* source) {
source_ = source;
}
现在我们在 WebRtcVideoChannel::WebRtcVideoSendStream 中查找 source_ 的调用。
void WebRtcVideoChannel::WebRtcVideoSendStream::AddOrUpdateSink(
rtc::VideoSinkInterface<webrtc::VideoFrame>* sink,
const rtc::VideoSinkWants& wants){
source_->AddOrUpdateSink(encoder_sink_, wants);
}
不出意外我找到了想要的代码,source_->AddOrUpdateSink(encoder_sink_, wants);
这行代码很好的解释了,我们的视频源流向了哪里,还记得文章开始时候提到的两个有关数据流向
的接口吗,在这里完美的对上了,因为 encoder需要有视频的输入,所以必然继承了sink的接口
,通过sink中的onframe 接收数据。至此,视频采集和送入编码器的过程已经分析完毕。
通过分析,我们知道的视频数据的初步传输过程。当然要想把视频推出去还是有很多
工作要做。 上文中关于编码器在 peer_connection_factory 中最后被存放到了哪里,大家
可以跟一下代码,个人觉得还是比较容易懂的,
总结:本人第一次写文章,排版有点生疏,望见谅, 上文中很多代码示例只是伪代码,只为
方便突出重点。