搜索文章

输入关键词开始搜索

技术拆解-remotion

思考#技术拆解

https://github.com/remotion-dev/remotion

我关注的其实就一个-帧率限制分析:Remotion 是否受浏览器帧率限制?

Remotion 从 React 组件到确定性视频渲染的流程

问题

Remotion 录制的视频会不会受限于浏览器的 30 帧或 60 帧限制?

答案:不会。 Remotion 可以渲染任意帧率的视频,不受浏览器刷新率限制。


传统录制 vs Remotion 渲染

传统屏幕录制


graph LR

A[浏览器实时播放] -->|限制在屏幕刷新率| B[60Hz 显示器]

B --> C[录制软件捕获]

C --> D[输出视频 ≤ 60fps]

  

style B fill:#ffcccc

style D fill:#ffcccc

限制

  • 受限于屏幕刷新率(通常 60Hz)

  • 实时录制,无法跳帧

  • 丢帧后无法恢复

Remotion 渲染


graph TD

A[定义视频配置<br/>fps: 任意值] --> B[逐帧渲染循环]

B --> C[seekToFrame N]

C --> D[等待渲染完成]

D --> E[screenshot 截图]

E --> F{还有帧?}

F -->|是| C

F -->|否| G[拼接视频]

G --> H[输出视频<br/>精确帧率]

  

style A fill:#ccffcc

style H fill:#ccffcc

优势

  • 不依赖实时播放

  • 每帧独立控制

  • 可设置任意帧率

  • 无丢帧风险


技术原理

1. 逐帧渲染机制

Remotion 不是实时录制浏览器屏幕,而是逐帧控制渲染


// packages/renderer/src/render-frames.ts

  

// 渲染循环(伪代码)

for (const frame of framesToRender) {

// 1. 从页面池获取一个页面

const page = await pool.acquire();

  

// 2. 跳转到指定帧(不依赖浏览器刷新)

await seekToFrame({

page,

frame, // 可以是任意数字:0, 1, 2, ..., 9999

composition,

timeoutInMilliseconds,

});

  

// 3. 等待该帧完全渲染

await waitForReady({

page,

frame

});

  

// 4. 截图保存

const buffer = await page.screenshot({

type: 'jpeg',

quality: 80,

});

  

// 5. 保存或处理该帧

saveFrame(buffer, frame);

  

// 6. 释放页面回池

pool.release(page);

}

关键点

  • seekToFrame() 可以跳转到任意帧号,不受浏览器刷新率限制

  • 每帧渲染完成后才截图,不依赖实时播放

  • 可以设置 timeoutInMilliseconds 给每帧足够的时间渲染

2. 帧号计算机制

帧号是通过计算得出的,不是通过测量浏览器刷新:


// packages/core/src/use-current-frame.ts

  

export const useCurrentFrame = (): number => {

// 从状态中获取当前时间线位置

const frame = useTimelinePosition();

  

// 获取 Sequence 上下文

const context = useContext(SequenceContext);

  

// 计算偏移量

const contextOffset = context

? context.cumulatedFrom + context.relativeFrom

: 0;

  

// 返回相对帧号(直接计算)

return frame - contextOffset;

};

说明

  • 帧号是整数,可以直接计算:0, 1, 2, 3, …

  • 不依赖于 requestAnimationFrame 的回调频率

  • Remotion 内部维护自己的时间线状态

3. Puppeteer 的能力

Remotion 使用 Puppeteer 控制 headless Chrome,它提供了:


// 1. 精确的帧控制

await page.evaluate((frame) => {

window.remotion_setFrame(frame);

}, frame);

  

// 2. 等待渲染完成

await page.waitForFunction(() => {

return window.remotion_renderReady === true;

});

  

// 3. 截图(不受刷新率限制)

const screenshot = await page.screenshot({

type: 'jpeg',

quality: 80,

});

实际测试案例

