From 87a3c1b641432b9474c38856273caafac34f3f6e Mon Sep 17 00:00:00 2001 From: polv Date: Thu, 25 Jun 2026 17:56:03 +0700 Subject: [PATCH] video compress to webm --- compress-all.js => compress-images.js | 0 compress-videos.js | 152 ++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) rename compress-all.js => compress-images.js (100%) create mode 100644 compress-videos.js diff --git a/compress-all.js b/compress-images.js similarity index 100% rename from compress-all.js rename to compress-images.js diff --git a/compress-videos.js b/compress-videos.js new file mode 100644 index 00000000..c81dc6ce --- /dev/null +++ b/compress-videos.js @@ -0,0 +1,152 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { spawn } from "node:child_process"; + +const INPUT_DIR = "docs/public"; // change if needed +const OUTPUT_DIR = "docs/public/videos"; // change if needed + +// Pick one: VP9 is more widely supported, AV1 is smaller but slower. +const CODEC = "vp9"; // "vp9" or "av1" + +const MAX_PARALLEL = 2; // limit concurrency +const AUDIO_BITRATE = "128k"; + +// Target-ish size approach: +// - For VP9: use -b:v (bitrate cap) for predictable sizes. +// - For AV1: use -b:v too (still more variable, but workable). +const VIDEO_BITRATE = "1.0M"; // increase/decrease based on your desired size/quality + +function isVideo(file) { + return /\.(mp4|mkv|mov|webm|avi|m4v)$/i.test(file); +} + +async function walk(dir) { + const entries = await fs.readdir(dir, { withFileTypes: true }); + const out = []; + for (const e of entries) { + const p = path.join(dir, e.name); + if (e.isDirectory()) { + } else if (isVideo(e.name)) out.push(p); + } + return out; +} + +function runFFmpeg(args) { + return new Promise((resolve, reject) => { + const p = spawn("ffmpeg", args, { stdio: "inherit" }); + p.on("close", (code) => + code === 0 ? resolve() : reject(new Error(`ffmpeg exited ${code}`)), + ); + p.on("error", reject); + }); +} + +async function processOne(inPath) { + const rel = path.relative(INPUT_DIR, inPath); + const outPath = path + .join(OUTPUT_DIR, rel) + .replace(path.extname(rel), ".webm"); + try { + await fs.access(outPath); + console.log(`Already existed: ${outPath}`); + return; + } catch (e) {} + + await fs.mkdir(path.dirname(outPath), { recursive: true }); + + if (/\.webm$/i.test(inPath)) { + console.log(`Copying: ${inPath} -> ${outPath}`); + await fs.copyFile(inPath, outPath); + return; + } + + const filter = [ + // optional: keep within reasonable resolution (prevents huge re-encodes) + // comment out if you don't want scaling + "scale='min(1280,iw)':-2", + ].join(","); + + const common = [ + "-loglevel", + "error", // or "warning" + "-y", + "-i", + inPath, + "-vf", + filter, + "-c:a", + "libopus", + "-b:a", + AUDIO_BITRATE, + "-movflags", + "+faststart", + ]; + + let videoArgs; + if (CODEC === "vp9") { + // VP9: good size, moderate speed + // -b:v is the easiest "size control" + videoArgs = [ + "-c:v", + "libvpx-vp9", + "-b:v", + VIDEO_BITRATE, + "-preset", + "good", + "-row-mt", + "1", + "-threads", + "0", + ]; + } else if (CODEC === "av1") { + // AV1 (libaom-av1): usually smaller, slower + // If AV1 is too slow, switch CODEC to "vp9". + videoArgs = [ + "-c:v", + "libaom-av1", + "-b:v", + VIDEO_BITRATE, + "-cpu-used", + "6", + "-threads", + "0", + "-row-mt", + "1", + ]; + } else { + throw new Error("CODEC must be vp9 or av1"); + } + + // Ensure reproducible-ish output and speed + const args = [...common, ...videoArgs, outPath]; + + console.log(`Compressing: ${inPath} -> ${outPath}`); + await runFFmpeg(args); + console.log(`Finished: ${outPath}`); +} + +async function runWithLimit(items, limit, fn) { + const queue = items.slice(); + const workers = Array.from( + { length: Math.min(limit, items.length) }, + async () => { + while (queue.length) { + const item = queue.shift(); + if (!item) break; + await fn(item); + } + }, + ); + await Promise.all(workers); +} + +const files = await walk(INPUT_DIR); +if (!files.length) { + console.log("No videos found."); + process.exit(0); +} + +await fs.mkdir(OUTPUT_DIR, { recursive: true }); + +await runWithLimit(files, MAX_PARALLEL, processOne); +console.log(`Done. Wrote ${files.length} file(s) to ${OUTPUT_DIR}.`);