WebRTC视频 01 - 视频采集整体架构
WebRTC视频 02 - 视频采集类 VideoCaptureModule(本文)
WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇
WebRTC视频 04 - 视频采集类 VideoCaptureDS 中篇
WebRTC视频 05 - 视频采集类 VideoCaptureDS 下篇
一、前言:
上一篇文章介绍了webrtc视频采集整体架构,分析了几个关键类的类关系,以及如何通过这几个类建立视频采集链路。主要在软件层面进行了分析,本节着重分析下如何进行设备层操作的。总体负责操作设备层的类叫做VideoCaptureModule(简称VCM),Windows平台负责具体操作硬件的组件是DirectShow。
二、DirectShow:
1、简介:
DirectShow是一种由微软开发的多媒体框架,主要用于Windows平台上处理和操控流媒体数据。该技术提供了一组API,支持音频和视频的捕获、处理、转换和播放。下面是对DirectShow的一些主要特性的介绍:
- 模块化结构:
- DirectShow基于过滤器(filter)的架构,每个过滤器执行特定的任务,如源读取、数据解析、编码、解码、渲染等。过滤器之间通过引脚(pin)连接,形成一个可定制的处理链。
- 多种格式支持:
- 支持多种媒体格式,包括AVI、MPEG、ASF、WAV、MP3等。这使得开发者可以构建能够处理多种媒体格式的应用程序。
- 实时流媒体处理:
- 能够处理实时数据流,非常适合用于视频会议、视频广播等需要实时处理的场景。
- 灵活性和可扩展性:
- 开发者可以创建自定义过滤器,以支持新的数据格式或实现新的处理算法。此外,还可以通过编程接口动态构建和操控过滤器图(filter graph)。
- 系统集成性:
- 作为Windows媒体框架的一部分,DirectShow与Windows操作系统和DirectX集成良好,能够利用硬件加速功能以提高性能。
- 应用领域:
- DirectShow被广泛应用于视频编辑软件、播放软件、流媒体服务以及各种需要音频视频处理的应用中。
尽管DirectShow功能强大,但随着Windows平台的发展,微软推出了更新的多媒体框架,如Windows Media Foundation,以提供更现代化的功能和更好的性能。在Windows 10及更高版本中,建议使用新的框架进行开发。
2、Filter
Filter 是DirectShow架构的基本单元,每个Filter执行特定的媒体数据处理功能。Filter可以分为以下几类:
- 源过滤器(Source Filter):负责从文件、设备或网络读取媒体数据。
- 变换过滤器(Transform Filter):用于对流媒体数据进行处理和转换,如编解码、特效处理等。
- 渲染过滤器(Renderer Filter):负责将媒体数据输出到设备,如显示到屏幕或播放到音频设备。
每个Filter通常有一个或多个输入和输出连接点,称为Pins,用于连接其他过滤器。
3、Filter Graph
Filter Graph 是一组按特定顺序连接的Filters,它定义了媒体流的处理路线。Filter Graph是DirectShow的核心,处理步骤如下:
- 定义要使用的Filters及其顺序。
- 连接Filters之间的Pins以建立媒体流的传输路径。
- 控制媒体流的运行状态,如开始、停止、暂停等。
Filter Graph的设计允许灵活地构建不同的媒体处理链条,以满足不同的应用需求。
4、Filter Graph Manager
Filter Graph Manager 是负责创建和管理Filter Graph的组件。它提供的主要功能包括:
- 自动构建和连接Filters来形成一个完整的Graph。
- 管理Graph的状态与控制,如启动、暂停、停止图中所有Filters。
- 提供接口供应用程序访问和操作Filter Graph。
通过Filter Graph Manager,开发者可以更容易地管理Filter Graph的生命周期和处理流程。
5、Pin
Pin 是Filters之间的数据连接点,用于传递媒体数据流。Pin分为两类:
- 输入Pin:接收来自上游过滤器的数据。
- 输出Pin:发送数据到下游过滤器。
Pins之间的连接称为“pin connection”,数据在Filter Graph中通过Pins在不同的Filters之间传递。Pins还会协商数据格式和流类型,以确保兼容性。
6、工作过程
在DirectShow的工作过程中,首先创建Filter Graph,然后通过Filter Graph Manager将合适的Filters添加到Graph,并通过Pins连接这些Filters,形成一个完整的处理链。当播放或采集媒体时,媒体数据会按照预定义的过程通过各类Filters处理,最终输出到用户设定的目标设备或存储介质中。
这种模块化和灵活性使DirectShow成为构建复杂媒体应用的强大工具。
三、核心类图:
上面类图的中最核心的就是VideoCaptureModule和DeviceInfo这两个接口类,这两个接口类的子类对象在什么时候创建的呢?如果还记得上一篇文章的代码,就是在VcmCapturer::Init()中利用VideoCaptureFactory创建的。
- DeviceInfo:主要存储采集设备的相关信息;比如,采集设备的数量,采集设备的能力(分辨率、帧率等),采集设备的方向;
- 上面两个重要的类都是接口类,在Windows平台下,实现者分别是VideoCaptureDS和DeviceInfoDS(其中DS就是DirectShow的缩写);
- VideoCaptureModule方法包括开始和停止采集,以及一个注册回调的接口,采集到数据之后,通过这个回调上传给上层。
- VideoCaptureDS成员:
- _dsInfo:就是DeviceInfoDS。
- captureFilter:主要负责视频的采集;
- graphBuilder:用于构造FilterGraph;
- mediaControl:用于控制FilterGraph什么时候开启,什么时候停止;
- sink_filter:使用captureFilter采集的数据,最终输出到Sink当中;
四、采集步骤:
具体函数在VcmCapturer::Init当中:
bool VcmCapturer::Init(size_t width,
size_t height,
size_t target_fps,
size_t capture_device_index) {
// 创建 DeviceInfo 对象
std::unique_ptr<VideoCaptureModule::DeviceInfo> device_info(
VideoCaptureFactory::CreateDeviceInfo());
char device_name[256];
char unique_name[256];
if (device_info->GetDeviceName(static_cast<uint32_t>(capture_device_index),
device_name, sizeof(device_name), unique_name,
sizeof(unique_name)) != 0) {
Destroy();
return false;
}
// 创建 VideoCapture 对象
vcm_ = webrtc::VideoCaptureFactory::Create(unique_name);
if (!vcm_) {
return false;
}
vcm_->RegisterCaptureDataCallback(this);
// 获取摄像头Capability
device_info->GetCapability(vcm_->CurrentDeviceName(), 0, capability_);
capability_.width = static_cast<int32_t>(width);
capability_.height = static_cast<int32_t>(height);
capability_.maxFPS = static_cast<int32_t>(target_fps);
capability_.videoType = VideoType::kI420;
// 通知 VideoCapture 开始采集
if (vcm_->StartCapture(capability_) != 0) {
Destroy();
return false;
}
RTC_CHECK(vcm_->CaptureStarted());
return true;
}
注意创建DeviceInfo和VideoCapture的具体对象,都是通过VideoCaptureFactory完成的,等会儿会详细分析。
VcmCaptureer::Init方法主要做了六件事:
- 创建并获取DeviceInfo;
- 创建并获取VideoCaptureModule,也就是真正的视频采集模块;
- 然后就将VcmCapturer自己注册到VideoCaptureModule中来接收采集的数据;
- 再就是通过DeviceInfo::GetCapability获取设备的能力,分辨率是多少,帧率是多少,使用的视频类型是什么。获取到这个能力之后,在初始化函数会修改这个能力为用户想要的能力。
- 然后通过StartCapture开始根据capability采集视频数据。
- 最后,通过CaptureStarted检测设备状态是否为已经采集状态。
下面看看这个非常关键的VCM类做了什么。
五、VideoCaptureModule:
1、采集通道搭建:
从前面的类图可知VideoCaptureModule只是一个接口类,它的实现类为VideoCaptureImpl,屏蔽了平台差异,不同平台又有自己的实现,比如Linux为VideoCaptureModuleV4L2,Windows平台为VideoCaptureDS,我是在Windows平台运行的,所以重点关注VideoCaptureDS。
我们前面说了VideoCaptureModule对象是通过VideoCaptureFactory::创建的:
vcm_ = webrtc::VideoCaptureFactory::Create(unique_name);
我们进去看看:
rtc::scoped_refptr<VideoCaptureModule> VideoCaptureFactory::Create(
const char* deviceUniqueIdUTF8) {
#if defined(WEBRTC_ANDROID) || defined(WEBRTC_MAC)
return nullptr;
#else
// linux 和 windows 平台走这儿
return videocapturemodule::VideoCaptureImpl::Create(deviceUniqueIdUTF8);
#endif
}
发现转手调用了VideoCaptureImpl里面的方法,再进去看看:
rtc::scoped_refptr<VideoCaptureModule> VideoCaptureImpl::Create(
const char* device_id) {
if (device_id == nullptr)
return nullptr;
// TODO(tommi): Use Media Foundation implementation for Vista and up.
// 创建了VideoCaptureDS对象,并进行初始化
rtc::scoped_refptr<VideoCaptureDS> capture(
new rtc::RefCountedObject<VideoCaptureDS>());
if (capture->Init(device_id) != 0) {
return nullptr;
}
return capture;
}
发现里面直接new了一个VideoCaptureDS对象,并进行初始化。
然后分析下VideoCaptureDS::Init函数;这里面有很多是和前面介绍的DirectShow相关的,具体代码:
int32_t VideoCaptureDS::Init(const char* deviceUniqueIdUTF8) {
const int32_t nameLength = (int32_t)strlen((char*)deviceUniqueIdUTF8);
if (nameLength > kVideoCaptureUniqueNameLength)
return -1;
// Store the device name
_deviceUniqueId = new (std::nothrow) char[nameLength + 1];
memcpy(_deviceUniqueId, deviceUniqueIdUTF8, nameLength + 1);
if (_dsInfo.Init() != 0)
return -1;
// 构造CaptureFilter
_captureFilter = _dsInfo.GetDeviceFilter(deviceUniqueIdUTF8);
if (!_captureFilter) {
RTC_LOG(LS_INFO) << "Failed to create capture filter.";
return -1;
}
// Get the interface for DirectShow's GraphBuilder
// 创建FilterGraph,并返回IGraphBuilder接口
HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
IID_IGraphBuilder, (void**)&_graphBuilder);
if (FAILED(hr)) {
RTC_LOG(LS_INFO) << "Failed to create graph builder.";
return -1;
}
// 获取IMediaControl接口,用于控制数据的流转
hr = _graphBuilder->QueryInterface(IID_IMediaControl, (void**)&_mediaControl);
if (FAILED(hr)) {
RTC_LOG(LS_INFO) << "Failed to create media control builder.";
return -1;
}
// 将前面构造好的CaptureFilter添加到FilterGraph当中
hr = _graphBuilder->AddFilter(_captureFilter, CAPTURE_FILTER_NAME);
if (FAILED(hr)) {
RTC_LOG(LS_INFO) << "Failed to add the capture device to the graph.";
return -1;
}
// 获取CaptureFilter的输出Pin
_outputCapturePin = GetOutputPin(_captureFilter, PIN_CATEGORY_CAPTURE);
if (!_outputCapturePin) {
RTC_LOG(LS_INFO) << "Failed to get output capture pin";
return -1;
}
// Create the sink filte used for receiving Captured frames.
// 开始构造CaptureSinkFilter
sink_filter_ = new ComRefCount<CaptureSinkFilter>(this);
// 将CaptureSinkFilter加入到GraphicBuilder当中
hr = _graphBuilder->AddFilter(sink_filter_, SINK_FILTER_NAME);
if (FAILED(hr)) {
RTC_LOG(LS_INFO) << "Failed to add the send filter to the graph.";
return -1;
}
// 获取SinkFilter的输入pin
_inputSendPin = GetInputPin(sink_filter_);
if (!_inputSendPin) {
RTC_LOG(LS_INFO) << "Failed to get input send pin";
return -1;
}
// Temporary connect here.
// This is done so that no one else can use the capture device.
// 将两个filter的pin连接起来
if (SetCameraOutput(_requestedCapability) != 0) {
return -1;
}
// 先暂停,因为此时还没有数据过来
hr = _mediaControl->Pause();
if (FAILED(hr)) {
RTC_LOG(LS_INFO)
<< "Failed to Pause the Capture device. Is it already occupied? " << hr;
return -1;
}
RTC_LOG(LS_INFO) << "Capture device '" << deviceUniqueIdUTF8
<< "' initialized.";
return 0;
}
我虽然每一步都写了注释,还是小结下:
- DeviceInfo::Init准备好设备需要的环境;
- 获取用于采集视频数据的CaptureFilter;
- 接下来就是创建一个IGraphBuilder,这个接口主要做了两件事:
- 创建IMediaControl接口,这个主要是控制采集的,比如,开始采集,停止采集等;
- AddFilter将之前获取到的CaptureFilter加入到FilterGraph当中,后面再给FilterGraph添加一个Sink,就可以获取CaptureFilter采集到的数据了;
- GetOutputPin获取CaptureFilter的输出引脚;
- 接下来就是通过CaptureSinkFilter创建SinkFilter了;
- 然后通过IGraphBuilder::AddFilter将SinkFilter添加到GraphFilter当中;
- 然后获取SinkFilter的输入引脚,通过GetInputPin;
- 通过SetCameraOutput将CaptureFilter的输出引脚和SinkFilter的输入引脚进行连接;数据就可以源源不断的从capture->sink。代码中其实就是就VideoCaptureDS作为入参传给SinkFilter;这样就一层层传给上层。
- 调用meidaControl->Pause暂停数据采集,因为这时候我们还不需要数据采集,什么时候开始呢?就是VcmCapture的CaptureStarted调用之后。
至此,数据通道就已经建立好了,通道启动之后,数据流向就是这样:CaptureFilter->SinkFilter->VideoCaptureDS->VcmCapture->上层应用。
2、开始采集:
就是VcmCapturer::Init最后一步 vcm_->StartCapture(capability_) 开始,启动采集了。
int32_t VideoCaptureDS::StartCapture(const VideoCaptureCapability& capability) {
MutexLock lock(&api_lock_);
// 如果用户请求的能力,和我们当前设备能力不同,需要重新连接下Filter
if (capability != _requestedCapability) {
DisconnectGraph();
if (SetCameraOutput(capability) != 0) {
return -1;
}
}
// 通过IMediaControl接口通知底层开始采集
HRESULT hr = _mediaControl->Run();
if (FAILED(hr)) {
RTC_LOG(LS_INFO) << "Failed to start the Capture device.";
return -1;
}
return 0;
}
六、总结:
本文主要说明了应用层如何通过VideoCaptureModule去控制DirectShow进行视频采集的,主要是创建了FilterGraph,并往里添加了输入CaptureFilter、输出SinkFilter两个Filter,最后通过SetCameraOutput将CaptureFilter的输出pin和SinkFilter的输入pin连起来,并通过IMediaControl接口启动采集。
后续再写一篇分析下和Filter相关的这几个步骤里面都是怎么做的。