feat: try sync audio to video

This commit is contained in:
2026-01-20 11:16:36 +01:00
parent 4343cee298
commit 490698cb92
5 changed files with 131 additions and 110 deletions
+24 -9
View File
@@ -31,11 +31,26 @@ 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"
async function muxAudioIntoVideo({ videoPath, audioPath, outPath }) {
async function muxAudioIntoVideo({ videoPath, audioPath, outPath, syncOffset = 0 }) {
return new Promise((resolve, reject) => {
ffmpeg()
.input(videoPath) // 0:v
.input(audioPath) // 1:a
const command = ffmpeg()
const offset = Number(syncOffset) || 0
// 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
command
.outputOptions([
'-map',
'0:v:0', // garder la 1re piste vidéo de l'entrée 0
@@ -72,7 +87,7 @@ async function muxAudioIntoVideo({ videoPath, audioPath, outPath }) {
})
}
async function uploadPlaybackAsset({ videoUrl, audioUrl, storagePath }) {
async function uploadPlaybackAsset({ videoUrl, audioUrl, storagePath, syncOffset = 0 }) {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'merge-'))
const videoPath = path.join(tmpDir, 'video.mp4')
const audioPath = path.join(tmpDir, 'audio.mp3')
@@ -84,8 +99,8 @@ async function uploadPlaybackAsset({ videoUrl, audioUrl, storagePath }) {
await downloadToFile(videoUrl, videoPath)
await downloadToFile(audioUrl, audioPath)
logger.info('[merge] transcodage/mux ffmpeg')
await muxAudioIntoVideo({ videoPath, audioPath, outPath })
logger.info('[merge] transcodage/mux ffmpeg', { syncOffset })
await muxAudioIntoVideo({ videoPath, audioPath, outPath, syncOffset })
const bucket = admin.storage().bucket()
const downloadToken = crypto.randomUUID()
@@ -141,7 +156,7 @@ exports.mergeVideoAndAudio = onCall(
const uid = auth?.uid
if (!uid) throw new HttpsError('unauthenticated', 'Authentification requise')
const { videoUrl, audioUrl, storagePath, projectId } = data || {}
const { videoUrl, audioUrl, storagePath, projectId, syncOffset } = data || {}
if (!videoUrl || !audioUrl || !storagePath) {
throw new HttpsError('invalid-argument', 'Requis: { videoUrl, audioUrl, storagePath }')
@@ -152,7 +167,7 @@ exports.mergeVideoAndAudio = onCall(
throw new HttpsError('permission-denied', `storagePath doit commencer par ${expectedPrefix}`)
}
const result = await uploadPlaybackAsset({ videoUrl, audioUrl, storagePath })
const result = await uploadPlaybackAsset({ videoUrl, audioUrl, storagePath, syncOffset })
if (projectId) {
await markPlaybackCompatibility({ projectId, initiatorUid: uid })
}