十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Delphi XE中DSPack视频播放:从DirectShow封装到工程实践

Delphi XE中DSPack视频播放:从DirectShow封装到工程实践 简介面向 Delphi XE 开发者的 DSPack 组件包资源重点解决在 Delphi XE 环境下集成视频播放功能时遇到的渲染、音频视频流处理与控制问题。压缩包共 808 个文件约 4.99MB内部以 pas 源代码、dpr 工程文件、dpk/dproj 组件包工程、dfm 窗体文件、dcu 编译单元为主体同时包含 bmp/ico 图标资源、chm 帮助文档、示例工程及版本管理元数据整体结构适合直接研究组件实现和二次开发。已有 627 人学习下载。包内 DSVideoWindowEx 组件支持 AVI、WMV、MP4 等格式提供播放控制、元数据读取、界面定制及硬件加速等能力。通过研究附带源码与示例可以快速掌握初始化组件、加载视频、设置播放参数与调试排错的方法为在 Delphi XE 平台构建高性能视频播放应用提供完整参考。1. 为什么我在 Delphi XE 里做视频播放首选 DSPack老 Delphi 开发者应该都有这种感觉一谈到“在 Delphi 里播放视频”很多人第一反应是拖一个TMediaPlayer控件然后指定FileName、调用Play完事。这个方案在 2000 年左右确实简单但它有几个让我忍不了的硬伤支持的格式极少、受 MCI 底层限制、进度条拖动响应迟钝、全屏播放时拉伸效果乱七八糟而且一旦系统装了不同解码器同一个 AVI 在客户机器上可能直接黑屏。还有一个思路是直接把 DirectShow 的 COM 接口声明复制进 Delphi自己封装IFilterGraph、IMediaControl、IVideoWindow。这条路可行但工作量不小DirectShow 的接口嵌套和引用计数问题处理起来非常容易踩坑。我大概在 2011 年转投 DSPack之后就再也没换过。DSPack 做的事情说白了一句话把微软 DirectShow 那套 COM 接口封装成 Delphi 能直接拖拽使用的组件和类。它提供的TFilterGraph、TSampleGrabber、TVideoWindow等组件把 DirectShow 最核心的 Filter Graph 管理、媒体控制、视频渲染、音视频同步这些复杂逻辑全部包了一层让我在做 Delphi XE 播放器时不用再去手写 COM 接口的QueryInterface和AddRef。我之所以在项目标题里写“最佳”并不是吹捧它有多完美而是在实测对比后它确实是当下在 Delphi XE 里免费开源、能最快落地、功能覆盖最全的视频播放方案。下面我会把整个踩坑和实操过程完整写出来包括安装版本选择、最小播放器搭建、播放结束检测、循环播放、以及我最头疼的重复闪烁问题该怎么处理。2. DSPack 的版本选择与安装配置细节编译 DSPack 并不是直接点一下Install就万事大吉这里面的选择会直接影响你在 Delphi XE 下能否顺利编译。DSPack 官方原本支持的老版本有2.3和3.0但官方源码在 Delphi 2007 之后基本停止维护到了 Delphi XE 时代网上流传的版本很多我建议你优先找DSPack 2.3.3 或 3.0.2 的源码包这俩版本对 XE 的兼容性相对稳定。2.1 安装前必须注意的路径与 DCU 清理先说一下最容易翻车的地方DSPack 安装包里的包文件分为DSPack_Run.dpk运行时包和DSPack_Design.dpk设计期包。我第一次安装时只装了设计期包结果编译时疯狂报找不到DSPack.pas。后来才明白必须先把包含DSPack.dcr和运行期包资源文件的目录加入 Library Path。推荐的操作顺序把源码包解压到一个不含中文和空格的路径例如D:\Components\DSPack。打开 Delphi XE在Tools Options Delphi Options Library - Win32里将D:\Components\DSPack\Source加入到 Library Path。打开D:\Components\DSPack\Packages\DSPack_Run.dpk先编译运行期包再编译安装设计期包。如果编译时提示缺少DirectShow9.pas、MMSystem.pas等文件检查一下DSPack\Source目录是否完整或者把D:\Components\DSPack\Source\DirectShow9也加进路径。很多编译失败案例的根源就是路径没配全源码里部分单元分散在Source和Source\DirectShow9两个子目录漏掉哪一个都会报“unit not found”的错。2.2 Delphi XE 下 32 位与 64 位的现实问题这里要明确说一个重要现实DSPack 最适合跑在 Win32 目标平台。虽然 Windows 本身是 64 位系统时Delphi XE 可以交叉编译 Win64 程序但 DSPack 官方源码对 Win64 的支持非常弱很多底层直接调用了 32 位结构体和指针强行编译 Win64 会碰到大量PAnsiChar与PWideChar类型混用、以及结构体内存对齐的报错。我在实际项目中的处理方式是播放器主程序始终以 Win32 编译。哪怕是运行在 64 位 Windows 上32 位程序访问 DirectShow 的VideoMixingRenderer也是完全没问题的DirectShow 本身就是系统组件不依赖目标程序位数。这样我既享受了 DSPack 的便利又避开了 Win64 移植的无底洞。另外安装完成后建议立即重启一次 Delphi。因为设计期组件注册后组件面板可能不会立刻刷新重启后你就能在DSPack标签页下看到TFilterGraph、TVideoWindow、TSampleGrabber这些组件了。3. 用 DSPack 搭一个最简视频播放器下面这段代码是我实际项目里不断精简后留下的“最小可用骨架”。只要按照这个结构来任何 Delphi XE 程序都能在十几分钟内跑出一个能播放本地视频的窗体。3.1 核心组件与最小代码骨架在窗体上放以下组件TFilterGraph命名FilterGraph1负责管理整个 DirectShow 滤镜图。TVideoWindow命名VideoWindow1用于视频画面渲染区域。两个TButton一个“打开”一个“播放/暂停”。TOpenDialog用于选择视频文件。在FormCreate事件里把 VideoWindow 关联到 FilterGraphprocedure TForm1.FormCreate(Sender: TObject); begin FilterGraph1.Active : True; FilterGraph1.VideoWindow : VideoWindow1; VideoWindow1.FilterGraph : FilterGraph1; end;打开并播放文件的代码procedure TForm1.btnOpenClick(Sender: TObject); var FileName: string; begin if not OpenDialog1.Execute then Exit; FileName : OpenDialog1.FileName; FilterGraph1.Active : False; FilterGraph1.Active : True; FilterGraph1.ClearGraph; if not FilterGraph1.RenderFile(FileName) then begin ShowMessage(无法渲染该文件请检查解码器是否安装); Exit; end; FilterGraph1.Play; end;这里有一个细节非常关键重新打开新文件前一定要先Active : False再Active : True最后ClearGraph。我最初图省事直接ClearGraph后就RenderFile结果第二次打开文件时经常出现画面卡死、声音正常但图像不刷新的情况。原因是旧的 Filter Graph 没有完全释放DirectShow 内部的状态机还停留在旧文件的光盘位置。先翻转 Active 状态相当于把整个 Graph 彻底重建能规避绝大多数文件切换问题。3.2 播放、暂停、停止、定位的封装写法DSPack 的TFilterGraph内部封装了IMediaControl和IMediaSeeking所以播放控制代码非常直观procedure TForm1.btnPlayPauseClick(Sender: TObject); begin if FilterGraph1.State 1 then // 1 正在播放 begin FilterGraph1.Pause; btnPlayPause.Caption : 继续; end else begin FilterGraph1.Play; btnPlayPause.Caption : 暂停; end; end;注意TFilterGraph.State返回的是TFilterState枚举但在 Delphi XE 里它实际上是Longint类型。State 1对应State_RunningState 2对应State_PausedState 0对应State_Stopped。你如果不想记数字可以直接引入DirectShow9单元然后写成FilterGraph1.State State_Running可读性更好。视频定位拖进度条的写法var Seek: IMediaSeeking; CurPos, Duration: Int64; begin if FilterGraph1.FilterGraph.QueryInterface(IMediaSeeking, Seek) S_OK then begin Seek.GetDuration(Duration); Seek.SetPositions(MyProgressBar.Position * Duration div 1000, AM_SEEKING_AbsolutePositioning, 0, AM_SEEKING_NoPositioning); end; end;这里要特别注意Int64的单位DSPack 默认的时间单位是 100 纳秒所以 1 秒等于10000000。很多新手把 Position 当成毫秒直接用导致拖动进度条后视频直接跳到结束位置。3.3 播放结束检测与循环播放的实现方式DSPack 提供了OnGraphNotify事件它是整个播放器的事件中枢。当视频播放结束时DirectShow 会向应用发送EC_COMPLETE事件你可以在事件里做循环播放procedure TForm1.FilterGraph1GraphNotify(Sender: TComponent; EventCode: Longint; Param1, Param2: Longint); begin case EventCode of EC_COMPLETE: begin // 循环播放重新从头开始 FilterGraph1.Play; // 或者通知 UI 层让按钮状态切换 end; EC_ERRORABORT: ShowMessage(播放过程中出现错误); end; end;这样写循环播放有一个潜在问题在OnGraphNotify回调里直接调用FilterGraph1.Play有时候会因为事件还没处理完而出现短暂黑屏或画面闪烁。我更推荐用一个TTimer做延迟重播例如在EC_COMPLETE里只设置一个标志位然后由 Timer 在下一次 tick 里重新调用 Play。这样画面切换更干净不会有闪烁。4. 我最头疼的问题重新开始播放时画面闪烁在把产品交付给客户做演示时我遇到了一个非常典型的场景一个教学视频播放完毕用户点击“重播”视频从头开始播放但画面会先闪一下黑屏再刷新第一帧。客户直接把这个现象定义为“播放器不稳定”导致我花了好几个晚上排查。4.1 闪烁的根源在哪里绝大多数人第一反应是“窗口重绘导致闪烁”于是去改DoubleBuffered属性其实方向完全不对。这个闪烁来自 DirectShow 内部渲染管线的状态切换当你执行FilterGraph1.Play重新播放时Filter Graph 会先从State_Stopped转到State_Paused然后才进入State_Running。在State_Stopped阶段视频渲染器会清空当前帧缓冲区这时候视频窗口显示的就是黑色或空白随后才加载新帧。也就是说闪烁的根源是停止到暂停之间的过渡阶段没有可用帧数据。只要我的播放策略是从停止状态直接重新播放就必然会经过这个空白窗口期。4.2 用暂停替代停止实际可行的解决思路我最终的解决方案并不复杂在播放结束和循环播放时不要把 Graph 置为停止状态而是让它停留在State_Paused状态然后立刻把播放位置移到起点再恢复播放。这样画面缓冲区里还保留着最后一帧图像不会突然清空为黑色用户看到的效果就是画面自然回到开头继续播放。代码大致如下procedure TForm1.HandleLoopPlayback; var Seek: IMediaSeeking; ZeroPos: Int64; begin if FilterGraph1.FilterGraph.QueryInterface(IMediaSeeking, Seek) S_OK then begin ZeroPos : 0; // 先到暂停状态保留当前帧 FilterGraph1.Pause; // 把播放位置移动到 0 Seek.SetPositions(ZeroPos, AM_SEEKING_AbsolutePositioning, 0, AM_SEEKING_NoPositioning); // 再恢复播放 FilterGraph1.Play; end; end;这个方案实测下来重启播放时的闪烁频率降低了 90% 以上。如果你还是觉得画面会闪再补充一个操作在播放新文件之前先调用VideoWindow1.Repaint强制渲染器刷新一下颜色缓冲也能辅助改善。4.3 另一种闪烁场景窗口尺寸变化时视频显示区域在动态拉伸时也会出现闪烁。这个问题我是在做一个全屏/窗口切换功能时遇到的。全屏模式下放大到整个屏幕画面会有明显的撕裂感和闪烁。原因是IVideoWindow默认使用 GDI 窗口渲染拉伸时没有启用覆盖表面或 VMR 的硬件缩放。我建议在窗体尺寸变化后显式调用一次 VideoWindow 的位置和大小设置procedure TForm1.SetVideoWindowSize; begin VideoWindow1.Left : 0; VideoWindow1.Top : 0; VideoWindow1.Width : pnlVideo.ClientWidth; VideoWindow1.Height : pnlVideo.ClientHeight; VideoWindow1.SetBounds(0, 0, pnlVideo.ClientWidth, pnlVideo.ClientHeight); end;然后在FormResize事件里调用它。同时把VideoWindow1的Owner设置为播放区域所在的TPanel而不是直接放窗体上这样视频画面会被裁剪在 Panel 范围内不会覆盖到按钮栏。5. 播放器隐藏但声音播放一个低频却又必踩的坑如果你开发的是后台预览功能比如在列表中选择一个视频右侧或后台就开始播放但界面上不显示视频画面这时候 DSPack 会给你一个意外惊喜视频窗口隐藏时声音还在但图像帧直接停滞不更新。这个问题困扰了我很久后来在 DirectShow 文档里找到解释当IVideoWindow的AutoShow属性为True时渲染器会尝试创建并显示一个视频窗口。如果这个窗口被隐藏或移动到了屏幕外部分渲染器会为了节省资源而直接停止帧递交导致画面冻结但音频流不受影响继续播放。解决方案有两种。简单粗暴的新建一个只有 1 像素大小的不可见窗口把 VideoWindow 放进去不做显示处理。我实际采用的是第二种方案在不需要显示视频时不启用视频渲染器的窗口只让它走 SampleGrabber 抓帧。比如做一个“只取声音不显示画面”的播放模式可以在RenderFile后手动断掉视频 Pin 的连接。但如果你没有特殊需求建议老老实实让视频窗口保持可见哪怕把它放在一个极小尺寸的 Panel 上也别完全隐藏。我曾尝试过给 VideoWindow 设置负数坐标结果 DirectShow 直接报E_INVALIDARG。6. 关于解码器和格式支持你需要知道的事实很多人以为 DSPack 能播放所有视频格式这其实是个误解。DSPack 本身只是一个框架它不包含任何解码器。它能不能播放某个文件完全取决于你的 Windows 系统里安装了多少 DirectShow 兼容解码器。我实测过的情况AVI包括 H.264 编码的 AVI 文件在 Windows 10/11 上默认可以播放因为系统内置了 Media Foundation 兼容层。MP4在 Windows 7 上经常播放失败报找不到解码器在 Windows 10 以上基本能播放但前提是安装过“桌面体验”组件或第三方解码器如 LAV Filters。MKV默认系统无法播放必须安装 LAV Filters 或 K-Lite Codec Pack。RMVB 这类老格式如果是新装的 Windows大概率播放失败必须装 Real Alternative。如果你需要做一个“自包含”的播放器不想依赖用户系统解码器有两个路线安装包集成 LAV Filters安装时静默注册系统的 DirectShow 解码器。使用 FFmpeg 解码后再喂给 DirectShow 渲染器但这就绕过了 DSPack 的解码环节实现复杂度明显上升。我自己的选择是“集成 LAV Filters”因为这保留了 DSPack 的简单代码结构又没有把解码逻辑写死。7. 基于完整项目的优化建议写到这里播放器的核心功能已经全部跑通了打开文件、播放、暂停、停止、拖动进度、循环播放、改善闪烁、处理解码器依赖。我再分享几个在工程化使用中非常加分的细节。进度条刷新不要放在 UI 线程频繁调用。用TTimer定时 200 毫秒刷新一次位置避免因为 QueryInterface 导致界面卡顿。实测 200ms 的刷新间隔看起来非常流畅又不占资源。音量调节直接操作 FilterGraph 的音频接口。传统做法是找到IBasicAudio调用put_Volume取值范围是-10000静音到0最大音量。如果需要平滑且细致的音量调节建议使用对数映射不要直接做线性变化否则人耳听到的感觉会是前面音量变化很小、后面突然变大。错误处理务必完备。DirectShow 有个特点一旦 Graph 出错整个播放器就“死”了再调用 Play 也不会恢复。我的经验是统一监听OnGraphNotify里的EC_ERRORABORT和EC_COMPLETE一旦收到错误事件立即执行一次FilterGraph1.Stop、ClearGraph并通知 UI 层关闭播放状态避免界面停留在假死状态。最后提一下多格式兼容的进阶思路。DSPack 原生不支持媒体流地址播放但如果你拿到了一个 RTMP 或 HTTP 直播地址需要靠 URL 里的扩展名或 MIME 类型让 Filter Graph 自动选择源 Filter。这方面我没有深入展开的打算但如果你有兴趣可以从TFileSourceAsync和IFileSourceFilter入手先实现本地文件播放再研究流媒体。DSPack 的好处是它永远不会“替你决定”所有逻辑都透明地摆在代码里这也正是我喜欢它的原因。希望这套方案能帮你少走一些弯路。本文还有配套的精品资源点击获取
返回列表