基于Windows Media Foundation的高性能USB摄像头采集框架解析

510 阅读5分钟

关键词:C++、WIN、USB、OPENCV、MF、NV12、MJPEG、YUY2、H264 github:github.com/kivenyangmi…

一. 引言

  今日闲暇之余,决定将去年烂尾的开发捡起来。这个项目文件主要是针对WIN系统中模仿Linux的V4L2获取USB相机,灵感来自微软公司的开源项目,我对其进行魔改。当然了还有我的两大得力助手,一个是deepseek另一个是chatGTP。这个项目可能没有什么实际意义,用来炫技或许还有点价值,因为你都选择WIN操作系统了,也就不在乎那点内存占用以及性能的问题了。

一、代码核心功能

  由于也是参考的微软的代码,对于win32的内容本人也是不怎么精通,大部分功劳还是二位助手帮我修改,我进行调试反馈以及完善。这里主要是分为如下的四个大模块:

  1. 设备枚举
    通过 EnumerateVideoDevices 函数,利用 MF 的 MFEnumDeviceSources API 枚举系统中所有可用的视频捕获设备(如 USB 摄像头),并将设备列表存储在 devices 容器中。
  2. 媒体源配置
    ConfigureSourceReader 函数用于配置 IMFSourceReader,获取设备的原生视频格式(如分辨率、像素格式),并设置视频流的输出参数。
  3. 视频帧格式转换
    pData2Mat 函数将不同像素格式(如 YUY2、NV12、MJPEG)的视频帧数据转换为 OpenCV 的 cv::Mat 格式,支持实时显示。
  4. 主流程
    初始化 COM 和 MF 库,选择第一个摄像头设备,读取视频帧并循环显示。

二、关键代码解析

1. 设备枚举与初始化

  在这里就是检查PC端是否存在USB相机以及存在几个USB相机,相关代码是借鉴(抄袭)微软的。

// 检测是否存在可用的摄像头 枚举所有视频捕获设备(USB 摄像头)
HRESULT EnumerateVideoDevices(std::vector<IMFActivate*>& devices) {
    IMFAttributes* pAttrs = nullptr;
    hr = MFCreateAttributes(&pAttrs, 1);
    if (FAILED(hr)) return hr;

    hr = pAttrs->SetGUID(
        MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
        MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID
    );
    if (FAILED(hr)) { pAttrs->Release(); return hr; }

    IMFActivate** ppDevices = nullptr;
    UINT32 count = 0;
    hr = MFEnumDeviceSources(pAttrs, &ppDevices, &count);
    if (SUCCEEDED(hr)) {
        for (UINT32 i = 0; i < count; i++) {
            devices.push_back(ppDevices[i]);
        }
        CoTaskMemFree(ppDevices);
    }
    pAttrs->Release();
    return hr;
}
2. 配置视频流格式

  这里需要注意的是USB相机可能存在NV12、 mjpeg、yuy2、h264输入格式,前两个比较常见,yuy2在我的双目相机中发现了,h264在我的大疆相机中发现了。不同的输入流格式对应的不同的编号可供选择,这样再后续的获取图像指针数据的时候便于解码。

