Delphi 中基于 DirectShow 的 USB 摄像头视频预览与录像实现
在工业控制、医疗设备和智能监控系统中,图像采集的稳定性和实时性至关重要。尽管现代开发平台不断演进,但在许多长期维护的专业软件项目中, Delphi 依然是构建高性能 Windows 桌面应用的重要工具。它结合 VCL 界面库的强大表现力与对 Windows API 的深度封装能力,特别适合用于快速搭建可靠的数据采集前端。
而当面对“如何让一个普通的 USB 摄像头在 Delphi 程序里流畅出图并录像”这个问题时,答案往往指向微软的经典多媒体框架 —— DirectShow 。虽然官方已将其归为“遗留技术”,推荐新项目使用 Media Foundation,但对于 Delphi 开发者而言,DirectShow 仍是目前文档最全、社区支持最好、兼容性最强的选择。
为什么选择 DirectShow?
我们先抛开术语堆砌,从实际需求出发:你需要的是一个能即插即用、低延迟、可编程控制分辨率帧率,并能把画面保存成标准格式文件的方案。DirectShow 正好满足这些要求。
它的核心思想是“流水线式处理”——把音视频流看作一条从源头到终点的数据管道,每个处理环节都由一个独立模块(Filter)完成。比如:
- 摄像头本身是一个 Source Filter
- 编码压缩是一个 Transform Filter
- 显示画面或写入文件则是 Renderer Filter
所有这些 Filter 被连接在一起形成一个
Filter Graph
,由
IGraphBuilder
统一管理。你可以想象这就像搭积木:选好摄像头、加上渲染器、再接上文件写入器,然后按下“运行”,数据就会自动流动起来。
更重要的是,大多数现代 USB 摄像头遵循 UVC(USB Video Class)标准 ,Windows 内置驱动即可识别,无需额外安装厂商驱动。这意味着只要你的程序能枚举到设备,几乎就能直接使用。
如何发现可用的摄像头?
第一步永远是:“我电脑上到底有几个摄像头?叫什么名字?”
这需要通过 COM 接口遍历系统设备列表。
function EnumerateCameras(lst: TStrings): Boolean;
var
MonikerEnum: IEnumMoniker;
Moniker: IMoniker;
PropBag: IPropertyBag;
cFetched: ULONG;
Variant: OleVariant;
begin
Result := False;
lst.Clear;
// 创建设备枚举器
var ClassFactory: IClassFactory;
if Failed(CoCreateInstance(CLSID_SystemDeviceEnum, nil,
CLSCTX_INPROC_SERVER, IID_IClassFactory, Pointer(ClassFactory))) then Exit;
if Failed(ClassFactory.CreateInstance(nil, IID_IEnumMoniker, Pointer(MonikerEnum))) then Exit;
// 枚举视频输入设备类别
MonikerEnum.Reset;
while (MonikerEnum.Next(1, Moniker, @cFetched) = S_OK) do
begin
if Succeeded(Moniker.BindToStorage(nil, nil, IID_IPropertyBag, PropBag)) then
begin
Variant := 'FriendlyName';
if Succeeded(PropBag.Read('FriendlyName', Variant, nil)) then
lst.Add(Variant);
PropBag := nil;
end;
Moniker := nil;
end;
Result := True;
end;
这个函数会将所有可用视频输入设备的友好名称填入传入的
TStrings
列表中,通常你会看到类似 “USB Camera”、“Integrated Camera” 或具体品牌型号的名字。用户可以在下拉框中选择目标设备,后续操作便基于此名称查找对应的源 Filter。
小技巧:如果多个摄像头名称相同,建议同时提取
DevicePath属性来做唯一标识,避免混淆。
实现视频预览:让画面动起来
有了设备,下一步就是把画面显示出来。这里的关键是使用
VMR-9(Video Mixing Renderer 9)
,它是 DirectShow 中较新的渲染器,支持 Alpha 混合、窗口缩放和 Z-order 控制,非常适合嵌入到 Delphi 的
TPanel
或其他容器控件中。
流程如下:
- 初始化 COM 库
-
创建
IGraphBuilder和ICaptureGraphBuilder2 - 查找并添加摄像头 Source Filter
- 设置 VMR-9 并绑定到窗体控件句柄
-
使用
RenderStream自动连接预览分支 - 启动播放
procedure StartPreview(CameraName: string; Panel: TPanel);
var
GraphBuilder: IGraphBuilder;
CaptureGraph: ICaptureGraphBuilder2;
SourceFilter: IBaseFilter;
begin
CoInitialize(nil);
CoCreateInstance(CLSID_FilterGraph, nil, CLSCTX_INPROC_SERVER,
IID_IGraphBuilder, Pointer(GraphBuilder));
CoCreateInstance(CLSID_CaptureGraphBuilder2, nil, CLSCTX_INPROC_SERVER,
IID_ICaptureGraphBuilder2, Pointer(CaptureGraph));
CaptureGraph.SetFiltergraph(GraphBuilder);
SourceFilter := FindCameraFilterByName(CameraName);
if not Assigned(SourceFilter) then raise Exception.Create('未找到指定摄像头');
GraphBuilder.AddFilter(SourceFilter, 'Video Capture');
// 配置 VMR-9 渲染器
SetupVMR9Renderer(GraphBuilder, Panel.Handle);
// 连接预览流
CaptureGraph.RenderStream(@PIN_CATEGORY_PREVIEW, @MEDIATYPE_Video,
SourceFilter, nil, nil);
// 开始运行
(GraphBuilder as IMediaControl).Run;
end;
其中
SetupVMR9Renderer
是一个辅助函数,负责创建 VMR-9 Filter 并设置其窗口模式为
Windowed
,并将宿主窗口设为 Panel 的 Handle。你还可以在此阶段设置背景色、是否拉伸填充等视觉参数。
工程经验:若出现黑屏或卡顿,优先检查是否正确设置了 VMR 模式;其次尝试强制摄像头输出 YUY2 格式而非 MJPEG,某些低端摄像头在压缩模式下容易丢帧。
开始录像:不只是截图那么简单
很多人误以为“录像”就是定时抓图然后打包成视频,但那样效率极低且无法保证时间连续性。真正的视频录制应该是在 Filter Graph 中开辟一条独立的数据通路,将原始视频流导向文件写入器。
典型结构是:
[Camera]
↓ (Capture Pin)
[AVI Mux] → [File Writer] → output.avi
AVI Mux 负责将视频帧打包成 AVI 容器格式,File Writer 则负责落地写盘。整个过程与预览并行不悖 —— 你可以一边看画面,一边后台安静地录着高清视频。
以下是启动录制的核心代码:
procedure StartRecording(GraphBuilder: IGraphBuilder;
CaptureGraph: ICaptureGraphBuilder2;
SourceFilter: IBaseFilter;
const FileName: string);
var
AviMux: IBaseFilter;
FileWriter: IBaseFilter;
begin
// 创建 AVI 多路复用器
CoCreateInstance(CLSID_AviDest, nil, CLSCTX_INPROC_SERVER,
IID_IBaseFilter, Pointer(AviMux));
GraphBuilder.AddFilter(AviMux, 'AVI Mux');
// 创建文件写入器
CoCreateInstance(CLSID_FileWriter, nil, CLSCTX_INPROC_SERVER,
IID_IBaseFilter, Pointer(FileWriter));
GraphBuilder.AddFilter(FileWriter, 'File Writer');
// 设置输出路径
(FileWriter as IFileSinkFilter).SetFileName(PWideChar(WideString(FileName)), nil);
// 连接 Capture Pin 到 AVI Mux(关键!不要走 Preview 分支)
CaptureGraph.RenderStream(@PIN_CATEGORY_CAPTURE, @MEDIATYPE_Video,
SourceFilter, nil, AviMux);
// 手动连接 AVI Mux 输出到 FileWriter 输入
ConnectFilters(GraphBuilder, AviMux, FileWriter);
// 开始录制
(GraphBuilder as IMediaControl).Run;
end;
注意这里使用的是
PIN_CATEGORY_CAPTURE
而非
PREVIEW
。这是因为部分摄像头的 Preview 流可能被降质(如降低分辨率以提升流畅度),而 Capture 流才是原始高质量输出,确保录像画质不受影响。
实践建议:录制结束后应先调用
.Stop停止 Graph,再移除新增的 Filter,否则生成的 AVI 文件可能缺少索引头导致无法拖动进度条播放。
实际开发中的常见问题与对策
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 枚举不到摄像头 | UVC 驱动未加载 | 检查设备管理器中是否有黄色感叹号;尝试更换 USB 接口 |
| 预览正常但无法录像 | Capture Pin 不可用 | 某些老旧摄像头仅支持单路输出,需停止预览后再开启录像 |
| 录像文件打不开 | 缺少索引或编码异常 | 使用标准 AVI Mux;避免中途强行终止程序 |
| 图像模糊/颜色偏绿 | 像素格式不匹配 |
强制设置
IAMStreamConfig
输出 RGB24 或 YUY2 格式
|
| 多次启停后崩溃 | COM 对象未释放 |
每次关闭时显式 Release 所有接口,最后调用
CoUninitialize
|
此外,在设计 UI 时也应注意用户体验细节:
- 提供“刷新设备”按钮,支持热插拔后重新扫描
-
显示当前流的分辨率和帧率(可通过
IAMStreamConfig.GetFormat获取) - 添加“测试预览”功能,一键验证摄像头状态
- 录像按钮禁用期间给出明确提示(如“正在录制中…”)
性能与稳定性优化建议
-
内存管理严格化
所有IInterface派生的对象(如IBaseFilter,IPin)必须手动Release(),尤其是在频繁开关摄像头的场景下,否则极易引发内存泄漏。 -
线程隔离
IMediaControl.Run和Stop必须在主线程调用,跨线程操作可能导致 Access Violation。如有异步需求,可通过消息机制通知主窗体执行控制命令。 -
错误处理全面化
每个 COM 调用都要判断返回值是否SUCCEEDED(hr),失败时记录日志并提示用户,而不是直接抛异常中断流程。 -
资源复用策略
若需频繁切换摄像头,可缓存 Filter Graph 实例,仅替换 Source Filter,减少重建开销。
这套方案适合谁?
如果你正在开发以下类型的系统,那么 Delphi + DirectShow 的组合依然极具价值:
- 医疗辅助设备 :如耳鼻喉镜、皮肤检测仪的图像采集前端
- 教学实验平台 :学生实验过程视频记录与回放
- 工业视觉初筛 :低成本产线质检相机控制台
- 生物特征采集 :指纹、虹膜、面部识别前的图像抓取模块
- 临时取证工具 :现场快速拍摄并封存证据视频
这类应用通常不需要 H.265 编码、HDR 支持或网络推流等高级特性,反而更看重稳定性、易维护性和部署便捷性 —— 这正是 DirectShow 的强项。
未来的延伸方向
尽管立足于传统技术,但这并不意味着止步不前。你可以在这个基础上轻松扩展更多功能:
- 帧级图像处理 :插入 Sample Grabber Filter,获取每一帧位图数据,送入 OpenCV 进行边缘检测、人脸识别等分析
- 音频同步录制 :添加麦克风 Source Filter,与视频流一同 mux 成 AVI
- RTSP 推流 :结合 FFmpeg 或自定义 Transform Filter,将本地流转发至网络
-
参数动态调节
:通过
IAMCameraControl和IAMVideoProcAmp接口调整焦距、曝光、亮度等属性
甚至可以考虑未来逐步迁移到 Media Foundation,利用
MFCreateSourceReaderFromMediaSource
实现更现代化的流处理,只是目前 Delphi 社区对此的支持仍显薄弱。
这种高度集成的设计思路,正引领着专业采集设备向更可靠、更高效的方向演进。掌握 Delphi 下 DirectShow 的摄像头开发技术,仍然是当前许多行业软件工程师不可或缺的一项实用技能。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
558

被折叠的 条评论
为什么被折叠?



