个人项目开发工具 BiliMerger 功能设计记录:需求判断与实现取舍
BiliMerger发布后偏向设计层面的问题——为什么要做三种模式、暂停恢复是怎么考虑的、自动关机是怎么设计的。这些问题涉及的不是代码本身而是开发过程中的需求判断和功能取舍。这篇文章记录BiliMerger开发过程中遇到的需求场景、功能设计思路以及具体的实现细节。功能背景B站App的缓存机制将视频和音频分开存储为独立的.m4s文件。每个缓存视频目录下通常包含一个.jpg封面文件、一个video.m4s视频流文件和一个audio.m4s音频流文件。用户若想将缓存视频导出为可播放的单一文件需要将音视频流合并。此过程涉及两个核心问题.m4s文件开头存在9字节的脏数据合并前需去除音视频流需通过外部工具ffmpeg进行合并。基于此需求BiliMerger的定位确定为自动扫描B站缓存目录配对音视频文件清理脏数据后调用ffmpeg合并为单一MP4或MKV文件。脏数据的处理刚开始做的时候我以为直接把两个.m4s文件丢给ffmpeg就能合并。结果ffmpeg报错提示无法识别格式。后来查了资料才发现B站缓存的.m4s文件开头有9个字节的脏数据。用十六进制编辑器打开一看开头9个字节全是30ASCII字符’0’第10个字节开始才是真正的文件头。这9个字节不删掉ffmpeg就没法正常读取。最开始的处理方案是用QFile读取文件从第9个字节开始往后复制到临时文件。这个方案在小文件上没问题但遇到几GB的大文件时额外读写一份数据对磁盘IO压力很大速度也慢。cleanDirtyData函数里做了这件事bool MergeWorker::cleanDirtyData(const QStringsrcPath, const QStringdstPath, qint64 totalSize, int startPercent, int endPercent, bool* cancelled){QFile src(srcPath);QFile dst(dstPath);if(!src.open(QIODevice::ReadOnly))returnfalse;if(!dst.open(QIODevice::WriteOnly)){src.close();returnfalse;}if(!src.seek(9)){src.close();dst.close();returnfalse;}const qint64 BUFFER_SIZE2*1024*1024;QByteArray buffer(BUFFER_SIZE, Qt::Uninitialized);qint64 processed0;while(!src.atEnd()){while((int)m_paused!(int)m_stopped){ QThread::msleep(100);QCoreApplication::processEvents();} if((int)m_stopped){*cancelledtrue;break;} qint64 readsrc.read(buffer.data(),BUFFER_SIZE);if(read0)break;if(dst.write(buffer.data(),read)!read){ src.close();dst.close();return false;} processedread;int percentstartPercent(int)((double)processed/totalSize*(endPercent-startPercent));emit progress(percent,);QCoreApplication::processEvents();}src.close();dst.close();return!(*cancelled);}后来查阅ffmpeg文档发现-skip_initial_bytes参数可以直接跳过输入文件开头的指定字节数省去了写临时文件的步骤。但要注意这个参数不是所有ffmpeg版本都支持。ffmpeg合并参数bool MergeWorker::mergeVideo(const QStringvideoPath, const QStringaudioPath, const QStringoutputPath, bool* cancelled){QProcess ffmpeg;QStringList args;args-ivideoPath-iaudioPath;if(m_qualityPreset0){args-c:vcopy-c:acopy;}else{args-c:vlibx264;switch(m_qualityPreset){case1: args-crf18-presetmedium;break;case2: args-crf23-presetfast;break;case3: args-crf28-presetfast;break;}args-c:aaac-b:a192k;}args-max_muxing_queue_size4096-movflagsfaststart-youtputPath;ffmpeg.start(m_ffmpegPath, args);while(!ffmpeg.waitForFinished(500)){while((int)m_paused!(int)m_stopped){QThread::msleep(100);QCoreApplication::processEvents();}if((int)m_stopped){*cancelledtrue;ffmpeg.kill();ffmpeg.waitForFinished(1000);returnfalse;}}returnffmpeg.exitCode()0;}-c:v copy -c:a copy直接复制流不重新编码速度最快且画质无损。质量预设非0时用libx264重新编码通过CRF控制画质。-max_muxing_queue_size 4096解决长视频合并时的队列溢出问题。-movflags faststart让MP4文件在网页上能边下边播。三种合并模式最初版本只支持单视频合并后续扩展了三种模式。startMerge()中通过优先级判断选择对应的处理分支void MainWindow::startMerge(){if(m_ffmpegPath.isEmpty()||!QFile::exists(m_ffmpegPath)){QMessageBox::critical(this,错误,ffmpeg.exe未找到);return;}QString outDirui-outputPathEdit-text().trimmed();if(outDir.isEmpty()||!QFile::exists(outDir)){outDirQCoreApplication::applicationDirPath();ui-outputPathEdit-setText(outDir);}bool multiModeui-multiSelectCheck-isChecked();bool batchAllModeui-batchModeCheck-isChecked();if(batchAllMode){// 创建批量工作线程处理m_allVideos全部列表}elseif(multiMode){QListQVariantMapselectedVideosgetSelectedVideos();if(selectedVideos.isEmpty()){QMessageBox::critical(this,错误,请先勾选要合并的视频);return;}// 创建批量工作线程处理selectedVideos}else{if(m_currentVideoFile.isEmpty()||m_currentAudioFile.isEmpty()){QMessageBox::critical(this,错误,请先选择一个视频);return;}// 创建单视频工作线程}}当“一键全部”与“选择多个”同时开启时以“一键全部”为准。此设计基于用户操作意图的判断——当用户开启全量模式时其核心需求即为处理全部文件而非手动筛选。多选模式下getSelectedVideos()遍历所有卡片收集勾选状态QListQVariantMapMainWindow::getSelectedVideos(){QListQVariantMapselected;for(VideoCard* card:qAsConst(m_cards)){if(card-isChecked()){selected.append(card-getData());}}returnselected;}暂停、恢复与取消通过QAtomicInt原子变量控制工作线程状态QAtomicInt m_paused;QAtomicInt m_stopped;void MergeWorker::pause(){m_paused1;}void MergeWorker::resume(){m_paused0;}void MergeWorker::stop(){m_stopped1;m_paused0;}在cleanDirtyData和mergeVideo函数的循环中检查状态。暂停时线程阻塞但不退出恢复时继续执行。停止时终止ffmpeg进程并清理资源。取消操作中遇到过一个问题直接killffmpeg进程后子进程可能残留。ffmpeg.kill()在Qt内部会尝试终止整个进程树但某些情况下子进程仍会残留。后续版本考虑用taskkill /T /F /PID作为补充。文件拖拽配对dropEvent中处理两种拖拽场景void MainWindow::dropEvent(QDropEvent* e){QListQUrlurlse-mimeData()-urls();if(urls.isEmpty())return;QStringList folders;QStringList m4sFiles;for(const QUrlurl:qAsConst(urls)){QString pathurl.toLocalFile();QFileInfo info(path);if(info.isDir()){folders.append(path);}elseif(path.endsWith(.m4s, Qt::CaseInsensitive)){m4sFiles.append(path);}}if(folders.size()1){scanMultipleFolders(folders);return;}if(folders.size()1){ui-sourcePathEdit-setText(folders.first());return;}if(m4sFiles.size()1){QString pathm4sFiles.first();QDirdirQFileInfo(path).dir();QStringList allM4sdir.entryList(QStringList()*.m4s, QDir::Files);if(allM4s.size()2){QListQPairQString, qint64fileSizes;for(const QStringf:qAsConst(allM4s)){QString fullPathdir.absoluteFilePath(f);fileSizes.append({fullPath, QFileInfo(fullPath).size()});}std::sort(fileSizes.begin(), fileSizes.end(),[](const autoa, const autob){returna.secondb.second;});m_currentAudioFilefileSizes.first().first;m_currentVideoFilefileSizes.last().first;// 更新界面}}}配对依据是文件大小——B站缓存的音频流通常比视频流小按大小排序即可区分。文件名模板模板支持{标题}、{日期}、{序号}三个占位符QString VideoCard::applyTemplate(const QStringtmpl, const QStringtitle, int index){QString resulttmpl;result.replace({标题}, title);result.replace({日期}, getCurrentDate());result.replace({序号}, QString::number(index));result.remove(QRegularExpression([\\\\/:*?\|]));if(result.isEmpty())resultmerged_video;returnresult;}移除非法字符是为了确保生成的文件名在Windows下有效。设置记忆使用QSettings存储配置支持下次启动恢复void MainWindow::saveSettings(){QSettings settings(BiliMerger,Settings);settings.setValue(sourcePath, ui-sourcePathEdit-text());settings.setValue(outputPath, ui-outputPathEdit-text());settings.setValue(geometry, saveGeometry());settings.setValue(batchMode, ui-batchModeCheck-isChecked());settings.setValue(outputFormat, ui-formatCombo-currentIndex());settings.setValue(nameTemplate, ui-templateEdit-text());settings.setValue(memoryDisk, ui-memoryDiskCheck-isChecked());}void MainWindow::loadSettings(){QSettings settings(BiliMerger,Settings);ui-sourcePathEdit-setText(settings.value(sourcePath,).toString());ui-outputPathEdit-setText(settings.value(outputPath, QCoreApplication::applicationDirPath()).toString());ui-batchModeCheck-setChecked(settings.value(batchMode,false).toBool());ui-formatCombo-setCurrentIndex(settings.value(outputFormat,0).toInt());ui-templateEdit-setText(settings.value(nameTemplate,{标题}).toString());ui-memoryDiskCheck-setChecked(settings.value(memoryDisk,false).toBool());restoreGeometry(settings.value(geometry).toByteArray());}自动关机用户勾选“完成后关机”后合并结束时触发void MainWindow::shutdownSystem(){QProcess::startDetached(shutdown, QStringList()/s/t60);QMessageBox* msgBoxnew QMessageBox(this);msgBox-setWindowTitle(即将关机);msgBox-setText(合并完成将在60秒后自动关机\n\n点击【取消】可取消关机);msgBox-setIcon(QMessageBox::Information);msgBox-addButton(取消, QMessageBox::RejectRole);msgBox-setStandardButtons(QMessageBox::NoButton);connect(msgBox,QMessageBox::buttonClicked, this,[this](QAbstractButton* button){if(button-text()取消){QProcess::startDetached(shutdown, QStringList()/a);log(已取消关机);}});msgBox-show();}60秒倒计时的设计给用户留出取消关机的缓冲时间。源码及下载地址https://github.com/Bologna-Zaicek/Tools-BiliMerger

相关新闻

SpringBoot+Vue构建售后管理系统的技术实践

SpringBoot+Vue构建售后管理系统的技术实践

1. 项目概述:SpringBootVue产品售后管理系统 这个基于SpringBootVue的售后管理系统,是一个典型的前后端分离架构项目。我去年在一家家电企业实施过类似的系统,当时用这个架构解决了他们售后流程混乱的问题。系统采用JavaMySQL技术栈&#xff…

2026/10/10 2:44:26 阅读更多 →
你们知道吗?Linux Mint 曾被寄予厚望取代 Windows

你们知道吗?Linux Mint 曾被寄予厚望取代 Windows

作为一名长期关注操作系统生态的科技博主,我经常收到读者咨询:想摆脱 Windows 的束缚,又担心学习成本太高,Linux Mint 是不是最好的选择? Linux Mint 确实是目前最受 Windows 用户欢迎的 Linux 发行版之一。它基于 Ubuntu,界面采用 Cinnamon 桌面环境,布局和操作习惯与…

2026/10/6 10:46:45 阅读更多 →
IPv4:地址、掩码、分片与 TTL 到底在管什么

IPv4:地址、掩码、分片与 TTL 到底在管什么

专栏:《计算机网络基础》 对应总览: 第四篇 网络层:让包跨越「很多个网络」 本篇角色: IPv4 专章——地址怎么写、掩码怎么算、TTL / 分片在路径上管什么 承接:《包怎么跨过很多个网络?网络层一次讲清》 读…

2026/10/11 22:52:25 阅读更多 →

最新新闻

MATLAB摄像头调试实战:webcam与videoinput全解析

MATLAB摄像头调试实战:webcam与videoinput全解析

简介:计算机视觉项目中,第一步往往是从摄像头获取图像帧。面向MATLAB初学者的摄像头采集与图像二值化示例程序,可直接运行调试,解决实时视频帧读取、灰度转换和自动阈值分割等基础问题。压缩包内仅含1个.m源文件,整体大…

2026/10/13 12:03:51 阅读更多 →
WinForm嵌入WPF控件:ElementHost桥接与避坑指南

WinForm嵌入WPF控件:ElementHost桥接与避坑指南

简介:面向C#桌面开发者的示例工程,展示如何在Winform应用中承载并刷新WPF DataGrid控件,解决跨框架UI集成的常见需求。资源以ElementHost为互操作核心,完整涵盖WPF用户控件设计、DataGrid列定义与样式配置、DataContext数据绑定、…

2026/10/13 12:03:51 阅读更多 →
激光雷达四强对比:禾赛速腾与Velodyne Ouster技术路线及选型指南

激光雷达四强对比:禾赛速腾与Velodyne Ouster技术路线及选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 12:03:51 阅读更多 →
ARM服务器离线部署Harbor 2.13.1:从拆包到避坑全指南

ARM服务器离线部署Harbor 2.13.1:从拆包到避坑全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 12:03:51 阅读更多 →
C#步进电机S型加速曲线代码包:从梯形到七段式规划与定时器实现

C#步进电机S型加速曲线代码包:从梯形到七段式规划与定时器实现

简介:面向自动化设备、机器人及精密定位场景中的嵌入式与上位机开发者,提供基于C#的步进电机S型加速曲线实现,重点解决电机启停瞬间冲击大、速度过渡不平滑的问题。压缩包共44个文件、1.16MB,涵盖cs源码、exe可执行程序、pdb调试符…

2026/10/13 12:03:51 阅读更多 →
嵌入式Linux显示驱动调试工具链实战:从黑屏到花屏的定位方法

嵌入式Linux显示驱动调试工具链实战:从黑屏到花屏的定位方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 12:02:50 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →