文章目录
-
- 效果图
- 一、先看整体替代思路
- [二、先做平台分流,不要一上来重写整套 UI](#二、先做平台分流,不要一上来重写整套 UI)
- 三、先把目标收小:只做一个最小系统
- [四、先把 FFmpeg 接进工程,不然后面全是假问题](#四、先把 FFmpeg 接进工程,不然后面全是假问题)
-
- [1. FFmpeg 路径不要写死成想当然的位置](#1. FFmpeg 路径不要写死成想当然的位置)
- [2. MSVC 下宏定义别漏](#2. MSVC 下宏定义别漏)
- 五、一个实际问题:本地文件居然打不开
- 六、这时候最稳的办法就是:自己接管本地文件读取
- 七、打开流和解码器,先别想着一步到位
- 八、我的第一目标不是"先播起来",而是"先把首帧解出来"
- 九、首帧能出来但视频不动,重点就去查线程和定时器
- 十、最后再把播放控制接回到媒体页
- 十一、最后总结
大家好,这篇接上一篇继续写。
上一篇主要记录的是,我在 QGC 二次开发里做本地媒体浏览时,为什么 Android 正常,而 Windows 在 Qt5 + DirectShow 这条链路下会出问题。
结论其实已经很清楚了:
- Android 上
MediaPlayer + VideoOutput基本能用 - Windows 上图片浏览没问题
- 但本地视频播放不稳定
- 最终问题落在 DirectShow 这条本地多媒体后端链路上
那接下来就只有两种选择:
- Windows 视频不在软件里播,直接外部打开
- 自己接管 Windows 本地视频播放
如果只是做个演示版本,第一种当然最快。但如果项目是要交付的,用户既然都已经在软件里进了媒体浏览页,那一般都会默认视频也应该能直接播放。
所以我最后还是走了第二条路:在 Windows 上单独加一套 FFmpeg 最小播放系统。
效果图

一、先看整体替代思路
第二篇其实不需要再画很长的流程图,因为核心思路很简单:
- 上层还是 QGC 的统一媒体浏览页
- Android 继续走 Qt5 自带播放器
- Windows 单独切到 FFmpeg 最小系统
也就是说,不是把整个媒体浏览页面推倒重做,而是只替换 Windows 下的视频播放后端。
QGC 媒体浏览页
统一视频预览层
Android:Qt5 MediaPlayer + VideoOutput
Windows:FFmpeg 最小系统
文件读取
解码
渲染输出
播放控制
所以这篇文章真正要解决的问题只有一个:
Windows 下如何把本地视频播放从 DirectShow 切到一套最小可用的 FFmpeg 系统。
二、先做平台分流,不要一上来重写整套 UI
我这次没有去重做媒体浏览页面,也没有去改 Android 已经能用的那套逻辑。
最先做的是平台分流,让同一个视频预览页在不同平台走不同后端。
关键代码如下:
qml
readonly property bool useWindowsLocalPlayer: XScreenTool.isWindows
XLocalVideoPlayer {
id: ffmpegPlayer
source: root.useWindowsLocalPlayer ? root.fileUrl : ""
videoOutput: ffmpegOutput
}
MediaPlayer {
id: mediaPlayer
source: root.useWindowsLocalPlayer ? "" : root.fileUrl
autoPlay: false
}
这段代码的意义很直接:
- Android:继续用 Qt5 自带
MediaPlayer - Windows:切到自定义
XLocalVideoPlayer
这样改的好处是很明显的:
- 上层媒体页 UI 不动
- Android 已经验证过可用的链路不动
- 只在 Windows 下替换本地视频播放核心
这一步其实很重要。因为一旦你把 UI 和后端一起重做,事情就会立刻变复杂。
三、先把目标收小:只做一个最小系统
我这里一直说"FFmpeg 最小系统",不是为了起名字,而是因为这个边界必须先收住。
我这次的目标不是做一个完整播放器,而只是解决媒体浏览页里的 Windows 本地视频播放问题。也就是说,这套最小系统只负责:
- 打开本地视频文件
- 找到视频流
- 打开解码器
- 解出视频帧
- 显示首帧
- 连续播放
- 支持暂停、停止和拖动进度
这里明确不追求:
- 音频
- 倍速
- 字幕
- 缩略图
- 硬解
先把最关键的本地视频播放链路跑通,后面的能力再慢慢补。
四、先把 FFmpeg 接进工程,不然后面全是假问题
因为你表面上可能已经写了播放器类、QML 也接上了,但如果 FFmpeg 头文件和库文件根本没真正编进工程,后面看到的很多现象其实都是伪问题。
这一步我自己主要踩了两个点。
1. FFmpeg 路径不要写死成想当然的位置
很多时候大家习惯直接找:
text
C:\ffmpeg
D:\ffmpeg
但实际项目里能用的 FFmpeg 头文件和库文件,不一定就在这些地方。这个时候关键不是"路径写得多标准",而是构建脚本必须真的能找到可用的头文件和库。
2. MSVC 下宏定义别漏
FFmpeg 头文件接进来以后,MSVC 下很容易先碰到这一类编译问题,所以相关宏要先处理干净。
例如:
pro
DEFINES += __STDC_CONSTANT_MACROS
DEFINES += __STDC_LIMIT_MACROS
这一步如果不先收掉,后面别说运行,连编译都过不了。
五、一个实际问题:本地文件居然打不开
SDK 接进来以后,按理说下一步就是交给 FFmpeg 去打开视频文件。
本来我也以为这是最简单的一步,结果实际一试,日志里直接报了这种错误:
text
[XLocalVideoPlayer] opening "C:\Users\42014\Documents\QPilot Pro\Video\out.mp4"
[XLocalVideoPlayer] error: "Unable to open video file: Protocol not found"
一开始看到这条错误其实挺别扭的,因为这是本地文件,不是网络流,按理说不应该和 protocol 扯得这么深。
但实际做下来,这条错误反而把方向说明白了:
不能继续依赖当前这套环境直接通过路径去打开本地文件。
也就是说,问题不一定在路径字符串本身,而是在这条"把文件路径直接交给 FFmpeg 协议层"的做法上。
六、这时候最稳的办法就是:自己接管本地文件读取
既然直接把路径交给 FFmpeg 不稳,那最直接的思路就是:本地文件不要让它自己去开,而是我自己开。
也就是:
- 用 Qt 自己的文件类打开本地文件
- 自己提供 read/seek 能力
- 再把这套读取能力交给 FFmpeg
关键代码如下:
cpp
QFile* file = new QFile(path);
if (!file->open(QIODevice::ReadOnly)) {
setError(QStringLiteral("Unable to open local file."));
return false;
}
unsigned char* buffer = static_cast<unsigned char*>(av_malloc(bufferSize));
_ioContext = avio_alloc_context(
buffer,
bufferSize,
0,
file,
&XLocalVideoPlayer::readPacket,
nullptr,
&XLocalVideoPlayer::seekPacket
);
_formatContext = avformat_alloc_context();
_formatContext->pb = _ioContext;
_formatContext->flags |= AVFMT_FLAG_CUSTOM_IO;
这一步改完以后,思路就完全变了。
现在 FFmpeg 只负责:
- 容器解析
- 视频流识别
- 视频帧解码
而"本地文件怎么读取"这件事,完全由我们自己接管。
这一步是整个最小系统里非常关键的一步,因为它直接把前面那个 Protocol not found 绕过去了。
七、打开流和解码器,先别想着一步到位
文件读取链路接上以后,下一步就是按最普通的 FFmpeg 方式把流和解码器打开。
关键代码如下:
cpp
if (avformat_open_input(&_formatContext, nullptr, nullptr, nullptr) < 0) {
setError(QStringLiteral("Unable to open video file."));
return false;
}
if (avformat_find_stream_info(_formatContext, nullptr) < 0) {
setError(QStringLiteral("Unable to read stream info."));
return false;
}
_videoStreamIndex = av_find_best_stream(_formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
然后再把视频解码器打开:
cpp
AVCodecParameters* codecPar = _formatContext->streams[_videoStreamIndex]->codecpar;
const AVCodec* codec = avcodec_find_decoder(codecPar->codec_id);
_codecContext = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(_codecContext, codecPar);
avcodec_open2(_codecContext, codec, nullptr);
到这一步为止,目标还是不要放太大。先别急着想连续播放、seek、控制栏这些事情,先确认一件事:
第一帧能不能解出来。
八、我的第一目标不是"先播起来",而是"先把首帧解出来"
这一步我觉得非常值得单独记一下。
做这种最小系统时,最容易一开始就想把所有能力都打通。但实际更高效的做法是,先把首帧解出来。
因为只要首帧能出来,基本就说明:
- 文件打开没问题
- 容器识别没问题
- 视频流找到了
- 解码器打开了
- 图像已经能送到界面显示
我这里当时解首帧的核心代码大致是这样:
cpp
while (av_read_frame(_formatContext, _packet) >= 0) {
if (_packet->stream_index == _videoStreamIndex) {
avcodec_send_packet(_codecContext, _packet);
if (avcodec_receive_frame(_codecContext, _frame) == 0) {
emit frameReady(convertFrameToImage(_frame));
break;
}
}
av_packet_unref(_packet);
}
当时日志里能看到类似下面这些信息:
text
[XLocalVideoPlayer] stream codec h264 size 1920 x 1080 pixFmt 0 durationMs 345966
[XLocalVideoPlayer] frameReady positionMs 0 format 0
看到这两行的时候,我基本就确定方向是对的了。因为这时候问题已经不是"这条链路能不能走通",而只是"怎么把它连续跑起来"。
九、首帧能出来但视频不动,重点就去查线程和定时器
首帧出来以后,继续播放时又碰到一个典型问题:画面有了,但视频不往下走。
继续看日志,能看到这种报错:
text
QObject::startTimer: Timers cannot be started from another thread
到这一步,问题范围其实就已经很小了:
- 解码链路已经基本通了
- 现在卡住的是播放推进逻辑
- 更准确一点说,是播放循环和线程归属没理顺
我后面处理时,核心思路就是把播放定时器和真正工作的对象放到同一线程里。
像这种代码就要特别注意对象归属:
cpp
_playbackTimer = new QTimer(this);
_playbackTimer->setTimerType(Qt::PreciseTimer);
connect(_playbackTimer, &QTimer::timeout, this, &XLocalVideoPlayer::decodeNextFrame);
这里关键不在 QTimer 本身,而在于:
谁在启动它,它又跟谁处在同一个线程里。
这一步处理完以后,视频就从"只显示第一帧"变成"可以连续播放"。
十、最后再把播放控制接回到媒体页
前面文件读取、流解析、首帧显示和播放推进都打通以后,最后一步就是把这个 Windows 本地播放器重新接回媒体浏览页。
这里的控制逻辑不用复杂,核心就是平台分流和播放控制。
比如播放入口我这里就是这样做的:
qml
function startPlayback() {
if (fileUrl === "" || !active || hasPlaybackError) {
return
}
if (useWindowsLocalPlayer) {
ffmpegPlayer.play()
} else {
mediaPlayer.play()
}
}
这样一来,上层媒体页还是原来的那套:
- 点击视频
- 自动播放
- 可以暂停
- 可以停止
- 可以拖进度
只是 Windows 下的具体执行者已经不再是 DirectShow 那套本地多媒体路径,而是自定义的 FFmpeg 本地播放器。
十一、最后总结
第二篇做到这里,其实核心结论已经很明确了。
第一篇的结论是:
- Android 这条线可以继续走 Qt5 自带播放器
- Windows 下问题收束到 Qt5 + DirectShow 的本地视频链路
第二篇的结论则是:
- 不要继续在 DirectShow 这条线上反复磨
- 直接给 Windows 单独接一套 FFmpeg 最小系统
- 只替换本地视频后端,不重做媒体浏览页
这次我自己总结下来,最关键的几个点就是:
- 先做平台分流,不要把 Android 和 Windows 强绑在一个后端上。
- 先做最小系统,先把首帧解出来,不要一开始就追完整播放器。
- 如果直接通过路径打开本地文件不稳,就自己接管文件读取。
- 首帧能出来以后,剩下的问题通常就收敛到播放时序和线程归属。
- 这套方案真正的价值,不是"技术上更复杂",而是它能把 Windows 本地视频播放这件事稳定接住。
所以如果你也在做 QGC 二次开发,而且需求是:
- Android 继续用 Qt5 本地播放器
- Windows 也要在软件内部稳定播放本地视频
那我的建议很直接:
Windows 这条线,别继续硬扛 DirectShow,直接上一个 FFmpeg 最小系统。
对工程来说,这比反复试错要省事得多。