☰
ffmpeg实战:B站m4s缓存视频合并成MP4指南
2026/10/19 3:16:03 网站建设 项目流程

简介:面向需要将B站缓存视频中的audio.m4s与video.m4s合并为mp4的用户,此压缩包提供了一套完整的小型自动化方案。资源仅5个文件,含3个bat批处理脚本和2个Python脚本,压缩包体积仅2KB,非常轻量。Python脚本承担核心逻辑,调用ffmpeg将音视频流重新封装为mp4;批处理脚本则用于批量调用、文件移动和命令行交互,让使用者无需手动输入复杂命令。这套工具直接解决了B站离线缓存分段文件无法直接播放的问题,操作简单,运行即可完成合并;目前已有8324人学习下载,适合需要整理本地B站视频的普通用户,也是学习Python自动化处理文件的入门范例。通过阅读源码,可以掌握subprocess调用外部程序、路径拼接、批量循环和异常处理等编程技巧,还能学会将ffmpeg命令行封装成可用工具的思路,对于希望提升脚本能力的开发者具有参考价值。

1. 从两个.m4s到一个 MP4:哔哩哔哩视频合并到底在解决什么问题

说到哔哩哔哩视频合并,很多人都是从手机缓存目录里翻出两个不认识的文件开始的。一个叫video.m4s,一个叫audio.m4s,语音里搜不到,播放器打开直接报错。其实这两个文件不是损坏,而是视频网站把画面和声音拆成了两条独立的媒体流,用 DASH 协议分开发给你,方便切换码率和节省流量。想把这些缓存变成能在电脑或电视上正常离线播放的单个 MP4,不能直接改后缀,也不能把两个文件简单拼一起,必须让工具按时间戳把两条流重新封装进同一个 MP4 容器。这套资源解决的就是这个完整过程:从定位缓存目录、识别音视频轨,到输出一个音画同步、可拖动进度条的画面文件。适合经常用 App 缓存视频的人、需要离线剪辑素材的人,以及想把收藏整理成本地资料库的下载党。

2. 先搞懂.m4s是什么:合并是「重新封装」而不是「格式转换」

2.1 为什么 B 站要把一条视频拆成两个文件:DASH 流的取舍

很多用户第一次在两个并列的文件夹里看到video.m4s和audio.m4s时,第一反应是缓存出错了。实际上,这是现代流媒体普遍采用的 DASH(Dynamic Adaptive Streaming over HTTP)方案的典型产物。与过去直接给你一个完整 MP4 文件不同,DASH 会把整个视频切片成若干以秒为单位的片段,每个片段独立编码、独立请求。这样做的好处是播放器可以根据当前的网络带宽,动态请求不同码率的片段,实现无缝切换清晰度。而更重要的是,视频和音频被拆成两个独立的轨道流,互不干扰,因为网络条件突变时,解码器可以只丢弃视频片段而保留音频,卡顿感更小。

回到 B 站 App 的缓存行为上。用户在客户端点「离线缓存」,实际下载到本地的是一个目录结构,里面通常有entry.json、danmaku.xml(弹幕)、video.m4s和audio.m4s。这个video.m4s存着一整段的视频画面流,audio.m4s存着对应的声音流。它们之所以被拆开,是播放器在线播放时本来就要分别请求两路流。缓存本地后,为了让离线播放也能保持同样的架构,客户端没有把它们重新拼成一个文件,而是原样保存为两个.m4s。

.m4s并不是一种独立的视频编码格式,而是一种媒体分段封装。它内部通常是 fragmented MP4(简称 fMP4)结构。fMP4 与普通 MP4 最大的不同在于,普通 MP4 的索引信息(moov box)要么在文件开头要么在文件末尾,而 fMP4 会把一些索引信息打散成多个moof+mdat小段,便于流式读取。视频编码一般是 H.264(部分 1080P 高帧率内容是 H.265/HEVC),音频编码常见的是 AAC,偶尔是 Opus。搞清楚这一点,就明白为什么「改名成 .mp4 不能播」:播放器需要从文件头寻找标准的 moov 元数据,fMP4 的元数据位置不符合普通播放器的预期。

