一、为什么要在浏览器里做转码?

说实话,我第一次听到「客户端转码」这个概念时,第一反应是——这玩意儿靠谱吗?

以前我们做视频处理,谁不是把文件扔给后端服务器?FFmpeg 装好,命令行一敲,完事。但 WebRTC 录制出来的视频,往往是原始格式,体积大得吓人。我有个项目,录了 10 分钟会议,原始 WebM 文件居然有 2 个 G。用户要下载?别想了。

后来我琢磨,能不能在浏览器里直接搞定转码?用户录完,浏览器自己压缩、转格式,再上传。这样服务器压力小,用户体验也顺滑。嗯,FFmpeg.wasm 就是干这个的。

二、FFmpeg.wasm 是什么?

FFmpeg.wasm 是 FFmpeg 的 WebAssembly 版本。说白了,就是把那个强大的命令行工具,搬到了浏览器里运行。

你想想看,以前要装 FFmpeg 到服务器,现在用户打开网页就能用。而且数据不用出浏览器,隐私安全也有保障。

核心能力:

  • 视频格式转换(WebM → MP4,等等)
  • 音频提取(从视频里抽出一条音轨)
  • 视频压缩(调整码率、分辨率)
  • 剪辑、拼接、加滤镜

我个人习惯把 FFmpeg.wasm 看作「浏览器里的瑞士军刀」。你不需要懂底层编解码,只需要知道怎么调用它的 API。

三、环境搭建与基础用法

3.1 安装

安装很简单,npm 一把梭:

npm install @ffmpeg/ffmpeg @ffmpeg/core

如果你用 CDN,也可以直接引入:

<script src="https://cdn.jsdelivr.net/npm/@ffmpeg/ffmpeg/dist/ffmpeg.min.js"></script>

注意: @ffmpeg/core 是核心的 wasm 文件,大概 30MB 左右。第一次加载会比较慢,建议做预加载或者加个 loading 提示。我曾经有个项目没做预加载,用户打开页面白屏了 10 秒,差点被投诉。

3.2 初始化 FFmpeg 实例

import { FFmpeg } from '@ffmpeg/ffmpeg';

const ffmpeg = new FFmpeg();

// 加载核心
await ffmpeg.load({
  coreURL: '/node_modules/@ffmpeg/core/dist/ffmpeg-core.js',
});

console.log('FFmpeg.wasm 已就绪');

这里要注意,load() 是异步的。我建议在页面初始化时就调用,别等到用户点击「转码」才开始加载。

3.3 写入文件与执行命令

FFmpeg.wasm 的工作方式,是在虚拟文件系统里操作文件。你需要先把录制好的视频「写」进去,然后执行命令,最后把结果「读」出来。

// 假设录制的视频数据是 Uint8Array
const videoData = await fetch('recording.webm').then(r => r.arrayBuffer());

// 写入虚拟文件系统
await ffmpeg.writeFile('input.webm', new Uint8Array(videoData));

// 执行转码命令:WebM → MP4
await ffmpeg.exec(['-i', 'input.webm', '-c:v', 'libx264', 'output.mp4']);

// 读取结果
const data = await ffmpeg.readFile('output.mp4');

嗯,这里有个坑。FFmpeg 的命令参数和命令行版几乎一样,但有些参数在 wasm 环境下不支持。比如硬件加速相关的 -hwaccel,就别想了。

四、实战:三种常见转码场景

4.1 视频格式转换:WebM → MP4

WebRTC 录制默认输出 WebM 格式,但很多播放器、社交平台对 MP4 支持更好。所以转码是第一需求。

async function convertToMp4(inputBlob) {
  const arrayBuf = await inputBlob.arrayBuffer();
  await ffmpeg.writeFile('input.webm', new Uint8Array(arrayBuf));

  // 关键参数说明:
  // -c:v libx264  使用 H.264 编码
  // -preset fast  编码速度优先,文件稍大
  // -crf 23       质量参数,18-28,越小质量越高
  await ffmpeg.exec([
    '-i', 'input.webm',
    '-c:v', 'libx264',
    '-preset', 'fast',
    '-crf', '23',
    'output.mp4'
  ]);

  const outputData = await ffmpeg.readFile('output.mp4');
  return new Blob([outputData.buffer], { type: 'video/mp4' });
}

我的经验: CRF 值设 23 是个不错的平衡点。如果视频要上传到平台,建议设 28,文件小很多,肉眼几乎看不出区别。如果是存档用,设 18 更保险。

4.2 提取音频:从视频中抽离音轨

