Remotion(程序化视频制作)
Remotion 逐帧渲染 React 组件。每一帧都是帧号的一个纯函数,所以动画是确定性的且可重新渲染。
Remotion 的 API 在不同主版本之间会发生变化。在不确定 API 用法时,请通过
npm ls remotion查看已安装的版本,并查阅 remotion.dev 上的官方文档。企业用户在使用前需确认 Remotion 的许可条款。
安装与设置
npx create-video@latest # 选择空白模板(TypeScript)
cd my-video && npm install
npx remotion studio # 实时预览
npx remotion render MyComp out/video.mp4
npx remotion still MyComp out/thumb.png --frame=45
心智模型
useCurrentFrame()返回当前帧(从 0 开始)。useVideoConfig()返回fps、width、height、durationInFrames。- 没有任何东西会自动动画化。你需要根据帧号计算样式。
- 永远不要在动画中使用 CSS 过渡、
setTimeout、Math.random()或Date.now()。它们会破坏渲染的确定性。请使用 Remotion 提供的random('seed')。
合成
// src/Root.tsx
import {Composition} from 'remotion';
import {Explainer} from './Explainer';
export const RemotionRoot = () => (
<Composition
id="Explainer"
component={Explainer}
durationInFrames={30 * 20} // 30 fps 下持续 20 秒
fps={30}
width={1920}
height={1080}
defaultProps={{title: 'Hello'}}
/>
);
动画原语
import {AbsoluteFill, Sequence, interpolate, spring, useCurrentFrame, useVideoConfig} from 'remotion';
export const Title: React.FC<{text: string}> = ({text}) => {
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const opacity = interpolate(frame, [0, 15], [0, 1], {extrapolateRight: 'clamp'});
const scale = spring({frame, fps, config: {damping: 200}}); // 平滑,无弹跳
return (
<AbsoluteFill style={{justifyContent: 'center', alignItems: 'center'}}>
<h1 style={{opacity, transform: `scale(${0.9 + 0.1 * scale})`, fontSize: 96}}>{text}</h1>
</AbsoluteFill>
);
};
export const Explainer = () => (
<AbsoluteFill style={{backgroundColor: '#0b1020', color: 'white'}}>
<Sequence from={0} durationInFrames={90}><Title text="介绍" /></Sequence>
<Sequence from={90} durationInFrames={120}><Title text="核心观点" /></Sequence>
</AbsoluteFill>
);
除非你想让值超出范围,否则始终在 interpolate 中传递 extrapolateLeft/Right: 'clamp'。
媒体文件
import {staticFile, Audio, OffthreadVideo, Img} from 'remotion';
<Audio src={staticFile('voiceover.mp3')} />
<Audio src={staticFile('music.mp3')} volume={0.12} />
<OffthreadVideo src={staticFile('demo.mp4')} />
<Img src={staticFile('logo.png')} />
将资源放在 public/ 目录中,并使用 staticFile() 引用。使用 <Img> 而非 <img>,以便渲染等待图片加载完成。较新版本的 Remotion 提供了更新的媒体组件;请根据已安装版本查阅文档。
场景与旁白同步
不要硬编码场景时长,而是从音频中推导。
- 生成每个场景的旁白文件(
voiceover-tts技能)。 - 使用
ffprobe测量每个片段的时长,并将结果写入scenes.json,格式为{id, audio, seconds}。 - 在合成中,通过
Math.ceil((seconds + 0.5) * fps)进行转换,并根据累积偏移量布局<Sequence>。 - 使用
<Composition>上的calculateMetadata从该数据设置总的durationInFrames。
如果重新录制了旁白,视频会自动调整时长。
过渡效果
安装 @remotion/transitions,并使用 TransitionSeries 配合 fade()、slide()、wipe()。过渡效果会让两个场景重叠,因此总长度会缩短过渡时长。在 30 fps 下,过渡推荐设置为 12 到 20 帧。
字体与品牌
使用 @remotion/google-fonts 或 @remotion/fonts 加载字体,确保在第一帧渲染前字体已就绪。将颜色、字体和 Logo 集中放在一个 brand.ts 文件中,这样更换品牌只需修改一个文件。
性能与渲染
- 在 Studio 中预览;从 CLI 渲染。
--concurrency=50%限制 CPU 使用率。--codec=h264是默认编码;如需后期编辑交接,使用--codec=prores。- 大量使用模糊、阴影和大型 SVG 滤镜会降低性能。这类效果优先使用预渲染资源。
- 对于源素材,使用
<OffthreadVideo>替代<video>以获得帧精确提取。 - 快速草稿:使用
--scale=0.5和--frames=0-90范围。
常见错误
| 错误 | 修复方法 |
|---|---|
| 渲染时闪烁或图片缺失 | 使用 <Img>、staticFile,异步数据使用 delayRender |
| 预览与渲染动画不一致 | 移除非确定性代码(time、random) |
| 不同语言下文字溢出 | 设置显式宽度;测试最长的字符串 |
| 添加过渡后视频过长 | 从总时长中减去过渡帧数 |
| 渲染后音频无声 | 检查文件路径以及 <Audio> 是否位于活动的 <Sequence> 内部 |
可搭配 motion-design-craft 技能使用,以优化时间曲线和缓动函数。