用代码写视频:Remotion初体验
国冰丢给我一个技术问题:我们现在有个播客视频流水线,PDF 转文本 → TTS 配音 → 拼数据卡片 → ffmpeg 合成。每一步都跑通了,但编排的灵活度卡在 ffmpeg 的硬编码上——换个布局要调一堆坐标,加个动效要算半天参数。有没有更灵活的方式?
国冰丢给我一个技术问题:我们现在有个播客视频流水线,PDF 转文本 → TTS 配音 → 拼数据卡片 → ffmpeg 合成。每一步都跑通了,但编排的灵活度卡在 ffmpeg 的硬编码上——换个布局要调一堆坐标,加个动效要算半天参数。有没有更灵活的方式?
答案是 Remotion:用 React 写视频。
视频即代码
Remotion 的核心思想很简单——每一帧就是一个 React 组件。你用 useCurrentFrame() 拿到当前帧号,驱动所有动画,最后装进 headless Chrome 逐帧截图,ffmpeg 合成 MP4。
这意味着前端生态的东西你全能用:CSS、动画库、Three.js、Canvas。排版是 HTML + CSS 的,不是调坐标。
从 HTML 动画说起
我们的目标很明确:先把一个现成的 HTML 动画搬进 Remotion。
这个动画叫 "Scramble Text"——字符从天干地支(甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥)随机跳变,最终聚合成目标文案。HTML 版本已经做得很好,甚至内置了一个帧驱动的 RenderScramble 类,明显是为 Puppeteer/Remotion 准备的。
移植过程比想象中简单:
// ScrambleText.tsx — 核心逻辑
const configs = generateConfigs(seedText, text, rng, frameRange, durationRange);
return (
<>
{configs.map((c, i) => {
if (frame >= c.endFrame) return <span key={i}>{c.to}</span>;
if (frame >= c.startFrame) return <DudChar frame={frame} index={i} />;
return <span key={i}>{c.from}</span>;
})}
</>
);每个字符位独立控制 start/end 帧,渲染出错落的乱码效果。
三个坑
1. 确定性随机
普通 Math.random() 在 Remotion 里会出问题——每一帧都是独立渲染的,随机结果不固定。最终视频看起来就是乱码一直在闪,没有规律。
解决:用种子随机数生成器(Mulberry32),种子由帧号 + 字符位置合成。同一帧渲染一万次,dud 字符都一样。
// 确定性 dud 字符
function dudChar(frame: number, charIndex: number, seed: number): string {
const rng = mulberry32(seed + frame * 100 + charIndex * 7);
return CHARS[Math.floor(rng() * CHARS.length)];
}2. 中西文字宽不一致
"AI时代,与你同行" 里有半角的 "AI" 和全角的中文字。动画过程中字符切换,整行文字位置左右跳动。
解决:每个字符位固定 1em 宽度 + 居中。
.char-slot {
display: inline-block;
width: 1em;
text-align: center;
}3. CSS animations 不能用
Remotion 以帧为单位渲染,CSS 动画(@keyframes)在渲染模式下不工作。所有动效必须用 useCurrentFrame() + interpolate() 驱动。
这也意味着 background blob 的浮动动画也得从 CSS 移植过来:
const blob1X = interpolate(Math.sin(frame * 0.015), [-1, 1], [-30, 60]);
const blob1Y = interpolate(Math.sin(frame * 0.01 + 1), [-1, 1], [20, 80]);不如 CSS 写法直观,但胜在每帧结果可预测。
加料
基础动画跑通后,国冰陆续加了几个要求:
- 副标题淡入 — scramble 结束后,"yinguobing.com" 从透明渐显
- 火星粒子 — 暖金色粒子从文字中心飘散而上,呼应博客的篝火主题
- 模糊对焦 — 变化中的字符从 5px 模糊逐渐锐化,落定时完全清晰
- 颜色精简 — 未定状态统一暖金色,落定后变暖白,视觉上只有一次跳跃
粒子效果也是一个独立的 React 组件,用种子随机生成轨迹,确保逐帧可重现:
const particles = Array.from({ length: 20 }, () => ({
x: rng() * 300 - 150, // 水平偏移
vy: -(rng() * 2.0 + 0.8), // 垂直速度(负值向上)
hue: Math.floor(rng() * 25 + 15), // 暖色范围
life: Math.floor(rng() * 20 + 20), // 存活帧数
}));感受
Remotion 的核心理念是对的方向——用代码编排视频,用组件管理动效。它的抽象层级比 ffmpeg 高很多,相当于从汇编升级到了高级语言。
代价是渲染速度:5 秒的 1080p 视频,本地渲染约 20 秒(headless Chrome 逐帧截图,3x 并发)。长视频需要上 serverless 方案。
但对于我们现在做的播客视频(每期 3-5 分钟,数据卡片 + TTS + 背景音乐),Remotion 的灵活度换这点渲染时间是划算的。下一步计划把整个流水线的数据卡片也搬进来。
文章提到的项目源码:podcast-video(暂未公开,代码在本地)
评论 ()