简介:基于VS2015、Qt与DirectShow的Windows 10多摄像头采集录像示例,演示了如何同时打开多个USB摄像头,将多路采集画面实时显示在Qt窗口上,并支持视频录制;录制时每隔30秒自动生成一个新视频文件,可有效降低长时间录制过程中因断电、程序异常或拔插摄像头造成整段录像损坏的风险。整个压缩包共53个文件,主要包含C++源码和头文件、Qt界面设计文件(ui与qrc)、VS工程配置文件(sln、vcxproj、props)、编译日志、中间obj文件以及可执行exe、调试pdb等,包体约20.41MB,目录结构清楚,可以快速找到摄像头管理、界面显示和录像保存等核心模块。已有1588人学习下载,适合正在学习DirectShow采集、或者希望在Windows上搭建视频监控原型的开发者。通过学习这份示例工程,读者能掌握DirectShow设备枚举与多路打开方式、Qt显示实时视频流的常见写法、按固定时长分段保存文件的实现思路,并了解VS2015中Qt工程的编译链接方式,稍加修改即可扩展到更多摄像头或融入现有监控系统。 老同学前几天找我,说他手上有一个在Win10系统、VS2015环境下用Qt框架配合DirectShow开发的多摄像头采集项目,需求是在同一界面中同时打开多个USB摄像头并把画面录制下来,遇到了不少问题来找我聊天。我听完他的描述不由得笑了——这不是我在工业视觉项目里反复折腾过的老套路吗。虽然现在市面上流行的已经是Qt6搭配V4L2或者Media Foundation的新玩法,但在老设备、老代码、老团队的项目里,Win10 + VS2015 + Qt + DirectShow这套组合仍然是很多实际项目的绝对主力配置。
这篇博文就把这套方案的完整落地流程写清楚,包括环境搭建的坑、采集框架选型逻辑、多路摄像头的线程模型、录制方案对比,以及打包发布时的注意点。不绕弯子,直接按实操顺序写,给正准备接手类似项目的同行一个能直接抄作业的参考。
1. 项目需求拆解与技术选型思路
1.1 为什么还在用VS2015和DirectShow
很多人看到VS2015和DirectShow,第一反应是“这玩意儿还能用吗”。我的答案是不仅能用,而且用得好好的。
在工业自动化、实验室设备、简易监控类的场景中,大量上位机程序是2016年到2019年之间写的,当时团队的开发环境就是VS2015 + Qt 5.x,采集方案选了DirectShow。原因很简单:DirectShow是Windows系统自带的组件,不需要额外安装驱动SDK,摄像头支持范围广,老旧的USB工业相机、普通UVC摄像头都能兼容。相比Media Foundation,DirectShow的资料多、代码范本多、问题排查思路成熟,遇到坑抱着老代码还能救一救。
如果你接手的是这种老项目,最忌讳的是“反正要改,不如换新框架”。硬件驱动、采集卡固件、Camera SDK都可能依赖特定的DirectShow Filter,强行迁移到其他采集框架,往往要把整套底层协议都重写一遍,成本完全失控。这套方案的核心思路是“缝缝补补”地让它跑起来,而不是推翻重来。
1.2 Qt版本与VS编译器的匹配逻辑
Qt在Windows下有两种常规编译路线:MinGW和MSVC。VS2015项目必须使用MSVC编译的Qt库,因为VS环境的运行时代码和ABI接口与MinGW不兼容,混用会导致链接阶段一堆莫名其妙的LNK错误。
很多新手在这块栽跟头,是从Qt官网下载了一个带MinGW的安装包,装完后VS2015里却怎么都配置不了,甚至提示“Unknown Qt version”。原因是Qt库的编译器类型没对上。
正确做法是安装Qt时勾选msvc2015_64或者msvc2015_32(取决于工程是x64还是x86),然后在VS里进行配置。这里有一个历史经验:Qt 5.15.2是支持VS2015的最后一个较好选择,再新的Qt版本虽然也能用,但很多功能开始默认依赖更高版本的编译器特性。老项目要的是稳定,我用的是Qt 5.12.10 msvc2015_64,实测整个开发周期没有出现过编译器不兼容的问题。
如果你手里有老项目的代码,千万要确认版本对应关系。Qt库位数、VS工程平台位数、摄像头SDK位数这三个必须全部一致,否则在程序运行到COM组件交互时就会突然崩溃,而且崩溃位置很难从调用栈里看出来。
1.3 采集框架选型:DirectShow、V4L2还是Media Foundation
从“多路USB摄像头并发采集”这个需求出发,可选框架有多个,这里给大家做一个直接对比:
| 对比项 | DirectShow | Media Foundation | V4L2 |
|---|---|---|---|
| 系统适用 | Windows全系列 | Windows 7及以上 | Linux |
| 设备兼容度 | 高,老设备支持好 | 高,新硬件支持好 | 高,Linux下是标准 |
| 多路采集并发 | 手动管理Graph,灵活 | 自带拓扑模型 | 文件节点方式 |
| 编码录制库 | 需配套Encoder Filter | 自带编码能力 | 需配套GStreamer等 |
| 老项目复用 | 极高 | 需重写采集逻辑 | 完全重写跨平台 |
这个项目需要打开多个USB摄像头并录制视频,录像是重头需求,DirectShow的优势在于配套的Encoder Filter(比如H.264、MJPEG编码器)在Windows下非常多,而且Filter Graph模型天然支持一张图里挂多个采集设备,逻辑上会清晰很多。
实际项目里我建议直接用DirectShow完成采集,编码录制用FFmpeg完成。DirectShow只管拿到YUV或MJPEG的原始帧,FFmpeg负责封装成MP4文件。这样做的好处是把“采集”和“编码”解耦,后续如果换系统或者换需求,只替换其中一个环节即可。如果非要用DirectShow一路打通到MP4录制,会遇到编码器兼容问题,不同编码Filter之间匹配性很差,折腾起来非常耗时。
2. 开发环境搭建与初始化避坑
2.1 在Win10下安装VS2015的常见问题
Win10系统下安装VS2015的问题确实不少,最典型的是安装时卡在“正在准备安装”这一步,原因多半是旧版补丁冲突或者Windows组件缺了。我的处理方式比较直接:先到系统设置里把“开发人员模式”打开,再用管理员身份运行安装程序,如果卡住就清理临时目录重启后再装。
VS2015安装完成后,需要确认C++工具集是否正确。打开Visual Studio Installer,看是否勾选了“Windows 8.1 SDK”和“Visual C++工具集”,这两个是编译DirectShow和Qt项目的关键。如果没装,后面编译会报大量关于strmbase.h、dshow.h找不到的致命错误。
一个重要的坑是Win10会对VS2015的调试器权限做限制,尤其是进行DirectShow开发时,有时候运行到COM接口调用时突然退出,但断点根本打不到。我后面采用了一个土办法:调试时把程序目录直接放到C盘根目录下,避开UAC和权限问题,虽然操作上难看了点,但确实能少掉很多莫名其妙的灵动。
2.2 Qt插件初始化失败的根治方法
老Qt项目在Win10上跑起来最容易见到的错误字眼是“windows no qt platform plugin could be initialized. reinstalling the applicat”,这个报错看着吓人,实际上就是指不到platforms目录下的qwindows.dll。
排查顺序我建议是:
- 先确认编译环境运行目录下有没有platforms文件夹,里面有没有qwindows.dll。
- 确认platforms文件夹的架构位数跟当前程序一致。不同位数混用会在运行时直接报这个错误。
- 确认所有Qt相关DLL是否放在应用程序exe同目录下,重复放置多个版本极容易导致误加载。
如果你用windeployqt工具打包,这个错误通常不会出现。具体命令为:
windeployqt.exe yourApp.exe这个命令会自动找出exe依赖的所有Qt模块,并把插件、翻译文件、运行库都拷贝到exe所在目录。不过要注意,windeployqt并不能帮你把DirectShow相关的自定义Filter也拷贝出来,这部分要自己手写部署脚本。
2.3 COM组件的初始化时机
DirectShow是建立在COM组件之上的。很多新人写了采集代码,打开设备时没问题,但录制视频一段时间后崩溃,根本原因是没有在正确的线程里初始化COM,或者多次初始化释放不配对。
我们项目里规定:每个使用DirectShow的线程,在创建Filter Graph之前必须调用一次:
CoInitializeEx(NULL, COINIT_MULTITHREADED);线程结束前再对应调用:
CoUninitialize();这一点很容易被忽略,因为单线程Demo跑起来没问题,一旦开了多路摄像头,每个采集线程都有自己的COM模型时,缺少初始化就会出现随机性崩溃。
3. 多摄像头采集的整体设计与线程模型
3.1 音频“一设备一线程”的设计取舍
多路USB摄像头并发采集,第一反应往往是在一个线程里循环打开、采集、显示。实测下来不推荐,原因有两个:
- USB摄像头读取是阻塞式操作,一路摄像头卡顿会拖慢其他摄像头的读取节奏。
- 录制视频时,编码器处理耗时较长,如果全塞一个线程,界面会严重掉帧。
推荐方案是“一设备一线程”加“帧缓冲队列”。每个摄像头创建独立的采集线程,线程内的Filter Graph只负责采集原始帧,然后把帧丢进一个大小有限的环形队列中,录制线程从队列里取帧编码。
这里有个取舍:线程数等于摄像头数再加上一个合成/录制线程,开4路摄像头就是5个线程,对于现代CPU来说压力很小。关键是每路采集线程要绑定到独立设备,不要在采集逻辑里做任何视频编码操作,采集线程的循环里只做读帧和拷贝。
3.2 帧缓冲队列与丢帧策略
多路视频录制时,如果编码跟不上采集速度,缓冲区就会无限增长,最终内存爆炸。合理的办法是给每路视频队列设定固定容量,比如60帧(约2秒),一旦队列已满,就直接丢弃新帧,而不是阻塞采集线程。
这个策略我称之为“丢帧保实时”,在工业监控和实验记录场景下非常合理。保留最近的数据,丢掉过期数据,比整条链路卡死要安全得多。代码层面用Qt的QQueue加QMutex即可实现,不需要引入额外依赖。
但要注意,丢帧策略不能盲目用到“录制模式”。如果项目要求完整无损录制每一帧,就需要把采集帧率和编码能力做匹配,比如摄像头采集设定在25fps,编码采用高质量预设保证每秒能处理30帧以上,留出富余量,不能压着极限跑。
3.3 界面刷新与采集线程的分离
Qt界面上的视频显示必须位于主线程(GUI线程),而采集发生在子线程,这种场景需要跨线程更新画面。最稳妥的方式是用Qt信号槽机制:采集线程在拿到新帧后发射一个携带QImage指针的信号,主线程槽函数负责更新界面。
信号槽默认是AutoConnection,跨线程时自动变成QueuedConnection,优化效率足够。这里有个性能细节:不要在信号里传递大体积的QImage值,要用指针或者共享指针,否则会反复拷贝图像数据,多路视频打开后内存和CPU占用率会显著上升。
实测参数供参考:4路720P摄像头,每帧约2MB原始数据,如果用值传递信号,CPU占用率达到25%以上;改为指针传递后,CPU占用率稳定在8%-10%左右。这个性能差相当可观。
4. DirectShow采集核心实现细节
4.1 枚举系统中的多个USB摄像头
DirectShow操作的第一步是枚举系统中所有视频输入设备。一般人不注意的是,枚举结果里包含了很多没有被物理连接但驱动存在的虚拟设备,所以枚举后需要做过滤。
核心API思路是使用ICreateDevEnum和IEnumMoniker遍历设备集合。代码如下,这段代码可以拿到所有视频输入设备的友好名称:
ICreateDevEnum* pCreateDevEnum = NULL; IEnumMoniker* pEnumMoniker = NULL; CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void**)&pCreateDevEnum); pCreateDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pEnumMoniker, 0); IMoniker* pMoniker = NULL; while (pEnumMoniker->Next(1, &pMoniker, NULL) == S_OK) { IPropertyBag* pPropBag = NULL; pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void**)&pPropBag); VARIANT varName; VariantInit(&varName); pPropBag->Read(L"FriendlyName", &varName, 0); // 这里是设备的显示名称,例如 "USB Camera" // 保存pMoniker以用于后续创建Filter VariantClear(&varName); pPropBag->Release(); pMoniker->Release(); }这段代码的完整度不足以直接用到生产环境,但核心枚举逻辑就这样。拿到pMoniker后,调用pMoniker->BindToObject可以创建出对应的DirectShow Filter,再添加到Filter Graph中。
经验提醒:多路摄像头的“设备号”一定要用pMoniker的持久标识符,而不是简单的索引0、1、2。USB设备在系统唤醒后顺序可能发生改变,如果按索引写死,很容易出现画面错乱,A摄像头显示成B的内容。
4.2 建立Filter Graph并绑定设备到渲染器
每路摄像头独立建立一个Filter Graph,Graph内部至少包含三个组件:
- 采集Filter(来自设备枚举结果)
- Sample Grabber(抓取帧数据)
- Null Renderer或Smart Tee(让数据流通起来)
Sample Grabber是DirectShow里最常用的“偷帧”组件,它可以在数据流经时回调到我们的代码里。配置Sample Grabber需要设置媒体类型,我用的是RGB24格式,这样可以省略掉格式转换,回调里直接得到QImage可用的像素数据。
建立Graph的典型调用顺序:
- CoCreateInstance(CLSID_FilterGraph)
- 通过QueryInterface拿到IGraphBuilder
- 绑定摄像头Filter并AddFilter到Graph
- 创建Sample Grabber Filter并AddFilter
- 连接采集Filter的Output Pin到Sample Grabber的Input Pin
- 连接Sample Grabber的Output Pin到Null Renderer
- 调用IMediaControl的Run方法启动采集
这里有一个最常见的坑:很多摄像头默认输出格式是MJPEG或NV12,Sample Grabber需要能处理压缩格式或者做转换。我在代码里选择让Sample Grabber主动请求RGB24,DirectShow会尝试在内部添加Color Converter Filter。如果连接失败,多半是摄像头的输出格式列表里直接不支持RGB24,这种时候Safe要让Sample Grabber接收原始格式,然后在回调里用算法转换。
4.3 视频回调中如何提高处理效率
Sample Grabber回调函数里,时间非常宝贵。很多Demo代码直接把图像写文件、做图像识别、甚至加滤镜,导致回调阻塞,然后采集线程就开始丢帧,录出来的视频卡顿。
正确做法是回调函数里只做两件事:
- 从回调缓冲拷贝图像到自建缓冲。
- 发射信号通知其他线程来处理。
回调里的缓冲要提前申请好,不要在回调里new内存或者扩大容器,这种动态分配在做监控视频流处理上是禁止的。
如果你希望录制视频,在回调里拷贝完图像后,不要做任何阻塞操作,把帧丢进那路摄像头对应的帧队列,录制线程自行从队列取数据,这样视频流会非常稳定。
5. 多路USB摄像头视频录制实现
5.1 录制方案对比:DirectShow Encoder vs FFmpeg
录制视频这步我自己经历过多轮方案尝试。最早用DirectShow的Windows Media Encoder Filter,做出来的AVI文件能用但不够通用,而且编码器版本一换,文件就打不开。后来改用FFmpeg做录制,稳定性和兼容性立刻上了一个数量级。
从工程角度说,FFmpeg对多重格式支持完善,MP4封装、H.264编码器都是标配。在Windows下可以用avformat_open_output+avcodec系列接口,Qt项目中常见的使用方式是直接集成FFmpeg库。
推荐集成方式:在VS2015工程里链接FFmpeg的lib,头文件用FFmpeg官网版本。注意一点,老版本VS2015对FFmpeg头文件的C语言特性支持很好,基本无冲突。准备音视频数据时,OpenCV里做出来的C++封装类有很多,直接复用即可,不需要自己从零封装。
5.2 多路录制文件的封装与命名
多路摄像头录制视频,除了技术实现,还要考虑文件管理。一盘散沙很容易录成一大堆乱七八糟的文件,后期寻找对应录像要疯掉。
我在项目里是这样设计的:
- 每路摄像头单独生成一个文件。
- 文件名格式为“摄像头编号_日期_时间.avi”,例如
CAM01_20240918_153000.avi。 - 启动录制时在主界面显示当前每一路的录制状态。
- 录像存盘时按天创建目录,每天一个文件夹。
这种管理方式看起来基础,实际维护时非常省心。因为项目不止录一路,如果是4路摄像头同时录制,统一格式命名可以很快定位到某一台设备的某一段时间。
5.3 启停录制的原子性与文件完整性
视频录制的启动和停止,逻辑上要保证“原子性”。意思是,启动时所有设备必须在同一时间开始写入,停止时必须在同一时间完成文件写入。否则会出现某一路文件只有几秒空白,另一路却录了十几分钟。
实现方法是设置一个控制位,由主线程统一下发“开始/停止”指令。采集线程读取该标志位后自己控制对应行为。要特别防止停止录制时,还有帧文章正在队列里等待编码,正确顺序是先停采集,再清空队列,最后关闭编码器并写入文件尾部信息。
如果中途程序崩溃,未正常关闭的AVI文件会损坏。为了降低损失,可以在录制时周期性写索引信息。FFmpeg的AVFMT_FLAG_AUTO_BSF也可以尽量避免这种问题,但最关键的还是代码逻辑上保证录制结束后能正常完成文件收尾。
6. 常见问题与排查技巧实录
6.1 录制出来的视频播放花屏或绿屏
这个问题多数是格式转换的错误。DirectShow采集回调里拿到的Buffer格式跟预期不一致,比如代码里按RGB24处理,实际上来的是YUV420P数据,显示出来自然花屏。
解决思路是调试时先打印媒体的AM_MEDIA_TYPE信息,确认实际格式,再做转换。如果是YUV系列,可以用libyuv转换,这个库在Windows下的表现非常稳定,支持各种YUV与RGB互转,还做了CPU优化,性能比手写循环高很多。
6.2 摄像头打开失败或打开后画面黑屏
打开失败有以下常见原因:
- 摄像头被其他程序占用(比如Windows相机应用没关)。
- 摄像头连接USB 3.0口但带宽不足。
- 设备枚举后没有正确Release,导致句柄泄漏。
USB摄像头同时工作会抢占带宽,4路1080P很难全部跑起来。实测中我把分辨率降到720P,帧率设到15fps,4路完全稳定。如果你需要高分辨率,最好用支持UVC的工业相机手动指定带宽预留,不能指望普通USB摄像头能支撑太高负荷。
黑屏问题多半是Graph运行没有进入Running状态。检查IMediaControl的Run返回值,同时查看DSHOW的日志输出。常见原因是某一路的Filter Graph里没有正确连接Sample Grabber,导致数据流被堵死。
6.3 Windows下Qt程序打包发布时Qt库缺失
老项目的坑往往还集中在打包上。开发环境能跑,换台电脑就报“windows no qt platform plugin could be initialized”。这个问题的核心是缺少platforms插件,或者是插件与程序位数不一致。
打包时建议用windeployqt工具自动化处理Qt运行库,手动复制DLL不仅容易漏,还会多出很多冗余文件。再用依赖扫描工具检查一遍,把缺失的VC运行库也一并拷贝进去。这样到客户电脑上直接解压运行即可,不需要安装Visual Studio环境。
如果你用的是MSVC编译的Qt,记得把vc_redist.x64.exe一起打进安装包,否则目标机器没有VC++运行库,程序会在启动阶段直接崩溃。
6.4 多路状态监控下的性能优化
实际运行监控程序时,CPU和内存都是有限的,要留一部分给系统的其他业务。我自己常做的优化有:
- 只在界面上显示正在处理的帧,不显示时立即释放。
- 降低界面刷新频率,比如每秒刷新15帧,人眼基本无感知。
- 用定时器定期清理录像文件,防止磁盘空间被录满。
- 给采集线程设置Sleep(10ms)降低空转损耗。
别小看这些优化,4路摄像头同时录制时,如果每路都按60fps去刷新和编码,再好的机器也会卡顿,回落到15fps后一切都会变得流畅许多。
7. 实操经验小结
回到开头老同学的困惑,我用这套方案帮他完成了4路USB摄像头的采集与录制,系统连续运行了72小时未出现一次死机或重启。整个项目大概花了两周时间,绝大部分时间消耗在摄像头兼容性和格式转换上,核心架构反而是一次性搭完就没有再动过。
最后再分享一个项目习惯:在开发过程中,把每路摄像头都固定一个专门的采集测试程序,单独测试通过后再集成到总程序里。多路并发的坑往往是“组合式”出现的,先把单路的边界条件都摸清楚,再组合起来排查会高效得多。写这类系统,最怕的就是所有问题混在一起,最后一锅粥谁也说不清,分开测试、逐个击破才是正路。
本文还有配套的精品资源,点击获取