确定性渲染
Hyperframes 围绕一个核心保证构建:相同的合成总是产生相同的视频。这正是自动化流程、CI 测试和 AI 驱动工作流可靠的基础。
工作原理
渲染流水线是逐帧、定位驱动的。不涉及实时播放——每一帧都独立地被定位和捕获。
帧时钟
引擎使用整数运算计算每帧的时间:time = floor(frame) / fps。没有挂钟依赖——渲染完全与实时解耦。
定位
帧适配器接收 seekFrame(frame) 调用,并确定性地将所有动画、DOM 状态和 Canvas 内容定位到精确的帧。适配器的 renderSeek 暂停所有 GSAP 时间线并将它们定位到计算出的时间。
捕获
Chrome 的 HeadlessExperimental.beginFrame API 捕获当前帧的像素缓冲区。这是一个原子操作——没有部分绘制或竞态条件。
编码
FFmpeg 将捕获的帧编码为最终的 MP4 视频。来自 <audio> 和 <video> 元素的音轨在此阶段被混合。
graph LR
A["帧时钟<br/>t = frame / fps"] --> B["定位<br/>adapter.seekFrame(frame)"]
B --> C["捕获<br/>beginFrame API"]
C --> D["编码<br/>FFmpeg"]
D --> E["MP4"]
style A fill:#00C4FF,color:#fff
style B fill:#00C4FF,color:#fff
style C fill:#00C4FF,color:#fff
style D fill:#00C4FF,color:#fff
style E fill:#00A8E1,color:#fff
什么使它具有确定性
- 无挂钟依赖 -- 渲染不使用
Date.now()、requestAnimationFrame或系统定时器 - 无未设种子的随机性 -- 没有种子的
Math.random()会破坏确定性 - 无渲染时网络请求 -- 所有资源必须在渲染开始前加载完毕
- 固定输出参数 --
fps、width和height在第一帧之前就已锁定 - 有限时长 -- 每个合成都有一个已知的、有限的长度
同样的规则适用于每个帧适配器。如果你正在构建自定义适配器,必须遵循确定性契约。
Docker 模式
为了获得最大可重现性,在 Docker 中渲染:
npx hyperframes render --docker --output output.mp4
Docker 模式使用精确的 Chrome 版本和字体集,确保:
- 所有平台上使用相同的 Chromium 渲染引擎
- 相同的系统字体(无平台特定的字体替换)
- 相同的 FFmpeg 编码器版本
所有渲染选项请参阅渲染指南。
预览与渲染的一致性
浏览器预览和渲染的 MP4 应该保持一致。Hyperframes 通过以下方式实现这一点:
- 统一运行时 -- 相同的
hyperframe.runtime同时驱动预览和渲染 - 生产器规范行为 -- 生产器的定位语义是权威来源
- 就绪门控 --
__playerReady和__renderReady确保合成在捕获任何帧之前完全加载
这里的一致性指的是视觉保真度——每一帧看起来相同。它不意味着性能一致性。预览在浏览器中实时播放,因此帧率受硬件限制。渲染是定位驱动的、逐帧进行的,因此无论单帧成本如何都不会丢帧。一个合成在预览中可能卡顿,但渲染时完美呈现。详情请参阅性能。
ℹ️ Note
本地渲染(不使用 Docker)可能由于平台特定的字体渲染和 Chrome 版本而显示轻微差异。当需要精确可重现性时,请使用 Docker 模式。
面向适配器作者
如果你正在构建帧适配器,你的适配器必须遵循确定性契约:
seekFrame(frame)必须是幂等的——相同帧,相同结果- 无依赖调用顺序的副作用(必须支持随机访问)
- 无在帧"提交"之后才完成的异步操作
- 干净的生命周期:
init->seekFrame(N 次)->destroy
下一步
- 帧适配器 构建遵守确定性契约的适配器
- 渲染 在本地或 Docker 中渲染为 MP4
- @hyperframes/producer 编排确定性输出的完整渲染流水线
- 常见错误 破坏确定性的陷阱及如何避免