mirror of
https://github.com/hanzoai/desktop.git
synced 2026-10-30 14:58:42 +00:00
95 lines
3.7 KiB
JavaScript
95 lines
3.7 KiB
JavaScript
// Playwright's takes -> the two masters the web hero consumes.
|
|
//
|
|
// Playwright writes VP8 webm, which Safari does not play and `@hanzo/frame`
|
|
// does not ask for. This is the one conversion step, kept out of the spec so a
|
|
// take can be re-encoded without re-recording it.
|
|
//
|
|
// pnpm film && pnpm film:masters
|
|
//
|
|
// This directory is scratch and stays gitignored. The take you CHOOSE gets
|
|
// copied to `assets/hero.mp4` (+ `-portrait`) and committed — at ~190 KB for a
|
|
// mostly-static dark UI that is cheap, and it keeps the README's demo pinned to
|
|
// the same commit as the app it shows. Re-encode rather than re-download: the
|
|
// videos this replaced were 6 MB each and somebody else's.
|
|
import { execFileSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
const TAKES = path.join(HERE, '.film');
|
|
const OUT = path.join(HERE, '.film', 'masters');
|
|
|
|
/** The take a project recorded: one webm, somewhere under the output dir. */
|
|
const takeFor = (project) => {
|
|
const hit = [];
|
|
const walk = (dir) => {
|
|
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const p = path.join(dir, e.name);
|
|
if (e.isDirectory()) walk(p);
|
|
else if (e.name.endsWith('.webm') && p.includes(project)) hit.push(p);
|
|
}
|
|
};
|
|
if (fs.existsSync(TAKES)) walk(TAKES);
|
|
// Newest wins: re-running `pnpm film` leaves the previous take on disk.
|
|
return hit.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0];
|
|
};
|
|
|
|
/**
|
|
* The first ffmpeg on PATH that actually RUNS on this machine.
|
|
*
|
|
* Not paranoia: this box had an x86-64 ffmpeg parked in ~/.local/bin ahead of
|
|
* the working arm64 one. `execvp` gets ENOEXEC on it and hands the ELF to
|
|
* /bin/sh, which reports a SYNTAX ERROR — so the failure names a shell and a
|
|
* paren and says nothing about architecture. An interactive shell hides it by
|
|
* caching the good path, so it reproduces only from a script.
|
|
*/
|
|
const ffmpeg = (() => {
|
|
const seen = new Set();
|
|
for (const dir of (process.env.PATH ?? '').split(path.delimiter)) {
|
|
const cand = path.join(dir, 'ffmpeg');
|
|
if (!dir || seen.has(cand)) continue;
|
|
seen.add(cand);
|
|
try {
|
|
execFileSync(cand, ['-version'], { stdio: 'ignore' });
|
|
return cand;
|
|
} catch {
|
|
/* wrong arch, not executable, or absent — keep walking */
|
|
}
|
|
}
|
|
console.error(' no runnable ffmpeg on PATH');
|
|
process.exit(1);
|
|
})();
|
|
|
|
const encode = (src, dest, w, h) => {
|
|
// -an: a hero film is muted by policy — it autoplays, and an autoplaying
|
|
// sound is a reason to close the tab. faststart puts the moov atom first so
|
|
// it starts on the first bytes instead of the last.
|
|
execFileSync(ffmpeg, [
|
|
'-y', '-loglevel', 'error', '-i', src,
|
|
'-vf', `scale=${w}:${h}:force_original_aspect_ratio=increase,crop=${w}:${h}`,
|
|
'-c:v', 'libx264', '-preset', 'slow', '-crf', '23',
|
|
'-pix_fmt', 'yuv420p', '-movflags', '+faststart', '-an', dest,
|
|
], { stdio: 'inherit' });
|
|
};
|
|
|
|
fs.mkdirSync(OUT, { recursive: true });
|
|
let wrote = 0;
|
|
for (const [project, name, w, h] of [
|
|
['film-landscape', 'hero.mp4', 1920, 1080],
|
|
['film-portrait', 'hero-portrait.mp4', 1080, 1920],
|
|
]) {
|
|
const take = takeFor(project);
|
|
if (!take) {
|
|
console.error(` ${project}: no take on disk — run \`pnpm film\` first`);
|
|
continue;
|
|
}
|
|
const dest = path.join(OUT, name);
|
|
encode(take, dest, w, h);
|
|
console.log(` ${name} ${(fs.statSync(dest).size / 1e6).toFixed(1)} MB <- ${path.basename(take)}`);
|
|
wrote++;
|
|
}
|
|
// Two masters or none: `@hanzo/frame` picks by orientation, so shipping one
|
|
// leaves every caller on the other orientation showing the middle of a crop.
|
|
if (wrote !== 2) process.exit(1);
|