// Render doodle.html to mp4 via headless Chromium frame-by-frame screenshots + ffmpeg.
// Usage: node scripts/render_mp4.mjs <doodle.html> <timing.json> <voice.mp3> <out.mp4>
// 1920x1080 输出；单帧三试 + 整页崩溃自动重建续渲（避免长片 5000+ 帧连续截图的 renderer crash）。
import { chromium } from "playwright";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";

const CHROME = "C:/Users/ALIENWARE/AppData/Local/ms-playwright/chromium-1223/chrome-win64/chrome.exe";
const FPS = 30;

const [, , htmlArg, timingArg, voiceArg, outArg] = process.argv;
if (!outArg) {
  console.error("Usage: node scripts/render_mp4.mjs <doodle.html> <timing.json> <voice.mp3> <out.mp4>");
  process.exit(1);
}
const htmlPath = path.resolve(htmlArg);
const voicePath = path.resolve(voiceArg);
const outPath = path.resolve(outArg);
const framesDir = path.join(path.dirname(outPath), "frames");
fs.mkdirSync(framesDir, { recursive: true });

let browser, page;
async function fresh() {
  if (browser) { try { await browser.close(); } catch {} }
  browser = await chromium.launch({ executablePath: CHROME, headless: true });
  page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
  page.on("pageerror", e => console.log("[pageerror]", (e.message || "").slice(0, 200)));
  await page.goto(pathToFileURL(htmlPath).href, { waitUntil: "load" });
}
await fresh();

const DURATION = await page.evaluate(() => window.__buildOnly());
console.log(`DURATION=${DURATION}s, fps=${FPS}`);
const total = Math.ceil(DURATION * FPS);
console.log(`rendering ${total} frames -> ${framesDir}`);

const t0 = Date.now();
let crashes = 0;
for (let i = 0; i < total; i++) {
  const fp = path.join(framesDir, "f" + String(i).padStart(5, "0") + ".png");
  let ok = false;
  for (let attempt = 0; attempt < 3 && !ok; attempt++) {
    try {
      await page.evaluate(t => window.__seek(t), i / FPS);
      await page.screenshot({ path: fp });
      ok = true;
    } catch (e) {
      crashes++;
      console.log(`frame ${i} attempt ${attempt} FAIL: ${(e.message || "").slice(0, 120)} — rebuilding`);
      try { await fresh(); await page.evaluate(() => window.__buildOnly()); } catch (e2) { console.log("rebuild failed:", (e2.message || "").slice(0, 120)); }
    }
  }
  if (!ok && i > 0) {
    fs.copyFileSync(path.join(framesDir, "f" + String(i - 1).padStart(5, "0") + ".png"), fp);
    console.log(`frame ${i} fallback to prev frame`);
  }
  if (i % 60 === 0) console.log(`  frame ${i}/${total} (${((Date.now() - t0) / 1000).toFixed(1)}s, ${crashes} crashes)`);
}
try { await browser.close(); } catch {}
console.log(`frames done in ${((Date.now() - t0) / 1000).toFixed(1)}s, ${crashes} crashes`);

const args = [
  "-y", "-framerate", String(FPS), "-i", path.join(framesDir, "f%05d.png"),
  "-i", voicePath, "-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", "-shortest", outPath,
];
console.log("ffmpeg " + args.join(" "));
const r = spawnSync("ffmpeg", args, { stdio: "inherit" });
if (r.status !== 0) { console.error("ffmpeg failed with status", r.status); process.exit(r.status ?? 1); }
console.log("OK wrote " + outPath);
