lypsync & other
This commit is contained in:
@@ -405,6 +405,7 @@ const Home = ({ navigation, route }) => {
|
|||||||
if (!action?.route) {
|
if (!action?.route) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
console.log("action route is ", action.route)
|
||||||
navigate(action.route, action.params)
|
navigate(action.route, action.params)
|
||||||
},
|
},
|
||||||
[currentProject, ensureProjectSelected, hasActiveProject, handleStartNew]
|
[currentProject, ensureProjectSelected, hasActiveProject, handleStartNew]
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
|||||||
|
|
||||||
const TIME_BEFORE_INCREMENT_MS = 20000 // 20s
|
const TIME_BEFORE_INCREMENT_MS = 20000 // 20s
|
||||||
const COUNTDOWN_SECONDS = 10
|
const COUNTDOWN_SECONDS = 10
|
||||||
|
const CAMERA_STARTUP_DELAY_MS = 250
|
||||||
|
const MAX_SYNC_OFFSET_MS = 2000
|
||||||
const LOG_PREFIX = '[RecordPlayback]'
|
const LOG_PREFIX = '[RecordPlayback]'
|
||||||
const KEEP_AWAKE_TAG = 'record-playback'
|
const KEEP_AWAKE_TAG = 'record-playback'
|
||||||
const log =
|
const log =
|
||||||
@@ -60,6 +62,7 @@ const RecordPlayback = ({ route }) => {
|
|||||||
const startedRef = useRef(false) // empêche les doubles démarrages
|
const startedRef = useRef(false) // empêche les doubles démarrages
|
||||||
const countdownActiveRef = useRef(false) // évite le déclenchement avant 1er tick
|
const countdownActiveRef = useRef(false) // évite le déclenchement avant 1er tick
|
||||||
const playbackStartedRef = useRef(false) // devient vrai lorsque l'audio progresse réellement
|
const playbackStartedRef = useRef(false) // devient vrai lorsque l'audio progresse réellement
|
||||||
|
const playbackStartedAtRef = useRef(0)
|
||||||
const progressLogRef = useRef({ bucket: -1, lastPos: -1, lastDur: -1 })
|
const progressLogRef = useRef({ bucket: -1, lastPos: -1, lastDur: -1 })
|
||||||
const originalLoopingValueRef = useRef({ hasValue: false, value: false })
|
const originalLoopingValueRef = useRef({ hasValue: false, value: false })
|
||||||
const latestLoopingValueRef = useRef(isLooping ?? false)
|
const latestLoopingValueRef = useRef(isLooping ?? false)
|
||||||
@@ -189,6 +192,7 @@ const RecordPlayback = ({ route }) => {
|
|||||||
incrementDoneRef.current = false
|
incrementDoneRef.current = false
|
||||||
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
|
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
|
||||||
playbackStartedRef.current = false
|
playbackStartedRef.current = false
|
||||||
|
playbackStartedAtRef.current = 0
|
||||||
log('Song URL changed, reset counters', { songUrl })
|
log('Song URL changed, reset counters', { songUrl })
|
||||||
}, [songUrl])
|
}, [songUrl])
|
||||||
|
|
||||||
@@ -201,7 +205,10 @@ const RecordPlayback = ({ route }) => {
|
|||||||
const pos = (player?.currentTime || 0) * 1000
|
const pos = (player?.currentTime || 0) * 1000
|
||||||
if (!playbackStartedRef.current && (player?.playing || pos > 0)) {
|
if (!playbackStartedRef.current && (player?.playing || pos > 0)) {
|
||||||
playbackStartedRef.current = true
|
playbackStartedRef.current = true
|
||||||
log('Playback detected', { pos, dur })
|
const now = Date.now()
|
||||||
|
const estimatedStartAt = pos > 0 ? now - pos : now
|
||||||
|
playbackStartedAtRef.current = estimatedStartAt
|
||||||
|
log('Playback detected', { pos, dur, startedAt: estimatedStartAt })
|
||||||
}
|
}
|
||||||
setProgressInfo((prev) => {
|
setProgressInfo((prev) => {
|
||||||
if (prev.pos === pos && prev.dur === dur) return prev
|
if (prev.pos === pos && prev.dur === dur) return prev
|
||||||
@@ -308,6 +315,7 @@ const RecordPlayback = ({ route }) => {
|
|||||||
listenedMsRef.current = 0
|
listenedMsRef.current = 0
|
||||||
incrementDoneRef.current = false
|
incrementDoneRef.current = false
|
||||||
playbackStartedRef.current = false
|
playbackStartedRef.current = false
|
||||||
|
playbackStartedAtRef.current = 0
|
||||||
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
|
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
|
||||||
log('Session reset')
|
log('Session reset')
|
||||||
|
|
||||||
@@ -544,7 +552,7 @@ const RecordPlayback = ({ route }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add artificial delay to compensate for camera startup latency (Audio usually starts faster than video recording)
|
// Add artificial delay to compensate for camera startup latency (Audio usually starts faster than video recording)
|
||||||
await new Promise(r => setTimeout(r, 250))
|
await new Promise(r => setTimeout(r, CAMERA_STARTUP_DELAY_MS))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await player.play?.()
|
await player.play?.()
|
||||||
@@ -698,16 +706,23 @@ const RecordPlayback = ({ route }) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rawOffsetMs =
|
||||||
|
playbackStartedAtRef.current && recordingStartTimeRef.current
|
||||||
|
? playbackStartedAtRef.current - recordingStartTimeRef.current
|
||||||
|
: 0
|
||||||
|
const syncOffsetMs = Math.max(0, Math.min(MAX_SYNC_OFFSET_MS, Math.round(rawOffsetMs || 0)))
|
||||||
|
log('Computed sync offset', { rawOffsetMs, syncOffsetMs })
|
||||||
|
|
||||||
if (video?.uri) {
|
if (video?.uri) {
|
||||||
log('Navigating to RecordedPlayback with video', {
|
log('Navigating to RecordedPlayback with video', {
|
||||||
uriLength: video.uri.length,
|
uriLength: video.uri.length,
|
||||||
})
|
})
|
||||||
navigate(Routes.RecordedPlayback, { videoUri: video.uri, project })
|
navigate(Routes.RecordedPlayback, { videoUri: video.uri, project, syncOffsetMs })
|
||||||
} else {
|
} else {
|
||||||
log('Navigating to RecordedPlayback without video', {
|
log('Navigating to RecordedPlayback without video', {
|
||||||
hasVideo: !!video,
|
hasVideo: !!video,
|
||||||
})
|
})
|
||||||
navigate(Routes.RecordedPlayback, { project })
|
navigate(Routes.RecordedPlayback, { project, syncOffsetMs })
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('startRecordingWithMusic error', {
|
log('startRecordingWithMusic error', {
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ const WEB_PREVIEW_WIDTH = 360
|
|||||||
const MEDIA_BOOTSTRAP_DELAY_MS = 250
|
const MEDIA_BOOTSTRAP_DELAY_MS = 250
|
||||||
const MEDIA_RETRY_DELAY_MS = 700
|
const MEDIA_RETRY_DELAY_MS = 700
|
||||||
const MEDIA_MAX_RETRIES = 2
|
const MEDIA_MAX_RETRIES = 2
|
||||||
|
const CAMERA_STARTUP_DELAY_MS = 250
|
||||||
|
const MAX_SYNC_OFFSET_MS = 2000
|
||||||
|
|
||||||
const toSeconds = (v) => {
|
const toSeconds = (v) => {
|
||||||
const n = Number(v ?? 0)
|
const n = Number(v ?? 0)
|
||||||
@@ -107,6 +109,8 @@ const RecordPlayback = ({ route }) => {
|
|||||||
const listenedMsRef = useRef(0)
|
const listenedMsRef = useRef(0)
|
||||||
const viewsIncrementedRef = useRef(false)
|
const viewsIncrementedRef = useRef(false)
|
||||||
const correctedInitialJumpRef = useRef(false)
|
const correctedInitialJumpRef = useRef(false)
|
||||||
|
const playbackStartedAtRef = useRef(null)
|
||||||
|
const recordingStartAtRef = useRef(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
latestLoopingValueRef.current = isLooping ?? false
|
latestLoopingValueRef.current = isLooping ?? false
|
||||||
@@ -154,6 +158,8 @@ const RecordPlayback = ({ route }) => {
|
|||||||
viewsIncrementedRef.current = false
|
viewsIncrementedRef.current = false
|
||||||
perfStartRef.current = null
|
perfStartRef.current = null
|
||||||
correctedInitialJumpRef.current = false
|
correctedInitialJumpRef.current = false
|
||||||
|
playbackStartedAtRef.current = null
|
||||||
|
recordingStartAtRef.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const releaseRecordingUrl = useCallback(() => {
|
const releaseRecordingUrl = useCallback(() => {
|
||||||
@@ -533,8 +539,19 @@ const RecordPlayback = ({ route }) => {
|
|||||||
isPausedRef.current = false
|
isPausedRef.current = false
|
||||||
pausedAtRef.current = null
|
pausedAtRef.current = null
|
||||||
pausedMsRef.current = 0
|
pausedMsRef.current = 0
|
||||||
navigate(Routes.RecordedPlayback, { project, videoUri: videoUrl || null })
|
|
||||||
}, [dur, player, pos, project, stopRecorderAndGetUrl])
|
const rawOffsetMs =
|
||||||
|
playbackStartedAtRef.current != null && recordingStartAtRef.current != null
|
||||||
|
? playbackStartedAtRef.current - recordingStartAtRef.current
|
||||||
|
: 0
|
||||||
|
const syncOffsetMs = Math.max(0, Math.min(MAX_SYNC_OFFSET_MS, Math.round(rawOffsetMs || 0)))
|
||||||
|
|
||||||
|
navigate(Routes.RecordedPlayback, {
|
||||||
|
project,
|
||||||
|
videoUri: videoUrl || null,
|
||||||
|
syncOffsetMs,
|
||||||
|
})
|
||||||
|
}, [player, project, stopRecorderAndGetUrl])
|
||||||
|
|
||||||
const startProgressLoop = useCallback(() => {
|
const startProgressLoop = useCallback(() => {
|
||||||
if (progressTimerRef.current) clearInterval(progressTimerRef.current)
|
if (progressTimerRef.current) clearInterval(progressTimerRef.current)
|
||||||
@@ -584,6 +601,11 @@ const RecordPlayback = ({ route }) => {
|
|||||||
setDur(nextDur)
|
setDur(nextDur)
|
||||||
setPos(nextPos)
|
setPos(nextPos)
|
||||||
|
|
||||||
|
if (nextPos > 0 && playbackStartedAtRef.current == null && perfStartRef.current != null) {
|
||||||
|
playbackStartedAtRef.current =
|
||||||
|
perfStartRef.current + Math.max(0, (nextPos * 1000) - pausedTotal)
|
||||||
|
}
|
||||||
|
|
||||||
if (isPausedRef.current) {
|
if (isPausedRef.current) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -621,10 +643,11 @@ const RecordPlayback = ({ route }) => {
|
|||||||
pausedMsRef.current = 0
|
pausedMsRef.current = 0
|
||||||
await player?.pause?.() // s'assure qu'on repart propre
|
await player?.pause?.() // s'assure qu'on repart propre
|
||||||
await player?.seekTo?.(0) // tente un seek d'amorçage
|
await player?.seekTo?.(0) // tente un seek d'amorçage
|
||||||
|
recordingStartAtRef.current = performance.now()
|
||||||
const recorderStarted = startRecorder()
|
const recorderStarted = startRecorder()
|
||||||
if (!recorderStarted) {
|
if (!recorderStarted) {
|
||||||
}
|
}
|
||||||
await new Promise(r => setTimeout(r, 250))
|
await new Promise(r => setTimeout(r, CAMERA_STARTUP_DELAY_MS))
|
||||||
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
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { goBack, navigate } from '../../navigation/NavigationService'
|
|||||||
import { gutters, Palette } from '../../styles'
|
import { gutters, Palette } from '../../styles'
|
||||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||||
const RecordedPlayback = ({ route }) => {
|
const RecordedPlayback = ({ route }) => {
|
||||||
const { videoUri, project } = route.params || {}
|
const { videoUri, project, syncOffsetMs = 0 } = route.params || {}
|
||||||
const songUrl = project?.songUrl || null
|
const songUrl = project?.songUrl || null
|
||||||
const audioPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, {
|
const audioPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, {
|
||||||
id: project?.id ? `recorded-${project.id}` : songUrl ? `recorded-${songUrl}` : undefined,
|
id: project?.id ? `recorded-${project.id}` : songUrl ? `recorded-${songUrl}` : undefined,
|
||||||
@@ -38,6 +38,8 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
})
|
})
|
||||||
const playbackEndedRef = useRef(false)
|
const playbackEndedRef = useRef(false)
|
||||||
|
|
||||||
|
const syncOffsetRef = useRef(Math.max(0, Number(syncOffsetMs) || 0))
|
||||||
|
|
||||||
// Start both players on mount
|
// Start both players on mount
|
||||||
const stopPlayback = useCallback(async () => {
|
const stopPlayback = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -60,6 +62,9 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
playbackEndedRef.current = false
|
playbackEndedRef.current = false
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (videoPlayer && syncOffsetRef.current > 0) {
|
||||||
|
videoPlayer.currentTime = syncOffsetRef.current / 1000
|
||||||
|
}
|
||||||
if (audioPlayer && songUrl) await audioPlayer.play?.()
|
if (audioPlayer && songUrl) await audioPlayer.play?.()
|
||||||
if (videoPlayer) videoPlayer.play()
|
if (videoPlayer) videoPlayer.play()
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
@@ -82,10 +87,12 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
|
|
||||||
// basic drift correction: if desync > 300ms, align video
|
// basic drift correction: if desync > 300ms, align video
|
||||||
if (videoPlayer && !Number.isNaN(videoPlayer.currentTime)) {
|
if (videoPlayer && !Number.isNaN(videoPlayer.currentTime)) {
|
||||||
|
const offset = syncOffsetRef.current || 0
|
||||||
|
const expected = Math.max(0, (pos || 0) + offset)
|
||||||
const v = (videoPlayer.currentTime || 0) * 1000
|
const v = (videoPlayer.currentTime || 0) * 1000
|
||||||
const drift = Math.abs(v - pos)
|
const drift = Math.abs(v - expected)
|
||||||
if (drift > 350) {
|
if (drift > 350) {
|
||||||
videoPlayer.currentTime = Math.max(0, (pos || 0) / 1000)
|
videoPlayer.currentTime = Math.max(0, expected / 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
@@ -98,7 +105,10 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
const dur = progressInfo.dur || 0
|
const dur = progressInfo.dur || 0
|
||||||
const pos = Math.max(0, Math.min(dur, Math.floor(targetMs)))
|
const pos = Math.max(0, Math.min(dur, Math.floor(targetMs)))
|
||||||
if (audioPlayer && dur > 0) await audioPlayer.seekTo?.(Math.floor(pos / 1000))
|
if (audioPlayer && dur > 0) await audioPlayer.seekTo?.(Math.floor(pos / 1000))
|
||||||
if (videoPlayer) videoPlayer.currentTime = Math.max(0, pos / 1000)
|
if (videoPlayer) {
|
||||||
|
const offset = syncOffsetRef.current || 0
|
||||||
|
videoPlayer.currentTime = Math.max(0, (pos + offset) / 1000)
|
||||||
|
}
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +169,10 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
|
|
||||||
if (isAtEnd) {
|
if (isAtEnd) {
|
||||||
if (audioPlayer) await audioPlayer.seekTo?.(0)
|
if (audioPlayer) await audioPlayer.seekTo?.(0)
|
||||||
if (videoPlayer) videoPlayer.currentTime = 0
|
if (videoPlayer) {
|
||||||
|
const offset = syncOffsetRef.current || 0
|
||||||
|
videoPlayer.currentTime = Math.max(0, offset / 1000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackEndedRef.current = false
|
playbackEndedRef.current = false
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const toSeconds = (value) => {
|
|||||||
const WEB_PREVIEW_WIDTH = 360
|
const WEB_PREVIEW_WIDTH = 360
|
||||||
|
|
||||||
const RecordedPlayback = ({ route }) => {
|
const RecordedPlayback = ({ route }) => {
|
||||||
const { videoUri, project } = route.params || {}
|
const { videoUri, project, syncOffsetMs = 0 } = route.params || {}
|
||||||
const songUrl = project?.songUrl || null
|
const songUrl = project?.songUrl || null
|
||||||
// AUDIO PLAYER (expo-audio → seconds)
|
// AUDIO PLAYER (expo-audio → seconds)
|
||||||
const audioPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, {
|
const audioPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, {
|
||||||
@@ -44,6 +44,7 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
|
|
||||||
// Optionnel: si tu veux quand même un rendu vidéo sur web si tu as une source
|
// Optionnel: si tu veux quand même un rendu vidéo sur web si tu as une source
|
||||||
const videoElRef = useRef(null)
|
const videoElRef = useRef(null)
|
||||||
|
const syncOffsetRef = useRef(Math.max(0, Number(syncOffsetMs) || 0) / 1000)
|
||||||
const playbackEndedRef = useRef(false)
|
const playbackEndedRef = useRef(false)
|
||||||
const shouldPreserveBlobRef = useRef(false)
|
const shouldPreserveBlobRef = useRef(false)
|
||||||
|
|
||||||
@@ -85,6 +86,9 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
playbackEndedRef.current = false
|
playbackEndedRef.current = false
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (videoElRef.current && videoUri && syncOffsetRef.current > 0) {
|
||||||
|
videoElRef.current.currentTime = Math.max(0, syncOffsetRef.current)
|
||||||
|
}
|
||||||
if (audioPlayer && songUrl) {
|
if (audioPlayer && songUrl) {
|
||||||
await audioPlayer.play?.()
|
await audioPlayer.play?.()
|
||||||
}
|
}
|
||||||
@@ -119,9 +123,10 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
// Sync vidéo si on a une source vidéo
|
// Sync vidéo si on a une source vidéo
|
||||||
if (videoElRef.current && videoUri && !Number.isNaN(videoElRef.current.currentTime)) {
|
if (videoElRef.current && videoUri && !Number.isNaN(videoElRef.current.currentTime)) {
|
||||||
const v = Number(videoElRef.current.currentTime || 0)
|
const v = Number(videoElRef.current.currentTime || 0)
|
||||||
const drift = Math.abs(v - posS)
|
const expected = Math.max(0, posS + (syncOffsetRef.current || 0))
|
||||||
|
const drift = Math.abs(v - expected)
|
||||||
if (drift > 0.35) {
|
if (drift > 0.35) {
|
||||||
videoElRef.current.currentTime = Math.max(0, posS)
|
videoElRef.current.currentTime = expected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
@@ -153,7 +158,7 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
} catch { }
|
} catch { }
|
||||||
try {
|
try {
|
||||||
if (videoElRef.current) {
|
if (videoElRef.current) {
|
||||||
videoElRef.current.currentTime = 0
|
videoElRef.current.currentTime = Math.max(0, syncOffsetRef.current || 0)
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
})()
|
})()
|
||||||
@@ -170,7 +175,8 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
await audioPlayer.seekTo?.(Math.max(0, target))
|
await audioPlayer.seekTo?.(Math.max(0, target))
|
||||||
}
|
}
|
||||||
if (videoElRef.current && videoUri) {
|
if (videoElRef.current && videoUri) {
|
||||||
videoElRef.current.currentTime = Math.max(0, target)
|
const offset = syncOffsetRef.current || 0
|
||||||
|
videoElRef.current.currentTime = Math.max(0, target + offset)
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
})()
|
})()
|
||||||
@@ -232,7 +238,7 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
if (isAtEnd) {
|
if (isAtEnd) {
|
||||||
if (audioPlayer) await audioPlayer.seekTo?.(0)
|
if (audioPlayer) await audioPlayer.seekTo?.(0)
|
||||||
if (videoElRef.current && videoUri) {
|
if (videoElRef.current && videoUri) {
|
||||||
videoElRef.current.currentTime = 0
|
videoElRef.current.currentTime = Math.max(0, syncOffsetRef.current || 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -448,7 +448,7 @@ const Playbacks = () => {
|
|||||||
onPress={() => setViewMode('charts')}
|
onPress={() => setViewMode('charts')}
|
||||||
accessibilityLabel="Revenir au classement des playbacks"
|
accessibilityLabel="Revenir au classement des playbacks"
|
||||||
>
|
>
|
||||||
<Text style={styles.backToChartsText}>Classement</Text>
|
<Text style={styles.backToChartsText}>Retour au classement</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
// </Page>
|
// </Page>
|
||||||
|
|||||||
Reference in New Issue
Block a user