From c97fb0a85663c28fb76b7296bbb851db40fe0809 Mon Sep 17 00:00:00 2001 From: leon-morival Date: Mon, 8 Dec 2025 14:27:27 +0100 Subject: [PATCH] feat: playbacks --- src/components/FullscreenIntroVideo.native.js | 23 +++++- src/providers/PlayerProvider.js | 81 ++++++++++--------- src/screens/Playback/RecordPlayback.js | 72 ++++++++++++++++- 3 files changed, 129 insertions(+), 47 deletions(-) diff --git a/src/components/FullscreenIntroVideo.native.js b/src/components/FullscreenIntroVideo.native.js index ab20580..9126cfe 100644 --- a/src/components/FullscreenIntroVideo.native.js +++ b/src/components/FullscreenIntroVideo.native.js @@ -63,7 +63,10 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => { return; } const remaining = player.duration - currentTime; - if (Number.isFinite(remaining) && remaining <= CLOSE_THRESHOLD_SECONDS) { + if ( + Number.isFinite(remaining) && + remaining <= CLOSE_THRESHOLD_SECONDS + ) { handleClose(); } } @@ -75,7 +78,10 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => { return; } const remaining = player.duration - (player.currentTime ?? 0); - if (Number.isFinite(remaining) && remaining <= CLOSE_THRESHOLD_SECONDS) { + if ( + Number.isFinite(remaining) && + remaining <= CLOSE_THRESHOLD_SECONDS + ) { handleClose(); } } @@ -99,6 +105,7 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => { pointerEvents="box-none" style={{ position: "absolute", + paddingHorizontal: 10, top: 0, left: 0, right: 0, @@ -112,8 +119,16 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => { { }; -const noop = () => { }; +const noopAsync = async () => {}; +const noop = () => {}; const DEFAULT_CONTEXT = { currentTrack: null, @@ -274,36 +274,36 @@ const PlayerProvider = ({ children }) => { (items = [], options = {}) => { const normalized = Array.isArray(items) ? items - .map((item) => { - if (!item) return null; - if (typeof item === "object") { - const { metadata, context, ...rest } = item; - const safeMetadata = - metadata && typeof metadata === "object" - ? { ...metadata } - : undefined; - if (safeMetadata) { - delete safeMetadata.queue; + .map((item) => { + if (!item) return null; + if (typeof item === "object") { + const { metadata, context, ...rest } = item; + const safeMetadata = + metadata && typeof metadata === "object" + ? { ...metadata } + : undefined; + if (safeMetadata) { + delete safeMetadata.queue; + } + const safeContext = + context && typeof context === "object" + ? { ...context } + : undefined; + if (safeContext) { + delete safeContext.queue; + } + return normalizeTrack( + { + ...rest, + ...(safeMetadata ? { metadata: safeMetadata } : {}), + ...(safeContext ? { context: safeContext } : {}), + }, + {} + ); } - const safeContext = - context && typeof context === "object" - ? { ...context } - : undefined; - if (safeContext) { - delete safeContext.queue; - } - return normalizeTrack( - { - ...rest, - ...(safeMetadata ? { metadata: safeMetadata } : {}), - ...(safeContext ? { context: safeContext } : {}), - }, - {} - ); - } - return normalizeTrack(item, {}); - }) - .filter((track) => !!track?.source) + return normalizeTrack(item, {}); + }) + .filter((track) => !!track?.source) : []; queueRef.current = normalized; @@ -450,10 +450,9 @@ const PlayerProvider = ({ children }) => { if (!player) return; try { player.loop = !!isLooping; - } catch (_err) { } + } catch (_err) {} }, [player, isLooping]); - const seekDebounceRef = useRef(null); const pendingSeekPosRef = useRef(null); const isSeekingRef = useRef(false); @@ -511,9 +510,12 @@ const PlayerProvider = ({ children }) => { const seekBy = useCallback( async (deltaMs) => { - const currentPos = pendingSeekPosRef.current !== null - ? (Platform.OS === "web" ? pendingSeekPosRef.current : pendingSeekPosRef.current * 1000) - : playback.positionMs; + const currentPos = + pendingSeekPosRef.current !== null + ? Platform.OS === "web" + ? pendingSeekPosRef.current + : pendingSeekPosRef.current * 1000 + : playback.positionMs; const next = Math.max(0, currentPos + Number(deltaMs || 0)); await seekTo(next); }, @@ -548,7 +550,6 @@ const PlayerProvider = ({ children }) => { await waitForActiveSeek(); }, [player, status?.isLoaded, waitForActiveSeek]); - const play = useCallback( async (trackInput, options = {}) => { const normalized = normalizeTrack(trackInput, options); @@ -680,8 +681,6 @@ const PlayerProvider = ({ children }) => { [currentTrack, playback.isPlaying, pause, play, resume] ); - - const stop = useCallback(async () => { try { autoPlayRef.current = false; @@ -720,7 +719,9 @@ const PlayerProvider = ({ children }) => { } return; } - const idx = queueRef.current.findIndex((item) => item.id === currentTrack.id); + const idx = queueRef.current.findIndex( + (item) => item.id === currentTrack.id + ); if (idx !== queueIndexRef.current) { setQueueIndexValue(idx); } diff --git a/src/screens/Playback/RecordPlayback.js b/src/screens/Playback/RecordPlayback.js index ae99255..8e6f03c 100644 --- a/src/screens/Playback/RecordPlayback.js +++ b/src/screens/Playback/RecordPlayback.js @@ -8,7 +8,13 @@ import React, { useRef, useState, } from "react"; -import { BackHandler, Text, TouchableOpacity, View } from "react-native"; +import { + AppState, + BackHandler, + Text, + TouchableOpacity, + View, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import Svg, { Circle } from "react-native-svg"; import alert from "../../components/Alert"; @@ -56,6 +62,7 @@ const RecordPlayback = ({ route }) => { const isPausedRef = useRef(false); const stopRequestedRef = useRef(false); const restartRequestedRef = useRef(false); + const interruptedRef = useRef(false); // New ref to track interruptions const manualRestartInFlightRef = useRef(false); const stopRequestedAtRef = useRef(0); const activeRecordingPromiseRef = useRef(null); @@ -107,6 +114,43 @@ const RecordPlayback = ({ route }) => { isPausedRef.current = isPaused; }, [isPaused]); + // Monitor AppState to detect backgrounding/interruptions + useEffect(() => { + const subscription = AppState.addEventListener("change", (nextAppState) => { + if ( + nextAppState.match(/inactive|background/) && + (isRecording || isPreparing) + ) { + log("App backgrounded/interrupted during recording session"); + interruptedRef.current = true; + + // If we are merely preparing (countdown), we can just stop the timer + if (isPreparing) { + if (countdownTimerRef.current) { + clearInterval(countdownTimerRef.current); + countdownTimerRef.current = null; + } + setIsPreparing(false); + setCountdown(0); + } + + // If recording, request stop immediately. + // The recording promise will resolve, and we will check interruptedRef there. + if (isRecording) { + stopRequestedRef.current = true; + stopRequestedAtRef.current = Date.now(); + try { + cameraRef.current?.stopRecording?.(); + } catch (_) {} + } + } + }); + + return () => { + subscription.remove(); + }; + }, [isRecording, isPreparing]); + const musicIndex = useMemo(() => { const i = Number(songIndex); return Number.isFinite(i) && i >= 0 ? i : 0; @@ -451,6 +495,9 @@ const RecordPlayback = ({ route }) => { try { stopRequestedRef.current = false; stopRequestedAtRef.current = 0; + stopRequestedRef.current = false; + stopRequestedAtRef.current = 0; + interruptedRef.current = false; // Reset interruption flag listenedMsRef.current = 0; incrementDoneRef.current = false; @@ -470,7 +517,9 @@ const RecordPlayback = ({ route }) => { }); if (activeRecordingPromiseRef.current) { - log("Waiting for previous recording to finish before starting a new one"); + log( + "Waiting for previous recording to finish before starting a new one" + ); try { await activeRecordingPromiseRef.current; } catch (error) { @@ -586,7 +635,8 @@ const RecordPlayback = ({ route }) => { if (!player) return; if (isPausedRef.current) return; if (stopRequestedRef.current) { - const sinceLastRequest = Date.now() - (stopRequestedAtRef.current || 0); + const sinceLastRequest = + Date.now() - (stopRequestedAtRef.current || 0); if (sinceLastRequest >= 1200) { stopRequestedAtRef.current = Date.now(); try { @@ -657,6 +707,22 @@ const RecordPlayback = ({ route }) => { progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }; const exitRequested = exitRequestedRef.current; const shouldRestart = restartRequestedRef.current; + const wasInterrupted = interruptedRef.current; + + if (wasInterrupted) { + log( + "Recording was interrupted (background/inactive), discarding and exiting" + ); + exitRequestedRef.current = false; + restartRequestedRef.current = false; + manualRestartInFlightRef.current = false; + interruptedRef.current = false; + await discardRecordingFile(video?.uri, "interrupted"); + try { + goBack(); + } catch (_) {} + return; + } if (exitRequested) { exitRequestedRef.current = false;