diff --git a/src/components/KaraokeLyrics.js b/src/components/KaraokeLyrics.js
index 06d5ff6..711d878 100644
--- a/src/components/KaraokeLyrics.js
+++ b/src/components/KaraokeLyrics.js
@@ -128,7 +128,6 @@ const alphaWhite = (alpha) => `rgba(255,255,255,${alpha})`
export default function KaraokeLyrics({
alignedWords = [],
currentTimeS = 0,
- timeOffsetS = 0,
showContext = true,
removeTags = true,
mode = 'focus',
@@ -148,10 +147,7 @@ export default function KaraokeLyrics({
)
const activeLines = mode === 'teleprompter' ? teleprompterData : lines
- const effectiveTimeS = useMemo(
- () => Math.max(0, Number(currentTimeS || 0) + Number(timeOffsetS || 0)),
- [currentTimeS, timeOffsetS]
- )
+ const effectiveTimeS = Math.max(0, Number(currentTimeS || 0))
const currentLineIdx = useMemo(() => {
return findFocusedLineIndex(activeLines, effectiveTimeS)
diff --git a/src/hooks/useSharedAudioPlayer.js b/src/hooks/useSharedAudioPlayer.js
index 1e3e702..d09ae25 100644
--- a/src/hooks/useSharedAudioPlayer.js
+++ b/src/hooks/useSharedAudioPlayer.js
@@ -69,13 +69,14 @@ const useSharedAudioPlayer = (source, options = null) => {
durationMs: 0,
})
- useEffect(() => {
- stateRef.current = {
- isPlaying,
- positionMs,
- durationMs,
- }
- }, [isPlaying, positionMs, durationMs])
+ // Keep the imperative player getters on the exact snapshot that triggered
+ // this render. Consumers can therefore use the shared player as their only
+ // playback clock without maintaining a second timer.
+ stateRef.current = {
+ isPlaying,
+ positionMs,
+ durationMs,
+ }
const controlsRef = useRef({
play,
@@ -135,6 +136,14 @@ const useSharedAudioPlayer = (source, options = null) => {
enumerable: true,
get: () => stateRef.current.positionMs / 1000,
},
+ positionMs: {
+ enumerable: true,
+ get: () => stateRef.current.positionMs,
+ },
+ durationMs: {
+ enumerable: true,
+ get: () => stateRef.current.durationMs,
+ },
})
player.load = async ({ startPositionMs = 0 } = {}) => {
diff --git a/src/screens/Playback/RecordPlayback.js b/src/screens/Playback/RecordPlayback.js
index 187dba1..c43489f 100644
--- a/src/screens/Playback/RecordPlayback.js
+++ b/src/screens/Playback/RecordPlayback.js
@@ -25,13 +25,12 @@ import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
const TIME_BEFORE_INCREMENT_MS = 20000 // 20s
const COUNTDOWN_SECONDS = 10
const CAMERA_STARTUP_DELAY_MS = 250
-const KARAOKE_LEAD_S = 0.68
const LOG_PREFIX = '[RecordPlayback]'
const KEEP_AWAKE_TAG = 'record-playback'
const log =
typeof __DEV__ === 'undefined' || __DEV__
? (...args) => console.log(LOG_PREFIX, ...args)
- : () => { }
+ : () => {}
const CAMERA_FACING_OPTIONS = [
{ label: 'Avant', value: 'front' },
@@ -53,7 +52,6 @@ const RecordPlayback = ({ route }) => {
const countdownTimerRef = useRef(null)
const listenTimerRef = useRef(null)
const checkSongEndRef = useRef(null)
- const isPausedRef = useRef(false)
const stopRequestedRef = useRef(false)
const restartRequestedRef = useRef(false)
const recordingStartTimeRef = useRef(0) // Track recording start time for duration check
@@ -63,12 +61,8 @@ const RecordPlayback = ({ route }) => {
const exitRequestedRef = useRef(false)
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 playbackStartedRef = useRef(false)
const playbackStartedAtRef = useRef(0)
- const playbackProbeRef = useRef({ lastPos: 0, lastAt: 0 })
- const playbackStartRequestAtRef = useRef(0)
- const playbackStartWarnedRef = useRef(false)
- const progressLogRef = useRef({ bucket: -1, lastPos: -1, lastDur: -1 })
const originalLoopingValueRef = useRef({ hasValue: false, value: false })
const latestLoopingValueRef = useRef(isLooping ?? false)
@@ -80,9 +74,7 @@ const RecordPlayback = ({ route }) => {
const [isPreparing, setIsPreparing] = useState(false)
const [countdown, setCountdown] = useState(0)
const [isRecording, setIsRecording] = useState(false)
- const [isPaused, setIsPaused] = useState(false)
const [showProgress, setShowProgress] = useState(false)
- const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 })
const [cameraFacing, setCameraFacing] = useState('front')
// Musique
@@ -103,10 +95,6 @@ const RecordPlayback = ({ route }) => {
useEffect(() => {
latestLoopingValueRef.current = isLooping ?? false
}, [isLooping])
- useEffect(() => {
- isPausedRef.current = isPaused
- }, [isPaused])
-
useEffect(() => {
const shouldKeepAwake = isPreparing || isRecording
const updateKeepAwake = async () => {
@@ -116,13 +104,13 @@ const RecordPlayback = ({ route }) => {
} else {
await deactivateKeepAwake(KEEP_AWAKE_TAG)
}
- } catch (_) { }
+ } catch (_) {}
}
void updateKeepAwake()
return () => {
try {
void deactivateKeepAwake(KEEP_AWAKE_TAG)
- } catch (_) { }
+ } catch (_) {}
}
}, [isPreparing, isRecording])
@@ -150,156 +138,23 @@ const RecordPlayback = ({ route }) => {
}))
}, [project?.musicTimestamps, musicIndex])
- // Build line groups to display line-by-line
- const lines = useMemo(() => {
- const out = []
- let buf = []
- let start = null
- const clean = (txt) =>
- String(txt || '')
- .replace(/\s+/g, ' ')
- .trim()
- const isSentenceEnd = (txt) => /[.!?]$/.test((txt || '').trim())
- const isSectionTag = (txt) => /^\s*\[[^\]]+\]\s*$/i.test((txt || '').trim())
- for (let i = 0; i < alignedWords.length; i++) {
- const w = alignedWords[i]
- const original = String(w.word || '')
- const textNoNewline = original.replace(/\n/g, ' ')
- const gapToNext =
- i < alignedWords.length - 1 ? Math.max(0, alignedWords[i + 1].startS - w.endS) : 0
-
- // If buffer empty, mark start
- if (buf.length === 0) start = w.startS
-
- // Section tag becomes its own line
- if (isSectionTag(textNoNewline)) {
- const joined = clean(textNoNewline)
- if (joined) out.push({ text: joined, startS: start ?? w.startS, endS: w.endS })
- buf = []
- start = null
- continue
- }
-
- buf.push(textNoNewline)
-
- const eolByNewline = /\n/.test(original)
- const eolByPause = gapToNext >= 0.6
- const eolByPunct = isSentenceEnd(textNoNewline)
- const isLast = i === alignedWords.length - 1
-
- if (eolByNewline || eolByPause || eolByPunct || isLast) {
- const joined = clean(buf.join(' '))
- if (joined) out.push({ text: joined, startS: start ?? w.startS, endS: w.endS })
- buf = []
- start = null
- }
- }
- return out
- }, [alignedWords])
-
useEffect(() => {
listenedMsRef.current = 0
incrementDoneRef.current = false
- progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
playbackStartedRef.current = false
playbackStartedAtRef.current = 0
- playbackProbeRef.current = { lastPos: 0, lastAt: 0 }
- playbackStartRequestAtRef.current = 0
- playbackStartWarnedRef.current = false
- setProgressInfo({ pos: 0, dur: 0 })
log('Song URL changed, reset counters', { songUrl })
}, [songUrl])
- // Poll player -> progress ring
- useEffect(() => {
- if (!player) return
- const id = setInterval(() => {
- try {
- const now = Date.now()
- const dur = (player?.duration || 0) * 1000
- const pos = (player?.currentTime || 0) * 1000
- const lastProbe = playbackProbeRef.current
- const advancing = pos > (lastProbe?.lastPos || 0) + 30
- const playbackHasAdvanced = pos >= 80 || advancing
- playbackProbeRef.current = { lastPos: pos, lastAt: now }
-
- if (
- playbackStartRequestAtRef.current &&
- !playbackStartedRef.current &&
- !playbackStartWarnedRef.current &&
- now - playbackStartRequestAtRef.current > 1500
- ) {
- playbackStartWarnedRef.current = true
- log('Playback not started yet', {
- playing: player?.playing,
- pos,
- dur,
- })
- }
-
- if (!playbackStartedRef.current && playbackHasAdvanced) {
- playbackStartedRef.current = true
- const estimatedStartAt = pos > 0 ? now - pos : now
- playbackStartedAtRef.current = estimatedStartAt
- log('Playback detected', {
- pos,
- dur,
- startedAt: estimatedStartAt,
- reason: advancing ? 'advancing' : 'position',
- })
- }
- setProgressInfo((prev) => {
- const effectivePos = playbackStartedRef.current ? pos : 0
- if (prev.pos === effectivePos && prev.dur === dur) return prev
- return { pos: effectivePos, dur }
- })
- if (dur <= 0) {
- if (progressLogRef.current.lastDur !== 0) {
- progressLogRef.current = { bucket: -1, lastPos: pos, lastDur: 0 }
- log('Progress waiting for duration', {
- pos,
- playing: player?.playing,
- })
- } else {
- progressLogRef.current.lastPos = pos
- }
- return
- }
- const pct = Math.max(0, Math.min(1, pos / dur))
- const bucket = Math.floor(pct * 10)
- const { bucket: prevBucket } = progressLogRef.current
- if (bucket !== prevBucket) {
- log('Progress update', {
- bucket,
- pct: Number.isFinite(pct) ? Number(pct.toFixed(2)) : pct,
- pos,
- dur,
- playing: player?.playing,
- })
- }
- progressLogRef.current = { bucket, lastPos: pos, lastDur: dur }
- } catch (_) { }
- }, 250)
- return () => clearInterval(id)
- }, [player])
-
- // Compute current line index from playback time
- const currentTimeS = (progressInfo?.pos || 0) / 1000
- const currentLineIdx = useMemo(() => {
- if (!lines || lines.length === 0) return -1
- for (let i = 0; i < lines.length; i++) {
- const L = lines[i]
- if (currentTimeS >= L.startS && currentTimeS <= L.endS) return i
- }
- if (currentTimeS > (lines[lines.length - 1]?.endS || 0)) return lines.length - 1
- return -1
- }, [lines, currentTimeS])
+ // The audio player is the single clock for the progress ring and lyrics.
+ const positionMs = player?.positionMs || 0
+ const durationMs = player?.durationMs || 0
const getScrollY = () => (typeof window !== 'undefined' ? window.scrollY : 0)
// Permissions au mount + cleanup
useEffect(() => {
- ; (async () => {
+ ;(async () => {
try {
if (!cameraPermission?.granted) {
log('Requesting camera permission on mount')
@@ -307,7 +162,7 @@ const RecordPlayback = ({ route }) => {
} else {
log('Camera permission already granted on mount')
}
- } catch (_) { }
+ } catch (_) {}
})()
return () => {
try {
@@ -318,7 +173,7 @@ const RecordPlayback = ({ route }) => {
listenTimerRef.current = null
checkSongEndRef.current = null
log('Cleanup on unmount, cleared timers')
- } catch (_) { }
+ } catch (_) {}
}
}, []) // eslint-disable-line react-hooks/exhaustive-deps
@@ -355,30 +210,23 @@ const RecordPlayback = ({ route }) => {
incrementDoneRef.current = false
playbackStartedRef.current = false
playbackStartedAtRef.current = 0
- playbackProbeRef.current = { lastPos: 0, lastAt: 0 }
- playbackStartRequestAtRef.current = 0
- playbackStartWarnedRef.current = false
- setProgressInfo({ pos: 0, dur: 0 })
- progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
log('Session reset')
setIsPreparing(false)
setIsRecording(false)
- setIsPaused(false)
- isPausedRef.current = false
setShowProgress(false)
setCountdown(0)
try {
await cameraRef.current?.resumePreview?.()
- } catch (_) { }
+ } catch (_) {}
if (player) {
try {
if (player.playing) await player.pause?.()
await player.seekTo?.(0)
- } catch (_) { }
+ } catch (_) {}
}
- } catch (_) { }
+ } catch (_) {}
},
[player]
)
@@ -456,8 +304,6 @@ const RecordPlayback = ({ route }) => {
preserveSongEndWatcher,
preserveStopRequest,
})
- setIsPaused(false)
- isPausedRef.current = false
if (!preserveRestartFlag) {
restartRequestedRef.current = false
manualRestartInFlightRef.current = false
@@ -504,23 +350,16 @@ const RecordPlayback = ({ route }) => {
try {
stopRequestedRef.current = false
stopRequestedAtRef.current = 0
- recordingStartTimeRef.current = Date.now() // Start timer
listenedMsRef.current = 0
incrementDoneRef.current = false
setIsRecording(true)
setShowProgress(true)
- setIsPaused(false)
- isPausedRef.current = false
try {
await cameraRef.current?.resumePreview?.()
- } catch (_) { }
+ } catch (_) {}
playbackStartedRef.current = false
- playbackProbeRef.current = { lastPos: 0, lastAt: 0 }
- playbackStartRequestAtRef.current = 0
- playbackStartWarnedRef.current = false
- setProgressInfo({ pos: 0, dur: 0 })
- progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
+ playbackStartedAtRef.current = 0
log('startRecordingWithMusic', {
hasPlayer: !!player,
songUrl,
@@ -586,6 +425,7 @@ const RecordPlayback = ({ route }) => {
throw new Error("L'enregistrement vidéo n'est pas supporté sur cet appareil.")
})()
+ recordingStartTimeRef.current = Date.now()
activeRecordingPromiseRef.current = recordPromise
if (player && songUrl) {
@@ -598,12 +438,14 @@ 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, CAMERA_STARTUP_DELAY_MS))
+ // Give the native recorder a short warm-up, then measure the real gap
+ // between the recording request and the audio start for the final trim.
+ await new Promise((resolve) => setTimeout(resolve, CAMERA_STARTUP_DELAY_MS))
try {
- playbackStartRequestAtRef.current = Date.now()
await player.play?.()
+ playbackStartedAtRef.current = Date.now()
+ playbackStartedRef.current = true
log('player.play invoked')
} catch (error) {
log('player.play failed', {
@@ -625,10 +467,10 @@ const RecordPlayback = ({ route }) => {
try {
await projectsRef.doc(project.id).set({ views: increment(1) }, { merge: true })
log('Views incremented')
- } catch (_) { }
+ } catch (_) {}
}
}
- } catch (_) { }
+ } catch (_) {}
}, 500)
}
@@ -638,7 +480,6 @@ const RecordPlayback = ({ route }) => {
checkSongEndRef.current = setInterval(() => {
try {
if (!player) return
- if (isPausedRef.current) return
if (stopRequestedRef.current) {
const sinceLastRequest = Date.now() - (stopRequestedAtRef.current || 0)
if (sinceLastRequest >= 1200) {
@@ -646,7 +487,7 @@ const RecordPlayback = ({ route }) => {
try {
cameraRef.current?.stopRecording?.()
log('stopRecording retried while awaiting stop')
- } catch (_) { }
+ } catch (_) {}
}
return
}
@@ -671,9 +512,9 @@ const RecordPlayback = ({ route }) => {
try {
cameraRef.current?.stopRecording?.()
log('stopRecording triggered')
- } catch (_) { }
+ } catch (_) {}
}
- } catch (_) { }
+ } catch (_) {}
}, 500)
}
@@ -691,7 +532,7 @@ const RecordPlayback = ({ route }) => {
}
try {
if (player?.playing) await player.pause?.()
- } catch (_) { }
+ } catch (_) {}
if (listenTimerRef.current) {
clearInterval(listenTimerRef.current)
listenTimerRef.current = null
@@ -699,12 +540,9 @@ const RecordPlayback = ({ route }) => {
}
setIsRecording(false)
- setIsPaused(false)
- isPausedRef.current = false
setShowProgress(false)
log('Recording flow completed', { hasVideo: !!video?.uri })
playbackStartedRef.current = false
- progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
const exitRequested = exitRequestedRef.current
const shouldRestart = restartRequestedRef.current
// Duration check logic
@@ -724,7 +562,7 @@ const RecordPlayback = ({ route }) => {
await discardRecordingFile(video?.uri, 'too_short')
try {
goBack()
- } catch (_) { }
+ } catch (_) {}
return
}
}
@@ -782,8 +620,6 @@ const RecordPlayback = ({ route }) => {
stopRequestedAtRef.current = 0
setIsRecording(false)
setIsPreparing(false)
- setIsPaused(false)
- isPausedRef.current = false
setShowProgress(false)
if (listenTimerRef.current) {
clearInterval(listenTimerRef.current)
@@ -796,7 +632,6 @@ const RecordPlayback = ({ route }) => {
log('checkSongEndRef cleared (error path)')
}
playbackStartedRef.current = false
- progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
const exitRequested = exitRequestedRef.current
const shouldRestart = restartRequestedRef.current
if (exitRequested) {
@@ -832,7 +667,7 @@ const RecordPlayback = ({ route }) => {
onPress: () => {
try {
goBack()
- } catch (_) { }
+ } catch (_) {}
},
},
],
@@ -859,21 +694,21 @@ const RecordPlayback = ({ route }) => {
if (player?.playing) {
const maybePromise = player.pause?.()
if (maybePromise && typeof maybePromise.catch === 'function') {
- maybePromise.catch(() => { })
+ maybePromise.catch(() => {})
}
}
- } catch (_) { }
+ } catch (_) {}
try {
if (isRecording) {
cameraRef.current?.stopRecording?.()
}
- } catch (_) { }
+ } catch (_) {}
} else {
exitRequestedRef.current = false
}
try {
goBack()
- } catch (_) { }
+ } catch (_) {}
return true
}, [goBack, isPreparing, isRecording, player])
@@ -886,33 +721,6 @@ const RecordPlayback = ({ route }) => {
}, [handleBackPress])
)
- const handleTogglePause = useCallback(async () => {
- try {
- if (!isRecording) return
- if (!isPausedRef.current) {
- setIsPaused(true)
- isPausedRef.current = true
- stopRequestedRef.current = false
- stopRequestedAtRef.current = 0
- try {
- await player?.pause?.()
- } catch (_) { }
- try {
- await cameraRef.current?.pausePreview?.()
- } catch (_) { }
- return
- }
- setIsPaused(false)
- isPausedRef.current = false
- try {
- await cameraRef.current?.resumePreview?.()
- } catch (_) { }
- try {
- await player?.play?.()
- } catch (_) { }
- } catch (_) { }
- }, [isRecording, player])
-
const handleRestartRecording = async () => {
try {
const hasRecordingPending = !!activeRecordingPromiseRef.current
@@ -921,11 +729,9 @@ const RecordPlayback = ({ route }) => {
isRecording,
hasRecordingPending,
})
- setIsPaused(false)
- isPausedRef.current = false
try {
await cameraRef.current?.resumePreview?.()
- } catch (_) { }
+ } catch (_) {}
if (isPreparing) {
if (hasRecordingPending) {
await startCountdownThenRecord({
@@ -952,16 +758,16 @@ const RecordPlayback = ({ route }) => {
stopRequestedAtRef.current = Date.now()
try {
if (player?.playing) await player.pause?.()
- } catch (_) { }
+ } catch (_) {}
try {
cameraRef.current?.stopRecording?.()
- } catch (_) { }
+ } catch (_) {}
await startCountdownThenRecord({
preserveRestartFlag: true,
preserveSongEndWatcher: true,
preserveStopRequest: true,
})
- } catch (_) { }
+ } catch (_) {}
}
const permissionsGranted = !!cameraPermission?.granted
@@ -1033,7 +839,7 @@ const RecordPlayback = ({ route }) => {
onPress={async () => {
try {
if (!cameraPermission?.granted) await requestCameraPermission()
- } catch (_) { }
+ } catch (_) {}
}}
/>
@@ -1074,9 +880,7 @@ const RecordPlayback = ({ route }) => {
{
- {isRecording && (
-
-
- {isPaused ? 'Reprendre' : 'Mettre en pause'}
-
-
- )}
)}
@@ -1146,8 +922,7 @@ const RecordPlayback = ({ route }) => {
{(isRecording || showProgress) && alignedWords?.length > 0 ? (
{
export default RecordPlayback
-// Render previous/current/next line to reduce jumpiness
-const KaraokeLines = ({ lines = [], currentLineIdx = -1 }) => {
- const prev = currentLineIdx > 0 ? lines[currentLineIdx - 1]?.text : ''
- const curr = currentLineIdx >= 0 ? lines[currentLineIdx]?.text : ''
- const next = currentLineIdx + 1 < lines.length ? lines[currentLineIdx + 1]?.text : ''
- return (
-
- {/* {prev ? (
-
- {prev}
-
- ) : null} */}
-
- {curr}
-
- {next ? (
-
- {next}
-
- ) : null}
-
- )
-}
-
const CameraFacingSelector = ({ value = 'front', onChange, disabled = false }) => {
return (
{
- const n = Number(v ?? 0)
- if (!Number.isFinite(n) || n < 0) return 0
- return n > 10000 ? n / 1000 : n // heuristique ms → s
-}
const pickRecorderMimeType = () => {
if (typeof window === 'undefined' || typeof MediaRecorder === 'undefined') return undefined
@@ -51,7 +43,7 @@ const pickRecorderMimeType = () => {
for (const mimeType of candidates) {
try {
if (MediaRecorder.isTypeSupported(mimeType)) return mimeType
- } catch { }
+ } catch {}
}
return undefined
@@ -74,6 +66,8 @@ const RecordPlayback = ({ route }) => {
coverUrl: project?.coverUrl || null,
metadata: { projectId: project?.id, screen: 'RecordPlayback' },
})
+ const positionMs = player?.positionMs || 0
+ const durationMs = player?.durationMs || 0
// MediaStream / Recorder (web only)
const previewVideoRef = useRef(null)
@@ -96,31 +90,19 @@ const RecordPlayback = ({ route }) => {
const [countdown, setCountdown] = useState(0)
const [isPreparing, setIsPreparing] = useState(false)
const [isRecording, setIsRecording] = useState(false)
- const [isPaused, setIsPaused] = useState(false)
- const [pos, setPos] = useState(0)
- const [dur, setDur] = useState(0)
// timers / refs
- const isPausedRef = useRef(false)
- const progressTimerRef = useRef(null)
const listenTimerRef = useRef(null)
const countdownTimerRef = useRef(null)
- const perfStartRef = useRef(null)
- const pausedAtRef = useRef(null)
- const pausedMsRef = useRef(0)
const listenedMsRef = useRef(0)
const viewsIncrementedRef = useRef(false)
- const correctedInitialJumpRef = useRef(false)
const playbackStartedAtRef = useRef(null)
const recordingStartAtRef = useRef(null)
+ const stopInFlightRef = useRef(false)
useEffect(() => {
latestLoopingValueRef.current = isLooping ?? false
}, [isLooping])
- useEffect(() => {
- isPausedRef.current = isPaused
- }, [isPaused])
-
// Lyrics
const alignedWords = useMemo(() => {
const ts = project?.musicTimestamps?.[songIndex]
@@ -134,10 +116,9 @@ const RecordPlayback = ({ route }) => {
}, [project?.musicTimestamps, songIndex])
const clearAllTimers = () => {
- if (progressTimerRef.current) clearInterval(progressTimerRef.current)
if (listenTimerRef.current) clearInterval(listenTimerRef.current)
if (countdownTimerRef.current) clearInterval(countdownTimerRef.current)
- progressTimerRef.current = listenTimerRef.current = countdownTimerRef.current = null
+ listenTimerRef.current = countdownTimerRef.current = null
}
const clearMediaRetry = useCallback(() => {
@@ -150,19 +131,12 @@ const RecordPlayback = ({ route }) => {
const resetUI = () => {
setIsPreparing(false)
setIsRecording(false)
- setIsPaused(false)
- isPausedRef.current = false
- pausedAtRef.current = null
- pausedMsRef.current = 0
setCountdown(0)
- setPos(0)
- setDur(0)
listenedMsRef.current = 0
viewsIncrementedRef.current = false
- perfStartRef.current = null
- correctedInitialJumpRef.current = false
playbackStartedAtRef.current = null
recordingStartAtRef.current = null
+ stopInFlightRef.current = false
}
const releaseRecordingUrl = useCallback(() => {
@@ -191,14 +165,14 @@ const RecordPlayback = ({ route }) => {
if (video.srcObject !== stream) {
video.srcObject = stream
}
- } catch (e) { }
+ } catch (e) {}
try {
const playPromise = video.play?.()
if (playPromise && typeof playPromise.catch === 'function') {
- playPromise.catch(() => { })
+ playPromise.catch(() => {})
}
- } catch (e) { }
+ } catch (e) {}
return true
}, [])
@@ -271,16 +245,16 @@ const RecordPlayback = ({ route }) => {
[clearMediaRetry, ensureMediaStream]
)
- const startRecorder = useCallback(() => {
+ const startRecorder = useCallback(async () => {
if (!mediaStreamRef.current) {
- return false
+ return null
}
try {
if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') {
mediaRecorderRef.current.stop()
}
- } catch (e) { }
+ } catch (e) {}
recordedChunksRef.current = []
releaseRecordingUrl()
@@ -294,7 +268,7 @@ const RecordPlayback = ({ route }) => {
: new MediaRecorder(mediaStreamRef.current)
} catch (e) {
setMediaError(e instanceof Error ? e : new Error(String(e || '')))
- return false
+ return null
}
mediaRecorderRef.current = recorder
@@ -308,9 +282,21 @@ const RecordPlayback = ({ route }) => {
}
}
+ let resolveRecorderStart
+ const recorderStartedPromise = new Promise((resolve) => {
+ resolveRecorderStart = resolve
+ })
+
+ recorder.onstart = () => {
+ resolveRecorderStart?.(performance.now())
+ resolveRecorderStart = null
+ }
+
recorder.onerror = (event) => {
const err = event?.error || event
setMediaError(err instanceof Error ? err : new Error(String(err || '')))
+ resolveRecorderStart?.(null)
+ resolveRecorderStart = null
}
recorder.onstop = () => {
@@ -340,8 +326,10 @@ const RecordPlayback = ({ route }) => {
try {
recorder.start(1000)
- return true
+ return await recorderStartedPromise
} catch (e) {
+ resolveRecorderStart?.(null)
+ resolveRecorderStart = null
if (stopRecordingResolveRef.current) {
stopRecordingResolveRef.current(null)
stopRecordingResolveRef.current = null
@@ -349,7 +337,7 @@ const RecordPlayback = ({ route }) => {
stopRecordingPromiseRef.current = null
mediaRecorderRef.current = null
setMediaError(e instanceof Error ? e : new Error(String(e || '')))
- return false
+ return null
}
}, [releaseRecordingUrl, setMediaError])
@@ -366,7 +354,7 @@ const RecordPlayback = ({ route }) => {
}
recorder.stop()
}
- } catch (e) { }
+ } catch (e) {}
if (!waitPromise) {
return recordedUrlRef.current || null
@@ -390,10 +378,10 @@ const RecordPlayback = ({ route }) => {
try {
await player?.pause?.()
await player?.seekTo?.(0)
- } catch (e) { }
+ } catch (e) {}
try {
await stopRecorderAndGetUrl()
- } catch (e) { }
+ } catch (e) {}
if (!preserveRecordingForNextScreenRef.current) {
releaseRecordingUrl()
}
@@ -433,12 +421,12 @@ const RecordPlayback = ({ route }) => {
)
useEffect(() => {
- ; (async () => {
+ ;(async () => {
try {
if (!cameraPermission?.granted) {
await requestCameraPermission()
}
- } catch (e) { }
+ } catch (e) {}
})()
return () => {
clearAllTimers()
@@ -481,7 +469,7 @@ const RecordPlayback = ({ route }) => {
} else {
try {
video.srcObject = null
- } catch { }
+ } catch {}
}
}, [attachPreview, mediaReady, mediaError])
@@ -513,7 +501,7 @@ const RecordPlayback = ({ route }) => {
if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') {
mediaRecorderRef.current.stop()
}
- } catch { }
+ } catch {}
stopMediaStream()
clearMediaRetry()
if (!preserveRecordingForNextScreenRef.current) {
@@ -523,11 +511,13 @@ const RecordPlayback = ({ route }) => {
}, [clearMediaRetry, releaseRecordingUrl, preserveRecordingForNextScreenRef, stopMediaStream])
const stopAndNavigate = useCallback(async () => {
+ if (stopInFlightRef.current) return
+ stopInFlightRef.current = true
preserveRecordingForNextScreenRef.current = true
clearAllTimers()
try {
await player?.pause?.()
- } catch (e) { }
+ } catch (e) {}
let videoUrl = null
try {
@@ -535,13 +525,9 @@ const RecordPlayback = ({ route }) => {
if (!videoUrl) {
videoUrl = recordedUrlRef.current || null
}
- } catch (e) { }
+ } catch (e) {}
setIsRecording(false)
- setIsPaused(false)
- isPausedRef.current = false
- pausedAtRef.current = null
- pausedMsRef.current = 0
const rawOffsetMs =
playbackStartedAtRef.current != null && recordingStartAtRef.current != null
@@ -556,68 +542,10 @@ const RecordPlayback = ({ route }) => {
})
}, [player, project, stopRecorderAndGetUrl])
- const startProgressLoop = useCallback(() => {
- if (progressTimerRef.current) clearInterval(progressTimerRef.current)
-
- progressTimerRef.current = setInterval(async () => {
- const rawDur = toSeconds(player?.duration)
- const rawPos = toSeconds(player?.currentTime)
- const pausedSince =
- pausedAtRef.current != null ? Math.max(0, performance.now() - pausedAtRef.current) : 0
- const pausedTotal = pausedMsRef.current + pausedSince
-
- // fallback monotone si le player ne donne rien
- const fallback =
- perfStartRef.current != null
- ? Math.max(0, (performance.now() - perfStartRef.current - pausedTotal) / 1000)
- : 0
-
- let nextDur = rawDur > 0 ? rawDur : dur || 0
- let nextPos = Number.isFinite(rawPos) && rawPos >= 0 ? rawPos : fallback > 0 ? fallback : 0
-
- // 🔧 Correction 1-shot du "jump initial" (ex: 0 → 93s au 1er tick)
- if (!correctedInitialJumpRef.current && nextPos > 1 && fallback < 1.5) {
- try {
- await player?.seekTo?.(0)
- correctedInitialJumpRef.current = true
- nextPos = 0
- } catch (e) { }
- }
-
- const timeSinceStart =
- perfStartRef.current != null
- ? Math.max(0, performance.now() - perfStartRef.current - pausedTotal)
- : null
-
- const shouldTrustFallback =
- fallback > 0 &&
- ((nextDur > 0 && nextPos > nextDur + 0.5) ||
- nextPos - fallback > 1.5 ||
- (timeSinceStart != null && timeSinceStart < 5000 && nextPos > fallback + 1))
-
- if (shouldTrustFallback) {
- nextPos = fallback
- }
-
- if (nextDur > 0 && nextPos > nextDur) nextPos = nextDur
-
- 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
- }
-
- if (nextDur > 0 && nextPos >= nextDur - 0.3) {
- void stopAndNavigate()
- }
- }, 250)
- }, [player, stopAndNavigate])
+ useEffect(() => {
+ if (!isRecording || !durationMs || positionMs < durationMs - 300) return
+ void stopAndNavigate()
+ }, [durationMs, isRecording, positionMs, stopAndNavigate])
const startListenLoop = useCallback(() => {
if (listenTimerRef.current) clearInterval(listenTimerRef.current)
@@ -630,39 +558,31 @@ const RecordPlayback = ({ route }) => {
if (project?.id) {
try {
await projectsRef.doc(project.id).set({ views: increment(1) }, { merge: true })
- } catch (e) { }
+ } catch (e) {}
}
}
}
- } catch (e) { }
+ } catch (e) {}
}, 500)
}, [player, project?.id])
const startPlayback = useCallback(async () => {
try {
- setIsPaused(false)
- isPausedRef.current = false
- pausedAtRef.current = null
- 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 player?.pause?.()
+ await player?.seekTo?.(0)
+ const recordingStartedAt = await startRecorder()
+ if (recordingStartedAt == null) {
+ throw new Error("Impossible de démarrer l'enregistrement vidéo")
}
- 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
+ recordingStartAtRef.current = recordingStartedAt
+ await player?.play?.()
+ playbackStartedAtRef.current = performance.now()
setIsRecording(true)
- startProgressLoop()
startListenLoop()
} catch (e) {
setIsRecording(false)
setIsPreparing(false)
- setIsPaused(false)
- isPausedRef.current = false
- pausedAtRef.current = null
+ stopInFlightRef.current = false
const msg = String(e?.message || e || '')
if (/not supported on the simulator/i.test(msg)) {
alert(
@@ -674,7 +594,7 @@ const RecordPlayback = ({ route }) => {
onPress: () => {
try {
goBack()
- } catch { }
+ } catch {}
},
},
],
@@ -682,7 +602,7 @@ const RecordPlayback = ({ route }) => {
)
}
}
- }, [player, startRecorder, startProgressLoop, startListenLoop])
+ }, [player, startRecorder, startListenLoop])
const permissionsGranted = !!cameraPermission?.granted
const canRecord = permissionsGranted && mediaReady && !mediaError
@@ -690,8 +610,6 @@ const RecordPlayback = ({ route }) => {
const startCountdownThenRecord = useCallback(async () => {
if (!songUrl || !canRecord) return
await resetSession()
- setIsPaused(false)
- isPausedRef.current = false
setIsPreparing(true)
setCountdown(COUNTDOWN_SECONDS)
if (countdownTimerRef.current) clearInterval(countdownTimerRef.current)
@@ -710,50 +628,13 @@ const RecordPlayback = ({ route }) => {
}, 1000)
}, [songUrl, canRecord, resetSession, startPlayback])
- const handleTogglePause = useCallback(async () => {
- try {
- if (!isRecording) return
- if (!isPausedRef.current) {
- isPausedRef.current = true
- setIsPaused(true)
- pausedAtRef.current = performance.now()
- try {
- await player?.pause?.()
- } catch (e) { }
- try {
- const recorder = mediaRecorderRef.current
- if (recorder && recorder.state === 'recording') {
- recorder.pause?.()
- }
- } catch (e) { }
- return
- }
- const pausedAt = pausedAtRef.current
- if (pausedAt != null) {
- pausedMsRef.current += performance.now() - pausedAt
- pausedAtRef.current = null
- }
- isPausedRef.current = false
- setIsPaused(false)
- try {
- const recorder = mediaRecorderRef.current
- if (recorder && recorder.state === 'paused') {
- recorder.resume?.()
- }
- } catch (e) { }
- try {
- await player?.play?.()
- } catch (e) { }
- } catch (e) { }
- }, [isRecording, player])
-
const handleRestartRecording = useCallback(async () => {
try {
await startCountdownThenRecord()
- } catch (e) { }
+ } catch (e) {}
}, [startCountdownThenRecord])
- const progressRatio = dur > 0 ? Math.min(1, pos / dur) : 0
+ const progressRatio = durationMs > 0 ? Math.min(1, positionMs / durationMs) : 0
return (
{
// containerStyle={{ backgroundColor: Palette.gray }}
headerType="NONE"
backgroundColor={Palette.grayMid}
- // backgroundImg={background.playbackBG2}
+ // backgroundImg={background.playbackBG2}
>
@@ -793,9 +674,9 @@ const RecordPlayback = ({ route }) => {
const video = event?.currentTarget
const playPromise = video?.play?.()
if (playPromise && typeof playPromise.catch === 'function') {
- playPromise.catch(() => { })
+ playPromise.catch(() => {})
}
- } catch { }
+ } catch {}
}}
/>
@@ -941,34 +822,6 @@ const RecordPlayback = ({ route }) => {
- {isRecording && (
-
-
- {isPaused ? 'Reprendre' : 'Mettre en pause'}
-
-
- )}
)}
@@ -988,7 +841,7 @@ const RecordPlayback = ({ route }) => {
onPress={async () => {
try {
if (!cameraPermission?.granted) await requestCameraPermission()
- } catch { }
+ } catch {}
}}
/>
@@ -1010,8 +863,7 @@ const RecordPlayback = ({ route }) => {
{isRecording && alignedWords.length > 0 ? (
{
[audioPlayer, videoPlayer]
)
+ const syncVideoToAudio = useCallback(() => {
+ if (!videoPlayer) return
+ const rawAudioMs = getCurrentRawAudioPositionMs()
+ const timelineMs = getTimelinePositionMs(rawAudioMs, syncOffsetRef.current)
+ const expectedVideoMs = getVideoPositionMs(timelineMs, syncOffsetRef.current)
+ videoPlayer.currentTime = expectedVideoMs / 1000
+ }, [getCurrentRawAudioPositionMs, videoPlayer])
+
+ const playSynchronized = useCallback(async () => {
+ if (audioPlayer && songUrl) await audioPlayer.play?.()
+ if (videoPlayer) videoPlayer.play()
+ syncVideoToAudio()
+ }, [audioPlayer, songUrl, syncVideoToAudio, videoPlayer])
+
const stopPlayback = useCallback(async () => {
try {
if (audioPlayer) await audioPlayer.pause?.()
@@ -104,8 +118,7 @@ const RecordedPlayback = ({ route }) => {
const start = async () => {
try {
await alignPlaybackToTimeline(0, initialSyncOffsetMs)
- if (audioPlayer && songUrl) await audioPlayer.play?.()
- if (videoPlayer) videoPlayer.play()
+ await playSynchronized()
} catch (e) {}
}
@@ -115,7 +128,7 @@ const RecordedPlayback = ({ route }) => {
playbackEndedRef.current = false
void stopPlayback()
}
- }, [alignPlaybackToTimeline, audioPlayer, initialSyncOffsetMs, songUrl, stopPlayback, videoPlayer])
+ }, [alignPlaybackToTimeline, initialSyncOffsetMs, playSynchronized, stopPlayback])
useEffect(() => {
const id = global.setInterval(() => {
@@ -139,7 +152,7 @@ const RecordedPlayback = ({ route }) => {
const currentVideoMs = (videoPlayer.currentTime || 0) * 1000
const driftMs = Math.abs(currentVideoMs - expectedVideoMs)
- if (driftMs > 350) {
+ if (driftMs > 120) {
videoPlayer.currentTime = Math.max(0, expectedVideoMs / 1000)
}
}
@@ -175,16 +188,9 @@ const RecordedPlayback = ({ route }) => {
const resumeAfterSeek = useCallback(async () => {
try {
- if (audioPlayer?.resume) {
- await audioPlayer.resume?.()
- } else if (audioPlayer) {
- await audioPlayer.play?.()
- }
+ await playSynchronized()
} catch (e) {}
- try {
- if (videoPlayer) videoPlayer.play()
- } catch (e) {}
- }, [audioPlayer, videoPlayer])
+ }, [playSynchronized])
const handleSyncOffsetChange = useCallback(
(nextOffsetMs) => {
@@ -261,23 +267,14 @@ const RecordedPlayback = ({ route }) => {
playbackEndedRef.current = false
- if (songUrl && audioPlayer) {
- if (audioPlayer?.resume) {
- await audioPlayer.resume?.()
- } else {
- await audioPlayer.play?.()
- }
- }
-
- if (videoPlayer) videoPlayer.play()
+ await playSynchronized()
} catch (e) {}
- }, [alignPlaybackToTimeline, audioPlayer, progressInfo, songUrl, videoPlayer])
+ }, [alignPlaybackToTimeline, audioPlayer, playSynchronized, progressInfo, videoPlayer])
return (
-
{/* Vidéo avec play/pause en overlay */}
{
Synchro audio / vidéo
-
+
handleSyncOffsetChange(clampSyncOffsetMs(currentSyncOffsetMs - 100))}
disabled={!songUrl || currentSyncOffsetMs <= -2000}
@@ -398,8 +402,8 @@ const RecordedPlayback = ({ route }) => {
{currentSyncOffsetMs === 0
? 'ms · synchronisé'
: currentSyncOffsetMs > 0
- ? 'ms · son en avance'
- : 'ms · son en retard'}
+ ? 'ms · son en avance'
+ : 'ms · son en retard'}
@@ -433,8 +437,8 @@ const RecordedPlayback = ({ route }) => {
!songUrl || currentSyncOffsetMs === 0
? 'rgba(255,255,255,0.06)'
: pressed
- ? 'rgba(255,255,255,0.18)'
- : 'rgba(255,255,255,0.1)',
+ ? 'rgba(255,255,255,0.18)'
+ : 'rgba(255,255,255,0.1)',
opacity: !songUrl || currentSyncOffsetMs === 0 ? 0.4 : 1,
})}
>
diff --git a/src/screens/Playback/RecordedPlayback.web.js b/src/screens/Playback/RecordedPlayback.web.js
index 7849d01..e51379f 100644
--- a/src/screens/Playback/RecordedPlayback.web.js
+++ b/src/screens/Playback/RecordedPlayback.web.js
@@ -78,6 +78,24 @@ const RecordedPlayback = ({ route }) => {
[audioPlayer, videoUri]
)
+ const syncVideoToAudio = useCallback(() => {
+ const video = videoElRef.current
+ if (!video || !videoUri) return
+ const rawAudioMs = getCurrentRawAudioPositionMs()
+ const timelineMs = getTimelinePositionMs(rawAudioMs, syncOffsetRef.current)
+ const expectedVideoMs = getVideoPositionMs(timelineMs, syncOffsetRef.current)
+ video.currentTime = expectedVideoMs / 1000
+ }, [getCurrentRawAudioPositionMs, videoUri])
+
+ const playSynchronized = useCallback(async () => {
+ if (audioPlayer && songUrl) await audioPlayer.play?.()
+ if (videoElRef.current && videoUri) {
+ videoElRef.current.muted = true
+ await videoElRef.current.play().catch(() => {})
+ }
+ syncVideoToAudio()
+ }, [audioPlayer, songUrl, syncVideoToAudio, videoUri])
+
const stopPlayback = useCallback(async () => {
try {
if (audioPlayer) await audioPlayer.pause?.()
@@ -113,15 +131,7 @@ const RecordedPlayback = ({ route }) => {
const start = async () => {
try {
await alignPlaybackToTimeline(0, initialSyncOffsetMs)
-
- if (audioPlayer && songUrl) {
- await audioPlayer.play?.()
- }
-
- if (videoElRef.current && videoUri) {
- videoElRef.current.muted = true
- videoElRef.current.play().catch(() => {})
- }
+ await playSynchronized()
} catch {}
}
@@ -131,7 +141,7 @@ const RecordedPlayback = ({ route }) => {
playbackEndedRef.current = false
void stopPlayback()
}
- }, [alignPlaybackToTimeline, audioPlayer, initialSyncOffsetMs, songUrl, stopPlayback, videoUri])
+ }, [alignPlaybackToTimeline, initialSyncOffsetMs, playSynchronized, stopPlayback])
useEffect(() => {
const id = setInterval(() => {
@@ -156,7 +166,7 @@ const RecordedPlayback = ({ route }) => {
const currentVideoMs = Number(videoElRef.current.currentTime || 0) * 1000
const driftMs = Math.abs(currentVideoMs - expectedVideoMs)
- if (driftMs > 350) {
+ if (driftMs > 120) {
videoElRef.current.currentTime = Math.max(0, expectedVideoMs / 1000)
}
}
@@ -194,19 +204,9 @@ const RecordedPlayback = ({ route }) => {
const resumeAfterSeek = useCallback(async () => {
try {
- if (audioPlayer?.resume) {
- await audioPlayer.resume?.()
- } else if (audioPlayer) {
- await audioPlayer.play?.()
- }
+ await playSynchronized()
} catch {}
- try {
- if (videoElRef.current && videoUri) {
- videoElRef.current.muted = true
- videoElRef.current.play().catch(() => {})
- }
- } catch {}
- }, [audioPlayer, videoUri])
+ }, [playSynchronized])
const handleSyncOffsetChange = useCallback(
(nextOffsetMs) => {
@@ -238,7 +238,12 @@ const RecordedPlayback = ({ route }) => {
void alignPlaybackToTimeline(nextTimelineMs, safeOffsetMs)
},
- [alignPlaybackToTimeline, audioPlayer?.playing, getCurrentRawAudioDurationMs, getCurrentRawAudioPositionMs]
+ [
+ alignPlaybackToTimeline,
+ audioPlayer?.playing,
+ getCurrentRawAudioDurationMs,
+ getCurrentRawAudioPositionMs,
+ ]
)
useEffect(() => {
@@ -278,20 +283,9 @@ const RecordedPlayback = ({ route }) => {
playbackEndedRef.current = false
- if (songUrl && audioPlayer) {
- if (audioPlayer?.resume) {
- await audioPlayer.resume?.()
- } else {
- await audioPlayer.play?.()
- }
- }
-
- if (videoElRef.current && videoUri) {
- videoElRef.current.muted = true
- videoElRef.current.play().catch(() => {})
- }
+ await playSynchronized()
} catch {}
- }, [alignPlaybackToTimeline, audioPlayer, progressInfo, songUrl, stopPlayback, videoUri])
+ }, [alignPlaybackToTimeline, audioPlayer, playSynchronized, progressInfo, stopPlayback])
return (
{
Synchro audio / vidéo
-
+
handleSyncOffsetChange(clampSyncOffsetMs(currentSyncOffsetMs - 100))}
disabled={!songUrl || currentSyncOffsetMs <= -2000}
@@ -454,8 +455,8 @@ const RecordedPlayback = ({ route }) => {
{currentSyncOffsetMs === 0
? 'ms · synchronisé'
: currentSyncOffsetMs > 0
- ? 'ms · son en avance'
- : 'ms · son en retard'}
+ ? 'ms · son en avance'
+ : 'ms · son en retard'}
@@ -489,8 +490,8 @@ const RecordedPlayback = ({ route }) => {
!songUrl || currentSyncOffsetMs === 0
? 'rgba(255,255,255,0.06)'
: pressed
- ? 'rgba(255,255,255,0.18)'
- : 'rgba(255,255,255,0.1)',
+ ? 'rgba(255,255,255,0.18)'
+ : 'rgba(255,255,255,0.1)',
opacity: !songUrl || currentSyncOffsetMs === 0 ? 0.4 : 1,
cursor: !songUrl || currentSyncOffsetMs === 0 ? 'not-allowed' : 'pointer',
})}
diff --git a/src/screens/Playbacks/components/PlaybackItem.web.js b/src/screens/Playbacks/components/PlaybackItem.web.js
index 76f3d7e..15b3769 100644
--- a/src/screens/Playbacks/components/PlaybackItem.web.js
+++ b/src/screens/Playbacks/components/PlaybackItem.web.js
@@ -19,7 +19,6 @@ import { Feather } from '@expo/vector-icons'
// Debug logging toggle for web playback
const DEBUG_PLAYBACK_WEB = true
-const PLAYBACK_LYRICS_LEAD_S = 0.18
// NOTE: Web-only implementation that uses a native