2699 字
约 8 分钟
16
S27-X03 · Remotion:用代码生成视频的工程方式

剪辑软件擅长的是"做一条片子"。当需求变成"用同一套版式出 500 条,每条只换标题、数据、封面",手工剪辑的成本会随条数线性上涨,而代码生成的成本几乎是常数。Remotion 的思路很直接:把视频的每一帧描述成一个 React 组件,帧号就是输入,画面就是输出,出片变成一次构建。

它解决的不是替代剪辑,而是把重复劳动的版式变成可复用的函数。


1. 用代码做视频解决什么

需求形态 手工剪辑的做法 代码生成的做法
批量出片 复制工程、逐个改文案 改数据文件,重跑渲染命令
数据驱动 人肉抄表格进画面 组件直接读 JSON / 接口返回值
版本控制 二进制工程文件无法 diff 源码进 Git,改动可评审可回滚
复用组件 复制粘贴时间轴片段 抽成组件,改一处全部生效
参数化变体 手动对齐时间点 props 驱动,同一份代码出多语言/多尺寸
确定性重出 依赖人记得所有细节 同一份输入永远得到同一份输出

判断标准很简单:如果同一条片子你做第二遍,且第二遍和第一遍只有数据不同,就该用代码生成。

2. 与剪辑软件的适用边界

场景 更适合 原因
一次性的宣传片、需要大量调色与运镜 剪辑软件 创意探索靠手感,不靠参数
每周几十条同版式短视频 代码生成 版式固定,只有数据在变
素材本身要精剪(多机位、访谈) 剪辑软件 波形对齐、节拍剪辑是编辑器强项
画面由数据决定(图表、榜单、排行榜) 代码生成 数据变化直接反映到画面
复杂特效、粒子、3D 合成 剪辑/合成软件,或代码生成 + 预渲染素材 两者可组合,不要二选一

实践中更常见的是混合:片头片尾与动态数据画面用代码生成,中间的精剪片段由剪辑软件产出,再用代码拼起来。

3. 核心模型:帧、插值与 Composition

三个概念撑起全部:useCurrentFrame() 给出当前帧号;interpolate() 把帧号映射成任意属性;Composition 定义这条片子的时长、帧率、分辨率。

// src/Root.tsx
import { Composition } from "remotion";
import { Episode } from "./Episode";

export const RemotionRoot: React.FC = () => (
  <Composition
    id="Episode"
    component={Episode}
    durationInFrames={30 * 12}   // 12 秒 × 30fps,注意这里是帧不是秒
    fps={30}
    width={1920}
    height={1080}
    defaultProps={{ title: "占位标题", items: [] as string[] }}
  />
);
// src/Episode.tsx
import { useCurrentFrame, useVideoConfig, interpolate, AbsoluteFill } from "remotion";

