ToB企服应用市场:ToB评测及商务社交产业平台

标题: WebRTC视频 01 - 视频采集整体架构 [打印本页]

作者: 张裕    时间: 2024-11-23 18:23
标题: WebRTC视频 01 - 视频采集整体架构
WebRTC视频 01 - 视频采集整体架构(本文)
WebRTC视频 02 - 视频采集类 VideoCaptureModule
WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇
WebRTC视频 04 - 视频采集类 VideoCaptureDS 中篇
WebRTC视频 05 - 视频采集类 VideoCaptureDS 下篇
一、前言:

我们从1对1通信说起,假如有一天,你和你情敌使用X信进行1v1通信,想象一下画面是不是一个大画面中有一个小画面?这在布局中就叫做PIP(picture in picture);这个随手一点,看似在1s不到就完成的动作,内里却经过了很多复杂的操作,我们本日开始写一系列文章介绍下这俩帅哥的图片怎么体现的。
二、宏观流程:


三、类图:



四、代码走读:

前面说了VideoTrack主要职责是创建数据链路,将数据源和数据消费者串起来,我们现在看看这条通道是怎么创建起来的。
我们先按照经验猜一下(其实我是看了代码的,冒充猜一下):
看代码:
代码入口:
  1. // 代码路径:examples\peerconnection\client\conductor.cc
  2. bool Conductor::InitializePeerConnection() {
  3.   // 创建PeerConnection部分省略...
  4.    
  5.   // 添加track到PeerConnection中
  6.   AddTracks();
  7.   return peer_connection_ != nullptr;
  8. }
复制代码
AddTracks内里会:

已经删除非关键代码。
  1. // 代码路径:examples\peerconnection\client\conductor.cc
  2. void Conductor::AddTracks() {
  3.   if (!peer_connection_->GetSenders().empty()) {
  4.     return;  // Already added tracks.
  5.   }
  6.   
  7.   // 1、构建一个数据源 CaptureTrackSource (里面会创建 VcmCapturer )
  8.   rtc::scoped_refptr<CapturerTrackSource> video_device = CapturerTrackSource::Create();
  9.   if (video_device) {
  10.     // 2、构建一个 VideoTrack , 返回其代理类
  11.     rtc::scoped_refptr<webrtc::VideoTrackInterface> video_track_(
  12.         peer_connection_factory_->CreateVideoTrack(kVideoLabel, video_device));
  13.     // 3、开始本地渲染
  14.     main_wnd_->StartLocalRenderer(video_track_);
  15.     // 4、将VideoTrack添加到PeerConnection当中管理
  16.     result_or_error = peer_connection_->AddTrack(video_track_, {kStreamId});
  17.     if (!result_or_error.ok()) {
  18.       RTC_LOG(LS_ERROR) << "Failed to add video track to PeerConnection: "
  19.                         << result_or_error.error().message();
  20.     }
  21.   }
  22.   main_wnd_->SwitchToStreamingUI();
  23. }
复制代码
分开看下上面几个关键步骤:
1)创建CapturerTrackSource:

  1. // 代码路径:examples\peerconnection\client\conductor.cc
  2.   static rtc::scoped_refptr<CapturerTrackSource> Create() {
  3.     const size_t kWidth = 640;
  4.     const size_t kHeight = 480;
  5.     const size_t kFps = 30;
  6.     std::unique_ptr<webrtc::test::VcmCapturer> capturer;
  7.     // 创建一个DeviceInfo对象,里面包含视频采集设备的属性信息
  8.     std::unique_ptr<webrtc::VideoCaptureModule::DeviceInfo> info(
  9.         webrtc::VideoCaptureFactory::CreateDeviceInfo());
  10.     if (!info) {
  11.       return nullptr;
  12.     }
  13.     // 获取采集设备数量(因为有些设备有多个摄像头),并遍历每个采集设备
  14.     int num_devices = info->NumberOfDevices();
  15.     for (int i = 0; i < num_devices; ++i) {
  16.       // 为每个采集设备创建VcmCapture,里面会实例化vcm对象 VideoCaptureImpl
  17.       capturer = absl::WrapUnique(webrtc::test::VcmCapturer::Create(kWidth, kHeight, kFps, i));
  18.       if (capturer) {
  19.         // 以VcmCapture为入参,创建CapturerTrackSource对象,并返回
  20.         return new rtc::RefCountedObject<CapturerTrackSource>(std::move(capturer));
  21.       }
  22.     }
  23.     return nullptr;
  24.   }
