
本文介绍如何纯前端将 html canvas 渲染的物理动画(如运动方块)录制为视频文件,无需后端服务器:先用 mediarecorder 实时捕获 canvas 流生成 webm,再通过 ffmpeg.wasm 转码为 mp4。全程运行于浏览器,支持离线使用。
想把浏览器里那个酷炫的 Canvas 物理动画,比如你刚实现的小球碰撞或者方块堆叠,直接保存成一个能在任何地方播放的 MP4 视频吗?而且,整个过程完全在浏览器里完成,不需要任何服务器参与。
听起来有点不可思议?其实,实现这个目标有个非常成熟的“两步走”策略:先录制,再转码。简单来说,就是利用浏览器原生的 MediaRecorder 高效捕捉 Canvas 画面流,生成 WebM 文件;然后,借助强大的 ffmpeg.wasm 在本地完成格式转换,最终输出兼容性极佳的 MP4 视频。这套组合拳,既保证了录制的实时性和低延迟,又解决了最终格式的通用性问题。
第一步:用 MediaRecorder 录制 Canvas 为 WebM
这是整个流程的基石。MediaRecorder API 可以直接接收 Canvas 通过 captureStream() 方法产生的媒体流,实现帧级别的同步录制。不过,有几个关键细节决定了录制的成败:
- Canvas 尺寸必须明确:务必通过 HTML 属性或 Ja vaScript 设置 Canvas 的
width和height,而不仅仅是 CSS 样式。否则,生成的流可能是空的。 - 锁定帧率:在调用
captureStream()时指定帧率(例如 30 FPS),并在MediaRecorder的配置中明确视频的frameRate。这能避免录制器自适应帧率导致的卡顿或跳帧。 - 数据收集与下载:录制过程中,数据会以“块”的形式通过事件回调出来。我们需要将它们收集起来,在录制结束时合并成一个 Blob 对象,并生成下载链接。
const canvas = document.getElementById('myCanvas');
const stream = canvas.captureStream(30); // 明确指定 30 FPS
const mediaRecorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp9'
});
const chunks = [];
mediaRecorder.ondataa vailable = e => chunks.push(e.data);
mediaRecorder.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'animation.webm';
a.click();
};
// 开始录制(例如点击按钮触发)
function startRecording() {
chunks.length = 0;
mediaRecorder.start();
}
// 停止录制(例如动画结束时调用)
function stopRecording() {
mediaRecorder.stop();
}
⚠️ 注意:
captureStream()在 Safari 浏览器中的支持情况需要留意。虽然 Safari 16.4+ 版本已提供原生支持,但在更早的版本或某些特定环境下可能需要开启实验性功能。开发时务必在目标浏览器中进行充分测试。
第二步:用 ffmpeg.wasm 将 WebM 转码为 MP4
虽然 WebM 格式在浏览器内处理效率很高,但若想视频能在微信、iOS 系统相册等更广泛的环境下无缝播放,MP4(H.264 视频编码 + AAC 音频编码)无疑是更稳妥的选择。这时,ffmpeg.wasm 就派上用场了——它把著名的 FFmpeg 工具搬到了浏览器里。
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
const ffmpeg = createFFmpeg({
corePath: 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/ffmpeg-core.js',
log: true,
progress: ({ ratio }) => console.log(`转码进度: ${(ratio * 100).toFixed(0)}%`)
});
async function transcodeToMP4(webmBlob) {
// 首次使用需要加载 WASM 核心模块(体积约20MB,建议预加载)
await ffmpeg.load();
// 将 WebM 文件写入 ffmpeg.wasm 的虚拟文件系统
const arrayBuffer = await webmBlob.arrayBuffer();
ffmpeg.FS('writeFile', 'input.webm', new Uint8Array(arrayBuffer));
// 执行转码命令
await ffmpeg.run(
'-i', 'input.webm', // 输入文件
'-c:v', 'libx264', // 视频编码器使用 H.264
'-crf', '23', // 画质控制,23 是常用平衡值(值越小画质越好)
'-preset', 'fast', // 编码预设,平衡速度与压缩率
'-c:a', 'aac', // 音频编码器使用 AAC(即使无声轨也建议保留,确保容器规范)
'-y', 'output.mp4' // 输出文件名,-y 表示覆盖已存在文件
);
// 从虚拟文件系统读取生成的 MP4 文件并提供下载
const data = ffmpeg.FS('readFile', 'output.mp4');
const blob = new Blob([data.buffer], { type: 'video/mp4' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'animation.mp4';
a.click();
}