这也解释了为什么很多缓存视频在播放器图库里显示为已下载,但导出到电脑后就成了两个「神秘文件」。本质上,你手里的两个.m4s是两个可以独立解码的流,只是缺一个混流步骤。理解了这一点,你再看所有合并工具,都会直接戳穿它们的本质:它们不是在修文件,而是在做 mux(混流)。

2.2 合并的本质:把两路流按时间线写进同一个 MP4 容器

合并不是一个简单的二进制拼接过程,也不是把两个文件交替写入。要生成一个正常的 MP4,你需要让视频流和音频流中的样本按时间戳交错排列,并且把编码信息(如分辨率、编码器、码率)写进新容器的 moov box。这一步在音视频开发中叫 remux(重新封装),与转码有本质区别。转码是重新编码,会有质量损失和较高的 CPU 耗时;remux 只是把编码数据从旧容器搬到新容器,几乎不损失画质和音质,速度极快。

所有稍微正规一点的合并工具,底层都会调用 ffmpeg 这类开源工具来实现 remux。ffmpeg 处理这种情况的核心参数是-c copy,意思是「直接复制编码数据,不重新编码」。它会把video.m4s中每一个 sample(也就是一帧)的时间戳解析出来,再把audio.m4s中每个音频包的时间戳解析出来,两者按 PTS(Presentation Time Stamp)顺序写进一个全新的 MP4 容器。这个操作对 CPU 的消耗非常低,一个 40 分钟的高清内容通常只需要几秒到十几秒,速度取决于磁盘 IO。

这里有一个常见的困惑:为什么合并后的文件大小与两个源文件之和不一样?很多人以为是「被压缩了」。其实 MP4 容器的封装本身有开销:码流在源文件的 fMP4 分段中会有一些重复的 meta 信息,合并成标准 MP4 时这些重复信息会合并,因此最终体积可能比两个.m4s加起来小一点,也可能差不多,但视频和音频本身的编码数据没有变,码率没有变,不能视为有损。判断方式很简单:看输出文件的分辨率、帧率和音频声道数是否和缓存内容一致。

另外,很多 GUI 工具默认会在合并时加上-movflags +faststart,这个参数的作用是把 moov(索引信息)移动到文件开头。这样播放器在刚打开文件时就能拿到完整的索引,进度条拖动会流畅很多。如果你只用命令行手工合并而没加这个参数,输出的 MP4 可能会出现「本地播放正常,但上传到网盘或放到电视上后,拖动卡顿」的问题。这不是文件坏了,只是索引位置对某些设备不友好。

2.3 这套资源里可能装了什么:GUI 程序、批处理脚本还是命令行工具

拿到BiliBili视频合并软件-audio.m4s-video.m4s合并成mp4.rar这样的压缩包,解压后你大概率会看到三种形态之一:第一种是图形界面程序,用 C#、PyQt 或 Electron 编写,把 ffmpeg 打包进资源目录,你只需要选择视频文件、选择音频文件、点「开始合并」;第二种是批处理脚本,例如合并.bat或merge.sh,配合一个ffmpeg.exe使用,通过拖拽文件到图标上运行;第三种是 Python 脚本,需要你本机有 Python 环境以及安装 imageio-ffmpeg 或直接调用系统 ffmpeg。资源包一般会附带一个 readme 或说明文档,写清楚依赖要求和操作步骤。

我的建议是:拿到资源后先不要急着双击运行,先看一眼目录结构和说明。如果目录下没有ffmpeg.exe而是只有脚本,说明它依赖系统 PATH 中的 ffmpeg。此时你在命令行输入ffmpeg -version能正常输出,脚本才能工作;如果提示找不到命令,你需要自行下载 ffmpeg 放到同目录,或者在脚本中写死 ffmpeg 的路径。很多翻车现场就是因为「脚本找到了但 ffmpeg 没找到」,弹出一堆乱码和 error,看起来像软件坏了,其实是环境问题。

至于选图形界面还是命令行,我的看法是:单集合并、偶尔用一次,GUI 足够;但如果一次要合并整个季度几十集,GUI 会让你点鼠标点到手酸,脚本循环一次搞定。后面章节里的命令,就是把 GUI 底下那层黑匣子打开给你看。你理解之后,再遇到 GUI 里「音画不同步」「输出文件打不开」这类问题,就知道是哪个环节出了岔子,而不是只能干瞪眼。

