more fixes web + mobile

This commit is contained in:
Thomas Demirdjian
2025-11-20 11:26:24 +01:00
parent fe9b2b9e8d
commit 70c94c97b4
32 changed files with 1214 additions and 226 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ const Playback = ({ route, navigation }) => {
return (
<Page headerType="NONE" backgroundImg={background.playbackBG2}>
<Image source={ai.john} style={styles.img} resizeMode="contain" />
<Image source={ai.theo} style={styles.img} resizeMode="contain" />
<MusicLandHeader
onPressBack={() => navigation.navigate(Routes.Home)}
progress={25}
+82
View File
@@ -52,6 +52,7 @@ const RecordPlayback = ({ route }) => {
const countdownTimerRef = useRef(null);
const listenTimerRef = useRef(null);
const checkSongEndRef = useRef(null);
const isPausedRef = useRef(false);
const stopRequestedRef = useRef(false);
const restartRequestedRef = useRef(false);
const manualRestartInFlightRef = useRef(false);
@@ -73,6 +74,7 @@ const RecordPlayback = ({ route }) => {
const [isPreparing, setIsPreparing] = useState(false);
const [countdown, setCountdown] = useState(0);
const [isRecording, setIsRecording] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [showProgress, setShowProgress] = useState(false);
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 });
const [cameraFacing, setCameraFacing] = useState("front");
@@ -100,6 +102,9 @@ const RecordPlayback = ({ route }) => {
useEffect(() => {
latestLoopingValueRef.current = isLooping ?? false;
}, [isLooping]);
useEffect(() => {
isPausedRef.current = isPaused;
}, [isPaused]);
const musicIndex = useMemo(() => {
const i = Number(songIndex);
@@ -302,8 +307,13 @@ const RecordPlayback = ({ route }) => {
setIsPreparing(false);
setIsRecording(false);
setIsPaused(false);
isPausedRef.current = false;
setShowProgress(false);
setCountdown(0);
try {
await cameraRef.current?.resumePreview?.();
} catch (_) {}
if (player) {
try {
@@ -392,6 +402,8 @@ const RecordPlayback = ({ route }) => {
preserveSongEndWatcher,
preserveStopRequest,
});
setIsPaused(false);
isPausedRef.current = false;
if (!preserveRestartFlag) {
restartRequestedRef.current = false;
manualRestartInFlightRef.current = false;
@@ -443,6 +455,11 @@ const RecordPlayback = ({ route }) => {
setIsRecording(true);
setShowProgress(true);
setIsPaused(false);
isPausedRef.current = false;
try {
await cameraRef.current?.resumePreview?.();
} catch (_) {}
playbackStartedRef.current = false;
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 };
log("startRecordingWithMusic", {
@@ -566,6 +583,7 @@ const RecordPlayback = ({ route }) => {
checkSongEndRef.current = setInterval(() => {
try {
if (!player) return;
if (isPausedRef.current) return;
if (stopRequestedRef.current) {
const sinceLastRequest = Date.now() - (stopRequestedAtRef.current || 0);
if (sinceLastRequest >= 1200) {
@@ -630,6 +648,8 @@ const RecordPlayback = ({ route }) => {
}
setIsRecording(false);
setIsPaused(false);
isPausedRef.current = false;
setShowProgress(false);
log("Recording flow completed", { hasVideo: !!video?.uri });
playbackStartedRef.current = false;
@@ -683,6 +703,8 @@ const RecordPlayback = ({ route }) => {
stopRequestedAtRef.current = 0;
setIsRecording(false);
setIsPreparing(false);
setIsPaused(false);
isPausedRef.current = false;
setShowProgress(false);
if (listenTimerRef.current) {
clearInterval(listenTimerRef.current);
@@ -786,6 +808,33 @@ const RecordPlayback = ({ route }) => {
}, [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 () => {
try {
const hasRecordingPending = !!activeRecordingPromiseRef.current;
@@ -794,6 +843,11 @@ const RecordPlayback = ({ route }) => {
isRecording,
hasRecordingPending,
});
setIsPaused(false);
isPausedRef.current = false;
try {
await cameraRef.current?.resumePreview?.();
} catch (_) {}
if (isPreparing) {
if (hasRecordingPending) {
await startCountdownThenRecord({
@@ -980,6 +1034,34 @@ const RecordPlayback = ({ route }) => {
<RestartSpinnerIcon />
</TouchableOpacity>
</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>
+121 -13
View File
@@ -25,6 +25,7 @@ import { FONT_FAMILY } from "../../styles/Fonts";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { background } from "../../assets";
import RestartSpinnerIcon from "../../assets/UI/RestartSpinnerIcon";
import { registerBlobUrl, releaseBlobUrl } from "../../utils/blobUrlCache";
const TIME_BEFORE_INCREMENT_MS = 20000;
const WEB_PREVIEW_WIDTH = 360;
@@ -82,6 +83,7 @@ const RecordPlayback = ({ route }) => {
const stopRecordingPromiseRef = useRef(null);
const stopRecordingResolveRef = useRef(null);
const recordedUrlRef = useRef(null);
const preserveRecordingForNextScreenRef = useRef(false);
const originalLoopingValueRef = useRef({ hasValue: false, value: false });
const latestLoopingValueRef = useRef(isLooping ?? false);
const [mediaReady, setMediaReady] = useState(false);
@@ -91,14 +93,18 @@ const RecordPlayback = ({ route }) => {
const [countdown, setCountdown] = useState(0);
const [isPreparing, setIsPreparing] = useState(false);
const [isRecording, setIsRecording] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [pos, setPos] = useState(0);
const [dur, setDur] = useState(0);
// timers / refs
const isPausedRef = useRef(false);
const progressTimerRef = useRef(null);
const listenTimerRef = useRef(null);
const countdownTimerRef = useRef(null);
const perfStartRef = useRef(null);
const pausedAtRef = useRef(null);
const pausedMsRef = useRef(0);
const listenedMsRef = useRef(0);
const viewsIncrementedRef = useRef(false);
const correctedInitialJumpRef = useRef(false);
@@ -106,6 +112,9 @@ const RecordPlayback = ({ route }) => {
useEffect(() => {
latestLoopingValueRef.current = isLooping ?? false;
}, [isLooping]);
useEffect(() => {
isPausedRef.current = isPaused;
}, [isPaused]);
// Lyrics
const alignedWords = useMemo(() => {
@@ -131,6 +140,10 @@ const RecordPlayback = ({ route }) => {
const resetUI = () => {
setIsPreparing(false);
setIsRecording(false);
setIsPaused(false);
isPausedRef.current = false;
pausedAtRef.current = null;
pausedMsRef.current = 0;
setCountdown(0);
setPos(0);
setDur(0);
@@ -141,12 +154,13 @@ const RecordPlayback = ({ route }) => {
};
const releaseRecordingUrl = useCallback(() => {
if (recordedUrlRef.current) {
try {
URL.revokeObjectURL(recordedUrlRef.current);
} catch {}
recordedUrlRef.current = null;
if (!recordedUrlRef.current) {
preserveRecordingForNextScreenRef.current = false;
return;
}
releaseBlobUrl(recordedUrlRef.current);
recordedUrlRef.current = null;
preserveRecordingForNextScreenRef.current = false;
}, []);
const startRecorder = useCallback(() => {
@@ -203,6 +217,7 @@ const RecordPlayback = ({ route }) => {
});
url = URL.createObjectURL(blob);
recordedUrlRef.current = url;
registerBlobUrl(url, blob);
} else {
releaseRecordingUrl();
}
@@ -273,8 +288,10 @@ const RecordPlayback = ({ route }) => {
} catch (e) {}
try {
await stopRecorderAndGetUrl();
releaseRecordingUrl();
} catch (e) {}
if (!preserveRecordingForNextScreenRef.current) {
releaseRecordingUrl();
}
}, [player, releaseRecordingUrl, stopRecorderAndGetUrl]);
const resetSessionRef = useRef(resetSession);
@@ -410,11 +427,14 @@ const RecordPlayback = ({ route }) => {
mediaStreamRef.current.getTracks().forEach((track) => track.stop());
mediaStreamRef.current = null;
}
releaseRecordingUrl();
if (!preserveRecordingForNextScreenRef.current) {
releaseRecordingUrl();
}
};
}, [releaseRecordingUrl]);
}, [releaseRecordingUrl, preserveRecordingForNextScreenRef]);
const stopAndNavigate = useCallback(async () => {
preserveRecordingForNextScreenRef.current = true;
clearAllTimers();
try {
await player?.pause?.();
@@ -429,6 +449,10 @@ const RecordPlayback = ({ route }) => {
} catch (e) {}
setIsRecording(false);
setIsPaused(false);
isPausedRef.current = false;
pausedAtRef.current = null;
pausedMsRef.current = 0;
navigate(Routes.RecordedPlayback, { project, videoUri: videoUrl || null });
}, [dur, player, pos, project, stopRecorderAndGetUrl]);
@@ -438,11 +462,19 @@ const RecordPlayback = ({ route }) => {
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) / 1000)
? Math.max(
0,
(performance.now() - perfStartRef.current - pausedTotal) / 1000
)
: 0;
let nextDur = rawDur > 0 ? rawDur : dur || 0;
@@ -464,7 +496,7 @@ const RecordPlayback = ({ route }) => {
const timeSinceStart =
perfStartRef.current != null
? performance.now() - perfStartRef.current
? Math.max(0, performance.now() - perfStartRef.current - pausedTotal)
: null;
const shouldTrustFallback =
@@ -484,6 +516,10 @@ const RecordPlayback = ({ route }) => {
setDur(nextDur);
setPos(nextPos);
if (isPausedRef.current) {
return;
}
if (nextDur > 0 && nextPos >= nextDur - 0.3) {
void stopAndNavigate();
}
@@ -516,6 +552,10 @@ const RecordPlayback = ({ route }) => {
const startPlayback = useCallback(async () => {
try {
setIsPaused(false);
isPausedRef.current = false;
pausedAtRef.current = null;
pausedMsRef.current = 0;
await player?.pause?.(); // s'assure qu'on repart propre
await player?.seekTo?.(0); // tente un seek d'amorçage
const recorderStarted = startRecorder();
@@ -530,6 +570,9 @@ const RecordPlayback = ({ route }) => {
} catch (e) {
setIsRecording(false);
setIsPreparing(false);
setIsPaused(false);
isPausedRef.current = false;
pausedAtRef.current = null;
const msg = String(e?.message || e || "");
if (/not supported on the simulator/i.test(msg)) {
alert(
@@ -557,6 +600,8 @@ const RecordPlayback = ({ route }) => {
const startCountdownThenRecord = useCallback(async () => {
if (!songUrl || !canRecord) return;
await resetSession();
setIsPaused(false);
isPausedRef.current = false;
setIsPreparing(true);
setCountdown(5);
if (countdownTimerRef.current) clearInterval(countdownTimerRef.current);
@@ -575,6 +620,43 @@ const RecordPlayback = ({ route }) => {
}, 1000);
}, [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 () => {
try {
await startCountdownThenRecord();
@@ -586,9 +668,7 @@ const RecordPlayback = ({ route }) => {
return (
<Page
style={{ flex: 1 }}
width={"100%"}
maxWidth={800}
containerStyle={{ margin: 0, padding: 0, backgroundColor: Palette.tran }}
containerStyle={{ backgroundColor: Palette.tran }}
headerType="NONE"
backgroundImg={background.playbackBG2}
>
@@ -766,6 +846,34 @@ const RecordPlayback = ({ route }) => {
<RestartSpinnerIcon />
</TouchableOpacity>
</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>
)}
-1
View File
@@ -15,7 +15,6 @@ import { gutters } from "../../styles";
const RecordedPlayback = ({ route }) => {
const { videoUri, project } = route.params || {};
const songUrl = project?.songUrl || null;
console.log("video uri is : ", videoUri);
const audioPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, {
id: project?.id ? `recorded-${project.id}` : songUrl ? `recorded-${songUrl}` : undefined,
title: typeof project?.title === "string" ? project.title : "Sans titre",
+11 -1
View File
@@ -17,6 +17,7 @@ import { goBack, navigate } from "../../navigation/NavigationService";
import { gutters, Palette } from "../../styles";
import { isWeb } from "../../hooks/useLayoutType";
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
import { releaseBlobUrl } from "../../utils/blobUrlCache";
const fmtSeconds = (s) => {
const total = Math.max(0, Math.floor(Number(s || 0)));
@@ -36,7 +37,6 @@ const WEB_PREVIEW_WIDTH = 360;
const RecordedPlayback = ({ route }) => {
const { videoUri, project } = route.params || {};
const songUrl = project?.songUrl || null;
console.log("video uri is : ", videoUri);
// AUDIO PLAYER (expo-audio → seconds)
const audioPlayer = useSharedAudioPlayer(
songUrl ? { uri: songUrl } : undefined,
@@ -58,6 +58,7 @@ const RecordedPlayback = ({ route }) => {
// Optionnel: si tu veux quand même un rendu vidéo sur web si tu as une source
const videoElRef = useRef(null);
const playbackEndedRef = useRef(false);
const shouldPreserveBlobRef = useRef(false);
const [progress, setProgress] = useState({
posS: 0, // secondes
@@ -76,6 +77,14 @@ const RecordedPlayback = ({ route }) => {
} catch {}
}, [audioPlayer]);
useEffect(() => {
return () => {
if (!shouldPreserveBlobRef.current) {
releaseBlobUrl(videoUri || null);
}
};
}, [videoUri]);
// Démarrage / arrêt
useEffect(() => {
playbackEndedRef.current = false;
@@ -345,6 +354,7 @@ const RecordedPlayback = ({ route }) => {
<GradientButton
title="Je valide"
onPress={() => {
shouldPreserveBlobRef.current = true;
navigate(Routes.DownloadSongs, {
action: "playback",
uri: videoUri || null, // peut être null sur web