video compress to webm
This commit is contained in:
1 parent
d72377b0bb
commit
87a3c1b641
2 files changed
+152
No files matched your search
@@ -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}.`);
|
||||
Reference in new issue
Block a user