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

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

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

2026/8/3 9:00:47 阅读更多 →
IPv4:地址、掩码、分片与 TTL 到底在管什么

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

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

2026/8/3 9:00:47 阅读更多 →

最新新闻

角色扮演 Agent 突然插入 JSON 结果?我用 Taotoken 实测 3 种分隔标记的翻车率

角色扮演 Agent 突然插入 JSON 结果?我用 Taotoken 实测 3 种分隔标记的翻车率

深度解析:为什么分隔问题如此棘手 在进一步的研究中,我们发现这个问题的复杂性远超预期。通过 Taotoken 的细粒度日志分析,可以识别出四个关键挑战点: 1. 模型对结构化数据的认知偏差 大型语言模型在训练过程中接触到的数据分布…

2026/8/4 15:50:00 阅读更多 →
NVMe和SATA选哪个?记住这几点

NVMe和SATA选哪个?记住这几点

买固态硬盘的时候,经常看到NVMe和SATA两个词,价格差不少,速度差更多,到底该选哪个?今天帮你理清楚。 一、先搞清一个概念 很多人把M.2和NVMe混为一谈。两者其实不是一回事:M.2是一种物理接口形态&#xff0…

2026/8/4 15:50:00 阅读更多 →
吉林楼宇对讲安装

吉林楼宇对讲安装

在现代社会,楼宇对讲系统已成为许多住宅小区和商业楼宇的标配。它不仅提升了居住的安全性,还为居民提供了极大的便利。本文将通过具体数据和案例,探讨吉林地区楼宇对讲系统的安装与应用,并提供一些实操建议。1. 楼宇对讲系统的重要…

2026/8/4 15:50:00 阅读更多 →
【AI能效比跃升37%实战指南】:20年架构师亲授低功耗大模型部署黄金公式

【AI能效比跃升37%实战指南】:20年架构师亲授低功耗大模型部署黄金公式

更多请点击: https://kaifayun.com 第一章:AI能效比跃升37%的底层逻辑与行业拐点 AI能效比(Energy Efficiency Ratio, EER)——即单位能耗下完成的AI推理/训练任务量——在2024年Q2实现37%的系统性跃升,其驱动力并非单…

2026/8/4 15:50:00 阅读更多 →
最小可运行示例:用 curl 搭建全平台视频元数据解析服务

最小可运行示例:用 curl 搭建全平台视频元数据解析服务

为什么需要一个最小可运行示例 面对一个陌生 API,很多开发者第一件事不是读完整文档,而是先把它跑起来。一个能复制、粘贴、执行的请求示例,可以快速确认网络连通性、鉴权方式、参数格式和响应结构,比逐行阅读文档更高效。 本文以…

2026/8/4 15:50:00 阅读更多 →
MPV播放器配置深度解析:从技术架构到专业级调优实战指南

MPV播放器配置深度解析:从技术架构到专业级调优实战指南

MPV播放器配置深度解析:从技术架构到专业级调优实战指南 【免费下载链接】mpv_PlayKit 🔄 mpv player 播放器折腾记录 Windows conf | 中文注释配置 汉化文档 快速帮助入门 | mpv-lazy 懒人包 Win11 x64 config | 着色器 shader 滤镜 filter 整合方案 …

2026/8/4 15:49:00 阅读更多 →

日新闻

AI Agent白手起家26: 使用标准事件驱动大模型实践

AI Agent白手起家26: 使用标准事件驱动大模型实践

纲要 练习目标:掌握大模型标准事件的调用回顾 LangChain 中的核心标准事件 invokestreambatchastream_eventswith_structured_output 环境准备实战代码:多种事件调用对比 同步调用与流式输出批量处理异步事件流监听结构化输出 运行说明与预期结果总结与扩…

2026/8/4 0:00:40 阅读更多 →
dealsea是什么?跨境卖家必知的美国deal站入门指南

dealsea是什么?跨境卖家必知的美国deal站入门指南

说实话,第一次听说美国这个老牌折扣网站的跨境卖家,十个有八个会问同一个问题:这个平台到底是干嘛的?我见过一个做家居出口的朋友,他在亚马逊上月销二十万美金,却从来没用过它。我给他看了首页——一屏一屏…

2026/8/4 0:01:40 阅读更多 →
清华大学重磅EST:植物自导电闪蒸焦耳热600°C/2600°C两步法!稀土超积累植物秒级转化为CeO₂-石墨烯电催化剂!

清华大学重磅EST:植物自导电闪蒸焦耳热600°C/2600°C两步法!稀土超积累植物秒级转化为CeO₂-石墨烯电催化剂!

通讯作者:邓兵、刘建国通讯单位:清华大学DOI:https://doi.org/10.1021/acs.est.6c00603研究背景稀土元素(REEs)是清洁能源技术与电子器件不可或缺的核心原料,然而传统提取方式依赖能耗高、排放大的采矿与强…

2026/8/4 0:01:40 阅读更多 →

周新闻

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

1. 从水管网络到最大流:一个核心问题的诞生想象一下,你是一个城市供水系统的总工程师。你的城市有多个水源(水库),需要通过一个复杂的地下管道网络,将水输送到各个居民区。每条管道都有其最大通水能力&…

2026/8/4 13:24:41 阅读更多 →
基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/8/4 11:41:39 阅读更多 →
MATLAB xcorr函数详解:从互相关原理到四大实战应用

MATLAB xcorr函数详解:从互相关原理到四大实战应用

1. 从一次信号“找茬”说起:为什么我们需要互相关几年前,我在处理一组声学传感器数据时遇到了一个棘手的问题。我有两个麦克风记录了一段相同的音频信号,理论上它们接收到的声音波形应该非常相似,只是由于麦克风位置不同&#xff…

2026/8/4 5:26:40 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/4 13:38:24 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/4 11:09:16 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/4 13:38:40 阅读更多 →