From b37bded58e3ebc228ef32e2170a5e27d7d5b1f38 Mon Sep 17 00:00:00 2001 From: "wangyang (wysaid)" Date: Sat, 7 Mar 2026 15:08:13 +0800 Subject: [PATCH 01/11] Add MSMF backend on Windows with DirectShow fallback --- CMakeLists.txt | 41 +- README.md | 16 +- README.zh-CN.md | 16 +- bindings/rust/README.md | 10 +- bindings/rust/src/provider.rs | 94 ++- cli/args_parser.cpp | 1 + docs/content/cli.md | 2 + docs/content/cli.zh.md | 2 + docs/content/documentation.md | 4 +- docs/content/documentation.zh.md | 4 +- docs/content/implementation-details.md | 5 + docs/content/implementation-details.zh.md | 5 + docs/content/rust-bindings.md | 4 +- docs/content/rust-bindings.zh.md | 4 +- docs/index.html | 10 +- include/ccap_c.h | 8 +- include/ccap_core.h | 46 +- include/ccap_def.h | 17 +- src/ccap_core.cpp | 329 +++++++- src/ccap_imp_windows_msmf.cpp | 911 ++++++++++++++++++++++ src/ccap_imp_windows_msmf.h | 99 +++ tests/CMakeLists.txt | 31 + tests/test_windows_backends.cpp | 449 +++++++++++ 23 files changed, 2041 insertions(+), 67 deletions(-) create mode 100644 src/ccap_imp_windows_msmf.cpp create mode 100644 src/ccap_imp_windows_msmf.h create mode 100644 tests/test_windows_backends.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 190314ec..cd635f61 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -124,7 +124,6 @@ if (CMAKE_SYSTEM_PROCESSOR MATCHES "[Aa][Rr][Mm]64|[Aa][Aa][Rr][Cc]h64" OR endif () endif () - file(GLOB LIB_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/*.h) # Exclude file reader sources if file playback is disabled @@ -235,31 +234,31 @@ elseif (UNIX AND NOT APPLE AND NOT WIN32) # Propagate to pkg-config for consumers set(PKG_CONFIG_LIBS_PRIVATE "Libs.private: -lpthread") elseif (WIN32) - # Windows – link Media Foundation libraries for video file playback (if enabled) + # Windows – Media Foundation is used by the MSMF camera backend and optional file playback. + target_link_libraries(ccap PUBLIC + mf + mfplat + mfreadwrite + mfuuid) + if (CCAP_ENABLE_FILE_PLAYBACK) target_link_libraries(ccap PUBLIC - mfplat - mfreadwrite - mfuuid shlwapi propsys) - - # MSVC: Delay load Media Foundation DLLs to support Windows XP camera functionality - # The program can start on XP and use DirectShow cameras, but will crash if video playback is attempted - if (MSVC) - target_link_options(ccap PRIVATE - /DELAYLOAD:mfplat.dll - /DELAYLOAD:mfreadwrite.dll - ) - target_link_libraries(ccap PUBLIC delayimp.lib) - endif () - - # Set pkg-config private libs for Windows with Media Foundation - set(PKG_CONFIG_LIBS_PRIVATE "Libs.private: -lmfplat -lmfreadwrite -lmfuuid -lshlwapi -lpropsys") + set(PKG_CONFIG_LIBS_PRIVATE "Libs.private: -lmf -lmfplat -lmfreadwrite -lmfuuid -lshlwapi -lpropsys") else () - # No Media Foundation libraries when file playback is disabled - set(PKG_CONFIG_LIBS_PRIVATE "") - message(STATUS "ccap: Windows build without Media Foundation libraries (file playback disabled)") + set(PKG_CONFIG_LIBS_PRIVATE "Libs.private: -lmf -lmfplat -lmfreadwrite -lmfuuid") + message(STATUS "ccap: Windows build without file playback extras (shlwapi/propsys not linked)") + endif () + + # MSVC: Delay load Media Foundation DLLs to preserve legacy DirectShow-only startup paths. + if (MSVC) + target_link_options(ccap PRIVATE + /DELAYLOAD:mf.dll + /DELAYLOAD:mfplat.dll + /DELAYLOAD:mfreadwrite.dll + ) + target_link_libraries(ccap PUBLIC delayimp.lib) endif () else () # Other platforms diff --git a/README.md b/README.md index f0012c0f..135598ab 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ A high-performance, lightweight cross-platform camera capture library with hardw - **High Performance**: Hardware-accelerated pixel format conversion with up to 10x speedup (AVX2, Apple Accelerate, NEON) - **Lightweight**: No third-party dependencies - uses only system frameworks -- **Cross Platform**: Windows (DirectShow), macOS/iOS (AVFoundation), Linux (V4L2) +- **Cross Platform**: Windows (Media Foundation with DirectShow fallback), macOS/iOS (AVFoundation), Linux (V4L2) - **Multiple Formats**: RGB, BGR, YUV (NV12/I420) with automatic conversion - **Dual Language APIs**: ✨ **Complete Pure C Interface** - Both modern C++ API and traditional C99 interface for various project integration and language bindings - **Video File Playback**: 🎬 Play video files (MP4, AVI, MOV, etc.) using the same API as camera capture - supports Windows and macOS @@ -211,6 +211,18 @@ int main() { } ``` +### Windows Backend Selection + +On Windows, camera capture uses Media Foundation by default and falls back to DirectShow when needed. You can override that choice when debugging device issues or validating backend-specific behavior: + +- Pass `extraInfo` as `"auto"`, `"msmf"`, `"dshow"`, or `"backend="` in the C++/C constructors that accept it. +- Set the environment variable `CCAP_WINDOWS_BACKEND=auto|msmf|dshow` to affect the whole process, including the CLI and Rust bindings. + +```cpp +ccap::Provider msmfProvider("", "msmf"); +ccap::Provider dshowProvider("", "dshow"); +``` + ### Rust Bindings Rust bindings are available as a crate on crates.io: @@ -276,7 +288,7 @@ For complete CLI documentation, see [CLI Tool Guide](./docs/content/cli.md). | Platform | Compiler | System Requirements | | -------- | -------- | ------------------- | -| **Windows** | MSVC 2019+ (including 2026) / MinGW-w64 | DirectShow | +| **Windows** | MSVC 2019+ (including 2026) / MinGW-w64 | Media Foundation (default) + DirectShow fallback | | **macOS** | Xcode 11+ | macOS 10.13+ | | **iOS** | Xcode 11+ | iOS 13.0+ | | **Linux** | GCC 7+ / Clang 6+ | V4L2 (Linux 2.6+) | diff --git a/README.zh-CN.md b/README.zh-CN.md index a40cdf80..8a8140ea 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -33,7 +33,7 @@ - **高性能**:硬件加速的像素格式转换,提升高达 10 倍性能(AVX2、Apple Accelerate、NEON) - **轻量级**:无第三方库依赖,仅使用系统框架 -- **跨平台**:Windows(DirectShow)、macOS/iOS(AVFoundation)、Linux(V4L2) +- **跨平台**:Windows(Media Foundation,自动回退到 DirectShow)、macOS/iOS(AVFoundation)、Linux(V4L2) - **多种格式**:RGB、BGR、YUV(NV12/I420)及自动转换 - **双语言接口**:✨ **新增完整纯 C 接口**,同时提供现代化 C++ API 和传统 C99 接口,支持各种项目集成和语言绑定 - **视频文件播放**:🎬 使用与相机相同的 API 播放视频文件(MP4、AVI、MOV 等)- 支持 Windows 和 macOS @@ -174,6 +174,18 @@ int main() { } ``` +### Windows 后端选择 + +Windows 上默认优先使用 Media Foundation,并在需要时自动回退到 DirectShow。如果你要排查设备兼容性问题,或验证某个后端的行为,可以显式指定后端: + +- 在支持 `extraInfo` 的 C++ / C 构造接口中传入 `"auto"`、`"msmf"`、`"dshow"` 或 `"backend="`。 +- 设置环境变量 `CCAP_WINDOWS_BACKEND=auto|msmf|dshow`,对整个进程生效,包括 CLI 和 Rust 绑定。 + +```cpp +ccap::Provider msmfProvider("", "msmf"); +ccap::Provider dshowProvider("", "dshow"); +``` + ### Rust 绑定 本项目提供 Rust bindings(已发布到 crates.io): @@ -237,7 +249,7 @@ cmake --build . | 平台 | 编译器 | 系统要求 | |------|--------|----------| -| **Windows** | MSVC 2019+(包括 2026)/ MinGW-w64 | DirectShow | +| **Windows** | MSVC 2019+(包括 2026)/ MinGW-w64 | Media Foundation(默认)+ DirectShow 回退 | | **macOS** | Xcode 11+ | macOS 10.13+ | | **iOS** | Xcode 11+ | iOS 13.0+ | | **Linux** | GCC 7+ / Clang 6+ | V4L2 (Linux 2.6+) - 相机捕获支持,视频播放暂不支持 | diff --git a/bindings/rust/README.md b/bindings/rust/README.md index 8d730e3e..604f7c7a 100644 --- a/bindings/rust/README.md +++ b/bindings/rust/README.md @@ -4,14 +4,14 @@ [![Documentation](https://docs.rs/ccap-rs/badge.svg)](https://docs.rs/ccap-rs) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -Safe Rust bindings for [CameraCapture (ccap)](https://github.com/wysaid/CameraCapture) — a high-performance, lightweight, cross-platform **webcam/camera capture** library with **hardware-accelerated pixel format conversion** (Windows DirectShow, macOS/iOS AVFoundation, Linux V4L2). +Safe Rust bindings for [CameraCapture (ccap)](https://github.com/wysaid/CameraCapture) — a high-performance, lightweight, cross-platform **webcam/camera capture** library with **hardware-accelerated pixel format conversion** (Windows Media Foundation with DirectShow fallback, macOS/iOS AVFoundation, Linux V4L2). > Note: The published *package* name on crates.io is `ccap-rs`, but the *crate name in code* is `ccap`. ## Features - **High Performance**: Hardware-accelerated pixel format conversion with up to 10x speedup (AVX2, Apple Accelerate, NEON) -- **Cross Platform**: Windows (DirectShow), macOS/iOS (AVFoundation), Linux (V4L2) +- **Cross Platform**: Windows (Media Foundation with DirectShow fallback), macOS/iOS (AVFoundation), Linux (V4L2) - **Multiple Formats**: RGB, BGR, YUV (NV12/I420) with automatic conversion - **Zero Dependencies**: Uses only system frameworks - **Memory Safe**: Safe Rust API with automatic resource management @@ -149,9 +149,11 @@ An ASan-instrumented `libccap.a` requires the ASan runtime at link/run time. ## Platform notes -- Camera capture: Windows (DirectShow), macOS/iOS (AVFoundation), Linux (V4L2) +- Camera capture: Windows (Media Foundation with DirectShow fallback), macOS/iOS (AVFoundation), Linux (V4L2) - Video file playback support depends on the underlying C/C++ library backend (currently Windows/macOS only). +On Windows, you can force backend selection by setting `CCAP_WINDOWS_BACKEND=auto|msmf|dshow`, or by using `Provider::with_device_name_and_extra_info`, `Provider::with_device_and_extra_info`, `Provider::open_device_with_extra_info`, and `Provider::open_with_index_and_extra_info`. + ## API Documentation ### Core Types @@ -188,7 +190,7 @@ match provider.grab_frame(3000) { // 3 second timeout | Platform | Backend | Status | | -------- | ------------ | ------------ | -| Windows | DirectShow | ✅ Supported | +| Windows | Media Foundation + DirectShow fallback | ✅ Supported | | macOS | AVFoundation | ✅ Supported | | iOS | AVFoundation | ✅ Supported | | Linux | V4L2 | ✅ Supported | diff --git a/bindings/rust/src/provider.rs b/bindings/rust/src/provider.rs index 15ddeb3d..20c3a19d 100644 --- a/bindings/rust/src/provider.rs +++ b/bindings/rust/src/provider.rs @@ -19,6 +19,16 @@ unsafe impl Sync for SendSyncPtr {} // Global error callback storage - must be at module level to be shared between functions static GLOBAL_ERROR_CALLBACK: Mutex> = Mutex::new(None); +fn optional_c_string(value: Option<&str>, parameter_name: &str) -> Result> { + value + .map(|text| { + CString::new(text).map_err(|_| { + CcapError::InvalidParameter(format!("{} contains null byte", parameter_name)) + }) + }) + .transpose() +} + /// Type alias for the global error callback /// /// # Thread Safety @@ -79,7 +89,24 @@ impl Provider { /// Create a provider with a specific device index pub fn with_device(device_index: i32) -> Result { - let handle = unsafe { sys::ccap_provider_create_with_index(device_index, ptr::null()) }; + Self::with_device_and_extra_info(device_index, None) + } + + /// Create a provider with a specific device index and optional extra info. + /// + /// On Windows, `extra_info` can be used to force backend selection with values like + /// `"auto"`, `"msmf"`, `"dshow"`, or `"backend="`. + pub fn with_device_and_extra_info( + device_index: i32, + extra_info: Option<&str>, + ) -> Result { + let extra_info = optional_c_string(extra_info, "extra info")?; + let handle = unsafe { + sys::ccap_provider_create_with_index( + device_index, + extra_info.as_ref().map_or(ptr::null(), |value| value.as_ptr()), + ) + }; if handle.is_null() { return Err(CcapError::InvalidDevice(format!( "device index {}", @@ -98,11 +125,28 @@ impl Provider { /// Create a provider with a specific device name pub fn with_device_name>(device_name: S) -> Result { + Self::with_device_name_and_extra_info(device_name, None) + } + + /// Create a provider with a specific device name and optional extra info. + /// + /// On Windows, `extra_info` can be used to force backend selection with values like + /// `"auto"`, `"msmf"`, `"dshow"`, or `"backend="`. + pub fn with_device_name_and_extra_info>( + device_name: S, + extra_info: Option<&str>, + ) -> Result { let c_name = CString::new(device_name.as_ref()).map_err(|_| { CcapError::InvalidParameter("device name contains null byte".to_string()) })?; + let extra_info = optional_c_string(extra_info, "extra info")?; - let handle = unsafe { sys::ccap_provider_create_with_device(c_name.as_ptr(), ptr::null()) }; + let handle = unsafe { + sys::ccap_provider_create_with_device( + c_name.as_ptr(), + extra_info.as_ref().map_or(ptr::null(), |value| value.as_ptr()), + ) + }; if handle.is_null() { return Err(CcapError::InvalidDevice(device_name.as_ref().to_string())); } @@ -213,6 +257,19 @@ impl Provider { /// Open device with optional device name and auto start pub fn open_device(&mut self, device_name: Option<&str>, auto_start: bool) -> Result<()> { + self.open_device_with_extra_info(device_name, None, auto_start) + } + + /// Open a device with optional device name, optional extra info, and optional auto start. + /// + /// On Windows, `extra_info` can be used to force backend selection with values like + /// `"auto"`, `"msmf"`, `"dshow"`, or `"backend="`. + pub fn open_device_with_extra_info( + &mut self, + device_name: Option<&str>, + extra_info: Option<&str>, + auto_start: bool, + ) -> Result<()> { if let Some(name) = device_name { // Recreate provider with specific device if !self.handle.is_null() { @@ -228,12 +285,19 @@ impl Provider { let c_name = CString::new(name).map_err(|_| { CcapError::InvalidParameter("device name contains null byte".to_string()) })?; - self.handle = - unsafe { sys::ccap_provider_create_with_device(c_name.as_ptr(), ptr::null()) }; + let extra_info = optional_c_string(extra_info, "extra info")?; + self.handle = unsafe { + sys::ccap_provider_create_with_device( + c_name.as_ptr(), + extra_info.as_ref().map_or(ptr::null(), |value| value.as_ptr()), + ) + }; if self.handle.is_null() { return Err(CcapError::InvalidDevice(name.to_string())); } self.is_opened = true; + } else if extra_info.is_some() { + return self.open_with_index_and_extra_info(-1, extra_info, auto_start); } else { self.open()?; } @@ -504,6 +568,19 @@ impl Provider { /// Open device with index and auto start pub fn open_with_index(&mut self, device_index: i32, auto_start: bool) -> Result<()> { + self.open_with_index_and_extra_info(device_index, None, auto_start) + } + + /// Open device with index, optional extra info, and optional auto start. + /// + /// On Windows, `extra_info` can be used to force backend selection with values like + /// `"auto"`, `"msmf"`, `"dshow"`, or `"backend="`. + pub fn open_with_index_and_extra_info( + &mut self, + device_index: i32, + extra_info: Option<&str>, + auto_start: bool, + ) -> Result<()> { // If the previous provider was running, stop it and detach callbacks // before destroying the underlying handle. if !self.handle.is_null() { @@ -518,8 +595,15 @@ impl Provider { self.cleanup_callback(); } + let extra_info = optional_c_string(extra_info, "extra info")?; + // Create a new provider with the specified device index - self.handle = unsafe { sys::ccap_provider_create_with_index(device_index, ptr::null()) }; + self.handle = unsafe { + sys::ccap_provider_create_with_index( + device_index, + extra_info.as_ref().map_or(ptr::null(), |value| value.as_ptr()), + ) + }; if self.handle.is_null() { return Err(CcapError::InvalidDevice(format!( diff --git a/cli/args_parser.cpp b/cli/args_parser.cpp index c96c189d..f8185d6a 100644 --- a/cli/args_parser.cpp +++ b/cli/args_parser.cpp @@ -97,6 +97,7 @@ void printUsage(const char* programName) { << " -q, --quiet quiet mode (only show errors, equivalent to log level Error)\n" << " --timeout seconds program timeout (auto-exit after N seconds)\n" << " --timeout-exit-code code exit code when timeout occurs (default: 0)\n" + << " environment: CCAP_WINDOWS_BACKEND=auto|msmf|dshow (Windows backend override)\n" << "\n" << "Input options:\n" << " -i, --input source input source: video file, device index, or device name\n" diff --git a/docs/content/cli.md b/docs/content/cli.md index d5604e07..9a33c531 100644 --- a/docs/content/cli.md +++ b/docs/content/cli.md @@ -71,6 +71,8 @@ The executable will be located in the `build/` directory (or `build/Debug`, `bui **Windows (MSVC)**: The CLI tool uses static runtime linking (`/MT` flag) to eliminate the dependency on VCRUNTIME DLL, allowing single-file distribution without requiring Visual C++ Redistributables. +**Windows backend override**: set `CCAP_WINDOWS_BACKEND=auto|msmf|dshow` before launching the CLI if you want to force Media Foundation or DirectShow during troubleshooting or validation. + **Linux**: Attempts to statically link libstdc++ and libgcc when available. Falls back to dynamic linking if not available (e.g., Fedora without `libstdc++-static` package). The binary still depends on glibc and may not work on systems with older glibc versions. diff --git a/docs/content/cli.zh.md b/docs/content/cli.zh.md index a73eeb7f..e34cafae 100644 --- a/docs/content/cli.zh.md +++ b/docs/content/cli.zh.md @@ -71,6 +71,8 @@ cmake --build build **Windows (MSVC)**:CLI 工具使用静态运行时链接(`/MT` 标志)来消除对 VCRUNTIME DLL 的依赖,允许单文件分发而无需用户安装 Visual C++ 运行库。 +**Windows 后端覆盖**:如果你希望在排障或验证时强制使用 Media Foundation 或 DirectShow,可以在启动 CLI 前设置 `CCAP_WINDOWS_BACKEND=auto|msmf|dshow`。 + **Linux**:当可用时尝试静态链接 libstdc++ 和 libgcc。如果不可用(例如 Fedora 未安装 `libstdc++-static` 包),则回退到动态链接。二进制文件仍然依赖 glibc,可能无法在旧 glibc 版本的系统上运行。 diff --git a/docs/content/documentation.md b/docs/content/documentation.md index a6c95b3a..c44e1d1e 100644 --- a/docs/content/documentation.md +++ b/docs/content/documentation.md @@ -220,7 +220,9 @@ if (provider.open("/path/to/video.mp4", true)) { ### Windows -Uses DirectShow for camera access. Requires MSVC 2019 or later. +Uses Media Foundation for camera access on modern Windows systems, with automatic DirectShow fallback for legacy or incompatible devices. Requires MSVC 2019 or later. + +To force a specific camera backend on Windows, either pass `extraInfo` as `auto`, `msmf`, `dshow`, or `backend=` to the constructors that accept it, or set `CCAP_WINDOWS_BACKEND=auto|msmf|dshow` for the current process. ```shell cl your_code.c /I"path\to\ccap\include" ^ diff --git a/docs/content/documentation.zh.md b/docs/content/documentation.zh.md index 8038a656..5cad15ea 100644 --- a/docs/content/documentation.zh.md +++ b/docs/content/documentation.zh.md @@ -219,7 +219,9 @@ if (provider.open("/path/to/video.mp4", true)) { ### Windows -使用 DirectShow 访问相机。需要 MSVC 2019 或更高版本。 +在现代 Windows 上默认使用 Media Foundation 访问相机,并在旧系统或不兼容设备上自动回退到 DirectShow。需要 MSVC 2019 或更高版本。 + +如果你需要在 Windows 上强制选择某个相机后端,可以在支持 `extraInfo` 的构造接口中传入 `auto`、`msmf`、`dshow` 或 `backend=`,也可以为当前进程设置 `CCAP_WINDOWS_BACKEND=auto|msmf|dshow`。 ```shell cl your_code.c /I"path\to\ccap\include" ^ diff --git a/docs/content/implementation-details.md b/docs/content/implementation-details.md index a4efbb50..7d6a7b04 100644 --- a/docs/content/implementation-details.md +++ b/docs/content/implementation-details.md @@ -215,6 +215,11 @@ Or use the script: ### Windows +**Media Foundation Backend:** +- Preferred on modern Windows systems +- Uses Source Reader for frame delivery and format negotiation +- Automatically falls back to DirectShow when Media Foundation is unavailable or device open fails + **DirectShow Backend:** - Mature, stable API - Good driver compatibility diff --git a/docs/content/implementation-details.zh.md b/docs/content/implementation-details.zh.md index e3440a59..b538b01e 100644 --- a/docs/content/implementation-details.zh.md +++ b/docs/content/implementation-details.zh.md @@ -198,6 +198,11 @@ cmake --build build ### Windows +**Media Foundation 后端:** +- 现代 Windows 上的首选后端 +- 使用 Source Reader 完成取帧与格式协商 +- 当 Media Foundation 不可用或设备打开失败时,会自动回退到 DirectShow + **DirectShow 后端:** - 成熟、稳定的 API - 良好的驱动兼容性 diff --git a/docs/content/rust-bindings.md b/docs/content/rust-bindings.md index 5bd12c08..ed651c2e 100644 --- a/docs/content/rust-bindings.md +++ b/docs/content/rust-bindings.md @@ -76,10 +76,12 @@ If needed, set: Camera capture backends: -- Windows: DirectShow +- Windows: Media Foundation with DirectShow fallback - macOS/iOS: AVFoundation - Linux: V4L2 +On Windows, you can force backend selection by setting `CCAP_WINDOWS_BACKEND=auto|msmf|dshow`, or by using the Rust APIs `Provider::with_device_name_and_extra_info`, `Provider::with_device_and_extra_info`, `Provider::open_device_with_extra_info`, and `Provider::open_with_index_and_extra_info`. + Video file playback support depends on the underlying C/C++ backend (currently Windows/macOS only). ## See also diff --git a/docs/content/rust-bindings.zh.md b/docs/content/rust-bindings.zh.md index 2bc46651..bf3d600b 100644 --- a/docs/content/rust-bindings.zh.md +++ b/docs/content/rust-bindings.zh.md @@ -77,10 +77,12 @@ fn main() -> Result<()> { 相机捕获后端: -- Windows:DirectShow +- Windows:Media Foundation,必要时回退到 DirectShow - macOS/iOS:AVFoundation - Linux:V4L2 +在 Windows 上,你可以通过设置环境变量 `CCAP_WINDOWS_BACKEND=auto|msmf|dshow` 强制选择后端,也可以使用 Rust API `Provider::with_device_name_and_extra_info`、`Provider::with_device_and_extra_info`、`Provider::open_device_with_extra_info`、`Provider::open_with_index_and_extra_info`。 + 视频文件播放是否可用取决于底层 C/C++ 后端(目前 Windows/macOS 支持,Linux 暂不支持)。 ## 另请参阅 diff --git a/docs/index.html b/docs/index.html index 264355ed..2d8343d7 100644 --- a/docs/index.html +++ b/docs/index.html @@ -5,7 +5,7 @@ ccap - Cross-Platform Camera Capture Library - + @@ -119,8 +119,8 @@

轻量级

🌍

Cross Platform

跨平台

-

Native support for Windows (DirectShow), macOS/iOS (AVFoundation), and Linux (V4L2).

-

原生支持 Windows (DirectShow)、macOS/iOS (AVFoundation) 和 Linux (V4L2)。

+

Native support for Windows (Media Foundation with DirectShow fallback), macOS/iOS (AVFoundation), and Linux (V4L2).

+

原生支持 Windows(Media Foundation,必要时回退到 DirectShow)、macOS/iOS (AVFoundation) 和 Linux (V4L2)。

@@ -159,7 +159,7 @@

支持的平台

🪟

Windows

-

DirectShow

+

Media Foundation + DirectShow fallback

MSVC 2019+

@@ -350,7 +350,7 @@

系统要求

Windows MSVC 2019+ - DirectShow + Media Foundation + DirectShow fallback macOS diff --git a/include/ccap_c.h b/include/ccap_c.h index f071e464..5d925ec2 100644 --- a/include/ccap_c.h +++ b/include/ccap_c.h @@ -152,7 +152,9 @@ CCAP_EXPORT CcapProvider* ccap_provider_create(void); /** * @brief Create a camera provider and open specified device * @param deviceName Device name to open (NULL for default device) - * @param extraInfo Extra information (currently unused, can be NULL) + * @param extraInfo Extra backend hint (can be NULL). + * On Windows, accepted values include `auto`, `msmf`, `dshow`, and `backend=`. + * Other platforms ignore this parameter. * @return Pointer to CcapProvider instance, or NULL on failure */ CCAP_EXPORT CcapProvider* ccap_provider_create_with_device(const char* deviceName, const char* extraInfo); @@ -160,7 +162,9 @@ CCAP_EXPORT CcapProvider* ccap_provider_create_with_device(const char* deviceNam /** * @brief Create a camera provider and open device by index * @param deviceIndex Device index (negative for default device) - * @param extraInfo Extra information (currently unused, can be NULL) + * @param extraInfo Extra backend hint (can be NULL). + * On Windows, accepted values include `auto`, `msmf`, `dshow`, and `backend=`. + * Other platforms ignore this parameter. * @return Pointer to CcapProvider instance, or NULL on failure */ CCAP_EXPORT CcapProvider* ccap_provider_create_with_index(int deviceIndex, const char* extraInfo); diff --git a/include/ccap_core.h b/include/ccap_core.h index 93bff818..45cc87ee 100644 --- a/include/ccap_core.h +++ b/include/ccap_core.h @@ -62,18 +62,22 @@ class CCAP_EXPORT Provider final { /// You can use the `open` method to open a camera device later. Provider(); - /** - * @brief Construct a new Provider object, and open the camera device. - * @param deviceName The name of the device to open. @see #open - * @param extraInfo Currently unused. - */ + /** + * @brief Construct a new Provider object, and open the camera device. + * @param deviceName The name of the device to open. @see #open + * @param extraInfo Optional backend hint. + * On Windows, accepted values include `auto`, `msmf`, `dshow`, and `backend=`. + * Other platforms ignore this parameter. + */ explicit Provider(std::string_view deviceName, std::string_view extraInfo = ""); - /** - * @brief Construct a new Provider object, and open the camera device. - * @param deviceIndex The index of the device to open. @see #open - * @param extraInfo Currently unused. - */ + /** + * @brief Construct a new Provider object, and open the camera device. + * @param deviceIndex The index of the device to open. @see #open + * @param extraInfo Optional backend hint. + * On Windows, accepted values include `auto`, `msmf`, `dshow`, and `backend=`. + * Other platforms ignore this parameter. + */ explicit Provider(int deviceIndex, std::string_view extraInfo = ""); /** @@ -230,6 +234,28 @@ class CCAP_EXPORT Provider final { ~Provider(); private: + void applyCachedState(ProviderImp* imp) const; + bool tryOpenWithImplementation(ProviderImp* imp, std::string_view deviceName, bool autoStart) const; + +private: + std::string m_extraInfo; + std::function&)> m_frameCallback; + std::function()> m_allocatorFactory; + uint32_t m_maxAvailableFrameSize = DEFAULT_MAX_AVAILABLE_FRAME_SIZE; + uint32_t m_maxCacheFrameSize = DEFAULT_MAX_CACHE_FRAME_SIZE; + int m_requestedWidth = 640; + int m_requestedHeight = 480; + double m_requestedFrameRate = 0.0; + PixelFormat m_requestedInternalFormat = PixelFormat::Unknown; + PixelFormat m_requestedOutputFormat{ +#ifdef __APPLE__ + PixelFormat::BGRA32 +#else + PixelFormat::BGR24 +#endif + }; + bool m_hasFrameOrientationOverride = false; + FrameOrientation m_requestedFrameOrientation = FrameOrientation::Default; ProviderImp* m_imp; }; diff --git a/include/ccap_def.h b/include/ccap_def.h index 20fe37b4..107cad3c 100644 --- a/include/ccap_def.h +++ b/include/ccap_def.h @@ -393,14 +393,15 @@ struct CCAP_EXPORT VideoFrame { */ std::shared_ptr allocator; - /** - * @brief Native handle for the frame, used for platform-specific operations. - * This field is optional and may be nullptr if not needed. - * @note Currently defined as follows: - * - Windows: When the backend is DirectShow, the actual type of nativeHandle is `IMediaSample*` - * - macOS/iOS: The actual type of nativeHandle is `CMSampleBufferRef` - * - Linux: The actual type is uint32_t, stands for `v4l2_buffer::index`. - */ + /** + * @brief Native handle for the frame, used for platform-specific operations. + * This field is optional and may be nullptr if not needed. + * @note Currently defined as follows: + * - Windows: When the backend is DirectShow, the actual type of nativeHandle is `IMediaSample*` + * - Windows: When the backend is Media Foundation, the actual type of nativeHandle is `IMFSample*` + * - macOS/iOS: The actual type of nativeHandle is `CMSampleBufferRef` + * - Linux: The actual type is uint32_t, stands for `v4l2_buffer::index`. + */ void* nativeHandle = nullptr; ///< Native handle for the frame, used for platform-specific operations /** diff --git a/src/ccap_core.cpp b/src/ccap_core.cpp index 02800c5a..6f992e4a 100644 --- a/src/ccap_core.cpp +++ b/src/ccap_core.cpp @@ -10,14 +10,21 @@ #include "ccap_imp.h" +#include #include +#include #include #include #include #include #include +#include #include +#if defined(_WIN32) || defined(_MSC_VER) +#include +#endif + #ifdef _MSC_VER #include #define ALIGNED_ALLOC(alignment, size) _aligned_malloc(size, alignment) @@ -33,12 +40,172 @@ namespace ccap { ProviderImp* createProviderApple(); ProviderImp* createProviderDirectShow(); +ProviderImp* createProviderMSMF(); ProviderImp* createProviderV4L2(); // Global error callback storage namespace { std::mutex g_errorCallbackMutex; ErrorCallback g_globalErrorCallback; + +#if defined(_WIN32) || defined(_MSC_VER) +enum class WindowsBackendPreference { + Auto, + MSMF, + DirectShow, +}; + +std::string toLowerCopy(std::string_view input) { + std::string normalized; + normalized.reserve(input.size()); + for (char ch : input) { + if (!std::isspace(static_cast(ch))) { + normalized.push_back(static_cast(std::tolower(static_cast(ch)))); + } + } + return normalized; +} + +std::optional parseWindowsBackendPreferenceValue(std::string_view value) { + std::string normalized = toLowerCopy(value); + if (normalized.empty()) { + return std::nullopt; + } + + constexpr const char* kBackendPrefix = "backend="; + if (normalized.rfind(kBackendPrefix, 0) == 0) { + normalized.erase(0, std::strlen(kBackendPrefix)); + } + + if (normalized == "auto") { + return WindowsBackendPreference::Auto; + } + if (normalized == "msmf" || normalized == "mediafoundation") { + return WindowsBackendPreference::MSMF; + } + if (normalized == "dshow" || normalized == "directshow") { + return WindowsBackendPreference::DirectShow; + } + return std::nullopt; +} + +WindowsBackendPreference resolveWindowsBackendPreference(std::string_view extraInfo) { + if (auto parsed = parseWindowsBackendPreferenceValue(extraInfo)) { + return *parsed; + } + + std::string envValue; +#if defined(_MSC_VER) + char* rawValue = nullptr; + size_t rawLength = 0; + if (_dupenv_s(&rawValue, &rawLength, "CCAP_WINDOWS_BACKEND") == 0 && rawValue != nullptr) { + envValue.assign(rawValue, rawLength == 0 ? 0 : rawLength - 1); + free(rawValue); + } +#else + if (const char* rawValue = std::getenv("CCAP_WINDOWS_BACKEND"); rawValue != nullptr) { + envValue = rawValue; + } +#endif + + if (!envValue.empty()) { + if (auto parsed = parseWindowsBackendPreferenceValue(envValue)) { + return *parsed; + } + } + + return WindowsBackendPreference::Auto; +} + +bool probeLibraryExport(const wchar_t* libraryName, const char* exportName) { + HMODULE module = LoadLibraryW(libraryName); + if (module == nullptr) { + return false; + } + + bool ok = GetProcAddress(module, exportName) != nullptr; + FreeLibrary(module); + return ok; +} + +bool isMediaFoundationCameraBackendAvailable() { + static const bool s_available = []() { + return probeLibraryExport(L"mf.dll", "MFEnumDeviceSources") && probeLibraryExport(L"mfplat.dll", "MFStartup") && + probeLibraryExport(L"mfreadwrite.dll", "MFCreateSourceReaderFromMediaSource"); + }(); + return s_available; +} + +ProviderImp* createWindowsProvider(std::string_view extraInfo) { + WindowsBackendPreference preference = resolveWindowsBackendPreference(extraInfo); + + if (preference == WindowsBackendPreference::DirectShow) { + return createProviderDirectShow(); + } + + if (preference == WindowsBackendPreference::MSMF && isMediaFoundationCameraBackendAvailable()) { + return createProviderMSMF(); + } + + if (preference == WindowsBackendPreference::Auto && isMediaFoundationCameraBackendAvailable()) { + return createProviderMSMF(); + } + + return createProviderDirectShow(); +} + +ProviderImp* createWindowsProvider(WindowsBackendPreference preference) { + switch (preference) { + case WindowsBackendPreference::MSMF: + return isMediaFoundationCameraBackendAvailable() ? createProviderMSMF() : nullptr; + case WindowsBackendPreference::DirectShow: + return createProviderDirectShow(); + case WindowsBackendPreference::Auto: + return isMediaFoundationCameraBackendAvailable() ? createProviderMSMF() : createProviderDirectShow(); + } + + return createProviderDirectShow(); +} + +int virtualCameraRank(std::string_view name) { + std::string normalized = toLowerCopy(name); + constexpr std::string_view keywords[] = { + "obs", + "virtual", + "fake", + }; + + for (size_t index = 0; index < std::size(keywords); ++index) { + if (normalized.find(keywords[index]) != std::string::npos) { + return static_cast(index); + } + } + + return -1; +} + +void sortDeviceNamesForDisplay(std::vector& deviceNames) { + std::stable_sort(deviceNames.begin(), deviceNames.end(), [](const std::string& lhs, const std::string& rhs) { + return virtualCameraRank(lhs) < virtualCameraRank(rhs); + }); +} + +std::vector mergeDeviceNames(std::vector preferred, const std::vector& fallback) { + for (const std::string& name : fallback) { + if (std::find(preferred.begin(), preferred.end(), name) == preferred.end()) { + preferred.push_back(name); + } + } + + sortDeviceNamesForDisplay(preferred); + return preferred; +} + +std::vector collectDeviceNamesFromBackend(WindowsBackendPreference preference) { + std::unique_ptr provider(createWindowsProvider(preference)); + return provider ? provider->findDeviceNames() : std::vector(); +} +#endif } // namespace CCAP_EXPORT void setErrorCallback(ErrorCallback callback) { @@ -112,7 +279,7 @@ ProviderImp* createProvider(std::string_view extraInfo) { #if __APPLE__ return createProviderApple(); #elif defined(_MSC_VER) || defined(_WIN32) - return createProviderDirectShow(); + return createWindowsProvider(extraInfo); #elif defined(__linux__) || defined(__linux) || defined(linux) || defined(__gnu_linux__) return createProviderV4L2(); #else @@ -128,6 +295,8 @@ Provider::Provider() : m_imp(createProvider("")) { if (!m_imp) { reportError(ErrorCode::InitializationFailed, ErrorMessages::FAILED_TO_CREATE_PROVIDER); + } else { + applyCachedState(m_imp); } } @@ -137,8 +306,10 @@ Provider::~Provider() { } Provider::Provider(std::string_view deviceName, std::string_view extraInfo) : + m_extraInfo(extraInfo), m_imp(createProvider(extraInfo)) { if (m_imp) { + applyCachedState(m_imp); open(deviceName); } else { reportError(ErrorCode::InitializationFailed, ErrorMessages::FAILED_TO_CREATE_PROVIDER); @@ -146,22 +317,128 @@ Provider::Provider(std::string_view deviceName, std::string_view extraInfo) : } Provider::Provider(int deviceIndex, std::string_view extraInfo) : + m_extraInfo(extraInfo), m_imp(createProvider(extraInfo)) { if (m_imp) { + applyCachedState(m_imp); open(deviceIndex); } else { reportError(ErrorCode::InitializationFailed, ErrorMessages::FAILED_TO_CREATE_PROVIDER); } } -std::vector Provider::findDeviceNames() { return m_imp ? m_imp->findDeviceNames() : std::vector(); } +void Provider::applyCachedState(ProviderImp* imp) const { + if (!imp) { + return; + } + + imp->setMaxAvailableFrameSize(m_maxAvailableFrameSize); + imp->setMaxCacheFrameSize(m_maxCacheFrameSize); + imp->setNewFrameCallback(m_frameCallback); + imp->setFrameAllocator(m_allocatorFactory); + imp->set(PropertyName::Width, m_requestedWidth); + imp->set(PropertyName::Height, m_requestedHeight); + imp->set(PropertyName::FrameRate, m_requestedFrameRate); + imp->set(PropertyName::PixelFormatInternal, static_cast(m_requestedInternalFormat)); + imp->set(PropertyName::PixelFormatOutput, static_cast(m_requestedOutputFormat)); + if (m_hasFrameOrientationOverride) { + imp->set(PropertyName::FrameOrientation, static_cast(m_requestedFrameOrientation)); + } +} + +bool Provider::tryOpenWithImplementation(ProviderImp* imp, std::string_view deviceName, bool autoStart) const { + if (!imp) { + return false; + } + + applyCachedState(imp); + if (!imp->open(deviceName)) { + imp->close(); + return false; + } + + if (autoStart && !imp->start()) { + imp->close(); + return false; + } + + return true; +} + +std::vector Provider::findDeviceNames() { + if (!m_imp) { + return std::vector(); + } + +#if defined(_MSC_VER) || defined(_WIN32) + if (m_imp->isOpened()) { + return m_imp->findDeviceNames(); + } + + WindowsBackendPreference preference = resolveWindowsBackendPreference(m_extraInfo); + if (preference == WindowsBackendPreference::DirectShow) { + return collectDeviceNamesFromBackend(WindowsBackendPreference::DirectShow); + } + + if (preference == WindowsBackendPreference::MSMF) { + return collectDeviceNamesFromBackend(WindowsBackendPreference::MSMF); + } + + std::vector preferred = collectDeviceNamesFromBackend(WindowsBackendPreference::MSMF); + std::vector fallback = collectDeviceNamesFromBackend(WindowsBackendPreference::DirectShow); + return mergeDeviceNames(std::move(preferred), fallback); +#else + return m_imp->findDeviceNames(); +#endif +} bool Provider::open(std::string_view deviceName, bool autoStart) { if (!m_imp) { reportError(ErrorCode::InitializationFailed, ErrorMessages::PROVIDER_IMPLEMENTATION_NULL); return false; } + +#if defined(_MSC_VER) || defined(_WIN32) + if (m_imp->isOpened()) { + return m_imp->open(deviceName) && (!autoStart || m_imp->start()); + } + + auto tryBackend = [&](WindowsBackendPreference preference) { + std::unique_ptr candidate(createWindowsProvider(preference)); + if (!candidate || !tryOpenWithImplementation(candidate.get(), deviceName, autoStart)) { + return false; + } + + delete m_imp; + m_imp = candidate.release(); + return true; + }; + + if (looksLikeFilePath(deviceName)) { + return tryBackend(WindowsBackendPreference::DirectShow); + } + + WindowsBackendPreference preference = resolveWindowsBackendPreference(m_extraInfo); + if (preference == WindowsBackendPreference::DirectShow) { + return tryBackend(WindowsBackendPreference::DirectShow); + } + + if (preference == WindowsBackendPreference::MSMF) { + if (!isMediaFoundationCameraBackendAvailable()) { + reportError(ErrorCode::InitializationFailed, "Media Foundation camera backend is unavailable on this system"); + return false; + } + return tryBackend(WindowsBackendPreference::MSMF); + } + + if (isMediaFoundationCameraBackendAvailable() && tryBackend(WindowsBackendPreference::MSMF)) { + return true; + } + + return tryBackend(WindowsBackendPreference::DirectShow); +#else return m_imp->open(deviceName) && (!autoStart || m_imp->start()); +#endif } bool Provider::open(int deviceIndex, bool autoStart) { @@ -181,7 +458,7 @@ bool Provider::open(int deviceIndex, bool autoStart) { } } - return open(deviceName) && (!autoStart || m_imp->start()); + return open(deviceName, autoStart); } bool Provider::isOpened() const { return m_imp && m_imp->isOpened(); } @@ -215,7 +492,47 @@ bool Provider::set(PropertyName prop, double value) { reportError(ErrorCode::InitializationFailed, ErrorMessages::PROVIDER_IMPLEMENTATION_NULL); return false; } - return m_imp->set(prop, value); + + bool result = m_imp->set(prop, value); + if (!result) { + return false; + } + + switch (prop) { + case PropertyName::Width: + m_requestedWidth = static_cast(value); + break; + case PropertyName::Height: + m_requestedHeight = static_cast(value); + break; + case PropertyName::FrameRate: + m_requestedFrameRate = value; + break; + case PropertyName::PixelFormatInternal: { + auto intValue = static_cast(value); +#if defined(_MSC_VER) || defined(_WIN32) + intValue &= ~kPixelFormatFullRangeBit; +#endif + m_requestedInternalFormat = static_cast(intValue); + break; + } + case PropertyName::PixelFormatOutput: { + uint32_t formatValue = static_cast(value); +#if defined(_MSC_VER) || defined(_WIN32) + formatValue &= ~static_cast(kPixelFormatFullRangeBit); +#endif + m_requestedOutputFormat = static_cast(formatValue); + break; + } + case PropertyName::FrameOrientation: + m_requestedFrameOrientation = static_cast(static_cast(value)); + m_hasFrameOrientationOverride = true; + break; + default: + break; + } + + return true; } double Provider::get(PropertyName prop) { return m_imp ? m_imp->get(prop) : NAN; } @@ -233,6 +550,7 @@ void Provider::setNewFrameCallback(std::functionsetNewFrameCallback(std::move(callback)); } @@ -241,6 +559,7 @@ void Provider::setFrameAllocator(std::function()> all reportError(ErrorCode::InitializationFailed, ErrorMessages::PROVIDER_IMPLEMENTATION_NULL); return; } + m_allocatorFactory = allocatorFactory; m_imp->setFrameAllocator(std::move(allocatorFactory)); } @@ -249,6 +568,7 @@ void Provider::setMaxAvailableFrameSize(uint32_t size) { reportError(ErrorCode::InitializationFailed, ErrorMessages::PROVIDER_IMPLEMENTATION_NULL); return; } + m_maxAvailableFrameSize = size; m_imp->setMaxAvailableFrameSize(size); } @@ -257,6 +577,7 @@ void Provider::setMaxCacheFrameSize(uint32_t size) { reportError(ErrorCode::InitializationFailed, ErrorMessages::PROVIDER_IMPLEMENTATION_NULL); return; } + m_maxCacheFrameSize = size; m_imp->setMaxCacheFrameSize(size); } diff --git a/src/ccap_imp_windows_msmf.cpp b/src/ccap_imp_windows_msmf.cpp new file mode 100644 index 00000000..fa3418af --- /dev/null +++ b/src/ccap_imp_windows_msmf.cpp @@ -0,0 +1,911 @@ +/** + * @file ccap_imp_windows_msmf.cpp + * @brief Implementation for Provider class using Media Foundation. + * @date 2026-03 + */ + +#if defined(_WIN32) || defined(_MSC_VER) + +#include "ccap_imp_windows_msmf.h" + +#include "ccap_convert_frame.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +template +void releaseComPtr(T*& ptr) { + if (ptr != nullptr) { + ptr->Release(); + ptr = nullptr; + } +} + +std::string wideToUtf8(const wchar_t* text) { + if (text == nullptr || *text == L'\0') { + return {}; + } + + int length = WideCharToMultiByte(CP_UTF8, 0, text, -1, nullptr, 0, nullptr, nullptr); + if (length <= 0) { + return {}; + } + + std::string value(static_cast(length - 1), '\0'); + WideCharToMultiByte(CP_UTF8, 0, text, -1, value.data(), length, nullptr, nullptr); + return value; +} + +int virtualCameraRank(std::string_view name) { + std::string normalized; + normalized.reserve(name.size()); + for (char ch : name) { + normalized.push_back(static_cast(std::tolower(static_cast(ch)))); + } + + constexpr std::string_view keywords[] = { + "obs", + "virtual", + "fake", + }; + + for (size_t index = 0; index < sizeof(keywords) / sizeof(keywords[0]); ++index) { + if (normalized.find(keywords[index]) != std::string::npos) { + return static_cast(index); + } + } + + return -1; +} + +struct PixelFormatInfo { + GUID subtype; + const char* name; + ccap::PixelFormat pixelFormat; + bool isCompressed; +}; + +const PixelFormatInfo s_pixelFormatInfos[] = { + { MFVideoFormat_MJPG, "MJPG", ccap::PixelFormat::Unknown, true }, + { MFVideoFormat_RGB24, "BGR24", ccap::PixelFormat::BGR24, false }, + { MFVideoFormat_RGB32, "BGRA32", ccap::PixelFormat::BGRA32, false }, + { MFVideoFormat_NV12, "NV12", ccap::PixelFormat::NV12, false }, +#ifdef MFVideoFormat_I420 + { MFVideoFormat_I420, "I420", ccap::PixelFormat::I420, false }, +#endif + { MFVideoFormat_IYUV, "I420", ccap::PixelFormat::I420, false }, + { MFVideoFormat_YUY2, "YUY2", ccap::PixelFormat::YUYV, false }, + { MFVideoFormat_UYVY, "UYVY", ccap::PixelFormat::UYVY, false }, +}; + +PixelFormatInfo findPixelFormatInfo(const GUID& subtype) { + for (const auto& info : s_pixelFormatInfos) { + if (info.subtype == subtype) { + return info; + } + } + + return { GUID_NULL, "Unknown", ccap::PixelFormat::Unknown, false }; +} + +GUID preferredSubtypeForPixelFormat(ccap::PixelFormat format) { + switch (static_cast(format) & ~static_cast(ccap::kPixelFormatFullRangeBit)) { + case static_cast(ccap::PixelFormat::NV12): + return MFVideoFormat_NV12; + case static_cast(ccap::PixelFormat::I420): +#ifdef MFVideoFormat_I420 + return MFVideoFormat_I420; +#else + return MFVideoFormat_IYUV; +#endif + case static_cast(ccap::PixelFormat::YUYV): + return MFVideoFormat_YUY2; + case static_cast(ccap::PixelFormat::UYVY): + return MFVideoFormat_UYVY; + case static_cast(ccap::PixelFormat::BGR24): + case static_cast(ccap::PixelFormat::RGB24): + return MFVideoFormat_RGB24; + case static_cast(ccap::PixelFormat::BGRA32): + case static_cast(ccap::PixelFormat::RGBA32): + return MFVideoFormat_RGB32; + default: + break; + } + + return GUID_NULL; +} + +void appendUniqueSubtype(std::vector& subtypes, const GUID& subtype) { + if (subtype == GUID_NULL) { + return; + } + + auto duplicate = std::find_if(subtypes.begin(), subtypes.end(), [&](const GUID& existing) { return existing == subtype; }); + if (duplicate == subtypes.end()) { + subtypes.push_back(subtype); + } +} + +} // namespace + +namespace ccap { + +ProviderMSMF::ProviderMSMF() { + m_frameOrientation = FrameOrientation::TopToBottom; +} + +ProviderMSMF::~ProviderMSMF() { + close(); + uninitMediaFoundation(); + if (m_didInitializeCom) { + CoUninitialize(); + m_didInitializeCom = false; + } +} + +bool ProviderMSMF::setup() { + if (m_mfInitialized) { + return true; + } + + if (!m_didSetup) { + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + m_didInitializeCom = SUCCEEDED(hr); + m_didSetup = m_didInitializeCom || hr == RPC_E_CHANGED_MODE; + if (!m_didSetup) { + reportError(ErrorCode::InitializationFailed, "COM initialization failed for Media Foundation backend"); + return false; + } + } + + return initMediaFoundation(); +} + +bool ProviderMSMF::initMediaFoundation() { + if (m_mfInitialized) { + return true; + } + + HRESULT hr = MFStartup(MF_VERSION); + if (FAILED(hr)) { + reportError(ErrorCode::InitializationFailed, "Failed to initialize Media Foundation"); + return false; + } + + m_mfInitialized = true; + return true; +} + +void ProviderMSMF::uninitMediaFoundation() { + if (m_mfInitialized) { + MFShutdown(); + m_mfInitialized = false; + } +} + +bool ProviderMSMF::enumerateDevices(std::vector& devices) { + devices.clear(); + + if (!setup()) { + return false; + } + + IMFAttributes* attributes = nullptr; + HRESULT hr = MFCreateAttributes(&attributes, 1); + if (FAILED(hr)) { + reportError(ErrorCode::NoDeviceFound, "Failed to create Media Foundation attributes for device enumeration"); + return false; + } + + hr = attributes->SetGUID(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID); + if (FAILED(hr)) { + releaseComPtr(attributes); + reportError(ErrorCode::NoDeviceFound, "Failed to configure Media Foundation video device enumeration"); + return false; + } + + IMFActivate** deviceArray = nullptr; + UINT32 count = 0; + hr = MFEnumDeviceSources(attributes, &deviceArray, &count); + releaseComPtr(attributes); + + if (FAILED(hr)) { + reportError(ErrorCode::NoDeviceFound, "Media Foundation device enumeration failed"); + return false; + } + + devices.reserve(count); + for (UINT32 index = 0; index < count; ++index) { + wchar_t* friendlyName = nullptr; + UINT32 friendlyLength = 0; + wchar_t* symbolicLink = nullptr; + UINT32 symbolicLength = 0; + + hr = deviceArray[index]->GetAllocatedString(MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, &friendlyName, &friendlyLength); + if (SUCCEEDED(hr) && friendlyName != nullptr) { + DeviceEntry entry; + entry.friendlyName = wideToUtf8(friendlyName); + + if (SUCCEEDED(deviceArray[index]->GetAllocatedString( + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK, &symbolicLink, &symbolicLength)) && + symbolicLink != nullptr) { + entry.symbolicLink.assign(symbolicLink, symbolicLength); + } + + devices.emplace_back(std::move(entry)); + } + + if (friendlyName != nullptr) { + CoTaskMemFree(friendlyName); + } + if (symbolicLink != nullptr) { + CoTaskMemFree(symbolicLink); + } + + releaseComPtr(deviceArray[index]); + } + + CoTaskMemFree(deviceArray); + + std::stable_sort(devices.begin(), devices.end(), [](const DeviceEntry& lhs, const DeviceEntry& rhs) { + return virtualCameraRank(lhs.friendlyName) < virtualCameraRank(rhs.friendlyName); + }); + + return true; +} + +bool ProviderMSMF::ensureDeviceCache() { + if (!m_devices.empty()) { + return true; + } + return enumerateDevices(m_devices); +} + +std::vector ProviderMSMF::findDeviceNames() { + if (!ensureDeviceCache()) { + return {}; + } + + std::vector deviceNames; + deviceNames.reserve(m_devices.size()); + for (const DeviceEntry& entry : m_devices) { + deviceNames.push_back(entry.friendlyName); + } + return deviceNames; +} + +bool ProviderMSMF::createMediaSource(const std::wstring& symbolicLink) { + IMFAttributes* attributes = nullptr; + HRESULT hr = MFCreateAttributes(&attributes, 2); + if (FAILED(hr)) { + reportError(ErrorCode::DeviceOpenFailed, "Failed to create Media Foundation attributes for device open"); + return false; + } + + hr = attributes->SetGUID(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID); + if (SUCCEEDED(hr)) { + hr = attributes->SetString(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK, symbolicLink.c_str()); + } + + if (SUCCEEDED(hr)) { + hr = MFCreateDeviceSource(attributes, &m_mediaSource); + } + + releaseComPtr(attributes); + + if (FAILED(hr) || m_mediaSource == nullptr) { + reportError(ErrorCode::DeviceOpenFailed, "Failed to create Media Foundation device source"); + return false; + } + + return true; +} + +bool ProviderMSMF::createSourceReader() { + IMFAttributes* attributes = nullptr; + HRESULT hr = MFCreateAttributes(&attributes, 2); + if (FAILED(hr)) { + reportError(ErrorCode::DeviceOpenFailed, "Failed to create Media Foundation source reader attributes"); + return false; + } + + hr = attributes->SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, TRUE); + if (SUCCEEDED(hr)) { +#ifdef MF_LOW_LATENCY + attributes->SetUINT32(MF_LOW_LATENCY, TRUE); +#endif + hr = MFCreateSourceReaderFromMediaSource(m_mediaSource, attributes, &m_sourceReader); + } + + releaseComPtr(attributes); + + if (FAILED(hr) || m_sourceReader == nullptr) { + reportError(ErrorCode::DeviceOpenFailed, "Failed to create Media Foundation source reader"); + return false; + } + + m_sourceReader->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE); + hr = m_sourceReader->SetStreamSelection(MF_SOURCE_READER_FIRST_VIDEO_STREAM, TRUE); + if (FAILED(hr)) { + reportError(ErrorCode::DeviceOpenFailed, "Failed to select Media Foundation video stream"); + return false; + } + + return true; +} + +std::vector ProviderMSMF::enumerateMediaTypes() const { + std::vector mediaTypes; + if (m_sourceReader == nullptr) { + return mediaTypes; + } + + for (DWORD index = 0;; ++index) { + IMFMediaType* mediaType = nullptr; + HRESULT hr = m_sourceReader->GetNativeMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, index, &mediaType); + if (hr == MF_E_NO_MORE_TYPES) { + break; + } + if (FAILED(hr) || mediaType == nullptr) { + continue; + } + + GUID subtype = GUID_NULL; + mediaType->GetGUID(MF_MT_SUBTYPE, &subtype); + PixelFormatInfo pixelFormatInfo = findPixelFormatInfo(subtype); + + UINT32 width = 0; + UINT32 height = 0; + MFGetAttributeSize(mediaType, MF_MT_FRAME_SIZE, &width, &height); + + UINT32 fpsNumerator = 0; + UINT32 fpsDenominator = 0; + MFGetAttributeRatio(mediaType, MF_MT_FRAME_RATE, &fpsNumerator, &fpsDenominator); + + MediaTypeInfo info; + info.mediaType = mediaType; + info.subtype = subtype; + info.pixelFormat = pixelFormatInfo.pixelFormat; + info.width = width; + info.height = height; + info.frameRateNumerator = fpsNumerator; + info.frameRateDenominator = fpsDenominator == 0 ? 1 : fpsDenominator; + info.fps = fpsNumerator != 0 ? static_cast(fpsNumerator) / static_cast(info.frameRateDenominator) : 0.0; + info.isCompressed = pixelFormatInfo.isCompressed; + mediaTypes.push_back(info); + } + + return mediaTypes; +} + +void ProviderMSMF::releaseMediaTypes(std::vector& mediaTypes) const { + for (MediaTypeInfo& info : mediaTypes) { + releaseComPtr(info.mediaType); + } +} + +bool ProviderMSMF::trySetCurrentMediaType(const MediaTypeInfo& selected, const GUID& subtype) { + IMFMediaType* outputType = nullptr; + HRESULT hr = MFCreateMediaType(&outputType); + if (FAILED(hr)) { + return false; + } + + hr = outputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + if (SUCCEEDED(hr)) { + hr = outputType->SetGUID(MF_MT_SUBTYPE, subtype); + } + if (SUCCEEDED(hr)) { + hr = MFSetAttributeSize(outputType, MF_MT_FRAME_SIZE, selected.width, selected.height); + } + if (SUCCEEDED(hr) && selected.frameRateNumerator != 0) { + hr = MFSetAttributeRatio(outputType, MF_MT_FRAME_RATE, selected.frameRateNumerator, selected.frameRateDenominator); + } + if (SUCCEEDED(hr)) { + hr = m_sourceReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, nullptr, outputType); + } + + releaseComPtr(outputType); + return SUCCEEDED(hr); +} + +bool ProviderMSMF::updateCurrentMediaType() { + IMFMediaType* currentType = nullptr; + HRESULT hr = m_sourceReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, ¤tType); + if (FAILED(hr) || currentType == nullptr) { + reportError(ErrorCode::UnsupportedPixelFormat, "Failed to query Media Foundation output media type"); + return false; + } + + GUID subtype = GUID_NULL; + currentType->GetGUID(MF_MT_SUBTYPE, &subtype); + PixelFormatInfo info = findPixelFormatInfo(subtype); + + UINT32 width = 0; + UINT32 height = 0; + MFGetAttributeSize(currentType, MF_MT_FRAME_SIZE, &width, &height); + + UINT32 fpsNumerator = 0; + UINT32 fpsDenominator = 0; + MFGetAttributeRatio(currentType, MF_MT_FRAME_RATE, &fpsNumerator, &fpsDenominator); + + LONG stride = 0; + UINT32 strideValue = 0; + if (FAILED(currentType->GetUINT32(MF_MT_DEFAULT_STRIDE, &strideValue))) { + switch (info.pixelFormat) { + case PixelFormat::BGRA32: + stride = static_cast(width * 4); + break; + case PixelFormat::BGR24: + stride = static_cast(width * 3); + break; + case PixelFormat::YUYV: + case PixelFormat::UYVY: + stride = static_cast(width * 2); + break; + default: + stride = static_cast(width); + break; + } + } else { + stride = static_cast(strideValue); + } + + releaseComPtr(currentType); + + if (info.pixelFormat == PixelFormat::Unknown) { + reportError(ErrorCode::UnsupportedPixelFormat, "Media Foundation selected an unsupported output pixel format"); + return false; + } + + m_activePixelFormat = info.pixelFormat; + m_activeWidth = width; + m_activeHeight = height; + m_activeFps = fpsNumerator != 0 ? static_cast(fpsNumerator) / static_cast(fpsDenominator == 0 ? 1 : fpsDenominator) : 0.0; + m_activeStride0 = stride; + + m_frameProp.cameraPixelFormat = m_activePixelFormat; + m_frameProp.width = static_cast(m_activeWidth); + m_frameProp.height = static_cast(m_activeHeight); + m_frameProp.fps = m_activeFps; + return true; +} + +bool ProviderMSMF::configureMediaType() { + std::vector mediaTypes = enumerateMediaTypes(); + if (mediaTypes.empty()) { + reportError(ErrorCode::UnsupportedPixelFormat, "No Media Foundation video formats were reported by the device"); + return false; + } + + const int desiredWidth = m_frameProp.width; + const int desiredHeight = m_frameProp.height; + std::vector matchedIndexes; + matchedIndexes.reserve(mediaTypes.size()); + + for (size_t index = 0; index < mediaTypes.size(); ++index) { + const MediaTypeInfo& info = mediaTypes[index]; + if (desiredWidth <= static_cast(info.width) && desiredHeight <= static_cast(info.height)) { + matchedIndexes.push_back(index); + } + } + + if (matchedIndexes.empty()) { + matchedIndexes.resize(mediaTypes.size()); + for (size_t index = 0; index < mediaTypes.size(); ++index) { + matchedIndexes[index] = index; + } + } + + double closestDistance = 1.e9; + std::vector bestIndexes; + for (size_t index : matchedIndexes) { + const MediaTypeInfo& info = mediaTypes[index]; + double distance = std::abs(static_cast(info.width) - desiredWidth) + std::abs(static_cast(info.height) - desiredHeight); + if (distance < closestDistance) { + closestDistance = distance; + bestIndexes = { index }; + } else if (std::abs(distance - closestDistance) < 1e-5) { + bestIndexes.push_back(index); + } + } + + PixelFormat preferredPixelFormat = m_frameProp.cameraPixelFormat != PixelFormat::Unknown ? m_frameProp.cameraPixelFormat : + m_frameProp.outputPixelFormat; + + const MediaTypeInfo* selected = nullptr; + for (size_t index : bestIndexes) { + const MediaTypeInfo& info = mediaTypes[index]; + if (info.pixelFormat == preferredPixelFormat || (!(preferredPixelFormat & kPixelFormatYUVColorBit) && info.isCompressed)) { + selected = &info; + break; + } + } + + if (selected == nullptr && !bestIndexes.empty()) { + selected = &mediaTypes[bestIndexes.front()]; + } + + if (selected == nullptr) { + releaseMediaTypes(mediaTypes); + reportError(ErrorCode::UnsupportedPixelFormat, "Failed to choose a Media Foundation camera format"); + return false; + } + + std::vector subtypesToTry; + appendUniqueSubtype(subtypesToTry, preferredSubtypeForPixelFormat(preferredPixelFormat)); + appendUniqueSubtype(subtypesToTry, preferredSubtypeForPixelFormat(selected->pixelFormat)); + if (preferredPixelFormat & kPixelFormatYUVColorBit) { + appendUniqueSubtype(subtypesToTry, MFVideoFormat_NV12); + } else { + appendUniqueSubtype(subtypesToTry, MFVideoFormat_RGB32); + appendUniqueSubtype(subtypesToTry, MFVideoFormat_RGB24); + } + + bool configured = false; + for (const GUID& subtype : subtypesToTry) { + if (trySetCurrentMediaType(*selected, subtype)) { + configured = true; + break; + } + } + + releaseMediaTypes(mediaTypes); + + if (!configured || !updateCurrentMediaType()) { + reportError(ErrorCode::UnsupportedPixelFormat, "Failed to configure Media Foundation output media type"); + return false; + } + + return true; +} + +bool ProviderMSMF::open(std::string_view deviceName) { + if (looksLikeFilePath(deviceName)) { + reportError(ErrorCode::DeviceOpenFailed, "The Media Foundation camera backend does not handle file playback"); + return false; + } + + if (m_isOpened || m_sourceReader != nullptr || m_mediaSource != nullptr) { + reportError(ErrorCode::DeviceOpenFailed, "Camera already opened, please close it first"); + return false; + } + + if (!ensureDeviceCache()) { + return false; + } + + const DeviceEntry* selectedDevice = nullptr; + for (const DeviceEntry& entry : m_devices) { + if (deviceName.empty() || entry.friendlyName == deviceName) { + selectedDevice = &entry; + break; + } + } + + if (selectedDevice == nullptr) { + reportError(ErrorCode::InvalidDevice, "No Media Foundation video capture device: " + std::string(deviceName)); + return false; + } + + m_deviceName = selectedDevice->friendlyName; + m_deviceSymbolicLink = selectedDevice->symbolicLink; + m_isFileMode = false; + m_frameIndex = 0; + + if (!createMediaSource(m_deviceSymbolicLink) || !createSourceReader() || !configureMediaType()) { + close(); + return false; + } + + m_isOpened = true; + return true; +} + +bool ProviderMSMF::isOpened() const { + return m_isOpened; +} + +std::optional ProviderMSMF::getDeviceInfo() const { + if (!m_isOpened || m_sourceReader == nullptr) { + return std::nullopt; + } + + std::vector mediaTypes = enumerateMediaTypes(); + if (mediaTypes.empty()) { + return std::nullopt; + } + + std::optional info; + info.emplace(); + info->deviceName = m_deviceName; + + bool hasMJPG = false; + for (const MediaTypeInfo& mediaType : mediaTypes) { + if (mediaType.pixelFormat != PixelFormat::Unknown) { + info->supportedPixelFormats.push_back(mediaType.pixelFormat); + } else if (mediaType.isCompressed) { + hasMJPG = true; + } + + if (mediaType.width != 0 && mediaType.height != 0) { + info->supportedResolutions.push_back({ mediaType.width, mediaType.height }); + } + } + + releaseMediaTypes(mediaTypes); + + if (hasMJPG) { + info->supportedPixelFormats.push_back(PixelFormat::BGR24); + info->supportedPixelFormats.push_back(PixelFormat::BGRA32); + info->supportedPixelFormats.push_back(PixelFormat::RGB24); + info->supportedPixelFormats.push_back(PixelFormat::RGBA32); + } + + auto& resolutions = info->supportedResolutions; + std::sort(resolutions.begin(), resolutions.end(), [](const DeviceInfo::Resolution& lhs, const DeviceInfo::Resolution& rhs) { + return lhs.width * lhs.height < rhs.width * rhs.height; + }); + resolutions.erase(std::unique(resolutions.begin(), resolutions.end(), [](const DeviceInfo::Resolution& lhs, const DeviceInfo::Resolution& rhs) { + return lhs.width == rhs.width && lhs.height == rhs.height; + }), + resolutions.end()); + + auto& formats = info->supportedPixelFormats; + std::sort(formats.begin(), formats.end()); + formats.erase(std::unique(formats.begin(), formats.end()), formats.end()); + return info; +} + +void ProviderMSMF::readLoop() { + HRESULT comResult = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + bool didInitCom = SUCCEEDED(comResult); + m_shouldStop = false; + + while (!m_shouldStop && m_sourceReader != nullptr) { + DWORD streamIndex = 0; + DWORD flags = 0; + LONGLONG timestamp = 0; + IMFSample* sample = nullptr; + + HRESULT hr = m_sourceReader->ReadSample(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, &streamIndex, &flags, ×tamp, &sample); + if (FAILED(hr)) { + if (!m_shouldStop) { + reportError(ErrorCode::FrameCaptureFailed, "Media Foundation frame read failed"); + } + releaseComPtr(sample); + break; + } + + if (flags & (MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED | MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED)) { + if (!updateCurrentMediaType()) { + releaseComPtr(sample); + break; + } + } + + if ((flags & MF_SOURCE_READERF_ENDOFSTREAM) || sample == nullptr) { + releaseComPtr(sample); + if (m_shouldStop) { + break; + } + continue; + } + + IMFMediaBuffer* buffer = nullptr; + hr = sample->ConvertToContiguousBuffer(&buffer); + if (FAILED(hr) || buffer == nullptr) { + releaseComPtr(sample); + continue; + } + + BYTE* data = nullptr; + DWORD maxLength = 0; + DWORD currentLength = 0; + hr = buffer->Lock(&data, &maxLength, ¤tLength); + if (FAILED(hr) || data == nullptr) { + releaseComPtr(buffer); + releaseComPtr(sample); + continue; + } + + auto newFrame = getFreeFrame(); + if (!newFrame) { + buffer->Unlock(); + releaseComPtr(buffer); + releaseComPtr(sample); + continue; + } + + newFrame->timestamp = static_cast(timestamp * 100); + newFrame->pixelFormat = m_activePixelFormat; + newFrame->width = m_activeWidth; + newFrame->height = m_activeHeight; + newFrame->nativeHandle = sample; + + bool isOutputYUV = (m_frameProp.outputPixelFormat & kPixelFormatYUVColorBit) != 0; + FrameOrientation targetOrientation = isOutputYUV ? FrameOrientation::TopToBottom : m_frameOrientation; + newFrame->orientation = targetOrientation; + + switch (m_activePixelFormat) { + case PixelFormat::NV12: + newFrame->data[0] = data; + newFrame->data[1] = data + static_cast(m_activeWidth) * static_cast(m_activeHeight); + newFrame->data[2] = nullptr; + newFrame->stride[0] = m_activeWidth; + newFrame->stride[1] = m_activeWidth; + newFrame->stride[2] = 0; + break; + case PixelFormat::I420: + newFrame->data[0] = data; + newFrame->data[1] = data + static_cast(m_activeWidth) * static_cast(m_activeHeight); + newFrame->data[2] = newFrame->data[1] + static_cast(m_activeWidth / 2) * static_cast(m_activeHeight / 2); + newFrame->stride[0] = m_activeWidth; + newFrame->stride[1] = m_activeWidth / 2; + newFrame->stride[2] = m_activeWidth / 2; + break; + case PixelFormat::YUYV: + case PixelFormat::UYVY: + newFrame->data[0] = data; + newFrame->data[1] = nullptr; + newFrame->data[2] = nullptr; + newFrame->stride[0] = m_activeWidth * 2; + newFrame->stride[1] = 0; + newFrame->stride[2] = 0; + break; + case PixelFormat::BGR24: + newFrame->data[0] = data; + newFrame->data[1] = nullptr; + newFrame->data[2] = nullptr; + newFrame->stride[0] = static_cast(m_activeStride0 > 0 ? m_activeStride0 : static_cast(m_activeWidth * 3)); + newFrame->stride[1] = 0; + newFrame->stride[2] = 0; + break; + case PixelFormat::BGRA32: + newFrame->data[0] = data; + newFrame->data[1] = nullptr; + newFrame->data[2] = nullptr; + newFrame->stride[0] = static_cast(m_activeStride0 > 0 ? m_activeStride0 : static_cast(m_activeWidth * 4)); + newFrame->stride[1] = 0; + newFrame->stride[2] = 0; + break; + default: + buffer->Unlock(); + releaseComPtr(buffer); + releaseComPtr(sample); + continue; + } + + bool shouldFlip = !isOutputYUV && targetOrientation != FrameOrientation::TopToBottom; + bool shouldConvert = newFrame->pixelFormat != m_frameProp.outputPixelFormat; + bool zeroCopy = !shouldConvert && !shouldFlip; + + if (!zeroCopy) { + if (!newFrame->allocator) { + newFrame->allocator = m_allocatorFactory ? m_allocatorFactory() : std::make_shared(); + } + + zeroCopy = !inplaceConvertFrame(newFrame.get(), m_frameProp.outputPixelFormat, shouldFlip); + newFrame->sizeInBytes = newFrame->stride[0] * newFrame->height + + (newFrame->stride[1] + newFrame->stride[2]) * newFrame->height / 2; + } + + if (zeroCopy) { + newFrame->sizeInBytes = currentLength; + sample->AddRef(); + buffer->AddRef(); + auto manager = std::make_shared([newFrame, buffer, sample]() mutable { + newFrame = nullptr; + buffer->Unlock(); + buffer->Release(); + sample->Release(); + }); + newFrame = std::shared_ptr(manager, newFrame.get()); + } else { + buffer->Unlock(); + releaseComPtr(buffer); + } + + newFrame->frameIndex = m_frameIndex++; + newFrameAvailable(std::move(newFrame)); + + releaseComPtr(buffer); + releaseComPtr(sample); + } + + m_isRunning = false; + notifyGrabWaiters(); + + if (didInitCom) { + CoUninitialize(); + } +} + +bool ProviderMSMF::start() { + if (!m_isOpened) { + return false; + } + + if (m_isRunning) { + return true; + } + + if (m_readThread.joinable()) { + m_readThread.join(); + } + + m_shouldStop = false; + m_isRunning = true; + m_readThread = std::thread([this]() { + readLoop(); + }); + return true; +} + +void ProviderMSMF::stop() { + if (m_grabFrameWaiting) { + m_grabFrameWaiting = false; + notifyGrabWaiters(); + } + + if (!m_isRunning) { + return; + } + + m_shouldStop = true; + if (m_sourceReader != nullptr) { + m_sourceReader->Flush(MF_SOURCE_READER_FIRST_VIDEO_STREAM); + } + + if (m_readThread.joinable()) { + m_readThread.join(); + } + + m_isRunning = false; +} + +bool ProviderMSMF::isStarted() const { + return m_isRunning && m_isOpened; +} + +void ProviderMSMF::close() { + stop(); + + releaseComPtr(m_sourceReader); + if (m_mediaSource != nullptr) { + m_mediaSource->Shutdown(); + } + releaseComPtr(m_mediaSource); + + { + std::lock_guard lock(m_availableFrameMutex); + m_availableFrames = {}; + } + + m_isOpened = false; + m_shouldStop = false; + m_isFileMode = false; + m_activePixelFormat = PixelFormat::Unknown; + m_activeWidth = 0; + m_activeHeight = 0; + m_activeFps = 0.0; + m_activeStride0 = 0; +} + +ProviderImp* createProviderMSMF() { + return new ProviderMSMF(); +} + +} // namespace ccap + +#endif \ No newline at end of file diff --git a/src/ccap_imp_windows_msmf.h b/src/ccap_imp_windows_msmf.h new file mode 100644 index 00000000..c6e06a6b --- /dev/null +++ b/src/ccap_imp_windows_msmf.h @@ -0,0 +1,99 @@ +/** + * @file ccap_imp_windows_msmf.h + * @brief Header file for ProviderMSMF class. + * @date 2026-03 + */ + +#pragma once +#ifndef CAMERA_CAPTURE_MSMF_H +#define CAMERA_CAPTURE_MSMF_H + +#if defined(_WIN32) || defined(_MSC_VER) + +#include "ccap_imp.h" + +#include +#include +#include +#include +#include + +struct IMFMediaSource; +struct IMFMediaType; +struct IMFSourceReader; + +namespace ccap { + +class ProviderMSMF : public ProviderImp { +public: + ProviderMSMF(); + ~ProviderMSMF() override; + + std::vector findDeviceNames() override; + bool open(std::string_view deviceName) override; + bool isOpened() const override; + std::optional getDeviceInfo() const override; + void close() override; + bool start() override; + void stop() override; + bool isStarted() const override; + +private: + struct DeviceEntry { + std::string friendlyName; + std::wstring symbolicLink; + }; + + struct MediaTypeInfo { + IMFMediaType* mediaType = nullptr; + GUID subtype{}; + PixelFormat pixelFormat = PixelFormat::Unknown; + uint32_t width = 0; + uint32_t height = 0; + uint32_t frameRateNumerator = 0; + uint32_t frameRateDenominator = 0; + double fps = 0.0; + bool isCompressed = false; + }; + +private: + bool setup(); + bool initMediaFoundation(); + void uninitMediaFoundation(); + bool ensureDeviceCache(); + bool enumerateDevices(std::vector& devices); + bool createMediaSource(const std::wstring& symbolicLink); + bool createSourceReader(); + std::vector enumerateMediaTypes() const; + void releaseMediaTypes(std::vector& mediaTypes) const; + bool configureMediaType(); + bool trySetCurrentMediaType(const MediaTypeInfo& selected, const GUID& subtype); + bool updateCurrentMediaType(); + void readLoop(); + +private: + std::vector m_devices; + std::string m_deviceName; + std::wstring m_deviceSymbolicLink; + IMFMediaSource* m_mediaSource = nullptr; + IMFSourceReader* m_sourceReader = nullptr; + std::thread m_readThread; + std::atomic m_shouldStop{ false }; + std::atomic m_isRunning{ false }; + bool m_isOpened{ false }; + bool m_didSetup{ false }; + bool m_didInitializeCom{ false }; + bool m_mfInitialized{ false }; + PixelFormat m_activePixelFormat = PixelFormat::Unknown; + uint32_t m_activeWidth = 0; + uint32_t m_activeHeight = 0; + double m_activeFps = 0.0; + int32_t m_activeStride0 = 0; +}; + +ProviderImp* createProviderMSMF(); + +} // namespace ccap + +#endif +#endif // CAMERA_CAPTURE_MSMF_H \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cdb23701..92d261a9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -178,6 +178,36 @@ if (WIN32) /D_CRT_SECURE_NO_WARNINGS ) endif () + + add_executable( + ccap_windows_backend_test + test_windows_backends.cpp + ) + + target_link_libraries( + ccap_windows_backend_test + PRIVATE + ccap_test_utils + gtest + gtest_main + ) + + set_target_properties(ccap_windows_backend_test PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + ) + + if (MSVC) + target_compile_options(ccap_windows_backend_test PRIVATE + /MP + /std:c++17 + /Zc:__cplusplus + /source-charset:utf-8 + /bigobj + /wd4996 + /D_CRT_SECURE_NO_WARNINGS + ) + endif () endif () # Performance test executable - should be compiled in Release mode @@ -347,6 +377,7 @@ if (NOT CMAKE_CROSSCOMPILING) gtest_discover_tests(ccap_performance_test DISCOVERY_MODE PRE_TEST) if (WIN32) gtest_discover_tests(ccap_guid_test DISCOVERY_MODE PRE_TEST) + gtest_discover_tests(ccap_windows_backend_test DISCOVERY_MODE PRE_TEST) endif () if (CCAP_BUILD_CLI) gtest_discover_tests(ccap_cli_test DISCOVERY_MODE PRE_TEST) diff --git a/tests/test_windows_backends.cpp b/tests/test_windows_backends.cpp new file mode 100644 index 00000000..4b39ab4b --- /dev/null +++ b/tests/test_windows_backends.cpp @@ -0,0 +1,449 @@ +/** + * @file test_windows_backends.cpp + * @brief Windows backend selection and fallback tests + * @date 2026-03-07 + */ + +#if defined(_WIN32) + +#include "test_utils.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +namespace { + +class ScopedEnvVar { +public: + ScopedEnvVar(const char* name, const char* value) : + m_name(name) { + char* oldValue = nullptr; + size_t length = 0; + if (_dupenv_s(&oldValue, &length, name) == 0 && oldValue != nullptr) { + m_hadValue = true; + m_oldValue.assign(oldValue, length == 0 ? 0 : length - 1); + free(oldValue); + } + + _putenv_s(name, value); + } + + ~ScopedEnvVar() { + if (m_hadValue) { + _putenv_s(m_name.c_str(), m_oldValue.c_str()); + } else { + _putenv_s(m_name.c_str(), ""); + } + } + +private: + std::string m_name; + std::string m_oldValue; + bool m_hadValue = false; +}; + +fs::path findProjectRoot() { + fs::path projectRoot = fs::current_path(); + while (projectRoot.has_parent_path()) { + if (fs::exists(projectRoot / "CMakeLists.txt") && fs::exists(projectRoot / "tests")) { + return projectRoot; + } + projectRoot = projectRoot.parent_path(); + } + return {}; +} + +fs::path getBuiltInTestVideoPath() { + fs::path projectRoot = findProjectRoot(); + if (projectRoot.empty()) { + return {}; + } + return projectRoot / "tests" / "test-data" / "test.mp4"; +} + +std::vector listDevicesForBackend(const char* backend) { + ScopedEnvVar env("CCAP_WINDOWS_BACKEND", backend); + ccap::Provider provider; + return provider.findDeviceNames(); +} + +int virtualCameraRank(std::string_view name) { + std::string normalized; + normalized.reserve(name.size()); + for (char ch : name) { + normalized.push_back(static_cast(std::tolower(static_cast(ch)))); + } + + constexpr std::array keywords = { + "obs", + "virtual", + "fake", + }; + + for (size_t index = 0; index < keywords.size(); ++index) { + if (normalized.find(keywords[index]) != std::string::npos) { + return static_cast(index); + } + } + + return -1; +} + +std::vector listCommonDevices() { + auto dshowDevices = listDevicesForBackend("dshow"); + auto msmfDevices = listDevicesForBackend("msmf"); + std::vector common; + + for (const std::string& device : dshowDevices) { + if (std::find(msmfDevices.begin(), msmfDevices.end(), device) != msmfDevices.end()) { + common.push_back(device); + } + } + + std::stable_sort(common.begin(), common.end(), [](const std::string& lhs, const std::string& rhs) { + return virtualCameraRank(lhs) < virtualCameraRank(rhs); + }); + return common; +} + +std::optional getDeviceInfoForBackend(const char* backend, const std::string& deviceName) { + ScopedEnvVar env("CCAP_WINDOWS_BACKEND", backend); + ccap::Provider provider; + if (!provider.open(deviceName, false)) { + return std::nullopt; + } + + auto info = provider.getDeviceInfo(); + provider.close(); + return info; +} + +std::optional pickCommonResolution(const std::string& deviceName) { + auto dshowInfo = getDeviceInfoForBackend("dshow", deviceName); + auto msmfInfo = getDeviceInfoForBackend("msmf", deviceName); + if (!dshowInfo || !msmfInfo) { + return std::nullopt; + } + + std::set> dshowResolutions; + for (const auto& resolution : dshowInfo->supportedResolutions) { + dshowResolutions.emplace(resolution.width, resolution.height); + } + + std::vector common; + for (const auto& resolution : msmfInfo->supportedResolutions) { + if (dshowResolutions.count({ resolution.width, resolution.height }) != 0) { + common.push_back(resolution); + } + } + + if (common.empty()) { + return std::nullopt; + } + + constexpr std::array, 3> preferredResolutions = { + std::pair{ 640, 480 }, + std::pair{ 1280, 720 }, + std::pair{ 1920, 1080 }, + }; + for (const auto& preferred : preferredResolutions) { + auto match = std::find_if(common.begin(), common.end(), [&](const ccap::DeviceInfo::Resolution& resolution) { + return resolution.width == preferred.first && resolution.height == preferred.second; + }); + if (match != common.end()) { + return *match; + } + } + + std::stable_sort(common.begin(), common.end(), [](const ccap::DeviceInfo::Resolution& lhs, const ccap::DeviceInfo::Resolution& rhs) { + uint64_t lhsArea = static_cast(lhs.width) * lhs.height; + uint64_t rhsArea = static_cast(rhs.width) * rhs.height; + if (lhsArea != rhsArea) { + return lhsArea < rhsArea; + } + if (lhs.width != rhs.width) { + return lhs.width < rhs.width; + } + return lhs.height < rhs.height; + }); + return common.front(); +} + +struct CapturedFrame { + uint32_t width = 0; + uint32_t height = 0; + uint32_t stride = 0; + ccap::PixelFormat pixelFormat = ccap::PixelFormat::Unknown; + ccap::FrameOrientation orientation = ccap::FrameOrientation::Default; + std::vector bytes; +}; + +CapturedFrame copyFrame(const std::shared_ptr& frame) { + CapturedFrame captured; + captured.width = frame->width; + captured.height = frame->height; + captured.stride = frame->stride[0]; + captured.pixelFormat = frame->pixelFormat; + captured.orientation = frame->orientation; + captured.bytes.assign(frame->data[0], frame->data[0] + static_cast(frame->stride[0]) * frame->height); + return captured; +} + +std::optional> captureBurstForBackend( + const char* backend, + const std::string& deviceName, + const ccap::DeviceInfo::Resolution& resolution, + int warmupFrames = 12, + int capturedFrames = 3) { + ScopedEnvVar env("CCAP_WINDOWS_BACKEND", backend); + ccap::Provider provider; + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + provider.set(ccap::PropertyName::Width, resolution.width); + provider.set(ccap::PropertyName::Height, resolution.height); + provider.set(ccap::PropertyName::PixelFormatOutput, ccap::PixelFormat::BGR24); + provider.set(ccap::PropertyName::FrameOrientation, ccap::FrameOrientation::TopToBottom); + + if (!provider.open(deviceName, false)) { + return std::nullopt; + } + + bool started = false; + for (int attempt = 0; attempt < 3; ++attempt) { + if (provider.start()) { + started = true; + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + } + if (!started) { + provider.close(); + return std::nullopt; + } + + for (int index = 0; index < warmupFrames; ++index) { + auto frame = provider.grab(3000); + if (!frame) { + provider.close(); + return std::nullopt; + } + } + + std::vector burst; + burst.reserve(static_cast(capturedFrames)); + for (int index = 0; index < capturedFrames; ++index) { + auto frame = provider.grab(3000); + if (!frame) { + provider.close(); + return std::nullopt; + } + + if (frame->pixelFormat != ccap::PixelFormat::BGR24 || frame->orientation != ccap::FrameOrientation::TopToBottom) { + provider.close(); + return std::nullopt; + } + + burst.push_back(copyFrame(frame)); + } + + provider.stop(); + provider.close(); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + return burst; +} + +struct ImageDiffStats { + double mse = 0.0; + double psnr = 0.0; + double meanAbsDiff = 0.0; + double withinToleranceRatio = 0.0; + uint8_t maxAbsDiff = 0; + size_t totalSamples = 0; +}; + +ImageDiffStats calculateImageDiffStats(const CapturedFrame& lhs, const CapturedFrame& rhs, uint8_t tolerance) { + ImageDiffStats stats; + if (lhs.width != rhs.width || lhs.height != rhs.height || lhs.pixelFormat != rhs.pixelFormat) { + stats.psnr = -std::numeric_limits::infinity(); + return stats; + } + + const size_t rowBytes = static_cast(lhs.width) * 3; + uint64_t totalAbsDiff = 0; + uint64_t withinToleranceCount = 0; + long double totalSquaredDiff = 0.0; + + for (uint32_t row = 0; row < lhs.height; ++row) { + const uint8_t* lhsRow = lhs.bytes.data() + static_cast(lhs.stride) * row; + const uint8_t* rhsRow = rhs.bytes.data() + static_cast(rhs.stride) * row; + for (size_t column = 0; column < rowBytes; ++column) { + uint8_t diff = static_cast(std::abs(static_cast(lhsRow[column]) - static_cast(rhsRow[column]))); + totalAbsDiff += diff; + totalSquaredDiff += static_cast(diff) * diff; + withinToleranceCount += diff <= tolerance ? 1 : 0; + stats.maxAbsDiff = std::max(stats.maxAbsDiff, diff); + } + } + + stats.totalSamples = rowBytes * lhs.height; + stats.meanAbsDiff = static_cast(totalAbsDiff) / static_cast(stats.totalSamples); + stats.withinToleranceRatio = static_cast(withinToleranceCount) / static_cast(stats.totalSamples); + stats.mse = static_cast(totalSquaredDiff / static_cast(stats.totalSamples)); + if (stats.mse == 0.0) { + stats.psnr = std::numeric_limits::infinity(); + } else { + stats.psnr = 10.0 * std::log10((255.0 * 255.0) / stats.mse); + } + return stats; +} + +std::optional findBestBurstMatch(const std::vector& dshowFrames, const std::vector& msmfFrames) { + std::optional best; + for (const auto& dshowFrame : dshowFrames) { + for (const auto& msmfFrame : msmfFrames) { + auto stats = calculateImageDiffStats(dshowFrame, msmfFrame, 24); + if (!best || stats.psnr > best->psnr) { + best = stats; + } + } + } + return best; +} + +std::string formatDiffStats(const ImageDiffStats& stats) { + std::ostringstream stream; + stream << "PSNR=" << stats.psnr << " dB, MSE=" << stats.mse << ", mean abs diff=" << stats.meanAbsDiff + << ", within tolerance=" << (stats.withinToleranceRatio * 100.0) << "%, max diff=" + << static_cast(stats.maxAbsDiff); + return stream.str(); +} + +} // namespace + +TEST(WindowsBackendsTest, ForcedBackendsEnumerateWithoutCrash) { + for (const char* backend : { "dshow", "msmf" }) { + SCOPED_TRACE(backend); + auto deviceNames = listDevicesForBackend(backend); + for (const std::string& name : deviceNames) { + EXPECT_FALSE(name.empty()); + } + } +} + +TEST(WindowsBackendsTest, AutoEnumerationContainsForcedBackendResults) { + auto dshowDevices = listDevicesForBackend("dshow"); + auto msmfDevices = listDevicesForBackend("msmf"); + auto autoDevices = listDevicesForBackend("auto"); + + for (const std::string& name : dshowDevices) { + EXPECT_NE(std::find(autoDevices.begin(), autoDevices.end(), name), autoDevices.end()) << name; + } + + for (const std::string& name : msmfDevices) { + EXPECT_NE(std::find(autoDevices.begin(), autoDevices.end(), name), autoDevices.end()) << name; + } +} + +TEST(WindowsBackendsTest, FilePlaybackStillWorksWhenMSMFIsForced) { +#ifdef CCAP_ENABLE_FILE_PLAYBACK + ScopedEnvVar env("CCAP_WINDOWS_BACKEND", "msmf"); + fs::path videoPath = getBuiltInTestVideoPath(); + if (videoPath.empty() || !fs::exists(videoPath)) { + GTEST_SKIP() << "Built-in test video not found"; + } + + ccap::Provider provider; + ASSERT_TRUE(provider.open(videoPath.string(), false)); + EXPECT_TRUE(provider.isOpened()); + EXPECT_TRUE(provider.isFileMode()); + provider.close(); +#else + GTEST_SKIP() << "File playback support is disabled"; +#endif +} + +TEST(WindowsBackendsTest, ForcedBackendCanOpenWhenDeviceExists) { + for (const char* backend : { "dshow", "msmf" }) { + SCOPED_TRACE(backend); + ScopedEnvVar env("CCAP_WINDOWS_BACKEND", backend); + ccap::Provider probe; + auto deviceNames = probe.findDeviceNames(); + if (deviceNames.empty()) { + continue; + } + + ccap::Provider provider; + ASSERT_TRUE(provider.open(deviceNames.front(), false)); + EXPECT_TRUE(provider.isOpened()); + provider.close(); + } +} + +TEST(WindowsBackendsTest, SharedPhysicalCameraProducesConsistentFrameMetadata) { + auto commonDevices = listCommonDevices(); + if (commonDevices.empty()) { + GTEST_SKIP() << "No camera is shared between DirectShow and Media Foundation"; + } + + auto resolution = pickCommonResolution(commonDevices.front()); + if (!resolution) { + GTEST_SKIP() << "No common resolution was reported for the shared camera"; + } + + auto dshowFrames = captureBurstForBackend("dshow", commonDevices.front(), *resolution, 8, 1); + auto msmfFrames = captureBurstForBackend("msmf", commonDevices.front(), *resolution, 8, 1); + ASSERT_TRUE(dshowFrames.has_value()) << "DirectShow failed to capture a frame"; + ASSERT_TRUE(msmfFrames.has_value()) << "MSMF failed to capture a frame"; + ASSERT_EQ(dshowFrames->size(), 1u); + ASSERT_EQ(msmfFrames->size(), 1u); + + const auto& dshowFrame = dshowFrames->front(); + const auto& msmfFrame = msmfFrames->front(); + EXPECT_EQ(dshowFrame.width, msmfFrame.width); + EXPECT_EQ(dshowFrame.height, msmfFrame.height); + EXPECT_EQ(dshowFrame.pixelFormat, ccap::PixelFormat::BGR24); + EXPECT_EQ(msmfFrame.pixelFormat, ccap::PixelFormat::BGR24); + EXPECT_EQ(dshowFrame.orientation, ccap::FrameOrientation::TopToBottom); + EXPECT_EQ(msmfFrame.orientation, ccap::FrameOrientation::TopToBottom); +} + +TEST(WindowsBackendsTest, MSMFFramesMatchDirectShowWithinTolerance) { + auto commonDevices = listCommonDevices(); + if (commonDevices.empty()) { + GTEST_SKIP() << "No camera is shared between DirectShow and Media Foundation"; + } + + auto resolution = pickCommonResolution(commonDevices.front()); + if (!resolution) { + GTEST_SKIP() << "No common resolution was reported for the shared camera"; + } + + auto dshowFrames = captureBurstForBackend("dshow", commonDevices.front(), *resolution); + auto msmfFrames = captureBurstForBackend("msmf", commonDevices.front(), *resolution); + ASSERT_TRUE(dshowFrames.has_value()) << "DirectShow failed to capture a comparison burst"; + ASSERT_TRUE(msmfFrames.has_value()) << "MSMF failed to capture a comparison burst"; + + auto bestStats = findBestBurstMatch(*dshowFrames, *msmfFrames); + ASSERT_TRUE(bestStats.has_value()) << "Failed to compare DirectShow and MSMF bursts"; + + EXPECT_GE(bestStats->psnr, 20.0) << formatDiffStats(*bestStats); + EXPECT_LE(bestStats->meanAbsDiff, 12.0) << formatDiffStats(*bestStats); + EXPECT_GE(bestStats->withinToleranceRatio, 0.80) << formatDiffStats(*bestStats); +} + +#endif \ No newline at end of file From b505638ce07f650a80f428f91f9ea05ea2e458cc Mon Sep 17 00:00:00 2001 From: "wangyang (wysaid)" Date: Sat, 7 Mar 2026 16:00:31 +0800 Subject: [PATCH 02/11] Use PowerShell tasks and enforce LF for Unix-sensitive files --- .vscode/tasks.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index bc1c5702..802a56c3 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -82,7 +82,7 @@ "-ExecutionPolicy", "Bypass", "-Command", - "git clean -fdx build; if ($LASTEXITCODE -ne 0 -and (Test-Path 'build')) { Remove-Item -Recurse -Force 'build' }; git clean -fdx build_shared; if ($LASTEXITCODE -ne 0 -and (Test-Path 'build_shared')) { Remove-Item -Recurse -Force 'build_shared' }; git clean -fdx examples; if ($LASTEXITCODE -ne 0 -and (Test-Path 'examples')) { Remove-Item -Recurse -Force 'examples' }" + "git clean -fdx build; if ($LASTEXITCODE -ne 0 -and (Test-Path 'build')) { Remove-Item -Recurse -Force 'build' }; git clean -fdx build_shared; if ($LASTEXITCODE -ne 0 -and (Test-Path 'build_shared')) { Remove-Item -Recurse -Force 'build_shared' }" ], "options": { "cwd": "${workspaceFolder}" From 55ebfdcfe7503dd880c973f1c171703b26d18455 Mon Sep 17 00:00:00 2001 From: "wangyang (wysaid)" Date: Sat, 7 Mar 2026 17:05:09 +0800 Subject: [PATCH 03/11] Address review fixes for Windows MSMF backend --- CMakeLists.txt | 18 ++- README.zh-CN.md | 13 +- bindings/rust/build.rs | 4 +- bindings/rust/src/provider.rs | 47 ++++-- docs/content/implementation-details.md | 3 +- include/ccap_core.h | 24 +-- src/ccap_core.cpp | 216 +++++++++++++++++++------ src/ccap_imp_windows_msmf.cpp | 13 +- tests/test_windows_backends.cpp | 35 ++-- 9 files changed, 267 insertions(+), 106 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cd635f61..caa431da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -253,11 +253,19 @@ elseif (WIN32) # MSVC: Delay load Media Foundation DLLs to preserve legacy DirectShow-only startup paths. if (MSVC) - target_link_options(ccap PRIVATE - /DELAYLOAD:mf.dll - /DELAYLOAD:mfplat.dll - /DELAYLOAD:mfreadwrite.dll - ) + if (CCAP_BUILD_SHARED) + target_link_options(ccap PRIVATE + /DELAYLOAD:mf.dll + /DELAYLOAD:mfplat.dll + /DELAYLOAD:mfreadwrite.dll + ) + else () + target_link_options(ccap INTERFACE + /DELAYLOAD:mf.dll + /DELAYLOAD:mfplat.dll + /DELAYLOAD:mfreadwrite.dll + ) + endif () target_link_libraries(ccap PUBLIC delayimp.lib) endif () else () diff --git a/README.zh-CN.md b/README.zh-CN.md index 8a8140ea..625c8132 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -182,8 +182,17 @@ Windows 上默认优先使用 Media Foundation,并在需要时自动回退到 - 设置环境变量 `CCAP_WINDOWS_BACKEND=auto|msmf|dshow`,对整个进程生效,包括 CLI 和 Rust 绑定。 ```cpp -ccap::Provider msmfProvider("", "msmf"); -ccap::Provider dshowProvider("", "dshow"); +// 下面两段是互斥示例;同一时刻不要对同一设备同时创建两个 Provider。 + +// Force MSMF +{ + ccap::Provider provider("", "msmf"); +} + +// Force DirectShow +{ + ccap::Provider provider("", "dshow"); +} ``` ### Rust 绑定 diff --git a/bindings/rust/build.rs b/bindings/rust/build.rs index 5adb5d90..516983a3 100644 --- a/bindings/rust/build.rs +++ b/bindings/rust/build.rs @@ -196,6 +196,7 @@ Please vendor the sources into bindings/rust/native/, or set CCAP_SOURCE_DIR to { build .file(ccap_root.join("src/ccap_imp_windows.cpp")) + .file(ccap_root.join("src/ccap_imp_windows_msmf.cpp")) .file(ccap_root.join("src/ccap_file_reader_windows.cpp")); } @@ -368,10 +369,11 @@ Please vendor the sources into bindings/rust/native/, or set CCAP_SOURCE_DIR to #[cfg(target_os = "windows")] { + println!("cargo:rustc-link-lib=mf"); println!("cargo:rustc-link-lib=strmiids"); println!("cargo:rustc-link-lib=ole32"); println!("cargo:rustc-link-lib=oleaut32"); - // Media Foundation libraries for video file playback + // Media Foundation libraries for the MSMF camera backend and video file playback println!("cargo:rustc-link-lib=mfplat"); println!("cargo:rustc-link-lib=mfreadwrite"); println!("cargo:rustc-link-lib=mfuuid"); diff --git a/bindings/rust/src/provider.rs b/bindings/rust/src/provider.rs index 20c3a19d..e03e8b2e 100644 --- a/bindings/rust/src/provider.rs +++ b/bindings/rust/src/provider.rs @@ -96,15 +96,14 @@ impl Provider { /// /// On Windows, `extra_info` can be used to force backend selection with values like /// `"auto"`, `"msmf"`, `"dshow"`, or `"backend="`. - pub fn with_device_and_extra_info( - device_index: i32, - extra_info: Option<&str>, - ) -> Result { + pub fn with_device_and_extra_info(device_index: i32, extra_info: Option<&str>) -> Result { let extra_info = optional_c_string(extra_info, "extra info")?; let handle = unsafe { sys::ccap_provider_create_with_index( device_index, - extra_info.as_ref().map_or(ptr::null(), |value| value.as_ptr()), + extra_info + .as_ref() + .map_or(ptr::null(), |value| value.as_ptr()), ) }; if handle.is_null() { @@ -144,7 +143,9 @@ impl Provider { let handle = unsafe { sys::ccap_provider_create_with_device( c_name.as_ptr(), - extra_info.as_ref().map_or(ptr::null(), |value| value.as_ptr()), + extra_info + .as_ref() + .map_or(ptr::null(), |value| value.as_ptr()), ) }; if handle.is_null() { @@ -271,6 +272,11 @@ impl Provider { auto_start: bool, ) -> Result<()> { if let Some(name) = device_name { + let c_name = CString::new(name).map_err(|_| { + CcapError::InvalidParameter("device name contains null byte".to_string()) + })?; + let extra_info = optional_c_string(extra_info, "extra info")?; + // Recreate provider with specific device if !self.handle.is_null() { // If the previous provider was running, stop it and detach callbacks @@ -281,21 +287,27 @@ impl Provider { unsafe { sys::ccap_provider_destroy(self.handle); } + self.handle = ptr::null_mut(); + self.is_opened = false; + } else { + self.cleanup_callback(); } - let c_name = CString::new(name).map_err(|_| { - CcapError::InvalidParameter("device name contains null byte".to_string()) - })?; - let extra_info = optional_c_string(extra_info, "extra info")?; + self.handle = unsafe { sys::ccap_provider_create_with_device( c_name.as_ptr(), - extra_info.as_ref().map_or(ptr::null(), |value| value.as_ptr()), + extra_info + .as_ref() + .map_or(ptr::null(), |value| value.as_ptr()), ) }; if self.handle.is_null() { return Err(CcapError::InvalidDevice(name.to_string())); } self.is_opened = true; + if !auto_start { + self.stop_capture()?; + } } else if extra_info.is_some() { return self.open_with_index_and_extra_info(-1, extra_info, auto_start); } else { @@ -581,6 +593,8 @@ impl Provider { extra_info: Option<&str>, auto_start: bool, ) -> Result<()> { + let extra_info = optional_c_string(extra_info, "extra info")?; + // If the previous provider was running, stop it and detach callbacks // before destroying the underlying handle. if !self.handle.is_null() { @@ -590,18 +604,20 @@ impl Provider { unsafe { sys::ccap_provider_destroy(self.handle); } + self.handle = ptr::null_mut(); + self.is_opened = false; } else { // Clean up any stale callback allocation even if handle is null. self.cleanup_callback(); } - let extra_info = optional_c_string(extra_info, "extra info")?; - // Create a new provider with the specified device index self.handle = unsafe { sys::ccap_provider_create_with_index( device_index, - extra_info.as_ref().map_or(ptr::null(), |value| value.as_ptr()), + extra_info + .as_ref() + .map_or(ptr::null(), |value| value.as_ptr()), ) }; @@ -614,6 +630,9 @@ impl Provider { // ccap C API contract: create_with_index opens the device. self.is_opened = true; + if !auto_start { + self.stop_capture()?; + } if auto_start { self.start_capture()?; } diff --git a/docs/content/implementation-details.md b/docs/content/implementation-details.md index 7d6a7b04..a42ed885 100644 --- a/docs/content/implementation-details.md +++ b/docs/content/implementation-details.md @@ -218,7 +218,8 @@ Or use the script: **Media Foundation Backend:** - Preferred on modern Windows systems - Uses Source Reader for frame delivery and format negotiation -- Automatically falls back to DirectShow when Media Foundation is unavailable or device open fails +- When backend selection is `auto`, `Provider::open()` falls back to DirectShow if Media Foundation is unavailable or device open fails +- When callers explicitly request `msmf` via `extraInfo` or `CCAP_WINDOWS_BACKEND`, `Provider::open()` returns an error instead of falling back **DirectShow Backend:** - Mature, stable API diff --git a/include/ccap_core.h b/include/ccap_core.h index 45cc87ee..887eb6be 100644 --- a/include/ccap_core.h +++ b/include/ccap_core.h @@ -229,8 +229,8 @@ class CCAP_EXPORT Provider final { // ↓ This part is not relevant to the user ↓ - Provider(Provider&&) = default; - Provider& operator=(Provider&&) = default; + Provider(Provider&&) noexcept; + Provider& operator=(Provider&&) noexcept; ~Provider(); private: @@ -238,25 +238,7 @@ class CCAP_EXPORT Provider final { bool tryOpenWithImplementation(ProviderImp* imp, std::string_view deviceName, bool autoStart) const; private: - std::string m_extraInfo; - std::function&)> m_frameCallback; - std::function()> m_allocatorFactory; - uint32_t m_maxAvailableFrameSize = DEFAULT_MAX_AVAILABLE_FRAME_SIZE; - uint32_t m_maxCacheFrameSize = DEFAULT_MAX_CACHE_FRAME_SIZE; - int m_requestedWidth = 640; - int m_requestedHeight = 480; - double m_requestedFrameRate = 0.0; - PixelFormat m_requestedInternalFormat = PixelFormat::Unknown; - PixelFormat m_requestedOutputFormat{ -#ifdef __APPLE__ - PixelFormat::BGRA32 -#else - PixelFormat::BGR24 -#endif - }; - bool m_hasFrameOrientationOverride = false; - FrameOrientation m_requestedFrameOrientation = FrameOrientation::Default; - ProviderImp* m_imp; + ProviderImp* m_imp = nullptr; }; /** diff --git a/src/ccap_core.cpp b/src/ccap_core.cpp index 6f992e4a..e1025cc6 100644 --- a/src/ccap_core.cpp +++ b/src/ccap_core.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #if defined(_WIN32) || defined(_MSC_VER) #include @@ -47,6 +48,92 @@ ProviderImp* createProviderV4L2(); namespace { std::mutex g_errorCallbackMutex; ErrorCallback g_globalErrorCallback; +std::mutex g_providerStateMutex; + +struct ProviderCachedState { + std::string extraInfo; + std::function&)> frameCallback; + std::function()> allocatorFactory; + uint32_t maxAvailableFrameSize = DEFAULT_MAX_AVAILABLE_FRAME_SIZE; + uint32_t maxCacheFrameSize = DEFAULT_MAX_CACHE_FRAME_SIZE; + int requestedWidth = 640; + int requestedHeight = 480; + double requestedFrameRate = 0.0; + PixelFormat requestedInternalFormat = PixelFormat::Unknown; + PixelFormat requestedOutputFormat{ +#ifdef __APPLE__ + PixelFormat::BGRA32 +#else + PixelFormat::BGR24 +#endif + }; + bool hasFrameOrientationOverride = false; + FrameOrientation requestedFrameOrientation = FrameOrientation::Default; +}; + +std::unordered_map g_providerStates; + +ProviderCachedState makeProviderCachedState(std::string_view extraInfo = {}) { + ProviderCachedState state; + state.extraInfo = std::string(extraInfo); + return state; +} + +ProviderCachedState copyProviderState(ProviderImp* imp) { + if (imp == nullptr) { + return makeProviderCachedState(); + } + + std::lock_guard lock(g_providerStateMutex); + auto iterator = g_providerStates.find(imp); + return iterator != g_providerStates.end() ? iterator->second : makeProviderCachedState(); +} + +void storeProviderState(ProviderImp* imp, ProviderCachedState state) { + if (imp == nullptr) { + return; + } + + std::lock_guard lock(g_providerStateMutex); + g_providerStates[imp] = std::move(state); +} + +template +void updateProviderState(ProviderImp* imp, Fn&& updater) { + if (imp == nullptr) { + return; + } + + std::lock_guard lock(g_providerStateMutex); + auto [iterator, inserted] = g_providerStates.emplace(imp, makeProviderCachedState()); + (void)inserted; + updater(iterator->second); +} + +void eraseProviderState(ProviderImp* imp) { + if (imp == nullptr) { + return; + } + + std::lock_guard lock(g_providerStateMutex); + g_providerStates.erase(imp); +} + +void transferProviderState(ProviderImp* from, ProviderImp* to) { + if (to == nullptr) { + return; + } + + std::lock_guard lock(g_providerStateMutex); + auto node = g_providerStates.extract(from); + if (node.empty()) { + g_providerStates.emplace(to, makeProviderCachedState()); + return; + } + + node.key() = to; + g_providerStates.insert(std::move(node)); +} #if defined(_WIN32) || defined(_MSC_VER) enum class WindowsBackendPreference { @@ -296,19 +383,39 @@ Provider::Provider() : if (!m_imp) { reportError(ErrorCode::InitializationFailed, ErrorMessages::FAILED_TO_CREATE_PROVIDER); } else { + storeProviderState(m_imp, makeProviderCachedState()); applyCachedState(m_imp); } } +Provider::Provider(Provider&& other) noexcept : + m_imp(other.m_imp) { + other.m_imp = nullptr; +} + +Provider& Provider::operator=(Provider&& other) noexcept { + if (this == &other) { + return *this; + } + + eraseProviderState(m_imp); + delete m_imp; + + m_imp = other.m_imp; + other.m_imp = nullptr; + return *this; +} + Provider::~Provider() { CCAP_LOG_V("ccap: Provider::~Provider() called, this=%p, imp=%p\n", this, m_imp); + eraseProviderState(m_imp); delete m_imp; } Provider::Provider(std::string_view deviceName, std::string_view extraInfo) : - m_extraInfo(extraInfo), m_imp(createProvider(extraInfo)) { if (m_imp) { + storeProviderState(m_imp, makeProviderCachedState(extraInfo)); applyCachedState(m_imp); open(deviceName); } else { @@ -317,9 +424,9 @@ Provider::Provider(std::string_view deviceName, std::string_view extraInfo) : } Provider::Provider(int deviceIndex, std::string_view extraInfo) : - m_extraInfo(extraInfo), m_imp(createProvider(extraInfo)) { if (m_imp) { + storeProviderState(m_imp, makeProviderCachedState(extraInfo)); applyCachedState(m_imp); open(deviceIndex); } else { @@ -332,17 +439,19 @@ void Provider::applyCachedState(ProviderImp* imp) const { return; } - imp->setMaxAvailableFrameSize(m_maxAvailableFrameSize); - imp->setMaxCacheFrameSize(m_maxCacheFrameSize); - imp->setNewFrameCallback(m_frameCallback); - imp->setFrameAllocator(m_allocatorFactory); - imp->set(PropertyName::Width, m_requestedWidth); - imp->set(PropertyName::Height, m_requestedHeight); - imp->set(PropertyName::FrameRate, m_requestedFrameRate); - imp->set(PropertyName::PixelFormatInternal, static_cast(m_requestedInternalFormat)); - imp->set(PropertyName::PixelFormatOutput, static_cast(m_requestedOutputFormat)); - if (m_hasFrameOrientationOverride) { - imp->set(PropertyName::FrameOrientation, static_cast(m_requestedFrameOrientation)); + ProviderCachedState state = copyProviderState(m_imp); + + imp->setMaxAvailableFrameSize(state.maxAvailableFrameSize); + imp->setMaxCacheFrameSize(state.maxCacheFrameSize); + imp->setNewFrameCallback(state.frameCallback); + imp->setFrameAllocator(state.allocatorFactory); + imp->set(PropertyName::Width, state.requestedWidth); + imp->set(PropertyName::Height, state.requestedHeight); + imp->set(PropertyName::FrameRate, state.requestedFrameRate); + imp->set(PropertyName::PixelFormatInternal, static_cast(state.requestedInternalFormat)); + imp->set(PropertyName::PixelFormatOutput, static_cast(state.requestedOutputFormat)); + if (state.hasFrameOrientationOverride) { + imp->set(PropertyName::FrameOrientation, static_cast(state.requestedFrameOrientation)); } } @@ -375,7 +484,7 @@ std::vector Provider::findDeviceNames() { return m_imp->findDeviceNames(); } - WindowsBackendPreference preference = resolveWindowsBackendPreference(m_extraInfo); + WindowsBackendPreference preference = resolveWindowsBackendPreference(copyProviderState(m_imp).extraInfo); if (preference == WindowsBackendPreference::DirectShow) { return collectDeviceNamesFromBackend(WindowsBackendPreference::DirectShow); } @@ -409,6 +518,7 @@ bool Provider::open(std::string_view deviceName, bool autoStart) { return false; } + transferProviderState(m_imp, candidate.get()); delete m_imp; m_imp = candidate.release(); return true; @@ -418,7 +528,7 @@ bool Provider::open(std::string_view deviceName, bool autoStart) { return tryBackend(WindowsBackendPreference::DirectShow); } - WindowsBackendPreference preference = resolveWindowsBackendPreference(m_extraInfo); + WindowsBackendPreference preference = resolveWindowsBackendPreference(copyProviderState(m_imp).extraInfo); if (preference == WindowsBackendPreference::DirectShow) { return tryBackend(WindowsBackendPreference::DirectShow); } @@ -498,39 +608,41 @@ bool Provider::set(PropertyName prop, double value) { return false; } - switch (prop) { - case PropertyName::Width: - m_requestedWidth = static_cast(value); - break; - case PropertyName::Height: - m_requestedHeight = static_cast(value); - break; - case PropertyName::FrameRate: - m_requestedFrameRate = value; - break; - case PropertyName::PixelFormatInternal: { - auto intValue = static_cast(value); + updateProviderState(m_imp, [&](ProviderCachedState& state) { + switch (prop) { + case PropertyName::Width: + state.requestedWidth = static_cast(value); + break; + case PropertyName::Height: + state.requestedHeight = static_cast(value); + break; + case PropertyName::FrameRate: + state.requestedFrameRate = value; + break; + case PropertyName::PixelFormatInternal: { + auto intValue = static_cast(value); #if defined(_MSC_VER) || defined(_WIN32) - intValue &= ~kPixelFormatFullRangeBit; + intValue &= ~kPixelFormatFullRangeBit; #endif - m_requestedInternalFormat = static_cast(intValue); - break; - } - case PropertyName::PixelFormatOutput: { - uint32_t formatValue = static_cast(value); + state.requestedInternalFormat = static_cast(intValue); + break; + } + case PropertyName::PixelFormatOutput: { + uint32_t formatValue = static_cast(value); #if defined(_MSC_VER) || defined(_WIN32) - formatValue &= ~static_cast(kPixelFormatFullRangeBit); + formatValue &= ~static_cast(kPixelFormatFullRangeBit); #endif - m_requestedOutputFormat = static_cast(formatValue); - break; - } - case PropertyName::FrameOrientation: - m_requestedFrameOrientation = static_cast(static_cast(value)); - m_hasFrameOrientationOverride = true; - break; - default: - break; - } + state.requestedOutputFormat = static_cast(formatValue); + break; + } + case PropertyName::FrameOrientation: + state.requestedFrameOrientation = static_cast(static_cast(value)); + state.hasFrameOrientationOverride = true; + break; + default: + break; + } + }); return true; } @@ -550,7 +662,9 @@ void Provider::setNewFrameCallback(std::functionsetNewFrameCallback(std::move(callback)); } @@ -559,7 +673,9 @@ void Provider::setFrameAllocator(std::function()> all reportError(ErrorCode::InitializationFailed, ErrorMessages::PROVIDER_IMPLEMENTATION_NULL); return; } - m_allocatorFactory = allocatorFactory; + updateProviderState(m_imp, [&](ProviderCachedState& state) { + state.allocatorFactory = allocatorFactory; + }); m_imp->setFrameAllocator(std::move(allocatorFactory)); } @@ -568,7 +684,9 @@ void Provider::setMaxAvailableFrameSize(uint32_t size) { reportError(ErrorCode::InitializationFailed, ErrorMessages::PROVIDER_IMPLEMENTATION_NULL); return; } - m_maxAvailableFrameSize = size; + updateProviderState(m_imp, [&](ProviderCachedState& state) { + state.maxAvailableFrameSize = size; + }); m_imp->setMaxAvailableFrameSize(size); } @@ -577,7 +695,9 @@ void Provider::setMaxCacheFrameSize(uint32_t size) { reportError(ErrorCode::InitializationFailed, ErrorMessages::PROVIDER_IMPLEMENTATION_NULL); return; } - m_maxCacheFrameSize = size; + updateProviderState(m_imp, [&](ProviderCachedState& state) { + state.maxCacheFrameSize = size; + }); m_imp->setMaxCacheFrameSize(size); } diff --git a/src/ccap_imp_windows_msmf.cpp b/src/ccap_imp_windows_msmf.cpp index fa3418af..9eff0926 100644 --- a/src/ccap_imp_windows_msmf.cpp +++ b/src/ccap_imp_windows_msmf.cpp @@ -41,8 +41,13 @@ std::string wideToUtf8(const wchar_t* text) { return {}; } - std::string value(static_cast(length - 1), '\0'); - WideCharToMultiByte(CP_UTF8, 0, text, -1, value.data(), length, nullptr, nullptr); + std::string value(static_cast(length), '\0'); + int written = WideCharToMultiByte(CP_UTF8, 0, text, -1, value.data(), length, nullptr, nullptr); + if (written <= 0) { + return {}; + } + + value.resize(static_cast(written - 1)); return value; } @@ -670,7 +675,6 @@ std::optional ProviderMSMF::getDeviceInfo() const { void ProviderMSMF::readLoop() { HRESULT comResult = CoInitializeEx(nullptr, COINIT_MULTITHREADED); bool didInitCom = SUCCEEDED(comResult); - m_shouldStop = false; while (!m_shouldStop && m_sourceReader != nullptr) { DWORD streamIndex = 0; @@ -731,7 +735,7 @@ void ProviderMSMF::readLoop() { newFrame->pixelFormat = m_activePixelFormat; newFrame->width = m_activeWidth; newFrame->height = m_activeHeight; - newFrame->nativeHandle = sample; + newFrame->nativeHandle = nullptr; bool isOutputYUV = (m_frameProp.outputPixelFormat & kPixelFormatYUVColorBit) != 0; FrameOrientation targetOrientation = isOutputYUV ? FrameOrientation::TopToBottom : m_frameOrientation; @@ -801,6 +805,7 @@ void ProviderMSMF::readLoop() { } if (zeroCopy) { + newFrame->nativeHandle = sample; newFrame->sizeInBytes = currentLength; sample->AddRef(); buffer->AddRef(); diff --git a/tests/test_windows_backends.cpp b/tests/test_windows_backends.cpp index 4b39ab4b..197fd4cb 100644 --- a/tests/test_windows_backends.cpp +++ b/tests/test_windows_backends.cpp @@ -6,12 +6,20 @@ #if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include + #include "test_utils.h" #include #include #include +#include +#include #include #include #include @@ -33,22 +41,24 @@ class ScopedEnvVar { public: ScopedEnvVar(const char* name, const char* value) : m_name(name) { - char* oldValue = nullptr; - size_t length = 0; - if (_dupenv_s(&oldValue, &length, name) == 0 && oldValue != nullptr) { - m_hadValue = true; - m_oldValue.assign(oldValue, length == 0 ? 0 : length - 1); - free(oldValue); + const DWORD length = GetEnvironmentVariableA(name, nullptr, 0); + if (length > 0) { + std::string buffer(length, '\0'); + const DWORD written = GetEnvironmentVariableA(name, buffer.data(), length); + if (written > 0 && written < length) { + m_hadValue = true; + m_oldValue.assign(buffer.data(), written); + } } - _putenv_s(name, value); + SetEnvironmentVariableA(name, value); } ~ScopedEnvVar() { if (m_hadValue) { - _putenv_s(m_name.c_str(), m_oldValue.c_str()); + SetEnvironmentVariableA(m_name.c_str(), m_oldValue.c_str()); } else { - _putenv_s(m_name.c_str(), ""); + SetEnvironmentVariableA(m_name.c_str(), nullptr); } } @@ -109,9 +119,14 @@ std::vector listCommonDevices() { auto dshowDevices = listDevicesForBackend("dshow"); auto msmfDevices = listDevicesForBackend("msmf"); std::vector common; + auto isUniqueInBoth = [&](const std::string& name) { + return std::count(dshowDevices.begin(), dshowDevices.end(), name) == 1 && + std::count(msmfDevices.begin(), msmfDevices.end(), name) == 1; + }; for (const std::string& device : dshowDevices) { - if (std::find(msmfDevices.begin(), msmfDevices.end(), device) != msmfDevices.end()) { + if (isUniqueInBoth(device) && + std::find(msmfDevices.begin(), msmfDevices.end(), device) != msmfDevices.end()) { common.push_back(device); } } From 10fde51fadcbae72bde5801b9fc9ede055db84e9 Mon Sep 17 00:00:00 2001 From: "wangyang (wysaid)" Date: Sat, 7 Mar 2026 19:20:00 +0800 Subject: [PATCH 04/11] Fix remaining PR review issues --- bindings/rust/build.rs | 4 ++++ src/ccap_imp_windows_msmf.cpp | 12 +++++++++++- src/ccap_imp_windows_msmf.h | 1 + tests/test_windows_backends.cpp | 6 +++++- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/bindings/rust/build.rs b/bindings/rust/build.rs index 516983a3..565851f2 100644 --- a/bindings/rust/build.rs +++ b/bindings/rust/build.rs @@ -460,6 +460,10 @@ Please vendor the sources into bindings/rust/native/, or set CCAP_SOURCE_DIR to ccap_root.display() ); println!( + "cargo:rerun-if-changed={}/src/ccap_imp_windows_msmf.cpp", + ccap_root.display() + ); + println!( "cargo:rerun-if-changed={}/src/ccap_file_reader_windows.cpp", ccap_root.display() ); diff --git a/src/ccap_imp_windows_msmf.cpp b/src/ccap_imp_windows_msmf.cpp index 9eff0926..84455d7f 100644 --- a/src/ccap_imp_windows_msmf.cpp +++ b/src/ccap_imp_windows_msmf.cpp @@ -466,6 +466,15 @@ bool ProviderMSMF::updateCurrentMediaType() { stride = static_cast(strideValue); } + if ((info.pixelFormat & kPixelFormatYUVColorBit) != 0) { + m_inputOrientation = FrameOrientation::TopToBottom; + } else if (stride < 0) { + m_inputOrientation = FrameOrientation::BottomToTop; + stride = -stride; + } else { + m_inputOrientation = FrameOrientation::TopToBottom; + } + releaseComPtr(currentType); if (info.pixelFormat == PixelFormat::Unknown) { @@ -790,7 +799,7 @@ void ProviderMSMF::readLoop() { continue; } - bool shouldFlip = !isOutputYUV && targetOrientation != FrameOrientation::TopToBottom; + bool shouldFlip = !isOutputYUV && targetOrientation != m_inputOrientation; bool shouldConvert = newFrame->pixelFormat != m_frameProp.outputPixelFormat; bool zeroCopy = !shouldConvert && !shouldFlip; @@ -905,6 +914,7 @@ void ProviderMSMF::close() { m_activeHeight = 0; m_activeFps = 0.0; m_activeStride0 = 0; + m_inputOrientation = FrameOrientation::TopToBottom; } ProviderImp* createProviderMSMF() { diff --git a/src/ccap_imp_windows_msmf.h b/src/ccap_imp_windows_msmf.h index c6e06a6b..1e9eaf9b 100644 --- a/src/ccap_imp_windows_msmf.h +++ b/src/ccap_imp_windows_msmf.h @@ -89,6 +89,7 @@ class ProviderMSMF : public ProviderImp { uint32_t m_activeHeight = 0; double m_activeFps = 0.0; int32_t m_activeStride0 = 0; + FrameOrientation m_inputOrientation = FrameOrientation::TopToBottom; }; ProviderImp* createProviderMSMF(); diff --git a/tests/test_windows_backends.cpp b/tests/test_windows_backends.cpp index 197fd4cb..3f25aa6e 100644 --- a/tests/test_windows_backends.cpp +++ b/tests/test_windows_backends.cpp @@ -74,7 +74,11 @@ fs::path findProjectRoot() { if (fs::exists(projectRoot / "CMakeLists.txt") && fs::exists(projectRoot / "tests")) { return projectRoot; } - projectRoot = projectRoot.parent_path(); + fs::path parent = projectRoot.parent_path(); + if (parent == projectRoot) { + break; + } + projectRoot = std::move(parent); } return {}; } From 5e4c20d63e84c027537320db986960a43a0a84e7 Mon Sep 17 00:00:00 2001 From: "wangyang (wysaid)" Date: Sat, 7 Mar 2026 19:22:18 +0800 Subject: [PATCH 05/11] Fix Rust build script formatting --- bindings/rust/build.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bindings/rust/build.rs b/bindings/rust/build.rs index 565851f2..e3f4d029 100644 --- a/bindings/rust/build.rs +++ b/bindings/rust/build.rs @@ -460,10 +460,10 @@ Please vendor the sources into bindings/rust/native/, or set CCAP_SOURCE_DIR to ccap_root.display() ); println!( - "cargo:rerun-if-changed={}/src/ccap_imp_windows_msmf.cpp", - ccap_root.display() - ); - println!( + "cargo:rerun-if-changed={}/src/ccap_imp_windows_msmf.cpp", + ccap_root.display() + ); + println!( "cargo:rerun-if-changed={}/src/ccap_file_reader_windows.cpp", ccap_root.display() ); From a0ab5ea31b8c2dec55756e9be38c6bfec9653b7b Mon Sep 17 00:00:00 2001 From: "wangyang (wysaid)" Date: Sun, 8 Mar 2026 01:25:17 +0800 Subject: [PATCH 06/11] Better instructions --- .github/copilot-instructions.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9afb476c..95b5a895 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -3,21 +3,23 @@ ## Code Guidelines - Add tests for new features/bug fixes and run all tests before committing - Use English for commit messages and PR descriptions -- Run `./scripts/update_version.sh ` to update version files -- Remove `dev.cmake` before standard builds/tests (use for custom CMake options) -- Run `scripts/format-all.sh` before committing +- Use `./scripts/update_version.sh ` to update version files +- Remove `dev.cmake` before standard builds/tests; use it only for custom CMake options +- Run `./scripts/format-all.sh` before committing ## Communication Guidelines -- Respond in user's language (except `docs/` which must be English) -- Maintain language consistency within conversations +- Respond in the user's language, except for `docs/`, which must be English +- Keep the same language throughout the conversation ## File Management - Store temporary files, development documents, and drafts in `dev-docs/` -- `.md` files in `docs/` must be English and require review before publishing -- Treat `dev-docs/` as `/tmp` +- Treat `dev-docs/` as disposable temporary storage +- Markdown files in `docs/` must be English and reviewed before publishing ## Tool Constraints -- **`gh` CLI:** Always prepend `$env:GH_PAGER=""` in PowerShell (e.g. `$env:GH_PAGER=""; gh pr list`), or `GH_PAGER=` in bash/zsh — never omit this or the command will hang; never modify global git/gh config +- **`gh` CLI:** Always prepend `$env:GH_PAGER=""` in PowerShell (for example, `$env:GH_PAGER=""; gh pr list`) or `GH_PAGER=` in bash/zsh; never omit it, and never modify global git or gh config. +- **Build/Test Work:** Use `.vscode/tasks.json` only as the reference for command sequence, `cwd`, config toggles, and prerequisites. Run the underlying commands directly in the terminal or with the appropriate dedicated tool; do **not** invoke VS Code tasks unless the user explicitly asks for a named task. + ## Skills From db24fb2da357822c11785973f69d16e3d2dd8d16 Mon Sep 17 00:00:00 2001 From: "wangyang (wysaid)" Date: Sun, 8 Mar 2026 02:26:38 +0800 Subject: [PATCH 07/11] Improve Windows backend selection and example UX --- README.md | 20 +- README.zh-CN.md | 17 +- bindings/rust/README.md | 20 +- cli/args_parser.cpp | 10 + cli/args_parser.h | 2 + docs/content/documentation.md | 4 +- docs/content/implementation-details.md | 8 +- docs/content/rust-bindings.md | 4 +- examples/desktop/1-minimal_example.cpp | 7 +- examples/desktop/1-minimal_example_c.c | 8 +- examples/desktop/2-capture_grab.cpp | 13 +- examples/desktop/2-capture_grab_c.c | 15 +- examples/desktop/3-capture_callback.cpp | 13 +- examples/desktop/3-capture_callback_c.c | 15 +- examples/desktop/4-example_with_glfw.cpp | 11 +- examples/desktop/4-example_with_glfw_c.c | 11 +- examples/desktop/5-play_video.cpp | 13 +- examples/desktop/5-play_video_c.c | 15 +- examples/desktop/utils/helper.cpp | 330 ++++++++++++++++++++--- examples/desktop/utils/helper.h | 22 +- include/ccap_c.h | 2 + include/ccap_core.h | 2 + src/ccap_core.cpp | 105 ++++++-- tests/CMakeLists.txt | 132 +++++++-- tests/test_cli_args_parser.cpp | 46 ++++ tests/test_example_helper.cpp | 66 +++++ tests/test_windows_backends.cpp | 101 +++++++ 27 files changed, 834 insertions(+), 178 deletions(-) create mode 100644 tests/test_cli_args_parser.cpp create mode 100644 tests/test_example_helper.cpp diff --git a/README.md b/README.md index 135598ab..a36a7fba 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,14 @@ A high-performance, lightweight cross-platform camera capture library with hardw - **High Performance**: Hardware-accelerated pixel format conversion with up to 10x speedup (AVX2, Apple Accelerate, NEON) - **Lightweight**: No third-party dependencies - uses only system frameworks -- **Cross Platform**: Windows (Media Foundation with DirectShow fallback), macOS/iOS (AVFoundation), Linux (V4L2) +- **Cross Platform**: Windows (DirectShow by default with optional Media Foundation), macOS/iOS (AVFoundation), Linux (V4L2) +- **Windows Dual Backends**: DirectShow is the default on Windows for compatibility with OBS Virtual Camera and other virtual devices, while Media Foundation stays available through explicit opt-in - **Multiple Formats**: RGB, BGR, YUV (NV12/I420) with automatic conversion - **Dual Language APIs**: ✨ **Complete Pure C Interface** - Both modern C++ API and traditional C99 interface for various project integration and language bindings - **Video File Playback**: 🎬 Play video files (MP4, AVI, MOV, etc.) using the same API as camera capture - supports Windows and macOS - **CLI Tool**: Ready-to-use command-line tool for quick camera operations and video processing - list devices, capture images, real-time preview, video playback ([Documentation](./docs/content/cli.md)) - **Production Ready**: Comprehensive test suite with 95%+ accuracy validation -- **Virtual Camera Support**: Compatible with OBS Virtual Camera and similar tools +- **Virtual Camera Support**: Compatible with OBS Virtual Camera and similar tools through the default DirectShow path on Windows ## Quick Start @@ -213,13 +214,24 @@ int main() { ### Windows Backend Selection -On Windows, camera capture uses Media Foundation by default and falls back to DirectShow when needed. You can override that choice when debugging device issues or validating backend-specific behavior: +On Windows, camera capture now uses DirectShow by default. This keeps OBS Virtual Camera and other virtual cameras working reliably after upgrades, while Media Foundation remains available when you explicitly request it. In `auto` mode, camera enumeration merges results from both Windows backends and `Provider::open()` routes the selected device to a compatible backend automatically: DirectShow-only devices stay on DirectShow, Media Foundation-only devices go straight to Media Foundation, and devices visible in both backends prefer DirectShow with Media Foundation as the secondary fallback. + +For most Windows applications, staying in `auto` mode is recommended. ccap normalizes the public capture API, frame orientation handling, and output pixel-format conversion across both backends so callers usually do not need backend-specific code. - Pass `extraInfo` as `"auto"`, `"msmf"`, `"dshow"`, or `"backend="` in the C++/C constructors that accept it. - Set the environment variable `CCAP_WINDOWS_BACKEND=auto|msmf|dshow` to affect the whole process, including the CLI and Rust bindings. +```powershell +# PowerShell: opt into Media Foundation for the current process +$env:CCAP_WINDOWS_BACKEND = "msmf" +.\ccap --list-devices +``` + ```cpp +// Force Media Foundation explicitly on Windows ccap::Provider msmfProvider("", "msmf"); + +// Force DirectShow explicitly on Windows ccap::Provider dshowProvider("", "dshow"); ``` @@ -288,7 +300,7 @@ For complete CLI documentation, see [CLI Tool Guide](./docs/content/cli.md). | Platform | Compiler | System Requirements | | -------- | -------- | ------------------- | -| **Windows** | MSVC 2019+ (including 2026) / MinGW-w64 | Media Foundation (default) + DirectShow fallback | +| **Windows** | MSVC 2019+ (including 2026) / MinGW-w64 | DirectShow (default) + Media Foundation opt-in | | **macOS** | Xcode 11+ | macOS 10.13+ | | **iOS** | Xcode 11+ | iOS 13.0+ | | **Linux** | GCC 7+ / Clang 6+ | V4L2 (Linux 2.6+) | diff --git a/README.zh-CN.md b/README.zh-CN.md index 625c8132..d9e8ae18 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -33,13 +33,14 @@ - **高性能**:硬件加速的像素格式转换,提升高达 10 倍性能(AVX2、Apple Accelerate、NEON) - **轻量级**:无第三方库依赖,仅使用系统框架 -- **跨平台**:Windows(Media Foundation,自动回退到 DirectShow)、macOS/iOS(AVFoundation)、Linux(V4L2) +- **跨平台**:Windows(默认 DirectShow,可选 Media Foundation)、macOS/iOS(AVFoundation)、Linux(V4L2) +- **Windows 双后端**:Windows 默认使用 DirectShow,以更好兼容 OBS Virtual Camera 等虚拟摄像头;如果需要,也可以显式切换到 Media Foundation - **多种格式**:RGB、BGR、YUV(NV12/I420)及自动转换 - **双语言接口**:✨ **新增完整纯 C 接口**,同时提供现代化 C++ API 和传统 C99 接口,支持各种项目集成和语言绑定 - **视频文件播放**:🎬 使用与相机相同的 API 播放视频文件(MP4、AVI、MOV 等)- 支持 Windows 和 macOS - **命令行工具**:开箱即用的命令行工具,快速实现相机操作和视频处理 - 列出设备、捕获图像、实时预览、视频播放([文档](./docs/content/cli.zh.md)) - **生产就绪**:完整测试套件,95%+ 精度验证 -- **虚拟相机支持**:兼容 OBS Virtual Camera 等工具 +- **虚拟相机支持**:在 Windows 上通过默认 DirectShow 路径兼容 OBS Virtual Camera 等工具 ## 快速开始 @@ -176,11 +177,19 @@ int main() { ### Windows 后端选择 -Windows 上默认优先使用 Media Foundation,并在需要时自动回退到 DirectShow。如果你要排查设备兼容性问题,或验证某个后端的行为,可以显式指定后端: +Windows 上现在默认使用 DirectShow。这样做的主要原因是 DirectShow 对 OBS Virtual Camera 等虚拟摄像头的兼容性更好,能够避免用户升级后因为默认后端变化而突然失去虚拟摄像头支持。在 `auto` 模式下,设备枚举会合并 DirectShow 和 Media Foundation 的结果,而 `Provider::open()` 会根据选中的设备自动路由到兼容的后端:仅 DirectShow 可见的设备会直接走 DirectShow,仅 MSMF 可见的设备会直接走 MSMF,同时被两个后端看到的设备优先走 DirectShow,必要时再回退到 MSMF。 + +对大多数 Windows 应用来说,建议直接使用 `auto` 模式。ccap 会在两个后端之上统一公开的采集 API、帧朝向处理和输出像素格式转换,所以调用方通常不需要编写后端分支逻辑。 - 在支持 `extraInfo` 的 C++ / C 构造接口中传入 `"auto"`、`"msmf"`、`"dshow"` 或 `"backend="`。 - 设置环境变量 `CCAP_WINDOWS_BACKEND=auto|msmf|dshow`,对整个进程生效,包括 CLI 和 Rust 绑定。 +```powershell +# PowerShell:为当前进程显式启用 Media Foundation +$env:CCAP_WINDOWS_BACKEND = "msmf" +.\ccap --list-devices +``` + ```cpp // 下面两段是互斥示例;同一时刻不要对同一设备同时创建两个 Provider。 @@ -258,7 +267,7 @@ cmake --build . | 平台 | 编译器 | 系统要求 | |------|--------|----------| -| **Windows** | MSVC 2019+(包括 2026)/ MinGW-w64 | Media Foundation(默认)+ DirectShow 回退 | +| **Windows** | MSVC 2019+(包括 2026)/ MinGW-w64 | DirectShow(默认)+ Media Foundation 可选启用 | | **macOS** | Xcode 11+ | macOS 10.13+ | | **iOS** | Xcode 11+ | iOS 13.0+ | | **Linux** | GCC 7+ / Clang 6+ | V4L2 (Linux 2.6+) - 相机捕获支持,视频播放暂不支持 | diff --git a/bindings/rust/README.md b/bindings/rust/README.md index 604f7c7a..f4c5fd42 100644 --- a/bindings/rust/README.md +++ b/bindings/rust/README.md @@ -4,14 +4,14 @@ [![Documentation](https://docs.rs/ccap-rs/badge.svg)](https://docs.rs/ccap-rs) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -Safe Rust bindings for [CameraCapture (ccap)](https://github.com/wysaid/CameraCapture) — a high-performance, lightweight, cross-platform **webcam/camera capture** library with **hardware-accelerated pixel format conversion** (Windows Media Foundation with DirectShow fallback, macOS/iOS AVFoundation, Linux V4L2). +Safe Rust bindings for [CameraCapture (ccap)](https://github.com/wysaid/CameraCapture) — a high-performance, lightweight, cross-platform **webcam/camera capture** library with **hardware-accelerated pixel format conversion** (Windows DirectShow by default with optional Media Foundation, macOS/iOS AVFoundation, Linux V4L2). > Note: The published *package* name on crates.io is `ccap-rs`, but the *crate name in code* is `ccap`. ## Features - **High Performance**: Hardware-accelerated pixel format conversion with up to 10x speedup (AVX2, Apple Accelerate, NEON) -- **Cross Platform**: Windows (Media Foundation with DirectShow fallback), macOS/iOS (AVFoundation), Linux (V4L2) +- **Cross Platform**: Windows (DirectShow by default with optional Media Foundation), macOS/iOS (AVFoundation), Linux (V4L2) - **Multiple Formats**: RGB, BGR, YUV (NV12/I420) with automatic conversion - **Zero Dependencies**: Uses only system frameworks - **Memory Safe**: Safe Rust API with automatic resource management @@ -149,10 +149,10 @@ An ASan-instrumented `libccap.a` requires the ASan runtime at link/run time. ## Platform notes -- Camera capture: Windows (Media Foundation with DirectShow fallback), macOS/iOS (AVFoundation), Linux (V4L2) +- Camera capture: Windows (DirectShow by default with optional Media Foundation), macOS/iOS (AVFoundation), Linux (V4L2) - Video file playback support depends on the underlying C/C++ library backend (currently Windows/macOS only). -On Windows, you can force backend selection by setting `CCAP_WINDOWS_BACKEND=auto|msmf|dshow`, or by using `Provider::with_device_name_and_extra_info`, `Provider::with_device_and_extra_info`, `Provider::open_device_with_extra_info`, and `Provider::open_with_index_and_extra_info`. +On Windows, camera capture defaults to DirectShow because virtual cameras such as OBS Virtual Camera are exposed there more reliably. To opt into Media Foundation, set `CCAP_WINDOWS_BACKEND=msmf`, or use `Provider::with_device_name_and_extra_info`, `Provider::with_device_and_extra_info`, `Provider::open_device_with_extra_info`, and `Provider::open_with_index_and_extra_info` with `"msmf"`. ## API Documentation @@ -188,12 +188,12 @@ match provider.grab_frame(3000) { // 3 second timeout ## Platform Support -| Platform | Backend | Status | -| -------- | ------------ | ------------ | -| Windows | Media Foundation + DirectShow fallback | ✅ Supported | -| macOS | AVFoundation | ✅ Supported | -| iOS | AVFoundation | ✅ Supported | -| Linux | V4L2 | ✅ Supported | +| Platform | Backend | Status | +| --- | --- | --- | +| Windows | DirectShow default, Media Foundation opt-in | ✅ Supported | +| macOS | AVFoundation | ✅ Supported | +| iOS | AVFoundation | ✅ Supported | +| Linux | V4L2 | ✅ Supported | ## System Requirements diff --git a/cli/args_parser.cpp b/cli/args_parser.cpp index f8185d6a..d99647f2 100644 --- a/cli/args_parser.cpp +++ b/cli/args_parser.cpp @@ -19,6 +19,8 @@ namespace ccap_cli { // Default values constexpr int DEFAULT_WIDTH = 1280; constexpr int DEFAULT_HEIGHT = 720; +constexpr int DEFAULT_PREVIEW_WIDTH = 1920; +constexpr int DEFAULT_PREVIEW_HEIGHT = 1080; constexpr double DEFAULT_FPS = 30.0; constexpr int DEFAULT_TIMEOUT_MS = 5000; @@ -135,6 +137,7 @@ void printUsage(const char* programName) { std::cout << "Preview options:\n" << " -p, --preview enable window preview\n" << " --preview-only same as --preview (kept for compatibility)\n" + << " Camera preview requests 1920x1080 by default when -w/-H are omitted\n" << "\n"; #endif @@ -318,10 +321,12 @@ CLIOptions parseArgs(int argc, char* argv[]) { } else if (arg == "-w" || arg == "--width") { if (i + 1 < argc) { opts.width = std::atoi(argv[++i]); + opts.widthSpecified = true; } } else if (arg == "-H" || arg == "--height") { if (i + 1 < argc) { opts.height = std::atoi(argv[++i]); + opts.heightSpecified = true; } } else if (arg == "-f" || arg == "--fps") { if (i + 1 < argc) { @@ -461,6 +466,11 @@ CLIOptions parseArgs(int argc, char* argv[]) { opts.saveFrames = true; } + if (opts.enablePreview && opts.videoFilePath.empty() && !opts.widthSpecified && !opts.heightSpecified) { + opts.width = DEFAULT_PREVIEW_WIDTH; + opts.height = DEFAULT_PREVIEW_HEIGHT; + } + return opts; } diff --git a/cli/args_parser.h b/cli/args_parser.h index e8eed59c..a6de740b 100644 --- a/cli/args_parser.h +++ b/cli/args_parser.h @@ -45,6 +45,8 @@ struct CLIOptions { // Capture parameters int width = 1280; int height = 720; + bool widthSpecified = false; + bool heightSpecified = false; double fps = 30.0; int captureCount = 1; bool captureCountSpecified = false; diff --git a/docs/content/documentation.md b/docs/content/documentation.md index c44e1d1e..d78c818b 100644 --- a/docs/content/documentation.md +++ b/docs/content/documentation.md @@ -220,7 +220,9 @@ if (provider.open("/path/to/video.mp4", true)) { ### Windows -Uses Media Foundation for camera access on modern Windows systems, with automatic DirectShow fallback for legacy or incompatible devices. Requires MSVC 2019 or later. +Uses DirectShow for camera access by default on Windows to preserve compatibility with OBS Virtual Camera and other virtual cameras. Media Foundation remains available when you explicitly request `msmf`, or as a secondary path when `auto` needs to recover from a DirectShow open failure. Requires MSVC 2019 or later. + +For most Windows applications, `auto` mode is the recommended choice. ccap merges device enumeration across both backends and keeps the public capture API, frame orientation handling, and output pixel-format conversion aligned so callers usually do not need backend-specific branching. To force a specific camera backend on Windows, either pass `extraInfo` as `auto`, `msmf`, `dshow`, or `backend=` to the constructors that accept it, or set `CCAP_WINDOWS_BACKEND=auto|msmf|dshow` for the current process. diff --git a/docs/content/implementation-details.md b/docs/content/implementation-details.md index a42ed885..246b9500 100644 --- a/docs/content/implementation-details.md +++ b/docs/content/implementation-details.md @@ -216,19 +216,21 @@ Or use the script: ### Windows **Media Foundation Backend:** -- Preferred on modern Windows systems +- Opt-in backend on Windows when callers explicitly request `msmf` - Uses Source Reader for frame delivery and format negotiation -- When backend selection is `auto`, `Provider::open()` falls back to DirectShow if Media Foundation is unavailable or device open fails +- When backend selection is `auto`, `Provider::open()` first checks which backend enumerated the requested device and routes to a compatible backend automatically +- If a device is visible in both Windows backends, `auto` prefers DirectShow for compatibility and keeps Media Foundation as the secondary fallback - When callers explicitly request `msmf` via `extraInfo` or `CCAP_WINDOWS_BACKEND`, `Provider::open()` returns an error instead of falling back **DirectShow Backend:** +- Default Windows camera backend because virtual cameras such as OBS Virtual Camera are exposed there more consistently - Mature, stable API - Good driver compatibility - Known issue: Some VR headsets crash during enumeration - **Solution:** Use `CCAP_WIN_NO_DEVICE_VERIFY=ON` **Virtual Cameras:** -- OBS Virtual Camera: ✅ Supported +- OBS Virtual Camera: ✅ Supported through DirectShow; typically not enumerated by the MSMF backend - ManyCam: ✅ Supported - NDI Virtual Input: ✅ Supported diff --git a/docs/content/rust-bindings.md b/docs/content/rust-bindings.md index ed651c2e..fc08ead3 100644 --- a/docs/content/rust-bindings.md +++ b/docs/content/rust-bindings.md @@ -76,11 +76,11 @@ If needed, set: Camera capture backends: -- Windows: Media Foundation with DirectShow fallback +- Windows: DirectShow by default with optional Media Foundation - macOS/iOS: AVFoundation - Linux: V4L2 -On Windows, you can force backend selection by setting `CCAP_WINDOWS_BACKEND=auto|msmf|dshow`, or by using the Rust APIs `Provider::with_device_name_and_extra_info`, `Provider::with_device_and_extra_info`, `Provider::open_device_with_extra_info`, and `Provider::open_with_index_and_extra_info`. +On Windows, camera capture defaults to DirectShow because virtual cameras such as OBS Virtual Camera are exposed there more reliably. You can opt into Media Foundation by setting `CCAP_WINDOWS_BACKEND=msmf`, or by using the Rust APIs `Provider::with_device_name_and_extra_info`, `Provider::with_device_and_extra_info`, `Provider::open_device_with_extra_info`, and `Provider::open_with_index_and_extra_info` with `msmf`. Video file playback support depends on the underlying C/C++ backend (currently Windows/macOS only). diff --git a/examples/desktop/1-minimal_example.cpp b/examples/desktop/1-minimal_example.cpp index e9637794..0682753e 100644 --- a/examples/desktop/1-minimal_example.cpp +++ b/examples/desktop/1-minimal_example.cpp @@ -18,9 +18,14 @@ int main() { << ", Description: " << description << std::endl; }); +#if defined(_WIN32) || defined(_WIN64) + // On Windows, you can force a backend like this: + // ccap::Provider cameraProvider(-1, "msmf"); + // ccap::Provider cameraProvider(-1, "dshow"); +#endif ccap::Provider cameraProvider; - cameraProvider.open(selectCamera(cameraProvider), true); + cameraProvider.open(selectCamera(cameraProvider, nullptr), true); cameraProvider.start(); diff --git a/examples/desktop/1-minimal_example_c.c b/examples/desktop/1-minimal_example_c.c index a0382d35..88a09c14 100644 --- a/examples/desktop/1-minimal_example_c.c +++ b/examples/desktop/1-minimal_example_c.c @@ -10,7 +10,6 @@ #include "utils/helper.h" #include -#include // Error callback function void error_callback(CcapErrorCode errorCode, const char* errorDescription, void* userData) { @@ -25,6 +24,11 @@ int main() { // Set error callback to receive error notifications ccap_set_error_callback(error_callback, NULL); +#if defined(_WIN32) || defined(_WIN64) + // On Windows, you can force a backend while opening the default camera: + // CcapProvider* provider = ccap_provider_create_with_index(-1, "msmf"); + // CcapProvider* provider = ccap_provider_create_with_index(-1, "dshow"); +#endif // Create provider CcapProvider* provider = ccap_provider_create(); if (!provider) { @@ -33,7 +37,7 @@ int main() { } // Select and open camera - int deviceIndex = selectCamera(provider); + int deviceIndex = selectCamera(provider, NULL); if (!ccap_provider_open_by_index(provider, deviceIndex, true)) { printf("Failed to open camera\n"); ccap_provider_destroy(provider); diff --git a/examples/desktop/2-capture_grab.cpp b/examples/desktop/2-capture_grab.cpp index a300838f..76e26506 100644 --- a/examples/desktop/2-capture_grab.cpp +++ b/examples/desktop/2-capture_grab.cpp @@ -16,6 +16,10 @@ #include int main(int argc, char** argv) { + ExampleCommandLine commandLine{}; + initExampleCommandLine(&commandLine, argc, argv); + applyExampleCameraBackend(&commandLine); + /// Enable verbose log to see debug information ccap::setLogLevel(ccap::LogLevel::Verbose); @@ -25,7 +29,7 @@ int main(int argc, char** argv) { << ", Description: " << description << std::endl; }); - std::string cwd = argv[0]; + std::string cwd = commandLine.argv[0]; if (auto lastSlashPos = cwd.find_last_of("/\\"); lastSlashPos != std::string::npos && cwd[0] != '.') { cwd = cwd.substr(0, lastSlashPos); @@ -55,12 +59,7 @@ int main(int argc, char** argv) { #endif cameraProvider.set(ccap::PropertyName::FrameRate, requestedFps); - int deviceIndex; - if (argc > 1 && std::isdigit(argv[1][0])) { - deviceIndex = std::stoi(argv[1]); - } else { - deviceIndex = selectCamera(cameraProvider); - } + int deviceIndex = selectCamera(cameraProvider, &commandLine); cameraProvider.open(deviceIndex, true); diff --git a/examples/desktop/2-capture_grab_c.c b/examples/desktop/2-capture_grab_c.c index b05cd651..d9478e54 100644 --- a/examples/desktop/2-capture_grab_c.c +++ b/examples/desktop/2-capture_grab_c.c @@ -24,6 +24,10 @@ int main(int argc, char** argv) { printf("ccap C Interface Capture Grab Example\n"); printf("Version: %s\n\n", ccap_get_version()); + ExampleCommandLine commandLine = { 0 }; + initExampleCommandLine(&commandLine, argc, argv); + applyExampleCameraBackend(&commandLine); + // Enable verbose log to see debug information ccap_set_log_level(CCAP_LOG_LEVEL_VERBOSE); @@ -32,8 +36,8 @@ int main(int argc, char** argv) { // Get current working directory and create capture directory char cwd[1024]; - if (argc > 0 && argv[0][0] != '.') { - strncpy(cwd, argv[0], sizeof(cwd) - 1); + if (commandLine.argc > 0 && commandLine.argv[0][0] != '.') { + strncpy(cwd, commandLine.argv[0], sizeof(cwd) - 1); cwd[sizeof(cwd) - 1] = '\0'; // Find last slash @@ -73,12 +77,7 @@ int main(int argc, char** argv) { ccap_provider_set_property(provider, CCAP_PROPERTY_FRAME_RATE, requestedFps); // Select and open camera - int deviceIndex; - if (argc > 1 && isdigit(argv[1][0])) { - deviceIndex = atoi(argv[1]); - } else { - deviceIndex = selectCamera(provider); - } + int deviceIndex = selectCamera(provider, &commandLine); if (!ccap_provider_open_by_index(provider, deviceIndex, true)) { printf("Failed to open camera\n"); diff --git a/examples/desktop/3-capture_callback.cpp b/examples/desktop/3-capture_callback.cpp index 07dd4d40..9882cd69 100644 --- a/examples/desktop/3-capture_callback.cpp +++ b/examples/desktop/3-capture_callback.cpp @@ -17,10 +17,14 @@ #include int main(int argc, char** argv) { + ExampleCommandLine commandLine{}; + initExampleCommandLine(&commandLine, argc, argv); + applyExampleCameraBackend(&commandLine); + /// Enable verbose log to see debug information ccap::setLogLevel(ccap::LogLevel::Verbose); - std::string cwd = argv[0]; + std::string cwd = commandLine.argv[0]; if (auto lastSlashPos = cwd.find_last_of("/\\"); lastSlashPos != std::string::npos && cwd[0] != '.') { cwd = cwd.substr(0, lastSlashPos); @@ -62,12 +66,7 @@ int main(int argc, char** argv) { #endif cameraProvider.set(ccap::PropertyName::FrameRate, requestedFps); - int deviceIndex; - if (argc > 1 && std::isdigit(argv[1][0])) { - deviceIndex = std::stoi(argv[1]); - } else { - deviceIndex = selectCamera(cameraProvider); - } + int deviceIndex = selectCamera(cameraProvider, &commandLine); cameraProvider.open(deviceIndex, true); if (!cameraProvider.isStarted()) { diff --git a/examples/desktop/3-capture_callback_c.c b/examples/desktop/3-capture_callback_c.c index 5308e9f1..e05a0fdc 100644 --- a/examples/desktop/3-capture_callback_c.c +++ b/examples/desktop/3-capture_callback_c.c @@ -76,13 +76,17 @@ int main(int argc, char** argv) { printf("ccap C Interface Capture Callback Example\n"); printf("Version: %s\n\n", ccap_get_version()); + ExampleCommandLine commandLine = { 0 }; + initExampleCommandLine(&commandLine, argc, argv); + applyExampleCameraBackend(&commandLine); + // Enable verbose log to see debug information ccap_set_log_level(CCAP_LOG_LEVEL_VERBOSE); // Get current working directory and create capture directory char cwd[1024]; - if (argc > 0 && argv[0][0] != '.') { - strncpy(cwd, argv[0], sizeof(cwd) - 1); + if (commandLine.argc > 0 && commandLine.argv[0][0] != '.') { + strncpy(cwd, commandLine.argv[0], sizeof(cwd) - 1); cwd[sizeof(cwd) - 1] = '\0'; // Find last slash @@ -134,12 +138,7 @@ int main(int argc, char** argv) { ccap_provider_set_property(provider, CCAP_PROPERTY_FRAME_RATE, requestedFps); // Select and open camera - int deviceIndex; - if (argc > 1 && isdigit(argv[1][0])) { - deviceIndex = atoi(argv[1]); - } else { - deviceIndex = selectCamera(provider); - } + int deviceIndex = selectCamera(provider, &commandLine); if (!ccap_provider_open_by_index(provider, deviceIndex, true)) { printf("Failed to open camera\n"); diff --git a/examples/desktop/4-example_with_glfw.cpp b/examples/desktop/4-example_with_glfw.cpp index db1486de..aa4ece1f 100644 --- a/examples/desktop/4-example_with_glfw.cpp +++ b/examples/desktop/4-example_with_glfw.cpp @@ -70,6 +70,10 @@ void main() { // selectCamera moved to utils/helper.{h,cpp} int main(int argc, char** argv) { + ExampleCommandLine commandLine{}; + initExampleCommandLine(&commandLine, argc, argv); + applyExampleCameraBackend(&commandLine); + /// Enable verbose log to see debug information ccap::setLogLevel(ccap::LogLevel::Verbose); @@ -95,12 +99,7 @@ int main(int argc, char** argv) { cameraProvider.set(ccap::PropertyName::FrameRate, requestedFps); cameraProvider.set(ccap::PropertyName::FrameOrientation, ccap::FrameOrientation::BottomToTop); - int deviceIndex; - if (argc > 1 && std::isdigit(argv[1][0])) { - deviceIndex = std::stoi(argv[1]); - } else { - deviceIndex = selectCamera(cameraProvider); - } + int deviceIndex = selectCamera(cameraProvider, &commandLine); cameraProvider.open(deviceIndex, true); if (!cameraProvider.isStarted()) { diff --git a/examples/desktop/4-example_with_glfw_c.c b/examples/desktop/4-example_with_glfw_c.c index 151cfc9a..2799a5b4 100644 --- a/examples/desktop/4-example_with_glfw_c.c +++ b/examples/desktop/4-example_with_glfw_c.c @@ -83,6 +83,10 @@ int main(int argc, char** argv) { printf("ccap C Interface GLFW Example\n"); printf("Version: %s\n\n", ccap_get_version()); + ExampleCommandLine commandLine = { 0 }; + initExampleCommandLine(&commandLine, argc, argv); + applyExampleCameraBackend(&commandLine); + // Enable verbose log to see debug information ccap_set_log_level(CCAP_LOG_LEVEL_VERBOSE); @@ -116,12 +120,7 @@ int main(int argc, char** argv) { ccap_provider_set_property(provider, CCAP_PROPERTY_FRAME_ORIENTATION, CCAP_FRAME_ORIENTATION_BOTTOM_TO_TOP); // Select and open camera - int deviceIndex; - if (argc > 1 && isdigit(argv[1][0])) { - deviceIndex = atoi(argv[1]); - } else { - deviceIndex = selectCamera(provider); - } + int deviceIndex = selectCamera(provider, &commandLine); if (!ccap_provider_open_by_index(provider, deviceIndex, true)) { printf("Failed to open camera\n"); diff --git a/examples/desktop/5-play_video.cpp b/examples/desktop/5-play_video.cpp index e2f548d0..dc284674 100644 --- a/examples/desktop/5-play_video.cpp +++ b/examples/desktop/5-play_video.cpp @@ -18,6 +18,9 @@ #include int main(int argc, char** argv) { + ExampleCommandLine commandLine{}; + initExampleCommandLine(&commandLine, argc, argv); + #ifdef __linux__ std::cerr << "\n[WARNING] Video playback is currently not supported on Linux." << std::endl; std::cerr << "This feature may be implemented in a future version." << std::endl; @@ -37,20 +40,20 @@ int main(int argc, char** argv) { std::string videoPath; - if (argc < 2) { + if (commandLine.argc < 2) { // Check if test.mp4 exists in current directory std::string defaultVideo = "test.mp4"; if (std::filesystem::exists(defaultVideo)) { std::cout << "No video path provided, using default: " << defaultVideo << std::endl; videoPath = defaultVideo; } else { - std::cerr << "Usage: " << argv[0] << " " << std::endl; - std::cerr << "Example: " << argv[0] << " /path/to/video.mp4" << std::endl; + std::cerr << "Usage: " << commandLine.argv[0] << " " << std::endl; + std::cerr << "Example: " << commandLine.argv[0] << " /path/to/video.mp4" << std::endl; std::cerr << "\nNote: You can also place a test.mp4 file in the same directory as this executable." << std::endl; return -1; } } else { - videoPath = argv[1]; + videoPath = commandLine.argv[1]; } // Check if file exists @@ -59,7 +62,7 @@ int main(int argc, char** argv) { return -1; } - std::string cwd = argv[0]; + std::string cwd = commandLine.argv[0]; if (auto lastSlashPos = cwd.find_last_of("/\\"); lastSlashPos != std::string::npos && cwd[0] != '.') { cwd = cwd.substr(0, lastSlashPos); } else { diff --git a/examples/desktop/5-play_video_c.c b/examples/desktop/5-play_video_c.c index a0cf68e6..2588747a 100644 --- a/examples/desktop/5-play_video_c.c +++ b/examples/desktop/5-play_video_c.c @@ -28,6 +28,9 @@ int main(int argc, char** argv) { printf("ccap C Interface Video Playback Example\n"); printf("Version: %s\n\n", ccap_get_version()); + ExampleCommandLine commandLine = { 0 }; + initExampleCommandLine(&commandLine, argc, argv); + #ifdef __linux__ fprintf(stderr, "\n[WARNING] Video playback is currently not supported on Linux.\n"); fprintf(stderr, "This feature may be implemented in a future version.\n"); @@ -37,7 +40,7 @@ int main(int argc, char** argv) { const char* videoPath = NULL; - if (argc < 2) { + if (commandLine.argc < 2) { // Check if test.mp4 exists in current directory const char* defaultVideo = "test.mp4"; FILE* testFile = fopen(defaultVideo, "rb"); @@ -46,13 +49,13 @@ int main(int argc, char** argv) { printf("No video path provided, using default: %s\n", defaultVideo); videoPath = defaultVideo; } else { - fprintf(stderr, "Usage: %s \n", argv[0]); - fprintf(stderr, "Example: %s /path/to/video.mp4\n", argv[0]); + fprintf(stderr, "Usage: %s \n", commandLine.argv[0]); + fprintf(stderr, "Example: %s /path/to/video.mp4\n", commandLine.argv[0]); fprintf(stderr, "\nNote: You can also place a test.mp4 file in the same directory as this executable.\n"); return -1; } } else { - videoPath = argv[1]; + videoPath = commandLine.argv[1]; } // Enable verbose log to see debug information @@ -63,8 +66,8 @@ int main(int argc, char** argv) { // Get current working directory and create capture directory char cwd[1024]; - if (argc > 0 && argv[0][0] != '.') { - strncpy(cwd, argv[0], sizeof(cwd) - 1); + if (commandLine.argc > 0 && commandLine.argv[0][0] != '.') { + strncpy(cwd, commandLine.argv[0], sizeof(cwd) - 1); cwd[sizeof(cwd) - 1] = '\0'; // Find last slash diff --git a/examples/desktop/utils/helper.cpp b/examples/desktop/utils/helper.cpp index 8a424391..a153a807 100644 --- a/examples/desktop/utils/helper.cpp +++ b/examples/desktop/utils/helper.cpp @@ -3,6 +3,7 @@ #include "ccap_c.h" #include +#include #include #include #include @@ -10,8 +11,284 @@ #include #include +#if defined(_WIN32) || defined(_WIN64) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +namespace { + +constexpr const char* kWindowsBackendEnvVar = "CCAP_WINDOWS_BACKEND"; + +ExampleCameraBackend parseCameraBackendArg(const char* arg) { + if (arg == nullptr) { + return EXAMPLE_CAMERA_BACKEND_UNSPECIFIED; + } + + if (std::strcmp(arg, "--auto") == 0) { + return EXAMPLE_CAMERA_BACKEND_AUTO; + } + if (std::strcmp(arg, "--msmf") == 0) { + return EXAMPLE_CAMERA_BACKEND_MSMF; + } + if (std::strcmp(arg, "--dshow") == 0) { + return EXAMPLE_CAMERA_BACKEND_DSHOW; + } + return EXAMPLE_CAMERA_BACKEND_UNSPECIFIED; +} + +bool parseNonNegativeInt(const char* text, int* value) { + if (text == nullptr || value == nullptr || *text == '\0') { + return false; + } + + char* end = nullptr; + long parsed = std::strtol(text, &end, 10); + if (end == text || parsed < 0 || parsed > INT_MAX) { + return false; + } + + while (*end != '\0') { + if (!std::isspace(static_cast(*end))) { + return false; + } + ++end; + } + + *value = static_cast(parsed); + return true; +} + +int promptSelection(const char* prompt, int defaultValue, int maxValue) { + char buffer[64] = {}; + std::printf("%s", prompt); + if (std::fgets(buffer, sizeof(buffer), stdin) == nullptr) { + return defaultValue; + } + + char* end = nullptr; + long parsed = std::strtol(buffer, &end, 10); + if (end == buffer) { + return defaultValue; + } + + while (*end != '\0') { + if (!std::isspace(static_cast(*end))) { + return defaultValue; + } + ++end; + } + + if (parsed < 0 || parsed > maxValue) { + return defaultValue; + } + + return static_cast(parsed); +} + +std::string getEnvironmentValue(const char* name) { +#if defined(_WIN32) || defined(_WIN64) + char* rawValue = nullptr; + size_t rawLength = 0; + if (_dupenv_s(&rawValue, &rawLength, name) != 0 || rawValue == nullptr) { + return {}; + } + + std::string value(rawValue); + std::free(rawValue); + return value; +#else + const char* rawValue = std::getenv(name); + return rawValue != nullptr ? std::string(rawValue) : std::string(); +#endif +} + +void setEnvironmentValue(const char* name, const char* value) { +#if defined(_WIN32) || defined(_WIN64) + SetEnvironmentVariableA(name, value); +#else + if (value != nullptr && *value != '\0') { + setenv(name, value, 1); + } else { + unsetenv(name); + } +#endif +} + +class ScopedEnvironmentValue { +public: + ScopedEnvironmentValue(const char* name, const char* value) : + m_name(name), + m_oldValue(getEnvironmentValue(name)), + m_hadValue(!m_oldValue.empty()) { + setEnvironmentValue(m_name.c_str(), value); + } + + ~ScopedEnvironmentValue() { + if (m_hadValue) { + setEnvironmentValue(m_name.c_str(), m_oldValue.c_str()); + } else { + setEnvironmentValue(m_name.c_str(), nullptr); + } + } + +private: + std::string m_name; + std::string m_oldValue; + bool m_hadValue = false; +}; + +int promptForDeviceIndex(const CcapDeviceNamesList& deviceList) { + std::printf("Multiple devices found, please select one:\n"); + for (size_t index = 0; index < deviceList.deviceCount; ++index) { + std::printf(" %zu: %s\n", index, deviceList.deviceNames[index]); + } + + int selectedIndex = promptSelection("Enter the index of the device you want to use [0]: ", 0, + static_cast(deviceList.deviceCount) - 1); + std::printf("Using device: %s\n", deviceList.deviceNames[selectedIndex]); + return selectedIndex; +} + +int promptForDeviceIndex(const std::vector& names) { + std::printf("Multiple devices found, please select one:\n"); + for (size_t index = 0; index < names.size(); ++index) { + std::printf(" %zu: %s\n", index, names[index].c_str()); + } + + int selectedIndex = promptSelection("Enter the index of the device you want to use [0]: ", 0, + static_cast(names.size()) - 1); + std::printf("Using device: %s\n", names[selectedIndex].c_str()); + return selectedIndex; +} + +#if defined(_WIN32) || defined(_WIN64) +std::vector listDevicesForBackend(const char* backend) { + ScopedEnvironmentValue scopedEnv(kWindowsBackendEnvVar, backend); + ccap::Provider provider; + return provider.findDeviceNames(); +} + +bool shouldPromptForWindowsBackendSelection(const ExampleCommandLine* commandLine) { + if (commandLine != nullptr && commandLine->cameraBackend != EXAMPLE_CAMERA_BACKEND_UNSPECIFIED) { + return false; + } + + if (!getEnvironmentValue(kWindowsBackendEnvVar).empty()) { + return false; + } + + return !listDevicesForBackend("msmf").empty() && !listDevicesForBackend("dshow").empty(); +} + +ExampleCameraBackend promptForWindowsBackendSelection() { + std::printf("Windows camera backends detected. Select backend for this run:\n"); + std::printf(" 0: auto (prefer DirectShow, fallback to MSMF)\n"); + std::printf(" 1: msmf\n"); + std::printf(" 2: dshow\n"); + + switch (promptSelection("Enter backend index [0]: ", 0, 2)) { + case 1: + return EXAMPLE_CAMERA_BACKEND_MSMF; + case 2: + return EXAMPLE_CAMERA_BACKEND_DSHOW; + default: + return EXAMPLE_CAMERA_BACKEND_AUTO; + } +} + +void applyWindowsBackendSelection(ExampleCameraBackend backend) { + switch (backend) { + case EXAMPLE_CAMERA_BACKEND_AUTO: + setEnvironmentValue(kWindowsBackendEnvVar, nullptr); + std::printf("Using Windows camera backend: auto (DirectShow preferred, MSMF fallback)\n"); + break; + case EXAMPLE_CAMERA_BACKEND_MSMF: + setEnvironmentValue(kWindowsBackendEnvVar, "msmf"); + std::printf("Using Windows camera backend: msmf\n"); + break; + case EXAMPLE_CAMERA_BACKEND_DSHOW: + setEnvironmentValue(kWindowsBackendEnvVar, "dshow"); + std::printf("Using Windows camera backend: dshow\n"); + break; + case EXAMPLE_CAMERA_BACKEND_UNSPECIFIED: + break; + } +} +#endif + +} // namespace + extern "C" { +void initExampleCommandLine(ExampleCommandLine* commandLine, int argc, char** argv) { + if (commandLine == nullptr) { + return; + } + + commandLine->argc = argc; + commandLine->argv = argv; + commandLine->cameraBackend = EXAMPLE_CAMERA_BACKEND_UNSPECIFIED; + + if (argv == nullptr || argc <= 0) { + return; + } + + int writeIndex = 1; + for (int readIndex = 1; readIndex < argc; ++readIndex) { + ExampleCameraBackend backend = parseCameraBackendArg(argv[readIndex]); + if (backend != EXAMPLE_CAMERA_BACKEND_UNSPECIFIED) { +#if defined(_WIN32) || defined(_WIN64) + commandLine->cameraBackend = backend; +#endif + continue; + } + + argv[writeIndex++] = argv[readIndex]; + } + + commandLine->argc = writeIndex; + argv[writeIndex] = nullptr; +} + +void applyExampleCameraBackend(const ExampleCommandLine* commandLine) { +#if defined(_WIN32) || defined(_WIN64) + ExampleCameraBackend backend = commandLine != nullptr ? commandLine->cameraBackend : EXAMPLE_CAMERA_BACKEND_UNSPECIFIED; + if (backend == EXAMPLE_CAMERA_BACKEND_UNSPECIFIED && shouldPromptForWindowsBackendSelection(commandLine)) { + backend = promptForWindowsBackendSelection(); + } + + applyWindowsBackendSelection(backend); +#else + (void)commandLine; +#endif +} + +int getExampleCameraIndex(const ExampleCommandLine* commandLine) { + if (commandLine == nullptr || commandLine->argv == nullptr || commandLine->argc <= 1) { + return -1; + } + + int deviceIndex = -1; + return parseNonNegativeInt(commandLine->argv[1], &deviceIndex) ? deviceIndex : -1; +} + +const char* getExampleCameraBackendName(ExampleCameraBackend backend) { + switch (backend) { + case EXAMPLE_CAMERA_BACKEND_AUTO: + return "auto"; + case EXAMPLE_CAMERA_BACKEND_MSMF: + return "msmf"; + case EXAMPLE_CAMERA_BACKEND_DSHOW: + return "dshow"; + case EXAMPLE_CAMERA_BACKEND_UNSPECIFIED: + default: + return "unspecified"; + } +} + // Create directory if not exists void createDirectory(const char* path) { std::error_code ec; @@ -43,29 +320,16 @@ int getCurrentWorkingDirectory(char* buffer, size_t size) { return 0; } -int selectCamera(CcapProvider* provider) { +int selectCamera(CcapProvider* provider, const ExampleCommandLine* commandLine) { + int deviceIndex = getExampleCameraIndex(commandLine); + if (deviceIndex >= 0) { + return deviceIndex; + } + CcapDeviceNamesList deviceList; if (ccap_provider_find_device_names_list(provider, &deviceList) && deviceList.deviceCount > 1) { - printf("Multiple devices found, please select one:\n"); - for (size_t i = 0; i < deviceList.deviceCount; i++) { - printf(" %zu: %s\n", i, deviceList.deviceNames[i]); - } - - int selectedIndex; - printf("Enter the index of the device you want to use: "); - if (scanf("%d", &selectedIndex) != 1) { - selectedIndex = 0; - } - - if (selectedIndex < 0 || selectedIndex >= static_cast(deviceList.deviceCount)) { - selectedIndex = 0; - fprintf(stderr, "Invalid index, using the first device: %s\n", deviceList.deviceNames[0]); - } else { - printf("Using device: %s\n", deviceList.deviceNames[selectedIndex]); - } - - return selectedIndex; + return promptForDeviceIndex(deviceList); } return -1; // One or no device, use default. @@ -73,27 +337,15 @@ int selectCamera(CcapProvider* provider) { } // extern "C" -int selectCamera(ccap::Provider& provider) { +int selectCamera(ccap::Provider& provider, const ExampleCommandLine* commandLine) { + int deviceIndex = getExampleCameraIndex(commandLine); + if (deviceIndex >= 0) { + return deviceIndex; + } + const auto names = provider.findDeviceNames(); if (names.size() > 1) { - printf("Multiple devices found, please select one:\n"); - for (size_t i = 0; i < names.size(); ++i) { - printf(" %zu: %s\n", i, names[i].c_str()); - } - - int selectedIndex; - printf("Enter the index of the device you want to use: "); - if (scanf("%d", &selectedIndex) != 1) { - selectedIndex = 0; - } - - if (selectedIndex < 0 || selectedIndex >= static_cast(names.size())) { - selectedIndex = 0; - fprintf(stderr, "Invalid index, using the first device: %s\n", names[0].c_str()); - } else { - printf("Using device: %s\n", names[selectedIndex].c_str()); - } - return selectedIndex; + return promptForDeviceIndex(names); } return -1; // One or no device, use default. } diff --git a/examples/desktop/utils/helper.h b/examples/desktop/utils/helper.h index 3fa79abb..11bc9599 100644 --- a/examples/desktop/utils/helper.h +++ b/examples/desktop/utils/helper.h @@ -13,12 +13,30 @@ extern "C" { #endif +typedef enum ExampleCameraBackend { + EXAMPLE_CAMERA_BACKEND_UNSPECIFIED = 0, + EXAMPLE_CAMERA_BACKEND_AUTO, + EXAMPLE_CAMERA_BACKEND_MSMF, + EXAMPLE_CAMERA_BACKEND_DSHOW, +} ExampleCameraBackend; + +typedef struct ExampleCommandLine { + int argc; + char** argv; + ExampleCameraBackend cameraBackend; +} ExampleCommandLine; + +void initExampleCommandLine(ExampleCommandLine* commandLine, int argc, char** argv); +void applyExampleCameraBackend(const ExampleCommandLine* commandLine); +int getExampleCameraIndex(const ExampleCommandLine* commandLine); +const char* getExampleCameraBackendName(ExampleCameraBackend backend); + // Select a camera device index interactively when multiple devices are found. // Returns: // - index >= 0: selected device index // - -1: zero or one device available (use default/open first) typedef struct CcapProvider CcapProvider; // forward declaration from ccap_c.h -int selectCamera(CcapProvider* provider); +int selectCamera(CcapProvider* provider, const ExampleCommandLine* commandLine); // Create directory if not exists (portable) void createDirectory(const char* path); // Get current working directory (portable) @@ -32,5 +50,5 @@ int getCurrentWorkingDirectory(char* buffer, size_t size); namespace ccap { class Provider; } -int selectCamera(ccap::Provider& provider); +int selectCamera(ccap::Provider& provider, const ExampleCommandLine* commandLine = nullptr); #endif diff --git a/include/ccap_c.h b/include/ccap_c.h index 5d925ec2..06602f8b 100644 --- a/include/ccap_c.h +++ b/include/ccap_c.h @@ -154,6 +154,7 @@ CCAP_EXPORT CcapProvider* ccap_provider_create(void); * @param deviceName Device name to open (NULL for default device) * @param extraInfo Extra backend hint (can be NULL). * On Windows, accepted values include `auto`, `msmf`, `dshow`, and `backend=`. + * `auto` enumerates both Windows backends and routes each device to a compatible backend automatically. * Other platforms ignore this parameter. * @return Pointer to CcapProvider instance, or NULL on failure */ @@ -164,6 +165,7 @@ CCAP_EXPORT CcapProvider* ccap_provider_create_with_device(const char* deviceNam * @param deviceIndex Device index (negative for default device) * @param extraInfo Extra backend hint (can be NULL). * On Windows, accepted values include `auto`, `msmf`, `dshow`, and `backend=`. + * `auto` enumerates both Windows backends and routes each device to a compatible backend automatically. * Other platforms ignore this parameter. * @return Pointer to CcapProvider instance, or NULL on failure */ diff --git a/include/ccap_core.h b/include/ccap_core.h index 887eb6be..f60c267d 100644 --- a/include/ccap_core.h +++ b/include/ccap_core.h @@ -67,6 +67,7 @@ class CCAP_EXPORT Provider final { * @param deviceName The name of the device to open. @see #open * @param extraInfo Optional backend hint. * On Windows, accepted values include `auto`, `msmf`, `dshow`, and `backend=`. + * `auto` enumerates both Windows backends and routes each device to a compatible backend automatically. * Other platforms ignore this parameter. */ explicit Provider(std::string_view deviceName, std::string_view extraInfo = ""); @@ -76,6 +77,7 @@ class CCAP_EXPORT Provider final { * @param deviceIndex The index of the device to open. @see #open * @param extraInfo Optional backend hint. * On Windows, accepted values include `auto`, `msmf`, `dshow`, and `backend=`. + * `auto` enumerates both Windows backends and routes each device to a compatible backend automatically. * Other platforms ignore this parameter. */ explicit Provider(int deviceIndex, std::string_view extraInfo = ""); diff --git a/src/ccap_core.cpp b/src/ccap_core.cpp index e1025cc6..2b0e8756 100644 --- a/src/ccap_core.cpp +++ b/src/ccap_core.cpp @@ -176,24 +176,36 @@ std::optional parseWindowsBackendPreferenceValue(std:: return std::nullopt; } -WindowsBackendPreference resolveWindowsBackendPreference(std::string_view extraInfo) { - if (auto parsed = parseWindowsBackendPreferenceValue(extraInfo)) { - return *parsed; +std::string getEnvironmentValue(std::string_view name) { +#if defined(_WIN32) || defined(_MSC_VER) + if (name.empty()) { + return {}; } - std::string envValue; -#if defined(_MSC_VER) - char* rawValue = nullptr; - size_t rawLength = 0; - if (_dupenv_s(&rawValue, &rawLength, "CCAP_WINDOWS_BACKEND") == 0 && rawValue != nullptr) { - envValue.assign(rawValue, rawLength == 0 ? 0 : rawLength - 1); - free(rawValue); + std::string envName(name); + DWORD required = GetEnvironmentVariableA(envName.c_str(), nullptr, 0); + if (required == 0) { + return {}; } -#else - if (const char* rawValue = std::getenv("CCAP_WINDOWS_BACKEND"); rawValue != nullptr) { - envValue = rawValue; + + std::string value(required > 0 ? required - 1 : 0, '\0'); + if (GetEnvironmentVariableA(envName.c_str(), value.data(), required) == 0) { + return {}; } + + return value; +#else + const char* rawValue = std::getenv(std::string(name).c_str()); + return rawValue != nullptr ? std::string(rawValue) : std::string(); #endif +} + +WindowsBackendPreference resolveWindowsBackendPreference(std::string_view extraInfo) { + if (auto parsed = parseWindowsBackendPreferenceValue(extraInfo)) { + return *parsed; + } + + std::string envValue = getEnvironmentValue("CCAP_WINDOWS_BACKEND"); if (!envValue.empty()) { if (auto parsed = parseWindowsBackendPreferenceValue(envValue)) { @@ -226,18 +238,10 @@ bool isMediaFoundationCameraBackendAvailable() { ProviderImp* createWindowsProvider(std::string_view extraInfo) { WindowsBackendPreference preference = resolveWindowsBackendPreference(extraInfo); - if (preference == WindowsBackendPreference::DirectShow) { - return createProviderDirectShow(); - } - if (preference == WindowsBackendPreference::MSMF && isMediaFoundationCameraBackendAvailable()) { return createProviderMSMF(); } - if (preference == WindowsBackendPreference::Auto && isMediaFoundationCameraBackendAvailable()) { - return createProviderMSMF(); - } - return createProviderDirectShow(); } @@ -246,9 +250,8 @@ ProviderImp* createWindowsProvider(WindowsBackendPreference preference) { case WindowsBackendPreference::MSMF: return isMediaFoundationCameraBackendAvailable() ? createProviderMSMF() : nullptr; case WindowsBackendPreference::DirectShow: - return createProviderDirectShow(); case WindowsBackendPreference::Auto: - return isMediaFoundationCameraBackendAvailable() ? createProviderMSMF() : createProviderDirectShow(); + return createProviderDirectShow(); } return createProviderDirectShow(); @@ -292,6 +295,47 @@ std::vector collectDeviceNamesFromBackend(WindowsBackendPreference std::unique_ptr provider(createWindowsProvider(preference)); return provider ? provider->findDeviceNames() : std::vector(); } + +bool deviceNameExistsInBackend(std::string_view deviceName, WindowsBackendPreference preference) { + if (deviceName.empty()) { + return false; + } + + std::vector deviceNames = collectDeviceNamesFromBackend(preference); + return std::find(deviceNames.begin(), deviceNames.end(), deviceName) != deviceNames.end(); +} + +std::vector getAutoBackendCandidates(std::string_view deviceName) { + std::vector candidates; + + if (deviceName.empty()) { + candidates.push_back(WindowsBackendPreference::DirectShow); + if (isMediaFoundationCameraBackendAvailable()) { + candidates.push_back(WindowsBackendPreference::MSMF); + } + return candidates; + } + + const bool inDirectShow = deviceNameExistsInBackend(deviceName, WindowsBackendPreference::DirectShow); + const bool inMSMF = deviceNameExistsInBackend(deviceName, WindowsBackendPreference::MSMF); + + if (inDirectShow) { + candidates.push_back(WindowsBackendPreference::DirectShow); + } + if (inMSMF) { + candidates.push_back(WindowsBackendPreference::MSMF); + } + + if (!candidates.empty()) { + return candidates; + } + + candidates.push_back(WindowsBackendPreference::DirectShow); + if (isMediaFoundationCameraBackendAvailable()) { + candidates.push_back(WindowsBackendPreference::MSMF); + } + return candidates; +} #endif } // namespace @@ -493,8 +537,8 @@ std::vector Provider::findDeviceNames() { return collectDeviceNamesFromBackend(WindowsBackendPreference::MSMF); } - std::vector preferred = collectDeviceNamesFromBackend(WindowsBackendPreference::MSMF); - std::vector fallback = collectDeviceNamesFromBackend(WindowsBackendPreference::DirectShow); + std::vector preferred = collectDeviceNamesFromBackend(WindowsBackendPreference::DirectShow); + std::vector fallback = collectDeviceNamesFromBackend(WindowsBackendPreference::MSMF); return mergeDeviceNames(std::move(preferred), fallback); #else return m_imp->findDeviceNames(); @@ -541,11 +585,16 @@ bool Provider::open(std::string_view deviceName, bool autoStart) { return tryBackend(WindowsBackendPreference::MSMF); } - if (isMediaFoundationCameraBackendAvailable() && tryBackend(WindowsBackendPreference::MSMF)) { - return true; + for (WindowsBackendPreference candidate : getAutoBackendCandidates(deviceName)) { + if (candidate == WindowsBackendPreference::MSMF && !isMediaFoundationCameraBackendAvailable()) { + continue; + } + if (tryBackend(candidate)) { + return true; + } } - return tryBackend(WindowsBackendPreference::DirectShow); + return false; #else return m_imp->open(deviceName) && (!autoStart || m_imp->start()); #endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 92d261a9..d8242932 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,14 +7,14 @@ include(FetchContent) # MinGW on Windows uses native threading, not pthread # Setting this BEFORE FetchContent prevents GoogleTest from searching for Threads package if(MINGW) - set(gtest_disable_pthreads ON CACHE BOOL "" FORCE) - message(STATUS "MinGW detected: Disabling pthread for GoogleTest") + set(gtest_disable_pthreads ON CACHE BOOL "" FORCE) + message(STATUS "MinGW detected: Disabling pthread for GoogleTest") endif() FetchContent_Declare( - googletest - GIT_REPOSITORY https://github.com/google/googletest.git - GIT_TAG release-1.11.0 + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG release-1.11.0 ) message(STATUS "Fetching Google Test archive...") @@ -26,30 +26,30 @@ FetchContent_MakeAvailable(googletest) # Fix compile definitions for tests on MinGW # GoogleTest sets GTEST_HAS_PTHREAD=1 by default on MinGW, which causes AutoHandle errors if(MINGW) - # Remove GTEST_HAS_PTHREAD=1 from all googletest targets and set it to 0 - foreach(target gtest gtest_main gmock gmock_main) - if(TARGET ${target}) - # Get current compile definitions - get_target_property(compile_defs ${target} INTERFACE_COMPILE_DEFINITIONS) - if(compile_defs) - # Create a new list without GTEST_HAS_PTHREAD - set(new_defs "") - foreach(def IN LISTS compile_defs) - if(NOT def MATCHES "GTEST_HAS_PTHREAD") - list(APPEND new_defs ${def}) - endif() - endforeach() - # Add GTEST_HAS_PTHREAD=0 - list(APPEND new_defs "GTEST_HAS_PTHREAD=0") - # Replace the property - set_property(TARGET ${target} PROPERTY INTERFACE_COMPILE_DEFINITIONS ${new_defs}) - else() - # If no definitions exist, just set GTEST_HAS_PTHREAD=0 - set_property(TARGET ${target} PROPERTY INTERFACE_COMPILE_DEFINITIONS "GTEST_HAS_PTHREAD=0") - endif() - endif() - endforeach() - message(STATUS "MinGW: Overridden GTEST_HAS_PTHREAD to 0 for test compilation") + # Remove GTEST_HAS_PTHREAD=1 from all googletest targets and set it to 0 + foreach(target gtest gtest_main gmock gmock_main) + if(TARGET ${target}) + # Get current compile definitions + get_target_property(compile_defs ${target} INTERFACE_COMPILE_DEFINITIONS) + if(compile_defs) + # Create a new list without GTEST_HAS_PTHREAD + set(new_defs "") + foreach(def IN LISTS compile_defs) + if(NOT def MATCHES "GTEST_HAS_PTHREAD") + list(APPEND new_defs ${def}) + endif() + endforeach() + # Add GTEST_HAS_PTHREAD=0 + list(APPEND new_defs "GTEST_HAS_PTHREAD=0") + # Replace the property + set_property(TARGET ${target} PROPERTY INTERFACE_COMPILE_DEFINITIONS ${new_defs}) + else() + # If no definitions exist, just set GTEST_HAS_PTHREAD=0 + set_property(TARGET ${target} PROPERTY INTERFACE_COMPILE_DEFINITIONS "GTEST_HAS_PTHREAD=0") + endif() + endif() + endforeach() + message(STATUS "MinGW: Overridden GTEST_HAS_PTHREAD to 0 for test compilation") endif() if (NOT DEFINED LIBYUV_REPO_URL) @@ -121,6 +121,78 @@ if (MSVC) ) endif () +add_executable( + ccap_example_helper_test + test_example_helper.cpp + ${CMAKE_SOURCE_DIR}/examples/desktop/utils/helper.cpp +) + +target_link_libraries( + ccap_example_helper_test + PRIVATE + ccap_test_utils + gtest_main +) + +target_include_directories( + ccap_example_helper_test + PRIVATE + ${CMAKE_SOURCE_DIR}/examples/desktop/utils +) + +set_target_properties(ccap_example_helper_test PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON +) + +if (MSVC) + target_compile_options(ccap_example_helper_test PRIVATE + /MP + /std:c++17 + /Zc:__cplusplus + /source-charset:utf-8 + /bigobj + /wd4996 + /D_CRT_SECURE_NO_WARNINGS + ) +endif () + +add_executable( + ccap_cli_args_parser_test + test_cli_args_parser.cpp + ${CMAKE_SOURCE_DIR}/cli/args_parser.cpp +) + +target_link_libraries( + ccap_cli_args_parser_test + PRIVATE + ccap_test_utils + gtest_main +) + +target_include_directories( + ccap_cli_args_parser_test + PRIVATE + ${CMAKE_SOURCE_DIR}/cli +) + +set_target_properties(ccap_cli_args_parser_test PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON +) + +if (MSVC) + target_compile_options(ccap_cli_args_parser_test PRIVATE + /MP + /std:c++17 + /Zc:__cplusplus + /source-charset:utf-8 + /bigobj + /wd4996 + /D_CRT_SECURE_NO_WARNINGS + ) +endif () + # Test executables - Refactored and simplified add_executable( ccap_convert_test @@ -373,6 +445,8 @@ include(GoogleTest) # for test listing on the host. We'll run tests explicitly (e.g., via QEMU) in CI. if (NOT CMAKE_CROSSCOMPILING) # Defer discovery to test time to avoid executing the binary during configure + gtest_discover_tests(ccap_example_helper_test DISCOVERY_MODE PRE_TEST) + gtest_discover_tests(ccap_cli_args_parser_test DISCOVERY_MODE PRE_TEST) gtest_discover_tests(ccap_convert_test DISCOVERY_MODE PRE_TEST) gtest_discover_tests(ccap_performance_test DISCOVERY_MODE PRE_TEST) if (WIN32) diff --git a/tests/test_cli_args_parser.cpp b/tests/test_cli_args_parser.cpp new file mode 100644 index 00000000..a31a18a8 --- /dev/null +++ b/tests/test_cli_args_parser.cpp @@ -0,0 +1,46 @@ +#include "args_parser.h" + +#include + +TEST(CLIArgsParserTest, PreviewWithoutExplicitSizeRequests1080p) { + char arg0[] = "ccap"; + char arg1[] = "--preview"; + char* argv[] = { arg0, arg1, nullptr }; + + const ccap_cli::CLIOptions opts = ccap_cli::parseArgs(2, argv); + + EXPECT_TRUE(opts.enablePreview); + EXPECT_EQ(opts.width, 1920); + EXPECT_EQ(opts.height, 1080); + EXPECT_FALSE(opts.widthSpecified); + EXPECT_FALSE(opts.heightSpecified); +} + +TEST(CLIArgsParserTest, PreviewKeepsExplicitResolution) { + char arg0[] = "ccap"; + char arg1[] = "--preview"; + char arg2[] = "--width"; + char arg3[] = "640"; + char arg4[] = "--height"; + char arg5[] = "480"; + char* argv[] = { arg0, arg1, arg2, arg3, arg4, arg5, nullptr }; + + const ccap_cli::CLIOptions opts = ccap_cli::parseArgs(6, argv); + + EXPECT_TRUE(opts.enablePreview); + EXPECT_EQ(opts.width, 640); + EXPECT_EQ(opts.height, 480); + EXPECT_TRUE(opts.widthSpecified); + EXPECT_TRUE(opts.heightSpecified); +} + +TEST(CLIArgsParserTest, CaptureDefaultsStayUnchangedWithoutPreview) { + char arg0[] = "ccap"; + char* argv[] = { arg0, nullptr }; + + const ccap_cli::CLIOptions opts = ccap_cli::parseArgs(1, argv); + + EXPECT_FALSE(opts.enablePreview); + EXPECT_EQ(opts.width, 1280); + EXPECT_EQ(opts.height, 720); +} \ No newline at end of file diff --git a/tests/test_example_helper.cpp b/tests/test_example_helper.cpp new file mode 100644 index 00000000..584c6488 --- /dev/null +++ b/tests/test_example_helper.cpp @@ -0,0 +1,66 @@ +#include "helper.h" + +#include + +TEST(ExampleHelperTest, FiltersBackendFlagsFromArgumentList) { + char arg0[] = "demo"; + char arg1[] = "--msmf"; + char arg2[] = "2"; + char arg3[] = "capture"; + char* argv[] = { arg0, arg1, arg2, arg3, nullptr }; + + ExampleCommandLine commandLine{}; + initExampleCommandLine(&commandLine, 4, argv); + + EXPECT_EQ(commandLine.argc, 3); + EXPECT_STREQ(commandLine.argv[0], "demo"); + EXPECT_STREQ(commandLine.argv[1], "2"); + EXPECT_STREQ(commandLine.argv[2], "capture"); +#if defined(_WIN32) || defined(_WIN64) + EXPECT_EQ(commandLine.cameraBackend, EXAMPLE_CAMERA_BACKEND_MSMF); +#else + EXPECT_EQ(commandLine.cameraBackend, EXAMPLE_CAMERA_BACKEND_UNSPECIFIED); +#endif +} + +TEST(ExampleHelperTest, LastBackendFlagWins) { + char arg0[] = "demo"; + char arg1[] = "--msmf"; + char arg2[] = "--dshow"; + char arg3[] = "1"; + char* argv[] = { arg0, arg1, arg2, arg3, nullptr }; + + ExampleCommandLine commandLine{}; + initExampleCommandLine(&commandLine, 4, argv); + + EXPECT_EQ(commandLine.argc, 2); + EXPECT_STREQ(commandLine.argv[1], "1"); +#if defined(_WIN32) || defined(_WIN64) + EXPECT_EQ(commandLine.cameraBackend, EXAMPLE_CAMERA_BACKEND_DSHOW); +#else + EXPECT_EQ(commandLine.cameraBackend, EXAMPLE_CAMERA_BACKEND_UNSPECIFIED); +#endif +} + +TEST(ExampleHelperTest, ParsesDeviceIndexAfterFiltering) { + char arg0[] = "demo"; + char arg1[] = "--auto"; + char arg2[] = "7"; + char* argv[] = { arg0, arg1, arg2, nullptr }; + + ExampleCommandLine commandLine{}; + initExampleCommandLine(&commandLine, 3, argv); + + EXPECT_EQ(getExampleCameraIndex(&commandLine), 7); +} + +TEST(ExampleHelperTest, RejectsNonNumericDeviceIndex) { + char arg0[] = "demo"; + char arg1[] = "camera0"; + char* argv[] = { arg0, arg1, nullptr }; + + ExampleCommandLine commandLine{}; + initExampleCommandLine(&commandLine, 2, argv); + + EXPECT_EQ(getExampleCameraIndex(&commandLine), -1); +} diff --git a/tests/test_windows_backends.cpp b/tests/test_windows_backends.cpp index 3f25aa6e..337acbc9 100644 --- a/tests/test_windows_backends.cpp +++ b/tests/test_windows_backends.cpp @@ -153,6 +153,59 @@ std::optional getDeviceInfoForBackend(const char* backend, con return info; } +class ScopedErrorCallback { +public: + explicit ScopedErrorCallback(ccap::ErrorCallback callback) : + m_previous(ccap::getErrorCallback()) { + ccap::setErrorCallback(std::move(callback)); + } + + ~ScopedErrorCallback() { + ccap::setErrorCallback(std::move(m_previous)); + } + +private: + ccap::ErrorCallback m_previous; +}; + +std::optional findDirectShowOnlyDevice() { + auto dshowDevices = listDevicesForBackend("dshow"); + auto msmfDevices = listDevicesForBackend("msmf"); + auto isVisibleInMSMF = [&](const std::string& deviceName) { + return std::find(msmfDevices.begin(), msmfDevices.end(), deviceName) != msmfDevices.end(); + }; + + for (const std::string& deviceName : dshowDevices) { + if (!isVisibleInMSMF(deviceName) && virtualCameraRank(deviceName) >= 0) { + return deviceName; + } + } + + for (const std::string& deviceName : dshowDevices) { + if (!isVisibleInMSMF(deviceName)) { + return deviceName; + } + } + + return std::nullopt; +} + +std::optional findMediaFoundationOnlyDevice() { + auto dshowDevices = listDevicesForBackend("dshow"); + auto msmfDevices = listDevicesForBackend("msmf"); + auto isVisibleInDirectShow = [&](const std::string& deviceName) { + return std::find(dshowDevices.begin(), dshowDevices.end(), deviceName) != dshowDevices.end(); + }; + + for (const std::string& deviceName : msmfDevices) { + if (!isVisibleInDirectShow(deviceName)) { + return deviceName; + } + } + + return std::nullopt; +} + std::optional pickCommonResolution(const std::string& deviceName) { auto dshowInfo = getDeviceInfoForBackend("dshow", deviceName); auto msmfInfo = getDeviceInfoForBackend("msmf", deviceName); @@ -378,6 +431,54 @@ TEST(WindowsBackendsTest, AutoEnumerationContainsForcedBackendResults) { } } +TEST(WindowsBackendsTest, AutoOpenPrefersDirectShowForDirectShowOnlyDevice) { + auto dshowOnlyDevice = findDirectShowOnlyDevice(); + if (!dshowOnlyDevice) { + GTEST_SKIP() << "No device is exposed only through DirectShow"; + } + + std::vector errorMessages; + ScopedErrorCallback scopedErrorCallback([&](ccap::ErrorCode, std::string_view description) { + errorMessages.emplace_back(description); + }); + + ScopedEnvVar env("CCAP_WINDOWS_BACKEND", "auto"); + ccap::Provider provider; + ASSERT_TRUE(provider.open(*dshowOnlyDevice, false)); + provider.close(); + + const auto msmfDeviceError = std::find_if(errorMessages.begin(), errorMessages.end(), [](const std::string& message) { + return message.find("No Media Foundation video capture device") != std::string::npos; + }); + + EXPECT_EQ(msmfDeviceError, errorMessages.end()) + << "auto mode should not probe MSMF first for a DirectShow-only device: " << *dshowOnlyDevice; +} + +TEST(WindowsBackendsTest, AutoOpenPrefersMediaFoundationForMediaFoundationOnlyDevice) { + auto msmfOnlyDevice = findMediaFoundationOnlyDevice(); + if (!msmfOnlyDevice) { + GTEST_SKIP() << "No device is exposed only through Media Foundation"; + } + + std::vector errorMessages; + ScopedErrorCallback scopedErrorCallback([&](ccap::ErrorCode, std::string_view description) { + errorMessages.emplace_back(description); + }); + + ScopedEnvVar env("CCAP_WINDOWS_BACKEND", "auto"); + ccap::Provider provider; + ASSERT_TRUE(provider.open(*msmfOnlyDevice, false)); + provider.close(); + + const auto dshowDeviceError = std::find_if(errorMessages.begin(), errorMessages.end(), [&](const std::string& message) { + return message.find("No video capture device: " + *msmfOnlyDevice) != std::string::npos; + }); + + EXPECT_EQ(dshowDeviceError, errorMessages.end()) + << "auto mode should not probe DirectShow first for a Media Foundation-only device: " << *msmfOnlyDevice; +} + TEST(WindowsBackendsTest, FilePlaybackStillWorksWhenMSMFIsForced) { #ifdef CCAP_ENABLE_FILE_PLAYBACK ScopedEnvVar env("CCAP_WINDOWS_BACKEND", "msmf"); From fcb19d782daf44ce5ab4e96cb0579432bdce43d3 Mon Sep 17 00:00:00 2001 From: "wangyang (wysaid)" Date: Sun, 8 Mar 2026 02:42:43 +0800 Subject: [PATCH 08/11] Add Windows backend selection to CLI --- cli/args_parser.cpp | 64 +++++++++++++++++++- cli/args_parser.h | 3 + cli/ccap_cli.cpp | 41 +++++++++---- cli/ccap_cli_utils.cpp | 106 ++++++++++++++++++++++++++++++++- cli/ccap_cli_utils.h | 11 +++- docs/content/cli.md | 13 +++- docs/content/cli.zh.md | 13 +++- tests/test_ccap_cli.cpp | 16 +++++ tests/test_cli_args_parser.cpp | 37 +++++++++++- 9 files changed, 282 insertions(+), 22 deletions(-) diff --git a/cli/args_parser.cpp b/cli/args_parser.cpp index d99647f2..16a75266 100644 --- a/cli/args_parser.cpp +++ b/cli/args_parser.cpp @@ -24,6 +24,44 @@ constexpr int DEFAULT_PREVIEW_HEIGHT = 1080; constexpr double DEFAULT_FPS = 30.0; constexpr int DEFAULT_TIMEOUT_MS = 5000; +std::string normalizeWindowsCameraBackend(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + + if (value == "auto") { + return "auto"; + } + if (value == "msmf" || value == "mediafoundation") { + return "msmf"; + } + if (value == "dshow" || value == "directshow") { + return "dshow"; + } + return {}; +} + +[[noreturn]] void printWindowsBackendOptionErrorAndExit(const char* programName, const std::string& message) { + std::cerr << "Error: " << message << "\n\n"; + printUsage(programName); + std::exit(1); +} + +void setWindowsCameraBackend(CLIOptions& opts, const char* programName, const std::string& rawValue) { +#if defined(_WIN32) || defined(_WIN64) + std::string normalized = normalizeWindowsCameraBackend(rawValue); + if (normalized.empty()) { + printWindowsBackendOptionErrorAndExit(programName, + "invalid Windows camera backend '" + rawValue + "'. Use auto, msmf, or dshow."); + } + opts.windowsCameraBackend = std::move(normalized); +#else + (void)opts; + printWindowsBackendOptionErrorAndExit(programName, + "Windows camera backend options are only supported on Windows."); +#endif +} + void printVersion() { std::cout << "ccap version " << CCAP_VERSION_STRING << " Copyright (c) 2025 wysaid\n"; @@ -99,7 +137,6 @@ void printUsage(const char* programName) { << " -q, --quiet quiet mode (only show errors, equivalent to log level Error)\n" << " --timeout seconds program timeout (auto-exit after N seconds)\n" << " --timeout-exit-code code exit code when timeout occurs (default: 0)\n" - << " environment: CCAP_WINDOWS_BACKEND=auto|msmf|dshow (Windows backend override)\n" << "\n" << "Input options:\n" << " -i, --input source input source: video file, device index, or device name\n" @@ -161,6 +198,16 @@ void printUsage(const char* programName) { << " --convert-output file output file for conversion\n" << "\n"; +#if defined(_WIN32) || defined(_WIN64) + std::cout << "Windows camera backend options:\n" + << " --camera-backend backend camera backend: auto, msmf, dshow\n" + << " --auto same as --camera-backend auto\n" + << " --msmf same as --camera-backend msmf\n" + << " --dshow same as --camera-backend dshow\n" + << " environment: CCAP_WINDOWS_BACKEND=auto|msmf|dshow (process-wide override)\n" + << "\n"; +#endif + std::cout << "Examples:\n" << " " << programName << " --list-devices\n" << " " << programName << " --device-info 0\n" @@ -171,6 +218,10 @@ void printUsage(const char* programName) { << " " << programName << " -i /path/to/video.mp4 -c 30 -o ./frames # extract 30 frames from video\n" << " " << programName << " -i /path/to/video.mp4 --loop # loop video playback\n" << " " << programName << " --timeout 60 -d 0 --preview # preview for 60 seconds then exit\n"; +#if defined(_WIN32) || defined(_WIN64) + std::cout << " " << programName << " --list-devices --msmf # list devices through Media Foundation\n" + << " " << programName << " -d 0 -c 1 --dshow # capture with DirectShow explicitly\n"; +#endif #ifdef CCAP_CLI_WITH_GLFW std::cout << " " << programName << " -d 0 --preview\n" << " " << programName << " -i /path/to/video.mp4 --preview\n"; @@ -314,6 +365,17 @@ CLIOptions parseArgs(int argc, char* argv[]) { opts.deviceName = val; } } + } else if (arg == "--camera-backend") { + if (i + 1 >= argc) { + printWindowsBackendOptionErrorAndExit(argv[0], "--camera-backend requires a value."); + } + setWindowsCameraBackend(opts, argv[0], argv[++i]); + } else if (arg == "--auto") { + setWindowsCameraBackend(opts, argv[0], "auto"); + } else if (arg == "--msmf") { + setWindowsCameraBackend(opts, argv[0], "msmf"); + } else if (arg == "--dshow") { + setWindowsCameraBackend(opts, argv[0], "dshow"); } else if (arg == "--video") { if (i + 1 < argc) { opts.videoFilePath = argv[++i]; diff --git a/cli/args_parser.h b/cli/args_parser.h index a6de740b..d5446284 100644 --- a/cli/args_parser.h +++ b/cli/args_parser.h @@ -35,6 +35,9 @@ struct CLIOptions { bool showDeviceInfo = false; bool verbose = false; + // Windows camera backend override + std::string windowsCameraBackend; + // Input source std::string inputSource; int deviceIndex = 0; diff --git a/cli/ccap_cli.cpp b/cli/ccap_cli.cpp index 1f3353fb..d05609be 100644 --- a/cli/ccap_cli.cpp +++ b/cli/ccap_cli.cpp @@ -18,6 +18,28 @@ #include #include +namespace { + +void logWindowsCameraBackendOverride(const ccap_cli::CLIOptions& opts) { +#if defined(_WIN32) || defined(_WIN64) + if (opts.windowsCameraBackend.empty() || !ccap::infoLogEnabled()) { + return; + } + + if (!opts.videoFilePath.empty() || !opts.convertInput.empty()) { + std::cout << "Ignoring Windows camera backend override for non-camera input: " + << opts.windowsCameraBackend << std::endl; + return; + } + + std::cout << "Using Windows camera backend override from CLI: " << opts.windowsCameraBackend << std::endl; +#else + (void)opts; +#endif +} + +} // namespace + int main(int argc, char* argv[]) { if (argc < 2) { ccap_cli::printUsage(argv[0]); @@ -87,13 +109,15 @@ int main(int argc, char* argv[]) { << ", Description: " << description << std::endl; }); + logWindowsCameraBackendOverride(opts); + // Handle different modes if (opts.listDevices) { - return ccap_cli::listDevices(); + return ccap_cli::listDevices(opts); } if (opts.showDeviceInfo) { - return ccap_cli::showDeviceInfo(opts.deviceInfoIndex); + return ccap_cli::showDeviceInfo(opts, opts.deviceInfoIndex); } if (!opts.convertInput.empty()) { @@ -131,18 +155,9 @@ int main(int argc, char* argv[]) { (opts.inputSource.empty() || !opts.deviceName.empty() || opts.deviceIndex >= 0)) { // Print camera info (equivalent to --device-info) if (!opts.deviceName.empty()) { - // Find device index by name - ccap::Provider provider; - auto deviceNames = provider.findDeviceNames(); - for (size_t i = 0; i < deviceNames.size(); ++i) { - if (deviceNames[i] == opts.deviceName) { - return ccap_cli::showDeviceInfo(static_cast(i)); - } - } - std::cerr << "Device not found: " << opts.deviceName << std::endl; - return 1; + return ccap_cli::showDeviceInfo(opts, opts.deviceName); } - return ccap_cli::showDeviceInfo(opts.deviceIndex); + return ccap_cli::showDeviceInfo(opts, opts.deviceIndex); } #ifdef CCAP_CLI_WITH_GLFW diff --git a/cli/ccap_cli_utils.cpp b/cli/ccap_cli_utils.cpp index 55185c4e..b2f1ec1d 100644 --- a/cli/ccap_cli_utils.cpp +++ b/cli/ccap_cli_utils.cpp @@ -13,13 +13,22 @@ #include #include #include +#include #include #include #include #include #include +#include #include +#if defined(_WIN32) || defined(_WIN64) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + #ifdef CCAP_CLI_WITH_STB_IMAGE #if defined(__clang__) || defined(__GNUC__) #pragma GCC diagnostic push @@ -41,11 +50,84 @@ namespace ccap_cli { +namespace { + +#if defined(_WIN32) || defined(_WIN64) +constexpr const char* kWindowsBackendEnvVar = "CCAP_WINDOWS_BACKEND"; +#endif + +std::string getEnvironmentValue(const char* name) { +#if defined(_WIN32) || defined(_WIN64) + char* rawValue = nullptr; + size_t rawLength = 0; + if (_dupenv_s(&rawValue, &rawLength, name) != 0 || rawValue == nullptr) { + return {}; + } + + std::string value(rawValue); + std::free(rawValue); + return value; +#else + const char* rawValue = std::getenv(name); + return rawValue != nullptr ? std::string(rawValue) : std::string(); +#endif +} + +void setEnvironmentValue(const char* name, const char* value) { +#if defined(_WIN32) || defined(_WIN64) + SetEnvironmentVariableA(name, value); +#else + if (value != nullptr && *value != '\0') { + setenv(name, value, 1); + } else { + unsetenv(name); + } +#endif +} + +class ScopedEnvironmentValue { +public: + ScopedEnvironmentValue(const char* name, const char* value) : + m_name(name), + m_oldValue(getEnvironmentValue(name)), + m_hadValue(!m_oldValue.empty()) { + setEnvironmentValue(m_name.c_str(), value); + } + + ~ScopedEnvironmentValue() { + if (m_hadValue) { + setEnvironmentValue(m_name.c_str(), m_oldValue.c_str()); + } else { + setEnvironmentValue(m_name.c_str(), nullptr); + } + } + +private: + std::string m_name; + std::string m_oldValue; + bool m_hadValue = false; +}; + +std::unique_ptr makeWindowsCameraBackendOverride(const CLIOptions& opts, bool isCameraMode) { +#if defined(_WIN32) || defined(_WIN64) + if (isCameraMode && !opts.windowsCameraBackend.empty()) { + return std::make_unique(kWindowsBackendEnvVar, opts.windowsCameraBackend.c_str()); + } +#else + (void)opts; + (void)isCameraMode; +#endif + return nullptr; +} + +} // namespace + // ============================================================================ // Device Operations // ============================================================================ -int listDevices() { +int listDevices(const CLIOptions& opts) { + auto backendOverride = makeWindowsCameraBackendOverride(opts, true); ccap::Provider provider; auto deviceNames = provider.findDeviceNames(); @@ -102,7 +184,8 @@ int listDevices() { return 0; } -int showDeviceInfo(int deviceIndex) { +int showDeviceInfo(const CLIOptions& opts, int deviceIndex) { + auto backendOverride = makeWindowsCameraBackendOverride(opts, true); ccap::Provider provider; auto deviceNames = provider.findDeviceNames(); @@ -161,6 +244,21 @@ int showDeviceInfo(int deviceIndex) { return 0; } +int showDeviceInfo(const CLIOptions& opts, const std::string& deviceName) { + auto backendOverride = makeWindowsCameraBackendOverride(opts, true); + ccap::Provider provider; + auto deviceNames = provider.findDeviceNames(); + + for (size_t index = 0; index < deviceNames.size(); ++index) { + if (deviceNames[index] == deviceName) { + return showDeviceInfo(opts, static_cast(index)); + } + } + + std::cerr << "Device not found: " << deviceName << std::endl; + return 1; +} + int printVideoInfo(const std::string& videoPath) { #if defined(CCAP_ENABLE_FILE_PLAYBACK) ccap::Provider provider; @@ -191,10 +289,11 @@ int printVideoInfo(const std::string& videoPath) { } int captureFrames(const CLIOptions& opts) { + bool isVideoMode = !opts.videoFilePath.empty(); + auto backendOverride = makeWindowsCameraBackendOverride(opts, !isVideoMode); ccap::Provider provider; // Set capture parameters (only meaningful for camera mode) - bool isVideoMode = !opts.videoFilePath.empty(); if (!isVideoMode) { provider.set(ccap::PropertyName::Width, opts.width); provider.set(ccap::PropertyName::Height, opts.height); @@ -766,6 +865,7 @@ void main() { )"; int runPreview(const CLIOptions& opts) { + auto backendOverride = makeWindowsCameraBackendOverride(opts, opts.videoFilePath.empty()); ccap::Provider provider; // Set capture parameters (only meaningful for camera mode) diff --git a/cli/ccap_cli_utils.h b/cli/ccap_cli_utils.h index b95d415a..e1995d1f 100644 --- a/cli/ccap_cli_utils.h +++ b/cli/ccap_cli_utils.h @@ -21,14 +21,21 @@ namespace ccap_cli { * @brief List all available camera devices * @return Exit code (0 on success) */ -int listDevices(); +int listDevices(const CLIOptions& opts); /** * @brief Show detailed information about a device * @param deviceIndex Device index (-1 for all devices) * @return Exit code (0 on success, 1 on error) */ -int showDeviceInfo(int deviceIndex); +int showDeviceInfo(const CLIOptions& opts, int deviceIndex); + +/** + * @brief Show detailed information about a device selected by name + * @param deviceName Device name + * @return Exit code (0 on success, 1 on error) + */ +int showDeviceInfo(const CLIOptions& opts, const std::string& deviceName); /** * @brief Print video file information diff --git a/docs/content/cli.md b/docs/content/cli.md index 9a33c531..5db687e4 100644 --- a/docs/content/cli.md +++ b/docs/content/cli.md @@ -71,7 +71,7 @@ The executable will be located in the `build/` directory (or `build/Debug`, `bui **Windows (MSVC)**: The CLI tool uses static runtime linking (`/MT` flag) to eliminate the dependency on VCRUNTIME DLL, allowing single-file distribution without requiring Visual C++ Redistributables. -**Windows backend override**: set `CCAP_WINDOWS_BACKEND=auto|msmf|dshow` before launching the CLI if you want to force Media Foundation or DirectShow during troubleshooting or validation. +**Windows backend override**: on Windows, the CLI also accepts `--camera-backend auto|msmf|dshow` and the `--auto`, `--msmf`, `--dshow` aliases. The CLI logs the selected backend override after the option is applied. You can still set `CCAP_WINDOWS_BACKEND=auto|msmf|dshow` before launching the CLI if you prefer a process-wide override. **Linux**: Attempts to statically link libstdc++ and libgcc when available. Falls back to dynamic linking if not available (e.g., Fedora without `libstdc++-static` package). The binary still depends on glibc and may not work on systems with older glibc versions. @@ -88,6 +88,17 @@ The executable will be located in the `build/` directory (or `build/Debug`, `bui | `--timeout SECONDS` | Program timeout: auto-exit after N seconds | | `--timeout-exit-code CODE` | Exit code when timeout occurs (default: 0) | +### Windows Camera Backend Options + +These options are available on Windows only. + +| Option | Description | +|--------|-------------| +| `--camera-backend auto\|msmf\|dshow` | Select the Windows camera backend explicitly | +| `--auto` | Alias for `--camera-backend auto` | +| `--msmf` | Alias for `--camera-backend msmf` | +| `--dshow` | Alias for `--camera-backend dshow` | + ### Device Enumeration | Option | Description | diff --git a/docs/content/cli.zh.md b/docs/content/cli.zh.md index e34cafae..9c031bb3 100644 --- a/docs/content/cli.zh.md +++ b/docs/content/cli.zh.md @@ -71,7 +71,7 @@ cmake --build build **Windows (MSVC)**:CLI 工具使用静态运行时链接(`/MT` 标志)来消除对 VCRUNTIME DLL 的依赖,允许单文件分发而无需用户安装 Visual C++ 运行库。 -**Windows 后端覆盖**:如果你希望在排障或验证时强制使用 Media Foundation 或 DirectShow,可以在启动 CLI 前设置 `CCAP_WINDOWS_BACKEND=auto|msmf|dshow`。 +**Windows 后端覆盖**:在 Windows 上,CLI 现在也支持 `--camera-backend auto|msmf|dshow`,以及 `--auto`、`--msmf`、`--dshow` 这几个别名。参数生效后,CLI 会输出当前选中的后端覆盖日志。如果你更希望使用进程级覆盖,也可以在启动前设置 `CCAP_WINDOWS_BACKEND=auto|msmf|dshow`。 **Linux**:当可用时尝试静态链接 libstdc++ 和 libgcc。如果不可用(例如 Fedora 未安装 `libstdc++-static` 包),则回退到动态链接。二进制文件仍然依赖 glibc,可能无法在旧 glibc 版本的系统上运行。 @@ -86,6 +86,17 @@ cmake --build build | `-v, --version` | 显示版本信息 | | `--verbose` | 启用详细日志输出 | +### Windows 相机后端选项 + +这些选项仅在 Windows 上支持。 + +| 选项 | 描述 | +|-----|------| +| `--camera-backend auto\|msmf\|dshow` | 显式选择 Windows 相机后端 | +| `--auto` | `--camera-backend auto` 的别名 | +| `--msmf` | `--camera-backend msmf` 的别名 | +| `--dshow` | `--camera-backend dshow` 的别名 | + ### 设备枚举 | 选项 | 描述 | diff --git a/tests/test_ccap_cli.cpp b/tests/test_ccap_cli.cpp index 47871588..dba38247 100644 --- a/tests/test_ccap_cli.cpp +++ b/tests/test_ccap_cli.cpp @@ -315,6 +315,22 @@ TEST_F(CCAPCLITest, VerboseOption) { EXPECT_THAT(result.output, ::testing::HasSubstr("usage:")); } +#if defined(_WIN32) || defined(_WIN64) +TEST_F(CCAPCLITest, HelpShowsWindowsBackendOptions) { + auto result = runCLI("--help"); + EXPECT_EQ(result.exitCode, 0); + EXPECT_THAT(result.output, ::testing::HasSubstr("--camera-backend")); + EXPECT_THAT(result.output, ::testing::HasSubstr("--msmf")); + EXPECT_THAT(result.output, ::testing::HasSubstr("--dshow")); +} + +TEST_F(CCAPCLITest, WindowsBackendOverrideLogsSelection) { + auto result = runCLI("--list-devices --msmf"); + EXPECT_EQ(result.exitCode, 0); + EXPECT_THAT(result.output, ::testing::HasSubstr("Using Windows camera backend override from CLI: msmf")); +} +#endif + TEST_F(CCAPCLITest, InvalidYUVConversion_MissingDimensions) { // Create a dummy YUV file fs::path yuvPath = testOutputDir / "test.yuv"; diff --git a/tests/test_cli_args_parser.cpp b/tests/test_cli_args_parser.cpp index a31a18a8..adc37969 100644 --- a/tests/test_cli_args_parser.cpp +++ b/tests/test_cli_args_parser.cpp @@ -43,4 +43,39 @@ TEST(CLIArgsParserTest, CaptureDefaultsStayUnchangedWithoutPreview) { EXPECT_FALSE(opts.enablePreview); EXPECT_EQ(opts.width, 1280); EXPECT_EQ(opts.height, 720); -} \ No newline at end of file +} + +#if defined(_WIN32) || defined(_WIN64) +TEST(CLIArgsParserTest, ParsesWindowsCameraBackendOption) { + char arg0[] = "ccap"; + char arg1[] = "--camera-backend"; + char arg2[] = "msmf"; + char* argv[] = { arg0, arg1, arg2, nullptr }; + + const ccap_cli::CLIOptions opts = ccap_cli::parseArgs(3, argv); + + EXPECT_EQ(opts.windowsCameraBackend, "msmf"); +} + +TEST(CLIArgsParserTest, ParsesWindowsCameraBackendAlias) { + char arg0[] = "ccap"; + char arg1[] = "--dshow"; + char* argv[] = { arg0, arg1, nullptr }; + + const ccap_cli::CLIOptions opts = ccap_cli::parseArgs(2, argv); + + EXPECT_EQ(opts.windowsCameraBackend, "dshow"); +} + +TEST(CLIArgsParserTest, LastWindowsCameraBackendOptionWins) { + char arg0[] = "ccap"; + char arg1[] = "--msmf"; + char arg2[] = "--camera-backend"; + char arg3[] = "auto"; + char* argv[] = { arg0, arg1, arg2, arg3, nullptr }; + + const ccap_cli::CLIOptions opts = ccap_cli::parseArgs(4, argv); + + EXPECT_EQ(opts.windowsCameraBackend, "auto"); +} +#endif \ No newline at end of file From 8a64c1afa0a9f13f9ccc82f3c946db92b159858e Mon Sep 17 00:00:00 2001 From: "wangyang (wysaid)" Date: Sun, 8 Mar 2026 13:51:07 +0800 Subject: [PATCH 09/11] refactor: use static COM init in MSMF backend to avoid thread-affinity issues Replace per-instance m_didInitializeCom/m_didSetup flags with a static setupCom pattern matching the DirectShow backend. Removes CoUninitialize from the destructor since static COM init is never paired with uninit, avoiding mismatched init/uninit on different threads. --- src/ccap_imp_windows_msmf.cpp | 17 +++++++++-------- src/ccap_imp_windows_msmf.h | 2 -- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/ccap_imp_windows_msmf.cpp b/src/ccap_imp_windows_msmf.cpp index 84455d7f..1ae13233 100644 --- a/src/ccap_imp_windows_msmf.cpp +++ b/src/ccap_imp_windows_msmf.cpp @@ -152,10 +152,6 @@ ProviderMSMF::ProviderMSMF() { ProviderMSMF::~ProviderMSMF() { close(); uninitMediaFoundation(); - if (m_didInitializeCom) { - CoUninitialize(); - m_didInitializeCom = false; - } } bool ProviderMSMF::setup() { @@ -163,11 +159,16 @@ bool ProviderMSMF::setup() { return true; } - if (!m_didSetup) { + // Use static COM initialization (matching DirectShow backend pattern). + // COM init/uninit is thread-affine — caching per-instance flags can + // mismatch if the destructor runs on a different thread. A static + // flag avoids the issue and is safe because COM reference-counts its + // per-thread initialization. + static bool s_didSetup = false; + if (!s_didSetup) { HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); - m_didInitializeCom = SUCCEEDED(hr); - m_didSetup = m_didInitializeCom || hr == RPC_E_CHANGED_MODE; - if (!m_didSetup) { + s_didSetup = SUCCEEDED(hr) || hr == RPC_E_CHANGED_MODE; + if (!s_didSetup) { reportError(ErrorCode::InitializationFailed, "COM initialization failed for Media Foundation backend"); return false; } diff --git a/src/ccap_imp_windows_msmf.h b/src/ccap_imp_windows_msmf.h index 1e9eaf9b..24aaf12e 100644 --- a/src/ccap_imp_windows_msmf.h +++ b/src/ccap_imp_windows_msmf.h @@ -81,8 +81,6 @@ class ProviderMSMF : public ProviderImp { std::atomic m_shouldStop{ false }; std::atomic m_isRunning{ false }; bool m_isOpened{ false }; - bool m_didSetup{ false }; - bool m_didInitializeCom{ false }; bool m_mfInitialized{ false }; PixelFormat m_activePixelFormat = PixelFormat::Unknown; uint32_t m_activeWidth = 0; From 6722f0c6f8681c62b49604d6bd38232e74b96931 Mon Sep 17 00:00:00 2001 From: "wangyang (wysaid)" Date: Tue, 10 Mar 2026 02:21:32 +0800 Subject: [PATCH 10/11] Fix missing climits include and MinGW compatibility --- cli/ccap_cli_utils.cpp | 6 ++++++ examples/desktop/utils/helper.cpp | 1 + 2 files changed, 7 insertions(+) diff --git a/cli/ccap_cli_utils.cpp b/cli/ccap_cli_utils.cpp index b2f1ec1d..05a51814 100644 --- a/cli/ccap_cli_utils.cpp +++ b/cli/ccap_cli_utils.cpp @@ -58,6 +58,11 @@ constexpr const char* kWindowsBackendEnvVar = "CCAP_WINDOWS_BACKEND"; std::string getEnvironmentValue(const char* name) { #if defined(_WIN32) || defined(_WIN64) +#if defined(__MINGW32__) || defined(__MINGW64__) + // MinGW doesn't support _dupenv_s, use getenv instead + const char* rawValue = std::getenv(name); + return rawValue != nullptr ? std::string(rawValue) : std::string(); +#else char* rawValue = nullptr; size_t rawLength = 0; if (_dupenv_s(&rawValue, &rawLength, name) != 0 || rawValue == nullptr) { @@ -67,6 +72,7 @@ std::string getEnvironmentValue(const char* name) { std::string value(rawValue); std::free(rawValue); return value; +#endif #else const char* rawValue = std::getenv(name); return rawValue != nullptr ? std::string(rawValue) : std::string(); diff --git a/examples/desktop/utils/helper.cpp b/examples/desktop/utils/helper.cpp index a153a807..4b757371 100644 --- a/examples/desktop/utils/helper.cpp +++ b/examples/desktop/utils/helper.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include From 7d76a2acf33ca0695f66f87d3b4cee4293e059c5 Mon Sep 17 00:00:00 2001 From: "wangyang (wysaid)" Date: Tue, 10 Mar 2026 02:26:24 +0800 Subject: [PATCH 11/11] Fix MinGW compatibility in helper.cpp --- examples/desktop/utils/helper.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/desktop/utils/helper.cpp b/examples/desktop/utils/helper.cpp index 4b757371..59930374 100644 --- a/examples/desktop/utils/helper.cpp +++ b/examples/desktop/utils/helper.cpp @@ -91,6 +91,11 @@ int promptSelection(const char* prompt, int defaultValue, int maxValue) { std::string getEnvironmentValue(const char* name) { #if defined(_WIN32) || defined(_WIN64) +#if defined(__MINGW32__) || defined(__MINGW64__) + // MinGW doesn't支持 _dupenv_s,使用 getenv 替代 + const char* rawValue = std::getenv(name); + return rawValue != nullptr ? std::string(rawValue) : std::string(); +#else char* rawValue = nullptr; size_t rawLength = 0; if (_dupenv_s(&rawValue, &rawLength, name) != 0 || rawValue == nullptr) { @@ -100,6 +105,7 @@ std::string getEnvironmentValue(const char* name) { std::string value(rawValue); std::free(rawValue); return value; +#endif #else const char* rawValue = std::getenv(name); return rawValue != nullptr ? std::string(rawValue) : std::string();