101 lines
2.6 KiB
TypeScript
101 lines
2.6 KiB
TypeScript
import { spawn } from "child_process";
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
import { getResolution } from "../constants";
|
|
import { getWatermarkPath } from "../storage";
|
|
|
|
export async function encodeVideo(options: {
|
|
imagePath: string;
|
|
audioPath: string;
|
|
outputPath: string;
|
|
resolution: string;
|
|
includeWatermark: boolean;
|
|
}): Promise<void> {
|
|
const res = getResolution(options.resolution);
|
|
if (!res) throw new Error(`Invalid resolution: ${options.resolution}`);
|
|
|
|
await fs.mkdir(path.dirname(options.outputPath), { recursive: true });
|
|
|
|
const scaleFilter = `scale=${res.width}:${res.height}:force_original_aspect_ratio=decrease,pad=${res.width}:${res.height}:(ow-iw)/2:(oh-ih)/2:black`;
|
|
|
|
const watermarkPath = getWatermarkPath();
|
|
let watermarkExists = false;
|
|
try {
|
|
await fs.access(watermarkPath);
|
|
watermarkExists = true;
|
|
} catch {
|
|
watermarkExists = false;
|
|
}
|
|
|
|
const useWatermark = options.includeWatermark && watermarkExists;
|
|
|
|
const args = ["-y", "-loop", "1", "-i", options.imagePath, "-i", options.audioPath];
|
|
|
|
if (useWatermark) {
|
|
args.push("-i", watermarkPath);
|
|
args.push(
|
|
"-filter_complex",
|
|
`[0:v]${scaleFilter}[scaled];[2:v]scale=iw*0.15:-1[wm];[scaled][wm]overlay=W-w-20:H-h-20[vout]`,
|
|
"-map",
|
|
"[vout]",
|
|
"-map",
|
|
"1:a",
|
|
);
|
|
} else if (options.includeWatermark && !watermarkExists) {
|
|
args.push(
|
|
"-filter_complex",
|
|
`[0:v]${scaleFilter},drawtext=text='s2yt':fontsize=24:fontcolor=white@0.7:x=w-tw-20:y=h-th-20[vout]`,
|
|
"-map",
|
|
"[vout]",
|
|
"-map",
|
|
"1:a",
|
|
);
|
|
} else {
|
|
args.push("-vf", scaleFilter);
|
|
}
|
|
|
|
args.push(
|
|
"-c:v",
|
|
"libx264",
|
|
"-tune",
|
|
"stillimage",
|
|
"-c:a",
|
|
"aac",
|
|
"-b:a",
|
|
"192k",
|
|
"-pix_fmt",
|
|
"yuv420p",
|
|
"-shortest",
|
|
options.outputPath,
|
|
);
|
|
|
|
await runFfmpeg(args);
|
|
}
|
|
|
|
function runFfmpeg(args: string[]): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
const proc = spawn("ffmpeg", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
let stderr = "";
|
|
proc.stderr.on("data", (chunk) => {
|
|
stderr += chunk.toString();
|
|
});
|
|
proc.on("close", (code) => {
|
|
if (code === 0) resolve();
|
|
else reject(new Error(`FFmpeg failed (code ${code}): ${stderr.slice(-500)}`));
|
|
});
|
|
proc.on("error", (err) => {
|
|
reject(new Error(`FFmpeg not found or failed to start: ${err.message}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function cleanupFiles(paths: string[]) {
|
|
for (const p of paths) {
|
|
try {
|
|
await fs.unlink(p);
|
|
} catch {
|
|
// ignore missing files
|
|
}
|
|
}
|
|
}
|