复制代码
上面就是创建了一个CapturerTrackSource对象,为什么我说是一个呢?因为,纵然你有多个摄像头,找到第一个可用的,并创建了CapturerTrackSource就返回了。并且,在创建CapturerTrackSource对象的时间传入了一个VcmCapture对象,并持有了。这个VcmCapture内里又会创建详细的数据源采集类对象,即VideoCaptureImpl类型的capturer,由于人脑栈有限,先不深究capturer如何创建的,继续回头看主干,也就是Conductor::AddTracks()函数。
至此,拉皮条的VcmCapture有了,CapturerTrackSource有了,数据源VideoCaptureImpl有了,记着我们的目的是创建链路,那么还必要创建管理者VideoTrack,以及数据分发器VideoBroadcaster。
2)创建VideoTrack:

   下面代码就不是examples,了是webrtc内核代码了。
  1. // 代码路径:pc\peer_connection_factory.cc
  2. rtc::scoped_refptr<VideoTrackInterface> PeerConnectionFactory::CreateVideoTrack(
  3.     const std::string& id,
  4.     VideoTrackSourceInterface* source) {
  5.   RTC_DCHECK(signaling_thread()->IsCurrent());
  6.   // 构建一个VideoTrack对象
  7.   rtc::scoped_refptr<VideoTrackInterface> track(VideoTrack::Create(id, source, worker_thread()));
  8.   return VideoTrackProxy::Create(signaling_thread(), worker_thread(), track);
  9. }
复制代码
留意是工作线程,一定要记着本身在哪个线程实行。还有,返回的是一个VideoTrack的代理类。
  1. // 代码路径:pc\video_track.cc
  2. rtc::scoped_refptr<VideoTrack> VideoTrack::Create(
  3.     const std::string& id,
  4.     VideoTrackSourceInterface* source,
  5.     rtc::Thread* worker_thread) {
  6.   // 创建了一个带有引用计数的VideoTrack对象,并返回了指针
  7.   rtc::RefCountedObject<VideoTrack>* track =
  8.       new rtc::RefCountedObject<VideoTrack>(id, source, worker_thread);
  9.   return track;
  10. }
复制代码
不明白这个智能指针的,可以去看看我的另外一篇博客:https://blog.csdn.net/Ziwubiancheng/article/details/142985264?spm=1001.2014.3001.5501
3)创建链路:

至此,我们创建好了VcmCapture,并且创建好了详细数的数据采集类VideoCaptureImpl,详细的数据分发器VideoBroadcaster,以及其管理者VideoTrack。那么,管理者VideoTrack什么时间(when),在哪儿(where),通过何种方式(how),创建了什么样(what)的数据链路呢?我们详细分析下:
起首,有两条链路,想想之前哪个视频PIP画面,因此,必要一条本地渲染链路,以及一条远端渲染链路。
a)本地渲染链路:

入口就在:Conductor::AddTracks()的main_wnd_->StartLocalRenderer(video_track_);
  1. // 代码路径:examples\peerconnection\client\main_wnd.cc
  2. //  开始本地渲染
  3. void MainWnd::StartLocalRenderer(webrtc::VideoTrackInterface* local_video) {
  4.   // VideoRenderer 构造函数里面会调用 AddOrUpdateSink,一路调用到 VideoBroadcaster 当中
  5.   // 这个 local_video 是一个 VideoTrack 对象
  6.   local_renderer_.reset(new VideoRenderer(handle(), 1, 1, local_video));
  7. }
复制代码
看看VideoRenderer构造函数:
  1. // 代码路径:examples\peerconnection\client\main_wnd.cc
  2. MainWnd::VideoRenderer::VideoRenderer(
  3.     HWND wnd,
  4.     int width,
  5.     int height,
  6.     webrtc::VideoTrackInterface* track_to_render)
  7.     : wnd_(wnd), rendered_track_(track_to_render) {
  8.   ::InitializeCriticalSection(&buffer_lock_);
  9.   ZeroMemory(&bmi_, sizeof(bmi_));
  10.   bmi_.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
  11.   bmi_.bmiHeader.biPlanes = 1;
  12.   bmi_.bmiHeader.biBitCount = 32;
  13.   bmi_.bmiHeader.biCompression = BI_RGB;
  14.   bmi_.bmiHeader.biWidth = width;
  15.   bmi_.bmiHeader.biHeight = -height;
  16.   bmi_.bmiHeader.biSizeImage =
  17.       width * height * (bmi_.bmiHeader.biBitCount >> 3);
  18.   // 这是一个 VideoTrack 对象,会将this(渲染器)添加到 VideoTrack 当中
  19.   rendered_track_->AddOrUpdateSink(this, rtc::VideoSinkWants());
  20. }
