Mediabunny 深度解剖:纯 TypeScript 音视频处理库如何重新定义浏览器端媒体开发
一、引言:浏览器端音视频处理的困境与破局
长期以来,浏览器端的音视频处理一直是 Web 开发中的"老大难"问题。传统方案要么依赖后端服务器进行转码和处理,导致高延迟和高成本;要么使用移植自桌面端的库(如 ffmpeg.wasm),体积臃肿且性能堪忧。这种两难处境让无数前端开发者在面对"视频压缩""音频提取""格式转换"等需求时,不得不让用户上传文件到服务器,忍受漫长的等待和额外的带宽开销。
Mediabunny 的出现彻底改变了这一局面。这是一个完全使用 TypeScript 从零编写的浏览器端音视频处理库,定位是"为 Web 原生设计,而非移植适配"。它不依赖任何外部库,支持硬件加速的编解码,提供极具 Tree-shakable 特性的 API,让开发者在不引入大量冗余代码的情况下,在浏览器中实现专业级的媒体处理能力。
本文将从架构设计、核心原理、API 使用、生产实践等多个维度,对 Mediabunny 进行深度剖析,带你理解它为何能在性能测试中将 ffmpeg.wasm 甩出几个数量级,以及如何在实际项目中用它构建真正的客户端媒体应用。
二、从 FFmpeg 时代到 WebCodecs 时代:技术背景
2.1 FFmpeg.wasm 的局限
在 Mediabunny 出现之前,ffmpeg.wasm 是浏览器端视频处理的主流方案。它将 FFmpeg 这个有着二十多年历史的 C 语言多媒体框架编译为 WebAssembly,试图让 Web 应用拥有桌面级的媒体处理能力。
但这种"移植"方案天然存在以下问题:
体积问题:ffmpeg.wasm 的核心 WASM 文件加上默认的编解码器支持,轻松超过 20MB。即使用户只需要裁剪一个 10 秒的片段,也必须下载整个库。
性能问题:WebAssembly 的沙箱环境意味着它无法直接调用宿主机的硬件加速能力,所有编解码都在软件层面进行。处理一段 1080p 的视频,CPU 占用率瞬间拉满,用户界面严重卡顿。
内存问题:WASM 内存模型与 JavaScript 不同,视频帧数据需要在两个空间之间频繁拷贝,大文件处理时内存占用失控。
同步阻塞:ffmpeg 的 API 设计是同步阻塞的,强行塞进单线程 JavaScript 环境,要么卡死 UI,要么需要复杂的 Worker 隔离方案。
2.2 WebCodecs API 的崛起
2020 年,W3C 开始推动 WebCodecs API 的标准化,2022 年底主流浏览器陆续支持。这一 API 的核心思想是:将浏览器内置的音视频编解码器以 JavaScript API 的形式暴露出来。
换句话说,你的 Chrome 浏览器在播放 YouTube 视频时使用的 H.264 解码器、AV1 解码器,现在可以通过 JavaScript 直接调用,而且绑定的就是硬件加速通道——GPU 来解码,零拷贝到 Canvas 或 WebGL。
// WebCodecs 解码示例
const decoder = new VideoDecoder({
output: (frame) => {
// frame 是 GPU 解码后的原始帧,可直接送入 Canvas/WebGL
ctx.drawImage(frame, 0, 0);
frame.close(); // 重要:释放 GPU 内存
},
error: (e) => console.error('解码错误:', e)
});
decoder.configure({
codec: 'avc1.42001E', // H.264 Baseline
codedWidth: 1920,
codedHeight: 1080,
});
// 送入编码后的视频包
decoder.decode(chunk);
这意味着,只要开发者愿意,可以在浏览器中构建一套完全自主控制的媒体处理流水线——从文件读取、格式解析、帧解码、画面处理、帧编码、文件封装,全部在客户端完成,且充分利用硬件加速。
WebCodecs 的局限性在于:它只提供原始的编解码能力。你需要自己处理 MP4/MKV/WebM 等容器格式的解析(demuxer),以及封装(muxer)——这正是 FFmpeg 的核心价值,也是最难从头实现的部分。
2.3 Mediabunny 的定位
Mediabunny 的核心价值,就是在 WebCodecs 之上构建完整的媒体处理抽象层:
┌─────────────────────────────────────────────────┐
│ Mediabunny API Layer │
│ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │
│ │ Input │ │ Output │ │ Conversion │ │
│ │ (Demux) │ │ (Mux) │ │ (Transmux/Re) │ │
│ └────┬─────┘ └────┬─────┘ └───────┬────────┘ │
│ │ │ │ │
│ ┌────▼────────────▼───────────────▼────────┐ │
│ │ WebCodecs API Layer │ │
│ │ VideoDecoder │ VideoEncoder │ Audio APIs │ │
│ └───────────────┴───────────────┴────────────┘ │
│ │ │ │ │
│ ┌────▼────────────▼───────────────▼────────┐ │
│ │ Browser HW Acceleration │ │
│ │ GPU Decoding │ Hardware Encode │ ... │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
它用纯 TypeScript 实现了 MP4/MOV/WebM/MKV/HLS/WAVE/MP3/Ogg/ADTS/FLAC/MPEG-TS 等格式的解析和封装逻辑,同时将编解码操作委托给 WebCodecs,由浏览器负责硬件加速。
三、核心架构:Input/Output 双轨制与流水线设计
3.1 Input:精准读取,只取所需
Mediabunny 的 Input 类是整个读取体系的核心。它的设计哲学是"按需加载,精准读取"——绝不加载整个文件,绝不解码不需要的帧。
3.1.1 多源抽象
Input 支持多种数据源,通过 Source 类抽象:
import { Input, UrlSource, BlobSource, BufferTarget, ALL_FORMATS } from 'mediabunny';
// 从 URL 读取
const input1 = new Input({
source: new UrlSource('https://example.com/video.mp4'),
formats: ALL_FORMATS,
});
// 从本地文件读取
const input2 = new Input({
source: new BlobSource(fileInputElement.files[0]),
formats: ALL_FORMATS,
});
// 从 ArrayBuffer 读取(用于更底层场景)
const input3 = new Input({
source: new ArrayBufferSource(arrayBuffer),
formats: ALL_FORMATS,
});
3.1.2 元数据读取
不需要解码整个文件,只需要获取时长、分辨率、编码格式等信息:
const input = new Input({
source: new UrlSource('./bigbuckbunny.mp4'),
formats: ALL_FORMATS,
});
// 计算总时长(底层优化:只读取 moov atom)
const duration = await input.computeDuration();
// 获取主视频轨道
const videoTrack = await input.getPrimaryVideoTrack();
const displayWidth = await videoTrack.getDisplayWidth();
const displayHeight = await videoTrack.getDisplayHeight();
const rotation = await videoTrack.getRotation(); // 0/90/180/270
// 获取主音频轨道
const audioTrack = await input.getPrimaryAudioTrack();
const sampleRate = await audioTrack.getSampleRate();
const numberOfChannels = await audioTrack.getNumberOfChannels(); // 1=单声道, 2=立体声
// 获取字幕轨道
const subtitleTracks = await input.getSubtitleTracks();
这些操作的本质,是只解析 MP4/MKV 容器中的 moov/mvhd/track 等元数据 atom,不触碰实际的视频帧数据。一个 1GB 的视频,获取元数据可能只需要几十毫秒。
3.1.3 帧级读取
需要获取原始视频帧时,通过 VideoSampleSink 逐帧或随机访问:
const input = new Input({
source: new UrlSource('./video.mp4'),
formats: ALL_FORMATS,
});
const videoTrack = await input.getPrimaryVideoTrack();
// 方式一:随机访问——获取指定时间的单帧
const sink = new VideoSampleSink(videoTrack);
const frame = await sink.getSample(3.5); // 获取第 3.5 秒的帧
// frame 是 VideoFrame 对象,可直接 draw 到 Canvas
ctx.drawImage(frame, 0, 0);
frame.close(); // 必须手动关闭,释放 GPU 内存
// 方式二:顺序遍历——遍历视频的所有帧
for await (const frame of sink.samples()) {
// 逐帧处理
processFrame(frame);
frame.close();
}
// 方式三:指定范围遍历——只处理 5~10 秒的帧
const rangeSink = new VideoSampleSink(videoTrack, {
startTime: 5,
endTime: 10,
});
这种设计的关键优势是:内存高效。VideoFrame 由浏览器底层管理,使用 GPU 共享内存,只有调用 close() 后才释放。相比将整个视频 decode 到 ArrayBuffer 再处理,内存占用降低数十倍。
3.1.4 音频数据读取
const input = new Input({
source: new BlobSource(audioFile),
formats: ALL_FORMATS,
});
const audioTrack = await input.getPrimaryAudioTrack();
// 获取指定时间的音频数据(返回 AudioData)
const audioSink = new AudioSampleSink(audioTrack);
const audioData = await audioSink.getSample(2.0); // 第 2 秒的音频帧
// 获取音频的原始 PCM 数据
const pcmData = audioData.allocation; // Float32Array 或 Int16Array
3.2 Output:程序化生成,可控封装
与 Input 对应,Output 类负责程序化创建媒体文件:
import { Output, Mp4OutputFormat, WebMOutputFormat, BufferTarget, StreamTarget, WAVEOutputFormat } from 'mediabunny';
// 方式一:生成到内存(适合小文件或 Blob 下载)
const output1 = new Output({
format: new Mp4OutputFormat(),
target: new BufferTarget(),
});
// 方式二:流式写入(适合大文件,避免内存溢出)
const output2 = new Output({
format: new WebMOutputFormat(),
target: new StreamTarget(writableStream), // 可接 FileSystemWritableFileStream 实现浏览器本地保存
});
// 添加视频轨道(来自 Canvas 实时绘制)
const videoSource = new CanvasSource(canvas, {
codec: 'av1', // 使用 AV1 编码(压缩率最高)
bitrate: 5_000_000, // 5Mbps
});
output.addVideoTrack(videoSource);
// 添加音频轨道(来自 AudioBuffer 混音)
const audioSource = new AudioBufferSource({
codec: 'opus', // Opus 编码,兼容性最好
bitrate: 128_000, // 128kbps
});
output.addAudioTrack(audioSource);
// 开始录制
await output.start();
// ... 在这里向轨道写入数据 ...
// 结束录制并生成文件
await output.finalize();
// 获取最终文件
const { buffer } = output.target; // ArrayBuffer
这种设计的灵活性在于:轨道的数据源完全由开发者控制。可以是 Canvas 实时绘制(实现屏幕录制)、麦克风采集、AudioBuffer 混音,甚至是从另一个 Input 管道过来的解码帧。
3.3 流水线架构:读写解耦与流水线并行
Mediabunny 内部实现了高效的流水线架构,将读取、解码、处理、编码、写入五个阶段解耦:
// 流水线读取示例:从一个视频读取,处理后写入新文件
const input = new Input({
source: new BlobSource(sourceFile),
formats: ALL_FORMATS,
});
const videoTrack = await input.getPrimaryVideoTrack();
const audioTrack = await input.getPrimaryAudioTrack();
const output = new Output({
format: new WebMOutputFormat(),
target: new BufferTarget(),
});
// 从 Input 解码帧
const videoSink = new VideoSampleSink(videoTrack);
const audioSink = new AudioSampleSink(audioTrack);
// 向 Output 编码帧(可并行进行)
const videoEncoder = new VideoEncoder({
output: (chunk) => output.addVideoChunk(chunk),
error: (e) => console.error(e),
});
videoEncoder.configure({
codec: 'vp09.00.10.08',
width: 1920,
height: 1080,
bitrate: 8_000_000,
});
// 异步并行读写
(async () => {
for await (const frame of videoSink.samples()) {
videoEncoder.encode(frame);
frame.close();
}
await videoEncoder.flush();
await output.finalize();
})();
四、性能奥秘:为何 Mediabunny 能甩开对手几个量级
4.1 基准测试数据解读
官方提供的基准测试(测试环境:Ryzen 7600X, RTX 4070, NVMe SSD)结果令人震撼:
元数据提取速度(ops/s,越高越好):
| 库 | 速度 |
|---|---|
| Mediabunny | 862 |
| @remotion/media-parser | 233 |
| web-demuxer | 80.6 |
| mp4box.js | 43.5 |
| ffmpeg.wasm | 1.83 |
Mediabunny 比第二名快 3.7 倍,比 ffmpeg.wasm 快 471 倍。这个差距的核心原因在于:Mediabunny 的元数据提取完全不需要 WASM 虚拟机,直接使用 JavaScript 解析二进制 Buffer,而 ffmpeg.wasm 需要启动 WASM 运行时。
视频包遍历速度(packets/s):
| 库 | 速度 |
|---|---|
| Mediabunny | 10,800 |
| web-demuxer | 2,390 |
| @remotion/media-parser | 842 |
格式转换+缩放速度(frames/s,处理 BigBuckBunny 1080p → 320x180 WebM):
| 库 | 速度 |
|---|---|
| Mediabunny | 804 |
| @remotion/webcodecs | 324 |
| ffmpeg.wasm | 12.0 |
Mediabunny 达到了 ffmpeg.wasm 的 67 倍。
4.2 性能优化的五个关键
4.2.1 WebCodecs 硬件加速
Mediabunny 的视频编解码全部走 WebCodecs API,绑定了浏览器内置的硬件编解码器。以 Chrome 为例:
- H.264/AVC:使用 FFmpeg 的 libavcodec,但实际调用的是
VideoDecode accelerator(Intel QuickSync/NVIDIA NVENC/Apple VideoToolbox) - VP8/VP9:使用 libvpx,通过 GPU 加速
- AV1:使用 libdav1d(CPU 为主,但通过 NEON/SSE 优化)
- Opus/AAC:硬件 DSP
相比之下,ffmpeg.wasm 的编解码完全在 WASM 虚拟机内执行,绕过了所有硬件加速通道。
4.2.2 Tree-shakable 极小 Bundle
Mediabunny 的 API 设计遵循"只打包你用到的"原则:
| 功能 | Bundle 大小(min+gzip) |
|---|---|
| 仅读取 .wav 文件 | 5.10 kB |
| 仅写入 .wav 文件 | 5.79 kB |
| 仅写入 .webm 文件 | 11.4 kB |
| 仅读取 .mp4 文件 | 16.0 kB |
| 读取所有格式 | 30.0 kB |
| 全部功能 | 69.6 kB |
| mp4box.js | 37.3 kB |
| ffmpeg.wasm(最精简) | ~20MB |
作为对比,ffmpeg.wasm 的最小体积约 20MB,Mediabunny 即使使用全部功能也只有 69.6 kB,是前者的 1/290。这种体积极大降低了首屏加载成本。
4.2.3 零依赖架构
Mediabunny 不依赖任何外部 npm 包,所有功能从零实现:
- MP4/MOV 解析:手写 Box/Atom 解析器,支持 ftyp/moov/mdat/moof/udta 等所有主要 Box 类型
- WebM/MKV 解析:基于 EBML 规范实现,支持 Opus/Vorbis/VP8/VP9
- HLS 解析:纯 JS 实现 m3u8 解析和 TS 流拼接
- 编解码桥接:VideoDecoder/VideoEncoder 封装为统一的帧接口
零依赖意味着没有间接调用开销,也没有依赖版本冲突风险,更容易通过 Tree-shaking 消除死代码。
4.2.4 流水线与流式处理
Mediabunny 的 Output 支持渐进式写入——不必等到所有数据都准备好才开始封装文件。视频帧可以一边编码一边写入目标容器,音频帧同理。这种流式架构使得:
- 内存占用与文件总时长无关,只与缓冲的帧数量有关
- 支持实时录制场景(如屏幕录制、直播推流)
- 大文件处理不会触发浏览器的内存警告
4.2.5 微秒级时间精度
Mediabunny 在容器封装层面实现了微秒级时间控制:
const output = new Output({
format: new Mp4OutputFormat(),
target: new BufferTarget(),
});
// 设置精确的起始时间戳
output.start({ startTimestamp: 1000 }); // 从 1ms 开始
// 每一帧都可以指定精确的 PTS(Presentation Time Stamp)
const frame = new VideoFrame(canvas, {
timestamp: 1_500_000, // 微秒
});
这种精度对于字幕同步、多轨道音视频对齐等场景至关重要。
五、核心 API 实战:从入门到进阶
5.1 基础篇:视频元数据提取与缩略图生成
最常见的场景:上传视频后,在客户端生成预览缩略图,不需要后端参与。
import { Input, UrlSource, VideoSampleSink, ALL_FORMATS } from 'mediabunny';
async function generateThumbnails(file: File, count: number = 9): Promise<string[]> {
const input = new Input({
source: new BlobSource(file),
formats: ALL_FORMATS,
});
// 获取基本信息
const duration = await input.computeDuration();
const videoTrack = await input.getPrimaryVideoTrack();
const displayWidth = await videoTrack.getDisplayWidth();
const displayHeight = await videoTrack.getDisplayHeight();
// 创建 Canvas 用于绘制缩略图
const thumbWidth = 160;
const thumbHeight = Math.round(thumbWidth * displayHeight / displayWidth);
const canvas = new OffscreenCanvas(thumbWidth, thumbHeight);
const ctx = canvas.getContext('2d')!;
const sink = new VideoSampleSink(videoTrack);
const thumbnails: string[] = [];
// 将视频均分为 count 段,在每段中点取一帧
for (let i = 0; i < count; i++) {
const timestamp = (duration / count) * (i + 0.5);
const frame = await sink.getSample(timestamp);
ctx.clearRect(0, 0, thumbWidth, thumbHeight);
ctx.drawImage(frame, 0, 0, thumbWidth, thumbHeight);
frame.close();
const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.8 });
thumbnails.push(URL.createObjectURL(blob));
}
return thumbnails;
}
// 使用
const input = document.getElementById('video-input') as HTMLInputElement;
input.addEventListener('change', async () => {
const file = input.files![0];
const thumbs = await generateThumbnails(file);
thumbs.forEach((url, i) => {
const img = document.createElement('img');
img.src = url;
document.body.appendChild(img);
});
});
5.2 进阶篇:视频转码与格式转换
将用户上传的 MOV 文件转换为 WebM,同时降低分辨率以适应网络传输:
import { Input, Output, Conversion, BlobSource, UrlSource, WebMOutputFormat, BufferTarget, ALL_FORMATS } from 'mediabunny';
async function transcodeToWebM(file: File, targetWidth: number = 1280): Promise<Blob> {
const input = new Input({
source: new BlobSource(file),
formats: ALL_FORMATS,
});
const output = new Output({
format: new WebMOutputFormat(),
target: new BufferTarget(),
});
// 初始化转换流水线
const conversion = await Conversion.init({
input,
output,
video: {
resize: {
width: targetWidth, // 自动保持宽高比
},
// 可选:调整码率
bitrate: 2_000_000, // 2Mbps
},
audio: {
// 音频重采样到 48kHz 立体声
sampleRate: 48000,
numberOfChannels: 2,
bitrate: 128_000,
},
});
await conversion.execute();
await output.finalize();
const { buffer } = output.target;
return new Blob([buffer], { type: 'video/webm' });
}
// 使用:上传后客户端转码,再上传到服务器
const input = document.getElementById('video-input') as HTMLInputElement;
input.addEventListener('change', async () => {
const file = input.files![0];
const progress = document.getElementById('progress')!;
const transcoded = await transcodeToWebM(file);
// 将转码后的 Blob 上传到服务器
const formData = new FormData();
formData.append('video', transcoded, 'video.webm');
await fetch('/api/upload', { method: 'POST', body: formData });
});
5.3 高级篇:视频水印与画面处理
使用 Canvas 在解码帧上叠加水印,再重新编码:
import { Input, Output, Conversion, BlobSource, VideoSampleSink, BufferTarget, Mp4OutputFormat, ALL_FORMATS } from 'mediabunny';
async function addWatermark(
sourceFile: File,
watermarkImage: HTMLImageElement,
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' = 'bottom-right'
): Promise<Blob> {
const input = new Input({
source: new BlobSource(sourceFile),
formats: ALL_FORMATS,
});
const output = new Output({
format: new Mp4OutputFormat(),
target: new BufferTarget(),
});
// 计算水印尺寸(宽度为视频宽度的 15%)
const videoTrack = await input.getPrimaryVideoTrack();
const displayWidth = await videoTrack.getDisplayWidth();
const displayHeight = await videoTrack.getDisplayHeight();
const wmWidth = Math.round(displayWidth * 0.15);
const wmHeight = Math.round(wmWidth * watermarkImage.naturalHeight / watermarkImage.naturalWidth);
// 水印位置
const padding = Math.round(displayWidth * 0.03);
const positions = {
'top-left': { x: padding, y: padding },
'top-right': { x: displayWidth - wmWidth - padding, y: padding },
'bottom-left': { x: padding, y: displayHeight - wmHeight - padding },
'bottom-right': { x: displayWidth - wmWidth - padding, y: displayHeight - wmHeight - padding },
};
const pos = positions[position];
// 创建离屏 Canvas 用于帧处理
const offscreen = new OffscreenCanvas(displayWidth, displayHeight);
const ctx = offscreen.getContext('2d')!;
const conversion = await Conversion.init({
input,
output,
video: {
// process 回调:对每一帧进行自定义处理
process: (sample) => {
// 绘制原始帧到离屏 Canvas
ctx.clearRect(0, 0, displayWidth, displayHeight);
sample.draw(ctx, 0, 0); // sample 支持直接 draw 到 Canvas
// 绘制水印
ctx.drawImage(watermarkImage, pos.x, pos.y, wmWidth, wmHeight);
// 返回处理后的 Canvas 作为新帧
return ctx.canvas;
},
},
});
await conversion.execute();
await output.finalize();
const { buffer } = output.target;
return new Blob([buffer], { type: 'video/mp4' });
}
5.4 高手篇:实时屏幕录制与 MP4 生成
import { Output, Mp4OutputFormat, StreamTarget, CanvasSource, AudioBufferSource } from 'mediabunny';
class ScreenRecorder {
private output: Output;
private canvasSource: CanvasSource;
private audioSource: AudioBufferSource | null = null;
private audioContext: AudioContext;
private mediaStream: MediaStream;
private chunks: Uint8Array[] = [];
constructor(width: number, height: number, fps: number = 30) {
this.audioContext = new AudioContext({ sampleRate: 48000 });
// 初始化 MP4 输出(流式写入 FileSystemWritableFileStream)
const stream = new MediaStream();
this.output = new Output({
format: new Mp4OutputFormat(),
target: new StreamTarget(stream), // 自定义流目标
});
// 视频轨道:捕获 Canvas 内容
this.canvasSource = new CanvasSource(this.captureCanvas.bind(this), {
codec: 'av1',
bitrate: 4_000_000,
framerate: fps,
});
this.output.addVideoTrack(this.canvasSource);
// 音频轨道:捕获系统音频
this.mediaStream = null as any; // 稍后初始化
}
async start() {
// 请求屏幕录制权限
this.mediaStream = await navigator.mediaDevices.getDisplayMedia({
video: { displaySurface: 'monitor' },
audio: true, // 捕获系统音频
});
if (this.mediaStream.getAudioTracks().length > 0) {
const source = this.audioContext.createMediaStreamSource(this.mediaStream);
const dest = this.audioContext.createMediaStreamDestination();
source.connect(dest);
this.audioSource = new AudioBufferSource({
codec: 'opus',
bitrate: 128_000,
});
this.output.addAudioTrack(this.audioSource);
}
await this.output.start();
}
private captureCanvas(): ImageBitmap | OffscreenCanvas {
// 周期性返回当前 Canvas 内容
// 在实际应用中,这里会捕获屏幕的 ImageBitmap
return this.cachedFrame;
}
async stop(): Promise<Blob> {
await this.output.finalize();
const { buffer } = this.output.target;
return new Blob([buffer], { type: 'video/mp4' });
}
}
// 使用
const recorder = new ScreenRecorder(1920, 1080, 30);
await recorder.start();
// ... 用户在操作 ...
const blob = await recorder.stop();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'recording.mp4';
a.click();
六、生产部署:场景选择与避坑指南
6.1 适合的场景
Mediabunny 非常适合以下场景:
- 客户端视频处理工具:截图软件、屏幕录制器、视频压缩器,完全不需要后端
- AI 视频生成工具的前处理:用 Canvas 生成视频帧序列,送入 AI 模型处理后再封装
- 媒体 CMS 的预览生成:上传视频后,浏览器端生成多种分辨率的缩略图
- 隐私敏感的视频处理:不希望视频内容上传到服务器(如医疗、法律行业的文档视频)
- Web 游戏中的过场动画生成:程序化生成 MP4/WebM 动画
6.2 不适合的场景
- 需要支持旧版浏览器:WebCodecs API 在 Safari 15.4+、Chrome 94+、Edge 94+ 才完整支持,iOS Safari 支持但存在已知 bug
- 需要编码不支持的格式:Mediabunny 目前不支持 H.265/HEVC(因为 WebCodecs 在 Safari 上的 HEVC 支持受限)
- 超长视频处理:虽然支持流式处理,但超过 2 小时的视频仍然可能遇到浏览器的内存管理问题
6.3 WebCodecs 兼容性检测
import { isSupported } from 'mediabunny';
async function checkSupport() {
const checks = {
mp4_h264: await VideoDecoder.isConfigSupported({
codec: 'avc1.42001E',
codedWidth: 1920,
codedHeight: 1080,
}),
webm_vp9: await VideoDecoder.isConfigSupported({
codec: 'vp09.00.10.08',
codedWidth: 1920,
codedHeight: 1080,
}),
webm_av1: await VideoDecoder.isConfigSupported({
codec: 'av01.0.04M.08',
codedWidth: 1920,
codedHeight: 1080,
}),
};
console.log('支持情况:', checks);
// { mp4_h264: { supported: true }, webm_vp9: { supported: true }, webm_av1: { supported: false } }
}
6.4 大文件处理的最佳实践
// 对于大文件(>500MB),推荐使用 FileSystemWritableFileStream 流式保存
async function saveLargeVideo(input: Input, filename: string) {
// 请求一个可写的文件句柄(需要用户授权)
const handle = await window.showSaveFilePicker({
suggestedName: filename,
types: [{
description: 'MP4 Video',
accept: { 'video/mp4': ['.mp4'] },
}],
});
const writable = await handle.createWritable();
const stream = new WritableStream({
write(chunk) {
return writable.write(chunk);
},
close() {
return writable.close();
},
});
const output = new Output({
format: new Mp4OutputFormat(),
target: new StreamTarget(stream),
});
// 转换并流式写入磁盘(内存占用恒定)
const conversion = await Conversion.init({ input, output });
await conversion.execute();
await output.finalize();
}
七、竞品对比与选型建议
| 特性 | Mediabunny | ffmpeg.wasm | @remotion/webcodecs | mp4box.js |
|---|---|---|---|---|
| 核心语言 | TypeScript | C→WASM | TypeScript | TypeScript |
| 体积(完整) | 69.6 kB | ~20MB | 108 kB | 37.3 kB |
| 硬件加速 | ✅ WebCodecs | ❌ 纯软件 | ✅ WebCodecs | ❌ 纯软件 |
| 格式写入 | ✅ 完整 | ✅ 完整 | ✅ 仅 WebM | ❌ 仅读取 |
| 依赖 | 0 | 无(WASM内置) | 多个 | 0 |
| 许可证 | MPL-2.0 | LGPL/GPL | MIT | BSD-3 |
| 学习曲线 | 低 | 高 | 中 | 中 |
选型建议:
- 需要写入视频文件且在意体积:选 Mediabunny
- 需要不常见格式(如 H.265、专业视频格式):选 ffmpeg.wasm
- 使用 Remotion 视频生成生态:选 @remotion/webcodecs
- 只需要读取 MP4 元数据:选 mp4box.js(体积更小)
八、总结与展望
Mediabunny 用一种"为 Web 原生"的思路,解决了浏览器端音视频处理的根本问题:不是把桌面端工具移植过来,而是充分利用 Web 平台的底层能力(WebCodecs、Canvas、OffscreenCanvas、FileSystem API),用 TypeScript 构建一套轻量、灵活、高效的媒体处理框架。
它的成功告诉我们:有时候,最好的方案不是最强大的方案,而是最适配平台的那一个。ffmpeg 在桌面端是无敌的存在,但在浏览器里,Mediabunny 的架构显然更聪明——它把编解码交给浏览器,把容器封装用 JS 重写,把硬件加速通道打通,把不需要的功能 tree-shake 掉,最终呈现给开发者的是一个既功能完整又体积娇小的库。
2026 年,随着 WebCodecs API 持续成熟、更多编解码器获得硬件支持,以及 WASI Component Model 允许 WASM 模块直接调用浏览器能力,浏览器端音视频处理的边界还将继续扩展。Mediabunny 的架构为此后的演进预留了充足的扩展空间——它的 Source/Track/Sink 抽象体系可以轻松接纳新的媒体格式和新的处理节点。
对于前端开发者而言,Mediabunny 意味着一个全新的可能性:所有曾经必须交给后端处理的媒体任务,现在都可以在用户的设备上完成。这不仅是技术上的进步,更是一种范式转变——客户端不再只是渲染层,而是一个具备完整媒体处理能力的计算平台。