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 { 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 { 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 } } }