最小播放器是三步:给源、给输出、播放。 剩下的全是信号:状态、位置、时长、错误,都靠信号回传。
上一节说 QMediaPlayer 是空壳,这一节看这个空壳怎么用。
三步 API 一学就会,真正的活在于「用信号把界面接上」。
这一节把五个必接的信号和三种输出方式讲完,后面的音乐播放器与视频播放器都只是在这上面加壳。
一、三步:给源、给输出、播放
Qt 6 的最小播放器:
QMediaPlayer *player = new QMediaPlayer(this);
QAudioOutput *audio = new QAudioOutput(this);
QVideoWidget *video = new QVideoWidget(this);
player->setAudioOutput(audio); // 声音往哪出
player->setVideoOutput(video); // 画面往哪出
player->setSource(QUrl::fromLocalFile("demo.mp4"));
player->play();
Qt 5 与 Qt 6 的差别全在这几个 API 上,必须分清:
| 动作 | Qt 5 | Qt 6 |
|---|---|---|
| 设源 | setMedia(QMediaContent(url)) |
setSource(QUrl) |
| 音量 | setVolume(0~100) |
QAudioOutput::setVolume(0~1) |
| 播放列表 | QMediaPlaylist |
已移除,自己维护 |
| 画面输出 | setVideoOutput(QWidget*) |
同一个,另有 setVideoSink |
| 错误信号 | error(Error) |
errorOccurred(Error, QString) |
音量搬到 QAudioOutput 是 Qt 6 最容易被漏掉的改动:
Qt 6 里 QMediaPlayer 根本没有 setVolume,代码能编译过才怪。
setSource 是异步的:它立刻返回,真正的加载在后台线程做,
结果通过 mediaStatusChanged 回来。所以「设完源马上取时长」拿到的一定是 0。
二、状态靠信号回传
五个信号,缺一个界面就不完整:
| 信号 | 什么时候来 | 用来干什么 |
|---|---|---|
mediaStatusChanged |
加载、缓冲、结束、出错 | 判断这一步走没走通 |
playbackStateChanged |
播放、暂停、停止 | 换按钮图标 |
positionChanged |
播放中周期性 | 推进度条 |
durationChanged |
拿到总时长 | 设滑块范围 |
errorOccurred |
出错时 | 提示用户,别让界面装死 |
mediaStatusChanged 有七个状态,但只有三个要处理:LoadedMedia(能播了)、
EndOfMedia(播完了,该切下一首)、InvalidMedia(播不了)。其余四个是过渡态,看一眼就行。
positionChanged 的频率不能假设。文档没说多久发一次,它由后端决定,
通常是几十到一百毫秒一次。
所以「进度条每帧都刷新」这种想法在这一层是错的——你拿到的位置本来就是粗糙的。
这也是为什么进度条不用刷得太勤:数据源本身就是百毫秒级的,你按 16 毫秒去刷, 刷的只是同一个值。Demo 里那个「100 到 250 毫秒」的结论,根子在这里。
三、三种画面输出方式
画面往哪去,有三种接法:
| 接法 | 适合 | 代价 |
|---|---|---|
QVideoWidget |
普通播放窗口 | 最省事,不能自定义渲染 |
QGraphicsVideoItem |
要在画面上叠图层 | 多一层场景,稍重 |
QVideoSink |
要拿 QVideoFrame 自己处理 |
要自己做格式转换 |
QVideoWidget 的两个细节值得知道:它有 aspectRatioMode 和 fullScreen,
默认会按比例居中、留黑边,想铺满就把 aspectRatioMode 设成 Qt::IgnoreAspectRatio。
但拉伸会变形,正片场景一般保持默认。
QVideoSink 是 Qt 6 才有的,videoFrameChanged(QVideoFrame) 一到,你就拿到了这一帧。
要截图、要送 AI 推理、要自己做 shader,走的都是这条。
注意 QVideoFrame 要 map() 之后才能读像素,读完必须 unmap(),
而且它可能是显存里的纹理——那种情况下 map 会触发一次回读,很慢。
所以「每帧取一帧做处理」这种用法,帧率会掉得很明显。
四、动手 Demo:三行代码就能播,这就是 Multimedia 的优势
这段没有 Qt 环境,所以用一个「最小解码器」当等价物:只解码、不显示, 量出「每帧的解码成本」是多少。 这个数越小,越说明「三行代码」买到的东西有多厚。
c = av.open(path)
s = c.streams.video[0]
t0 = time.perf_counter()
n = 0
for frame in c.decode(s):
n += 1
if n >= 60:
break
elapsed = (time.perf_counter() - t0) * 1e3
print("%d 帧,合计 %.1f 毫秒" % (n, elapsed))
print("平均每帧 %.2f 毫秒" % (elapsed / n))
实跑输出:
--------------------------------------------------------------------
三行代码就能播,这就是 Multimedia 的优势
--------------------------------------------------------------------
**QMediaPlayer 的用法是三行:setMedia、setVideoOutput、play。**
**解码、同步、渲染、声音全在背后。**
**真跑一个「最小播放器」的等价物**(PyAV 版,只做解码不做显示):
解出 60 帧,一共 8.0 毫秒,**平均每帧 0.13 毫秒**。
**三行代码换来了什么,又没换来什么**:
能力 QMediaPlayer 给了吗 LibVLC 给了吗
----------------------------------------------------------------------
解封装+解码 给了 给了
音视频同步 给了 给了
显示与声音输出 给了 给了
拿单帧图像做处理 很难 要写回调
自定义渲染(滤镜、叠加) 基本不行 可以
冷门格式 看系统 基本全覆盖
**前两行是「三行代码买到的」**,**后三行是「三行代码买不到的」。**
**要做视频分析(人脸、车牌、AI 推理),必须能拿到单帧图像**——QMediaPlayer 在这件事上很别扭,
LibVLC 有视频回调,FFmpeg 最直接。
**要做自定义渲染(比如把画面贴到 3D 场景里)**,只能自己控制。
**所以「三行代码」是一个很危险的诱惑**:
**它能让你两天做出一个能播的 demo,也能让你两个月后推倒重来。**
**选型时要先问一句:将来需不需要「看到帧」?**
60 帧一共 8 毫秒,平均每帧 0.13 毫秒。 这个数小得有点可疑,原因是测试片只有 320×240、而且第二帧之后都是 P 帧, 真实 1080p 的 H.264 每帧解码在几毫秒量级。
重点是那张六行表:前三行是「三行代码买到的」——解封装、解码、同步、显示、声音。 后三行是「三行代码买不到的」——取帧、自定义渲染、冷门格式。
「取单帧」那一行写「很难」不是客套:Qt 6 之前根本没有公开接口,
Qt 6 有了 QVideoSink,但给的是 QVideoFrame,
你还得自己转到 QImage。做 AI 推理的流水线里,这一层转换就够写一天。
所以「三行代码」这个词要这么理解:它买到的是「一个能播的窗口」,不是「一个播放器内核」。 前者两天能做出来,后者才是要两个月的那部分。
30 秒实验
- 写一个最小播放器,只
setSource加play,看能不能出声(提示: 忘了setAudioOutput就没声音)。 - 算一下:
positionChanged每 100 毫秒来一次,进度条按 16 毫秒刷,一秒里有多少次是白刷? - 判断一下:只想要「播完自动播下一首」,用
mediaStatusChanged的哪个状态?
常见误区
- 忘了
setAudioOutput。 Qt 6 里没有音频输出对象就完全没声音,而且不报错。这是最经典的一个。 - 在
setSource之后立刻duration()。 它是异步加载的,这时候拿到的是 0。 要在durationChanged里取。 - 用
QMediaPlayer::setVolume调音量。 Qt 6 里这个方法不存在,音量在QAudioOutput上, 而且范围是 0 到 1 的浮点。 - 假设
positionChanged会每帧来。 频率由后端定,通常百毫秒级。 想逐帧控制的,得自己按QVideoSink或走 FFmpeg。
自测三问(点开对答案)
1. Qt 6 里最小播放器的三步是什么? —— `setSource(QUrl)` 给源、`setVideoOutput` / `setAudioOutput` 给输出、`play()`。 注意 Qt 6 把音频输出独立成了 `QAudioOutput` 对象。 2. 为什么设完源不能马上取时长? —— 因为 `setSource` 是异步的,加载在后台线程做。 要等 `durationChanged` 或者 `mediaStatusChanged` 到 `LoadedMedia`。 3. 要把每一帧送给 AI 推理,该用哪种输出方式? —— `QVideoSink`,接 `videoFrameChanged` 拿 `QVideoFrame`。`QVideoWidget` 只负责显示,拿不到帧。能播之后,一个真正的播放器还差三样东西:进度条、音量、播放列表。 下一节把它们挨个装上,顺便解决进度条那个「自己动自己」的回环。
- ☐ (本节完,下一节 04.03 Qt实现音乐播放器)