Files
musicland/functions/src/upload.js
T
2025-10-23 11:27:40 +02:00

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 });
}
}
);