@hyperframes/engine

基于 Chrome BeginFrame API 的可寻址页面到视频捕获引擎。

"@hyperframes/engine"

engine 包提供了底层视频捕获管道:它在无头 Chrome 中加载 HTML 页面,独立地跳转到每一帧,并使用 Chrome 的 HeadlessExperimental.beginFrame API 捕获像素缓冲区。这是使 Hyperframes 渲染具有确定性的核心层。

npm install @hyperframes/engine

何时使用

⚠️ Warning

大多数用户不应直接使用 engine。 请改用 CLInpx hyperframes render)或 producer 包 — 它们会为你处理运行时注入、音频混音和编码。

在以下场景使用 @hyperframes/engine

  • 构建具有完全帧捕获控制的自定义渲染管道
  • 将 Hyperframes 捕获集成到现有的视频处理系统中
  • 捕获单个帧(例如用于缩略图或精灵图)而不编码为视频
  • 实现自定义编码后端(非 FFmpeg)

如果需要以下功能,请使用其他包:

  • 将 HTML 合成渲染为成品 MP4 或 WebM — 使用 producerCLI
  • 在浏览器中预览合成 — 使用 CLIstudio
  • 检查或解析合成 HTML — 使用 core

工作原理

engine 实现了一个与屏幕录制根本不同的跳转并捕获循环:

启动无头 Chrome

engine 启动 chrome-headless-shell,一个为通过 Chrome DevTools Protocol (CDP) 进行编程控制而优化的最小化无头 Chrome 二进制文件。

加载合成

你的 HTML 合成被加载到浏览器页面中。注入 Hyperframes 运行时来管理时间轴跳转。

逐帧跳转

对于视频中的每一帧(例如 30fps 的 30 秒视频有 900 帧),engine 调用 renderSeek(time) 将合成推进到精确的时间戳。不涉及时钟时间 — 每一帧都是独立定位的。

通过 BeginFrame 捕获

Chrome 的 HeadlessExperimental.beginFrame API 将合成器输出捕获为像素缓冲区。这会产生像素完美的帧,没有任何屏幕录制伪影。

传递帧数据

捕获的帧缓冲区被传递给消费者 — 通常是 FFmpeg(通过 producer)编码为 MP4,但你也可以提供自己的消费者。

这种方法保证了确定性渲染:相同的 HTML 始终产生相同的视频,无论系统负载或时序如何。

配置

import { resolveConfig, DEFAULT_CONFIG } from '@hyperframes/engine';
import type { EngineConfig } from '@hyperframes/engine';

// 使用默认值
const config = DEFAULT_CONFIG;

// 或使用覆盖项解析
const config = resolveConfig({
// ... 自定义选项
});

质量预设

预设使用场景速度
draft开发期间的快速迭代最快
standard生产渲染,质量与速度的良好平衡中等
high最终交付,最高质量最慢

FPS 选项

FPS使用场景
24电影感,较小的文件大小
30标准网页视频,良好的平衡
60流畅的运动,UI 动画,屏幕录制

编程方式使用

engine 使用基于会话的 API 进行帧捕获:

import {
createCaptureSession,
initializeSession,
captureFrame,
captureFrameToBuffer,
getCompositionDuration,
closeCaptureSession,
} from '@hyperframes/engine';

// 1. 创建捕获会话
const session = await createCaptureSession({ fps: { num: 30, den: 1 }, width: 1920, height: 1080 });

// 2. 用合成初始化
await initializeSession(session, './my-video/index.html');

// 3. 获取总时长
const duration = getCompositionDuration(session);

// 4. 捕获帧
const totalFrames = Math.ceil(duration * 30);
for (let i = 0; i < totalFrames; i++) {
// 捕获到磁盘
const result = await captureFrame(session, i);
// result.path, result.captureTimeMs

// 或捕获到缓冲区(内存中)
const bufResult = await captureFrameToBuffer(session, i);
// bufResult.buffer, bufResult.captureTimeMs
}

// 5. 清理
await closeCaptureSession(session);

浏览器管理

import {
acquireBrowser,
releaseBrowser,
resolveHeadlessShellPath,
buildChromeArgs,
} from '@hyperframes/engine';

// 获取浏览器实例(创建或从池中复用)
const browser = await acquireBrowser();

// 获取 Chrome 二进制路径
const chromePath = await resolveHeadlessShellPath();

// 使用完毕后释放
await releaseBrowser(browser);

编码

engine 包含 FFmpeg 编码工具,支持 MP4 (h264) 和 WebM (VP9 带 alpha):

import {
encodeFramesFromDir,
muxVideoWithAudio,
applyFaststart,
detectGpuEncoder,
getEncoderPreset,
ENCODER_PRESETS,
} from '@hyperframes/engine';

