feat: fix cloud function for audio merge

This commit is contained in:
2026-03-03 15:13:39 +01:00
parent 7241d46937
commit 4755c0d8e2
4 changed files with 245 additions and 1524 deletions
+87 -33
View File
@@ -32,59 +32,54 @@ async function downloadToFile(url, destPath) {
const SCALE_FILTER =
"scale='trunc(min(1920,iw)/2)*2':'trunc(min(1920,ih)/2)*2':force_original_aspect_ratio=decrease"
// functions/mergeVideoAndAudio.js
async function muxAudioIntoVideo({ videoPath, audioPath, outPath, syncOffset = 0 }) {
return new Promise((resolve, reject) => {
const command = ffmpeg()
let command = ffmpeg()
const offset = Number(syncOffset) || 0
// L'offset : si > 0, on retarde la vidéo. Si < 0, on retarde l'audio.
if (syncOffset > 0) command.inputOptions(['-itsoffset', String(syncOffset)])
command.input(videoPath)
// Si offset > 0 : la vidéo est en avance (ou capturée en retard dans le passé relatif ?), on veut la retarder pour qu'elle commence plus tard
// c-à-d on décale le flux vidéo.
// Si offset < 0 : on décale l'audio.
if (offset > 0) {
command.inputOptions(['-itsoffset', String(offset)])
}
command.input(videoPath) // 0:v
if (offset < 0) {
command.inputOptions(['-itsoffset', String(Math.abs(offset))])
}
command.input(audioPath) // 1:a
if (syncOffset < 0) command.inputOptions(['-itsoffset', String(Math.abs(syncOffset))])
command.input(audioPath)
// Remplace le bloc .save(outPath) par celui-ci :
command
.outputOptions([
'-map',
'0:v:0', // garder la 1re piste vidéo de l'entrée 0
'0:v:0',
'-map',
'1:a:0', // prendre la 1re piste audio de l'entrée 1
// Force une sortie H264 1080p max pour compatibilité totale iOS (les WebM VP8/9 posaient problème)
'1:a:0',
'-vf',
SCALE_FILTER,
`fps=30,${SCALE_FILTER}`,
'-af',
'aresample=async=1',
'-c:v',
'libx264',
'-preset',
'veryfast',
'superfast',
'-crf',
'22',
'-pix_fmt',
'yuv420p',
'-profile:v',
'high',
'-level:v',
'4.1',
'23',
'-c:a',
'aac',
'-b:a',
'192k',
'128k',
'-movflags',
'+faststart',
'-shortest', // couper à la plus courte des 2 sources
'-tag:v',
'avc1',
'-shortest',
])
.on('error', reject)
.on('end', resolve)
.save(outPath)
.on('error', (err) => {
console.error('FFmpeg Error:', err)
reject(err)
})
.on('end', () => {
console.log('Processing finished !')
resolve()
})
.output(outPath) // On définit la sortie ici
.run() // Et on lance l'exécution ici
})
}
@@ -175,6 +170,65 @@ exports.mergeVideoAndAudio = onCall(
return result
}
)
/**
* Fusionne une vidéo et un audio avec correction de synchronisation.
* Gère les paramètres : videoUrl, audioUrl, storagePath, projectId, syncOffset
*/
exports.mergeVideoAndAudio = onCall(
{ region: REGION, timeoutSeconds: 540, memory: '1GiB' },
async (request) => {
// Dans v2, les arguments sont dans request.data
const { data, auth } = request;
const uid = auth?.uid;
if (!uid) {
throw new HttpsError('unauthenticated', 'Authentification requise');
}
const { videoUrl, audioUrl, storagePath, projectId, syncOffset } = data || {};
// 1. Validation des paramètres
if (!videoUrl || !audioUrl || !storagePath) {
throw new HttpsError(
'invalid-argument',
'Paramètres manquants : videoUrl, audioUrl et storagePath sont requis.'
);
}
// 2. Sécurité : Vérifier que l'utilisateur écrit dans son propre dossier
const expectedPrefix = `users/${uid}/`;
if (!storagePath.startsWith(expectedPrefix)) {
throw new HttpsError(
'permission-denied',
`Accès refusé : le chemin doit commencer par ${expectedPrefix}`
);
}
try {
logger.info(`[merge] Début du traitement pour le projet : ${projectId || 'inconnu'}`);
// 3. Appel de la logique de traitement (download -> ffmpeg -> upload)
// On passe le syncOffset s'il existe (ex: -0.150 pour 150ms de latence)
const result = await uploadPlaybackAsset({
videoUrl,
audioUrl,
storagePath,
syncOffset: syncOffset || 0
});
// 4. Marquer la compatibilité dans Firestore si un ID de projet est fourni
if (projectId) {
await markPlaybackCompatibility({ projectId, initiatorUid: uid });
}
return result;
} catch (error) {
logger.error('[merge] Erreur fatale lors de la fusion', error);
if (error instanceof HttpsError) throw error;
throw new HttpsError('internal', error.message || 'Erreur interne de fusion');
}
}
);
exports.reencodePlayback = onCall(
{ region: REGION, timeoutSeconds: 540, memory: '1GiB' },