
本文详解为何 captureStream() 录制 Vanta.js 动画时返回空 Blob,并提供完整可运行的修复方案:移除冗余 canvas、精准选取 Vanta 自动生成的 canvas、合理设置 MediaRecorder 参数及 DOM 结构。
本文详解为何 `capturestream()` 录制 vanta.js 动画时返回空 blob,并提供完整可运行的修复方案:移除冗余 canvas、精准选取 vanta 自动生成的 canvas、合理设置 mediarecorder 参数及 dom 结构。
在使用 Vanta.js 创建动态 3D 背景(如云朵、网格、噪声等)并配合 MediaRecorder 录制 canvas 动画时,一个常见却隐蔽的问题是:录制生成的 .webm 文件体积为 0 字节,Blob 对象为空。根本原因在于 DOM 中存在多个 <canvas></canvas> 元素,而 JavaScript 错误地获取了未被 Vanta 初始化的空白 canvas。
Vanta.js 在调用 VANTA.CLOUDS("#ele") 时,会自动向目标容器(如 #ele)内部动态插入一个专属的 <canvas></canvas> 元素,用于渲染 Three.js 动画。若你在 HTML 中预先声明了 <canvas id="canvas"></canvas>,该元素将与 Vanta 创建的 canvas 并存。此时 document.querySelector("canvas") 默认匹配第一个(即你手动写的空 canvas),其尚未被 Three.js 上下文初始化,captureStream() 虽能成功调用,但流中无有效帧数据,最终导致 chunks 数组为空,Blob 为空。
✅ 正确做法是:
-
彻底移除手动声明的
<canvas></canvas>标签,避免干扰; - 确保 Vanta 容器(如
#ele)是纯净的 div,由 Vanta 自主注入 canvas; - 使用
setTimeout或requestAnimationFrame等方式确保 Vanta 初始化完成后再启动录制(推荐延迟 100–300ms); -
captureStream()的帧率建议设为60(而非30),以更好匹配 Vanta 的默认渲染节奏; -
MediaRecorder.start()可传入timeslice(毫秒),但非必需;更稳妥的是显式调用stop(); - 注意
Blob的 MIME 类型应简化为"video/webm"(Chrome/Firefox 均兼容),避免硬编码codecs=h264(WebM 实际常用 VP8/VP9,h264 多用于 MP4)。
以下是精简、健壮、可直接运行的完整示例:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vanta + MediaRecorder Recording</title>
</head>
<body>
<!-- ✅ 移除所有手动 canvas -->
<div style="display: flex; flex-direction: column; height: 100vh; margin: 0;">
<div id="link" style="padding: 12px; font-size: 14px;"></div>
<div id="ele" style="flex: 1; margin: 0;"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.clouds.min.js"></script>
<script>
// ✅ 初始化 Vanta(异步,内部创建 canvas)
const vanta = VANTA.CLOUDS({
el: "#ele",
mouseControls: true,
touchControls: true,
gyroControls: false,
minHeight: 200,
minWidth: 200,
scale: 1.0,
backgroundColor: 0x000000
});
// ✅ 等待 Vanta 渲染一帧后开始录制(更可靠 than fixed timeout)
const startRecording = () => {
// 确保 canvas 已挂载且有 WebGL context
const canvas = document.querySelector("#ele canvas");
if (!canvas) {
console.error("Vanta canvas not found. Retry after init.");
return;
}
const chunks = [];
const stream = canvas.captureStream(60); // 60fps 匹配 Vanta 渲染
const mediaRecorder = new MediaRecorder(stream, {
mimeType: "video/webm"
});
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) chunks.push(e.data);
};
mediaRecorder.onstop = () => {
if (chunks.length === 0) {
alert("⚠️ No video data captured — check canvas rendering & permissions.");
return;
}
const blob = new Blob(chunks, { type: "video/webm" });
downloadVideo(blob);
};
mediaRecorder.start();
setTimeout(() => mediaRecorder.stop(), 2000); // 录制 2 秒
};
// ✅ 推荐:在 Vanta ready 后触发(Vanta v0.5.25+ 支持 onReady 回调)
if (vanta && typeof vanta.destroy === 'function') {
// 模拟等待:实际项目中可监听 vanta.ready 或使用 Promise 包装
setTimeout(startRecording, 300);
} else {
startRecording();
}
const downloadVideo = (blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "vanta-recording.webm";
a.textContent = "✅ Click to download recording (2s)";
a.style.display = "block";
a.style.margin = "8px 0";
const span = document.createElement("span");
span.innerHTML = `<small>Preview URL: <code>${url}</code></small>`;
span.style.display = "block";
span.style.fontSize = "12px";
span.style.color = "#666";
const linkDiv = document.getElementById("link");
linkDiv.innerHTML = "";
linkDiv.append(a, span);
};
</script>
</body>
</html>⚠️ 关键注意事项:
-
不要在本地文件系统(
file://协议)中运行:MediaRecorder在部分浏览器中对file://限制严格,可能导致captureStream()返回空流或报错。请使用本地服务器(如npx serve、VS Code Live Server 或 Pythonhttp.server)部署测试。 - 权限与上下文:确保页面处于活跃标签页,且 canvas 渲染未被暂停(如切换到其他 tab 时,Three.js 可能节流)。
-
错误处理:务必检查
chunks.length和e.data.size,避免静默失败。 - 性能提示:长时间录制高帧率动画可能产生较大文件,生产环境建议添加进度反馈与取消机制。
通过以上调整,即可稳定捕获 Vanta.js 动画为高质量 WebM 视频,解决 blob is null 的核心痛点。


















