← 返回首頁

HTML to Video 轉換難題解決方案:HyperFrames 透過 seek 機制實現確定性渲染

James Russo
James Russo
@Rames_Jusso
𝕏 (Twitter)🔥
AI 中文摘要Claude 生成

HTML to Video 轉換難題解決方案:HyperFrames 透過 seek 機制實現確定性渲染。

一年多前,團隊嘗試讓大型語言模型(LLM)生成 HTML 程式碼來製作影片,初期面臨巨量提示、反覆互動與手寫補丁等問題,即便加入 Agent 輔助,仍未達生產級水準。後續測試 Remotion 雖具備 React 元件與生產渲染工具,但框架限制了 Agent 創意;回歸純 HTML/CSS/JS 後,創意回歸,進而開發「HyperFrames」,解決 HTML 自由度與確定性 MP4 渲染的矛盾。

核心創新:seek 而非 play

HyperFrames 的每個組合僅暴露單一運行時介面 window.__hf,包含 duration: 10(秒)與 seek(timeSeconds) 函式,渲染器不呼叫 play(),而是依序呼叫 seek(0)、截圖、seek(1/30)、截圖,直至產生 10 秒 30fps 影片的 300 幀。

  • 時間不自動推進,避免依賴 requestAnimationFrame。
  • 瀏覽器僅維持固定幀,直至下一個請求。
  • 工作室預覽透過 iframe 與 postMessage 橋接實現 play/pause/scrub;無頭渲染則經 Puppeteer 與 CDP 呼叫相同 window.__hf,確保相同程式碼路徑與輸出。

動畫函式庫透過三方法 FrameAdapter 整合:

interface FrameAdapter {
  id: string;
  init?: (ctx) => Promise<void> | void;
  getDurationFrames: () => number;
  seekFrame: (frame: number) => Promise<void> | void;
}
  • GSAP 為預設,因其 timeline 已支援 pause() 與 totalTime(t, false),完美契合。
  • Lottie、CSS WAAPI、Three.js 時鐘皆易適配。
  • 不相容者如無控制器 CSS 關鍵影格、video 元件、多數 canvas 函式庫,需包裝適配器剝離時鐘,或離線渲染成幀後以圖像重播。

此抽象將工作室預覽與無頭渲染合併單一程式碼庫。

擷取挑戰:逐幀控制 Chrome

初期 Puppeteer 擷取僅四行程式碼:

await page.evaluate(t => window.__hf.seek(t), time);
await page.screenshot({ path: `frame_${i}.jpg` });

但遭遇四項問題:

  1. Page.captureScreenshot 競爭條件:截圖僅捕捉合成器就緒畫面,非「佈局完成、字型載入、GSAP tween 提交樣式、GPU 繪製結束」時刻,導致文字未渲染、SVG 填充預設、video 顯示 300x150 預設尺寸等錯誤幀,重試才正常。團隊花費大量時間寫入「幀是否落地」啟發式:輪詢 fonts.ready、等待計算樣式、比較像素雜湊。此法於 macOS/Windows 使用,但不適合大規模生產。

  2. HeadlessExperimental.beginFrame 提供原子控制:CDP 方法執行單一 layout→paint→composite→screenshot 週期:

await cdp.send("HeadlessExperimental.beginFrame", {
  frameTimeTicks,
  interval,
  screenshot: { format: "jpeg", quality: 80, optimizeForSpeed: true }
});
  • 回應含 hasDamage,確認視覺變化,無背景競爭條件。
  • 需特定 Chrome 組建 chrome-headless-shell 與旗標:
--deterministic-mode
--enable-begin-frame-control
--run-all-compositor-stages-before-draw
--disable-threaded-animation
--disable-threaded-scrolling
--disable-checker-imaging
--disable-image-animation-resync
--enable-surface-synchronization

這些旗標關閉非同步排程來源:執行緒合成器、滾動、漸進圖像解碼、圖像動畫重同步、vsync 表面計時。啟用後,合成器於主執行緒同步運行,performance.now() 由 frameTimeTicks 驅動,而非系統時鐘。僅 Linux 相容,macOS/Windows 回退啟發式。

  1. Chrome 事件迴圈停止推進:啟用 --enable-begin-frame-control 後,主執行緒不自動滴答,無 frame callbacks、setTimeout、microtask 排空。頁面載入時 document.fonts.ready 永遠懸掛。解決以暖身迴圈:載入中每 33ms 發 beginFrame(noDisplayUpdates: true),推進事件迴圈無產生幀:
