timeout for record playback

This commit is contained in:
2026-02-12 14:02:08 +01:00
parent 6ccac084dc
commit 9d7421e3d8
+159 -46
View File
@@ -24,6 +24,9 @@ import { registerBlobUrl, releaseBlobUrl } from '../../utils/blobUrlCache'
const TIME_BEFORE_INCREMENT_MS = 20000 const TIME_BEFORE_INCREMENT_MS = 20000
const COUNTDOWN_SECONDS = __DEV__ ? 1 : 10 const COUNTDOWN_SECONDS = __DEV__ ? 1 : 10
const WEB_PREVIEW_WIDTH = 360 const WEB_PREVIEW_WIDTH = 360
const MEDIA_BOOTSTRAP_DELAY_MS = 250
const MEDIA_RETRY_DELAY_MS = 700
const MEDIA_MAX_RETRIES = 2
const toSeconds = (v) => { const toSeconds = (v) => {
const n = Number(v ?? 0) const n = Number(v ?? 0)
@@ -79,6 +82,9 @@ const RecordPlayback = ({ route }) => {
const preserveRecordingForNextScreenRef = useRef(false) const preserveRecordingForNextScreenRef = useRef(false)
const originalLoopingValueRef = useRef({ hasValue: false, value: false }) const originalLoopingValueRef = useRef({ hasValue: false, value: false })
const latestLoopingValueRef = useRef(isLooping ?? false) const latestLoopingValueRef = useRef(isLooping ?? false)
const mediaInitInFlightRef = useRef(false)
const mediaRetryTimerRef = useRef(null)
const mediaInitAttemptsRef = useRef(0)
const [mediaReady, setMediaReady] = useState(false) const [mediaReady, setMediaReady] = useState(false)
const [mediaError, setMediaError] = useState(null) const [mediaError, setMediaError] = useState(null)
@@ -127,6 +133,13 @@ const RecordPlayback = ({ route }) => {
progressTimerRef.current = listenTimerRef.current = countdownTimerRef.current = null progressTimerRef.current = listenTimerRef.current = countdownTimerRef.current = null
} }
const clearMediaRetry = useCallback(() => {
if (mediaRetryTimerRef.current) {
clearTimeout(mediaRetryTimerRef.current)
mediaRetryTimerRef.current = null
}
}, [])
const resetUI = () => { const resetUI = () => {
setIsPreparing(false) setIsPreparing(false)
setIsRecording(false) setIsRecording(false)
@@ -153,6 +166,102 @@ const RecordPlayback = ({ route }) => {
preserveRecordingForNextScreenRef.current = false preserveRecordingForNextScreenRef.current = false
}, []) }, [])
const stopMediaStream = useCallback(() => {
if (mediaStreamRef.current) {
mediaStreamRef.current.getTracks().forEach((track) => track.stop())
mediaStreamRef.current = null
}
}, [])
const attachPreview = useCallback(() => {
const video = previewVideoRef.current
const stream = mediaStreamRef.current
if (!video || !stream) return false
try {
if (video.srcObject !== stream) {
video.srcObject = stream
}
} catch (e) { }
try {
const playPromise = video.play?.()
if (playPromise && typeof playPromise.catch === 'function') {
playPromise.catch(() => { })
}
} catch (e) { }
return true
}, [])
const ensureMediaStream = useCallback(
async ({ force = false } = {}) => {
if (!cameraPermission?.granted) return false
if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia) {
setMediaError(new Error("La capture vidéo n'est pas supportée sur ce navigateur"))
setMediaReady(false)
return false
}
if (mediaInitInFlightRef.current) return false
if (!force && mediaStreamRef.current) {
setMediaReady(true)
setMediaError(null)
attachPreview()
return true
}
mediaInitInFlightRef.current = true
try {
if (force && mediaStreamRef.current) {
stopMediaStream()
}
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'user' },
audio: false,
})
mediaStreamRef.current = stream
setMediaReady(true)
setMediaError(null)
mediaInitAttemptsRef.current = 0
attachPreview()
return true
} catch (e) {
setMediaError(e instanceof Error ? e : new Error(String(e || '')))
setMediaReady(false)
return false
} finally {
mediaInitInFlightRef.current = false
}
},
[attachPreview, cameraPermission?.granted, stopMediaStream]
)
const schedulePreviewRetry = useCallback(
function schedulePreviewRetry() {
clearMediaRetry()
if (mediaInitAttemptsRef.current >= MEDIA_MAX_RETRIES) return
mediaRetryTimerRef.current = setTimeout(async () => {
const video = previewVideoRef.current
const previewOk =
!!video &&
video.readyState >= 2 &&
video.videoWidth > 0 &&
video.videoHeight > 0 &&
!video.paused
if (previewOk) return
mediaInitAttemptsRef.current += 1
const ok = await ensureMediaStream({ force: true })
if (ok) schedulePreviewRetry()
}, MEDIA_RETRY_DELAY_MS)
},
[clearMediaRetry, ensureMediaStream]
)
const startRecorder = useCallback(() => { const startRecorder = useCallback(() => {
if (!mediaStreamRef.current) { if (!mediaStreamRef.current) {
return false return false
@@ -330,45 +439,28 @@ const RecordPlayback = ({ route }) => {
useEffect(() => { useEffect(() => {
if (!cameraPermission?.granted) { if (!cameraPermission?.granted) {
setMediaReady(false) setMediaReady(false)
return clearMediaRetry()
} mediaInitAttemptsRef.current = 0
if (mediaStreamRef.current) {
setMediaReady(true)
return
}
if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia) {
setMediaError(new Error("La capture vidéo n'est pas supportée sur ce navigateur"))
setMediaReady(false)
return return
} }
let cancelled = false let cancelled = false
; (async () => {
try { const boot = async () => {
const stream = await navigator.mediaDevices.getUserMedia({ const ok = await ensureMediaStream()
video: { facingMode: 'user' }, if (!cancelled && ok) {
audio: false, schedulePreviewRetry()
}) }
if (cancelled) { }
stream.getTracks().forEach((track) => track.stop())
return const timeoutId = setTimeout(boot, MEDIA_BOOTSTRAP_DELAY_MS)
}
mediaStreamRef.current = stream
setMediaReady(true)
setMediaError(null)
} catch (e) {
if (cancelled) return
setMediaError(e instanceof Error ? e : new Error(String(e || '')))
setMediaReady(false)
}
})()
return () => { return () => {
cancelled = true cancelled = true
clearTimeout(timeoutId)
clearMediaRetry()
} }
}, [cameraPermission?.granted]) }, [cameraPermission?.granted, clearMediaRetry, ensureMediaStream, schedulePreviewRetry])
useEffect(() => { useEffect(() => {
const video = previewVideoRef.current const video = previewVideoRef.current
@@ -376,21 +468,35 @@ const RecordPlayback = ({ route }) => {
if (!video) return if (!video) return
if (stream) { if (stream) {
try { attachPreview()
if (video.srcObject !== stream) {
video.srcObject = stream
}
const playPromise = video.play?.()
if (playPromise && typeof playPromise.catch === 'function') {
playPromise.catch(() => { })
}
} catch (e) { }
} else { } else {
try { try {
video.srcObject = null video.srcObject = null
} catch { } } catch { }
} }
}, [mediaReady, mediaError]) }, [attachPreview, mediaReady, mediaError])
useFocusEffect(
useCallback(() => {
let active = true
const timeoutId = setTimeout(() => {
if (!active) return
if (mediaStreamRef.current) {
attachPreview()
schedulePreviewRetry()
return
}
if (cameraPermission?.granted) {
void ensureMediaStream()
}
}, MEDIA_BOOTSTRAP_DELAY_MS)
return () => {
active = false
clearTimeout(timeoutId)
}
}, [attachPreview, cameraPermission?.granted, ensureMediaStream, schedulePreviewRetry])
)
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -399,15 +505,13 @@ const RecordPlayback = ({ route }) => {
mediaRecorderRef.current.stop() mediaRecorderRef.current.stop()
} }
} catch { } } catch { }
if (mediaStreamRef.current) { stopMediaStream()
mediaStreamRef.current.getTracks().forEach((track) => track.stop()) clearMediaRetry()
mediaStreamRef.current = null
}
if (!preserveRecordingForNextScreenRef.current) { if (!preserveRecordingForNextScreenRef.current) {
releaseRecordingUrl() releaseRecordingUrl()
} }
} }
}, [releaseRecordingUrl, preserveRecordingForNextScreenRef]) }, [clearMediaRetry, releaseRecordingUrl, preserveRecordingForNextScreenRef, stopMediaStream])
const stopAndNavigate = useCallback(async () => { const stopAndNavigate = useCallback(async () => {
preserveRecordingForNextScreenRef.current = true preserveRecordingForNextScreenRef.current = true
@@ -658,6 +762,15 @@ const RecordPlayback = ({ route }) => {
transform: 'scaleX(-1)', transform: 'scaleX(-1)',
backgroundColor: 'transparent', backgroundColor: 'transparent',
}} }}
onLoadedMetadata={(event) => {
try {
const video = event?.currentTarget
const playPromise = video?.play?.()
if (playPromise && typeof playPromise.catch === 'function') {
playPromise.catch(() => { })
}
} catch { }
}}
/> />
{/* Overlay */} {/* Overlay */}