import path from 'path'; import fs from 'fs'; import { Plugin } from './plugin.js'; import { FFMpegUtils } from 'common-utils'; export class CropPlugin extends Plugin { constructor(name, options) { super(name, options); } mediaPathFlatten(mediaPath) { return path.join('public', path.basename(mediaPath)); } async applyPrerender(originalManuscript, jobId) { const transcript = originalManuscript.transcript || []; const targetWidth = +this.options.width || +this.options.targetWidth || 1080; const targetHeight = +this.options.height || +this.options.targetHeight || 1920; const targetAspect = targetWidth / targetHeight; 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}. Trying at flattened path: ${flattenedPath} failed.`); 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 srcAspect = srcW / srcH; let cropW = srcW, cropH = srcH, cropX = 0, cropY = 0; if (Math.abs(srcAspect - targetAspect) < 0.0001) { this.log(`Aspect ratio matches target for ${mediaPath}, skipping crop.`); continue; } if (srcAspect > targetAspect) { // source is too wide -> reduce width, keep height cropW = Math.round(targetAspect * srcH); cropX = Math.round((srcW - cropW) / 2); cropH = srcH; cropY = 0; } else { // source is too tall -> reduce height, keep width cropH = Math.round(srcW / targetAspect); cropY = Math.round((srcH - cropH) / 2); cropW = srcW; cropX = 0; } if (cropW === srcW && cropH === srcH) { continue; } const ext = path.extname(mediaPath); const base = path.basename(mediaPath, ext); const outPath = path.join(path.dirname(mediaPath), `${base}.cropped${ext}`); const cmd = `ffmpeg -i "${mediaPath}" -filter:v "crop=${cropW}:${cropH}:${cropX}:${cropY}" -c:v libx264 -preset veryfast -crf 23 -c:a copy "${outPath}" -y`; this.log(`Cropping ${mediaPath} -> ${outPath} crop=${cropW}x${cropH}+${cropX}+${cropY}`); await FFMpegUtils.execute(cmd); // keep original reference and replace path mediaObj._originalPath = mediaObj.path; mediaObj.path = outPath; } catch (err) { this.log(`Error cropping media ${mediaObj?.path}: ${err}`); } } } } }