import path from 'path'; import fs from 'fs'; import { Plugin } from './plugin.js'; import { FFMpegUtils } from 'common-utils'; export class OverlayPlugin extends Plugin { constructor(name, options) { super(name, options); } async applyPrerender(originalManuscript, jobId) { const transcript = originalManuscript.transcript || []; const overlayOpt = this.options.overlay; if (!overlayOpt) { this.log('No overlay option provided, skipping overlay plugin.'); return; } let overlayPath = null; // if option looks like a file path if (/\.(mp4|webm|png)$/i.test(overlayOpt)) { const flattened = this.mediaPathFlatten(overlayOpt); if (fs.existsSync(flattened)) overlayPath = flattened; else if (fs.existsSync(overlayOpt)) overlayPath = overlayOpt; else { this.log(`Overlay path does not exist: ${overlayOpt}`); return; } } else { // look inside public/assets/effects for mp4, webm, or png const effectsDir = path.join('public', 'assets', 'effects'); const mp4Path = path.join(effectsDir, `${overlayOpt}.mp4`); const webmPath = path.join(effectsDir, `${overlayOpt}.webm`); const pngPath = path.join(effectsDir, `${overlayOpt}.png`); if (fs.existsSync(mp4Path)) overlayPath = mp4Path; else if (fs.existsSync(webmPath)) overlayPath = webmPath; else if (fs.existsSync(pngPath)) overlayPath = pngPath; else { this.log(`No overlay found for '${overlayOpt}' in ${effectsDir}`); return; } } const overlayExt = path.extname(overlayPath).toLowerCase(); this.log(`Using overlay: ${overlayPath}`); for (let item of transcript) { if (!item.mediaAbsPaths || !item.mediaAbsPaths.length) continue; for (let mediaObj of item.mediaAbsPaths) { try { let mediaPath = mediaObj.path; if (!mediaPath || !fs.existsSync(mediaPath)) { const flattenedPath = this.mediaPathFlatten(mediaPath); if (fs.existsSync(flattenedPath)) { mediaObj.path = flattenedPath; mediaPath = flattenedPath; this.log(`Using flattened media path: ${flattenedPath}`); } else { this.log(`Media path does not exist: ${mediaPath}. Tried flattened: ${flattenedPath}`); continue; } } const meta = await FFMpegUtils.getMediaMetadata(mediaPath); if (!meta || !meta.video || !meta.video.width || !meta.video.height) { this.log(`No video stream found for ${mediaPath}`); continue; } const srcW = parseInt(meta.video.width); const srcH = parseInt(meta.video.height); const srcDuration = parseFloat(meta.duration || meta.format?.duration || 0); const durationArg = srcDuration > 0 ? `-t ${srcDuration}` : ''; const ext = path.extname(mediaPath); const base = path.basename(mediaPath, ext); const outPath = path.join(path.dirname(mediaPath), `${base}.overlay${ext}`); let cmd = null; if (overlayExt === '.mp4') { // mp4 uses green-screen (chroma key) -> remove green then overlay // chromakey params: color (green), similarity, blend cmd = `ffmpeg -i "${mediaPath}" -stream_loop -1 -i "${overlayPath}" -filter_complex "[1:v]scale=${srcW}:${srcH},chromakey=0x00FF00:0.3:0.1[ovr];[0:v][ovr]overlay=0:0:format=auto,format=yuv420p" -c:v libx264 -preset veryfast -crf 23 -c:a copy ${durationArg} "${outPath}" -y`; } else if (overlayExt === '.webm') { // webm assumed to have alpha (transparent video) and should loop to match source duration cmd = `ffmpeg -i "${mediaPath}" -stream_loop -1 -i "${overlayPath}" -filter_complex "[1:v]scale=${srcW}:${srcH}[ovr];[0:v][ovr]overlay=0:0:format=auto,format=yuv420p" -c:v libx264 -preset veryfast -crf 23 -c:a copy ${durationArg} "${outPath}" -y`; } else if (overlayExt === '.png') { // png is static image overlay, loop to cover full video duration cmd = `ffmpeg -i "${mediaPath}" -loop 1 -i "${overlayPath}" -filter_complex "[1:v]scale=${srcW}:${srcH}[ovr];[0:v][ovr]overlay=0:0:format=auto,format=yuv420p" -c:v libx264 -preset veryfast -crf 23 -c:a copy ${durationArg} "${outPath}" -y`; } else { this.log(`Unsupported overlay extension: ${overlayExt}`); continue; } this.log(`Applying overlay ${overlayPath} -> ${outPath} on ${mediaPath}`); await FFMpegUtils.execute(cmd); mediaObj._originalPath = mediaObj.path; mediaObj.path = outPath; } catch (err) { this.log(`Error applying overlay to ${mediaObj?.path}: ${err}`); } } } } }