复制代码
再进去看看这个AddOrUpdateSink方法(留意看上面的VideoRenderer对于VideoTrack来说就是个sink)
  1. // 代码路径:pc\video_track.cc
  2. // AddOrUpdateSink and RemoveSink should be called on the worker
  3. // thread.
  4. void VideoTrack::AddOrUpdateSink(rtc::VideoSinkInterface<VideoFrame>* sink,
  5.                                  const rtc::VideoSinkWants& wants) {
  6.   RTC_DCHECK(worker_thread_->IsCurrent());
  7.   VideoSourceBase::AddOrUpdateSink(sink, wants);
  8.   rtc::VideoSinkWants modified_wants = wants;
  9.   modified_wants.black_frames = !enabled();
  10.   // video_source_ 是 CapturerTrackSource
  11.   video_source_->AddOrUpdateSink(sink, modified_wants);
  12. }
复制代码
CaptureTrackSource是VideoTrackSource的子类,因此,会去调用VideoTrackSource:
  1. // 代码路径:pc\video_track_source.cc
  2. void VideoTrackSource::AddOrUpdateSink(
  3.     rtc::VideoSinkInterface<VideoFrame>* sink,
  4.     const rtc::VideoSinkWants& wants) {
  5.   RTC_DCHECK(worker_thread_checker_.IsCurrent());
  6.   // 直接调用 source里面的方法,这个source是 TestVideoCapturer
  7.   source()->AddOrUpdateSink(sink, wants);
  8. }
复制代码
进去TestVideoCapturer看看:
  1. void TestVideoCapturer::AddOrUpdateSink(
  2.     rtc::VideoSinkInterface<VideoFrame>* sink,
  3.     const rtc::VideoSinkWants& wants) {
  4.   broadcaster_.AddOrUpdateSink(sink, wants);
  5.   UpdateVideoAdapter();
  6. }
复制代码
TestVideoCapturer内里又调用了broadcaster的Add方法(记着我们设置渲染器的目的,就是终极设置给broadcaster)
看看VideoBroadcaster内里做了啥:
  1. // 代码路径:media\base\video_broadcaster.cc
  2. void VideoBroadcaster::AddOrUpdateSink(
  3.     VideoSinkInterface<webrtc::VideoFrame>* sink,
  4.     const VideoSinkWants& wants) {
  5.   RTC_DCHECK(sink != nullptr);
  6.   webrtc::MutexLock lock(&sinks_and_wants_lock_);
  7.   if (!FindSinkPair(sink)) {
  8.     // |Sink| is a new sink, which didn't receive previous frame.
  9.     previous_frame_sent_to_all_sinks_ = false;
  10.   }
  11.   // 又调用了基类的方法
  12.   VideoSourceBase::AddOrUpdateSink(sink, wants);
  13.   UpdateWants();
  14. }
复制代码
进去看看其基类VideoSourceBase:
  1. // 代码路径:media\base\video_source_base.cc
  2. void VideoSourceBase::AddOrUpdateSink(
  3.     VideoSinkInterface<webrtc::VideoFrame>* sink,
  4.     const VideoSinkWants& wants) {
  5.   RTC_DCHECK(sink != nullptr);
  6.   SinkPair* sink_pair = FindSinkPair(sink);
  7.   if (!sink_pair) {
  8.     // 直接放到成员变量sinks里面了
  9.     sinks_.push_back(SinkPair(sink, wants));
  10.   } else {
  11.     sink_pair->wants = wants;
  12.   }
  13. }
复制代码
由于之前VideoCaptureModule已经将采集模块启动起来了,因此,数据就远远不断的从VideoCaptureModule进入到VcmCapture,然后,再进入VideoBroadCaster当中,broadcaster就会给sinks内里全部成员发一份数据。
至此,本地渲染链路就启动起来了。至于,拿到这些数据如何渲染到屏幕上,后续再分析。
b)远端发送链路:

上面创建了本地渲染链路,那么数据分发器VideoBroadCaster内里通常还会编码发送给远端。

