前段时间接了个桌面端视频展示的小需求,核心就一句话:把本地视频播出来,能暂停、能调进度、能控制音量。我当时想这还不简单,Qt自带的多媒体库拉出来就能用。结果从打开工程那一刻起就开始连续踩坑——先是被Qt 6的API变化搞得编译不过去,然后又遇到无声、白屏、特定格式播放失败。等到全部跑通,我才意识到:QMediaPlayer这套组件的坑,远比官方示例那几页代码要多得多。
这篇文章把基于QMediaPlayer实现视频播放器的完整链路复盘一遍。从一个最小的可运行播放器写起,到播放列表、截图、倍速等进阶功能,再把最容易翻车的几个问题的排查过程完整还原。适合刚接触Qt的初学者,也适合用Qt 5写过多媒体应用、正准备迁移到Qt 6的开发者,当作一份可以对照复现的工程笔记。
1. 先想清楚:QMediaPlayer在音视频技术栈里的位置
1.1 为什么是它,而不是FFmpeg或VLC这种"全能选手"
接到播放器需求时,很多人的第一反应其实不是Qt,而是FFmpeg。FFmpeg确实能解决一切跟解封装、解码、转码有关的问题,但代价是你要自己处理一整套工程问题:解码后的帧怎么送到屏幕上、音频数据怎么喂给声卡、播放进度变了怎么同步音画、暂停和续播怎么处理缓冲状态。这些工作单独拎出来每一项都不算难,但合在一起,就是一个小型播放引擎的体量。如果只是做一个普通业务里的视频展示功能,用FFmpeg属于"杀鸡用了牛刀",开发周期和后续维护成本都会明显拉高。
VLC的SDK是另一个常见方案,功能确实大而全,但集成体积大、初始化链路长、API风格也和Qt的信号槽体系不太协调。我更愿意把它看成"需要播放一切格式、包括各种诡异流媒体场景"时的选择。至于直接调Windows Media Foundation或者macOS的AVFoundation,跨平台就是一个绕不开的噩梦——同一个播放器,要按三套系统API写三套实现,光同步行为差异就能把团队拖垮。
QMediaPlayer之所以适合这个场景,因为它把"播放一个媒体文件"这件事抽象成了非常干净的信号槽接口:setSource指定媒体源,play/pause/stop控制状态,positionChanged和durationChanged管进度,底层是WMF、AVFoundation还是GStreamer,调用方完全不用关心。对于业务型桌面应用来说,这个抽象层级刚刚好——你不用懂编解码细节,也能把功能稳稳做出来;同时信号槽机制和Qt UI天然配合,进度条、音量滑块的联动几乎不需要额外粘合代码。
1.2 QMediaPlayer的边界:它到底负责什么,不负责什么
把QMediaPlayer当成"万能播放器"是很多新手栽跟头的根源。它本质是个播放控制中枢,而不是解码器集合。真正做解封装和解码的,是它背后挂接的平台多媒体后端。在Windows上通常是基于Windows Media Foundation的实现,在Linux上通常是GStreamer,在macOS上是AVFoundation。这意味着同一个MP4文件,在不同平台能不能播、支持哪些编码,取决于当前系统里后端的能力,而不是QMediaPlayer本身。
这就解释了一个非常经典的现象:开发机上用某个MP4文件测试一切正常,发布到另一台Windows机器上却直接弹"格式不支持"。根本原因往往是目标机器缺解码组件,或者文件本身用了后端不认的编码格式。QMediaPlayer能做的是把errorOccurred信号抛出来,把错误字符串传给你,至于"怎么让它能播",就得靠选型兜底或转码策略解决。
下表是我做技术选型时常用的对比视角,供参考:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| QMediaPlayer | 跨平台、API简洁、与Qt UI深度融合 | 格式兼容性取决于系统后端 | 业务型播放器、视频展示、快速交付 |
| FFmpeg | 解码能力强、可控性极高 | 自绘渲染、音频设备管理成本高 | 播放引擎、格式转换、滤镜处理 |
| VLC SDK | 格式支持广、封装度高 | 集成重、体积大、与Qt风格不一致 | 需要播放一切格式的通用播放器 |
| 系统原生API | 性能好、系统能力利用充分 | 每平台一套实现、维护成本高 | 定制化极高的单平台应用 |
就大多数"播放本地文件、带基础控制"的需求来说,QMediaPlayer是投入产出比最划算的选择。但心里一定要有数:它给你的是"够用"的边界,不是"无限"的边界。
2. 工程配置与模块分工:别在第一步就踩编译坑
2.1 Qt 5到Qt 6的API变化,是第一个大坑
如果你看过网上大量Qt 5时代的教程,再打开Qt 6敲代码,第一眼就会懵:player->setVideoOutput(widget)还在,但player->setAudioOutput是什么?为什么还要自己创建QAudioOutput?Qt 5里音频输出是QMediaPlayer内部隐式处理的,你只需要play()就有声音;到了Qt 6,"播放器负责控制,但音频输出必须显式挂接一个出口设备"这个设计被强制突出了。少这一步,最常见的症状就是画面正常,一点声音没有。
另外,Qt 5里的QMediaPlaylist类在Qt 6中已经从Multimedia模块中移除了。官方态度很明确:播放列表是业务逻辑,不该由播放器管。所以如果你照着老教程写QMediaPlaylist的代码,在Qt 6里根本找不到这个头文件。这个变化直接影响播放列表的实现方式,我是怎么处理的,后面第四部分会展开。
2.2 工程配置:qmake与CMake两种姿势,以及模块缺失的判断方法
两种构建系统都需要显式引入多媒体模块。qmake在.pro文件里这样写:
QT += multimedia multimediawidgets如果是CMake,在CMakeLists.txt里要这样:
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Multimedia MultimediaWidgets) target_link_libraries(app_name PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Multimedia Qt6::MultimediaWidgets )这里特别容易漏的是MultimediaWidgets。QMediaPlayer本体在Multimedia模块里,但QVideoWidget这个显示组件在MultimediaWidgets模块里。很多编译报错比如"setVideoOutput不存在"或者"找不到QVideoWidget头文件",追到底其实是multimediawidgets没被链接进来。
判断本机Qt安装是否包含Multimedia组件,最直接的办法是在Qt安装目录下看include文件夹里有没有QMediaPlayer头文件,或者在Qt Creator里新建一个带Multimedia模块的示例工程,能编译过就说明组件齐全。Linux发行版通过apt或yum装的Qt,多媒体模块经常被拆成独立包,比如Debian系的qt6-multimedia-dev,缺了编译期不会立刻报错,运行期才暴露问题。
2.3 三个核心类怎么配合:一个"大脑、音响、屏幕"的类比
QMediaPlayer体系里,真正的主角不是那个Player类本身,而是三样东西的配合。我习惯用一个类比帮人理解:
QMediaPlayer是大脑。它负责读文件、解封装、跟踪播放状态,但它既不会发出声音,也不会显示画面。QAudioOutput是音响。它接收音频数据并送到声卡,音量、静音都归它管。QVideoWidget是显示器。它负责把视频帧渲染到屏幕上,全屏、宽高比也由它负责。
播放器要正常工作,"大脑"必须同时接上"音响"和"显示器",缺一个就出现我们后面要排查的半死不活状态。对应到代码,就是下面这套初始化逻辑:
m_player = new QMediaPlayer(this); m_audioOutput = new QAudioOutput(this); // 必须先接好输出设备,再设置媒体源 m_player->setAudioOutput(m_audioOutput); m_player->setVideoOutput(ui->videoWidget); m_player->setSource(QUrl::fromLocalFile(filePath)); m_player->play();注意顺序:先setAudioOutput和setVideoOutput,再setSource,避免媒体已经开始加载却还没挂好输出的竞态。虽然很多时候顺序错了也能跑,但遇到某些后端实现,会出现"已经开始播放但没接到画面"的偶现问题。
3. 播放器骨架搭建:从播放暂停到进度、音量、倍速
3.1 让视频先跑起来:不要把任务想复杂
如果你的目标只是"把视频放出来",那么核心代码其实就是上面那六行。真正让播放器像"产品"而不是"测试用例"的,是后续那些交互细节。我通常会先把一个最小的可运行版本跑通,再逐步往上加功能。这个过程里,有两个信号特别容易搞混,先在这里说清楚:
playbackStateChanged:播控状态,只有Playing、Paused、Stopped三种。它表示"播放器本身在不在工作"。mediaStatusChanged:媒体状态,包括Loading、Buffering、Buffered、EndOfMedia、InvalidMedia等一长串。它表示"当前媒体文件处于什么生命周期阶段"。
进度条联动用positionChanged和durationChanged;判断一个文件是否加载完成用mediaStatusChanged里的Buffered;判断播完了用EndOfMedia;判断失败用errorOccurred。把这几个信号对应清楚,后面写逻辑就不会到处乱猜。
3.2 进度条与时间显示:不要让信号互相"打架"
进度条是播放器里最容易写出隐患的部分。常见写法是这样:
connect(m_player, &QMediaPlayer::durationChanged, this, [this](qint64 duration) { ui->positionSlider->setRange(0, static_cast<int>(duration)); }); connect(m_player, &QMediaPlayer::positionChanged, this, [this](qint64 pos) { if (!m_uiUpdating) { ui->positionSlider->setValue(static_cast<int>(pos)); } ui->timeLabel->setText(formatTime(pos) + " / " + formatTime(m_player->duration())); }); ui->positionSlider->setTracking(true); connect(ui->positionSlider, &QSlider::sliderMoved, this, [this](int value) { m_player->setPosition(value); });这里positionChanged是个高频信号,播放过程中会被反复触发。如果用户正在拖动滑块,你又在这个信号里反向setValue,就会出现"拖过去又被弹回来"的现象。我的处理是加一个m_uiUpdating标志位,在用户拖动时置true,拖完再置false。更省事的做法是直接用QSignalBlocker临时锁住信号,但标志位方案在调试时更直观。
时间格式按需处理。时长超过一小时的视频用hh:mm:ss,否则mm:ss就够了。写一个小的格式化函数避免到处复制:
QString MainWindow::formatTime(qint64 ms) { if (ms < 0) return "00:00"; qint64 totalSeconds = ms / 1000; int hours = static_cast<int>(totalSeconds / 3600); int minutes = static_cast<int>((totalSeconds % 3600) / 60); int seconds = static_cast<int>(totalSeconds % 60); if (hours > 0) { return QString("%1:%2:%3") .arg(hours, 2, 10, QLatin1Char('0')) .arg(minutes, 2, 10, QLatin1Char('0')) .arg(seconds, 2, 10, QLatin1Char('0')); } return QString("%1:%2") .arg(minutes, 2, 10, QLatin1Char('0')) .arg(seconds, 2, 10, QLatin1Char('0')); }3.3 音量、静音、倍速:看似简单,坑都藏在边界里
音量控件映射时注意单位。QAudioOutput::setVolume接收的是0.0到1.0的浮点数,而很多UI滑块的默认范围是0到100,中间别忘了除以100:
connect(ui->volumeSlider, &QSlider::valueChanged, this, [this](int value) { m_audioOutput->setVolume(value / 100.0); });静音直接用m_audioOutput->setMuted(bool),不需要动音量值,这样静音再取消后能保持原来的音量。
倍速功能用m_player->setPlaybackRate(qreal),接受小数。但要注意:不是所有格式和所有后端都支持流畅变速。我在实测中发现,Windows后端下多数H.264视频支持0.5到2.0倍速,声音会同步变速;某些高帧率或特殊采样率的音频,变速后可能出现杂音或丢音。所以如果要做倍速菜单,建议把档位限制在0.5、0.75、1.0、1.25、1.5、2.0这几个常用档,并且不要在倍速状态下做逐帧定位,行为会非常怪。
3.4 全屏与宽高比:视频比例不对是显示层的锅
视频显示出来变形,十有八九是宽高比问题。QVideoWidget默认是Qt::KeepAspectRatio,但如果你把widget放进了某个布局后又手动调整了尺寸,某些情况下会回到拉伸状态。稳妥做法是显式设置:
ui->videoWidget->setAspectRatioMode(Qt::KeepAspectRatio);全屏可以调用ui->videoWidget->setFullScreen(true),但注意QVideoWidget的全屏模式后,Esc键默认可以退出,双击退出就需要自己处理。我惯用的做法是重写一个自定义的VideoWidget子类,在mouseDoubleClickEvent里切换全屏状态,同时发出信号给主窗口同步UI按钮状态。这个细节看似不起眼,但在交付时很影响使用体验。
4. 播放列表与状态机:自己动手实现媒体切换
4.1 Qt 6移除的QMediaPlaylist,反而让逻辑更清晰
Qt 5时代的QMediaPlaylist把"队列管理"内置了,但接口设计得并不算好用。Qt 6直接把它删掉,一开始让我挺焦虑,写完后反倒觉得:播放列表本来就应该自己做。因为不同业务对列表的语义完全不一样——有的要循环,有的要随机,有的要记忆上次播放位置,内置方案反而限制发挥。
我用的方案是维护一个简单的结构体列表加当前索引:
struct MediaItem { QString title; QUrl url; }; QVector<MediaItem> m_playlist; int m_currentIndex = -1;播放指定索引的核心逻辑是这样的:
void MainWindow::playIndex(int index) { if (m_playlist.isEmpty() || index < 0 || index >= m_playlist.size()) { return; } m_currentIndex = index; ui->listWidget->setCurrentRow(index); m_player->setSource(m_playlist[index].url); m_player->play(); }setSource本身就是一种隐式的stop行为:设置新媒体源后,旧媒体的播放状态会被重置。所以不需要手动调用stop(),调用了反而会引入额外状态闪烁。
上一首、下一首的逻辑也在这个基础上扩展:
void MainWindow::playNext() { if (m_playlist.isEmpty()) return; int nextIndex = (m_currentIndex + 1) % m_playlist.size(); playIndex(nextIndex); } void MainWindow::playPrev() { if (m_playlist.isEmpty()) return; int prevIndex = (m_currentIndex - 1 + m_playlist.size()) % m_playlist.size(); playIndex(prevIndex); }4.2 EndOfMedia之后的三种策略:循环、下一首、还是停止?
播放完成的处理,要挂在mediaStatusChanged上,判断状态是不是EndOfMedia:
connect(m_player, &QMediaPlayer::mediaStatusChanged, this, [this](QMediaPlayer::MediaStatus status) { if (status == QMediaPlayer::EndOfMedia) { if (m_playMode == PlayMode::SingleLoop) { m_player->setPosition(0); m_player->play(); } else if (m_playMode == PlayMode::Sequence) { playNext(); } else { // StopMode:保持停止,进度停在结尾 m_player->stop(); } } });这里有个经验:在EndOfMedia后调用playNext(),新文件通常会正常加载,但如果设了setPosition(0)后立刻play(),少数后端会出现画面停在最后一帧、状态却已经是Playing的情况。我的对策是把position重置放到play()之前,并且在重置后加一句m_player->play()而不是setPaused(false),这两种写法在边界情况下的表现并不完全等价。
4.3 错误处理:把后端抛来的字符串变成人能看懂的提示
QMediaPlayer的错误信号是errorOccurred(QMediaPlayer::Error, const QString &)。错误枚举不多,但整理成对应文案后,排查效率会高很多:
| 错误值 | 含义 | 推荐的用户提示 |
|---|---|---|
| NoError | 无错误 | 不需要处理 |
| ResourceError | 媒体资源不可用,文件缺失或设备占用 | 检查文件是否存在 |
| FormatError | 格式不支持或后端解析失败 | 不支持该视频格式 |
| NetworkError | 网络流媒体源拉取失败 | 网络连接异常 |
| AccessDeniedError | 无访问权限 | 文件权限不足 |
| PermissionDeniedError | 权限被拒,常见于采集设备 | 检查设备是否被占用 |
别小看这个映射。裸奔的状态下,用户看到的可能是一串来自后端的、包含大量专业词汇的长字符串;做完映射后,对话框里显示"无法播放此文件,可能是不支持的格式",体验完全两回事。
5. 截图、逐帧获取与录像入口:把播放器玩成工具
5.1 最简单的截图姿势:QVideoWidget::grab()
如果只是给"当前画面"拍快照,QVideoWidget::grab()是最省事的方案。它直接返回一个QPixmap,保存成图片文件即可:
QPixmap snapshot = ui->videoWidget->grab(); snapshot.save("snapshot.png", "PNG", 100);这个方式不需要自己处理视频帧格式,底层是直接从渲染表面抓的。适合做"保存当前播放画面"这类功能,或者生成视频封面缩略图。缺点是它抓到的内容在部分平台上依赖窗口是否可见,如果窗口被完全遮挡或最小化,抓出来的图可能不是期望的画面,应当提醒用户播放时使用。
5.2 通过QVideoSink拿原始帧,做真正的逐帧处理
如果你想在播放过程中拿视频帧做额外处理,比如做图像分析、抽帧入库,就得走QVideoSink这条路:
connect(m_player->videoSink(), &QVideoSink::videoFrameChanged, this, &MainWindow::onVideoFrameChanged);槽函数里拿到的是QVideoFrame,要转换成QImage才能继续处理:
void MainWindow::onVideoFrameChanged(const QVideoFrame &frame) { if (!frame.isValid()) return; QVideoFrame writableFrame(frame); if (!writableFrame.map(QVideoFrame::ReadOnly)) return; QImage::Format fmt = QVideoFrameFormat::imageFormatFromPixelFormat( writableFrame.pixelFormat()); if (fmt != QImage::Format_Invalid) { QImage image(writableFrame.bits(0), writableFrame.width(), writableFrame.height(), writableFrame.bytesPerLine(0), fmt); // 到这里,image就可以交给业务层做后续处理了 } else { // 像素格式无法直接映射,比如常见的YUV420, // 需要先做格式转换 } writableFrame.unmap(); }这段代码里有两个容易忽略的点。第一,信号传进来的帧不要直接map(),因为信号参数是const引用,帧的内部缓冲区可能被复用,最好先拷贝一份再操作。第二,如果imageFormatFromPixelFormat返回Format_Invalid,说明当前像素格式不能直接构造QImage,最常见的就是YUV420系列。这种场景下更稳妥的思路是把需要的图像的格式需求提前在源头处理,或者接一个格式转换层;这一部分如果展开写又是一整篇,这里先提示方向,避免读者卡在空图像上。
5.3 从播放到采集:QMediaCaptureSession打开的扩展空间
QMediaPlayer解决的是"播放已有媒体"的问题,而Qt多媒体体系里还有一个关键类叫QMediaCaptureSession,它解决的是"采集新建媒体"的问题。播放器做到后面,业务方大概率会提这样的需求:播放某段视频时顺便截一段保存下来,或者把摄像头画面显示出来。
QMediaCaptureSession配合QMediaRecorder可以实现录像;配合QCamera和QVideoWidget可以显示摄像头预览。它的设置逻辑和播放器很相近:同样需要把video输出挂到widget上,audio输出挂到QAudioOutput上。一旦理解了播放器三件套的协作方式,采集侧的上手成本会低很多,因为它们共享同一套输出抽象。
6. 实战排错:复现六类高频问题的完整排查链路
6.1 编译报错:setVideoOutput找不到对应函数
现象:按照示例代码写player->setVideoOutput(ui->videoWidget);,编译提示找不到这个函数的匹配版本。
排查链路:
- 先确认Qt版本。Qt 5和Qt 6的写法本身没有本质区别,但Qt 6.0刚发布那阵子,
setVideoOutput(QVideoWidget*)的重载确实有过变动,如果你用的老版本头文件,签名可能对不上。 - 检查头文件包含。
QVideoWidget需要#include <QVideoWidget>,只包含QMediaPlayer是不够的。 - 重点检查CMake或qmake是否链接了
MultimediaWidgets。这个最隐蔽,缺失时IDE里不一定有红线提示,编到一半才炸。
解决方式:把multimediawidgets模块补进工程。如果是CMake,find_package补上MultimediaWidgets,target_link_libraries补上Qt6::MultimediaWidgets,重新配置后再编译。
6.2 有画面没声音:十有八九是QAudioOutput没挂上
现象:视频画面正常走动,音量滑块怎么调都没声音。
排查链路:
- 先看
m_audioOutput是否为nullptr。很多从Qt 5迁过来的代码根本没创建这个对象,这是最直接的原因。 - 确认是否调用了
setAudioOutput。有对象但不挂接,等于音响买了没接线。 - 检查系统音量合成器。Windows下按音量图标进"音量合成器",看看你的程序是不是被单独静音了——这个和Qt完全无关,但常常被误判成代码问题。
- 最后看
m_audioOutput->isMuted()和volume(),初始化时顺手打印一下,能省去很多怀疑。
解决方式:Qt 6下创建对象并挂接,代码在2.3节已经给出。只要挂上,音频默认就是通的。我遇到过一次更隐蔽的情况:播放器对象和QAudioOutput的父对象不是同一个窗口,程序退出时析构顺序错乱,运行期偶发音频设备句柄冲突。所以建议两者都挂在同一个父对象下,比如都new在以窗口为父的上下文里。
6.3 白屏没画面:三个方向逐个排除
现象:打开视频文件,声音正常,播放器也处于Playing状态,但视频区域是白屏或全黑。
排查链路:
- 检查
setVideoOutput有没有执行。如果play之前没挂video widget,声音照常有,画面永远出不来。 - 检查widget是否真的有尺寸。把widget塞进了一个折叠的布局、或者父容器还没
show(),widget尺寸可能是0,画面自然无法绘制。 - 检查是否有多个
QVideoWidget对应同一个播放器。同一个QMediaPlayer的视频输出只有一个,重复设置会让前面的widget失去画面。
解决方式:写播放器初始化时,第一行就setVideoOutput(ui->videoWidget);界面上先保证widget可见,再加载媒体。排查白屏问题时,可以在mediaStatusChanged里把mediaStatus打出来,看看是否停在Buffered状态——如果媒体状态正常,基本就是显示层的问题,不是解码的问题。
6.4 特定MP4在另一台电脑上播不了:解码后端差异
现象:开发机播放正常,发布到另一台Windows机器上,部分文件报FormatError。
排查链路:
- 先看文件编码,用工具查一下视频轨是H.264还是H.265,音频是AAC还是其他格式。H.265和部分老编码在Windows后端下兼容性堪忧。
- 再看目标机器有没有对应解码组件。Windows系统的Media Foundation播放能力受系统版本和已安装组件影响很大。
- Linux平台则先看GStreamer插件有没有装全,缺
gst-plugins-bad和gst-libav时,能播的格式范围会窄不少。
解决方式:最省心的兜底是给媒体文件自动转码后再播放,但转码会引入延迟。业务上能接受的话,更简单的做法是发布包自带一套解码组件,或者在Windows上安装扩展解码包。如果产品形态允许,也可以考虑在播放链路里接入FFmpeg做私有解码,形成"能播走QMediaPlayer,播不了走FFmpeg"的双保险——这个方案复杂度高,但确实解决了不少商业化播放器的最后一公里问题。
6.5 拖动进度条时画面反复跳变:信号互相踩脚
现象:播放中拖动进度滑块,手一放画面就倒退到拖动前位置,或者滑块自己跳来跳去。
排查链路:
- 检查
sliderMoved信号里是否真的调用了setPosition。 - 检查
positionChanged到setValue之间有没有条件过滤。如果100毫秒一次的positionChanged持续把滑块值写回,而用户拖动的值被覆盖,就会出现"拉不动"或"被弹回"。 - 检查有没有在
valueChanged里写日志或保存配置,positionChanged高频触发时valueChanged也会高频触发,严重的会拖慢UI线程,间接导致画面卡顿。
解决方式:使用m_uiUpdating标志位挡住自动更新,用户拖动期间置true,sliderReleased时置false并执行一次定位。另一个简洁方案是创建滑块时设置setTracking(false),让valueChanged只在释放时触发,配合sliderReleased读取值定位,写法更干净。
6.6 播放器退出时崩溃:生命周期和析构顺序都不能乱
现象:播放一个视频后直接关闭窗口,程序偶发崩溃,报错信息指向多媒体后端动态库。
排查链路:
- 检查
QMediaPlayer、QAudioOutput、QVideoWidget的父对象。父对象为窗口时,窗口析构会按子对象顺序释放,但QMediaPlayer的析构在走~QMediaPlayer()时可能还需要访问video输出,如果video widget已经被先释放,就会崩溃。 - 检查是否在析构前手动
stop()并断开信号。很多崩溃来自播放还在进行中,后端线程还没停下来,窗口的销毁事件就来了。
解决方式:窗口关闭事件里主动做清理,这是最稳的做法:
void MainWindow::closeEvent(QCloseEvent *event) { m_player->stop(); m_player->setVideoOutput(static_cast<QVideoWidget*>(nullptr)); m_player->setAudioOutput(nullptr); m_player->disconnect(); QMainWindow::closeEvent(event); }先把输出设备从播放器上解绑,再stop,再断开所有信号,最后才让窗口继续析构。这套顺序我实测下来能规避绝大多数退出崩溃。开发期间如果还想继续追问题,在调试器里打开"中断于异常"选项,崩溃堆栈指向哪个库,基本就能锁定是后端线程还是对象析构的锅。
我自己后来在这些坑里摸了一遍之后,干脆把QMediaPlayer的创建、输出挂接、信号连接全部封装进一个独立的Controller类里,界面只跟Controller通信。这样一来,Qt版本差异被收敛在一个文件里,换版本或换后端时不需要满工程找播放器调用点。这个封装思路,以后有机会再单独写一篇展开。