99 lines
2.9 KiB
JavaScript
99 lines
2.9 KiB
JavaScript
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,
|
|
};
|