|
|
|
|
const express = require("express");
|
|
|
|
|
const multer = require("multer");
|
|
|
|
|
const { spawn, execSync } = require("child_process");
|
|
|
|
|
const fs = require("fs");
|
|
|
|
|
|
|
|
|
|
const app = express();
|
|
|
|
|
const upload = multer({ dest: "uploads/" });
|
|
|
|
|
|
|
|
|
|
app.post("/convert", upload.fields([{ name: "audio" }, { name: "image" }]), async (req, res) => {
|
|
|
|
|
if (!req.files?.audio || !req.files?.image) {
|
|
|
|
|
return res.status(400).send("Missing audio or image file.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const audioPath = req.files.audio[0].path;
|
|
|
|
|
const imagePath = req.files.image[0].path;
|
|
|
|
|
const output = `output-${Date.now()}.mp4`;
|
|
|
|
|
|
|
|
|
|
let durationSec = 0;
|
|
|
|
|
try {
|
|
|
|
|
const durationStr = execSync(
|
|
|
|
|
`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 ${audioPath}`
|
|
|
|
|
).toString().trim();
|
|
|
|
|
durationSec = parseFloat(durationStr);
|
|
|
|
|
console.log(`🎧 Audio duration: ${durationSec.toFixed(2)} sec`);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("❌ Could not determine audio duration");
|
|
|
|
|
return res.status(500).send("Failed to analyze audio.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const args = [
|
|
|
|
|
"-loop", "1",
|
|
|
|
|
"-i", imagePath,
|
|
|
|
|
"-i", audioPath,
|
|
|
|
|
"-c:v", "libx264",
|
|
|
|
|
"-preset", "ultrafast",
|
|
|
|
|
"-tune", "stillimage",
|
|
|
|
|
"-crf", "23",
|
|
|
|
|
"-c:a", "aac",
|
|
|
|
|
"-shortest",
|
|
|
|
|
"-pix_fmt", "yuv420p",
|
|
|
|
|
"-movflags", "+faststart",
|
|
|
|
|
output
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
console.log(`🚀 Starting conversion to ${output}`);
|
|
|
|
|
const ffmpeg = spawn("ffmpeg", args);
|
|
|
|
|
|
|
|
|
|
ffmpeg.stderr.on("data", (data) => {
|
|
|
|
|
const line = data.toString().trim();
|
|
|
|
|
const timeMatch = line.match(/time=(\d+):(\d+):([\d.]+)/);
|
|
|
|
|
|
|
|
|
|
if (timeMatch) {
|
|
|
|
|
const [, h, m, s] = timeMatch;
|
|
|
|
|
const timeSec = parseInt(h) * 3600 + parseInt(m) * 60 + parseFloat(s);
|
|
|
|
|
const percent = Math.min(100, ((timeSec / durationSec) * 100).toFixed(1));
|
|
|
|
|
console.log(`${line} | 📊 ${percent}%`);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(line);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ffmpeg.on("exit", (code) => {
|
|
|
|
|
if (code !== 0) {
|
|
|
|
|
console.error(`❌ FFmpeg exited with code ${code}`);
|
|
|
|
|
return res.status(500).send("Conversion failed");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log("✅ Conversion complete. Sending file...");
|
|
|
|
|
res.setHeader('Content-Type', 'video/mp4');
|
|
|
|
|
res.setHeader('Content-Disposition', `attachment; filename="${output}"`);
|
|
|
|
|
|
|
|
|
|
const stream = fs.createReadStream(output);
|
|
|
|
|
|
|
|
|
|
stream.pipe(res);
|
|
|
|
|
|
|
|
|
|
stream.on('close', () => {
|
|
|
|
|
fs.unlinkSync(audioPath);
|
|
|
|
|
fs.unlinkSync(imagePath);
|
|
|
|
|
fs.unlinkSync(output);
|
|
|
|
|
console.log("🧹 Cleaned up temp files");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
stream.on('error', (err) => {
|
|
|
|
|
console.error("❌ Stream error:", err);
|
|
|
|
|
res.status(500).send("Error streaming file.");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.get("/", (req, res) => {
|
|
|
|
|
res.send("🎧 FileConvert API is running.");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.listen(3000, () => console.log("✅ Listening on http://0.0.0.0:3000"));
|