// 获取格式感知的编码器设置
const mp4Preset = getEncoderPreset('standard', 'mp4');
// { codec: "h264", pixelFormat: "yuv420p", preset: "medium", quality: 23 }

const webmPreset = getEncoderPreset('standard', 'webm');
// { codec: "vp9", pixelFormat: "yuva420p", preset: "good", quality: 23 }

// 将捕获的帧编码为视频
await encodeFramesFromDir(framesDir, 'frame_%06d.png', outputPath, {
fps: { num: 30, den: 1 },
...webmPreset,
});

// 将视频与音频混音(WebM 使用 Opus,MP4 使用 AAC)
await muxVideoWithAudio(videoPath, audioPath, outputPath);

// 应用 MP4 faststart 用于流式传输(WebM 不适用)
await applyFaststart(inputPath, outputPath);

// 检测 GPU 编码支持
const gpu = await detectGpuEncoder();
// gpu: "nvenc" | "videotoolbox" | "vaapi" | "qsv" | "amf" | null

WebM VP9 Alpha

当编码透明度时,使用 format: "webm" 配合 getEncoderPreset()。这会配置:

  • VP9 编解码器 (libvpx-vp9) 配合支持 alpha 的 yuva420p 像素格式
  • -auto-alt-ref 0alpha_mode=1 元数据用于正确的 alpha 编码
  • -row-mt 1 用于多线程 VP9 编码
  • Opus 音频 在混流步骤中(而非 MP4 的 AAC)

流式编码器

无需将帧写入磁盘即可进行内存高效的编码:

import { spawnStreamingEncoder } from '@hyperframes/engine';

const encoder = await spawnStreamingEncoder({
outputPath: './output.mp4',
fps: { num: 30, den: 1 },
width: 1920,
height: 1080,
});

// 直接将帧送入编码器
encoder.writeFrame(frameBuffer);
// ...
const result = await encoder.finalize();

视频帧提取

从源视频文件中提取帧以注入浏览器:

import {
parseVideoElements,
extractAllVideoFrames,
getFrameAtTime,
createFrameLookupTable,
FrameLookupTable,
} from '@hyperframes/engine';

// 从 HTML 解析视频元素
const videos = parseVideoElements(html);

// 从视频中提取所有帧
const frames = await extractAllVideoFrames(videoPath, { fps: 30 });

// 创建用于快速帧访问的查找表
const lookup = createFrameLookupTable(frames);
const frame = lookup.getFrameAtTime(5.0);

音频处理

import { parseAudioElements, processCompositionAudio } from '@hyperframes/engine';

// 从 HTML 解析音频元素
const audioElements = parseAudioElements(html);

// 处理并混音所有音频轨道
const mixResult = await processCompositionAudio({ audioElements, duration, fps });

并行渲染

import {
calculateOptimalWorkers,
distributeFrames,
executeParallelCapture,
getSystemResources,
} from '@hyperframes/engine';

// 检查系统资源
const resources = getSystemResources();

// 计算最优 worker 数量
const workers = calculateOptimalWorkers(totalFrames);

// 在 worker 之间分配帧
const tasks = distributeFrames(totalFrames, workers);

// 执行并行捕获
const results = await executeParallelCapture(tasks);

文件服务器

通过 HTTP 为浏览器提供合成文件:

import { createFileServer } from '@hyperframes/engine';

const server = await createFileServer({ root: './my-video', port: 0 });
// server.url, server.port
// ... 使用 server.url 作为合成 URL
await server.close();

HDR API

engine 导出两层 HDR 支持:色彩空间工具(用于分类源并配置 FFmpeg 编码器)和 WebGPU 回读运行时(用于将 CSS 动画 DOM 直接捕获为 HDR)。

要进行端到端的 HDR 渲染(将 HDR 视频和图像源合成为 HDR10 MP4),请使用 producer 或带有 HDR 自动检测 / --hdr / --sdr 的 CLI 渲染管道 — 参见 HDR 渲染。以下 API 用于自定义集成。

色彩空间工具

import {
isHdrColorSpace,
detectTransfer,
analyzeCompositionHdr,
getHdrEncoderColorParams,
DEFAULT_HDR10_MASTERING,
} from '@hyperframes/engine';
import type { HdrTransfer, HdrEncoderColorParams, HdrMasteringMetadata } from '@hyperframes/engine';

// 从 ffprobe 色彩空间分类单个源
isHdrColorSpace(colorSpace);          // boolean — BT.2020 / PQ / HLG 时为 true
detectTransfer(colorSpace);           // 'pq' | 'hlg'(先检查 isHdrColorSpace)

// 在多个源中选择主要的传输特性
analyzeCompositionHdr([cs1, cs2]);    // { hasHdr, dominantTransfer: 'pq' | 'hlg' | null }

