From acfc7a8394ca3c43439efb1baaaa16494dae4406 Mon Sep 17 00:00:00 2001 From: Leon Date: Thu, 12 Feb 2026 15:46:44 +0100 Subject: [PATCH] lypsync & other --- src/screens/Home/Home.js | 1 + src/screens/Playback/RecordPlayback.js | 23 +++++++++++++--- src/screens/Playback/RecordPlayback.web.js | 29 ++++++++++++++++++-- src/screens/Playback/RecordedPlayback.js | 23 ++++++++++++---- src/screens/Playback/RecordedPlayback.web.js | 18 ++++++++---- src/screens/Playbacks/Playbacks.web.js | 2 +- 6 files changed, 77 insertions(+), 19 deletions(-) diff --git a/src/screens/Home/Home.js b/src/screens/Home/Home.js index 55c01d1..20ff4f0 100644 --- a/src/screens/Home/Home.js +++ b/src/screens/Home/Home.js @@ -405,6 +405,7 @@ const Home = ({ navigation, route }) => { if (!action?.route) { return } + console.log("action route is ", action.route) navigate(action.route, action.params) }, [currentProject, ensureProjectSelected, hasActiveProject, handleStartNew] diff --git a/src/screens/Playback/RecordPlayback.js b/src/screens/Playback/RecordPlayback.js index 40a4197..1097a4d 100644 --- a/src/screens/Playback/RecordPlayback.js +++ b/src/screens/Playback/RecordPlayback.js @@ -22,6 +22,8 @@ import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader' const TIME_BEFORE_INCREMENT_MS = 20000 // 20s const COUNTDOWN_SECONDS = 10 +const CAMERA_STARTUP_DELAY_MS = 250 +const MAX_SYNC_OFFSET_MS = 2000 const LOG_PREFIX = '[RecordPlayback]' const KEEP_AWAKE_TAG = 'record-playback' const log = @@ -60,6 +62,7 @@ const RecordPlayback = ({ route }) => { const startedRef = useRef(false) // empêche les doubles démarrages const countdownActiveRef = useRef(false) // évite le déclenchement avant 1er tick 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 originalLoopingValueRef = useRef({ hasValue: false, value: false }) const latestLoopingValueRef = useRef(isLooping ?? false) @@ -189,6 +192,7 @@ const RecordPlayback = ({ route }) => { incrementDoneRef.current = false progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 } playbackStartedRef.current = false + playbackStartedAtRef.current = 0 log('Song URL changed, reset counters', { songUrl }) }, [songUrl]) @@ -201,7 +205,10 @@ const RecordPlayback = ({ route }) => { const pos = (player?.currentTime || 0) * 1000 if (!playbackStartedRef.current && (player?.playing || pos > 0)) { 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) => { if (prev.pos === pos && prev.dur === dur) return prev @@ -308,6 +315,7 @@ const RecordPlayback = ({ route }) => { listenedMsRef.current = 0 incrementDoneRef.current = false playbackStartedRef.current = false + playbackStartedAtRef.current = 0 progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 } 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) - await new Promise(r => setTimeout(r, 250)) + await new Promise(r => setTimeout(r, CAMERA_STARTUP_DELAY_MS)) try { await player.play?.() @@ -698,16 +706,23 @@ const RecordPlayback = ({ route }) => { 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) { log('Navigating to RecordedPlayback with video', { uriLength: video.uri.length, }) - navigate(Routes.RecordedPlayback, { videoUri: video.uri, project }) + navigate(Routes.RecordedPlayback, { videoUri: video.uri, project, syncOffsetMs }) } else { log('Navigating to RecordedPlayback without video', { hasVideo: !!video, }) - navigate(Routes.RecordedPlayback, { project }) + navigate(Routes.RecordedPlayback, { project, syncOffsetMs }) } } catch (e) { log('startRecordingWithMusic error', { diff --git a/src/screens/Playback/RecordPlayback.web.js b/src/screens/Playback/RecordPlayback.web.js index 9af7619..af5a069 100644 --- a/src/screens/Playback/RecordPlayback.web.js +++ b/src/screens/Playback/RecordPlayback.web.js @@ -27,6 +27,8 @@ const WEB_PREVIEW_WIDTH = 360 const MEDIA_BOOTSTRAP_DELAY_MS = 250 const MEDIA_RETRY_DELAY_MS = 700 const MEDIA_MAX_RETRIES = 2 +const CAMERA_STARTUP_DELAY_MS = 250 +const MAX_SYNC_OFFSET_MS = 2000 const toSeconds = (v) => { const n = Number(v ?? 0) @@ -107,6 +109,8 @@ const RecordPlayback = ({ route }) => { const listenedMsRef = useRef(0) const viewsIncrementedRef = useRef(false) const correctedInitialJumpRef = useRef(false) + const playbackStartedAtRef = useRef(null) + const recordingStartAtRef = useRef(null) useEffect(() => { latestLoopingValueRef.current = isLooping ?? false @@ -154,6 +158,8 @@ const RecordPlayback = ({ route }) => { viewsIncrementedRef.current = false perfStartRef.current = null correctedInitialJumpRef.current = false + playbackStartedAtRef.current = null + recordingStartAtRef.current = null } const releaseRecordingUrl = useCallback(() => { @@ -533,8 +539,19 @@ const RecordPlayback = ({ route }) => { isPausedRef.current = false pausedAtRef.current = null 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(() => { if (progressTimerRef.current) clearInterval(progressTimerRef.current) @@ -584,6 +601,11 @@ const RecordPlayback = ({ route }) => { setDur(nextDur) setPos(nextPos) + if (nextPos > 0 && playbackStartedAtRef.current == null && perfStartRef.current != null) { + playbackStartedAtRef.current = + perfStartRef.current + Math.max(0, (nextPos * 1000) - pausedTotal) + } + if (isPausedRef.current) { return } @@ -621,10 +643,11 @@ const RecordPlayback = ({ route }) => { pausedMsRef.current = 0 await player?.pause?.() // s'assure qu'on repart propre await player?.seekTo?.(0) // tente un seek d'amorçage + recordingStartAtRef.current = performance.now() const recorderStarted = startRecorder() 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 perfStartRef.current = perfStartRef.current ?? performance.now() correctedInitialJumpRef.current = false // autorise la correction 1-shot diff --git a/src/screens/Playback/RecordedPlayback.js b/src/screens/Playback/RecordedPlayback.js index 1d40254..3aa32a9 100644 --- a/src/screens/Playback/RecordedPlayback.js +++ b/src/screens/Playback/RecordedPlayback.js @@ -15,7 +15,7 @@ import { goBack, navigate } from '../../navigation/NavigationService' import { gutters, Palette } from '../../styles' import { FONT_FAMILY } from '../../styles/Fonts' const RecordedPlayback = ({ route }) => { - const { videoUri, project } = route.params || {} + const { videoUri, project, syncOffsetMs = 0 } = route.params || {} const songUrl = project?.songUrl || null const audioPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, { id: project?.id ? `recorded-${project.id}` : songUrl ? `recorded-${songUrl}` : undefined, @@ -38,6 +38,8 @@ const RecordedPlayback = ({ route }) => { }) const playbackEndedRef = useRef(false) + const syncOffsetRef = useRef(Math.max(0, Number(syncOffsetMs) || 0)) + // Start both players on mount const stopPlayback = useCallback(async () => { try { @@ -60,6 +62,9 @@ const RecordedPlayback = ({ route }) => { playbackEndedRef.current = false const start = async () => { try { + if (videoPlayer && syncOffsetRef.current > 0) { + videoPlayer.currentTime = syncOffsetRef.current / 1000 + } if (audioPlayer && songUrl) await audioPlayer.play?.() if (videoPlayer) videoPlayer.play() } catch (e) { } @@ -82,10 +87,12 @@ const RecordedPlayback = ({ route }) => { // basic drift correction: if desync > 300ms, align video 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 drift = Math.abs(v - pos) + const drift = Math.abs(v - expected) if (drift > 350) { - videoPlayer.currentTime = Math.max(0, (pos || 0) / 1000) + videoPlayer.currentTime = Math.max(0, expected / 1000) } } } catch (e) { } @@ -98,7 +105,10 @@ const RecordedPlayback = ({ route }) => { const dur = progressInfo.dur || 0 const pos = Math.max(0, Math.min(dur, Math.floor(targetMs))) 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) { } } @@ -159,7 +169,10 @@ const RecordedPlayback = ({ route }) => { if (isAtEnd) { 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 diff --git a/src/screens/Playback/RecordedPlayback.web.js b/src/screens/Playback/RecordedPlayback.web.js index 9bfb61d..2e6677a 100644 --- a/src/screens/Playback/RecordedPlayback.web.js +++ b/src/screens/Playback/RecordedPlayback.web.js @@ -30,7 +30,7 @@ const toSeconds = (value) => { const WEB_PREVIEW_WIDTH = 360 const RecordedPlayback = ({ route }) => { - const { videoUri, project } = route.params || {} + const { videoUri, project, syncOffsetMs = 0 } = route.params || {} const songUrl = project?.songUrl || null // AUDIO PLAYER (expo-audio → seconds) 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 const videoElRef = useRef(null) + const syncOffsetRef = useRef(Math.max(0, Number(syncOffsetMs) || 0) / 1000) const playbackEndedRef = useRef(false) const shouldPreserveBlobRef = useRef(false) @@ -85,6 +86,9 @@ const RecordedPlayback = ({ route }) => { playbackEndedRef.current = false const start = async () => { try { + if (videoElRef.current && videoUri && syncOffsetRef.current > 0) { + videoElRef.current.currentTime = Math.max(0, syncOffsetRef.current) + } if (audioPlayer && songUrl) { await audioPlayer.play?.() } @@ -119,9 +123,10 @@ const RecordedPlayback = ({ route }) => { // Sync vidéo si on a une source vidéo if (videoElRef.current && videoUri && !Number.isNaN(videoElRef.current.currentTime)) { 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) { - videoElRef.current.currentTime = Math.max(0, posS) + videoElRef.current.currentTime = expected } } } catch { } @@ -153,7 +158,7 @@ const RecordedPlayback = ({ route }) => { } catch { } try { if (videoElRef.current) { - videoElRef.current.currentTime = 0 + videoElRef.current.currentTime = Math.max(0, syncOffsetRef.current || 0) } } catch { } })() @@ -170,7 +175,8 @@ const RecordedPlayback = ({ route }) => { await audioPlayer.seekTo?.(Math.max(0, target)) } 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 { } })() @@ -232,7 +238,7 @@ const RecordedPlayback = ({ route }) => { if (isAtEnd) { if (audioPlayer) await audioPlayer.seekTo?.(0) if (videoElRef.current && videoUri) { - videoElRef.current.currentTime = 0 + videoElRef.current.currentTime = Math.max(0, syncOffsetRef.current || 0) } } diff --git a/src/screens/Playbacks/Playbacks.web.js b/src/screens/Playbacks/Playbacks.web.js index b3da8ce..30c09af 100644 --- a/src/screens/Playbacks/Playbacks.web.js +++ b/src/screens/Playbacks/Playbacks.web.js @@ -448,7 +448,7 @@ const Playbacks = () => { onPress={() => setViewMode('charts')} accessibilityLabel="Revenir au classement des playbacks" > - Classement + Retour au classement //