3. 把缓存变成 MP4:手把手操作步骤与关键参数

3.1 准备工作:找到缓存目录,并确认哪个是视频、哪个是音频

在用工具合并之前,必须先找到两个.m4s文件。Android 手机上的 B 站 App,缓存路径一般是一串包名目录下的download文件夹,每集视频单独一个子目录,子目录里才是video.m4s、audio.m4s、danmaku.xml这些文件。如果你用的是 Android 11 及以上系统,Android/data目录被限制访问,文件管理器直接进不去,需要先把整个缓存目录导出到电脑上再操作。iOS 由于沙盒机制,缓存文件只能通过 App 内分享或 iTunes 文件共享导出,无法直接在文件系统层面找到。

把缓存拷贝出来后,正常情况下一个文件夹里正好有一个大的video.m4s和一个相对小的audio.m4s。视频文件比音频文件大得多,因为画面码率通常远高于音频码率。但也有例外:某些清晰度下音频可能比视频大吗?基本不会,所以用文件大小判断最直观。

如果缓存目录里有两个.m4s但名字不是video和audio,而是类似2068、6201之类的数字编号,就需要用 ffprobe 来判断哪条是视频流。命令如下:

ffprobe -v error -show_entries stream=codec_type,codec_name,width,height -of default=noprint_wrappers=1 2068.m4s

代码说明:-v error只输出错误级别以上的日志,避免刷屏;-show_entries stream=...指定要显示流类型、编码名称、宽高;-of default=noprint_wrappers=1让输出变成一行字段,方便识别。执行后如果看到codec_type=video和width、height有值,那这个文件就是视频流;如果只有codec_type=audio,则是音频流。这一步虽然简单,但在处理批量文件时能帮你避免把两个文件写反。很多 GUI 工具会自动识别,但如果你是用批处理脚本合并,顺序写反会直接导致输出文件倒置,后面会专门讲。

3.2 用 ffmpeg 一条命令完成合并:每个参数都对应一个动作

当确认了两个文件后,最核心的合并命令如下。这里以video.m4s和audio.m4s位于当前目录为例:

ffmpeg -i video.m4s -i audio.m4s -map 0:v:0 -map 1:a:0 -c copy -movflags +faststart -y output.mp4

这条命令是合并操作的基础模板。-i video.m4s声明第一个输入文件是画面轨,-i audio.m4s声明第二个输入文件是声音轨。-map 0:v:0表示从第一个输入文件中选取视频轨,-map 1:a:0表示从第二个输入文件中选取音频轨。这里的三个数字分别是输入文件序号、流类型、流序号。加上显式 map 可以避免 ffmpeg 自动选择流时的误判,尤其当某个.m4s里同时存在多条音轨时,自动选择可能挑错。

-c copy是这条命令的灵魂,它告诉 ffmpeg 对选中的流不重新编码,直接拷贝编码数据。对于 flv、mkv、mp4 等容器,这种操作速度非常快。如果去掉-c copy,ffmpeg 会按默认编码器重新压缩视频,耗时巨大且画质下降。-movflags +faststart是一个封装优化项,它将 MP4 的索引 moov 移动到文件头部,让输出文件更适合网络播放和拖动进度条。-y则是在输出文件已存在时自动覆盖,避免中断确认。

如果执行成功,终端会显示进度条和速度,最终生成output.mp4。如果合并过程中提示类似Invalid data found when processing input或者moov atom not found,说明输入流不是标准状态。这时可以在每条-i之前加-fflags +genpts,让 ffmpeg 忽略输入流里有问题的 PTS,重新生成时间戳:

ffmpeg -fflags +genpts -i video.m4s -fflags +genpts -i audio.m4s -map 0:v:0 -map 1:a:0 -c copy -movflags +faststart -y output.mp4