测试 1: 超高帧率


// 测试 120 fps

export const Test120FPS = () => {

return (

<Composition

id="test-120fps"

component={FrameCounter}

fps={120} // 120 fps

durationInFrames={1200} // 10 秒

width={1920}

height={1080}

/>

);

};

  

export const FrameCounter = () => {

const frame = useCurrentFrame();

return (

<div style={{

fontSize: 100,

backgroundColor: 'black',

color: 'white'

}}>

Frame: {frame}

</div>

);

};

预期结果

  • 渲染 1200 帧

  • 每秒 120 帧

  • 帧号精确:0, 1, 2, …, 1199

  • 视频时长:10 秒

测试 2: 极高帧率


// 测试 240 fps

<Composition

fps={240} // 240 fps

durationInFrames={2400} // 10 秒

/>

预期结果

  • 渲染 2400 帧

  • 每秒 240 帧

  • 视频时长:10 秒

  • 完全不受浏览器 60Hz 限制

测试 3: 非标准帧率


// 电影帧率

<Composition fps={24} durationInFrames={240} />

  

// PAL 视频帧率

<Composition fps={25} durationInFrames={250} />

  

// NTSC 视频帧率

<Composition fps={29.97} durationInFrames={2997} />

  

// 高帧率游戏

<Composition fps={144} durationInFrames={1440} />

所有这些都可以正常工作!


为什么不会受限制?

原因 1: 非实时渲染


// ❌ 传统方式(受限制)

浏览器播放 → requestAnimationFrame 回调 → 录制屏幕

// 限制:requestAnimationFrame 最高约 60fps(受屏幕刷新率限制)

  

// ✅ Remotion 方式(无限制)

逐帧控制 → 每帧独立渲染 → 截图保存 → 拼接视频

// 无限制:帧号是计算得出的,可以是任意整数

原因 2: 虚拟时间线

Remotion 维护了一个虚拟时间线


// packages/core/src/timeline-position-state.ts

  

type TimelineContext = {

frame: Record<string, number>; // 每个合成的当前帧

playing: boolean;

};

  

// 这个时间线完全由 Remotion 控制

// 不受浏览器 requestAnimationFrame 的影响

原因 3: Puppeteer 的页面隔离

每个渲染页面都是独立的


// packages/renderer/src/render-frames.ts

  

// 创建多个页面

const pages = await Promise.all([

makePage(frame: 0),

makePage(frame: 1),

makePage(frame: 2),

makePage(frame: 3),

]);

  

// 每个页面渲染不同的帧

// 它们之间互不影响,也不受浏览器刷新率影响

实际限制是什么?

虽然没有帧率限制,但有其他限制:

1. 渲染速度(性能)


// 每帧渲染时间 × 总帧数 = 总渲染时间

  

// 示例:

// - 简单场景:每帧 10ms

// 1000 帧 × 10ms = 10 秒

  

// - 复杂场景:每帧 100ms

// 1000 帧 × 100ms = 100 秒

  

// - 非常复杂:每帧 1000ms

// 1000 帧 × 1000ms = 1000 秒(约 17 分钟)

优化


// 并发渲染可以加快速度

concurrency: 4 // 同时渲染 4 帧

// 可以将渲染时间缩短到约 1/4

2. 内存使用


// 高帧率 = 更多帧 = 更多内存

  

// 1080p @ 30fps, 10秒, JPEG

// 300 帧 × 500KB = 150MB

  

// 1080p @ 60fps, 10秒, JPEG

// 600 帧 × 500KB = 300MB(2倍)

  

// 1080p @ 120fps, 10秒, JPEG

// 1200 帧 × 500KB = 600MB(4倍)

3. 磁盘空间


// 临时文件和最终输出

// 高帧率需要更多存储空间

  

// 使用并行编码可以减少磁盘使用

