Cap 开源项目中的 DirectShow 相机采集封装:cap-camera-directshow 的架构、API 与实战解析
2026/9/13 18:17:43 网站建设 项目流程

Cap 开源项目中的 DirectShow 相机采集封装:cap-camera-directshow 的架构、API 与实战解析

【免费下载链接】CapOpen source Loom alternative. Beautiful, shareable screen recordings.项目地址: https://gitcode.com/GitHub_Trending/cap1/Cap

导读

Cap 是一个开源 Loom 替代品,用于录制并分享高质量的屏幕与摄像头画面。在 Windows 平台上,为了让老式摄像头与旧驱动依然可用,Cap 在crates/camera-directshow中提供了对 Windows DirectShow API 的 Rust 安全封装cap-camera-directshow:它通过 COM 滤镜图(Filter Graph)完成设备枚举、格式协商与同步帧回调采集,并在上层与 Media Foundation 采集路径互为补充。阅读本文后,你将掌握该 crate 的核心 API 设计、start_capturing的完整调用链、自定义 Sink 滤镜的工作原理,以及如何在 Cap 的camera-windows层中将其作为媒体采集回退方案使用。

一、为什么 Cap 需要 DirectShow:定位与背景

DirectShow 是 Windows 上历史悠久的媒体框架,其基于 COM 的滤镜图架构至今仍是许多老式摄像头设备(尤其是采集卡、虚拟摄像头以及缺少 Media Foundation 驱动的硬件)唯一可靠的采集途径。Cap 在 crates/camera-directshow/README.md 中明确描述了该 crate 的目标:

  • 为“旧式摄像头采集”提供符合人体工学的 Rust 封装(Ergonomic Rust wrapper);
  • 在 COM 接口之上提供安全抽象,同时保持与不支持 Media Foundation 的旧设备/旧驱动的兼容性;
  • 在消费 DirectShow 的方式上对标 Chromium(Aims to mirror how Chromium consumes DirectShow),即采用“枚举 → 协商格式 → 建立滤镜图 → 通过自定义 Sink 滤镜回调取帧”的经典路线。

从 crates/camera-windows/src/lib.rs 的集成代码可以看到它的实际定位:get_devices()会同时枚举 Media Foundation 与 DirectShow 设备,并以name_and_model()为键将同一台设备的 MF/DS 两个实例“配对”,把 DS 设备挂到 MF 设备的dshow_fallback字段上——当 MF 设备激活失败或报不出格式时,采集流程会回退到 DirectShow,这正是该 crate 在项目中的核心价值场景(详见第七节)。

二、架构总览:把 COM 滤镜图模型桥接到 Rust 所有权体系

cap-camera-directshow的架构核心(见 crates/camera-directshow/src/lib.rs)由四层构成:

  1. COM 初始化层:通过initialize_directshow()完成CoInitialize,为后续 COM 调用建立公寓环境;
  2. 设备枚举层VideoInputDeviceIterator借助系统设备枚举器(ICreateDevEnum+CLSID_VideoInputDeviceCategory)与 COM Moniker 遍历视频采集设备;
  3. 格式协商层:通过采集引脚上的IAMStreamConfig枚举AM_MEDIA_TYPE能力(分辨率、像素格式、帧率);
  4. 同步采集层:自定义SinkFilter/SinkInputPin作为滤镜图的接收端,在IMemInputPin::Receive中以回调方式同步交付每一帧IMediaSample

这种“滤镜图 + 自定义 Sink + 回调”的组合,配合 RAII 管理 COM 对象生命周期,让调用方可以在完全不接触裸 COM 的情况下完成一次实时视频采集。

延迟绑定(Deferred Binding):枚举绝不打开设备

源码中有一处值得注意的设计(见 src/lib.rs 中VideoInputDevice的定义与注释):

#[derive(Clone)] pub struct VideoInputDevice { moniker: IMoniker, prop_bag: IPropertyBag, bound: OnceLock<BoundFilter>, }