-fflags +genpts会对输入流重新生成 PTS 时间戳,解决大量 DASH 缓存文件中时间戳不连续的问题。如果加上这个参数后仍然报错,可以尝试把-c copy改成-c:v libx264 -c:a aac强制转码,但那是最后手段,因为会花很长时间,且画质有损。常规操作都应该从无损 remux 开始。

3.3 用图形界面工具操作:选项和底层命令的对应关系

如果你不想记命令,直接用资源包里的 GUI 软件,操作逻辑和上面的参数是逐项对应的。常规流程是:双击打开主程序 → 通过「打开文件」或拖拽方式把video.m4s和audio.m4s分别填入视频框和音频框 → 设置输出目录和文件名 → 点击「开始合并」。有些工具做得更智能,会自动搜索目标目录里的两个.m4s并自动配对,你只需要点一下「合并」即可。

GUI 工具还有几个常用开关:一个是「快速合并(不重新编码)」,对应-c copy,默认开启,速度很快;另一个是「输出为 MP4(适合网络播放)」,对应-movflags +faststart,建议保持勾选。有些工具提供「修复音画同步」选项,对应-fflags +genpts或-itsoffset。当你遇到音画不同步问题时,打开这个选项往往比手动处理更省事,但代价是合并时间略长一点。

使用 GUI 时有一个容易忽略的点:输出文件的路径不要和输入文件在同一目录且同名。如果输出文件叫video.mp4,而输入叫video.m4s,一起放在同一个文件夹里,很多工具会误把新生成的video.mp4当成输入,导致二次处理或覆盖出错。更稳妥的做法是单独建一个output目录,把所有合并结果集中放进去,既不污染原始缓存,也方便后续批量管理。此外,GUI 工具通常会在界面底部显示底层命令日志,如果你熟悉命令行,可以直接复制这段日志去排查。如果日志中出现红色error,别急着忽略,绝大多数翻车都能从最后一行找到线索。

3.4 一次合并一整个季度:用目录遍历脚本解锁批量操作

当你有几十集缓存要合并,再一个个打开 GUI 点按钮就不现实了。此时写一个简单的循环脚本,配合 ffmpeg,可以一次性处理整个目录。以下是一个在 Linux / macOS 终端中可用的 Bash 示例,它会遍历当前目录的所有子目录,找到名为video.m4s的文件,并在同一目录下寻找audio.m4s,然后输出为以目录名命名的 MP4:

#!/bin/bash for video in */video.m4s; do dir=$(dirname "$video") audio="$dir/audio.m4s" out="${dir}/${dir%/}.mp4" if [ ! -f "$audio" ]; then echo "[跳过] $dir 中缺少 audio.m4s" continue fi echo "[正在合并] $dir" ffmpeg -y -i "$video" -i "$audio" -map 0:v:0 -map 1:a:0 \ -c copy -movflags +faststart "$out" >/dev/null 2>>merge.log if [ $? -eq 0 ]; then echo "[完成] $out" else echo "[失败] $dir 合并出错,查看 merge.log" fi done

脚本的逻辑其实很直接:for video in */video.m4s负责把当前目录下每个子文件夹里的video.m4s路径取出来。dirname去掉文件名部分,得到目录路径。然后拼出audio.m4s路径和输出文件名。>/dev/null 2>>merge.log把正常的进度信息丢弃,把错误信息追加到日志文件,避免终端刷屏。脚本运行结束后,如果某些集失败,merge.log里会记录具体的 ffmpeg 输入输出路径和报错信息。

在运行这个脚本前,最好先确认所有子目录里都是同一部作品的多个分集,因为脚本会把当前目录下所有子目录都处理一遍。如果混入了其他视频目录,输出文件名也会跟着变乱。Windows 用户可以用for /d %%i in (*) do (...)的批处理版本,但涉及路径含空格时需要额外加引号。我一般会优先用 Bash,因为路径处理比 CMD 干净很多,也不容易踩到编码问题。

4. 避坑指南:合并失败的四类症状与排查方法

4.1 音画不同步:先看起始时间戳,再决定是否偏移

现象:合并后的 MP4 播放时,嘴巴对不上声音,画面比声音快了半秒,而且越到后面偏差越明显,变成「伪双轨」。

