feat: fix timestamps

This commit is contained in:
2026-03-04 15:07:43 +01:00
parent d4fd96ce03
commit 8ad410d8a5
6 changed files with 212 additions and 50 deletions
+56 -9
View File
@@ -18,6 +18,7 @@ import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import trimLeadingDuplicateSection from '../../utils/trimLeadingDuplicateSection'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
const TIME_BEFORE_INCREMENT_MS = 20000 // 20s
@@ -64,6 +65,9 @@ const RecordPlayback = ({ route }) => {
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 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)
@@ -132,9 +136,14 @@ const RecordPlayback = ({ route }) => {
const alignedWords = useMemo(() => {
const ts = project?.musicTimestamps?.[musicIndex]
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : []
return arr.map((w) => ({
const rawAlignedWords = Array.isArray(ts?.alignedWords) ? ts.alignedWords : []
const trimmedAlignedWords = trimLeadingDuplicateSection(rawAlignedWords)
if (trimmedAlignedWords.length !== rawAlignedWords.length) {
log('Trimmed duplicated leading lyrics section', {
removedWords: rawAlignedWords.length - trimmedAlignedWords.length,
})
}
return trimmedAlignedWords.map((w) => ({
word: String(w?.word ?? ''),
startS: Number(w?.startS ?? 0),
endS: Number(w?.endS ?? 0),
@@ -194,6 +203,10 @@ const RecordPlayback = ({ route }) => {
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])
@@ -202,18 +215,43 @@ const RecordPlayback = ({ route }) => {
if (!player) return
const id = setInterval(() => {
try {
const now = Date.now()
const dur = (player?.duration || 0) * 1000
const pos = (player?.currentTime || 0) * 1000
if (!playbackStartedRef.current && (player?.playing || pos > 0)) {
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 now = Date.now()
const estimatedStartAt = pos > 0 ? now - pos : now
playbackStartedAtRef.current = estimatedStartAt
log('Playback detected', { pos, dur, startedAt: estimatedStartAt })
log('Playback detected', {
pos,
dur,
startedAt: estimatedStartAt,
reason: advancing ? 'advancing' : 'position',
})
}
setProgressInfo((prev) => {
if (prev.pos === pos && prev.dur === dur) return prev
return { pos, dur }
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) {
@@ -317,6 +355,10 @@ 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')
@@ -474,6 +516,10 @@ const RecordPlayback = ({ route }) => {
await cameraRef.current?.resumePreview?.()
} 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 }
log('startRecordingWithMusic', {
hasPlayer: !!player,
@@ -556,6 +602,7 @@ const RecordPlayback = ({ route }) => {
await new Promise(r => setTimeout(r, CAMERA_STARTUP_DELAY_MS))
try {
playbackStartRequestAtRef.current = Date.now()
await player.play?.()
log('player.play invoked')
} catch (error) {
@@ -605,7 +652,7 @@ const RecordPlayback = ({ route }) => {
}
const duration = (player?.duration || 0) * 1000
const currentTime = (player?.currentTime || 0) * 1000
if (!playbackStartedRef.current && (player?.playing || currentTime > 0)) {
if (!playbackStartedRef.current && player?.playing) {
playbackStartedRef.current = true
}
const nearEnd = duration > 0 && currentTime >= duration - 600
+4 -2
View File
@@ -20,6 +20,7 @@ import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
import { background } from '../../assets'
import RestartSpinnerIcon from '../../assets/UI/RestartSpinnerIcon'
import { registerBlobUrl, releaseBlobUrl } from '../../utils/blobUrlCache'
import trimLeadingDuplicateSection from '../../utils/trimLeadingDuplicateSection'
const TIME_BEFORE_INCREMENT_MS = 20000
const COUNTDOWN_SECONDS = __DEV__ ? 1 : 10
@@ -123,8 +124,9 @@ const RecordPlayback = ({ route }) => {
// Lyrics
const alignedWords = useMemo(() => {
const ts = project?.musicTimestamps?.[songIndex]
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : []
return arr.map((w) => ({
const rawAlignedWords = Array.isArray(ts?.alignedWords) ? ts.alignedWords : []
const trimmedAlignedWords = trimLeadingDuplicateSection(rawAlignedWords)
return trimmedAlignedWords.map((w) => ({
word: String(w?.word ?? ''),
startS: Number(w?.startS ?? 0),
endS: Number(w?.endS ?? 0),
+6 -1
View File
@@ -16,6 +16,7 @@ import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
const RecordedPlayback = ({ route }) => {
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,
@@ -127,7 +128,11 @@ const RecordedPlayback = ({ route }) => {
const resumeAfterSeek = useCallback(async () => {
try {
if (audioPlayer) await audioPlayer.play?.()
if (audioPlayer?.resume) {
await audioPlayer.resume?.()
} else if (audioPlayer) {
await audioPlayer.play?.()
}
} catch (e) { }
try {
if (videoPlayer) videoPlayer.play()
+21 -37
View File
@@ -354,7 +354,6 @@ const SongOptionCard = ({
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 })
const [isPlaying, setIsPlaying] = useState(false)
const shouldResumeAfterSeekRef = useRef(false)
useEffect(() => {
registerPlayer(index, player)
@@ -402,27 +401,6 @@ const SongOptionCard = ({
return () => clearInterval(id)
}, [player])
const hasCapturedSeekStateRef = useRef(false)
const resumePlaybackIfNeeded = useCallback(async () => {
if (!player) return
const shouldResume = shouldResumeAfterSeekRef.current
shouldResumeAfterSeekRef.current = false
hasCapturedSeekStateRef.current = false // Reset capture flag
if (!shouldResume) return
try {
if (player.resume) {
await player.resume?.()
} else {
await player.play?.()
}
} catch (error) {
console.log('SongReady resume after seek', error?.message)
}
}, [player])
const handleSeek = useCallback(
async (targetMs) => {
if (!player || !progressInfo?.dur) {
@@ -443,26 +421,31 @@ const SongOptionCard = ({
[player, progressInfo?.dur]
)
const handleSeekStart = useCallback(async () => {
const handleSeekStart = useCallback(() => {
onSelect(index)
}, [index, onSelect])
const handlePause = useCallback(async () => {
if (!player) return
const isCurrentlyPlaying = !!player.playing || isPlaying
// Only capture state if we haven't already for this drag interaction
if (!hasCapturedSeekStateRef.current) {
hasCapturedSeekStateRef.current = true
shouldResumeAfterSeekRef.current = isCurrentlyPlaying
}
try {
if (isCurrentlyPlaying) {
await player.pause?.()
await player.pause?.()
} catch (error) {
console.log('SongReady pause on seek', error?.message)
}
}, [player])
const handlePlay = useCallback(async () => {
if (!player) return
try {
if (player.resume) {
await player.resume?.()
} else {
await player.play?.()
}
} catch (error) {
console.log('SongReady pause on seek start', error?.message)
console.log('SongReady resume on seek', error?.message)
}
}, [index, isPlaying, onSelect, player])
}, [player])
const handleToggle = () => {
onSelect(index)
@@ -521,7 +504,8 @@ const SongOptionCard = ({
isPlaying={isPlaying}
onSeekStart={handleSeekStart}
onSeek={handleSeek}
onSeekEnd={resumePlaybackIfNeeded}
onPause={handlePause}
onPlay={handlePlay}
disabled={!url}
/>
</View>