播放器开发--项目概述与整体框架
这是什么
MyPlayer 是一个基于 Qt6 + FFmpeg + OpenGL 的桌面视频播放器。支持常见的视频格式(mp4、avi、mkv、mov、flv 等),具备播放列表、进度拖动、音量调节、键盘快捷键等基本功能。
核心播放能力包括:
- 音视频解码与渲染
- 基于音频时钟的 A/V 同步
- 播放帧丢弃与尾部保护
- 响应式 Seek(长按方向键不卡顿)
- 多格式 YUV 渲染(YUV420/422/444/NV12)
写这个项目的主要目的是学习音视频播放器的底层原理,所以没有使用 Qt Multimedia 的高层播放接口,而是直接从 FFmpeg 解码到 OpenGL 渲染,自己搭建整个播放管线。
技术栈
| 层面 | 技术 | 用途 |
|---|---|---|
| UI 框架 | Qt 6.10.2 + QML | 窗口界面、播放列表、控制栏 |
| 解封装/解码 | FFmpeg(libavformat / libavcodec / libswscale / libswresample) | 文件解析、音视频解码、像素/采样格式转换 |
| 视频渲染 | OpenGL 3.3 Core | YUV→RGB 转换、GPU 渲染 |
| 音频输出 | Qt Multimedia(QAudioSink) | 将解码后的 PCM 数据推送到声卡 |
| 语言 | C++20 | 整个后端逻辑 |
整体架构
播放器的核心是一个 五级流水线,各级之间通过有界阻塞队列连接:
1 | |
具体展开后是两条并行的管道——视频和音频各自独立解码,最终在时钟的协调下同步输出:
1 | |
数据流的分步说明:
- Demuxer(解封装线程) — 打开文件,读取 AVPacket,根据 stream index 分流到视频或音频的 PacketQueue
- VideoDecoder(视频解码线程) — 从 VideoPacketQueue 取包,
avcodec_send_packet/avcodec_receive_frame解码,帧格式统一转换为 YUV420p 后放入 VideoFrameQueue - AudioDecoder(音频解码线程) — 同上,音频帧经过重采样后放入 AudioFrameQueue
- VideoSurface(Qt 渲染线程) — 从 VideoFrameQueue 取帧,对比 Clock 时间做同步(丢弃/等待),通过 OpenGL shader 将 YUV 数据转换为 RGB 纹理渲染到屏幕
- AudioOutput(音频输出线程) — 从 AudioFrameQueue 取帧,写入 QAudioSink 播放,同时用已播放的音频样本数更新 Clock
线程模型
整个播放器涉及 4 个常驻工作线程 + 1 个 Qt 渲染线程:
| 线程 | 所在模块 | 职责 |
|---|---|---|
| Demux 线程 | PlayerController::demuxLoop() |
循环读取文件包,分发到两个 PacketQueue |
| Video 解码线程 | VideoDecoder::decodeLoop() |
解码视频帧,归一化格式,推入 VideoFrameQueue |
| Audio 解码线程 | AudioDecoder::decodeLoop() |
解码音频帧,重采样,推入 AudioFrameQueue |
| Audio 输出线程 | AudioOutput 内部 |
从 AudioFrameQueue 取帧写入 QAudioSink,更新 Clock |
| Qt 渲染线程 | VideoSurface::createRenderer() |
OpenGL 渲染,与 AudioOutput 共享 Clock 做帧同步 |
线程安全机制:
- 队列:
BaseQueue内部使用std::mutex+std::condition_variable保护,生产者和消费者互不干扰 - 时钟:
Clock内部使用std::mutex保护,AudioOutput 写入、VideoSurface 读取 - 生命周期:通过
_abort原子标志位 +condition_variable::notify_all()实现各线程的快速唤醒和安全退出
核心模块一览
Demuxer(解封装器)
封装 AVFormatContext 的操作:打开文件、查找流、读取包、Seek。支持 I/O 中断回调(interruptCallback),在 stop/seek 时不会因为网络或磁盘 I/O 卡住。
PacketQueue / FrameQueue(有界阻塞队列)
基于 BaseQueue<T> 模板实现的线程安全队列。
PacketQueue:最大容量 100,存储压缩数据包(AVPacket)FrameQueue:最大容量 5,存储解码后的帧(AVFrame),额外支持尾部保护(protectTailFrom/markDraining)
队列满时生产者阻塞等待,队列空时消费者阻塞等待,形成自然的背压机制,防止内存无限增长。
VideoDecoder / AudioDecoder(解码器)
各自运行一个解码线程,封装 FFmpeg 的 avcodec_send_packet / avcodec_receive_frame 流程。
- 视频端通过
SwsContext将各种像素格式统一转为 YUV420p - 音频端通过
SwrContext将各种采样格式重采样为播放设备支持的格式
AudioOutput(音频输出)
封装 QAudioSink,用一个内部线程将解码后的 PCM 数据写入音频设备。同时负责 驱动时钟——每次写入后根据已播放的样本数更新 Clock 的当前时间。
Clock(时钟)
基于 std::chrono::steady_clock 的单调时钟。支持暂停/恢复,线程安全。音频输出线程是时钟的唯一写入者,视频渲染线程读取时钟来判断帧应该何时显示。
VideoSurface(视频渲染面)
继承 QQuickFramebufferObject,在 Qt 的渲染线程中执行 OpenGL 渲染。核心同步逻辑在 synchronize() 中:
- 帧 PTS 落后时钟超过 120ms → 丢弃(追赶进度)
- 帧 PTS 超前时钟超过 8ms → 等待(不提前显示)
- 队列末尾 0.7s 区间内 → 保护不丢弃(保证播放到结尾时不跳帧)
GenericYuvRender(OpenGL 渲染器)
通过 C++ 模板 + YUVTraits 编译期派发,支持 YUV420p / YUV422p / YUV444p / NV12 四种像素格式。每种格式有独立的 GLSL shader,将 YUV 平面纹理上传后在 GPU 上完成 YUV→RGB 转换。
目录结构
1 | |
下一步
后续文章会逐一深入每个模块的实现细节:
- 有界阻塞队列 —
BaseQueue的实现、条件变量背压机制 - 音视频解码 — FFmpeg 解码流程与格式归一化
- A/V 同步与时钟 — 音频驱动时钟的原理、帧丢弃与尾部保护
- 音频重采样 —
SwrContext用法、采样率/声道/格式转换 - OpenGL YUV 渲染 — 模板化多格式渲染、YUV→RGB GPU shader
- 播放控制与 Seek — pause/resume/seek、Demux 线程、队列清空、I/O 中断
这次的插图来自画师RuneXiao
图片地址:https://www.pixiv.net/artworks/137010221