原因:DASH 缓存文件里的视频流和音频流,起始时间戳不是对齐的。例如视频流的 PTS 从0.017秒开始,音频流的 PTS 从0.000秒开始,两者相差 17 毫秒,在 30 分钟的视频里累积起来就是几十毫秒级别的偏移,加上播放器的缓冲策略不同,最终体现为持续的错位。另一个常见原因是 fMP4 分段文件里存在丢帧或重复帧,导致时间戳序列不连续,而 ffmpeg 在-c copy模式下会原样保留这些时间戳。

解决:合并前先用 ffprobe 查看两条流的 start_time 是否一致:

ffprobe -v error -show_entries stream=codec_type,start_time -of default=noprint_wrappers=1 video.m4s ffprobe -v error -show_entries stream=codec_type,start_time -of default=noprint_wrappers=1 audio.m4s

如果start_time不一样,可以用-itsoffset对其中一条流做固定时间偏移。比如视频流 start_time 是0.017,音频流是0.000,可以这样合并:

ffmpeg -itsoffset 0.017 -i video.m4s -i audio.m4s -map 0:v:0 -map 1:a:0 -c copy -movflags +faststart -y output.mp4

-itsoffset 0.017会把第一个输入流的所有时间戳整体延迟 17 毫秒。注意这个偏移量有正负之分,如果视频比音频晚,偏移量用负值。如果不知道具体差多少,更稳妥的做法是加-fflags +genpts让 ffmpeg 重新生成时间戳,然后再播放检查。实战中八成以上的音画不同步都可以靠+genpts解决,因为它会强制让 ffmpeg 为新容器内容打上连续的时序。

4.2 合并后没有声音或声音是杂音:问题多半在音频编码

现象:画面正常,但播放器显示没有音频轨道;或者有声音但像是失真、沙沙声、音量爆炸。

原因:B 站部分缓存音频不是标准的 AAC-LC,而是 Opus 或 HE-AAC。虽然 MP4 容器支持 Opus,但很多播放器(尤其是电视、车载系统)的 MP4 解码器只对 AAC-LC 做了支持。另有一种情况是audio.m4s文件本身不完整,比如缓存中途被清理,导致音频流缺少结束标记。

解决:先用 ffprobe 看音频编码是什么:

ffprobe -v error -show_entries stream=codec_name,sample_rate,channels -of default=noprint_wrappers=1 audio.m4s

如果codec_name是opus,那就别用-c copy了,把音频转成 AAC 再封装:

ffmpeg -i video.m4s -i audio.m4s -map 0:v:0 -map 1:a:0 -c:v copy -c:a aac -b:a 192k -movflags +faststart -y output.mp4

这条命令中-c:v copy仍然保持视频不重编码,只把音频重新编码成 192kbps 的 AAC。192k 是 44.1kHz 立体声音频在 128k 和 256k 之间的平衡点,人耳听起来足够通透,文件也不会膨胀太多。如果你的缓存音量本来是正常的,转码后不应该变差。如果转码后依然没有声音,那就把audio.m4s单独丢进 ffmpeg 转成 WAV 试听一下,确定是源文件损坏而不是封装问题。源损坏的情况下,任何合并工具都救不回来,只能重新缓存。

4.3 报错moov atom not found或Invalid data:先尝试重封装备用方案

现象:ffmpeg 在执行合并时直接退出,终端提示moov atom not found或Invalid data found when processing input;GUI 工具也弹窗报错,合并进度条一直停在 0%。

原因:.m4s是分段式 fMP4,它的索引信息散布在多个moof里,而不是像普通 MP4 那样集中在moov。如果工具读取时没有正确识别分段结构,就会认为文件头缺少必要的 moov;或者缓存过程中文件没有完整写盘,尾部缺失了mfra(分片索引)信息。这也解释了为什么同一个文件用某些播放器能播,放到 ffmpeg 里却报错——不同工具对 fMP4 的容错程度不一样。

解决:第一步,先尝试加-fflags +genpts重新生成时间戳,有时只是 PTS 问题导致解析中断。第二步,如果第一步不行,用-c copy附加-movflags skip_trailer输出试一下:

ffmpeg -fflags +genpts -i video.m4s -fflags +genpts -i audio.m4s \ -map 0:v:0 -map 1:a:0 -c copy -movflags +faststart+skip_trailer -y output.mp4

skip_trailer会忽略文件尾部的某些损坏块,对 M4S 这种分段流比较实用。但如果源文件本身不完整,输出视频很可能在最后几秒出现声音消失或画面凝固,这时只能回去重新检查缓存。第三步,如果以上都不行,就放弃零损耗,改用转码方案兜底:

ffmpeg -i video.m4s -i audio.m4s -map 0:v:0 -map 1:a:0 -c:v libx264 -preset veryfast -crf 20 -c:a aac -b:a 192k -y output.mp4

-preset veryfast让 x264 在编码速度和压缩率之间偏向速度,-crf 20是接近视觉无损的质量档位。这个命令会重新编码视频,虽然耗时增加,但能绕过几乎所有源文件解析问题。作为一个经验原则:先试 remux,再试+genpts,最后才用重编码。不要一开始就转码,那是性能杀手段。

4.4 合并后无法拖动或手机不播放:封装兼容性参数没调对

现象:在电脑上用 PotPlayer 能正常播,放到 iPhone 相册或某些电视播放器里却打不开;或者打开进度条能拖动,但一拖就卡死,然后软件崩掉。

原因:最常见的两个坑:一是输出 MP4 没有加faststart,moov 在文件尾部,某些播放器需要先下载整个文件才能读到索引,所以无法快进;二是视频编码是 H.265/HEVC,而旧手机或电视盒子不支持硬解 HEVC,一旦拖动到关键帧,解码器跟不上,就表现成卡死。还有一种情况是输出文件虽然扩展名是.mp4,但内部 brand 不是标准 MP4,某些播放器会识别为未知格式。

解决:如果你用命令行合并,建议把输出品牌强制为 MP4 标准,并加上 faststart:

ffmpeg -i video.m4s -i audio.m4s -map 0:v:0 -map 1:a:0 -c copy -brand mp42 -movflags +faststart -y output.mp4

-brand mp42把容器品牌标记为 MP4 v2,这是兼容性最好的 MP4 标志之一。如果视频编码是 HEVC,无法换硬件解码,那就需要转成 H.264:

ffmpeg -i video.m4s -i audio.m4s -map 0:v:0 -map 1:a:0 -c:v libx264 -preset fast -crf 21 -c:a copy -movflags +faststart -y output.mp4

这里-c:a copy表示音频流不重编码,因为 AAC 本来就是 MP4 标准音频,没必要二次处理。如果你只打算在自己的电脑或现代电视上播放,HEVC 通常没问题;但如果要给朋友传文件,或者放到老设备上,H.264 依然是保险的选择。判断是否 HEVC 的方法,用 ffprobe 看codec_name,如果是hevc就说明命中了这个坑。

5. 进阶玩法:批量合并、字幕封装与输出验收

5.1 批量合并的完整形态:支持异常目录和日志记录的脚本

第 3 章的循环脚本可以处理「每个子目录里恰好有标准命名的两个 m4s」的理想情况,但实际缓存目录里经常混有不完整的文件、多音轨目录、甚至是已经合并过的 MP4。为了避免处理到旧输出,我通常会在脚本里加上类型判断和输出日志,形成一套可反复使用的流程。

#!/bin/bash mkdir -p merged for dir in */; do dir=${dir%/} video="$dir/video.m4s" audio="$dir/audio.m4s" if [ ! -f "$video" ]; then continue fi if [ ! -f "$audio" ]; then echo "[警告] $dir 缺少 audio.m4s,跳过" >> merge_report.txt continue fi out="merged/${dir}.mp4" if [ -f "$out" ]; then echo "[跳过] $out 已存在" | tee -a merge_report.txt continue fi ffmpeg -y -fflags +genpts -i "$video" -fflags +genpts -i "$audio" \ -map 0:v:0 -map 1:a:0 -c copy -movflags +faststart -brand mp42 "$out" \ 2>>merge_report.txt if [ $? -eq 0 ]; then echo "[OK] $out" | tee -a merge_report.txt else echo "[FAIL] $dir" | tee -a merge_report.txt fi done