while (warmupRunning) {
  await cdp.send("HeadlessExperimental.beginFrame", {
    frameTimeTicks: warmupFrameTime,
    interval: 33,
    noDisplayUpdates: true,
  });
  warmupFrameTime += 33;
  await sleep(33);
}

暖身結束後,從超出範圍幀時開始真實擷取,避免時間倒退。

  1. Puppeteer waitForFunction 失效:依賴 requestAnimationFrame 輪詢,beginFrame 模式下 rAF 不觸發,導致懸掛。改用自訂輪詢:
while (Date.now() < deadline) {
  const ready = await page.evaluate(
    "!!(window.__hf && typeof window.__hf.seek === 'function' && window.__hf.duration > 0)"
  );
  if (ready) break;
  await new Promise(r => setTimeout(r, 100));
}

最終擷取迴圈確定性無懈:

for (let i = 0; i < totalFrames; i++) {
  const time = quantizeTimeToFrame(i / fps, fps);
  await page.evaluate(t => window.__hf.seek(t), time);
  const { buffer } = await beginFrameCapture(page, options, frameTicks, interval);
  writeFileSync(`frame_${i}.jpg`, buffer);
}

無重試、無抖動幀。

影片內嵌問題:預先解碼取代播放

瀏覽器於渲染時播放

解決:擷取前 FFmpeg 預先將每個

<!-- before -->
<video data-start="2" data-duration="5" src="clip.mp4" />

<!-- at capture time, frame 60 of 150 -->
<video style="visibility: hidden" ... />
<img src="data:image/jpeg;base64,..." class="__render_frame__" />

注入 複製原元素計算樣式,確保 GSAP tween、CSS transform、opacity、object-fit 等無變:

img.style.position = computedStyle.position;
img.style.transform = computedStyle.transform;
img.style.opacity = computedStyle.opacity;
img.style.objectFit = computedStyle.objectFit;
// ...約十餘項

動畫函式庫視為無變元素,僅幀切換如翻書。相較 Remotion(Rust 合成器即時解碼 HTTP 供 )、Replit(mp4box.js 瀏覽器 demux + WebCodecs 解碼至 canvas),HyperFrames 最簡:FFmpeg 預解碼存盤 JPEG,犧牲彈性(blob URL、串流、動態 src 較難)換短管道。目前基底運作優異,後續可改善。

其他確定性陷阱

控制時間與渲染解決大部分,非全部:

  • 字型變異:Google Fonts @import url(fonts.googleapis.com/...) 受網路影響不穩。編譯 HTML 時改寫為本地 base64 嵌入 @fontsource 複本,消除網路延遲與不穩。
  • 時間量化:30fps 每幀 33.3333ms,若 seek(0.0333333) 與 seek(0.0333334) 差異,需量化:
function quantizeTimeToFrame(time, fps) {
  return Math.round(time * fps) / fps;
}

一行程式碼,避免像素差異。

  • 作者規則:組合程式碼禁 Date.now()、無種子 Math.random()、渲染時網路擷取。違規即破壞確定性,此為契約。

最終成果與生產考量

相同 window.__hf 運行時 bundle 於工作室預覽(iframe)與無頭渲染通用,渲染器驗證 bundle sha256 對照清單,強制預覽=渲染程式碼一致。

  • 長影片拆 N Chrome 程序並行,每 worker 渲染份額,FFmpeg 末端串接 MP4 片段。
  • 缺點:影片密集組合並行易超時(Chrome 解碼器不足),回退單 worker。

HyperFrames 建基既有:GSAP timeline 設計、Remotion HTML 影片格式。HyperFrames 採限縮創作模型與 seekable 運行契約,致敬先驅。

建置動機與開放

團隊建 AI 模型與 Agent,認為 Agent 將以此法製作影片,並透過影片溝通。目前 HyperFrames 專案已開源,開發者可透過 GitHub 存取相關程式碼與說明,歡迎貢獻 adapter 系統,期待上層產品應用。Repo:github.com/heygen-com/hyperframes。