由于大量媒体协商的内容在之前介绍过,我们就看下调用栈,关注我们视频相关内容即可。
调用栈:
  1. AdaptedVideoTrackSource::AddOrUpdateSink
  2. VideoSourceSinkController::SetSource
  3. VideoStreamEncoder::SetSource
  4. VideoSendStream::SetSource
  5. WebRtcVideoChannel::WebRtcVideoSendStream::RecreateWebRtcStream
  6. WebRtcVideoChannel::WebRtcVideoSendStream::SetCodec
  7. WebRtcVideoChannel::WebRtcVideoSendStream::SetSendParameters
  8. WebRtcVideoChannel::ApplyChangedParams
  9. WebRtcVideoChannel::SetSendParameters(应用获取到的编码参数设置)
  10. VideoChannel::SetRemoteContent_w
  11. // 切换到工作线程
  12. BaseChannel::SetRemoteContent
  13. SdpOfferAnswerHandler::PushdownMediaDescription(根据SDP媒体部分的描述,更新内部对象)
  14. SdpOfferAnswerHandler::UpdateSessionState(更新媒体协商状态机、媒体流、编解码器)
  15. SdpOfferAnswerHandler::ApplyRemoteDescription
  16. SdpOfferAnswerHandler::SetRemoteDescription
  17. PeerConnection::SetRemoteDescription
复制代码
AdaptedVideoTrackSource::AddOrUpdateSink当中:
  1. // 代码路径:media\base\adapted_video_track_source.cc
  2. void AdaptedVideoTrackSource::AddOrUpdateSink(
  3.     rtc::VideoSinkInterface<webrtc::VideoFrame>* sink,
  4.     const rtc::VideoSinkWants& wants) {
  5.   // 添加到broadcaster当中了
  6.   broadcaster_.AddOrUpdateSink(sink, wants);
  7.   OnSinkWantsChanged(broadcaster_.wants());
  8. }
复制代码
至此,SetRemoteDescription的时间就将视频编码器添加进去视频分发器VideoBroadcaster了。
c)视频数据流动:

那么,视频数据毕竟是如何进入到视频分发器VideoBroadcaster的呢?思路如下:
详细调用栈如下:
  1. TestVideoCapturer::OnFrame
  2. VcmCapturer::OnFrame
  3. VideoCaptureImpl::DeliverCapturedFrame
  4. VideoCaptureImpl::IncomingFrame
  5. CaptureSinkFilter::ProcessCapturedFrame
  6. CaptureInputPin::Receive
复制代码
看看详细函数:
  1. // 代码路径:modules\video_capture\windows\sink_filter_ds.cc
  2. /**
  3. * 接收采集到的视频数据时候,首先会进入到这儿
  4. * @param media_sample:就是采集到的数据
  5. */
  6. STDMETHODIMP CaptureInputPin::Receive(IMediaSample* media_sample) {
  7.   RTC_DCHECK_RUN_ON(&capture_checker_);
  8.   // 通过Filter()获取到这个pin所属的filter,也就是sinkFilter
  9.   CaptureSinkFilter* const filter = static_cast<CaptureSinkFilter*>(Filter());
  10.   // 收到数据之后调用这个方法将数据从pin传给filter
  11.   filter->ProcessCapturedFrame(sample_props.pbBuffer, sample_props.lActual,
  12.                                resulting_capability_);
  13.   return S_OK;
  14. }
