
简介这是一份微软AMCap摄像头采集演示程序源码基于DirectShow框架实现音频与视频捕获面向需要开发摄像头预览、视频采集功能的Windows平台C开发者。源码在Visual Studio环境下已亲测编译通过可直接打开摄像头设备并实时显示画面适合初学者快速建立DirectShow采集流程的整体概念也适合后续扩展学习设备枚举、视频格式协商、预览渲染、Crossbar信号切换等核心模块。包内代码结构完整保留了官方示例的注释与声明有关设备通知注册、COM组件安全释放等实现都可以对照查看。压缩包共有96个文件大小约1.63MB以34个cpp源文件和38个h头文件为主体另有解决方案、工程配置、静态库、可执行程序及界面资源文件等基本覆盖从源码编译到运行调试的完整链条。目录划分清晰便于按功能模块查阅和复用。目前已有1410人学习下载对从事Windows多媒体编程、USB摄像头应用开发的技术人员来说是一份难得的官方级参考资料。1. 为什么“amcap source code源代码”值得当教材翻从一次摄像头采集翻车说起前些天调试一个 USB 摄像头采集程序预览窗口黑屏Graph 构建报错官方文档列了一堆 HRESULT翻到一半就想找现成代码对照。这时候最合适的参考就是 AMCap 的源代码——它是最经典的 Windows DirectShow 视频捕获示例设备枚举、预览、拍照、录像全串在一条主线上。项目标题虽然只写着“amcap source code源代码”但背后是一个完整可编译、可改造的采集框架。新手可以从头看懂过滤器图怎么搭熟手也能拿它当改写的底稿。想快速在 Windows 上做出“摄像头预览 抓帧保存”的功能直接啃 AMCap 源码比对着抽象文档自学省力得多。2. AMCap 源代码里的三条主线路捕获图、媒体控制与预览窗口2.1 先看代码地图AMCap 的项目结构为什么这么分不同版本的文件名有出入但大部分 AMCap 源码包都延续同一套逻辑。常见做法是核心文件拆成捕获、窗口、工具三个模块Capture.cpp 负责枚举视频设备、构建过滤器图、启动预览和停止流程Player.cpp 负责视频窗口的创建、绘制和缩放AmCap.cpp 主要是 WinMain、消息循环和菜单命令处理。还有一组公共头文件用来放全局 GUID、自定义消息和资源 ID。这种拆分很适合学习因为 DirectShow 编程天生分两层一层是 COM 过滤器图的构建另一层是窗口消息的交互。AMCap 把这两层分开后想改采集逻辑不用动 UI想换界面也不用碰过滤器图。很多入门者第一眼会被 DirectShow 的 COM 接口吓住但看 AMCap 的源码会发现它其实只用了十几个接口来回就那么几个函数。我一般建议拿到源代码之后先不要急着编译用文本搜索工具看三处搜索CoCreateInstance找入口搜索RenderStream找图连接搜索ISampleGrabberCB找帧回调。这三个关键词能串起整个程序的主干。2.2 设备枚举与过滤器图搭建为什么 AMCap 能自动发现摄像头AMCap 启动后第一件事是遍历系统里的视频输入设备这一步靠的是 DirectShow 的系统设备枚举器。枚举结果的每一项对应一个 Moniker再通过 Moniker 绑定设备名称。下面这段就是等价的最小枚举代码AMCap 源码里核心逻辑也长这样#include dshow.h #include atlbase.h std::vectorCString EnumerateVideoDevices() { std::vectorCString names; CComPtrICreateDevEnum pEnumBuilder; HRESULT hr CoCreateInstance( CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(pEnumBuilder)); if (FAILED(hr)) return names; CComPtrIEnumMoniker pMonikerEnum; hr pEnumBuilder-CreateClassEnumerator( CLSID_VideoInputDeviceCategory, pMonikerEnum, 0); if (hr S_FALSE) return names; // 没有设备时返回 S_FALSE CComPtrIMoniker pMoniker; while (pMonikerEnum-Next(1, pMoniker, NULL) S_OK) { CComPtrIPropertyBag pBag; pMoniker-BindToStorage(0, 0, IID_PPV_ARGS(pBag)); CComVariant varName; if (SUCCEEDED(pBag-Read(LFriendlyName, varName, NULL))) names.push_back(CString(varName.bstrVal)); pMonikerEnum-Reset(); } return names; }关键点是CreateClassEnumerator的返回值没有设备时返回S_FALSE而不是S_OK。很多人直接判断SUCCEEDED(hr)结果设备不存在时也往下走后面全崩。第二个坑是Next返回S_OK时调用方要负责Release用智能指针能省这个麻烦。FriendlyName是设备显示名实际驱动可能还提供DevicePath可以用来区分两个同名的摄像头。拿到设备后AMCap 会创建过滤器图把摄像头源过滤器加进图里再调用RenderStream自动选择视频渲染器。这里贴一下核心连接代码CComPtrIGraphBuilder pGraph; CComPtrICaptureGraphBuilder2 pCaptureBuilder; CComPtrIBaseFilter pCameraFilter; CComPtrIMediaControl pMediaControl; CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(pGraph)); pGraph-QueryInterface(IID_PPV_ARGS(pCaptureBuilder)); pCaptureBuilder-SetFiltergraph(pGraph); pGraph-AddFilter(pCameraFilter, LCamera); pCaptureBuilder-RenderStream( PIN_CATEGORY_PREVIEW, MEDIATYPE_Video, pCameraFilter, NULL, NULL); pGraph-QueryInterface(IID_PPV_ARGS(pMediaControl)); pMediaControl-Run();RenderStream是 AMCap 最值得抄的地方它内部根据 Pin 类别和主类型自动找路径预览用PIN_CATEGORY_PREVIEW录制用PIN_CATEGORY_CAPTURE中间接 Sample Grabber 时要把PIN_CATEGORY_CAPTURE上的流分叉到两个目标上。参数里NULL代表让系统选择中间过滤器比如从摄像头源到输出之间自动加颜色空间转换器。这种写法比手动枚举 Pin 再Connect省了很多代码前提是你只做标准化场景。遇到特殊硬件时RenderStream会失败这时候再退回手动连接。2.3 预览窗口不是“贴个控件”而是把视频渲染器挂到你的窗口上AMCap 的老版本常用窗口模式 Video Renderer做法是先拿到IVideoWindow接口把渲染器的窗口作为子窗口嵌入自己的窗口。新版本开始用 VMR9 Windowless 或 EVR用IVMRWindowlessControl9把视频画到指定 DC 上。窗口化方案简单适合新手阅读。CComPtrIVideoWindow pVideoWindow; pGraph-QueryInterface(IID_PPV_ARGS(pVideoWindow)); pVideoWindow-put_Owner((OAHWND)m_hWnd); pVideoWindow-put_WindowStyle(WS_CHILD | WS_CLIPSIBLINGS); pVideoWindow-SetWindowPosition(0, 0, m_iWidth, m_iHeight);put_Owner把视频渲染器的窗口所有权交给你的窗口WS_CLIPSIBLINGS防止视频窗口把系统菜单盖住。SetWindowPosition里三个参数分别是 x、y、宽度和高度注意拉伸视频不会改变源分辨率颜色空间转换器会自动处理缩放。如果画面比例不对通常是渲染器保持源比例的设置没打开可以在put_AutoShow(TRUE)之后用IVMRWindowlessControl的SetAspectRatioMode控制。3. 把 AMCap 源代码编译成 exe环境准备与最小构建命令3.1 先确认版本再选工具链MFC 版和 SDK 版的编译差异AMCap 源码流传的版本不少有的基于旧版 DirectX SDK有的已经迁移到纯 Windows SDK。拿到压缩包后先看文件后缀.sln、.vcproj、.vcxproj直接双击用 Visual Studio 打开如果只有 makefile 或一堆.cpp就用命令行编译。还有一类版本用 MFC 对话框做界面这种需要在 VS 里安装 MFC 组件否则打开工程会提示找不到 afxwin.h。我自己的习惯是先看stdafx.h或pch.h里面写着#include streams.h的多半是 DirectShow 基础类库版本对 Windows SDK 版本很敏感。streams.h是 DirectShow 的基类库需要额外的Strmbase.lib或Strmiids.lib。现代 Windows SDK 里已经不默认提供 base class 源码得从旧版 DirectX SDK 里找这是编译 AMCap 最常见的坎。3.2 用命令行完成一次最小构建批处理里的关键参数假设你拿到的是纯 SDK 版本没有 MFC 依赖最省事的方式是打开 Visual Studio 的开发者命令提示符然后跑一段批处理。下面是一个可用模板echo off call C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat set SDK_UM%WindowsSdkDir%Include\10.0.22621.0\um set SDK_SHARED%WindowsSdkDir%Include\10.0.22621.0\shared set SDK_HKCM%WindowsSdkDir%Include\10.0.22621.0\winrt cl.exe /nologo /EHsc /W3 /MT ^ /I %SDK_UM% /I %SDK_SHARED% /I %SDK_HKCM% ^ src\AmCap.cpp src\Capture.cpp src\Player.cpp ^ src\StdAfx.cpp ^ /Fe:AmCap.exe ^ /link strmiids.lib ole32.lib oleaut32.lib ^ uuid.lib user32.lib gdi32.lib winmm.lib/MT表示静态链接运行时库生成结果换到别的机器不会因为缺 VC 运行库而启动失败。/EHsc开启 C 异常DirectShow 代码本身不用异常但有些封装会用。/I指定的三个目录来自vcvars64.bat设置的环境变量um目录放 dshow.h 和 strmif.hshared目录放 GUID 定义winrt目录有时被 DirectShow 头文件间接引用。/link后面这些库缺一不可strmiids.lib提供CLSID_FilterGraph等 GUIDuuid.lib提供 COM 基础 GUIDole32提供 CoCreateInstance。如果你的源码包里有.rc资源文件批处理要先调用rc.exe把资源编译成.res再让link.exe把它吃进去。否则菜单和图标会丢失程序虽然能跑但界面是空的。3.3 用 Visual Studio 工程时要注意的三处兼容性设置很多 AMCap 版本拿过来直接打开会报错原因集中在三处。一是平台工具集太老默认 v100 而本机只有 v143。在项目属性里把“平台工具集”改成Visual Studio 2022 (v143)一般就能过编译如果源码用了过时的for循环变量语法还要在“语言”里把 C 标准设为 ISO C14 或更旧。二是字符集不匹配。老代码经常出现char*字符串而新版项目默认 Unicode。最保险的办法是把项目属性里的“字符集”改成“使用多字节字符集”而不是大范围改代码。三是 Windows SDK 版本。DirectShow 的头文件从 Win10 SDK 开始已经收缩但基本接口都还在。如果编译时遇到找不到dshow.h检查“VC 目录”里的“包含目录”是否真的指向了 SDK 的um和shared子目录。这步错最常见因为 VS 的默认包含路径并不总覆盖 DirectShow 专用目录。4. 从 AMCap 源代码里换出自己的拍照功能取帧回调到 BMP 落盘4.1 回调线程不是 UI 线程Sample Grabber 的正确用法AMCap 要做拍照不能直接抓渲染器画面而是要在数据流路上放一个 Sample Grabber让它把每一帧拷贝一份到你手里。Sample Grabber 有两种回调方式SampleCB给你IMediaSample指针BufferCB给你BYTE*缓冲区和长度。AMCap 通常把 SampleGrabber 放在摄像头源过滤器之后设置SetBufferSamples(TRUE)保证缓冲区一直有效。CComPtrIBaseFilter pGrabberBase; pGrabber.CoCreateInstance(CLSID_SampleGrabber, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(pGrabberBase)); pGrabberBase-QueryInterface(IID_PPV_ARGS(pGrabber)); CComPtrIMediaType pMediaType; pMediaType.p NULL; pMediaType-majortype MEDIATYPE_Video; pMediaType-subtype MEDIASUBTYPE_RGB24; pGrabber-SetMediaType(pMediaType); pGrabber-SetBufferSamples(TRUE); pGrabber-SetOneShot(FALSE);这里subtype设成MEDIASUBTYPE_RGB24表示你希望收到 24 位色深、按 BGR 顺序排列的数据。并不是所有摄像头都直接输出 RGB24DirectShow 会自动插入转换器满足这个要求。SetOneShot(FALSE)让回调持续触发如果改成TRUE只收到一帧后过滤器图就停流适合拍摄单帧快照。回调接口只用实现BufferCB就够了方法里收到的参数包括采样时间和帧数据指针class CSampleCallback : public ISampleGrabberCB { public: STDMETHODIMP_(ULONG) AddRef() { return 1; } STDMETHODIMP_(ULONG) Release() { return 1; } STDMETHODIMP QueryInterface(REFIID riid, void** ppv) { if (riid IID_ISampleGrabberCB || riid IID_IUnknown) { *ppv static_castISampleGrabberCB*(this); return S_OK; } return E_NOINTERFACE; } STDMETHODIMP SampleCB(double Time, IMediaSample *pSample) { return E_NOTIMPL; } STDMETHODIMP BufferCB(double Time, BYTE* pBuf, long Len) { if (pBuf Len 0) { // 关键这里不能弹窗、不能写磁盘只能浅拷贝 memcpy(m_canvas, pBuf, Len); } return S_OK; } };回调里最忌讳的是做耗时操作。AMCap 的源码注释里会反复强调“回调运行在流线程上”一旦阻塞整个视频管线就会卡住画面直接冻结。正确做法是拷贝到一个预先申请好的缓冲区再PostMessage通知 UI 线程去写文件或显示。4.2 把 RGB24 帧写成 BMP别忘位图文件头和行对齐拿到pBuf之后保存为 BMP 文件是 AMCap 时代最经典的验证手段。写 BMP 有两个细节一是文件头加信息头一共 54 字节二是每行字节数必须按 4 字节对齐。RGB24 格式下原始像素行宽度等于width * 3但文件里每行必须是 4 的倍数不够要补齐。bool SaveRgb24AsBmp(BYTE* pBuf, int width, int height, const wchar_t* path) { int stride (width * 3 3) ~3; DWORD imgSize stride * height; BITMAPFILEHEADER bf { 0 }; bf.bfType 0x4D42; // BM bf.bfSize sizeof(bf) sizeof(BITMAPINFOHEADER) imgSize; bf.bfOffBits sizeof(bf) sizeof(BITMAPINFOHEADER); BITMAPINFOHEADER bi { 0 }; bi.biSize sizeof(bi); bi.biWidth width; bi.biHeight height; // 正数表示自下而上 bi.biPlanes 1; bi.biBitCount 24; bi.biSizeImage imgSize; HANDLE hFile CreateFileW(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile INVALID_HANDLE_VALUE) return false; DWORD written 0; WriteFile(hFile, bf, sizeof(bf), written, NULL); WriteFile(hFile, bi, sizeof(bi), written, NULL); for (int y 0; y height; y) { WriteFile(hFile, pBuf y * width * 3, width * 3, written, NULL); if (stride width * 3) { BYTE pad[4] { 0 }; WriteFile(hFile, pad, stride - width * 3, written, NULL); } } CloseHandle(hFile); return true; }这里最容易被绕进去的参数是biHeight。大多数摄像头输出的数据是自下而上排列biHeight写正数时BMP 读取器会按倒序排列显示出来就是正的如果发现图像上下颠倒把biHeight改成负数即可。stride的计算公式(width * 3 3) ~3就是位图行对齐的标准写法不要把width * 3直接当长度写入文件。5. AMCap 源码调试避坑5 个高频翻车现场与对应处理5.1 编译找不到strmiids.lib链接器直接报 LNK1104现象是编译已经生成一堆.obj最后链接时报错LNK1104 cannot open file strmiids.lib。原因很直接strmiids.lib不是默认库需要手动把它加进链接输入。AMCap 老工程自带这个依赖但迁移到新解决方案时工程文件经常丢。另一种原因是把 DirectShow 和 DirectX SDK 搞混装了一堆带版本的 DXSDK却没想到 DirectShow 的库就藏在 Windows SDK 的lib目录里。解决方法是打开项目属性链接器 - 输入 - 附加依赖项填入strmiids.lib如果你不想改工程也可以把源码里的#pragma comment(lib, strmiids.lib)加在包含 DirectShow 头文件之后。注意 64 位工程要确认 SDK 库路径指向x64子目录否则还是会报同样的错误。5.2 拍照按钮一按整个程序画面冻结现象是程序预览正常按钮点击保存 BMP 后窗口变成“未响应”或者画面停在一帧上不刷新。原因是把文件写入直接放进了 Sample Grabber 回调里。BufferCB运行在流线程上写文件哪怕只有几十毫秒也会拖住整个过滤器图的数据流动。解决方法是回调里只做两件事拷贝帧数据到自己的缓冲区、PostMessage给 UI 窗口。文件保存放在 WM_USER 自定义消息的处理函数里这样文件读写不会阻塞流线程。另外要在回调对象里用临界区或std::mutex保护缓冲区否则 UI 线程读数据时回调线程正在写会得到半个帧。5.3 第二次打开程序提示设备被占用或初始化失败现象是第一次运行正常退出后再启动摄像头状态灯不亮程序报 VFW_E_INVALIDMEDIATYPE 或返回失败必须重启机器才行。原因是你退出程序时没有真正停止过滤器图。IMediaControl::Stop()没调用或者图里的过滤器引用计数没归零驱动还认为设备在流里。某些摄像头驱动设计得更蛮横进程退出后设备还会被占用几秒到几十秒。解决方法是做一个干净的清理顺序先pMediaControl-Stop()然后枚举图里的所有过滤器并断开 Pin 连接先释放 Sample Grabber再释放渲染器最后CoUninitialize()。如果这个流程太长至少保证程序退出前调用Stop()和pGraph-RemoveFilter。遇到顽固的设备可以在清理后Sleep(200)再允许下一次初始化。5.4 64 位系统上摄像头画面出不来但 32 位程序正常现象是同一个 AMCap 源码编译成 x86 版本能出画面编译成 x64 版本黑屏或枚举不到设备。原因是 DirectShow 过滤器与进程位数必须一致。你的程序是 64 位就只能在 64 位进程中加载 64 位过滤器如果摄像头的驱动只提供了 32 位包那么 64 位程序里根本看不到它。还有一类老摄像头自带解码过滤器只编译了 32 位版本同样不支持 64 位进程。解决方法是先看设备管理器里驱动的位数。如果驱动没有 64 位版老老实实把输出平台切回 x86。反过来如果摄像头原生支持 UVC那么它微软自带驱动不区分位数两个平台都能用。不要在这一点上跟系统较劲这属于硬件驱动的边界不是代码 bug。5.5 源码里大量“无法从 char* 转换为 CString”现象是编译时报错成堆集中在字符串相关的函数调用上。原因是 AMCap 年代久远很多版本用的是 ANSI 编码和CStringA而现代 Visual Studio 默认项目字符集是 Unicode。CString在 Unicode 工程里相当于CStringW窄字符指针不能直接隐式转换。解决方法是项目属性 - 常规 - 字符集改为“使用多字节字符集”。如果按钮、菜单等资源在某些语言下显示乱码再考虑改用CStringW并把字面量都加上L。这个选择属于编译期策略不影响 DirectShow 的接口逻辑但能省下大量改写工作量。6. 从 AMCap 源代码里带走的三个自检技巧6.1 给所有 HRESULT 加日志宏排查黑匣子不再靠猜DirectShow 的报错经常只有一个hr光看值根本不知道哪一步断了。我后来养成一个习惯把所有关键调用包一层宏失败时把文件、函数、行号和错误值一起打出来。#define CHECK_HR(expr) do { \ HRESULT _hr (expr); \ if (FAILED(_hr)) { \ printf([%s:%d] %s 0x%08X\n, \ __FILE__, __LINE__, #expr, _hr); \ } \ } while (0)这个宏不会改变程序行为只负责在失败时暴露位置。排查 AMCap 类似的代码时在哪一句RenderStream断掉一目了然。等程序稳定后再关掉打印不用删代码。6.2 把过滤器图导出成.grf文件用 TopoEdit 验证 Pin 连接AMCap 源码里你可以通过IFilterGraph::SaveToFile或远程图查看器把运行时的图结构存下来再用 TopoEdit 打开。调试暗病时这个手段特别管用。CComPtrIStorage pStorage; DWORD flags STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE | STGM_DIRECT; HRESULT hr StgCreateDocfile(Ldebug.grf, flags, 0, pStorage); if (SUCCEEDED(hr)) { pGraph-Save(pStorage, TRUE); pStorage-Release(); }生成.grf后用 TopoEdit 打开能看到过滤器、Pin、渲染器之间的连接关系。如果某条路径上的颜色转换器多了一到两个说明源格式和请求格式不匹配如果渲染器输入端没有连接说明RenderStream的 Pin 类别选错了。6.3 每改一次采集逻辑先做一次“一帧自检”最后一个习惯来自 AMCap 这类程序的天然风险代码能编译不代表画面正常。我最常做的快速验证是只拍一帧保存成 BMP 后检查三点文件头大小是不是 54 字节、画面是否颠倒、颜色通道是否被反转。这一帧测试能同时验证设备格式、转换器行为和回调线程工作正常。后来我把这段检查固化成一个独立函数每次调完参数都会先跑一遍确认无误后再切换到预览模式。时间久了你会发现DirectShow 很多报错是玄学但只要每一步都有稳定的自检手段真正难缠的其实就那么几个。希望这些方法也能帮到你。本文还有配套的精品资源点击获取