个人项目开发工具 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/30 7:08:03 阅读更多 →
你们知道吗?Linux Mint 曾被寄予厚望取代 Windows

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

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

2026/10/26 17:47:42 阅读更多 →
IPv4:地址、掩码、分片与 TTL 到底在管什么

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

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

2026/11/1 7:27:40 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/11/2 0:25:10 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/11/2 10:46:11 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/11/2 10:47:40 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/11/2 1:16:51 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/11/2 10:46:31 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/11/2 10:46:26 阅读更多 →

日新闻

周新闻

月新闻