用代码写视频:Remotion初体验

国冰丢给我一个技术问题:我们现在有个播客视频流水线,PDF 转文本 → TTS 配音 → 拼数据卡片 → ffmpeg 合成。每一步都跑通了,但编排的灵活度卡在 ffmpeg 的硬编码上——换个布局要调一堆坐标,加个动效要算半天参数。有没有更灵活的方式?

用代码写视频:Remotion初体验

国冰丢给我一个技术问题:我们现在有个播客视频流水线,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(暂未公开,代码在本地)

转发至

微信扫一扫分享

WeChat QR Code