feat: synchronise audio and video playback
This commit is contained in:
@@ -128,7 +128,6 @@ const alphaWhite = (alpha) => `rgba(255,255,255,${alpha})`
|
|||||||
export default function KaraokeLyrics({
|
export default function KaraokeLyrics({
|
||||||
alignedWords = [],
|
alignedWords = [],
|
||||||
currentTimeS = 0,
|
currentTimeS = 0,
|
||||||
timeOffsetS = 0,
|
|
||||||
showContext = true,
|
showContext = true,
|
||||||
removeTags = true,
|
removeTags = true,
|
||||||
mode = 'focus',
|
mode = 'focus',
|
||||||
@@ -148,10 +147,7 @@ export default function KaraokeLyrics({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const activeLines = mode === 'teleprompter' ? teleprompterData : lines
|
const activeLines = mode === 'teleprompter' ? teleprompterData : lines
|
||||||
const effectiveTimeS = useMemo(
|
const effectiveTimeS = Math.max(0, Number(currentTimeS || 0))
|
||||||
() => Math.max(0, Number(currentTimeS || 0) + Number(timeOffsetS || 0)),
|
|
||||||
[currentTimeS, timeOffsetS]
|
|
||||||
)
|
|
||||||
|
|
||||||
const currentLineIdx = useMemo(() => {
|
const currentLineIdx = useMemo(() => {
|
||||||
return findFocusedLineIndex(activeLines, effectiveTimeS)
|
return findFocusedLineIndex(activeLines, effectiveTimeS)
|
||||||
|
|||||||
@@ -69,13 +69,14 @@ const useSharedAudioPlayer = (source, options = null) => {
|
|||||||
durationMs: 0,
|
durationMs: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
// 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 = {
|
stateRef.current = {
|
||||||
isPlaying,
|
isPlaying,
|
||||||
positionMs,
|
positionMs,
|
||||||
durationMs,
|
durationMs,
|
||||||
}
|
}
|
||||||
}, [isPlaying, positionMs, durationMs])
|
|
||||||
|
|
||||||
const controlsRef = useRef({
|
const controlsRef = useRef({
|
||||||
play,
|
play,
|
||||||
@@ -135,6 +136,14 @@ const useSharedAudioPlayer = (source, options = null) => {
|
|||||||
enumerable: true,
|
enumerable: true,
|
||||||
get: () => stateRef.current.positionMs / 1000,
|
get: () => stateRef.current.positionMs / 1000,
|
||||||
},
|
},
|
||||||
|
positionMs: {
|
||||||
|
enumerable: true,
|
||||||
|
get: () => stateRef.current.positionMs,
|
||||||
|
},
|
||||||
|
durationMs: {
|
||||||
|
enumerable: true,
|
||||||
|
get: () => stateRef.current.durationMs,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
player.load = async ({ startPositionMs = 0 } = {}) => {
|
player.load = async ({ startPositionMs = 0 } = {}) => {
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ 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 CAMERA_STARTUP_DELAY_MS = 250
|
||||||
const KARAOKE_LEAD_S = 0.68
|
|
||||||
const LOG_PREFIX = '[RecordPlayback]'
|
const LOG_PREFIX = '[RecordPlayback]'
|
||||||
const KEEP_AWAKE_TAG = 'record-playback'
|
const KEEP_AWAKE_TAG = 'record-playback'
|
||||||
const log =
|
const log =
|
||||||
@@ -53,7 +52,6 @@ const RecordPlayback = ({ route }) => {
|
|||||||
const countdownTimerRef = useRef(null)
|
const countdownTimerRef = useRef(null)
|
||||||
const listenTimerRef = useRef(null)
|
const listenTimerRef = useRef(null)
|
||||||
const checkSongEndRef = useRef(null)
|
const checkSongEndRef = useRef(null)
|
||||||
const isPausedRef = useRef(false)
|
|
||||||
const stopRequestedRef = useRef(false)
|
const stopRequestedRef = useRef(false)
|
||||||
const restartRequestedRef = useRef(false)
|
const restartRequestedRef = useRef(false)
|
||||||
const recordingStartTimeRef = useRef(0) // Track recording start time for duration check
|
const recordingStartTimeRef = useRef(0) // Track recording start time for duration check
|
||||||
@@ -63,12 +61,8 @@ const RecordPlayback = ({ route }) => {
|
|||||||
const exitRequestedRef = useRef(false)
|
const exitRequestedRef = useRef(false)
|
||||||
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)
|
||||||
const playbackStartedAtRef = useRef(0)
|
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 originalLoopingValueRef = useRef({ hasValue: false, value: false })
|
||||||
const latestLoopingValueRef = useRef(isLooping ?? false)
|
const latestLoopingValueRef = useRef(isLooping ?? false)
|
||||||
|
|
||||||
@@ -80,9 +74,7 @@ const RecordPlayback = ({ route }) => {
|
|||||||
const [isPreparing, setIsPreparing] = useState(false)
|
const [isPreparing, setIsPreparing] = useState(false)
|
||||||
const [countdown, setCountdown] = useState(0)
|
const [countdown, setCountdown] = useState(0)
|
||||||
const [isRecording, setIsRecording] = useState(false)
|
const [isRecording, setIsRecording] = useState(false)
|
||||||
const [isPaused, setIsPaused] = useState(false)
|
|
||||||
const [showProgress, setShowProgress] = useState(false)
|
const [showProgress, setShowProgress] = useState(false)
|
||||||
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 })
|
|
||||||
const [cameraFacing, setCameraFacing] = useState('front')
|
const [cameraFacing, setCameraFacing] = useState('front')
|
||||||
|
|
||||||
// Musique
|
// Musique
|
||||||
@@ -103,10 +95,6 @@ const RecordPlayback = ({ route }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
latestLoopingValueRef.current = isLooping ?? false
|
latestLoopingValueRef.current = isLooping ?? false
|
||||||
}, [isLooping])
|
}, [isLooping])
|
||||||
useEffect(() => {
|
|
||||||
isPausedRef.current = isPaused
|
|
||||||
}, [isPaused])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const shouldKeepAwake = isPreparing || isRecording
|
const shouldKeepAwake = isPreparing || isRecording
|
||||||
const updateKeepAwake = async () => {
|
const updateKeepAwake = async () => {
|
||||||
@@ -150,150 +138,17 @@ const RecordPlayback = ({ route }) => {
|
|||||||
}))
|
}))
|
||||||
}, [project?.musicTimestamps, musicIndex])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
listenedMsRef.current = 0
|
listenedMsRef.current = 0
|
||||||
incrementDoneRef.current = false
|
incrementDoneRef.current = false
|
||||||
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
|
|
||||||
playbackStartedRef.current = false
|
playbackStartedRef.current = false
|
||||||
playbackStartedAtRef.current = 0
|
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 })
|
log('Song URL changed, reset counters', { songUrl })
|
||||||
}, [songUrl])
|
}, [songUrl])
|
||||||
|
|
||||||
// Poll player -> progress ring
|
// The audio player is the single clock for the progress ring and lyrics.
|
||||||
useEffect(() => {
|
const positionMs = player?.positionMs || 0
|
||||||
if (!player) return
|
const durationMs = player?.durationMs || 0
|
||||||
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])
|
|
||||||
|
|
||||||
const getScrollY = () => (typeof window !== 'undefined' ? window.scrollY : 0)
|
const getScrollY = () => (typeof window !== 'undefined' ? window.scrollY : 0)
|
||||||
|
|
||||||
@@ -355,17 +210,10 @@ const RecordPlayback = ({ route }) => {
|
|||||||
incrementDoneRef.current = false
|
incrementDoneRef.current = false
|
||||||
playbackStartedRef.current = false
|
playbackStartedRef.current = false
|
||||||
playbackStartedAtRef.current = 0
|
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')
|
log('Session reset')
|
||||||
|
|
||||||
setIsPreparing(false)
|
setIsPreparing(false)
|
||||||
setIsRecording(false)
|
setIsRecording(false)
|
||||||
setIsPaused(false)
|
|
||||||
isPausedRef.current = false
|
|
||||||
setShowProgress(false)
|
setShowProgress(false)
|
||||||
setCountdown(0)
|
setCountdown(0)
|
||||||
try {
|
try {
|
||||||
@@ -456,8 +304,6 @@ const RecordPlayback = ({ route }) => {
|
|||||||
preserveSongEndWatcher,
|
preserveSongEndWatcher,
|
||||||
preserveStopRequest,
|
preserveStopRequest,
|
||||||
})
|
})
|
||||||
setIsPaused(false)
|
|
||||||
isPausedRef.current = false
|
|
||||||
if (!preserveRestartFlag) {
|
if (!preserveRestartFlag) {
|
||||||
restartRequestedRef.current = false
|
restartRequestedRef.current = false
|
||||||
manualRestartInFlightRef.current = false
|
manualRestartInFlightRef.current = false
|
||||||
@@ -504,23 +350,16 @@ const RecordPlayback = ({ route }) => {
|
|||||||
try {
|
try {
|
||||||
stopRequestedRef.current = false
|
stopRequestedRef.current = false
|
||||||
stopRequestedAtRef.current = 0
|
stopRequestedAtRef.current = 0
|
||||||
recordingStartTimeRef.current = Date.now() // Start timer
|
|
||||||
listenedMsRef.current = 0
|
listenedMsRef.current = 0
|
||||||
incrementDoneRef.current = false
|
incrementDoneRef.current = false
|
||||||
|
|
||||||
setIsRecording(true)
|
setIsRecording(true)
|
||||||
setShowProgress(true)
|
setShowProgress(true)
|
||||||
setIsPaused(false)
|
|
||||||
isPausedRef.current = false
|
|
||||||
try {
|
try {
|
||||||
await cameraRef.current?.resumePreview?.()
|
await cameraRef.current?.resumePreview?.()
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
playbackStartedRef.current = false
|
playbackStartedRef.current = false
|
||||||
playbackProbeRef.current = { lastPos: 0, lastAt: 0 }
|
playbackStartedAtRef.current = 0
|
||||||
playbackStartRequestAtRef.current = 0
|
|
||||||
playbackStartWarnedRef.current = false
|
|
||||||
setProgressInfo({ pos: 0, dur: 0 })
|
|
||||||
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
|
|
||||||
log('startRecordingWithMusic', {
|
log('startRecordingWithMusic', {
|
||||||
hasPlayer: !!player,
|
hasPlayer: !!player,
|
||||||
songUrl,
|
songUrl,
|
||||||
@@ -586,6 +425,7 @@ const RecordPlayback = ({ route }) => {
|
|||||||
|
|
||||||
throw new Error("L'enregistrement vidéo n'est pas supporté sur cet appareil.")
|
throw new Error("L'enregistrement vidéo n'est pas supporté sur cet appareil.")
|
||||||
})()
|
})()
|
||||||
|
recordingStartTimeRef.current = Date.now()
|
||||||
activeRecordingPromiseRef.current = recordPromise
|
activeRecordingPromiseRef.current = recordPromise
|
||||||
|
|
||||||
if (player && songUrl) {
|
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)
|
// Give the native recorder a short warm-up, then measure the real gap
|
||||||
await new Promise(r => setTimeout(r, CAMERA_STARTUP_DELAY_MS))
|
// between the recording request and the audio start for the final trim.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, CAMERA_STARTUP_DELAY_MS))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
playbackStartRequestAtRef.current = Date.now()
|
|
||||||
await player.play?.()
|
await player.play?.()
|
||||||
|
playbackStartedAtRef.current = Date.now()
|
||||||
|
playbackStartedRef.current = true
|
||||||
log('player.play invoked')
|
log('player.play invoked')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log('player.play failed', {
|
log('player.play failed', {
|
||||||
@@ -638,7 +480,6 @@ const RecordPlayback = ({ route }) => {
|
|||||||
checkSongEndRef.current = setInterval(() => {
|
checkSongEndRef.current = setInterval(() => {
|
||||||
try {
|
try {
|
||||||
if (!player) return
|
if (!player) return
|
||||||
if (isPausedRef.current) return
|
|
||||||
if (stopRequestedRef.current) {
|
if (stopRequestedRef.current) {
|
||||||
const sinceLastRequest = Date.now() - (stopRequestedAtRef.current || 0)
|
const sinceLastRequest = Date.now() - (stopRequestedAtRef.current || 0)
|
||||||
if (sinceLastRequest >= 1200) {
|
if (sinceLastRequest >= 1200) {
|
||||||
@@ -699,12 +540,9 @@ const RecordPlayback = ({ route }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsRecording(false)
|
setIsRecording(false)
|
||||||
setIsPaused(false)
|
|
||||||
isPausedRef.current = false
|
|
||||||
setShowProgress(false)
|
setShowProgress(false)
|
||||||
log('Recording flow completed', { hasVideo: !!video?.uri })
|
log('Recording flow completed', { hasVideo: !!video?.uri })
|
||||||
playbackStartedRef.current = false
|
playbackStartedRef.current = false
|
||||||
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
|
|
||||||
const exitRequested = exitRequestedRef.current
|
const exitRequested = exitRequestedRef.current
|
||||||
const shouldRestart = restartRequestedRef.current
|
const shouldRestart = restartRequestedRef.current
|
||||||
// Duration check logic
|
// Duration check logic
|
||||||
@@ -782,8 +620,6 @@ const RecordPlayback = ({ route }) => {
|
|||||||
stopRequestedAtRef.current = 0
|
stopRequestedAtRef.current = 0
|
||||||
setIsRecording(false)
|
setIsRecording(false)
|
||||||
setIsPreparing(false)
|
setIsPreparing(false)
|
||||||
setIsPaused(false)
|
|
||||||
isPausedRef.current = false
|
|
||||||
setShowProgress(false)
|
setShowProgress(false)
|
||||||
if (listenTimerRef.current) {
|
if (listenTimerRef.current) {
|
||||||
clearInterval(listenTimerRef.current)
|
clearInterval(listenTimerRef.current)
|
||||||
@@ -796,7 +632,6 @@ const RecordPlayback = ({ route }) => {
|
|||||||
log('checkSongEndRef cleared (error path)')
|
log('checkSongEndRef cleared (error path)')
|
||||||
}
|
}
|
||||||
playbackStartedRef.current = false
|
playbackStartedRef.current = false
|
||||||
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }
|
|
||||||
const exitRequested = exitRequestedRef.current
|
const exitRequested = exitRequestedRef.current
|
||||||
const shouldRestart = restartRequestedRef.current
|
const shouldRestart = restartRequestedRef.current
|
||||||
if (exitRequested) {
|
if (exitRequested) {
|
||||||
@@ -886,33 +721,6 @@ const RecordPlayback = ({ route }) => {
|
|||||||
}, [handleBackPress])
|
}, [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 () => {
|
const handleRestartRecording = async () => {
|
||||||
try {
|
try {
|
||||||
const hasRecordingPending = !!activeRecordingPromiseRef.current
|
const hasRecordingPending = !!activeRecordingPromiseRef.current
|
||||||
@@ -921,8 +729,6 @@ const RecordPlayback = ({ route }) => {
|
|||||||
isRecording,
|
isRecording,
|
||||||
hasRecordingPending,
|
hasRecordingPending,
|
||||||
})
|
})
|
||||||
setIsPaused(false)
|
|
||||||
isPausedRef.current = false
|
|
||||||
try {
|
try {
|
||||||
await cameraRef.current?.resumePreview?.()
|
await cameraRef.current?.resumePreview?.()
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -1074,9 +880,7 @@ const RecordPlayback = ({ route }) => {
|
|||||||
<ProgressRing
|
<ProgressRing
|
||||||
size={120}
|
size={120}
|
||||||
strokeWidth={8}
|
strokeWidth={8}
|
||||||
progress={
|
progress={durationMs ? Math.min(1, positionMs / durationMs) : 0}
|
||||||
progressInfo.dur ? Math.min(1, (progressInfo.pos || 0) / progressInfo.dur) : 0
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
activeOpacity={0.9}
|
activeOpacity={0.9}
|
||||||
@@ -1099,34 +903,6 @@ const RecordPlayback = ({ route }) => {
|
|||||||
<RestartSpinnerIcon />
|
<RestartSpinnerIcon />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
{isRecording && (
|
|
||||||
<TouchableOpacity
|
|
||||||
activeOpacity={0.9}
|
|
||||||
onPress={handleTogglePause}
|
|
||||||
style={{
|
|
||||||
marginTop: 14,
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 18,
|
|
||||||
borderRadius: 999,
|
|
||||||
backgroundColor: Palette.white,
|
|
||||||
shadowColor: '#000000',
|
|
||||||
shadowOpacity: 0.12,
|
|
||||||
shadowRadius: 10,
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
elevation: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
color: Palette.black,
|
|
||||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
|
||||||
fontSize: 14,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isPaused ? 'Reprendre' : 'Mettre en pause'}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -1146,8 +922,7 @@ const RecordPlayback = ({ route }) => {
|
|||||||
{(isRecording || showProgress) && alignedWords?.length > 0 ? (
|
{(isRecording || showProgress) && alignedWords?.length > 0 ? (
|
||||||
<KaraokeLyrics
|
<KaraokeLyrics
|
||||||
alignedWords={alignedWords}
|
alignedWords={alignedWords}
|
||||||
currentTimeS={(progressInfo?.pos || 0) / 1000}
|
currentTimeS={positionMs / 1000}
|
||||||
timeOffsetS={KARAOKE_LEAD_S}
|
|
||||||
mode="teleprompter"
|
mode="teleprompter"
|
||||||
teleprompterLines={6}
|
teleprompterLines={6}
|
||||||
teleprompterAnchorLine={1}
|
teleprompterAnchorLine={1}
|
||||||
@@ -1202,44 +977,6 @@ const ProgressRing = ({ size = 50, strokeWidth = 12, progress = 0 }) => {
|
|||||||
|
|
||||||
export default RecordPlayback
|
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 (
|
|
||||||
<View style={{ gap: 4 }}>
|
|
||||||
{/* {prev ? (
|
|
||||||
<Text style={{ color: "#FFFFFF99", fontSize: 14, textAlign: "center", fontFamily: FONT_FAMILY.InterMedium }}>
|
|
||||||
{prev}
|
|
||||||
</Text>
|
|
||||||
) : null} */}
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
color: Palette.white,
|
|
||||||
fontSize: 18,
|
|
||||||
textAlign: 'center',
|
|
||||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{curr}
|
|
||||||
</Text>
|
|
||||||
{next ? (
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
color: '#FFFFFF99',
|
|
||||||
fontSize: 14,
|
|
||||||
textAlign: 'center',
|
|
||||||
fontFamily: FONT_FAMILY.InterMedium,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{next}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CameraFacingSelector = ({ value = 'front', onChange, disabled = false }) => {
|
const CameraFacingSelector = ({ value = 'front', onChange, disabled = false }) => {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -29,14 +29,6 @@ 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 KARAOKE_LEAD_S = 0.68
|
|
||||||
|
|
||||||
const toSeconds = (v) => {
|
|
||||||
const n = Number(v ?? 0)
|
|
||||||
if (!Number.isFinite(n) || n < 0) return 0
|
|
||||||
return n > 10000 ? n / 1000 : n // heuristique ms → s
|
|
||||||
}
|
|
||||||
|
|
||||||
const pickRecorderMimeType = () => {
|
const pickRecorderMimeType = () => {
|
||||||
if (typeof window === 'undefined' || typeof MediaRecorder === 'undefined') return undefined
|
if (typeof window === 'undefined' || typeof MediaRecorder === 'undefined') return undefined
|
||||||
@@ -74,6 +66,8 @@ const RecordPlayback = ({ route }) => {
|
|||||||
coverUrl: project?.coverUrl || null,
|
coverUrl: project?.coverUrl || null,
|
||||||
metadata: { projectId: project?.id, screen: 'RecordPlayback' },
|
metadata: { projectId: project?.id, screen: 'RecordPlayback' },
|
||||||
})
|
})
|
||||||
|
const positionMs = player?.positionMs || 0
|
||||||
|
const durationMs = player?.durationMs || 0
|
||||||
|
|
||||||
// MediaStream / Recorder (web only)
|
// MediaStream / Recorder (web only)
|
||||||
const previewVideoRef = useRef(null)
|
const previewVideoRef = useRef(null)
|
||||||
@@ -96,31 +90,19 @@ const RecordPlayback = ({ route }) => {
|
|||||||
const [countdown, setCountdown] = useState(0)
|
const [countdown, setCountdown] = useState(0)
|
||||||
const [isPreparing, setIsPreparing] = useState(false)
|
const [isPreparing, setIsPreparing] = useState(false)
|
||||||
const [isRecording, setIsRecording] = useState(false)
|
const [isRecording, setIsRecording] = useState(false)
|
||||||
const [isPaused, setIsPaused] = useState(false)
|
|
||||||
const [pos, setPos] = useState(0)
|
|
||||||
const [dur, setDur] = useState(0)
|
|
||||||
|
|
||||||
// timers / refs
|
// timers / refs
|
||||||
const isPausedRef = useRef(false)
|
|
||||||
const progressTimerRef = useRef(null)
|
|
||||||
const listenTimerRef = useRef(null)
|
const listenTimerRef = useRef(null)
|
||||||
const countdownTimerRef = useRef(null)
|
const countdownTimerRef = useRef(null)
|
||||||
const perfStartRef = useRef(null)
|
|
||||||
const pausedAtRef = useRef(null)
|
|
||||||
const pausedMsRef = useRef(0)
|
|
||||||
const listenedMsRef = useRef(0)
|
const listenedMsRef = useRef(0)
|
||||||
const viewsIncrementedRef = useRef(false)
|
const viewsIncrementedRef = useRef(false)
|
||||||
const correctedInitialJumpRef = useRef(false)
|
|
||||||
const playbackStartedAtRef = useRef(null)
|
const playbackStartedAtRef = useRef(null)
|
||||||
const recordingStartAtRef = useRef(null)
|
const recordingStartAtRef = useRef(null)
|
||||||
|
const stopInFlightRef = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
latestLoopingValueRef.current = isLooping ?? false
|
latestLoopingValueRef.current = isLooping ?? false
|
||||||
}, [isLooping])
|
}, [isLooping])
|
||||||
useEffect(() => {
|
|
||||||
isPausedRef.current = isPaused
|
|
||||||
}, [isPaused])
|
|
||||||
|
|
||||||
// Lyrics
|
// Lyrics
|
||||||
const alignedWords = useMemo(() => {
|
const alignedWords = useMemo(() => {
|
||||||
const ts = project?.musicTimestamps?.[songIndex]
|
const ts = project?.musicTimestamps?.[songIndex]
|
||||||
@@ -134,10 +116,9 @@ const RecordPlayback = ({ route }) => {
|
|||||||
}, [project?.musicTimestamps, songIndex])
|
}, [project?.musicTimestamps, songIndex])
|
||||||
|
|
||||||
const clearAllTimers = () => {
|
const clearAllTimers = () => {
|
||||||
if (progressTimerRef.current) clearInterval(progressTimerRef.current)
|
|
||||||
if (listenTimerRef.current) clearInterval(listenTimerRef.current)
|
if (listenTimerRef.current) clearInterval(listenTimerRef.current)
|
||||||
if (countdownTimerRef.current) clearInterval(countdownTimerRef.current)
|
if (countdownTimerRef.current) clearInterval(countdownTimerRef.current)
|
||||||
progressTimerRef.current = listenTimerRef.current = countdownTimerRef.current = null
|
listenTimerRef.current = countdownTimerRef.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearMediaRetry = useCallback(() => {
|
const clearMediaRetry = useCallback(() => {
|
||||||
@@ -150,19 +131,12 @@ const RecordPlayback = ({ route }) => {
|
|||||||
const resetUI = () => {
|
const resetUI = () => {
|
||||||
setIsPreparing(false)
|
setIsPreparing(false)
|
||||||
setIsRecording(false)
|
setIsRecording(false)
|
||||||
setIsPaused(false)
|
|
||||||
isPausedRef.current = false
|
|
||||||
pausedAtRef.current = null
|
|
||||||
pausedMsRef.current = 0
|
|
||||||
setCountdown(0)
|
setCountdown(0)
|
||||||
setPos(0)
|
|
||||||
setDur(0)
|
|
||||||
listenedMsRef.current = 0
|
listenedMsRef.current = 0
|
||||||
viewsIncrementedRef.current = false
|
viewsIncrementedRef.current = false
|
||||||
perfStartRef.current = null
|
|
||||||
correctedInitialJumpRef.current = false
|
|
||||||
playbackStartedAtRef.current = null
|
playbackStartedAtRef.current = null
|
||||||
recordingStartAtRef.current = null
|
recordingStartAtRef.current = null
|
||||||
|
stopInFlightRef.current = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const releaseRecordingUrl = useCallback(() => {
|
const releaseRecordingUrl = useCallback(() => {
|
||||||
@@ -271,9 +245,9 @@ const RecordPlayback = ({ route }) => {
|
|||||||
[clearMediaRetry, ensureMediaStream]
|
[clearMediaRetry, ensureMediaStream]
|
||||||
)
|
)
|
||||||
|
|
||||||
const startRecorder = useCallback(() => {
|
const startRecorder = useCallback(async () => {
|
||||||
if (!mediaStreamRef.current) {
|
if (!mediaStreamRef.current) {
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -294,7 +268,7 @@ const RecordPlayback = ({ route }) => {
|
|||||||
: new MediaRecorder(mediaStreamRef.current)
|
: new MediaRecorder(mediaStreamRef.current)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setMediaError(e instanceof Error ? e : new Error(String(e || '')))
|
setMediaError(e instanceof Error ? e : new Error(String(e || '')))
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaRecorderRef.current = recorder
|
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) => {
|
recorder.onerror = (event) => {
|
||||||
const err = event?.error || event
|
const err = event?.error || event
|
||||||
setMediaError(err instanceof Error ? err : new Error(String(err || '')))
|
setMediaError(err instanceof Error ? err : new Error(String(err || '')))
|
||||||
|
resolveRecorderStart?.(null)
|
||||||
|
resolveRecorderStart = null
|
||||||
}
|
}
|
||||||
|
|
||||||
recorder.onstop = () => {
|
recorder.onstop = () => {
|
||||||
@@ -340,8 +326,10 @@ const RecordPlayback = ({ route }) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
recorder.start(1000)
|
recorder.start(1000)
|
||||||
return true
|
return await recorderStartedPromise
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
resolveRecorderStart?.(null)
|
||||||
|
resolveRecorderStart = null
|
||||||
if (stopRecordingResolveRef.current) {
|
if (stopRecordingResolveRef.current) {
|
||||||
stopRecordingResolveRef.current(null)
|
stopRecordingResolveRef.current(null)
|
||||||
stopRecordingResolveRef.current = null
|
stopRecordingResolveRef.current = null
|
||||||
@@ -349,7 +337,7 @@ const RecordPlayback = ({ route }) => {
|
|||||||
stopRecordingPromiseRef.current = null
|
stopRecordingPromiseRef.current = null
|
||||||
mediaRecorderRef.current = null
|
mediaRecorderRef.current = null
|
||||||
setMediaError(e instanceof Error ? e : new Error(String(e || '')))
|
setMediaError(e instanceof Error ? e : new Error(String(e || '')))
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
}, [releaseRecordingUrl, setMediaError])
|
}, [releaseRecordingUrl, setMediaError])
|
||||||
|
|
||||||
@@ -523,6 +511,8 @@ const RecordPlayback = ({ route }) => {
|
|||||||
}, [clearMediaRetry, releaseRecordingUrl, preserveRecordingForNextScreenRef, stopMediaStream])
|
}, [clearMediaRetry, releaseRecordingUrl, preserveRecordingForNextScreenRef, stopMediaStream])
|
||||||
|
|
||||||
const stopAndNavigate = useCallback(async () => {
|
const stopAndNavigate = useCallback(async () => {
|
||||||
|
if (stopInFlightRef.current) return
|
||||||
|
stopInFlightRef.current = true
|
||||||
preserveRecordingForNextScreenRef.current = true
|
preserveRecordingForNextScreenRef.current = true
|
||||||
clearAllTimers()
|
clearAllTimers()
|
||||||
try {
|
try {
|
||||||
@@ -538,10 +528,6 @@ const RecordPlayback = ({ route }) => {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
setIsRecording(false)
|
setIsRecording(false)
|
||||||
setIsPaused(false)
|
|
||||||
isPausedRef.current = false
|
|
||||||
pausedAtRef.current = null
|
|
||||||
pausedMsRef.current = 0
|
|
||||||
|
|
||||||
const rawOffsetMs =
|
const rawOffsetMs =
|
||||||
playbackStartedAtRef.current != null && recordingStartAtRef.current != null
|
playbackStartedAtRef.current != null && recordingStartAtRef.current != null
|
||||||
@@ -556,68 +542,10 @@ const RecordPlayback = ({ route }) => {
|
|||||||
})
|
})
|
||||||
}, [player, project, stopRecorderAndGetUrl])
|
}, [player, project, stopRecorderAndGetUrl])
|
||||||
|
|
||||||
const startProgressLoop = useCallback(() => {
|
useEffect(() => {
|
||||||
if (progressTimerRef.current) clearInterval(progressTimerRef.current)
|
if (!isRecording || !durationMs || positionMs < durationMs - 300) return
|
||||||
|
|
||||||
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()
|
void stopAndNavigate()
|
||||||
}
|
}, [durationMs, isRecording, positionMs, stopAndNavigate])
|
||||||
}, 250)
|
|
||||||
}, [player, stopAndNavigate])
|
|
||||||
|
|
||||||
const startListenLoop = useCallback(() => {
|
const startListenLoop = useCallback(() => {
|
||||||
if (listenTimerRef.current) clearInterval(listenTimerRef.current)
|
if (listenTimerRef.current) clearInterval(listenTimerRef.current)
|
||||||
@@ -640,29 +568,21 @@ const RecordPlayback = ({ route }) => {
|
|||||||
|
|
||||||
const startPlayback = useCallback(async () => {
|
const startPlayback = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setIsPaused(false)
|
await player?.pause?.()
|
||||||
isPausedRef.current = false
|
await player?.seekTo?.(0)
|
||||||
pausedAtRef.current = null
|
const recordingStartedAt = await startRecorder()
|
||||||
pausedMsRef.current = 0
|
if (recordingStartedAt == null) {
|
||||||
await player?.pause?.() // s'assure qu'on repart propre
|
throw new Error("Impossible de démarrer l'enregistrement vidéo")
|
||||||
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, CAMERA_STARTUP_DELAY_MS))
|
recordingStartAtRef.current = recordingStartedAt
|
||||||
await player?.play?.() // attend la promesse → l'élément est prêt
|
await player?.play?.()
|
||||||
perfStartRef.current = perfStartRef.current ?? performance.now()
|
playbackStartedAtRef.current = performance.now()
|
||||||
correctedInitialJumpRef.current = false // autorise la correction 1-shot
|
|
||||||
setIsRecording(true)
|
setIsRecording(true)
|
||||||
startProgressLoop()
|
|
||||||
startListenLoop()
|
startListenLoop()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setIsRecording(false)
|
setIsRecording(false)
|
||||||
setIsPreparing(false)
|
setIsPreparing(false)
|
||||||
setIsPaused(false)
|
stopInFlightRef.current = false
|
||||||
isPausedRef.current = false
|
|
||||||
pausedAtRef.current = null
|
|
||||||
const msg = String(e?.message || e || '')
|
const msg = String(e?.message || e || '')
|
||||||
if (/not supported on the simulator/i.test(msg)) {
|
if (/not supported on the simulator/i.test(msg)) {
|
||||||
alert(
|
alert(
|
||||||
@@ -682,7 +602,7 @@ const RecordPlayback = ({ route }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [player, startRecorder, startProgressLoop, startListenLoop])
|
}, [player, startRecorder, startListenLoop])
|
||||||
|
|
||||||
const permissionsGranted = !!cameraPermission?.granted
|
const permissionsGranted = !!cameraPermission?.granted
|
||||||
const canRecord = permissionsGranted && mediaReady && !mediaError
|
const canRecord = permissionsGranted && mediaReady && !mediaError
|
||||||
@@ -690,8 +610,6 @@ const RecordPlayback = ({ route }) => {
|
|||||||
const startCountdownThenRecord = useCallback(async () => {
|
const startCountdownThenRecord = useCallback(async () => {
|
||||||
if (!songUrl || !canRecord) return
|
if (!songUrl || !canRecord) return
|
||||||
await resetSession()
|
await resetSession()
|
||||||
setIsPaused(false)
|
|
||||||
isPausedRef.current = false
|
|
||||||
setIsPreparing(true)
|
setIsPreparing(true)
|
||||||
setCountdown(COUNTDOWN_SECONDS)
|
setCountdown(COUNTDOWN_SECONDS)
|
||||||
if (countdownTimerRef.current) clearInterval(countdownTimerRef.current)
|
if (countdownTimerRef.current) clearInterval(countdownTimerRef.current)
|
||||||
@@ -710,50 +628,13 @@ const RecordPlayback = ({ route }) => {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
}, [songUrl, canRecord, resetSession, startPlayback])
|
}, [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 () => {
|
const handleRestartRecording = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await startCountdownThenRecord()
|
await startCountdownThenRecord()
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}, [startCountdownThenRecord])
|
}, [startCountdownThenRecord])
|
||||||
|
|
||||||
const progressRatio = dur > 0 ? Math.min(1, pos / dur) : 0
|
const progressRatio = durationMs > 0 ? Math.min(1, positionMs / durationMs) : 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
@@ -941,34 +822,6 @@ const RecordPlayback = ({ route }) => {
|
|||||||
<RestartSpinnerIcon />
|
<RestartSpinnerIcon />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
{isRecording && (
|
|
||||||
<TouchableOpacity
|
|
||||||
activeOpacity={0.9}
|
|
||||||
onPress={handleTogglePause}
|
|
||||||
style={{
|
|
||||||
marginTop: 14,
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 18,
|
|
||||||
borderRadius: 999,
|
|
||||||
backgroundColor: Palette.white,
|
|
||||||
shadowColor: '#000000',
|
|
||||||
shadowOpacity: 0.12,
|
|
||||||
shadowRadius: 10,
|
|
||||||
shadowOffset: { width: 0, height: 4 },
|
|
||||||
elevation: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
color: Palette.black,
|
|
||||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
|
||||||
fontSize: 14,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isPaused ? 'Reprendre' : 'Mettre en pause'}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1010,8 +863,7 @@ const RecordPlayback = ({ route }) => {
|
|||||||
{isRecording && alignedWords.length > 0 ? (
|
{isRecording && alignedWords.length > 0 ? (
|
||||||
<KaraokeLyrics
|
<KaraokeLyrics
|
||||||
alignedWords={alignedWords}
|
alignedWords={alignedWords}
|
||||||
currentTimeS={pos}
|
currentTimeS={positionMs / 1000}
|
||||||
timeOffsetS={KARAOKE_LEAD_S}
|
|
||||||
mode="teleprompter"
|
mode="teleprompter"
|
||||||
teleprompterLines={6}
|
teleprompterLines={6}
|
||||||
teleprompterAnchorLine={1}
|
teleprompterAnchorLine={1}
|
||||||
|
|||||||
@@ -79,6 +79,20 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
[audioPlayer, videoPlayer]
|
[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 () => {
|
const stopPlayback = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
if (audioPlayer) await audioPlayer.pause?.()
|
if (audioPlayer) await audioPlayer.pause?.()
|
||||||
@@ -104,8 +118,7 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
const start = async () => {
|
const start = async () => {
|
||||||
try {
|
try {
|
||||||
await alignPlaybackToTimeline(0, initialSyncOffsetMs)
|
await alignPlaybackToTimeline(0, initialSyncOffsetMs)
|
||||||
if (audioPlayer && songUrl) await audioPlayer.play?.()
|
await playSynchronized()
|
||||||
if (videoPlayer) videoPlayer.play()
|
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +128,7 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
playbackEndedRef.current = false
|
playbackEndedRef.current = false
|
||||||
void stopPlayback()
|
void stopPlayback()
|
||||||
}
|
}
|
||||||
}, [alignPlaybackToTimeline, audioPlayer, initialSyncOffsetMs, songUrl, stopPlayback, videoPlayer])
|
}, [alignPlaybackToTimeline, initialSyncOffsetMs, playSynchronized, stopPlayback])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = global.setInterval(() => {
|
const id = global.setInterval(() => {
|
||||||
@@ -139,7 +152,7 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
const currentVideoMs = (videoPlayer.currentTime || 0) * 1000
|
const currentVideoMs = (videoPlayer.currentTime || 0) * 1000
|
||||||
const driftMs = Math.abs(currentVideoMs - expectedVideoMs)
|
const driftMs = Math.abs(currentVideoMs - expectedVideoMs)
|
||||||
|
|
||||||
if (driftMs > 350) {
|
if (driftMs > 120) {
|
||||||
videoPlayer.currentTime = Math.max(0, expectedVideoMs / 1000)
|
videoPlayer.currentTime = Math.max(0, expectedVideoMs / 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,16 +188,9 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
|
|
||||||
const resumeAfterSeek = useCallback(async () => {
|
const resumeAfterSeek = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
if (audioPlayer?.resume) {
|
await playSynchronized()
|
||||||
await audioPlayer.resume?.()
|
|
||||||
} else if (audioPlayer) {
|
|
||||||
await audioPlayer.play?.()
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
try {
|
}, [playSynchronized])
|
||||||
if (videoPlayer) videoPlayer.play()
|
|
||||||
} catch (e) {}
|
|
||||||
}, [audioPlayer, videoPlayer])
|
|
||||||
|
|
||||||
const handleSyncOffsetChange = useCallback(
|
const handleSyncOffsetChange = useCallback(
|
||||||
(nextOffsetMs) => {
|
(nextOffsetMs) => {
|
||||||
@@ -261,23 +267,14 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
|
|
||||||
playbackEndedRef.current = false
|
playbackEndedRef.current = false
|
||||||
|
|
||||||
if (songUrl && audioPlayer) {
|
await playSynchronized()
|
||||||
if (audioPlayer?.resume) {
|
|
||||||
await audioPlayer.resume?.()
|
|
||||||
} else {
|
|
||||||
await audioPlayer.play?.()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoPlayer) videoPlayer.play()
|
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}, [alignPlaybackToTimeline, audioPlayer, progressInfo, songUrl, videoPlayer])
|
}, [alignPlaybackToTimeline, audioPlayer, playSynchronized, progressInfo, videoPlayer])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page backgroundColor={Palette.grayMid} headerType="NONE">
|
<Page backgroundColor={Palette.grayMid} headerType="NONE">
|
||||||
<MusicLandHeader progress={19} onPressBack={goBack} />
|
<MusicLandHeader progress={19} onPressBack={goBack} />
|
||||||
<View style={{ flex: 1, paddingBottom: gutters * 2 }}>
|
<View style={{ flex: 1, paddingBottom: gutters * 2 }}>
|
||||||
|
|
||||||
{/* Vidéo avec play/pause en overlay */}
|
{/* Vidéo avec play/pause en overlay */}
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={handleTogglePlayback}
|
onPress={handleTogglePlayback}
|
||||||
@@ -363,7 +360,14 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
Synchro audio / vidéo
|
Synchro audio / vidéo
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 16 }}>
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => handleSyncOffsetChange(clampSyncOffsetMs(currentSyncOffsetMs - 100))}
|
onPress={() => handleSyncOffsetChange(clampSyncOffsetMs(currentSyncOffsetMs - 100))}
|
||||||
disabled={!songUrl || currentSyncOffsetMs <= -2000}
|
disabled={!songUrl || currentSyncOffsetMs <= -2000}
|
||||||
|
|||||||
@@ -78,6 +78,24 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
[audioPlayer, videoUri]
|
[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 () => {
|
const stopPlayback = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
if (audioPlayer) await audioPlayer.pause?.()
|
if (audioPlayer) await audioPlayer.pause?.()
|
||||||
@@ -113,15 +131,7 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
const start = async () => {
|
const start = async () => {
|
||||||
try {
|
try {
|
||||||
await alignPlaybackToTimeline(0, initialSyncOffsetMs)
|
await alignPlaybackToTimeline(0, initialSyncOffsetMs)
|
||||||
|
await playSynchronized()
|
||||||
if (audioPlayer && songUrl) {
|
|
||||||
await audioPlayer.play?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoElRef.current && videoUri) {
|
|
||||||
videoElRef.current.muted = true
|
|
||||||
videoElRef.current.play().catch(() => {})
|
|
||||||
}
|
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +141,7 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
playbackEndedRef.current = false
|
playbackEndedRef.current = false
|
||||||
void stopPlayback()
|
void stopPlayback()
|
||||||
}
|
}
|
||||||
}, [alignPlaybackToTimeline, audioPlayer, initialSyncOffsetMs, songUrl, stopPlayback, videoUri])
|
}, [alignPlaybackToTimeline, initialSyncOffsetMs, playSynchronized, stopPlayback])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = setInterval(() => {
|
const id = setInterval(() => {
|
||||||
@@ -156,7 +166,7 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
const currentVideoMs = Number(videoElRef.current.currentTime || 0) * 1000
|
const currentVideoMs = Number(videoElRef.current.currentTime || 0) * 1000
|
||||||
const driftMs = Math.abs(currentVideoMs - expectedVideoMs)
|
const driftMs = Math.abs(currentVideoMs - expectedVideoMs)
|
||||||
|
|
||||||
if (driftMs > 350) {
|
if (driftMs > 120) {
|
||||||
videoElRef.current.currentTime = Math.max(0, expectedVideoMs / 1000)
|
videoElRef.current.currentTime = Math.max(0, expectedVideoMs / 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,19 +204,9 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
|
|
||||||
const resumeAfterSeek = useCallback(async () => {
|
const resumeAfterSeek = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
if (audioPlayer?.resume) {
|
await playSynchronized()
|
||||||
await audioPlayer.resume?.()
|
|
||||||
} else if (audioPlayer) {
|
|
||||||
await audioPlayer.play?.()
|
|
||||||
}
|
|
||||||
} catch {}
|
} catch {}
|
||||||
try {
|
}, [playSynchronized])
|
||||||
if (videoElRef.current && videoUri) {
|
|
||||||
videoElRef.current.muted = true
|
|
||||||
videoElRef.current.play().catch(() => {})
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}, [audioPlayer, videoUri])
|
|
||||||
|
|
||||||
const handleSyncOffsetChange = useCallback(
|
const handleSyncOffsetChange = useCallback(
|
||||||
(nextOffsetMs) => {
|
(nextOffsetMs) => {
|
||||||
@@ -238,7 +238,12 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
|
|
||||||
void alignPlaybackToTimeline(nextTimelineMs, safeOffsetMs)
|
void alignPlaybackToTimeline(nextTimelineMs, safeOffsetMs)
|
||||||
},
|
},
|
||||||
[alignPlaybackToTimeline, audioPlayer?.playing, getCurrentRawAudioDurationMs, getCurrentRawAudioPositionMs]
|
[
|
||||||
|
alignPlaybackToTimeline,
|
||||||
|
audioPlayer?.playing,
|
||||||
|
getCurrentRawAudioDurationMs,
|
||||||
|
getCurrentRawAudioPositionMs,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -278,20 +283,9 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
|
|
||||||
playbackEndedRef.current = false
|
playbackEndedRef.current = false
|
||||||
|
|
||||||
if (songUrl && audioPlayer) {
|
await playSynchronized()
|
||||||
if (audioPlayer?.resume) {
|
|
||||||
await audioPlayer.resume?.()
|
|
||||||
} else {
|
|
||||||
await audioPlayer.play?.()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoElRef.current && videoUri) {
|
|
||||||
videoElRef.current.muted = true
|
|
||||||
videoElRef.current.play().catch(() => {})
|
|
||||||
}
|
|
||||||
} catch {}
|
} catch {}
|
||||||
}, [alignPlaybackToTimeline, audioPlayer, progressInfo, songUrl, stopPlayback, videoUri])
|
}, [alignPlaybackToTimeline, audioPlayer, playSynchronized, progressInfo, stopPlayback])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
@@ -419,7 +413,14 @@ const RecordedPlayback = ({ route }) => {
|
|||||||
Synchro audio / vidéo
|
Synchro audio / vidéo
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 16 }}>
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => handleSyncOffsetChange(clampSyncOffsetMs(currentSyncOffsetMs - 100))}
|
onPress={() => handleSyncOffsetChange(clampSyncOffsetMs(currentSyncOffsetMs - 100))}
|
||||||
disabled={!songUrl || currentSyncOffsetMs <= -2000}
|
disabled={!songUrl || currentSyncOffsetMs <= -2000}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { Feather } from '@expo/vector-icons'
|
|||||||
|
|
||||||
// Debug logging toggle for web playback
|
// Debug logging toggle for web playback
|
||||||
const DEBUG_PLAYBACK_WEB = true
|
const DEBUG_PLAYBACK_WEB = true
|
||||||
const PLAYBACK_LYRICS_LEAD_S = 0.18
|
|
||||||
|
|
||||||
// NOTE: Web-only implementation that uses a native <video> element instead of expo-video
|
// NOTE: Web-only implementation that uses a native <video> element instead of expo-video
|
||||||
// - No Platform checks
|
// - No Platform checks
|
||||||
@@ -474,11 +473,7 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid, onBackgroundSyn
|
|||||||
<View style={styles.lyricsContainer}>
|
<View style={styles.lyricsContainer}>
|
||||||
<BlurView tint="dark" intensity={20} style={styles.lyricsCard}>
|
<BlurView tint="dark" intensity={20} style={styles.lyricsCard}>
|
||||||
{alignedWords?.length > 0 ? (
|
{alignedWords?.length > 0 ? (
|
||||||
<KaraokeLyrics
|
<KaraokeLyrics alignedWords={alignedWords} currentTimeS={currentTimeS} />
|
||||||
alignedWords={alignedWords}
|
|
||||||
currentTimeS={currentTimeS}
|
|
||||||
timeOffsetS={PLAYBACK_LYRICS_LEAD_S}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.lyricsText}>{descriptionText}</Text>
|
<Text style={styles.lyricsText}>{descriptionText}</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,18 +5,12 @@ import KaraokeLyrics from '../../../components/KaraokeLyrics'
|
|||||||
import { Palette } from '../../../styles'
|
import { Palette } from '../../../styles'
|
||||||
import { FONT_FAMILY } from '../../../styles/Fonts'
|
import { FONT_FAMILY } from '../../../styles/Fonts'
|
||||||
|
|
||||||
const PLAYBACK_LYRICS_LEAD_S = 0.18
|
|
||||||
|
|
||||||
const PlaybackLyricsCard = ({ alignedWords, currentTimeS, fallbackTitle }) => {
|
const PlaybackLyricsCard = ({ alignedWords, currentTimeS, fallbackTitle }) => {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<BlurView tint="dark" intensity={Platform.OS !== 'ios' ? 10 : 20} style={styles.blur}>
|
<BlurView tint="dark" intensity={Platform.OS !== 'ios' ? 10 : 20} style={styles.blur}>
|
||||||
{alignedWords?.length > 0 ? (
|
{alignedWords?.length > 0 ? (
|
||||||
<KaraokeLyrics
|
<KaraokeLyrics alignedWords={alignedWords} currentTimeS={currentTimeS} />
|
||||||
alignedWords={alignedWords}
|
|
||||||
currentTimeS={currentTimeS}
|
|
||||||
timeOffsetS={PLAYBACK_LYRICS_LEAD_S}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.title}>{fallbackTitle || 'Description chanson'}</Text>
|
<Text style={styles.title}>{fallbackTitle || 'Description chanson'}</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user