// 为 x265 构建 FFmpeg 色彩参数 + HDR10 静态元数据
const params = getHdrEncoderColorParams('pq');
// {
//   colorPrimaries: 'bt2020',
//   colorTrc: 'smpte2084',
//   colorspace: 'bt2020nc',
//   pixelFormat: 'yuv420p10le',
//   x265ColorParams: 'colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc:master-display=...:max-cll=1000,400',
//   mastering: { masterDisplay: '...', maxCll: '1000,400' },
// }

getHdrEncoderColorParams 始终包含颜色标签 HDR10 静态元数据(母版显示 + 内容光照级别)。没有这些元数据,下游播放器会将文件视为 SDR BT.2020 并进行错误的色调映射。如果你有测量过的逐内容值,请传入自定义 HdrMasteringMetadata;否则保守的 DEFAULT_HDR10_MASTERING 默认值与大多数 HDR10 调色套件标记内容的方式一致。

WebGPU HDR DOM 捕获

要将 CSS 动画 DOM 直接捕获为 HDR(不涉及 FFmpeg 源),engine 提供了单独的 WebGPU 管道:

import {
launchHdrBrowser,
buildHdrChromeArgs,
initHdrReadback,
uploadAndReadbackHdrFrame,
float16ToPqRgb,
} from '@hyperframes/engine';

// 启动启用了 WebGPU 的有头 Chrome
const { browser, page } = await launchHdrBrowser({ width: 1920, height: 1080 });

// 注入 WebGPU 回读运行时
const ok = await initHdrReadback(page, 1920, 1080);

// 对于每一帧:上传 float16 像素,回读 float16 RGBA
const { rgba16, bytesPerRow } = await uploadAndReadbackHdrFrame(page, float16Base64);

// 将线性 float16 转换为 PQ 编码的 16 位 RGB,适合通过管道传入 ffmpeg/x265
const pqRgb = float16ToPqRgb(rgba16, width, height, bytesPerRow);

⚠️ Warning

此路径需要带有 --enable-unsafe-webgpu 的有头 Chrome — WebGPU 在 chrome-headless-shell 中不可用。默认的 HDR 感知渲染管道(通过 FFmpeg 从源中提取 HDR 像素并在 Node 中合成)不使用此路径。仅在需要 CSS 动画驱动 HDR 像素输出的高级自定义管道中使用它。

window.__hf 协议

engine 通过 window.__hf 协议与浏览器页面通信。任何实现了此协议的页面都可以被 engine 捕获 — 你不仅限于 Hyperframes 合成。

// 页面必须在 window.__hf 上暴露此接口
interface HfProtocol {
duration: number;                  // 总时长(秒)
seek(time: number): void;         // 跳转到指定时间
media?: HfMediaElement[];         // 可选的媒体元素声明
}

interface HfMediaElement {
elementId: string;                 // DOM 元素 ID
src: string;                       // 媒体源 URL
startTime: number;                 // 时间轴上的开始时间
endTime: number;                   // 时间轴上的结束时间
mediaOffset?: number;              // 源中的播放偏移
volume?: number;                   // 音量 (0-1)
hasAudio?: boolean;                // 元素是否包含音频
}

核心概念

BeginFrame 渲染

传统屏幕录制以实际时间速度记录 — 如果你的系统负载较高,帧就会被丢弃。engine 使用 Chrome 的 HeadlessExperimental.beginFrame 来显式推进合成器,按需生成每一帧。这意味着:

  • 不丢帧 — 每一帧都被捕获
  • 无时间依赖 — 60 秒的视频不需要 60 秒来捕获
  • 像素完美输出 — 合成器产生与其显示完全相同的像素

有关这如何实现确定性输出的更多信息,请参阅确定性渲染

跳转约定

engine 依赖 Hyperframes 运行时的 renderSeek(time) 函数。调用时,renderSeek

  1. 暂停所有 GSAP 时间轴
  2. 将每个时间轴跳转到精确的时间戳
  3. 更新所有媒体元素(视频、音频)以匹配
  4. 根据 data-startdata-duration 挂载/卸载 clip

这个约定使得逐帧捕获成为可能 — 每一帧都是合成在该时间点的完整、独立的快照。

Chrome 要求

engine 需要 chrome-headless-shell,安装包时已包含。它使用固定的 Chrome 版本以确保跨环境一致的渲染。要获得完全确定性的输出(包括字体),请通过 producer 使用 Docker 模式。

相关包

  • Producer 封装了 engine,提供运行时注入、FFmpeg 编码和音频混音,实现完整的 MP4 输出。
  • Core 提供 engine 所依赖的类型、运行时和 Linter。
  • CLI 最简单的渲染方式 — 底层调用 producer(和 engine)。
  • Studio 在使用 engine 渲染之前构建合成的可视化编辑器。