export const Episode: React.FC<{ title: string; items: string[] }> = ({ title, items }) => {
  const frame = useCurrentFrame();
  const { fps, durationInFrames } = useVideoConfig();

  // 0~20 帧内从透明到不透明,之后保持
  const opacity = interpolate(frame, [0, 20], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  // 整片进度,用来做进度条
  const progress = interpolate(frame, [0, durationInFrames], [0, 1]);

  return (
    <AbsoluteFill style={{ backgroundColor: "#0f172a", padding: 80 }}>
      <h1 style={{ color: "#fff", opacity }}>{title}</h1>
      <div style={{ color: "#38bdf8", width: `${progress * 100}%`, height: 8 }} />
      <ul style={{ color: "#e2e8f0" }}>
        {items.map((it) => <li key={it}>{it}</li>)}
      </ul>
    </AbsoluteFill>
  );
};

几个必须记牢的点:

API 单位 常见误用
durationInFrames 帧 直接填"秒数",结果片子短了 30 倍
fps 帧/秒 与素材帧率不一致,音画不同步
interpolate 的 extrapolate* clamp / extend / identity 不设 clamp,动画结束后数值继续外推
useVideoConfig() 当前 Composition 配置 在组件里硬编码 1920×1080,换尺寸就错位
Sequence 的 from / durationInFrames 帧 用毫秒算,时间轴整体错位

4. 项目结构、预览与出片

# 预览:本地打开 Studio,改代码即时热更新
npx remotion studio

# 出片:渲染某个 Composition
npx remotion render Episode out/episode-001.mp4

# 指定并发与质量
npx remotion render Episode out/episode-001.mp4 --concurrency=4

# 用外部数据覆盖 defaultProps
npx remotion render Episode out/001.mp4 --props=./data/001.json

典型目录形态:

路径 放什么
src/Root.tsx Composition 注册表,所有片子的入口
src/<Name>.tsx 每条片子的组件
src/components/ 可复用片段(片头、字幕条、数据卡)
public/ 图片、音频、字体等静态素材
data/ 每条片子一份 JSON 数据

--props 读的是 JSON 文件,也可以用 --props='{"title":"..."}' 直接内联。命令行 props 会覆盖 defaultProps,但不会自动帮你补齐缺失字段——少传的字段要么在组件里给默认值,要么在渲染前校验,否则画面里会出现 undefined。

5. 数据驱动与参数化

组件入口处做一次 props 收敛(title 兜底 "未命名"、items 限长 slice(0, 5)),是批量场景里最便宜的一道保护。

批量渲染最省事的写法是让每条片子一个 JSON,然后用一个循环驱动:

#!/usr/bin/env bash
set -euo pipefail
mkdir -p out
for f in data/*.json; do
  name="$(basename "$f" .json)"
  [[ -f "out/$name.mp4" ]] && continue          # 断点续跑:已有产物就跳过
  echo "== render $name"
  npx remotion render Episode "out/$name.mp4" --props="$f" --concurrency=4
done

set -euo pipefail 与"已有产物跳过"这两句在批量场景里很关键:500 条里挂第 300 条时,重跑不会把前 299 条重做一遍。

6. 音频与字幕轨道

音频不是"背景",它和时间轴一样是帧级的:

import { Audio, Sequence, staticFile, useCurrentFrame, useVideoConfig } from "remotion";

// 背景音乐:整片铺底,带淡入
<Audio src={staticFile("bgm.mp3")} volume={(f) =>
  interpolate(f, [0, 15], [0, 0.6], { extrapolateRight: "clamp" })
} />

// 第 3 秒起插入一段 2 秒的旁白
<Sequence from={3 * 30} durationInFrames={2 * 30}>
  <Audio src={staticFile("vo/001.mp3")} />
</Sequence>
轨道 做法 坑
背景音乐 <Audio> 铺满整片 + volume 回调 音频比视频长/短,结尾被硬切
旁白 Sequence 定位起点 from 用秒算,整体偏移
字幕 把 SRT 解析成 [{startMs, endMs, text}],再按帧区间渲染 直接渲染整段文本,时间轴对不上

字幕的稳定做法是先解析成结构化数组,渲染时用当前帧反查命中项,而不是在页面里做 DOM 动画。

7. 渲染性能与并发

渲染是把每一帧交给浏览器引擎画一遍,再编码成视频,所以瓶颈通常有三处:帧数 × 复杂度、编码器、内存。

旋钮 作用 调整思路
--concurrency 同时渲染的帧数(按浏览器标签/线程切分) 从机器核数起步,观察内存再往上加
分辨率与码率 单帧像素量与编码压力 需要 4K 就 4K,不需要别默认拉满
帧率 帧总数 30fps 足够就不上 60fps,帧数直接翻倍

长视频的时间成本可以用一个粗算公式自查:总帧数 ÷ 并发数 × 单帧平均渲染耗时。12 秒 30fps 是 360 帧,看起来很小;把片子拉到 20 分钟,就是 36000 帧,同样的单帧耗时会变成近百倍的时长。所以工程上更推荐分片渲染再拼接,而不是一次性渲染一条长片。

8. 放进 CI 批量出片

# .github/workflows/render.yml(片段)
jobs:
  render:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        data: [001, 002, 003]      # 需要多少条就在这里展开
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - name: Render
        run: npx remotion render Episode "out/${{ matrix.data }}.mp4" \
             --props="data/${{ matrix.data }}.json" --concurrency=2
      - uses: actions/upload-artifact@v4
        with:
          name: video-${{ matrix.data }}
          path: out/*.mp4

CI 里要额外留意三件事:渲染需要的浏览器运行时必须在 CI 环境里存在(安装依赖时按当前版本核对是否需要显式安装);字体要么打进仓库、要么用字体包显式加载,不能依赖构建机的系统字体;产物走 artifact 上传,不要塞进 Git 历史。


9. 排错:七种"看着不对"的情况

现象 原因 处理
出来的片子比预期短/长几倍 把秒数填进了 durationInFrames,或把 30fps 当 25fps 算 统一用"秒 × fps"推导帧数,全项目只维护 fps 一处
中文全是方框或豆腐块 渲染环境没有该字体 public/ 放字体文件并显式加载,CI 与本地字体一致
本地能渲染、CI 报浏览器相关错误 缺少渲染所需的浏览器运行时 在 CI 步骤里按当前版本核对补齐依赖与系统库
渲染中途进程被杀 并发太高导致内存爆 降 --concurrency,或拆成分片渲染
音画不同步 音频素材采样率/偏移与帧率换算不一致 以帧为单位描述音频起点,长片分段核对
画面里出现 undefined --props 缺字段且组件没有兜底 渲染前做 props 校验,给默认值

10. 边界:这些情况别硬上

场景 更合适的做法 理由
只出一次、以后不再改 剪辑软件 写组件的前期投入收不回来
大量运镜、调色、手工特效 剪辑/合成软件 创意型操作不适合参数化
极长视频(几十分钟以上) 分片渲染 + 拼接,或交给专业转码链路 单进程渲染的内存与时间成本不可控

一句话定位:Remotion 把"版式"变成可版本控制、可参数化、可批量执行的代码,但它不替代剪辑,它替代的是"重复剪同一套版式"。


11. 自测

  1. 什么时候值得把视频从剪辑软件搬到代码生成?
  2. durationInFrames 填 12 而 fps 是 30,实际会出多长的片子?
  3. interpolate 不写 extrapolateRight: "clamp" 会出现什么现象?
  4. 批量渲染 500 条时,为什么要先判断产物是否已存在?
  5. 本地渲染正常、CI 渲染报浏览器错误,先查哪一类依赖?
  6. 一段 20 分钟、30fps 的片子大概是多少帧,为什么建议分片渲染?
S27-X03 · Remotion:用代码生成视频的工程方式
https://www.clxhxhhr.top/posts/5776/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。