浏览器里转换 M3U8:内存开销、FFmpeg 引擎与上线时的 25 MiB 限制
M3U8 转 MP4 的页面可以很轻,但它背后的媒体引擎并不小。开发时,我们解决了分片、音轨和 AES-128 解密,准备上线又遇到一个完全不同的问题:FFmpeg 的 WASM 文件超过了托管平台的单文件大小限制。本地能够转换,不代表同样的资源布局可以直接发布。
这篇文章记录浏览器转换的资源取舍,以及这次部署限制的解决过程。数据来自本项目构建产物与 2026 年 10 月 1 日的检查记录。这里只讨论我们当前使用的单线程引擎和静态站点方案,不把结果延伸为所有 FFmpeg 或所有托管平台的通用配置。
为什么把转换放在浏览器里?
当前工具没有服务器端视频转换接口。播放列表和媒体资源由用户浏览器获取,解密与 MP4 封装也在浏览器中执行。网站服务器主要提供页面和引擎文件;它不代替用户下载、转码并保存完整视频。
这个选择减少了网站端的视频处理负担,但计算和内存需求转移到了用户设备。来源仍会收到来自浏览器的资源请求,浏览器仍要遵守跨域策略。所谓“在本地处理”并不代表媒体请求完全离线,也不代表工具能访问任意受保护来源。
我们选择复制已有音视频编码进行 MP4 封装,没有增加重新编码步骤。这样的操作通常比重新编码节省计算,也能保留原有画质,但它受目标容器兼容性限制。如果来源编码或流结构不适合当前封装路径,工具仍可能失败,而不会自动进行一次耗时的转码。
256 MiB 下载限制不是内存使用上限
最需要向用户解释的资源限制是:文件大小不等于浏览器内存占用。当前实现累计媒体下载数据,并设定 256 MiB 的预算。初始化段、独立音轨等实际取得的数据也要计入。若服务器忽略范围请求,返回了整个媒体文件,计入的是实际取得的数据,而非原本只想要的小片段。
转换时可能同时存在网络响应数据、解密结果、媒体引擎内存文件系统,以及最终输出。不同步骤之间还可能发生复制。因此,下载一个接近预算的视频,并不能保证进程内存也接近同一个数值。低内存设备可能在达到下载上限之前就受到压力。
我们采用逐段解密,避免同时发起所有分片的解密任务;但这不等于整个转换已经成为可以无限处理大文件的流式方案。媒体引擎仍需要其输入和输出资源。长期来看,支持更大的内容,需要重新考虑存储、写入和输出方式,不能只把预算数字调大。
对普通使用者,比较实际的建议是关闭不必要的大型页面、保持足够可用内存,先用较短的授权点播样例确认兼容性。设备性能、媒体编码、分片数量和来源网络都会影响体验,没有可靠的统一“每分钟视频需要多少秒”换算。
AES 解密通常不是最主要的额外负担
AES-128 分片使用浏览器原生密码接口处理,解密后仍然执行原有封装流程。它增加的是分片解密和相应数据缓冲,而不是重新编码整段视频。关于密钥、IV 和加密判断的问题,见 能播放却不能下载的排查记录。
我们没有依据单个样例承诺固定的解密速度。网络慢时,下载会主导等待;内存紧张时,数据复制与媒体引擎的工作可能成为压力来源;首次使用时,还要加载引擎。页面应分别提示当前阶段,而不是把所有等待都称为“转码”。
取消操作也要覆盖这些阶段。我们的实现会中止正在进行的资源请求,并终止媒体引擎任务,完成或取消后释放相关对象 URL 和引擎资源。这有助于避免重复操作不断叠加占用,但浏览器何时实际回收内存仍由其运行环境决定。
本地成功后,上线为什么卡在一个文件?
我们使用的 FFmpeg 核心 WASM 文件实际大小为 32,232,419 字节,约 30.7 MiB。而 Cloudflare Pages 当前的文件限制 是单个站点资产最大 25 MiB。这里的限制针对构建后发布的每个资产,不是整个网站总大小。
因此,即使页面、脚本和 CSS 都正常,这个二进制文件仍会阻碍按原方案发布。把文件改名或换一个目录没有作用,它的大小不会因此改变。仅看到本地开发页面能加载,也不足以确认静态部署可行。
这个问题不需要改变视频转换逻辑。我们调整的是媒体引擎的分发方式:构建前准备资源,把原始 WASM 按最多 8 MiB 一份拆成四个文件,并生成记录各部分地址和长度的清单。发布的每个资源都小于平台单文件限制。
拆分发布,浏览器再还原原始引擎
加载过程先获取清单,再逐个取得分片并检查长度,然后用这些字节创建 WASM Blob URL,交给媒体引擎加载。浏览器拿到的仍然是原始二进制序列,拆分没有改变 FFmpeg 的功能,也没有减少总下载量。临时 URL 在对应使用完成后释放。
构建生成的文件名带有内容摘要,降低新旧部署资源混用的机会;清单读取不依赖长时间缓存。我们还把准备步骤接入依赖安装、开发启动和正式构建,避免只在某台开发电脑上手工生成一次,线上构建却缺少文件。
验证不能停留在“清单里有四个地址”。我们将生成文件重新拼接,与依赖包里的原始 WASM 做逐字节比较,确认完全一致;同时检查最终静态输出,确认没有超过平台限制的单个文件。运行时检查每一部分的预期长度,可以发现明显缺失或不完整的响应;长度检查本身并不是完整的内容真实性校验。
这套方案也有代价:请求数量增加,浏览器需要组织这些字节,加载时仍要考虑内存。因此,它解决的是当前托管环境的单文件限制,不应被描述成“压缩了引擎”或“让所有转换都更快”。
单线程、HTTPS 和其他部署条件
本项目选择单线程 FFmpeg WASM,适合当前静态部署和浏览器兼容性取舍。若改成依赖共享内存的多线程方案,部署条件会变化,需要重新评估相关浏览器隔离要求。有关不同核心与部署方式,可参考 ffmpeg.wasm 官方文档。不能把单线程方案中的结论直接套到多线程核心上。
AES 解密还要求浏览器提供 Web Crypto。正式网站使用 HTTPS,本地开发的 localhost 也有相应支持;把页面随意放到不安全的 HTTP 来源上,可能导致密码接口不可用。密钥与分片的跨域权限则是另外一项要求,部署了 HTTPS 不会自动解决来源服务器的 CORS 配置。
上线时还要检查静态路径是否完整。若清单可读取但某个分片返回 404,引擎就无法还原;若请求返回错误页却被当成二进制文件,也会在加载时失败。页面能够打开,并不能证明这个延迟加载的功能已经正常运行。
如何验证真正的上线结果?
我们的上线检查先验证两个工具在七种语言下的入口,再检查清单与实际引擎资源,随后通过浏览器执行受控转换。受控样例包括普通 TS、独立音轨、带初始化段的 fMP4、范围请求回退和 AES-128 加密内容。这样可以把来源网络的不确定性与工具本身的行为分开验证。
输出也要检查:下载文件应能被识别为 MP4,视频元数据和时长合理,且能够定位接近结尾的位置。只看到转换进度达到 100% 或下载按钮出现,还不足以判断媒体是否正确。本文所述测试主要在 Chromium 中完成,不声称覆盖所有手机或浏览器。
最后还要等待实际部署生效。代码推送成功代表提交到了仓库,不代表所有用户已经看到新站点。我们在部署后重新访问正式网址验证页面和资源,并检查站内链接与 sitemap。这样才能区分“构建完成”“发布完成”和“功能在线可用”。
这次经历说明,浏览器视频工具既需要正确理解 HLS,也需要为媒体引擎的体积、设备资源和托管限制做准备。若你面对的是分片地址错误、无声输出或初始化段缺失,可以继续阅读 分片结构与地址处理的开发记录,先确定问题所在的阶段,再决定是否需要调整部署方案。