手把手教你用Qt写一个B站缓存视频合并工具——BiliMerg
先说点废话
事情是这样的。我平时喜欢在B站缓存视频路上看,但B站App那个缓存机制是真的让人头大——视频和音频是分开的两个.m4s文件,想存到电脑里或者发给朋友根本没法直接看。网上倒是有现成的合并工具,但要么收费,要么广告满天飞,要么就是那种一眼看上去就不想用的界面。
于是我一拍大腿:自己写一个算了。
这一写吧,就从一个简单的"合并工具"写成了一个带界面、带批量、带暂停恢复、带关机功能的完整应用。期间踩了不少坑,也学到不少东西。今天就把整个过程捋一遍,给想自己动手的朋友们一个参考。
项目名称:BiliMerger
技术选型:C++17 + Qt 6 Widgets
核心依赖:ffmpeg(外部调用)
源码地址:GitHub上搜BiliMerger就能找到
第一步:搞清楚B站的缓存结构长啥样
动手之前得先搞明白我们要处理什么。
B站App(安卓版)缓存的视频一般在手机存储的/Android/data/tv.danmaku.bili/download/目录下。每个缓存视频是一个文件夹,里面通常有这几个东西:
一个.jpg文件——视频封面图
一个video.m4s——纯视频流(没有声音)
一个audio.m4s——纯音频流(没有画面)
而且这两个.m4s文件开头还有9个字节的"脏数据",直接拿去给ffmpeg合并会报错。得先跳过这9个字节才行。
所以工具的核心逻辑就很清晰了:
扫描目录,把.jpg、video.m4s、audio.m4s三个文件配成一套
读取.jpg作为封面显示
调用ffmpeg,把去掉头部的音视频流合并成一个MP4或MKV
第二步:搭架子——项目结构怎么设计
既然是Qt项目,自然是用Qt Creator新建一个Qt Widgets Application。但我用的CMake管理项目,方便以后跨平台。
项目结构长这样:
BiliMerger/
├── CMakeLists.txt # 构建配置
├── resources.qrc # 资源文件(图标啥的)
├── icon.png / icon.ico # 程序图标
├── main.cpp # 入口
├── mainwindow.h/cpp/ui # 主窗口
├── videocard.h/cpp # 视频卡片组件
├── mergeworker.h/cpp # 单视频合并线程
└── batchmergeworker.h/cpp # 批量合并线程
为什么要分这么细?因为界面和后台逻辑得分开啊。界面卡死是Qt新手最容易犯的毛病——你在主线程跑耗时操作,界面直接冻住,用户还以为是程序崩了。所以合并这种耗时活儿必须放到子线程里干。
第三步:最核心的——怎么用Qt调用ffmpeg合并
这是整个工具的灵魂,其他都是围绕这个转的。
Qt里调用外部程序用QProcess。合并一条视频的ffmpeg命令大概长这样:
ffmpeg -y -skip_initial_bytes 9 -i video.m4s -skip_initial_bytes 9 -i audio.m4s -c:v copy -c:a copy output.mp4
-c:v copy和-c:a copy表示不重新编码,直接复制流,速度最快,而且画质音质无损。
但问题来了——B站那个9字节头怎么处理?
我试过两种方案:
用QFile读取文件,跳过前9个字节,写到临时文件里,再让ffmpeg读临时文件
用ffmpeg的-skip_initial_bytes参数直接跳
方案1太蠢了,大文件要额外写一份数据,硬盘吃不消。方案2简单高效:
ffmpeg -y -skip_initial_bytes 9 -i video.m4s -skip_initial_bytes 9 -i audio.m4s -c:v copy -c:a copy output.mp4
就多了两个参数,干净利落。
这里踩了个坑:-skip_initial_bytes这个参数不是所有版本的ffmpeg都支持。我一开始用的旧版ffmpeg就不认,报错"Option not found"。后来换成release-essentials版本就好了。所以记得提醒用户ffmpeg版本别太老。
第四步:界面怎么做才好看又好用
说实话我审美一般,但我知道一个原则:别让用户动脑子。
所以整个界面设计思路就是:选择目录 → 自动扫出视频卡片 → 点一下就能合并。
具体来说:
卡片式展示
每个视频显示封面图、标题、时长、分辨率。横向滚动排列,看着直观。
三种合并模式
单视频:点一下卡片选中,点"开始合并"就搞这一个
选择多个:每个卡片带复选框,勾几个合几个
一键全部:啥也不挑,扫到的全给合了
这里有个优先级逻辑:如果"一键全部"和"选择多个"同时勾选,以"一键全部"为准。用户不会想那么多,我们替他们决策就行。
拖拽支持
用户不一定要点"选择目录"按钮,直接把文件夹拖到窗口上也行。甚至拖单个.m4s文件进来也能自动配对找到同目录下的另一半。
这个用Qt的dragEnterEvent和dropEvent实现,不复杂但很贴心。
深色主题
纯粹是我自己喜欢,而且做深色比做浅色省事——不用纠结各种颜色的搭配,黑底白字就完了。
第五步:批量合并——暂停、恢复、取消怎么实现
批量合并是需求里最麻烦的一块。用户要能随时停,停完还能继续,或者直接取消。
我用的方案是:
主线程只负责调度,不干活
每个视频的合并单独起一个QThread(通过MergeWorker)
批量合并由BatchMergeWorker管理一个任务队列
暂停的时候,当前正在跑的MergeWorker收到暂停信号,ffmpeg进程被挂起(QProcess::suspend),但要注意——suspend在Windows上表现不太好,我最后用的是另一种方式:让工作线程在循环里检查一个m_paused标志,如果为true就QWaitCondition阻塞住。
恢复就是唤醒这个条件变量。
取消的话,直接QProcess::kill,然后清理临时文件。
这里又有个坑:QProcess::kill有时候杀不干净子进程。ffmpeg自己可能还会fork子进程,光杀父进程不够。解决方法是用操作系统的进程树杀法——Windows上用taskkill /T /F /PID xxx。
第六步:那些"锦上添花"的功能
核心功能做完了,我开始加一些边角料:
自动记住设置
用QSettings存到注册表(Windows)或配置文件(Linux/macOS)。每次启动时读回来,不用用户每次重新选输出目录、格式、模板。
文件名模板
用户可以用{标题} {日期} {序号}自定义输出文件名。这个解析逻辑是自己写的字符串替换,不复杂但很实用。
内存盘加速
如果用户有内存盘(比如RamDisk),把临时目录设到内存盘上能提速不少。其实就是改一下QTemporaryDir的路径前缀。
完成后自动关机
这个需求是群友提的——说合并一堆视频可能要几十分钟,不想守着,合完直接关机睡觉。
实现方式:合并完成后弹一个窗口,显示60秒倒计时,用户点取消就不关机。倒计时结束执行system(“shutdown -s -t 0”)。
第七步:遇到的几个印象深刻的问题
- 长视频合并卡住
有个用户反馈说合并5小时以上的视频会卡在99%不动。最后发现是ffmpeg的-skip_initial_bytes在某些情况下会导致音视频不同步,后面加了-fflags +genpts强制生成时间戳才解决。 - 界面拖拽时崩溃
Qt的拖拽事件处理不好容易野指针。特别是拖拽过程中用户又把源文件夹删了——这种极端情况得用QPointer做防御。 - 中文路径乱码
Windows下路径含中文,传给ffmpeg需要转成本地编码(GBK),不然ffmpeg识别不了。Qt的toLocal8Bit()就是干这个的。
第八步:打包发布——让不懂Qt的人也能用
Qt程序不能直接扔给别人用,因为缺一堆DLL。

打包方法:
编译Release版本
用Qt自带的windeployqt.exe把依赖DLL自动收集过来
把ffmpeg.exe也放进去
最后说几句
这个项目从开始写到今天,前前后后改了十几版。功能不算复杂,但把每个细节做好确实花了不少心思。
如果你也想写类似的工具,我的建议是:
先把核心流程跑通(扫描→配对→合并),再慢慢加花活
多考虑"用户会怎么用",而不是"我怎么实现方便"
善用Qt的线程和进程,别在主线程干重活
当然如果你完全不想动手写代码,直接用我打包好的版本也行。GitHub上搜BiliMerger,Releases里有现成的exe。
希望这篇对你有帮助。写代码这事儿吧,最开始觉得难,上手之后发现也就那么回事。关键是别怕踩坑,踩得多了就成老手了。
项目地址:https://github.com/Bologna-Zaicek/Tools-BiliMerger

有问题可以提Issue,看到会回。

386

被折叠的 条评论
为什么被折叠?



