FlowPick 是怎么在浏览器里把几百个视频切片合成 MP4 的
你点击 FlowPick 的下载按钮,等几分钟,一个完整的 MP4 出现在下载文件夹里。
这件事有点反直觉。浏览器不是下载工具,更不是视频处理工具。传统的流媒体下载方案无一例外依赖本地安装的程序,或者把数据发到服务器处理再发回来。
FlowPick 全程在浏览器标签页内完成,没有服务器,没有 App,没有命令行。这篇文章解释为什么这在技术上是可行的,以及实际是怎么做到的。
先搞清楚问题是什么
HLS 视频在传输时不是一个完整的文件。它是一个播放列表文件(M3U8)加上几百个 2-10 秒的切片(.ts 文件)。播放器读清单,按顺序拉切片,实时拼接播放。
一个 30 分钟的 1080P 视频,按 10 秒切片就是 180 个文件。DASH 格式类似,只是切片是 .m4s,且音频和视频在两条独立的流里传输。
"下载"这个视频意味着什么?
- 解析 M3U8/MPD,拿到所有切片 URL
- 逐个下载几百个切片
- 如果有 AES-128 加密,解密每个切片
- 把几百个碎片拼成一个连续的文件
- 如果是 HLS TS 切片,还要重封装成 MP4(播放器友好的格式)
- DASH 的话,还要把视频轨和音频轨合并
这不是简单的文件下载,是一条处理流水线。传统工具把这些交给 FFmpeg 命令行或服务器,FlowPick 把整条流水线搬进了浏览器。
检测是第一步:webRequest API
在处理切片之前,FlowPick 需要知道页面上有什么视频。
浏览器扩展可以通过 webRequest API 在网络层监听所有请求——不是 DOM 层,而是实际的 HTTP 请求层。每一个从当前标签页发出的网络请求,FlowPick 都能看到请求 URL 和响应头。
FlowPick 扫描 Content-Type 和 URL 特征,把命中 HLS/DASH/直链视频/音频/图片特征的请求记录下来。当你打开 FlowPick 弹窗,看到的那个列表就是它截止此刻截获的所有媒体资源。

