资讯详情

资讯详情

Qt Multimedia实战:播放、采集、QML用法与发布排障

如果你平时做Qt项目主要就是窗口、按钮、表格第一次打开多媒体模块那一排类名大概率是懵的。QMediaPlayer、QAudioOutput、QMediaCaptureSession、QCamera、QVideoWidget光是理清这些类谁管声音、谁管画面、谁管设备就得花一下午。去年我接手一个桌面工具需求是“播放本地视频、录音、摄像头预览”三件事一起做第一版直接拿QMediaPlayer硬凑文档翻了不少也踩了不少坑。这篇文章就把我最终理清楚的模块地图、播放器落地、采集流程、QML用法、发布排障一次写明白适合正准备在项目里接入音视频播放或录制的Qt开发者也适合那些已经在用、但总觉得API很别扭的人。1. 先看清地图Qt Multimedia 模块里到底有什么1.1 Qt 5 到 Qt 6多媒体模块的分家先说版本问题。Qt 5.15.2 msvc2019_64 这套组合在现在的存量项目里还很常见但你点开Qt安装器勾选组件时会看到多媒体不是单独一项而是拆成了 Qt Multimedia 和 Qt Multimedia Widgets 两个子模块。装完头文件分别在src/multimedia和src/multimediawidgets下面。Qt 6 之后拆得更细变成了 Qt Multimedia、Qt Multimedia Quick、Qt Multimedia Widgets 三块。这个拆分的直接后果就是你写#include QMediaPlayer不一定能编过。QMediaPlayer本身在 multimedia 里但QVideoWidget在 multimediawidgets 里如果你只加了QT multimedia编译时会报找不到QVideoWidget很多新人第一关就卡在这。所以第一步一定要确认两件事当前用的是 Qt 5 还是 Qt 6以及工程文件里到底引入了哪些多媒体子模块。另外Qt 5.15 的QMediaPlayer用的是setMedia(QMediaContent(...))Qt 6 改成了setSource(QUrl(...))。网上搜教程经常混着来你自己写代码之前先看一眼 Qt 版本不然编译报一堆“no matching member function”的时候真的会怀疑人生。1.2 播放、采集、底层音频选型看范围Qt Multimedia 能做的事可以分成四块音视频播放核心类是QMediaPlayer、QAudioOutput、QVideoWidget负责把本地文件或网络流播出来。音视频采集QCamera、QAudioInput、QMediaRecorder通过QMediaCaptureSession把设备串起来实现录音和录像。底层音频读写QAudioSource和QAudioSink直接读写 PCM 数据。你做波形显示、实时变调、音频分析就走这一层。视频帧处理QVideoFrame/QVideoSink每一帧画面拿到手可以截图、识别、推流。选型判断我总结成一句话需求是“能播、能录、能预览”官方模块完全够用如果涉及低延迟音频、自定义音频数据流、特殊编码格式再考虑引入其他库。不要一上来就折腾 FFmpeg 或 GStreamer先把官方模块边界摸清楚多数业务场景都能省下大量开发时间。我之前见过一个项目需求只是循环播放一个提示视频团队却直接上了视频解码库二次封装结果维护成本翻了几倍。其实QMediaPlayer一个类就够了还自带音量、倍速、进度控制何必造轮子。1.3 工程文件里怎么正确引入模块Qt 5 的.pro文件里这样写QT core gui widgets multimedia multimediawidgetsQt 6 用 CMake 的话CMakeLists.txt里这样写find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Multimedia MultimediaWidgets) target_link_libraries(myapp PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Multimedia Qt6::MultimediaWidgets)重点在于multimediawidgets这个模块。很多人只加了multimedia然后#include QVideoWidget报错第一反应是去查头文件路径其实问题就是模块没加全。IDE 提示找不到类时先检查工程文件比查路径快得多。2. 播放器落地QMediaPlayer QVideoWidget 的最小实践2.1 代码骨架播放一个本地视频我写播放器 Demo 的习惯是不加播放列表、不加快捷键、不加皮肤先用最朴素的几个对象把链路跑通。链路里四个角色各司其职QMediaPlayer负责播放状态和指令比如 play、pause、stop、seek。QAudioOutput负责把音频数据送到声卡。QVideoWidget负责把视频帧渲染到窗口。QMediaPlayer自己只做控制不直接碰画面和声音。一个能跑的最小 C 示例#include QApplication #include QMediaPlayer #include QVideoWidget #include QPushButton #include QVBoxLayout int main(int argc, char *argv[]) { QApplication app(argc, argv); QMediaPlayer player; QVideoWidget videoWidget; player.setVideoOutput(videoWidget); player.setMedia(QMediaContent(QUrl::fromLocalFile(D:/demo.mp4))); QPushButton playBtn(QStringLiteral(播放)); QObject::connect(playBtn, QPushButton::clicked, player, QMediaPlayer::play); QVBoxLayout layout; layout.addWidget(videoWidget); layout.addWidget(playBtn); QWidget window; window.setLayout(layout); window.resize(800, 450); window.show(); return app.exec(); }这段代码在 Qt 5.15 下可以直接跑通。注意setMedia的参数QUrl::fromLocalFile会把本地路径转成file:///格式直接传QString路径有时候能工作但遇到中文路径或特殊字符容易出问题所以统一用fromLocalFile最稳。Qt 6 下要改两处setMedia(QMediaContent(...))换成setSource(QUrl::fromLocalFile(...))音频输出要显式创建QAudioOutput并调用player.setAudioOutput(audioOutput)。如果代码是从网上抄的看到这两种写法混搭时先判断版本。2.2 进度条、音量、倍速控制类需求的标准写法播放器最常做的功能是进度条。进度条有两个方向的数据流一是播放位置变化时要更新滑块位置二是拖动滑块时要改变播放位置。基础代码是这样connect(player, QMediaPlayer::durationChanged, this, [this](qint64 duration) { slider-setRange(0, static_castint(duration)); }); connect(player, QMediaPlayer::positionChanged, this, [this](qint64 position) { slider-setValue(static_castint(position)); }); connect(slider, QSlider::sliderMoved, player, [player](int value) { player.setPosition(value); });这里有个小坑拖动滑块时positionChanged信号还会继续过来会跟你的拖动“打架”表现为滑块一边拖动一边被拉回去。处理办法是加一个isSliderPressed标志位按下滑块时置 true松手时置 false 并真正调用setPosition拖动过程中只更新界面。这个细节不注意做出来的进度条手感会很差。durationChanged只在切换媒体时触发一次所以初始化时要把QSlider的范围设置好。对于本地大文件positionChanged默认每 1000 毫秒发一次信号想要界面更跟手可以调用player.setNotifyInterval(500)代价是信号频率高一点CPU 占用略微上升一般项目不需要设太频繁。音量在 Qt 5.15 里直接用player.setVolume(60)范围 0 到 100。Qt 6 改成通过QAudioOutput::setVolume(0.6)控制范围 0 到 1。倍速播放两边都有接口player.setPlaybackRate(1.5)实测 1.5 倍速下大多数视频都正常超过 2 倍容易出现画面明显掉帧这是解码器的限制不是 Qt 的问题。2.3 截图与帧处理想做更多必须理解 QVideoFrame项目做到后面加了“保存当前帧为图片”的功能。截图有两条路差别很大。第一条路直接videoWidget.grab()抓窗口内容。优点是代码简单缺点是抓到的是整个控件画面如果播放器有黑边、有控制条截图里全都带进去而且受窗口大小影响清晰度不可控。第二条路从视频帧里直接取数据。想要拿原始帧需要设置一个自定义的QAbstractVideoSurface子类。核心代码思路如下class FrameGrabber : public QAbstractVideoSurface { Q_OBJECT public: QListQVideoFrame::PixelFormat supportedPixelFormats( QAbstractVideoBuffer::HandleType type) const override { Q_UNUSED(type); return {QVideoFrame::Format_RGB32, QVideoFrame::Format_ARGB32}; } bool present(const QVideoFrame frame) override { QVideoFrame f frame; if (!f.map(QAbstractVideoBuffer::ReadOnly)) { return false; } QImage image(f.bits(), f.width(), f.height(), f.bytesPerLine(), QImage::Format_ARGB32); // 这里必须拷贝一份unmap 之后 image 指向的内存就失效了 QImage copy image.copy(); f.unmap(); emit frameReady(copy); return true; } };这段代码有几个点要特别留意。QAbstractVideoSurface的present是播放后端每来一帧都会调用的在这个函数里不能做耗时操作否则会阻塞播放线程。bytesPerLine不一定等于width * 4因为底层数据有对齐如果你直接用QImage(f.bits(), f.width(), f.height(), QImage::Format_ARGB32)而不传 bytesPerLine很可能出现图像歪斜或上下错位。部分视频格式不是 ARGB32这时候需要查frame.pixelFormat()做格式转换实际项目中建议统一在present里转成 RGB32 再发出去上层处理省心很多。Qt 6 的 API 有变化map的参数变成QVideoFrame::ReadOnly同时新增了QVideoSink可以直接拿到帧不用自己继承 surface。如果项目是新的直接走 Qt 6 的QVideoSink会舒服很多。3. 采集这条路录音和摄像头预览完整走一遍3.1 音频录制三步走Qt 5.15 开始引入了QMediaCaptureSession这套统一采集框架音频录制的正确流程是三步创建QMediaCaptureSession对象。给 session 设置QAudioInput。创建QMediaRecorder调用session.setRecorder(recorder)然后recorder.record()。只创建 recorder 不挂 sessionrecord()之后你会收到 failed 信号且 error 信息莫名其妙。我第一次写的时候就是漏了setRecorder查了半天文档才发现 capture session 才是连接设备和录制器的枢纽。一个可用的录音片段QMediaCaptureSession session; QAudioInput audioInput; QMediaRecorder recorder; session.setAudioInput(audioInput); session.setRecorder(recorder); recorder.setMediaFormat(QMediaRecorder::MPEG4); recorder.setAudioCodec(QMediaRecorder::AAC); recorder.setEncodingMode(QMediaRecorder::ConstantQuality); recorder.setQuality(QMediaRecorder::HighQuality); recorder.setOutputLocation(QUrl::fromLocalFile(rec.mp4)); recorder.record();别忘了在结束录音时调用recorder.stop()否则文件可能没有正常写入。文件路径有个坑setOutputLocation传的路径只是“期望路径”实际生成的文件位置要通过recorder.actualLocation()获取。有些平台会在你指定路径后面自动加扩展名你要是硬按outputLocation去打开文件会得到“文件不存在”。3.2 摄像头预览与设备枚举摄像头预览的套路跟录音一模一样QCamera作为视频输入设备挂到QMediaCaptureSession再把QVideoWidget设置成 session 的视频输出。QCamera camera; QMediaCaptureSession session; QVideoWidget view; session.setCamera(camera); session.setVideoOutput(view); camera.start();打开摄像头之前要枚举设备。Qt 5 用QCameraInfo::availableCameras()Qt 6 改成了QMediaDevices::videoInputs()。如果你做的是一个多摄像头切换界面建议直接用 Qt 6 的QMediaDevices它提供了videoInputsChanged()信号可以在摄像头热插拔时通知界面刷新列表Qt 5 的QCameraInfo没有这么方便的可用性通知接口。还有一个必须提前处理的点权限。Windows 上第一次打开摄像头会触发系统权限弹窗用户如果点了拒绝程序里不一定报错camera.start()调完没反应mediaStatus一直停在NoMedia。所以界面上要有状态提示别让用户以为程序坏了。Linux 下要注意/dev/video0是否被其他进程占用占用的表现是QCamera能创建但start()之后一直没有视频帧过来这大概率是权限或设备占用问题。3.3 录制参数组合容器、编码、质量录制参数设置看起来简单实际处处是雷。QMediaRecorder可以设置容器格式、音频编码、视频编码、质量模式。我的建议是不要全部手动指定而是先看看当前平台支持什么。const QListQMediaRecorder::ContainerFormat containers recorder.supportedContainers(); const QListQMediaRecorder::AudioCodec audioCodecs recorder.supportedAudioCodecs();这两个函数返回的是当前系统媒体后端真正支持的列表。不同平台的差异非常大Windows 上默认能用的编码器、Linux 上可能完全没有你在 Windows 上设置 H.265 录制到 Linux 上直接 failed。稳妥做法是写一个“设备能力探测”页面在设置界面里把 supported 列表打印出来根据结果再决定用户可选项。用表格总结一下常见的参数组合场景推荐容器音频编码备注一般视频录制MPEG4AAC兼容性最好纯音频录音MPEG4AAC文件小高质量音频采样WAVPCM文件大但无版权和编码器顾虑视频录制如果需要指定分辨率调用recorder.setVideoResolution(1920, 1080)但要注意这个分辨率必须是摄像头实际支持的否则录制出来的视频可能只有声音没有画面。4. QML 侧的 Multimedia如果你不想全用 Widgets 写界面4.1 QML 多媒体的基本元素用 QML 做界面的话不再需要QVideoWidget核心元素变成MediaPlayer、VideoOutput、Camera、Audio。一个视频播放组件可以很短import QtQuick 2.15 import QtMultimedia 5.15 Item { MediaPlayer { id: player source: D:/demo.mp4 videoOutput: videoOut playbackRate: 1.0 } VideoOutput { id: videoOut anchors.fill: parent } MouseArea { anchors.fill: parent onClicked: { if (player.playbackState MediaPlayer.PlayingState) player.pause(); else player.play(); } } }QML 的声明式写法比 Widgets 省很多代码尤其适合自定义控制条、动画过渡、悬浮球这类界面需求。最近的很多项目问我 Qt 桌面悬浮球开发怎么做我通常建议优先考虑 QML因为悬浮球的拖拽、半透明、动画效果在 QML 里实现成本远低于 Widgets 的setWindowFlags加paintEvent组合拳。需要注意QML 的VideoOutput走的是场景图Scene Graph渲染底层还是同一套播放引擎。Qt 5.15 需要在安装时勾选 Qt Multimedia Quick 模块否则 QML 里找不到VideoOutput元素编译器会告诉你“VideoOutput is not a type”。4.2 Widgets 与 QML 混合的注意点实际项目里经常出现“主体还是 Widgets某块页面用 QML 嵌入”的架构。QQuickWidget嵌到 Widgets 窗口时VideoOutput 偶尔会出现黑屏、层级不对、刷新不跟手的问题。常见原因有两个一是 OpenGL 上下文冲突。QQuickWidget 默认使用 OpenGL 渲染如果主窗口里还有其他 OpenGL 绘制代码两边可能互相干扰。二是窗口复合时视频渲染的纹理没有正确同步。解决办法是先确保QQuickWidget的setAttribute(Qt::WA_AlwaysStackOnTop)正确设置并给QQuickWidget指定与父窗口一致的背景透明策略如果黑屏把QQuickWidget::setResizeMode(QQuickWidget::SizeRootObjectToView)调一次通常能缓解。还有一个很常见的崩溃在 QML 里动态创建MediaPlayer组件用完直接销毁但没有先调用stop()。播放引擎内部可能还在处理缓冲区销毁瞬间 Qt 会报类似 “QObject: Cannot destroy ... while children are still used” 的错误运气好只打日志运气不好直接崩溃。所以销毁前先player.stop()再player.source 最后才销毁组件。5. 发布与排障跑起来只是开始带出去才见真章5.1 最常报的编译错误dependent 路径问题网上很多 Qt 新手会碰到一条非常误导人的报错:-1: error: dependent ..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwidgets... does not exist这个报错有个特点报错信息里的路径明显是别人的机器路径跟你当前工程的目录结构对不上。原因基本一样.pro.user文件里写死了旧环境的绝对路径。项目从别人电脑拷过来或者你自己换过 Qt 安装目录、换过 Qt KitQt Creator 仍按旧路径去定位 Qt 头文件找不到就崩。不是代码问题也不是 Qt 安装坏了。处理办法很直接关闭 Qt Creator。删除工程目录下所有.pro.user文件。重新用 Qt Creator 打开.pro文件。重新选择匹配的 Kit重新构建。如果还报错把build目录整个删掉再试。我每次换 Qt 版本都走一遍这个流程基本上能解决 90% 的 dependent 路径报错。再补充一条同一个工程尽量只用一套 Qt 版本和编译器组合混着用 Qt 5.15 和 Qt 6 的 Kit 来回切最容易把.pro.user搞乱。5.2 播放崩溃与线程问题多媒体播放器崩溃的高频原因我总结下来就三类第一类QMediaPlayer是局部变量函数退出后播放器被销毁但音频后端可能还持有内部资源典型的崩溃场景是在按钮点击函数里 new 一个播放器播放完就 delete。正确做法是让播放器生命周期跟窗口一致不要频繁创建销毁。第二类把QMediaPlayer用moveToThread移到子线程然后又从主线程直接调用它的 play/pause。这会产生跨线程的信号/槽问题播放器内部状态还没准备好崩溃就很正常。很多人以为播放视频卡界面就要开线程这是误解Qt 播放后端本身就是异步解包的你只管调接口真正解码不在主线程没必要自己移线程。第三类程序退出时没有先stop()和置空 videoOutput。界面已经销毁了播放器还在尝试往已经不存在的QVideoWidget上画帧程序退出阶段崩溃往往就是这个原因。写析构顺序时先销毁播放器再销毁 videoWidget不要在两者之间插入可能触发重绘的操作。顺便说一句QMessageBox::information这类阻塞弹窗在调试播放逻辑时尽量少用。有人进度条不走了就弹窗提示弹窗会阻塞事件循环播放器信号被卡住反而让画面和进度都停掉。调试用qDebug()打日志永远比弹窗好使。5.3 解码器与格式依赖发布包为何在别的机器上放不出声音用windeployqt打包 Qt 程序时多媒体模块会带上自己的插件目录。但有个很多人不知道的事实Qt 桌面多媒体后端依赖系统媒体框架。Windows 上走 DirectShow/WMFLinux 上走 GStreamermacOS 走 AVFoundation。也就是说解码能力不完全由 Qt 提供而是由操作系统媒体框架提供。所以经常出现这个现象开发机上 MP4 一切正常把程序发给客户客户机器上能出画面、没声音甚至画面都出不来。原因不是你的程序写坏了而是客户机器缺解码器或者系统媒体后端被精简过。遇到这种情况先在程序里打印player.errorString()和mediaStatus看具体是什么错误。如果确定是解码器缺失临时方案有两个一是要求客户机器安装系统解码器二是彻底绕开把 FFmpeg 的解码能力封装进来放弃 Qt 默认后端。第二个方案工程量不小但有些行业对播放可靠性要求高回头看看也值。另外windeployqt打包时记得加上 QML 目录参数QML 多媒体项目要这样执行windeployqt --qmldir ./qml myapp.exe不带--qmldirQML 文件里的import QtMultimedia在目标机器上可能加载不到对应插件。5.4 性能观察与小技巧最后分享几个性能相关的实测体会。播放 4K 视频画面花屏、掉帧时先看QVideoWidget的渲染线程和主线程关系。如果主线程里有大量耗时操作视频帧被拖累是必然的。把耗时操作移到工作线程播放体验通常会明显改善。多路摄像头预览的场景每个摄像头建一个QMediaCaptureSession和一个QVideoWidget注意显卡并发能力。实测在普通笔记本上同时预览四路 1080p 摄像头已经接近极限继续加路数会开始卡顿。关于视频帧里做图像识别这类需求千万不要在present()回调里直接跑识别算法。正确做法是把QVideoFrame复制一份转成QImage丢给独立工作线程处理识别完再通过信号回传结果。我在项目里就是这么做的实测帧率稳定很多。还有个调试技巧遇到多媒体行为诡异时可以检查环境变量QT_DEBUG_PLUGINS。设置了这个变量Qt 启动时会打印加载了哪些插件、哪些插件加载失败。很多时候“怎么没声音”“怎么画面黑”的原因在这个输出里一眼就能看到。坦白说Qt Multimedia 这些年一直被人吐槽文档不全、API 变来变去我自己也没少骂。真要用好它我的经验是先把安装目录里自带的官方示例跑熟再看源码和文档。你安装 Qt 的路径下Examples/Qt-x.x.x/multimedia里已经有完整的播放器、音频录制、摄像头示例把这些示例改一改直接就是项目雏形。我后来接手多媒体相关的需求第一反应就是先翻官方示例这个习惯帮我省了至少一半的探索时间。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →