🎬 第四章 Qt播放音视频及Multimedia多媒体模块
📄 一句话概括

最小播放器是三步:给源、给输出、播放。 剩下的全是信号:状态、位置、时长、错误,都靠信号回传。

📝 本节和前面几节的关系

上一节说 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 秒实验

  1. 写一个最小播放器,只 setSource 加 play,看能不能出声(提示: 忘了 setAudioOutput 就没声音)。
  2. 算一下:positionChanged 每 100 毫秒来一次,进度条按 16 毫秒刷,一秒里有多少次是白刷?
  3. 判断一下:只想要「播完自动播下一首」,用 mediaStatusChanged 的哪个状态?

常见误区

  1. 忘了 setAudioOutput。 Qt 6 里没有音频输出对象就完全没声音,而且不报错。这是最经典的一个。
  2. 在 setSource 之后立刻 duration()。 它是异步加载的,这时候拿到的是 0。 要在 durationChanged 里取。
  3. 用 QMediaPlayer::setVolume 调音量。 Qt 6 里这个方法不存在,音量在 QAudioOutput 上, 而且范围是 0 到 1 的浮点。
  4. 假设 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` 只负责显示,拿不到帧。

能播之后,一个真正的播放器还差三样东西:进度条、音量、播放列表。 下一节把它们挨个装上,顺便解决进度条那个「自己动自己」的回环。

相关笔记