这也解释了为什么视频没开始播放,FlowPick 就看不到流——M3U8 请求只在播放器初始化时发出,FlowPick 只能看到已经发生的请求,不能主动去探测。
核心难题:把 TS 切片变成 MP4
视频切片下载完,格式问题来了。
HLS 的切片是 MPEG-TS 格式(.ts)。直接把 180 个 .ts 文件首尾相接二进制拼起来,你会得到一个 .ts 大文件——这个文件部分播放器能播,但大多数设备、视频编辑软件、视频平台偏好 MP4。
TS 和 MP4 的核心区别是容器格式:
| MPEG-TS | MP4 | |
|---|---|---|
| 设计初衷 | 广播传输,丢包容忍 | 文件存储,随机访问 |
| 每个包 | 188 字节固定大小 | 可变长 Box 结构 |
| 索引位置 | 无独立索引,只能顺序读 | moov atom,可放文件头 |
| 随机跳转 | 麻烦 | 直接用 moov 索引跳 |
把 TS 变成 MP4,本质上是提取里面的视频和音频裸数据,重新装进 MP4 的 Box 结构。这个操作叫 remux(重封装),不碰编解码器,视频和音频的每一个比特都原封不动,只是换了个"壳"。
重封装不需要显卡,CPU 压力也很小,主要时间花在文件 I/O 上。这是 FlowPick 能在浏览器里做它的技术前提——如果需要重新编码(transcoding),浏览器的 CPU 算力根本应付不来。
两套引擎的选择逻辑
FlowPick 内部有两套重封装引擎,根据场景自动切换:
引擎一:FFmpeg WASM
FFmpeg 是行业标准的视频处理命令行工具,支持几乎所有格式。WebAssembly(WASM) 是一种可以在浏览器里运行接近原生速度的二进制格式。把 FFmpeg 编译成 WASM,就能在浏览器标签页内调用 FFmpeg 的全部功能。
FlowPick 调用 FFmpeg WASM 时,实际执行的命令大概是这样:
ffmpeg -f concat -safe 0 -i filelist.txt \
-c copy \
-movflags +faststart \
-bsf:a aac_adtstoasc \
output.mp4
参数分解:
| 参数 | 作用 |
|---|---|
-f concat | 用拼接解复用器处理多个 TS 片段 |
-c copy | 流复制,零重编码 |
-movflags +faststart | 把索引移到文件头,支持边下边看 |
-bsf:a aac_adtstoasc | 转换 AAC 封装格式(TS 用 ADTS,MP4 用 ASC) |
-c copy 是关键——它确保 FlowPick 做的是纯重封装,没有任何质量损失,也不需要高 CPU 算力。
WASM 的 FFmpeg 跑在浏览器的 JavaScript 引擎里,有性能损耗,但重封装是 I/O 密集型任务,不是 CPU 密集型,所以实际表现不差。根据实测数据(Chrome + Intel i7-13700 + 16GB RAM):
| 视频 | 切片数 | 文件大小 | FFmpeg WASM 重封装耗时 |
|---|---|---|---|
| 10 分钟 1080P | ~60 | ~200 MB | ~3 秒 |
| 30 分钟 1080P | ~180 | ~600 MB | ~8 秒 |
| 60 分钟 4K | ~360 | ~2.5 GB | ~30 秒 |
| 120 分钟 4K | ~720 | ~5 GB | ~60 秒 |
这个速度对日常使用已经够用了。
FFmpeg WASM 的限制: WASM 运行在 32 位地址空间,理论内存上限 4GB。在实际场景下,FFmpeg WASM 需要把所有切片写入虚拟文件系统(WASM 内存),大文件场景会吃掉大量内存,超过限制就会崩溃。
引擎二:TSToMP4Muxer
这是 FlowPick 自研的一套纯 JavaScript 流式重封装器,专门处理 TS → MP4 这一个场景,但做法完全不同。
FFmpeg WASM 的模式是:先把所有切片集齐,再统一处理。TSToMP4Muxer 的模式是:边下载边处理边写盘。
具体来说,TSToMP4Muxer 在切片下载回来的同时,实时解析 TS 数据包(每个包固定 188 字节),提取出 PES(Packetized Elementary Stream)数据,实时转换为 MP4 Box 格式,通过浏览器的 Streams API 持续写到磁盘。
好处显而易见:
- 内存占用与文件大小无关 — 处理 5GB 视频和处理 200MB 视频内存占用差不多
- 不需要加载 FFmpeg WASM(30MB+ 的 WASM 文件),启动更快
- 天然支持大文件 — 内存里同时只存一小段数据
代价是覆盖场景更窄:只做单 TS → MP4,无法处理 DASH 音视频合并这类需要两路输入的任务。
FlowPick 的切换逻辑是:普通 HLS 优先走 TSToMP4Muxer;遇到 DASH 或者需要合并两路流的情况,走 FFmpeg WASM。文件超过 2GB,也会优先用 TSToMP4Muxer 或者直接输出 TS 跳过重封装,避免 WASM 内存溢出。
文件写到哪里去:File System Access API
处理好的视频数据往哪放?在传统下载工具里这不是问题,但浏览器 JavaScript 默认没有直接写文件系统的权限。
FlowPick 使用 Chrome 和 Edge 提供的 File System Access API(FSA)。用户选择保存路径后,FSA 提供一个可写的文件句柄,允许 JavaScript 以流式方式持续写入,而不是先在内存里凑齐所有数据再一次性写出。
这意味着:哪怕是 5GB 的 4K 视频,内存里同时只存当前正在处理的那一小块数据。FlowPick 理论上没有文件大小上限(受限的是磁盘空间和 WASM 内存上限,后者通过 TSToMP4Muxer 绕开)。
Firefox 也支持,但走的是 Blob 方案,内存行为和 Chrome FSA 略有差异,超大文件时稳定性稍弱。
AES-128 解密:Web Crypto API
部分 HLS 流会给每个切片加密,M3U8 里有这样一行:
#EXT-X-KEY:METHOD=AES-128,URI="https://api.example.com/key?token=..."
FlowPick 检测到这个标签后,先向 Key URI 发请求拿密钥(带上你当前的 Cookie 和会话,因为是从浏览器扩展里发的请求,就是你浏览器的真实会话)。拿到密钥后,对每个下载完的切片,用浏览器内置的 Web Crypto API 做 AES-128-CBC 解密,再把解密后的数据送进重封装流程。
整个密钥获取和解密过程完全在浏览器本地发生,密钥不会经过 FlowPick 的任何服务器。
FlowPick 处理不了的: Widevine、PlayReady、FairPlay 等 DRM 体系。这类内容保护把解密密钥绑定在专用硬件或操作系统模块里,JavaScript 访问不到,这是技术限制,不是功能取舍。
DASH:音视频分开下载再合并
DASH 比 HLS 多一层复杂度——音频和视频是两条独立的流,分别切片,分别传输。你下载的不是一堆完整视频切片,而是一堆纯视频切片(没有声音)加上一堆纯音频切片(没有画面)。
FlowPick 对 DASH 的处理:
- 解析 MPD XML,识别视频 AdaptationSet 和音频 AdaptationSet
- 同时下载两路切片(并行)
- 各自下载 init segment(包含编解码器参数,合并时必须有)
- 把两路数据送进 FFmpeg WASM,执行音视频合并
合并命令大概是:
ffmpeg \
-f concat -safe 0 -i video_list.txt \
-f concat -safe 0 -i audio_list.txt \
-c:v copy -c:a copy \
-movflags +faststart \
output.mp4
同样是 -c copy,零重编码。主要时间花在并发下载两路切片和最后的合并 I/O 上。
并发下载:切片不用一个个来
几百个切片如果串行下载,速度完全由单个 HTTP 请求的延迟决定。FlowPick 使用多线程并发下载,默认 2 个并发(保守值,避免触发服务端限速),设置里可以调到 4-6 个。
几个切片并行下载,哪个先完成就先处理哪个,但写入文件时要按序号重排,确保拼接顺序正确。FlowPick 内部维护一个下载队列和有序写入缓冲区处理这个细节。
下载失败的切片会自动重试(默认 3 次)。切片粒度小,单个失败重试代价很小,不需要从头来。
输出格式的实际差异
FlowPick 支持两种输出:
MP4:重封装后的标准 MP4,moov atom 放在文件头(faststart),兼容性最好,适合长期保存、上传、视频编辑。代价是需要重封装这一步,大视频要多等几十秒。
TS:把所有切片直接二进制拼接,零 CPU 开销,几乎瞬间完成。但文件比 MP4 大 5-15%(TS 容器本身的开销),且部分设备和平台对 TS 支持不如 MP4 好。
如果你下完准备传 B 站或者导进剪映,选 MP4。如果你只是暂存一下或者打算继续用 FFmpeg 命令行处理,选 TS,省时间。
在浏览器里做这件事有什么代价
说完能做什么,也说清楚做不到什么:
没有重编码。 FlowPick 只换容器,不碰编解码器。H.265 的视频出来还是 H.265,老播放器不支持 H.265 就还是放不了,FlowPick 帮不了这个。
FFmpeg WASM 内存有上限。 4GB 的 32 位 WASM 地址空间,加上浏览器自身的开销,实际上能用的比 4GB 少。2GB 以上的文件自动切换策略(TSToMP4Muxer 或直接输出 TS)就是为了绕开这个限制。
WASM 比原生 FFmpeg 慢。 WASM 不是原生代码,有性能损耗。60 分钟 4K 视频 FFmpeg WASM 需要约 30 秒完成重封装,原生 FFmpeg 可能在 5 秒内搞定。对于日常使用,30 秒可以接受;如果你每天处理大量内容,原生 FFmpeg 命令行更合适。
没有硬件加速。 FFmpeg 原生版本可以调用 NVENC/QSV/VideoToolbox 做 GPU 加速编解码,WASM 版本没有这个能力。不过 -c copy 不涉及编解码,所以这个限制在 FlowPick 的使用场景里影响不大。
DRM 绕不开。 这是硬限制,不是技术取舍。Widevine 的设计目标就是让浏览器扩展访问不到密钥。
整体来看,FlowPick 能在浏览器里完整处理 HLS/DASH 下载,靠的是几个关键技术点的组合:WebAssembly 让 FFmpeg 能在浏览器里跑,-c copy 把处理成本压到只有 I/O,TSToMP4Muxer 解决大文件内存问题,File System Access API 解决大文件写盘问题,Web Crypto API 处理 AES-128 解密。没有哪一个是"黑魔法",都是现代浏览器标准能力的直接应用。
推荐阅读
- 如何下载 M3U8/HLS 流:完整入门指南 — 原理懂了,操作看这篇
- 代码开源了!三个月肝出 FlowPick 的完整历程 — 这些原理是怎么被逼出来的,开发故事版
- 什么是 DASH 流媒体?MPD 文件解析 — DASH 的音视频分离设计为什么要存在
- 如何下载 Bilibili 视频到本地 — B 站的 DASH 是上述技术的典型真实案例
- 如何批量下载网页上的所有图片 — 同一个扩展,图片也能批量搞定
- FlowPick 与 Video DownloadHelper 对比 — 为什么"不需要 CoApp"是个大事