tips

FlowPick 是怎么在浏览器里把几百个视频切片合成 MP4 的

不靠服务器、不装 FFmpeg、不转码——拆解 FlowPick 在浏览器标签页内完成 HLS/DASH 分片合并的完整技术路径。
FlowPick 团队
13 分钟阅读
# 技术 # ffmpeg # wasm # hls # dash # 架构

你点击 FlowPick 的下载按钮,等几分钟,一个完整的 MP4 出现在下载文件夹里。

这件事有点反直觉。浏览器不是下载工具,更不是视频处理工具。传统的流媒体下载方案无一例外依赖本地安装的程序,或者把数据发到服务器处理再发回来。

FlowPick 全程在浏览器标签页内完成,没有服务器,没有 App,没有命令行。这篇文章解释为什么这在技术上是可行的,以及实际是怎么做到的。

先搞清楚问题是什么

HLS 视频在传输时不是一个完整的文件。它是一个播放列表文件(M3U8)加上几百个 2-10 秒的切片.ts 文件)。播放器读清单,按顺序拉切片,实时拼接播放。

一个 30 分钟的 1080P 视频,按 10 秒切片就是 180 个文件。DASH 格式类似,只是切片是 .m4s,且音频和视频在两条独立的流里传输。

"下载"这个视频意味着什么?

  1. 解析 M3U8/MPD,拿到所有切片 URL
  2. 逐个下载几百个切片
  3. 如果有 AES-128 加密,解密每个切片
  4. 把几百个碎片拼成一个连续的文件
  5. 如果是 HLS TS 切片,还要重封装成 MP4(播放器友好的格式)
  6. 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-TSMP4
设计初衷广播传输,丢包容忍文件存储,随机访问
每个包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 的处理:

  1. 解析 MPD XML,识别视频 AdaptationSet 和音频 AdaptationSet
  2. 同时下载两路切片(并行)
  3. 各自下载 init segment(包含编解码器参数,合并时必须有)
  4. 把两路数据送进 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 解密。没有哪一个是"黑魔法",都是现代浏览器标准能力的直接应用。


推荐阅读