// Make a clips directory unfurl well before clips-publish.sh ships it: // - remux every mp4 with its index up front (faststart). Previewers in // iMessage, Discord, Slack and Facebook read the head of the file to // decide whether to show a player, and give up on a tail-indexed one. // - cut a 1200x630 share card per clip — the poster's first-person // viewport, full-bleed, with the title set over it — for og:image. // Share sheets crop anything that isn't 1.91:1; X crops to 2:1. // - stamp each clip's video width/height into clips.json so the page // can emit og:video:width/height, without which Facebook, LinkedIn // and Discord refuse to embed a player. // // npx tsx deploy/clips-prep.mjs // // Needs ffmpeg + ffprobe and the Playwright chromium cache (see // verify-scenes.mjs). Idempotent: run it again after re-recording. import { execFileSync } from "node:child_process"; import { existsSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { chromium } from "playwright-core"; const dir = process.argv[2]; if (!dir || !existsSync(join(dir, "clips.json"))) { console.error("usage: clips-prep.mjs (a directory holding clips.json)"); process.exit(1); } // The workshop poster is a screenshot of the replay stage; the wizard's // viewport sits at this rectangle within it — a 16:9 window. const VIEWPORT = { x: 18, y: 51, w: 700, h: 393 }; const CARD = { width: 1200, height: 630 }; function chromiumExe() { if (process.env.WIZWAR_CHROME) return process.env.WIZWAR_CHROME; const cache = join(process.env.HOME ?? "", "Library", "Caches", "ms-playwright"); const builds = existsSync(cache) ? readdirSync(cache).filter((d) => /^chromium-\d+$/.test(d)).sort() : []; for (const build of builds.reverse()) { const exe = join(cache, build, "chrome-mac-arm64", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing"); if (existsSync(exe)) return exe; } throw new Error("no Playwright chromium found — run: npx playwright-core install chromium (or set WIZWAR_CHROME)"); } const ff = (...args) => execFileSync("ffmpeg", ["-loglevel", "error", "-y", ...args]); function faststart(file) { const tmp = `${file}.tmp.mp4`; ff("-i", file, "-c", "copy", "-movflags", "+faststart", tmp); renameSync(tmp, file); } function videoSize(file) { const [width, height] = execFileSync("ffprobe", ["-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "csv=p=0", file]).toString().trim().split(",").map(Number); return { width, height }; } const esc = (s) => s.replace(/&/g, "&").replace(/
${esc(title)}
WIZ-WAR  ·  a clip from the labyrinth
`; } const clipsFile = join(dir, "clips.json"); const clips = JSON.parse(readFileSync(clipsFile, "utf8")); const browser = await chromium.launch({ executablePath: chromiumExe() }); const page = await browser.newPage({ viewport: CARD, deviceScaleFactor: 1 }); try { for (const clip of clips) { for (const take of ["fpv", "board"]) faststart(join(dir, `${clip.name}-${take}.mp4`)); Object.assign(clip, videoSize(join(dir, `${clip.name}-fpv.mp4`))); const crop = join(dir, `.${clip.name}-viewport.png`); ff("-i", join(dir, `${clip.name}.jpg`), "-vf", `crop=${VIEWPORT.w}:${VIEWPORT.h}:${VIEWPORT.x}:${VIEWPORT.y}`, crop); const dataUrl = `data:image/png;base64,${readFileSync(crop).toString("base64")}`; unlinkSync(crop); await page.setContent(cardHtml(clip.title, dataUrl)); await page.evaluate(() => document.fonts.ready); await page.screenshot({ path: join(dir, `${clip.name}-card.jpg`), type: "jpeg", quality: 88 }); console.log(`${clip.name}: ${clip.width}x${clip.height}, faststart, card`); } } finally { await browser.close(); } writeFileSync(clipsFile, JSON.stringify(clips, null, 2) + "\n"); console.log(`prepped ${clips.length} clips in ${dir}`);