diff --git a/src/screens/Playback/RecordPlayback.web.js b/src/screens/Playback/RecordPlayback.web.js index 2df13df..9af7619 100644 --- a/src/screens/Playback/RecordPlayback.web.js +++ b/src/screens/Playback/RecordPlayback.web.js @@ -24,6 +24,9 @@ import { registerBlobUrl, releaseBlobUrl } from '../../utils/blobUrlCache' const TIME_BEFORE_INCREMENT_MS = 20000 const COUNTDOWN_SECONDS = __DEV__ ? 1 : 10 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 n = Number(v ?? 0) @@ -79,6 +82,9 @@ const RecordPlayback = ({ route }) => { const preserveRecordingForNextScreenRef = useRef(false) const originalLoopingValueRef = useRef({ hasValue: false, value: false }) const latestLoopingValueRef = useRef(isLooping ?? false) + const mediaInitInFlightRef = useRef(false) + const mediaRetryTimerRef = useRef(null) + const mediaInitAttemptsRef = useRef(0) const [mediaReady, setMediaReady] = useState(false) const [mediaError, setMediaError] = useState(null) @@ -127,6 +133,13 @@ const RecordPlayback = ({ route }) => { progressTimerRef.current = listenTimerRef.current = countdownTimerRef.current = null } + const clearMediaRetry = useCallback(() => { + if (mediaRetryTimerRef.current) { + clearTimeout(mediaRetryTimerRef.current) + mediaRetryTimerRef.current = null + } + }, []) + const resetUI = () => { setIsPreparing(false) setIsRecording(false) @@ -153,6 +166,102 @@ const RecordPlayback = ({ route }) => { 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(() => { if (!mediaStreamRef.current) { return false @@ -330,45 +439,28 @@ const RecordPlayback = ({ route }) => { useEffect(() => { if (!cameraPermission?.granted) { setMediaReady(false) - return - } - - 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) + clearMediaRetry() + mediaInitAttemptsRef.current = 0 return } let cancelled = false - ; (async () => { - try { - const stream = await navigator.mediaDevices.getUserMedia({ - video: { facingMode: 'user' }, - audio: false, - }) - if (cancelled) { - stream.getTracks().forEach((track) => track.stop()) - return - } - mediaStreamRef.current = stream - setMediaReady(true) - setMediaError(null) - } catch (e) { - if (cancelled) return - setMediaError(e instanceof Error ? e : new Error(String(e || ''))) - setMediaReady(false) - } - })() + + const boot = async () => { + const ok = await ensureMediaStream() + if (!cancelled && ok) { + schedulePreviewRetry() + } + } + + const timeoutId = setTimeout(boot, MEDIA_BOOTSTRAP_DELAY_MS) return () => { cancelled = true + clearTimeout(timeoutId) + clearMediaRetry() } - }, [cameraPermission?.granted]) + }, [cameraPermission?.granted, clearMediaRetry, ensureMediaStream, schedulePreviewRetry]) useEffect(() => { const video = previewVideoRef.current @@ -376,21 +468,35 @@ const RecordPlayback = ({ route }) => { if (!video) return if (stream) { - try { - if (video.srcObject !== stream) { - video.srcObject = stream - } - const playPromise = video.play?.() - if (playPromise && typeof playPromise.catch === 'function') { - playPromise.catch(() => { }) - } - } catch (e) { } + attachPreview() } else { try { video.srcObject = null } 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(() => { return () => { @@ -399,15 +505,13 @@ const RecordPlayback = ({ route }) => { mediaRecorderRef.current.stop() } } catch { } - if (mediaStreamRef.current) { - mediaStreamRef.current.getTracks().forEach((track) => track.stop()) - mediaStreamRef.current = null - } + stopMediaStream() + clearMediaRetry() if (!preserveRecordingForNextScreenRef.current) { releaseRecordingUrl() } } - }, [releaseRecordingUrl, preserveRecordingForNextScreenRef]) + }, [clearMediaRetry, releaseRecordingUrl, preserveRecordingForNextScreenRef, stopMediaStream]) const stopAndNavigate = useCallback(async () => { preserveRecordingForNextScreenRef.current = true @@ -658,6 +762,15 @@ const RecordPlayback = ({ route }) => { transform: 'scaleX(-1)', backgroundColor: 'transparent', }} + onLoadedMetadata={(event) => { + try { + const video = event?.currentTarget + const playPromise = video?.play?.() + if (playPromise && typeof playPromise.catch === 'function') { + playPromise.catch(() => { }) + } + } catch { } + }} /> {/* Overlay */}