Files
yt2press/compress-videos.js
T
2026-06-25 17:56:03 +07:00

153 lines
3.6 KiB
JavaScript

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}.`);