有时候用户只需要音频,比如会议录音、播客素材。这时候提取音频就很有用。

async function extractAudio(videoBlob) {
  const arrayBuf = await videoBlob.arrayBuffer();
  await ffmpeg.writeFile('input.webm', new Uint8Array(arrayBuf));

  // 提取音频,输出为 MP3
  await ffmpeg.exec([
    '-i', 'input.webm',
    '-vn',                // 不要视频流
    '-acodec', 'libmp3lame',
    '-ab', '128k',        // 比特率 128kbps
    'audio.mp3'
  ]);

  const audioData = await ffmpeg.readFile('audio.mp3');
  return new Blob([audioData.buffer], { type: 'audio/mp3' });
}

我曾经帮一个播客团队做过工具,他们录制的视频访谈,需要自动提取音频做后期剪辑。这个功能帮他们省了大量手动操作的时间。

4.3 压缩视频:减小文件体积

压缩是刚需。尤其是 WebRTC 录制的原始视频,码率往往偏高。

async function compressVideo(videoBlob, targetBitrate = '500k') {
  const arrayBuf = await videoBlob.arrayBuffer();
  await ffmpeg.writeFile('input.webm', new Uint8Array(arrayBuf));

  // 压缩:降低码率 + 缩放分辨率
  await ffmpeg.exec([
    '-i', 'input.webm',
    '-c:v', 'libx264',
    '-b:v', targetBitrate,   // 目标视频码率
    '-vf', 'scale=640:-2',   // 宽度缩放到 640,高度自动保持比例
    '-preset', 'fast',
    '-crf', '28',
    'compressed.mp4'
  ]);

  const compressedData = await ffmpeg.readFile('compressed.mp4');
  return new Blob([compressedData.buffer], { type: 'video/mp4' });
}

注意: 压缩不是无损的。码率设得太低,画面会糊。我建议给用户提供「高质量」「标准」「压缩」三个选项,让用户自己权衡。

五、核心流程与知识体系

下面这张图,是我梳理的 FFmpeg.wasm 转码核心流程。你看一眼,基本就明白整个链路了。

FFmpeg.wasm 转码核心流程 ① 获取录制数据 Blob / Uint8Array ② 写入虚拟文件 writeFile('input.webm') ③ 执行转码 ffmpeg.exec() ④ 读取结果 readFile('output.mp4') ⑤ 生成 Blob new Blob([data]) ⑥ 下载 / 上传 用户拿到转码后文件 常见转码场景 格式转换 (WebM→MP4) | 提取音频 | 压缩视频 (降低码率/分辨率)

六、性能与注意事项

场景 耗时(10秒视频) 内存占用 建议
格式转换(WebM→MP4) 约 3-5 秒 约 200MB 使用 -preset fast 加速
提取音频 约 1-2 秒 约 100MB 很快,几乎无感
压缩视频(降低码率) 约 5-10 秒 约 300MB 长视频建议分片处理

性能警告: 浏览器环境的内存是有限的。我测试过,一个 5 分钟的视频,转码过程中内存峰值能到 1GB。如果用户设备比较老,可能会卡死。建议加一个「内存不足」的兜底提示。

七、避坑指南

做 FFmpeg.wasm 开发,有几个坑我踩过,分享给你:

  • 文件路径别搞错: 虚拟文件系统的路径是大小写敏感的。我一开始写 input.WebM,结果死活读不到文件。
  • 输出文件要唯一: 如果多次转码,记得每次用不同的输出文件名,或者先删除旧文件。否则会读到缓存。
  • 大文件分片处理: 超过 100MB 的视频,建议切成小段分别转码,再合并。不然浏览器会崩。
  • Worker 模式: FFmpeg.wasm 支持在 Web Worker 里运行,避免阻塞主线程。我强烈建议开启。

我的小技巧: 转码前先调用 ffmpeg.listDir('/') 看看虚拟文件系统里有什么文件,能帮你快速定位问题。

八、总结

FFmpeg.wasm 让浏览器具备了强大的视频处理能力。格式转换、音频提取、视频压缩,这些以前必须后端做的事,现在前端也能搞定。

我个人觉得,这个技术最大的价值不是「炫技」,而是实实在在提升了用户体验。用户录完视频,立刻就能拿到处理好的文件,不用等服务器响应,也不用担心隐私泄露。

当然,性能瓶颈和内存限制是绕不开的坎。但只要你做好预加载、分片处理、错误提示,大部分场景都能流畅运行。

嗯,这一章的内容就到这里。代码你都拿到了,去试试吧。


公众号:蓝海资料掘金营,微信deep3321