这个脚本最大的改进是merged输出目录和对已有文件的检查。-brand mp42避免兼容性问题,+genpts预处理时间戳,tee -a让终端显示和日志文件双轨记录,方便事后查看哪些集失败了。还有一个细节:用dir=${dir%/}去掉目录末尾的斜杠,否则拼接路径时会出现双斜杠,虽然 Linux 不介意,但 Windows 环境就容易出问题。

对于大量文件的场景,我不建议用find -exec ... &并行合并。ffmpeg 本身对磁盘 IO 和 CPU 的压力都不小,并行 4 个任务往往会把硬盘怼到 100%,反而拖慢总时间。串行循环虽然慢,但是稳定、能排查、不会出现几十个进程同时写日志导致乱序的问题。如果你想要并行,可以限定最多 2 个后台任务,并等它们结束再继续下一批,这个复杂度对于家庭用户来说不值得。

5.2 弹幕字幕和封面一起封装进 MP4:三个命令把收藏做完整

B 站缓存的danmaku.xml是弹幕,它不是标准字幕文件。想把它放到 MP4 里,得先转成 ass 格式。很多资源包没有集成这一步,所以需要借助额外工具,我这里只说封装命令。

假设你已经把danmaku.xml转成了subtitle.ass,现在把字幕轨道加进之前合并好的output.mp4:

ffmpeg -i output.mp4 -i subtitle.ass -map 0 -map 1:0 -c copy -c:s mov_text -metadata:s:s:0 language=chi output_with_sub.mp4

这个命令把第一个输入(已合并的视频)的所有流和第二个输入的字幕流同时映射到输出。-c copy对视频和音频生效,-c:s mov_text把 ass 字幕转成 MP4 内置字幕常用的 mov_text 编码。mov_text的优点是兼容性好,缺点是只支持基本的样式,弹幕中的彩色字体、滚动效果会丢失。如果你的播放器支持外部字幕,保留独立的.ass文件反而更清晰。这一步只是「锦上添花」,不是合并缓存视频的必要步骤。

封面封装稍微冷门一点。如果你下载了视频封面cover.jpg,想把它设成 MP4 的封面(在某些播放器列表里显示缩略图):

ffmpeg -i output.mp4 -i cover.jpg -map 0 -map 1 -c copy -disposition:v:1 attached_pic -metadata:s:v:1 mimetype=image/jpeg output_cover.mp4

-map 1把封面图片作为第二条视频流输入,-disposition:v:1 attached_pic将这条图片流标记为「附件图」,主流的 MP4 播放器会把它当成封面,而不会当作额外视频轨道显示出来。如果你不需要这两个功能,完全可以忽略。核心思路是:合并工具只解决音视频混流,而完整的本地视频库管理还需要字幕封面这些元数据,你用命令操作时,等于是把工具的功能向前延伸了一点点。

5.3 用 ffprobe 验收:一份输出文件到底能不能用

合并完成不是终点,我建议养成「合并后必查流信息」的习惯,否则万一输出文件有问题,等到看完整部剧才发现音轨延迟,那才叫血亏。验收命令只有一个:

ffprobe -v error \ -show_entries stream=index,codec_name,codec_type,width,height,channels,duration \ -of compact output.mp4

这段命令会把每个流的序号、编码、类型、宽高、声道数和时长逐条列出来。你需要检查三处:视频流宽高是否和缓存清晰度一致,音频流 codec_name 是否为 aac,以及视频时长与音频时长差是否在 0.2 秒以内。如果视频流显示 hevc 且你目标设备是旧手机,就要考虑转码;如果音频流显示 opus,说明前面的-c copy命令把非标准编码也带出来了,需要重封装成 AAC;如果时长差超过 0.5 秒,基本可以判定音画不同步已经存在。

一个更严苛的验收方式是检查视频流的 start_time:

ffprobe -v error -show_entries stream=codec_type,start_time -of csv output.mp4