// 配置 SourceReader,并获取分辨率
HRESULT ConfigureSourceReader(IMFSourceReader* pReader, int index_pix, uint32_t& width, uint32_t& height) {
    // —— 枚举原生类型 —— 
    printf("枚举原生媒体类型:\n");
     for (DWORD i = 0; ; ++i) {
        IMFMediaType* pNative = nullptr;
        hr = pReader->GetNativeMediaType(
            static_cast<DWORD>(MF_SOURCE_READER_FIRST_VIDEO_STREAM), i, &pNative
        );
        if (FAILED(hr)) {
            printf("  [%u] —— 无更多类型 (hr=0x%08X)\n", i, hr);
            break;
        }
        UINT32 nativeWidth = 0, nativeHeight = 0;
        if (SUCCEEDED(MFGetAttributeSize(pNative, MF_MT_FRAME_SIZE, &nativeWidth, &nativeHeight))) {
            printf("  [%u] 分辨率: %dx%d\n", i, nativeWidth, nativeHeight);
        }
        GUID subType = { 0 };
        pNative->GetGUID(MF_MT_SUBTYPE, &subType);
        WCHAR szGuid[64] = { 0 };
        StringFromGUID2(subType, szGuid, ARRAYSIZE(szGuid));
        wprintf(L"  [%u] subtype = %s\n", i, szGuid);
        pNative->Release();
    }
    // 获取设备原生支持的媒体类型
    IMFMediaType* pNativeType = nullptr;
    hr = pReader->GetNativeMediaType(static_cast<DWORD>(MF_SOURCE_READER_FIRST_VIDEO_STREAM), index_pix, &pNativeType);
    if (FAILED(hr)) {
        printf("获取设备原生支持的媒体类型失败\n");
        return hr;
    }
    // 直接使用原生格式配置
    hr = pReader->SetCurrentMediaType(static_cast<DWORD>(MF_SOURCE_READER_FIRST_VIDEO_STREAM), nullptr, pNativeType);
    // 获取对应视频流格式的尺寸width, height 修改width, &height参数地址上的值
    MFGetAttributeSize(pNativeType, MF_MT_FRAME_SIZE, &width, &height);
    pNativeType->Release();
    return hr;
}

3. 帧采集模块

  关于初始化和释放相关变量这里我省去,核心是获取USB数据部分,这里为了避免一些输入和函数过程中的错误,编写了大量屎山般的if进行判断(原谅如此粗俗的话术)进行返回false。其中核心模块是将byte指针转为char指针。无论是NV12格式的数据以及mjpeg数据的格式似乎都是char,但转uchar的话需要解码。   亮点如下:

  • 使用CComPtr自动管理COM对象生命周期
  • 严格的缓冲区边界检查
  • 安全内存拷贝函数防止溢出
bool get_pix(char** nv12prt, int bufferSize)
{
    // 参数有效性检查
    if (!nv12prt || !*nv12prt) {
        printf("错误:输入指针无效\n");
        return false;
    }

    DWORD streamIndex = 0, flags = 0;
    LONGLONG timestamp = 0;
    CComPtr<IMFSample> pSample = nullptr;
    hr = pReader->ReadSample(static_cast<DWORD>(MF_SOURCE_READER_FIRST_VIDEO_STREAM), 0, &streamIndex, &flags, &timestamp, &pSample);
    if (FAILED(hr) || (flags & MF_SOURCE_READERF_ENDOFSTREAM)) {
        printf("USB数据帧获取为空\n");
        return false;
    }
    if (!pSample) return false;
    CComPtr<IMFMediaBuffer> pBuffer = nullptr;
    hr = pSample->ConvertToContiguousBuffer(&pBuffer);
    if (FAILED(hr)) return false;
    BYTE* pData = nullptr; // 原始数据指针
    DWORD maxLen = 0, currLen = 0;
    // 添加缓冲区锁定保护
    if (FAILED(pBuffer->Lock(&pData, &maxLen, &currLen))) return false;
    // 添家缓冲区大小验证
    if (currLen > static_cast<DWORD>(bufferSize)) {
        printf("缓冲区溢出!需要%u字节,仅提供%u字节\n", currLen, bufferSize);
        pBuffer->Unlock();
        return false;
    }
    // 安全内存复制
    if (memcpy_s(nv12prt, bufferSize, pData, currLen) != 0) {
        printf("内存复制失败\n");
        pBuffer->Unlock();
        return false;
    }
    pBuffer->Unlock();
    return true;
}

结余

  五一假期即将来临,赶在放假前将该项目代码修改完成,于是趁着快下班了编写了这篇博客,在博客中必然存在不够完美的地方,还望各位大佬及时批评指正,编写该文供大家飨食。祝大家节日愉快!