feat: fix cloud function for audio merge
This commit is contained in:
+87
-33
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user