前言
如果你正把项目从 Qt5 迁移到 Qt6,第一次#include <QMediaPlaylist>时大概率会直接吃到一个编译错误:
fatal error: QMediaPlaylist: No such file or directory这不是你环境装错了,而是Qt6 把QMediaPlaylist整个类删掉了,同时被删的还有QMediaPlayer::setPlaylist()/playlist()/playbackMode()这一套 API。官方给出的理由很直白:播放列表是应用层业务逻辑,不是多媒体框架该管的事。
于是很多人被迫做的一件事,就是"自己手写一个QMediaPlaylist"。本文讲清楚三件事:Qt6 为什么砍掉它、Qt6 的新架构长什么样、以及如何用不到 200 行代码写出一个功能对等的播放列表(含顺序、循环、单曲循环、随机四种模式),并且避开几个能让人调一下午的坑。
一、为什么 QMediaPlaylist 会被废弃
Qt5 的 Qt Multimedia 是一个"薄封装"层,底下同时压着三套后端:Linux 上的 GStreamer、Windows 上的 DirectShow / WMF、macOS 上的 AVFoundation。QMediaPlaylist的设计缺陷在这套架构下被放大成了三个具体问题。
第一,双份真相源(two sources of truth)。QMediaPlaylist自己维护m_currentIndex,而QMediaPlayer又维护m_currentMedia(也就是currentMedia())。两者通过setPlaylist()建立双向引用:
// Qt5 的写法,Qt6 中已不存在
player->setPlaylist(playlist); // playlist 反向持有 player 指针
playlist->setCurrentIndex(3); // 内部再去调用 player->setMedia(...)一旦你绕过 playlist 直接调用player->setMedia(otherUrl),playlist->currentIndex()就和实际播放的内容对不上了。这种"谁说了算"的模糊,是后续无数 bug 的根源。
第二,模式语义分散且后端不一致。PlaybackMode有Sequential、Loop、CurrentItemInLoop、Random、CurrentItemOnce五种。CurrentItemOnce和CurrentItemInLoop的名字与实际行为经常让人记混,而且不同后端在"列表播完到底停在哪一项"这件事上表现并不完全一致。
第三,EndOfMedia的处理权被抢走了。在 Qt5 里,列表自动切下一曲是由QMediaPlaylist在内部监听QMediaPlayer::mediaStatusChanged完成的。你的业务代码想插一脚(比如统计播放量、插入广告)就非常别扭。
Qt6 的重构哲学是职责单一:QMediaPlayer只负责"解码并播放一个媒体源",播放顺序、循环策略、队列调度全部交还给应用层。这反而给了你完全的控制权。
Qt5 / Qt6 API 对照
| 功能 | Qt5 写法 | Qt6 替代方案 |
|---|
| 播放列表容器 | QMediaPlaylist | 自己用QList<QUrl>维护 |
| 设置列表 | player->setPlaylist(pl) | 手动player->setSource(currentUrl) |
| 下一曲 / 上一曲 | pl->next()/previous() | 自己改 index 后重新setSource |
| 播放模式 | QMediaPlaylist::PlaybackMode | 自定义enum class PlayMode |
| 列表播完的通知 | QMediaPlaylist::currentIndexChanged | QMediaPlayer::mediaStatusChanged+EndOfMedia |
| 音频输出 | QMediaPlayer内置音量 | 必须显式创建QAudioOutput并setAudioOutput() |
| 循环单曲 | setPlaybackMode(CurrentItemInLoop) | Qt6.5+ 的player->setLoops(QMediaPlayer::InfiniteLoops) |
注意最后一行:Qt6.5 补上了setLoops(),它的语义是"当前这一个 media 循环多少次",而不是"整个列表循环"。所以它只能替你实现单曲循环,列表循环仍然要自己写。
二、Qt6 多媒体模块的新架构
Qt6 把原来一个大而全的QMediaPlayer拆成了三个协作对象:
QMediaPlayer ──setAudioOutput()──> QAudioOutput ──> 系统音频设备
│
└──setVideoOutput()────────> QVideoWidget / QGraphicsVideoItem / QVideoSinkQMediaPlayer:状态机 + 解码调度。负责play() / pause() / stop() / setSource()。QAudioOutput:音频设备与音量。不创建它就没有声音,这是迁移时最高频的"坑"。QVideoWidget:视频渲染窗口(音频播放不需要)。
核心信号只有四个,掌握它们就能实现播放列表:
| 信号 | 参数 | 用途 |
|---|
mediaStatusChanged | QMediaPlayer::MediaStatus | 检测EndOfMedia、LoadedMedia、InvalidMedia |
playbackStateChanged | QMediaPlayer::PlaybackState | 更新 UI 播放/暂停按钮 |
positionChanged | qint64(ms) | 刷新进度条 |
errorOccurred | QMediaPlayer::Error,QString | 单曲失败时跳过,避免整条队列卡死 |
MediaStatus里最关键的两个值是LoadedMedia(媒体已就绪,可以安全播了)和EndOfMedia(播完了,该切下一曲了)。
CMake 配置
Qt6 的模块划分比 Qt5 更细,MultimediaWidgets依赖Multimedia,一定要都写上:
cmake_minimum_required(VERSION 3.16)
project(Qt6PlaylistDemo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Multimedia)
qt_standard_project_setup()
add_executable(Qt6PlaylistDemo
main.cpp
MediaPlaylist.h
MediaPlaylist.cpp
)
target_link_libraries(Qt6PlaylistDemo PRIVATE
Qt6::Core
Qt6::Gui
Qt6::Widgets
Qt6::Multimedia
)三、播放列表的本质:队列 + 状态机 + 取下一项的策略
抛开 Qt5 的历史包袱,一个播放列表要解决的问题只有两个:
- 顺序问题:当前播第几首?下一首是谁?
- 边界问题:播完最后一首之后干什么?
所以数据结构非常简单:
tracks : QList<QUrl> // 队列本体
current : int // 当前下标,-1 表示空
mode : PlayMode // 边界策略真正的逻辑全部集中在nextIndex()这一个函数里——它就是"取下一项的策略"。把策略抽出来,四种模式就是四行代码:
| 模式 | 语义 | nextIndex()行为 |
|---|
Sequential | 顺序播放 | cur+1,越界则停止 |
Loop | 列表循环 | (cur+1) % size |
CurrentItemLoop | 单曲循环 | 返回cur本身 |
Random | 随机播放 | 随机一个不等于cur 的下标 |
至于随机模式"不重复播放同一首",只要加一句if (size > 1) while (idx == cur) idx = rand();就够了。
四、代码实战:手写一个 Qt6 播放列表
4.1 MediaPlaylist.h
#pragma once
#include <QObject>
#include <QList>
#include <QUrl>
#include <QMediaPlayer>
#include <QAudioOutput>
#include <QRandomGenerator>
class MediaPlaylist : public QObject
{
Q_OBJECT
public:
enum class PlayMode {
Sequential, // 顺序,播完停止
Loop, // 列表循环
CurrentItemLoop, // 单曲循环
Random // 随机
};
Q_ENUM(PlayMode)
explicit MediaPlaylist(QObject *parent = nullptr);
void addMedia(const QUrl &url);
void addMedia(const QString &localPath); // 便捷重载
void clear();
int count() const { return m_tracks.size(); }
int currentIndex() const { return m_current; }
QUrl currentMedia() const;
void setPlayMode(PlayMode mode) { m_mode = mode; }
PlayMode playMode() const { return m_mode; }
void setVolume(float v); // 0.0 ~ 1.0
public slots:
void play(); // 从当前项开始播
void playAt(int index);
void next();
void previous();
void pause();
void stop();
void togglePause();
signals:
void currentIndexChanged(int index);
void playModeChanged(PlayMode mode);
void playbackStateChanged(QMediaPlayer::PlaybackState state);
void mediaFinished(); // 整个列表播完(顺序模式)
void errorOccurred(const QString &message);
private slots:
void onMediaStatusChanged(QMediaPlayer::MediaStatus status);
void onErrorOccurred(QMediaPlayer::Error error, const QString &msg);
private:
int resolveNextIndex(bool forward) const;
void loadAndPlay(int index);
QMediaPlayer *m_player = nullptr;
QAudioOutput *m_audio = nullptr;
QList<QUrl> m_tracks;
int m_current = -1;
PlayMode m_mode = PlayMode::Sequential;
bool m_loading = false; // 防止 setSource 期间的重复触发
};4.2 MediaPlaylist.cpp
#include "MediaPlaylist.h"
#include <QFileInfo>
#include <QDebug>
MediaPlaylist::MediaPlaylist(QObject *parent)
: QObject(parent)
, m_player(new QMediaPlayer(this))
, m_audio(new QAudioOutput(this))
{
// Qt6 必须显式绑定音频输出,否则无声
m_player->setAudioOutput(m_audio);
m_audio->setVolume(0.8f);
connect(m_player, &QMediaPlayer::mediaStatusChanged,
this, &MediaPlaylist::onMediaStatusChanged);
connect(m_player, &QMediaPlayer::playbackStateChanged,
this, &MediaPlaylist::playbackStateChanged);
connect(m_player, &QMediaPlayer::errorOccurred,
this, &MediaPlaylist::onErrorOccurred);
}
void MediaPlaylist::addMedia(const QUrl &url) { m_tracks.append(url); }
void MediaPlaylist::addMedia(const QString &localPath)
{
// 本地路径务必走 fromLocalFile,它会正确处理盘符与中文
addMedia(QUrl::fromLocalFile(localPath));
}
void MediaPlaylist::clear()
{
stop();
m_tracks.clear();
m_current = -1;
emit currentIndexChanged(m_current);
}
QUrl MediaPlaylist::currentMedia() const
{
if (m_current < 0 || m_current >= m_tracks.size())
return {};
return m_tracks.at(m_current);
}
void MediaPlaylist::setVolume(float v)
{
m_audio->setVolume(qBound(0.0f, v, 1.0f));
}
int MediaPlaylist::resolveNextIndex(bool forward) const
{
const int size = m_tracks.size();
if (size == 0) return -1;
switch (m_mode) {
case PlayMode::CurrentItemLoop:
return m_current < 0 ? 0 : m_current;
case PlayMode::Loop:
if (m_current < 0) return 0;
return forward ? (m_current + 1) % size
: (m_current - 1 + size) % size;
case PlayMode::Random: {
if (size == 1) return 0;
int idx = m_current;
// 避免随机到同一首,造成"没切歌"的错觉
while (idx == m_current)
idx = QRandomGenerator::global()->bounded(size);
return idx;
}
case PlayMode::Sequential:
default:
if (m_current < 0) return 0;
return forward ? m_current + 1 : m_current - 1;
}
}
void MediaPlaylist::play()
{
if (m_tracks.isEmpty()) {
emit errorOccurred(tr("播放列表为空"));
return;
}
loadAndPlay(m_current < 0 ? 0 : m_current);
}
void MediaPlaylist::playAt(int index)
{
if (index < 0 || index >= m_tracks.size()) return;
loadAndPlay(index);
}
void MediaPlaylist::next()
{
const int idx = resolveNextIndex(true);
// 顺序模式下越界 => 整个列表结束
if (idx < 0 || idx >= m_tracks.size()) {
m_player->stop();
emit mediaFinished();
return;
}
// 单曲循环:同一首重新 setSource 会失败,改为 seek 回 0
if (idx == m_current && m_player->playbackState() != QMediaPlayer::StoppedState) {
m_player->setPosition(0);
m_player->play();
return;
}
loadAndPlay(idx);
}
void MediaPlaylist::previous()
{
const int idx = resolveNextIndex(false);
if (idx < 0 || idx >= m_tracks.size()) return;
loadAndPlay(idx);
}
void MediaPlaylist::loadAndPlay(int index)
{
if (index < 0 || index >= m_tracks.size()) return;
m_loading = true;
if (m_current != index) {
m_current = index;
emit currentIndexChanged(m_current);
}
m_player->setSource(m_tracks.at(index));
m_player->play();
m_loading = false;
}
void MediaPlaylist::onMediaStatusChanged(QMediaPlayer::MediaStatus status)
{
if (m_loading) return; // 忽略装载过程中的中间态
if (status == QMediaPlayer::EndOfMedia) {
next(); // 唯一的"切歌"入口
} else if (status == QMediaPlayer::InvalidMedia) {
emit errorOccurred(tr("无法播放:%1").arg(currentMedia().toString()));
next(); // 坏文件直接跳过,别卡住队列
}
}
void MediaPlaylist::onErrorOccurred(QMediaPlayer::Error error, const QString &msg)
{
Q_UNUSED(error)
qWarning() << "[MediaPlaylist]" << msg;
}
void MediaPlaylist::pause() { m_player->pause(); }
void MediaPlaylist::stop() { m_player->stop(); }
void MediaPlaylist::togglePause()
{
if (m_player->playbackState() == QMediaPlayer::PlayingState)
m_player->pause();
else
m_player->play();
}4.3 main.cpp(可直接运行)
#include <QCoreApplication>
#include <QTimer>
#include <QDebug>
#include "MediaPlaylist.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
MediaPlaylist playlist;
playlist.setPlayMode(MediaPlaylist::PlayMode::Loop);
playlist.setVolume(0.5f);
// 中文路径也没问题:fromLocalFile 内部按本地编码转换
playlist.addMedia("D:/music/周杰伦 - 晴天.mp3");
playlist.addMedia("D:/music/海阔天空.flac");
QObject::connect(&playlist, &MediaPlaylist::currentIndexChanged, [](int i) {
qInfo() << "切歌 ->" << i;
});
QObject::connect(&playlist, &MediaPlaylist::mediaFinished, []() {
qInfo() << "列表播放结束";
QCoreApplication::quit();
});
playlist.play();
return app.exec();
}4.4 qmake 工程(老项目迁移用)
如果项目还在用.pro,在QT +=里加上 multimedia 模块即可:
QT += core gui widgets multimedia
CONFIG += c++17常见坑点
坑 1:没有声音,但一切正常
Qt6 最常见的问题。QMediaPlayer不再内置音频输出,必须new QAudioOutput并setAudioOutput。
// ❌ Qt6 中这样写是无声的
auto *player = new QMediaPlayer(this);
player->setSource(QUrl::fromLocalFile("a.mp3"));
player->play(); // 没声音,也没有任何报错
// ✅ 正确写法
auto *player = new QMediaPlayer(this);
auto *audio = new QAudioOutput(this); // 父对象交给 this,避免泄漏
player->setAudioOutput(audio);
audio->setVolume(0.8f);
player->setSource(QUrl::fromLocalFile("a.mp3"));
player->play();坑 2:setSource() 之后立刻读 duration() 拿到 0
setSource()是异步的。刚调用完duration()还是 0,playbackState()也还是StoppedState。要在mediaStatusChanged收到LoadedMedia之后再读:
// ❌ duration() 恒为 0
player->setSource(url);
qDebug() << player->duration();
// ✅ 等待状态到位
connect(player, &QMediaPlayer::mediaStatusChanged,
[player](QMediaPlayer::MediaStatus s) {
if (s == QMediaPlayer::LoadedMedia)
qDebug() << "时长:" << player->duration();
});
player->setSource(url);坑 3:Windows 上提示 "Could not open media" / 后端缺失
Qt6 的 Windows 后端依赖Windows Media Foundation;如果解码失败,先确认系统装了 [Media Feature Pack](N 版/KN 版 Windows 精简掉了它)。而在自编译Qt 或使用某些发行版二进制包时,Qt6.5 起默认走FFmpeg 后端,需要把avcodec、avformat、avutil等 DLL 放到 exe 同目录:
windeployqt --multimedia your_app.exewindeployqt会自动带上plugins/multimedia/下的后端插件。只复制Qt6Multimedia.dll是不够的,插件目录缺失时表现为"能编译、能运行、就是不出声"。
可以用环境变量强制指定后端,验证是不是后端问题:
set QT_MEDIA_BACKEND=ffmpeg
set QT_MEDIA_BACKEND=windows坑 4:路径含中文或空格导致加载失败
QMediaPlayer走的是底层多媒体框架,对 URL 编码敏感。
// ❌ 手工拼 "file://" 在中文路径下会失败
player->setSource(QUrl("file:///" + path));
// ✅ 交给 Qt 处理编码
player->setSource(QUrl::fromLocalFile(path));如果路径来自argv或窄字符串,Windows 下建议显式转换:
QString path = QString::fromLocal8Bit(argv[1]); // GBK 控制台
player->setSource(QUrl::fromLocalFile(path));坑 5:信号重复连接导致"跳两首"
把currentIndexChanged连到某个 lambda 里再次调用next(),或者在loadAndPlay()里又触发一次mediaStatusChanged,就会出现一次EndOfMedia切两首的现象。两个对策:一是像上文那样用m_loading标志位屏蔽装载期的中间状态;二是对可能重复连接的信号使用Qt::UniqueConnection:
connect(m_player, &QMediaPlayer::mediaStatusChanged,
this, &MediaPlaylist::onMediaStatusChanged,
Qt::UniqueConnection);坑 6:单曲循环时重复 setSource 同一 URL 无效
Qt6 对"同一个 source 重复 set"会做短路优化,不会重新播放。正确做法是setPosition(0)后play(),这也是上面next()里那段特殊分支存在的原因。
坑 7:对象析构顺序
QAudioOutput必须晚于或同时于QMediaPlayer析构。把两者都new到 parent 上是最安全的做法;如果手动delete,先删 player 再删 audio。
总结
| 关注点 | Qt5 方案 | Qt6 方案 | 关键点 |
|---|
| 队列容器 | QMediaPlaylist | QList<QUrl>+ index | 状态自己管,避免双份真相源 |
| 切歌时机 | 框架内部处理 | mediaStatusChanged == EndOfMedia | 唯一入口,防止跳曲 |
| 播放模式 | PlaybackMode枚举 | 自定义PlayMode+resolveNextIndex() | 策略集中在取下标函数 |
| 音频输出 | 内置 | QAudioOutput显式绑定 | 不绑定 = 无声 |
| 单曲循环 | CurrentItemInLoop | setPosition(0)或 Qt6.5+setLoops | 重复 setSource 无效 |
| 部署 | windeployqt | windeployqt --multimedia | 别漏plugins/multimedia/ |
迁移的核心思路只有一句话:把"下一曲是谁"这个决策从框架手里拿回到自己手里。一旦接受了这个设定,QMediaPlaylist的消失就不再是麻烦,而是一次让播放逻辑变得可观测、可测试、可扩展的机会——你可以在next()里轻松插入"播放统计""广告插播""跨平台音源切换",这些在 Qt5 时代都很难做干净。