输出会显示视频流和音频流的 start_time。它们相差越小越好,一般小于0.050秒都可以接受。如果视频流是0.000,音频流是0.100,那就有 100 毫秒的延迟,虽然肉眼不一定看得出来,但如果要做剪辑素材,这 100 毫秒会在剪辑软件里变成明显的错位。遇到这种情况,要么重新用-itsoffset修复,要么就用 GUI 工具的「修复音画同步」重来一次。验收这一步看起来多花 30 秒,但它能避免你日后在播放器里反复定位问题。

6. 一个让合并提速的小技巧:把工具变成「拖拽即用」

6.1 在 Windows 上做一个小脚本,把两个文件拖进图标直接合并

如果你已经熟悉了命令行,仍然会觉得每次输入ffmpeg -i ...很啰嗦。一个实用的小技巧是:在资源包所在目录放一个批处理脚本,平时不需要打开终端,直接把video.m4s和audio.m4s按顺序拖到这个脚本图标上,就能自动调用 ffmpeg 输出结果。以下是一个适合 Windows 的merge.bat脚本:

@echo off chcp 65001 >nul if "%~1"=="" goto usage if "%~2"=="" goto usage set VIDEO=%~1 set AUDIO=%~2 set OUT=%~dp1_merged.mp4 ffmpeg -y -fflags +genpts -i "%VIDEO%" -fflags +genpts -i "%AUDIO%" ^ -map 0:v:0 -map 1:a:0 -c copy -movflags +faststart -brand mp42 "%OUT%" if %errorlevel%==0 ( echo. echo 合并完成: "%OUT%" ) else ( echo. echo 合并失败,请检查文件是否完整 ) pause exit /b :usage echo 请把 video.m4s 和 audio.m4s 两个文件依次拖到本脚本图标上 pause exit /b

脚本里的%1是第一个拖入文件的完整路径,%2是第二个。%~dp1是第一个文件所在目录,%~dp1_merged.mp4会把合并结果放在原始视频目录下并加上_merged后缀,避免覆盖输入文件。chcp 65001将命令行编码切换为 UTF-8,否则包含中文的路径会显示乱码或传参错误。if %errorlevel%==0判断 ffmpeg 的退出码,为 0 说明合并成功,否则就提示失败。

这个脚本有一个前提:你拖入的顺序必须是视频在前、音频在后。如果顺序反了,-map 0:v:0会取到音频文件里的视频流(通常不存在),合并就会失败。为了更稳妥,你可以在脚本里加一个按文件大小交换顺序的步骤。视频流文件通常比音频大,比较一下两个文件大小,自动把大的放在视频位置。做法是在set VIDEO=%~1后加一段 if 比较%~z1和%~z2,但文件大小在批处理里获取比较麻烦,用for %%F in ("%1") do set size1=%%~zF也能拿到。我个人习惯是在拖入前先看一下文件名,毕竟video.m4s比audio.m4s多几个字母,肉眼就能分出来。

6.2 把它扩展到更多场景:拖一部剧的文件夹进去,自动找文件

如果你觉得拖两个文件还不够省事,可以再写一个merge_folder.bat,把整个缓存目录拖进去,脚本自动遍历该目录下的所有子文件夹,找到每组video.m4s和audio.m4s并合并。这样你就不需要在乎文件名和顺序了,只要缓存目录结构是 B 站默认的形态,脚本就能处理。

这个进阶脚本的批处理版本会比较长,因为 CMD 的for /r递归遍历和中文字符编码处理都不够优雅。我一般会建议这类需求直接用 PowerShell 或 Bash 写,如果非要 Bat,也可以借助for /d %%i in ("%~1\*") do遍历子目录,然后对每个子目录检查文件是否存在,调用同一个 ffmpeg 命令。核心思想和前面第 5 章的 Bash 脚本一致,只是语法换了个牌子。

从那以后,我每次合并 B 站缓存视频都会先做两件事:用 ffprobe 看一眼时间戳和编码,然后把文件拖进自制的批处理脚本里。顺序错了不用担心,输出名自动带_merged后缀,再补一条 ffprobe 验收,整个过程不超过一分钟。这种「命令 + 小脚本」的组合,比单双击图形按钮更可复用,出了问题时也能定位到是 ffmpeg 哪一步在报错,而不是对着软件弹窗干着急。如果你也经常整理缓存视频,希望这个习惯能帮你少走一些弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询