const fs = require("fs-extra"); const path = require("path"); const { exec } = require("child_process"); const util = require("util"); const PostModel = require("../models/PostModel"); const execPromise = util.promisify(exec); async function runFfmpeg(command) { try { await execPromise(command); return true; } catch (error) { console.error("[postVideoProcessing]", error.message); return false; } } /** faststart بدون re-encode — شروع پخش سریع‌تر */ async function optimizeVideoFaststart(filePath) { if (!(await fs.pathExists(filePath))) return; const dir = path.dirname(filePath); const base = path.basename(filePath); const tempPath = path.join(dir, `faststart-${Date.now()}-${base}`); const ok = await runFfmpeg( `ffmpeg -i "${filePath}" -c copy -movflags +faststart "${tempPath}" -y` ); if (!ok || !(await fs.pathExists(tempPath))) return; try { await fs.rename(tempPath, filePath); console.log(`[postVideoProcessing] faststart applied: ${base}`); } catch (error) { await fs.remove(tempPath).catch(() => {}); console.error("[postVideoProcessing] faststart rename failed:", error.message); } } /** thumbnail برای poster در گرید و ریلز */ async function generateVideoThumbnail(filePath, postId) { if (!(await fs.pathExists(filePath))) return null; const thumbDir = path.join(path.dirname(filePath), "..", "thumbnails"); await fs.ensureDir(thumbDir); const thumbName = `${postId}.jpg`; const thumbPath = path.join(thumbDir, thumbName); const ok = await runFfmpeg( `ffmpeg -ss 00:00:00.5 -i "${filePath}" -vframes 1 -q:v 3 -y "${thumbPath}"` ); if (!ok || !(await fs.pathExists(thumbPath))) return null; // مسیر نسبی قابل سرو از /storage/posts/thumbnails return `/posts/thumbnails/${thumbName}`; } async function processPostVideoInBackground(filePath, postId) { if (!filePath || !postId) return; const existing = await PostModel.findById(postId).select('files post_images').lean(); const hasCustomCover = existing?.files?.some((file) => file.type === 'image') || (existing?.post_images?.length ?? 0) > 0; if (hasCustomCover) { console.log(`[postVideoProcessing] skip thumbnail (custom cover) post=${postId}`); await optimizeVideoFaststart(filePath); return; } console.log(`[postVideoProcessing] start post=${postId}`); await optimizeVideoFaststart(filePath); const relativeThumb = await generateVideoThumbnail(filePath, postId); if (!relativeThumb) return; try { await PostModel.findByIdAndUpdate(postId, { $set: { post_images: [relativeThumb], }, }); console.log(`[postVideoProcessing] thumbnail saved post=${postId} path=${relativeThumb}`); } catch (error) { console.error("[postVideoProcessing] db update failed:", error.message); } } module.exports = { processPostVideoInBackground, optimizeVideoFaststart, generateVideoThumbnail, };