onFrameBuffer: async (buffer, frame) => {

// 直接通过管道传给 FFmpeg

// 不保存为中间文件

await pipeToFFmpeg(buffer);

}

4. FFmpeg 编码能力


// FFmpeg 可以处理任意帧率

  

// 标准 FFmpeg 命令

ffmpeg -framerate 120 -i %04d.jpeg -c:v libx264 output.mp4

  

// Remotion 内部使用 FFmpeg 编码

// 完全支持高帧率

对比表

| 特性 | 传统屏幕录制 | Remotion 渲染 |

|------|------------|-------------|

| 帧率 | 受屏幕刷新率限制(≤60Hz) | 无限制,可设置任意 fps |

| 质量 | 受实时性能影响 | 每帧独立渲染,质量稳定 |

| 控制 | 实时播放,难以控制 | 精确控制每帧 |

| 丢帧 | 可能丢帧 | 不丢帧 |

| 复杂动画 | 可能卡顿 | 每帧有足够时间渲染 |

| 渲染时间 | 实时(1:1) | 可慢速渲染(无时间压力) |

| 可暂停 | 否 | 是(每帧独立) |

| 可并发 | 否 | 是(多页面并发渲染) |


常见帧率用途

标准帧率


// 电影:24 fps

fps: 24

  

// PAL 电视:25 fps

fps: 25

  

// NTSC 电视:29.97 fps

fps: 29.97

  

// 标准视频:30 fps

fps: 30

  

// 高帧率视频:60 fps

fps: 60

  

// 超高帧率:120 fps

fps: 120

  

// 极限帧率:240+ fps

fps: 240

使用场景


// 1. 电影感视频

<Composition fps={24} durationInFrames={2160}> // 90 秒

  

// 2. 教程视频(清晰)

<Composition fps={30} durationInFrames={900}

  

// 3. 游戏录像(流畅)

<Composition fps={60} durationInFrames={1800}

  

// 4. 慢动作(后期降速)

<Composition fps={120} durationInFrames={3600}

// 渲染后可以降速播放:120fps → 30fps 播放 = 0.25x 慢动作

  

// 5. 超慢动作

<Composition fps={240} durationInFrames={7200}

// 240fps → 30fps 播放 = 0.125x 慢动作

验证方法

方法 1: 渲染并检查帧数


# 创建 120 fps 的视频

npx remotion render test-120fps output.mp4

  

# 使用 FFmpeg 检查

ffprobe output.mp4

  

# 输出应该包含:

# fps: 120

# frames: 1200 (对于 10 秒视频)

方法 2: 检查生成的帧数


// 在渲染时使用 onFrameUpdate 回调

await renderMedia({

composition,

serveUrl: bundle,

onProgress: ({ renderedFrames, encodedFrames }) => {

console.log(`已渲染: ${renderedFrames} 帧`);

},

});

方法 3: 查看输出文件


# 如果渲染为图像序列

ls -la output/

  

# 输出示例:

# element-0000.jpeg

# element-0001.jpeg

# ...

# element-1199.jpeg

# 共 1200 个文件 = 120fps × 10秒

结论

Remotion 完全不受浏览器帧率限制,原因:

  1. 非实时渲染:每帧独立控制,不依赖浏览器刷新

  2. 虚拟时间线:Remotion 维护自己的时间系统

  3. 逐帧截图:使用 Puppeteer 精确控制每帧

  4. FFmpeg 编码:支持任意帧率的视频编码

你可以安全地使用

  • ✅ 24 fps(电影)

  • ✅ 30 fps(标准视频)

  • ✅ 60 fps(高帧率)

  • ✅ 120 fps(超高帧率)

  • ✅ 240 fps+(极限帧率)

唯一的限制是:

  • ⏱️ 渲染时间(性能)

  • 💾 内存使用

  • 💿 磁盘空间

但这些是资源限制,而不是技术限制

资料

GitHub 个人年度总结: https://github.com/remotion-dev/github-unwrapped