QGC二次开发本地媒体浏览实战(二)FFmpeg最小系统实战

文章目录

大家好,这篇接上一篇继续写。

上一篇主要记录的是,我在 QGC 二次开发里做本地媒体浏览时,为什么 Android 正常,而 Windows 在 Qt5 + DirectShow 这条链路下会出问题。

结论其实已经很清楚了:

  • Android 上 MediaPlayer + VideoOutput 基本能用
  • Windows 上图片浏览没问题
  • 但本地视频播放不稳定
  • 最终问题落在 DirectShow 这条本地多媒体后端链路上

那接下来就只有两种选择:

  1. Windows 视频不在软件里播,直接外部打开
  2. 自己接管 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

这样改的好处是很明显的:

  1. 上层媒体页 UI 不动
  2. Android 已经验证过可用的链路不动
  3. 只在 Windows 下替换本地视频播放核心

这一步其实很重要。因为一旦你把 UI 和后端一起重做,事情就会立刻变复杂。

三、先把目标收小:只做一个最小系统

我这里一直说"FFmpeg 最小系统",不是为了起名字,而是因为这个边界必须先收住。

我这次的目标不是做一个完整播放器,而只是解决媒体浏览页里的 Windows 本地视频播放问题。也就是说,这套最小系统只负责:

  1. 打开本地视频文件
  2. 找到视频流
  3. 打开解码器
  4. 解出视频帧
  5. 显示首帧
  6. 连续播放
  7. 支持暂停、停止和拖动进度

这里明确不追求:

  • 音频
  • 倍速
  • 字幕
  • 缩略图
  • 硬解

先把最关键的本地视频播放链路跑通,后面的能力再慢慢补。

四、先把 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 不稳,那最直接的思路就是:本地文件不要让它自己去开,而是我自己开。

也就是:

  1. 用 Qt 自己的文件类打开本地文件
  2. 自己提供 read/seek 能力
  3. 再把这套读取能力交给 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、控制栏这些事情,先确认一件事:

第一帧能不能解出来。

八、我的第一目标不是"先播起来",而是"先把首帧解出来"

这一步我觉得非常值得单独记一下。

做这种最小系统时,最容易一开始就想把所有能力都打通。但实际更高效的做法是,先把首帧解出来。

因为只要首帧能出来,基本就说明:

  1. 文件打开没问题
  2. 容器识别没问题
  3. 视频流找到了
  4. 解码器打开了
  5. 图像已经能送到界面显示

我这里当时解首帧的核心代码大致是这样:

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 最小系统
  • 只替换本地视频后端,不重做媒体浏览页

这次我自己总结下来,最关键的几个点就是:

  1. 先做平台分流,不要把 Android 和 Windows 强绑在一个后端上。
  2. 先做最小系统,先把首帧解出来,不要一开始就追完整播放器。
  3. 如果直接通过路径打开本地文件不稳,就自己接管文件读取。
  4. 首帧能出来以后,剩下的问题通常就收敛到播放时序和线程归属。
  5. 这套方案真正的价值,不是"技术上更复杂",而是它能把 Windows 本地视频播放这件事稳定接住。

所以如果你也在做 QGC 二次开发,而且需求是:

  • Android 继续用 Qt5 本地播放器
  • Windows 也要在软件内部稳定播放本地视频

那我的建议很直接:

Windows 这条线,别继续硬扛 DirectShow,直接上一个 FFmpeg 最小系统。

对工程来说,这比反复试错要省事得多。

相关推荐
rookie软工3 小时前
Qt代理委托实现
开发语言·python·qt
feiyangqingyun3 小时前
Qt/C++源码/地图控件/实时动态轨迹/历史轨迹回放/无人机/飞行轨迹/性能强悍
qt·轨迹回放·实时轨迹
不被定义的~wolf3 小时前
qt小游戏贪吃蛇
qt
火山上的企鹅4 小时前
QGC二次开发本地媒体浏览实战(一)Qt5+DirectShow 在 Android正常_Windows为什么出问题
android·qt·媒体·qgc
肖恭伟4 小时前
Cursor(VSCode) + clangd 无法跳转 Qt 类/变量
ide·vscode·qt
csdn_zhangchunfeng4 小时前
Qt之智能指针使用建议
开发语言·qt
王江奎13 小时前
FFmpeg 中编译和使用 soxr 重采样引擎
ffmpeg·音视频
优选资源分享15 小时前
小丸工具箱 vR236|ffmpeg 图形化视频压制工具
ffmpeg·音视频
U-52184F6918 小时前
深度解析:从 Qt 的 Q_D 宏说起,C++ 工业级 SDK 是如何保证 ABI 稳定性的
数据库·c++·qt