feat: try sync audio to video
This commit is contained in:
+24
-9
@@ -31,11 +31,26 @@ async function downloadToFile(url, destPath) {
|
|||||||
const SCALE_FILTER =
|
const SCALE_FILTER =
|
||||||
"scale='trunc(min(1920,iw)/2)*2':'trunc(min(1920,ih)/2)*2':force_original_aspect_ratio=decrease"
|
"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) => {
|
return new Promise((resolve, reject) => {
|
||||||
ffmpeg()
|
const command = ffmpeg()
|
||||||
.input(videoPath) // 0:v
|
|
||||||
.input(audioPath) // 1:a
|
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([
|
.outputOptions([
|
||||||
'-map',
|
'-map',
|
||||||
'0:v:0', // garder la 1re piste vidéo de l'entrée 0
|
'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 tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'merge-'))
|
||||||
const videoPath = path.join(tmpDir, 'video.mp4')
|
const videoPath = path.join(tmpDir, 'video.mp4')
|
||||||
const audioPath = path.join(tmpDir, 'audio.mp3')
|
const audioPath = path.join(tmpDir, 'audio.mp3')
|
||||||
@@ -84,8 +99,8 @@ async function uploadPlaybackAsset({ videoUrl, audioUrl, storagePath }) {
|
|||||||
await downloadToFile(videoUrl, videoPath)
|
await downloadToFile(videoUrl, videoPath)
|
||||||
await downloadToFile(audioUrl, audioPath)
|
await downloadToFile(audioUrl, audioPath)
|
||||||
|
|
||||||
logger.info('[merge] transcodage/mux ffmpeg')
|
logger.info('[merge] transcodage/mux ffmpeg', { syncOffset })
|
||||||
await muxAudioIntoVideo({ videoPath, audioPath, outPath })
|
await muxAudioIntoVideo({ videoPath, audioPath, outPath, syncOffset })
|
||||||
|
|
||||||
const bucket = admin.storage().bucket()
|
const bucket = admin.storage().bucket()
|
||||||
const downloadToken = crypto.randomUUID()
|
const downloadToken = crypto.randomUUID()
|
||||||
@@ -141,7 +156,7 @@ exports.mergeVideoAndAudio = onCall(
|
|||||||
const uid = auth?.uid
|
const uid = auth?.uid
|
||||||
if (!uid) throw new HttpsError('unauthenticated', 'Authentification requise')
|
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) {
|
if (!videoUrl || !audioUrl || !storagePath) {
|
||||||
throw new HttpsError('invalid-argument', 'Requis: { 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}`)
|
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) {
|
if (projectId) {
|
||||||
await markPlaybackCompatibility({ projectId, initiatorUid: uid })
|
await markPlaybackCompatibility({ projectId, initiatorUid: uid })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -521,6 +521,10 @@ const RecordPlayback = ({ route }) => {
|
|||||||
message: error?.message || String(error || ''),
|
message: error?.message || String(error || ''),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add artificial delay to compensate for camera startup latency (Audio usually starts faster than video recording)
|
||||||
|
await new Promise(r => setTimeout(r, 250))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await player.play?.()
|
await player.play?.()
|
||||||
log('player.play invoked')
|
log('player.play invoked')
|
||||||
|
|||||||
@@ -520,6 +520,7 @@ const RecordPlayback = ({ route }) => {
|
|||||||
const recorderStarted = startRecorder()
|
const recorderStarted = startRecorder()
|
||||||
if (!recorderStarted) {
|
if (!recorderStarted) {
|
||||||
}
|
}
|
||||||
|
await new Promise(r => setTimeout(r, 250))
|
||||||
await player?.play?.() // attend la promesse → l'élément est prêt
|
await player?.play?.() // attend la promesse → l'élément est prêt
|
||||||
perfStartRef.current = perfStartRef.current ?? performance.now()
|
perfStartRef.current = perfStartRef.current ?? performance.now()
|
||||||
correctedInitialJumpRef.current = false // autorise la correction 1-shot
|
correctedInitialJumpRef.current = false // autorise la correction 1-shot
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Page from '../../layouts/Page'
|
|||||||
import { Routes } from '../../navigation'
|
import { Routes } from '../../navigation'
|
||||||
import { goBack, navigate } from '../../navigation/NavigationService'
|
import { goBack, navigate } from '../../navigation/NavigationService'
|
||||||
import { gutters, Palette } from '../../styles'
|
import { gutters, Palette } from '../../styles'
|
||||||
|
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||||
const RecordedPlayback = ({ route }) => {
|
const RecordedPlayback = ({ route }) => {
|
||||||
const { videoUri, project } = route.params || {}
|
const { videoUri, project } = route.params || {}
|
||||||
const songUrl = project?.songUrl || null
|
const songUrl = project?.songUrl || null
|
||||||
|
|||||||
Reference in New Issue
Block a user