Files
portifolio-evandrodevbr/scripts/encode-scrub-video.mjs
T

102 lines
3.2 KiB
JavaScript

/**
* Recodifica um vídeo para a vitrine animada, em que o tempo do vídeo muda a cada quadro da rolagem.
* Para a busca ser instantânea o vídeo precisa de um quadro-chave a cada 6 quadros e nenhum B-frame; o tamanho
* de cada arquivo fica em até 3 MB (sobe o CRF e, se preciso, reduz a imagem até caber).
*
* ffmpeg-static não é dependência do site: instale-o numa pasta temporária e rode o script a partir dela
* (as dependências são resolvidas a partir da pasta onde o comando foi executado):
*
* mkdir rec && cd rec && npm init -y && npm i ffmpeg-static
* node <repo>/scripts/encode-scrub-video.mjs --in desktop-original.mp4 --out <repo>/public/videos/<id>/scrub-desktop.mp4 --size 1120x700
* node <repo>/scripts/encode-scrub-video.mjs --in mobile-original.mp4 --out <repo>/public/videos/<id>/scrub-mobile.mp4 --size 390x844
*
* Use como entrada o vídeo de melhor qualidade que existir (a entrada de record-project-videos.mjs antes do corte
* para MP4/WebM), não os MP4 já comprimidos de public/videos.
*
* A capa de compartilhamento (og.jpg, 1200x630) sai do poster desktop:
* ffmpeg -i public/videos/<id>/desktop.webp -vf "scale=1200:750:flags=lanczos,crop=1200:630:0:0" -q:v 4 public/videos/<id>/og.jpg
*/
import { spawnSync } from "node:child_process";
import { statSync } from "node:fs";
import { createRequire } from "node:module";
import { join } from "node:path";
const require = createRequire(join(process.cwd(), "noop.js"));
const ffmpegPath = require("ffmpeg-static");
const args = Object.fromEntries(
process.argv.slice(2).reduce((acc, cur, i, all) => {
if (cur.startsWith("--")) acc.push([cur.slice(2), all[i + 1]]);
return acc;
}, []),
);
const { in: input, out: output } = args;
const [width, height] = (args.size ?? "1120x700").split("x").map(Number);
const MAX_BYTES = Number(args["max-mb"] ?? 3) * 1024 * 1024;
if (!input || !output || !width || !height) {
console.error(
"uso: --in <video> --out <video.mp4> --size 1120x700 [--max-mb 3]",
);
process.exit(1);
}
function encode(w, h, crf) {
const result = spawnSync(
ffmpegPath,
[
"-hide_banner",
"-loglevel",
"error",
"-y",
"-i",
input,
"-vf",
`scale=${w}:${h}:flags=lanczos,fps=24`,
"-an",
"-c:v",
"libx264",
"-preset",
"slow",
"-profile:v",
"main",
"-crf",
String(crf),
"-g",
"6",
"-keyint_min",
"6",
"-sc_threshold",
"0",
"-bf",
"0",
"-refs",
"1",
"-pix_fmt",
"yuv420p",
"-movflags",
"+faststart",
output,
],
{ encoding: "utf8" },
);
if (result.status !== 0) throw new Error(result.stderr);
return statSync(output).size;
}
for (const scale of [1, 0.9, 0.8]) {
const w = Math.round((width * scale) / 2) * 2;
const h = Math.round((height * scale) / 2) * 2;
for (const crf of [28, 31, 34, 37]) {
const bytes = encode(w, h, crf);
if (bytes <= MAX_BYTES) {
console.log(
`${output}: ${w}x${h}, crf ${crf}, ${Math.round(bytes / 1024)} KB`,
);
process.exit(0);
}
}
}
console.error("não coube no limite de tamanho, nem reduzindo a imagem");
process.exit(1);