复制代码
  1. // 代码路径:modules\video_capture\video_capture_impl.cc
  2. /**
  3. * 通过 SinkFilter 获取到数据之后,会调用此函数,
  4. * 这个函数会将采集到的数据统一转换为I420格式的数据(因为用户request的格式是I420)
  5. */
  6. int32_t VideoCaptureImpl::IncomingFrame(uint8_t* videoFrame,
  7.                                         size_t videoFrameLength,
  8.                                         const VideoCaptureCapability& frameInfo,
  9.                                         int64_t captureTime /*=0*/) {
  10.   // 由于我们最终采集的数据肯定是YUV,下面计算一些YUV相关的参数
  11.   int stride_y = width;
  12.   int stride_uv = (width + 1) / 2;
  13.   int target_width = width;
  14.   int target_height = abs(height);
  15.   // SetApplyRotation doesn't take any lock. Make a local copy here.
  16.   // 采集到数据帧是否进行了旋转
  17.   bool apply_rotation = apply_rotation_;
  18.   // 如果进行了旋转,那么,还要旋转回来
  19.   if (apply_rotation) {
  20.     // Rotating resolution when for 90/270 degree rotations.
  21.     if (_rotateFrame == kVideoRotation_90 ||
  22.         _rotateFrame == kVideoRotation_270) {
  23.       target_width = abs(height);
  24.       target_height = width;
  25.     }
  26.   }
  27.   // Setting absolute height (in case it was negative).
  28.   // In Windows, the image starts bottom left, instead of top left.
  29.   // Setting a negative source height, inverts the image (within LibYuv).
  30.   // TODO(nisse): Use a pool?
  31.   // 由于我们采集的数据不是I420,因此我们分配个I420的buffer,将数据转换为I420
  32.   rtc::scoped_refptr<I420Buffer> buffer = I420Buffer::Create(
  33.       target_width, target_height, stride_y, stride_uv, stride_uv);
  34.   libyuv::RotationMode rotation_mode = libyuv::kRotate0;
  35.   if (apply_rotation) {
  36.     switch (_rotateFrame) {
  37.       case kVideoRotation_0:
  38.         rotation_mode = libyuv::kRotate0;
  39.         break;
  40.       case kVideoRotation_90:
  41.         rotation_mode = libyuv::kRotate90;
  42.         break;
  43.       case kVideoRotation_180:
  44.         rotation_mode = libyuv::kRotate180;
  45.         break;
  46.       case kVideoRotation_270:
  47.         rotation_mode = libyuv::kRotate270;
  48.         break;
  49.     }
  50.   }
  51.   // 通过libyuv的方法将数据转换成I420
  52.   const int conversionResult = libyuv::ConvertToI420(
  53.       videoFrame, videoFrameLength, buffer.get()->MutableDataY(),
  54.       buffer.get()->StrideY(), buffer.get()->MutableDataU(),
  55.       buffer.get()->StrideU(), buffer.get()->MutableDataV(),
  56.       buffer.get()->StrideV(), 0, 0,  // No Cropping
  57.       width, height, target_width, target_height, rotation_mode,
  58.       ConvertVideoType(frameInfo.videoType));
  59.   if (conversionResult < 0) {
  60.     RTC_LOG(LS_ERROR) << "Failed to convert capture frame from type "
  61.                       << static_cast<int>(frameInfo.videoType) << "to I420.";
  62.     return -1;
  63.   }
  64.   // 将转换后的数据重新封装成一个 VideoFrame 格式
  65.   VideoFrame captureFrame =
  66.       VideoFrame::Builder()
  67.           .set_video_frame_buffer(buffer)
  68.           .set_timestamp_rtp(0)
  69.           .set_timestamp_ms(rtc::TimeMillis())
  70.           .set_rotation(!apply_rotation ? _rotateFrame : kVideoRotation_0)
  71.           .build();
  72.   captureFrame.set_ntp_time_ms(captureTime);
  73.   // 里面会调用 RegisterCaptureDataCallback 的onFrame,将数据传给onFrame函数
  74.   DeliverCapturedFrame(captureFrame);
  75.   return 0;
  76. }
复制代码
重点关注最后的DeliverCapturedFrame函数
  1. // 代码路径:modules\video_capture\video_capture_impl.cc
  2. /**
  3. * 里面会调用 RegisterCaptureDataCallback 的onFrame,将数据传给onFrame函数
  4. */
  5. int32_t VideoCaptureImpl::DeliverCapturedFrame(VideoFrame& captureFrame) {
  6.   UpdateFrameCount();  // frame count used for local frame rate callback.
  7.   if (_dataCallBack) {
  8.     _dataCallBack->OnFrame(captureFrame);
  9.   }
  10.   return 0;
  11. }
复制代码
然后就到了VcmCpaturer
  1. // 接收采集到视频数据(格式已经转换成用户请求的了)
  2. void VcmCapturer::OnFrame(const VideoFrame& frame) {
  3.   TestVideoCapturer::OnFrame(frame);
  4. }
复制代码
到了熟悉的TestVideoCapturer
  1. /**
  2. * 从 VcmCapturer::OnFrame 抛上来的
  3. */
  4. void TestVideoCapturer::OnFrame(const VideoFrame& original_frame) {
  5.   int cropped_width = 0;
  6.   int cropped_height = 0;
  7.   int out_width = 0;
  8.   int out_height = 0;
  9.   // 对原始视频帧进行处理(比如你加一些特效)
  10.   VideoFrame frame = MaybePreprocess(original_frame);
  11.   
  12.   if (out_height != frame.height() || out_width != frame.width()) {
  13.    // 缩放部分省略...
  14.   } else {
  15.     // 如果不需要缩放,那么直接交给 VideoBroadcaster 进行分发
  16.     // No adaptations needed, just return the frame as is.
  17.     broadcaster_.OnFrame(frame);
  18.   }
  19. }
复制代码
这样,就通过broadcaster分发给其内部已经添加的sink了。
五、总结:

本章主要介绍了视频数据采集的关键类VcmCapture、VideoTrack、VideoBroadcaster,VideoCapture。并且交接了这几个类的主要职责,以及如何利用他们创建一条数据链路的。后续,对详细的引擎再做分析。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4