124 lines
3.9 KiB
JavaScript
124 lines
3.9 KiB
JavaScript
// functions/mergeVideoAndAudio.js (ou dans index.js)
|
|
|
|
const { onCall, HttpsError } = require("firebase-functions/v2/https");
|
|
const admin = require("firebase-admin");
|
|
const logger = require("firebase-functions/logger");
|
|
const axios = require("axios");
|
|
const fs = require("node:fs/promises");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
const crypto = require("node:crypto");
|
|
const ffmpeg = require("fluent-ffmpeg");
|
|
const ffmpegInstaller = require("@ffmpeg-installer/ffmpeg");
|
|
const { Buffer } = require("node:buffer");
|
|
if (!admin.apps.length) admin.initializeApp();
|
|
ffmpeg.setFfmpegPath(ffmpegInstaller.path);
|
|
|
|
async function downloadToFile(url, destPath) {
|
|
if (!/^https?:\/\//i.test(url || "")) {
|
|
throw new HttpsError("invalid-argument", `URL non supportée: ${url}`);
|
|
}
|
|
const res = await axios.get(url, { responseType: "arraybuffer" });
|
|
await fs.writeFile(destPath, Buffer.from(res.data));
|
|
return res.headers?.["content-type"] || "";
|
|
}
|
|
|
|
async function muxAudioIntoVideo({ videoPath, audioPath, outPath }) {
|
|
return new Promise((resolve, reject) => {
|
|
ffmpeg()
|
|
.input(videoPath) // 0:v
|
|
.input(audioPath) // 1:a
|
|
.outputOptions([
|
|
"-map",
|
|
"0:v:0", // garder la 1re piste vidéo de l'entrée 0
|
|
"-map",
|
|
"1:a:0", // prendre la 1re piste audio de l'entrée 1
|
|
"-c:v",
|
|
"copy", // ne pas réencoder la vidéo
|
|
"-c:a",
|
|
"aac",
|
|
"-b:a",
|
|
"192k",
|
|
"-movflags",
|
|
"+faststart",
|
|
"-shortest", // couper à la plus courte des 2 sources
|
|
])
|
|
.on("error", reject)
|
|
.on("end", resolve)
|
|
.save(outPath);
|
|
});
|
|
}
|
|
|
|
exports.mergeVideoAndAudio = onCall(
|
|
{ timeoutSeconds: 540, memory: "1GiB" },
|
|
async ({ data = {}, auth }) => {
|
|
const uid = auth?.uid;
|
|
if (!uid)
|
|
throw new HttpsError("unauthenticated", "Authentification requise");
|
|
|
|
const { videoUrl, audioUrl, storagePath } = data || {};
|
|
|
|
if (!videoUrl || !audioUrl || !storagePath) {
|
|
throw new HttpsError(
|
|
"invalid-argument",
|
|
"Requis: { videoUrl, audioUrl, storagePath }"
|
|
);
|
|
}
|
|
|
|
// sécurité simple: forcer dans le dossier de l'utilisateur si tu veux
|
|
const expectedPrefix = `users/${uid}/`;
|
|
if (!storagePath.startsWith(expectedPrefix)) {
|
|
throw new HttpsError(
|
|
"permission-denied",
|
|
`storagePath doit commencer par ${expectedPrefix}`
|
|
);
|
|
}
|
|
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "merge-"));
|
|
const videoPath = path.join(tmpDir, "video.mp4");
|
|
const audioPath = path.join(tmpDir, "audio.mp3");
|
|
const outPath = path.join(tmpDir, "output.mp4");
|
|
|
|
try {
|
|
logger.info("[merge] téléchargement des sources", { videoUrl, audioUrl });
|
|
|
|
await downloadToFile(videoUrl, videoPath);
|
|
await downloadToFile(audioUrl, audioPath);
|
|
|
|
logger.info("[merge] fusion ffmpeg (mux)");
|
|
await muxAudioIntoVideo({ videoPath, audioPath, outPath });
|
|
|
|
const bucket = admin.storage().bucket();
|
|
const downloadToken = crypto.randomUUID();
|
|
|
|
await bucket.upload(outPath, {
|
|
destination: storagePath,
|
|
metadata: {
|
|
contentType: "video/mp4",
|
|
cacheControl: "public,max-age=86400",
|
|
metadata: { firebaseStorageDownloadTokens: downloadToken },
|
|
},
|
|
});
|
|
|
|
const fileUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent(
|
|
storagePath
|
|
)}?alt=media&token=${downloadToken}`;
|
|
|
|
logger.info("[merge] upload terminé", { storagePath });
|
|
|
|
return {
|
|
success: true,
|
|
url: fileUrl,
|
|
contentType: "video/mp4",
|
|
storagePath,
|
|
};
|
|
} catch (err) {
|
|
logger.error("[merge] échec", { error: err?.message || String(err) });
|
|
if (err instanceof HttpsError) throw err;
|
|
throw new HttpsError("internal", err?.message || "Fusion échouée");
|
|
} finally {
|
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
);
|