VideoInputDevice内部用OnceLock<BoundFilter>缓存“已绑定的滤镜”。注释明确说明:绑定采集滤镜会通过 KS 驱动打开设备,这会消耗一个线程和数十个内核句柄,且这些句柄在释放时不会立即回收(对应仓库记录 CapSoftware/Cap#2132)。因此:

  • 纯枚举(获取name()/id()/model_id()永远不会触发绑定,只读取IPropertyBag中的属性;
  • 只有真正需要media_types()start_capturing()时,才通过BindToObject绑定滤镜,绑定结果以OnceLock缓存,多次调用不重复打开设备。

这保证了上层轮询设备列表时不会产生资源泄漏。

三、核心 API 详解

3.1 设备管理

API作用
initialize_directshow()初始化 DirectShow COM 子系统(内部调用CoInitialize
VideoInputDeviceIterator::new()通过系统设备枚举器枚举可用摄像头
VideoInputDevice::name()读取设备显示名称:优先读Description属性,回退到FriendlyName
VideoInputDevice::id()读取DevicePath属性(回退到设备名)
VideoInputDevice::model_id()从 DevicePath 中解析vid_xxxx/pid_xxxx,生成"vid:pid"形式的型号标识
VideoInputDevice::media_types()返回支持格式的迭代器(会触发延迟绑定)

设备枚举在源码中的实现(见 src/lib.rs 的VideoInputDeviceIterator::new):

let create_device_enum: ICreateDevEnum = CoCreateInstance( &CLSID_SystemDeviceEnum, None::<&windows_core::IUnknown>, CLSCTX_INPROC_SERVER, )?; let mut enum_moniker = None; create_device_enum.CreateClassEnumerator( &CLSID_VideoInputDeviceCategory, &mut enum_moniker, 0, )?;

其中有一个关键细节:CreateClassEnumerator在无设备时可能返回S_FALSE(被当作成功处理),所以枚举器可能为None,迭代器会自然结束而非报错——这是处理“无摄像头”场景的健壮做法。

model_id()的解析逻辑(见 src/lib.rs 的get_device_model_id)在设备路径中定位vid_pid_标记,取其后 4 位十六进制拼接为"vendor:product",这是上层判断设备类别(如是否是虚拟摄像头)的重要依据。

3.2 格式处理

  • AMMediaType:对AM_MEDIA_TYPE的安全包装。new()通过copy_media_typeCoTaskMemAlloc深拷贝pbFormatDrop时用CoTaskMemFree释放,杜绝内存泄漏;支持CloneDeref,可无缝传入采集 API。
  • AM_MEDIA_TYPEExt::subtype_str():把格式 GUID 映射为可读字符串,内置支持以下格式(见 src/lib.rs 的subtype_str实现):
GUID 常量返回字符串
MEDIASUBTYPE_I420i420
MEDIASUBTYPE_IYUViyuv
MEDIASUBTYPE_RGB24rgb24
MEDIASUBTYPE_RGB32rgb32
MEDIASUBTYPE_YUY2yuy2
MEDIASUBTYPE_MJPGmjpg
MEDIASUBTYPE_UYVYuyvy
MEDIASUBTYPE_ARGB32argb32
MEDIASUBTYPE_NV12nv12
MEDIASUBTYPE_YV12yv12
  • AM_MEDIA_TYPEVideoExt::video_info():将pbFormat强转为KS_VIDEOINFOHEADER,从而读取bmiHeader.biWidthbiHeightAvgTimePerFrame等视频尺寸与帧率信息。示例代码正是用它获取分辨率与默认帧率。

3.3 采集管线

API作用
VideoInputDevice::start_capturing(format, callback)以指定格式开始同步采集,返回CaptureHandle
CaptureHandle::stop_capturing()停止采集会话并断开滤镜图
SinkCallback帧处理回调,接收CallbackData

回调数据结构(见 src/lib.rs):

pub struct CallbackData<'a> { pub sample: &'a IMediaSample, // 当前帧样本 pub media_type: &'a AMMediaType, // 当前媒体类型 pub timestamp: Duration, // 采样时间戳(微秒) pub perf_counter: i64, // QueryPerformanceCounter 高精度计数器 } pub type SinkCallback = Box<dyn FnMut(CallbackData)>;

perf_counterQueryPerformanceCounter在每次Receive时采样,可供上层做精确的时间同步与性能统计。

3.4 滤镜图扩展 trait

  • IBaseFilterExt::get_pin():按“方向 + 引脚类别 + 主类型”三条件查找引脚。directionPINDIR_OUTPUT/PINDIR_INPUTcategorymajor_typeGUID::zeroed()表示不约束该项。内部用EnumPins遍历,并用matches_category/matches_major_type过滤。
  • IPinExt::matches_category():通过IKsPropertySet::Get读取AMPROPSETID_Pin/AMPROPERTY_PIN_CATEGORY判断引脚类别(如采集引脚PIN_CATEGORY_CAPTURE)。
  • IPinExt::matches_major_type():通过ConnectionMediaType读取已连接媒体类型的主类型。
  • IAMStreamConfigExt::media_types():先调GetNumberOfCapabilities获取能力数量,再逐个GetStreamCaps(i, ...)拉取AM_MEDIA_TYPEVIDEO_STREAM_CONFIG_CAPS,返回迭代器(可配合IAMVideoControlExt::time_per_frame_list()读取每种分辨率下的帧率列表)。

四、采集流水线深潜:start_capturing 的完整调用链

start_capturing(format, callback)(见 src/lib.rs)内部依次完成以下步骤:

  1. 绑定设备:通过bound()获取缓存的滤镜与采集引脚(失败映射为StartCapturingError::BindDevice);
  2. 设置格式:在IAMStreamConfig上调用SetFormat(&format),把AMMediaType写入设备;
  3. 创建 Sink 滤镜SinkFilter::new(format.clone(), callback),并取出唯一的输入引脚input_sink_pin(取不到则返回NoInputPin);
  4. 实例化滤镜图CoCreateInstance(CLSID_FilterGraph)CLSID_CaptureGraphBuilder2,并将IGraphBuilder强转为IMediaControl
  5. 构图SetFiltergraph绑定构图器;AddFilter依次加入设备滤镜与 Sink 滤镜;FindInterface(PIN_CATEGORY_CAPTURE, MEDIATYPE_Video, ...)重新取得IAMStreamConfig
  6. 连接graph_builder.Connect(&bound.output_pin, &input_sink_pin)把设备采集引脚接到 Sink 输入引脚;
  7. 运行media_control.Run()启动滤镜图,返回持有media_controlgraph_builder及两端引脚的CaptureHandle

整个流程中任何一步失败都会映射为对应的StartCapturingError变体(见第五节),保证错误可定位、可传播。

Sink 滤镜如何工作

SinkFilter实现了IBaseFilterIMediaFilterIPersist,其状态机(State_Stopped/State_Paused/State_Running)与JoinFilterGraph钩子(记录宿主图)都通过RefCell安全维护。真正的帧处理发生在SinkInputPin::ReceiveIMemInputPin_Impl):

  1. 记录QueryPerformanceCounter高精度时间;
  2. 若样本携带新的媒体类型(GetMediaType成功),则更新current_media_type
  3. 校验数据长度大于 0(否则返回S_FALSE跳过);
  4. 校验GetPointer可取到缓冲区;
  5. GetTime读取起止时间,换算为微秒级timestampstart_time / 10);
  6. 组装CallbackData并调用回调。

SinkInputPin同时实现了IPinIMemInputPin,覆盖ConnectReceiveConnectionDisconnectConnectionMediaTypeQueryAcceptEnumMediaTypes(返回期望格式)以及分配器相关接口,能够完整参与滤镜图的连接协商。

停止采集:对标 Chromium

CaptureHandle::stop_capturing()(见 src/lib.rs)先IMediaControl::Stop(),再主动Disconnect设备输出引脚与 Sink 输入引脚。源码注释明确指出该流程对标 Chromium 的VideoCaptureDeviceWin::StopAndDeallocate,确保滤镜图状态机回到停止态、资源可回收。

五、错误处理模型

StartCapturingError是一个基于thiserror的枚举(见 src/lib.rs),完整覆盖采集启动各阶段:

变体含义
BindDevice(windows_core::Error)绑定设备滤镜失败(延迟绑定阶段)
NoInputPinSink 滤镜输入引脚创建失败
CreateGraph(windows_core::Error)滤镜图 / 采集图构建器实例化失败
ConfigureGraph(windows_core::Error)滤镜连接与配置失败(构图、AddFilter、Connect 等)
Run(windows_core::Error)IMediaControl::Run执行失败
Other(windows_core::Error)其他通用 DirectShow COM 错误(如SetFormat失败)

由于 crate 的Drop实现(AMMediaType自动释放pbFormat)与CaptureHandle持有的 COM 引用都会在离开作用域时自动清理,错误传播过程中不会出现 COM 资源泄漏。这种“RAII + 强类型错误”的组合既满足了实时视频采集所需的回调架构,又保持了 Rust 的内存安全承诺。

六、开箱即用的命令行示例

仓库在 crates/camera-directshow/examples/cli.rs 提供了一个完整的交互式示例,演示了从枚举到采样的全流程:

  1. 初始化 COM(CoInitialize(None))并初始化tracing_subscriber
  2. VideoInputDeviceIterator::new()收集所有设备,通过inquire::Select交互选择;
  3. 取设备输出引脚并cast::<IAMVideoControl>(),为后续帧率查询做准备;
  4. media_types()枚举格式,过滤出MEDIATYPE_Video+FORMAT_VideoInfo的格式,读取biWidth/biHeight
  5. 帧率获取优先走IAMVideoControl::time_per_frame_list(对指定分辨率的GetFrameRateList),将time_per_frame换算为10_000_000.0 / t(100ns 单位换算为 fps)并保留两位小数;若列表为空,则回退用KS_VIDEOINFOHEADER::AvgTimePerFrame计算;
  6. 交互选择格式后调用start_capturing,回调中打印每帧的data_lengthtimestamp,持续 10 秒。

该示例展示的核心模式——先用IAMVideoControl精确获取帧率列表、失败再回退到AvgTimePerFrame——可以直接复用到真实产品中。需要注意的是,示例与整个 crate 一样仅支持 Windows(非 Windows 平台会直接panic!)。

七、与 camera-windows 的集成:作为 Media Foundation 的回退路径

cap-camera-directshow在上层被 crates/camera-windows/src/lib.rs 消费,形成“MF 优先、DS 兜底”的双通道策略:

  • 枚举get_devices()同时调用initialize_directshow()initialize_mediafoundation(),分别枚举 DS 设备与 MF 设备;对每个 DS 设备,若能在 MF 列表中按name_and_model()(名称 + model_id)找到尚未挂接回退的“孪生”MF 设备,则把 DS 实例挂到其dshow_fallback,否则 DS 设备单独入列(src/lib.rs 的get_devices实现);
  • 格式ds_formats(device)遍历media_types(),通过VideoFormat::new_ds转成统一的VideoFormat;MF 设备的格式列表会在激活失败时回退到dshow_fallback的格式;
  • 采集start_capturing按设备类型分发——MediaFoundation + MF format走 MF 采集;DirectShowMediaFoundation { dshow_fallback: Some(..) } + DirectShow format走 DS 采集。DS 回调中通过KS_VIDEOINFOHEADER读取biWidth/biHeight,并结合directshow_frame_is_bottom_up判断是否需要翻转:对 RGB24/RGB32/BGR24/ARGB/RGB565 这类传统自下而上(bottom-up)的像素格式,biHeight > 0即表示图像方向需要处理(src/lib.rs 的directshow_frame_is_bottom_up)。

这种设计正是 README 中“兼容不支持 Media Foundation 的旧设备”这一目标的落地实现:一台同时被 MF 与 DS 注册的设备,MF 路径不可用时无需用户干预即可平滑切换

八、构建与使用注意事项

从 crates/camera-directshow/Cargo.toml 可以看出该 crate 的使用前提:

  • 平台限制:源码开头为#![cfg(windows)]windowswindows-core依赖位于[target.'cfg(windows)'.dependencies],因此只能在 Windows 目标上编译使用;
  • 依赖特性:启用windowscrate 的Win32_System_ComWin32_Media_DirectShowWin32_Media_MediaFoundationWin32_System_Com_StructuredStorageWin32_System_OleWin32_System_VariantWin32_System_PerformanceWin32_Media_KernelStreaming特性(Win32_Media_KernelStreaming提供KS_VIDEOINFOHEADERWin32_System_Performance提供QueryPerformanceCounter);
  • 工作区集成:依赖workspace-hacktracingthiserror,遵循工作区统一的 lints 与 profile 配置;示例的inquiretracing-subscriber仅作为 dev-dependencies;
  • crate 元信息:包名为cap-camera-directshow,版本0.1.0,edition 2024,MIT 协议;它是 Cap 工作区(见根目录 Cargo.toml 的 workspace members)中crates/*的一员。

在仓库中运行示例的方式(Windows 环境):

cargo run -p cap-camera-directshow --example cli

结语

cap-camera-directshow用不到 1200 行的 Rust 代码,把 DirectShow 的 COM 滤镜图体系封装成了一套类型安全、资源安全且贴近产品需求的 API:延迟绑定避免枚举泄漏、AMMediaType以 RAII 管理原生内存、StartCapturingError精确刻画启动失败点、自定义SinkFilter支撑同步回调取帧。在 Cap 的 Windows 相机链路中,它与 Media Foundation 路径互为镜像,共同保证了从现代网络摄像头到老式采集卡的广泛兼容性。对于希望在 Rust 中消费 DirectShow 的开发者,这是一个值得直接参考的完整范本。

【免费下载链接】CapOpen source Loom alternative. Beautiful, shareable screen recordings.项目地址: https://gitcode.com/GitHub_Trending/cap1/Cap

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询