feat: clean playback web

This commit is contained in:
2025-11-17 11:13:26 +01:00
parent add6a707a2
commit d2094b453e
+212 -382
View File
@@ -15,7 +15,6 @@ import GradientButton from "../../components/GradientButton";
import KaraokeLyrics from "../../components/KaraokeLyrics"; import KaraokeLyrics from "../../components/KaraokeLyrics";
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from "../../components/MusicLandHeader";
import { increment, projectsRef } from "../../config/firebase"; import { increment, projectsRef } from "../../config/firebase";
import { isWeb } from "../../hooks/useLayoutType";
import usePlayer from "../../hooks/usePlayer"; import usePlayer from "../../hooks/usePlayer";
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer"; import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
@@ -25,11 +24,9 @@ import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { background } from "../../assets"; import { background } from "../../assets";
import playbackBG2 from "../../assets/UI/playbackBG2.png";
import RestartSpinnerIcon from "../../assets/UI/RestartSpinnerIcon"; import RestartSpinnerIcon from "../../assets/UI/RestartSpinnerIcon";
const TIME_BEFORE_INCREMENT_MS = 20000; const TIME_BEFORE_INCREMENT_MS = 20000;
const LOG_PREFIX = "[RecordPlayback.web]";
const WEB_PREVIEW_WIDTH = 360; const WEB_PREVIEW_WIDTH = 360;
const toSeconds = (v) => { const toSeconds = (v) => {
@@ -105,7 +102,6 @@ const RecordPlayback = ({ route }) => {
const listenedMsRef = useRef(0); const listenedMsRef = useRef(0);
const viewsIncrementedRef = useRef(false); const viewsIncrementedRef = useRef(false);
const correctedInitialJumpRef = useRef(false); const correctedInitialJumpRef = useRef(false);
const progressDebugCounterRef = useRef(0);
useEffect(() => { useEffect(() => {
latestLoopingValueRef.current = isLooping ?? false; latestLoopingValueRef.current = isLooping ?? false;
@@ -123,7 +119,6 @@ const RecordPlayback = ({ route }) => {
}, [project?.musicTimestamps, songIndex]); }, [project?.musicTimestamps, songIndex]);
const clearAllTimers = () => { const clearAllTimers = () => {
console.log(LOG_PREFIX, "clearAllTimers");
if (progressTimerRef.current) clearInterval(progressTimerRef.current); 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);
@@ -134,7 +129,6 @@ const RecordPlayback = ({ route }) => {
}; };
const resetUI = () => { const resetUI = () => {
console.log(LOG_PREFIX, "resetUI");
setIsPreparing(false); setIsPreparing(false);
setIsRecording(false); setIsRecording(false);
setCountdown(0); setCountdown(0);
@@ -157,7 +151,6 @@ const RecordPlayback = ({ route }) => {
const startRecorder = useCallback(() => { const startRecorder = useCallback(() => {
if (!mediaStreamRef.current) { if (!mediaStreamRef.current) {
console.log(LOG_PREFIX, "mediaRecorder:start:noStream");
return false; return false;
} }
@@ -168,13 +161,7 @@ const RecordPlayback = ({ route }) => {
) { ) {
mediaRecorderRef.current.stop(); mediaRecorderRef.current.stop();
} }
} catch (e) { } catch (e) {}
console.log(
LOG_PREFIX,
"mediaRecorder:stopPrevious:error",
String(e?.message || e || ""),
);
}
recordedChunksRef.current = []; recordedChunksRef.current = [];
releaseRecordingUrl(); releaseRecordingUrl();
@@ -187,11 +174,6 @@ const RecordPlayback = ({ route }) => {
? new MediaRecorder(mediaStreamRef.current, { mimeType }) ? new MediaRecorder(mediaStreamRef.current, { mimeType })
: new MediaRecorder(mediaStreamRef.current); : new MediaRecorder(mediaStreamRef.current);
} catch (e) { } catch (e) {
console.log(
LOG_PREFIX,
"mediaRecorder:createError",
String(e?.message || e || ""),
);
setMediaError(e instanceof Error ? e : new Error(String(e || ""))); setMediaError(e instanceof Error ? e : new Error(String(e || "")));
return false; return false;
} }
@@ -209,11 +191,6 @@ const RecordPlayback = ({ route }) => {
recorder.onerror = (event) => { recorder.onerror = (event) => {
const err = event?.error || event; const err = event?.error || event;
console.log(
LOG_PREFIX,
"mediaRecorder:error",
String(err?.message || err || ""),
);
setMediaError(err instanceof Error ? err : new Error(String(err || ""))); setMediaError(err instanceof Error ? err : new Error(String(err || "")));
}; };
@@ -230,11 +207,6 @@ const RecordPlayback = ({ route }) => {
releaseRecordingUrl(); releaseRecordingUrl();
} }
} catch (e) { } catch (e) {
console.log(
LOG_PREFIX,
"mediaRecorder:onstop:createBlobError",
String(e?.message || e || ""),
);
releaseRecordingUrl(); releaseRecordingUrl();
} }
if (stopRecordingResolveRef.current) { if (stopRecordingResolveRef.current) {
@@ -248,16 +220,8 @@ const RecordPlayback = ({ route }) => {
try { try {
recorder.start(1000); recorder.start(1000);
console.log(LOG_PREFIX, "mediaRecorder:start", {
mimeType: recorder.mimeType,
});
return true; return true;
} catch (e) { } catch (e) {
console.log(
LOG_PREFIX,
"mediaRecorder:startError",
String(e?.message || e || ""),
);
if (stopRecordingResolveRef.current) { if (stopRecordingResolveRef.current) {
stopRecordingResolveRef.current(null); stopRecordingResolveRef.current(null);
stopRecordingResolveRef.current = null; stopRecordingResolveRef.current = null;
@@ -282,13 +246,7 @@ const RecordPlayback = ({ route }) => {
} }
recorder.stop(); recorder.stop();
} }
} catch (e) { } catch (e) {}
console.log(
LOG_PREFIX,
"mediaRecorder:stopError",
String(e?.message || e || ""),
);
}
if (!waitPromise) { if (!waitPromise) {
return recordedUrlRef.current || null; return recordedUrlRef.current || null;
@@ -298,11 +256,6 @@ const RecordPlayback = ({ route }) => {
try { try {
url = await waitPromise; url = await waitPromise;
} catch (e) { } catch (e) {
console.log(
LOG_PREFIX,
"mediaRecorder:waitStopError",
String(e?.message || e || ""),
);
} finally { } finally {
stopRecordingPromiseRef.current = null; stopRecordingPromiseRef.current = null;
stopRecordingResolveRef.current = null; stopRecordingResolveRef.current = null;
@@ -312,31 +265,16 @@ const RecordPlayback = ({ route }) => {
}, []); }, []);
const resetSession = useCallback(async () => { const resetSession = useCallback(async () => {
console.log(LOG_PREFIX, "resetSession:start");
clearAllTimers(); clearAllTimers();
resetUI(); resetUI();
try { try {
await player?.pause?.(); await player?.pause?.();
await player?.seekTo?.(0); await player?.seekTo?.(0);
console.log(LOG_PREFIX, "resetSession:playerReset"); } catch (e) {}
} catch (e) {
console.log(
LOG_PREFIX,
"resetSession:error",
String(e?.message || e || ""),
);
}
try { try {
await stopRecorderAndGetUrl(); await stopRecorderAndGetUrl();
releaseRecordingUrl(); releaseRecordingUrl();
} catch (e) { } catch (e) {}
console.log(
LOG_PREFIX,
"resetSession:recorderError",
String(e?.message || e || ""),
);
}
console.log(LOG_PREFIX, "resetSession:end");
}, [player, releaseRecordingUrl, stopRecorderAndGetUrl]); }, [player, releaseRecordingUrl, stopRecorderAndGetUrl]);
const resetSessionRef = useRef(resetSession); const resetSessionRef = useRef(resetSession);
@@ -363,34 +301,25 @@ const RecordPlayback = ({ route }) => {
setLooping(!!originalLoopingValueRef.current.value); setLooping(!!originalLoopingValueRef.current.value);
} }
}; };
}, [setLooping]), }, [setLooping])
); );
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
void resetSessionRef.current?.(); void resetSessionRef.current?.();
return () => { return () => {
console.log(LOG_PREFIX, "focusEffect:cleanup");
void resetSessionRef.current?.(); void resetSessionRef.current?.();
}; };
}, []), }, [])
); );
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
if (!cameraPermission?.granted) { if (!cameraPermission?.granted) {
console.log(LOG_PREFIX, "requestingCameraPermission");
await requestCameraPermission(); await requestCameraPermission();
console.log(LOG_PREFIX, "requestingCameraPermission:done");
} }
} catch (e) { } catch (e) {}
console.log(
LOG_PREFIX,
"requestingCameraPermission:error",
String(e?.message || e || ""),
);
}
})(); })();
return () => { return () => {
clearAllTimers(); clearAllTimers();
@@ -413,7 +342,7 @@ const RecordPlayback = ({ route }) => {
!navigator.mediaDevices?.getUserMedia !navigator.mediaDevices?.getUserMedia
) { ) {
setMediaError( setMediaError(
new Error("La capture vidéo n'est pas supportée sur ce navigateur"), new Error("La capture vidéo n'est pas supportée sur ce navigateur")
); );
setMediaReady(false); setMediaReady(false);
return; return;
@@ -459,13 +388,7 @@ const RecordPlayback = ({ route }) => {
if (playPromise && typeof playPromise.catch === "function") { if (playPromise && typeof playPromise.catch === "function") {
playPromise.catch(() => {}); playPromise.catch(() => {});
} }
} catch (e) { } catch (e) {}
console.log(
LOG_PREFIX,
"previewVideo:attachError",
String(e?.message || e || ""),
);
}
} else { } else {
try { try {
video.srcObject = null; video.srcObject = null;
@@ -492,22 +415,10 @@ const RecordPlayback = ({ route }) => {
}, [releaseRecordingUrl]); }, [releaseRecordingUrl]);
const stopAndNavigate = useCallback(async () => { const stopAndNavigate = useCallback(async () => {
console.log(LOG_PREFIX, "stopAndNavigate:start", {
pos,
dur,
listenedMs: listenedMsRef.current,
});
clearAllTimers(); clearAllTimers();
try { try {
await player?.pause?.(); await player?.pause?.();
console.log(LOG_PREFIX, "stopAndNavigate:playerPaused"); } catch (e) {}
} catch (e) {
console.log(
LOG_PREFIX,
"stopAndNavigate:pauseError",
String(e?.message || e || ""),
);
}
let videoUrl = null; let videoUrl = null;
try { try {
@@ -515,26 +426,14 @@ const RecordPlayback = ({ route }) => {
if (!videoUrl) { if (!videoUrl) {
videoUrl = recordedUrlRef.current || null; videoUrl = recordedUrlRef.current || null;
} }
} catch (e) { } catch (e) {}
console.log(
LOG_PREFIX,
"stopAndNavigate:recorderError",
String(e?.message || e || ""),
);
}
setIsRecording(false); setIsRecording(false);
console.log(LOG_PREFIX, "stopAndNavigate:navigate", {
route: Routes.RecordedPlayback,
hasVideo: !!videoUrl,
});
navigate(Routes.RecordedPlayback, { project, videoUri: videoUrl || null }); navigate(Routes.RecordedPlayback, { project, videoUri: videoUrl || null });
}, [dur, player, pos, project, stopRecorderAndGetUrl]); }, [dur, player, pos, project, stopRecorderAndGetUrl]);
const startProgressLoop = useCallback(() => { const startProgressLoop = useCallback(() => {
if (progressTimerRef.current) clearInterval(progressTimerRef.current); if (progressTimerRef.current) clearInterval(progressTimerRef.current);
progressDebugCounterRef.current = 0;
console.log(LOG_PREFIX, "startProgressLoop");
progressTimerRef.current = setInterval(async () => { progressTimerRef.current = setInterval(async () => {
const rawDur = toSeconds(player?.duration); const rawDur = toSeconds(player?.duration);
@@ -560,14 +459,7 @@ const RecordPlayback = ({ route }) => {
await player?.seekTo?.(0); await player?.seekTo?.(0);
correctedInitialJumpRef.current = true; correctedInitialJumpRef.current = true;
nextPos = 0; nextPos = 0;
console.log(LOG_PREFIX, "progressLoop:correctedInitialJump"); } catch (e) {}
} catch (e) {
console.log(
LOG_PREFIX,
"progressLoop:correctInitialJumpError",
String(e?.message || e || ""),
);
}
} }
const timeSinceStart = const timeSinceStart =
@@ -584,12 +476,6 @@ const RecordPlayback = ({ route }) => {
nextPos > fallback + 1)); nextPos > fallback + 1));
if (shouldTrustFallback) { if (shouldTrustFallback) {
console.log(LOG_PREFIX, "progressLoop:useFallback", {
rawPos,
fallback,
nextPosBeforeFallback: nextPos,
timeSinceStart,
});
nextPos = fallback; nextPos = fallback;
} }
@@ -598,28 +484,7 @@ const RecordPlayback = ({ route }) => {
setDur(nextDur); setDur(nextDur);
setPos(nextPos); setPos(nextPos);
progressDebugCounterRef.current += 1;
if (
progressDebugCounterRef.current <= 12 ||
nextPos >= nextDur - 0.5 ||
progressDebugCounterRef.current % 20 === 0
) {
console.log(LOG_PREFIX, "progressLoop:tick", {
tick: progressDebugCounterRef.current,
rawDur,
rawPos,
fallback,
nextDur,
nextPos,
playing: !!player?.playing,
});
}
if (nextDur > 0 && nextPos >= nextDur - 0.3) { if (nextDur > 0 && nextPos >= nextDur - 0.3) {
console.log(LOG_PREFIX, "progressLoop:willStop", {
nextDur,
nextPos,
});
void stopAndNavigate(); void stopAndNavigate();
} }
}, 250); }, 250);
@@ -636,58 +501,33 @@ const RecordPlayback = ({ route }) => {
listenedMsRef.current >= TIME_BEFORE_INCREMENT_MS listenedMsRef.current >= TIME_BEFORE_INCREMENT_MS
) { ) {
viewsIncrementedRef.current = true; viewsIncrementedRef.current = true;
console.log(LOG_PREFIX, "listenLoop:incrementViews");
if (project?.id) { if (project?.id) {
try { try {
await projectsRef await projectsRef
.doc(project.id) .doc(project.id)
.set({ views: increment(1) }, { merge: true }); .set({ views: increment(1) }, { merge: true });
console.log(LOG_PREFIX, "listenLoop:incrementViews:done"); } catch (e) {}
} catch (e) {
console.log(
LOG_PREFIX,
"listenLoop:incrementViews:error",
String(e?.message || e || ""),
);
}
} }
} }
} }
} catch (e) { } catch (e) {}
console.log(
LOG_PREFIX,
"listenLoop:error",
String(e?.message || e || ""),
);
}
}, 500); }, 500);
}, [player, project?.id]); }, [player, project?.id]);
const startPlayback = useCallback(async () => { const startPlayback = useCallback(async () => {
console.log(LOG_PREFIX, "startPlayback:start");
try { try {
await player?.pause?.(); // s'assure qu'on repart propre await player?.pause?.(); // s'assure qu'on repart propre
await player?.seekTo?.(0); // tente un seek d'amorçage await player?.seekTo?.(0); // tente un seek d'amorçage
const recorderStarted = startRecorder(); const recorderStarted = startRecorder();
if (!recorderStarted) { if (!recorderStarted) {
console.log(LOG_PREFIX, "startPlayback:recorderNotStarted");
} }
await player?.play?.(); // attend la promesse → l'élément est prêt await player?.play?.(); // attend la promesse → l'élément est prêt
perfStartRef.current = perfStartRef.current ?? performance.now(); perfStartRef.current = perfStartRef.current ?? performance.now();
correctedInitialJumpRef.current = false; // autorise la correction 1-shot correctedInitialJumpRef.current = false; // autorise la correction 1-shot
setIsRecording(true); setIsRecording(true);
console.log(LOG_PREFIX, "startPlayback:playing", {
duration: toSeconds(player?.duration),
currentTime: toSeconds(player?.currentTime),
});
startProgressLoop(); startProgressLoop();
startListenLoop(); startListenLoop();
} catch (e) { } catch (e) {
console.log(
LOG_PREFIX,
"startPlayback:error",
String(e?.message || e || ""),
);
setIsRecording(false); setIsRecording(false);
setIsPreparing(false); setIsPreparing(false);
const msg = String(e?.message || e || ""); const msg = String(e?.message || e || "");
@@ -705,7 +545,7 @@ const RecordPlayback = ({ route }) => {
}, },
}, },
], ],
{ cancelable: false }, { cancelable: false }
); );
} }
} }
@@ -716,7 +556,6 @@ const RecordPlayback = ({ route }) => {
const startCountdownThenRecord = useCallback(async () => { const startCountdownThenRecord = useCallback(async () => {
if (!songUrl || !canRecord) return; if (!songUrl || !canRecord) return;
console.log(LOG_PREFIX, "startCountdownThenRecord");
await resetSession(); await resetSession();
setIsPreparing(true); setIsPreparing(true);
setCountdown(5); setCountdown(5);
@@ -724,7 +563,6 @@ const RecordPlayback = ({ route }) => {
countdownTimerRef.current = setInterval(() => { countdownTimerRef.current = setInterval(() => {
setCountdown((c) => { setCountdown((c) => {
if (c <= 1) { if (c <= 1) {
console.log(LOG_PREFIX, "countdown:launchRecording");
clearAllTimers(); clearAllTimers();
setIsPreparing(false); setIsPreparing(false);
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -732,27 +570,16 @@ const RecordPlayback = ({ route }) => {
}); });
return 0; return 0;
} }
console.log(LOG_PREFIX, "countdown:tick", c - 1);
return c - 1; return c - 1;
}); });
}, 1000); }, 1000);
}, [songUrl, canRecord, resetSession, startPlayback]); }, [songUrl, canRecord, resetSession, startPlayback]);
const handleRestartRecording = useCallback(async () => { const handleRestartRecording = useCallback(async () => {
console.log(LOG_PREFIX, "restartButton:pressed", {
isPreparing,
isRecording,
});
try { try {
await startCountdownThenRecord(); await startCountdownThenRecord();
} catch (e) { } catch (e) {}
console.log( }, [startCountdownThenRecord]);
LOG_PREFIX,
"restartButton:error",
String(e?.message || e || ""),
);
}
}, [isPreparing, isRecording, startCountdownThenRecord]);
const progressRatio = dur > 0 ? Math.min(1, pos / dur) : 0; const progressRatio = dur > 0 ? Math.min(1, pos / dur) : 0;
@@ -761,239 +588,242 @@ const RecordPlayback = ({ route }) => {
style={{ flex: 1 }} style={{ flex: 1 }}
width={"100%"} width={"100%"}
maxWidth={800} maxWidth={800}
containerStyle={{ margin: 0, padding: 0, backgroundColor: Palette.black }} containerStyle={{ margin: 0, padding: 0, backgroundColor: Palette.tran }}
headerType="NONE" headerType="NONE"
backgroundImg={background.playbackBG2} backgroundImg={background.playbackBG2}
> >
<MusicLandHeader progress={9} onPressBack={goBack} /> <MusicLandHeader progress={9} onPressBack={goBack} />
<View <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
style={{
width: isWeb ? WEB_PREVIEW_WIDTH : "100%",
maxWidth: "100%",
aspectRatio: 9 / 16,
overflow: "hidden",
borderRadius: 12,
alignSelf: "center",
position: "relative",
}}
>
<video
ref={previewVideoRef}
autoPlay
playsInline
muted
style={{
width: "100%",
height: "100%",
objectFit: "cover",
transform: "scaleX(-1)",
backgroundColor: "transparent",
}}
/>
{/* Overlay */}
<View <View
style={{ style={{
position: "absolute", width: WEB_PREVIEW_WIDTH,
top: 0, maxWidth: "100%",
left: 0, aspectRatio: 9 / 16,
right: 0, overflow: "hidden",
bottom: 0, borderRadius: 12,
paddingHorizontal: gutters, position: "relative",
paddingTop: top,
paddingBottom: gutters * 2,
}} }}
> >
<View style={{ flex: 1, marginTop: 11 }}> <video
{mediaError && ( ref={previewVideoRef}
autoPlay
playsInline
muted
style={{
width: "100%",
height: "100%",
objectFit: "cover",
transform: "scaleX(-1)",
backgroundColor: "transparent",
}}
/>
{/* Overlay */}
<View
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
paddingHorizontal: gutters,
paddingTop: top,
paddingBottom: gutters * 2,
}}
>
<View style={{ flex: 1, marginTop: 11 }}>
{mediaError && (
<View
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "transparent",
alignItems: "center",
justifyContent: "center",
paddingHorizontal: gutters,
}}
>
<Text
style={{
textAlign: "center",
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
}}
>
{"Caméra indisponible : "}
{String(mediaError?.message || "").trim() ||
"vérifie les permissions"}
</Text>
</View>
)}
{!mediaError && !mediaReady && (
<View
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "transparent",
alignItems: "center",
justifyContent: "center",
}}
>
<Text
style={{
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
}}
>
Initialisation de la caméra
</Text>
</View>
)}
{isPreparing && countdown >= 1 && !isRecording && (
<View
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
alignItems: "center",
justifyContent: "center",
}}
>
<Text
style={{
fontSize: 72,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueBold,
}}
>
{countdown}
</Text>
</View>
)}
</View>
{/* Start */}
{permissionsGranted && !isPreparing && !isRecording && (
<GradientButton
title="Lancer ma musique"
containerStyle={{ width: "80%", alignSelf: "center" }}
disabled={!songUrl || !canRecord}
onPress={startCountdownThenRecord}
/>
)}
{/* Progress circulaire + restart */}
{permissionsGranted && (isRecording || isPreparing) && (
<View <View
style={{ style={{
position: "absolute", position: "absolute",
top: 0,
left: 0, left: 0,
right: 0, right: 0,
// bottom: gutters,
bottom: 0, bottom: 0,
backgroundColor: "transparent",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
paddingHorizontal: gutters,
}} }}
> >
<Text <View
style={{ style={{
textAlign: "center", width: 120,
color: Palette.white, height: 120,
fontFamily: FONT_FAMILY.InterMedium, borderRadius: 120,
alignItems: "center",
justifyContent: "center",
}} }}
> >
{"Caméra indisponible : "} <ProgressRing
{String(mediaError?.message || "").trim() || size={120}
"vérifie les permissions"} strokeWidth={8}
</Text> progress={progressRatio}
/>
<TouchableOpacity
activeOpacity={0.9}
onPress={handleRestartRecording}
style={{
position: "absolute",
width: 70,
height: 70,
borderRadius: 80,
alignItems: "center",
justifyContent: "center",
backgroundColor: Palette.white,
shadowColor: "#000000",
shadowOpacity: 0.12,
shadowRadius: 10,
shadowOffset: { width: 0, height: 4 },
elevation: 4,
}}
>
<RestartSpinnerIcon />
</TouchableOpacity>
</View>
</View> </View>
)} )}
{!mediaError && !mediaReady && (
{/* Permission prompt */}
{!permissionsGranted && (
<View <View
style={{ style={{
position: "absolute", position: "absolute",
top: 0,
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: gutters,
backgroundColor: "transparent", padding: gutters,
alignItems: "center",
justifyContent: "center",
}} }}
> >
<Text <GradientButton
style={{ title="Autoriser la caméra"
color: Palette.white, onPress={async () => {
fontFamily: FONT_FAMILY.InterMedium, try {
if (!cameraPermission?.granted)
await requestCameraPermission();
} catch {}
}} }}
> />
Initialisation de la caméra
</Text>
</View>
)}
{isPreparing && countdown >= 1 && !isRecording && (
<View
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
alignItems: "center",
justifyContent: "center",
}}
>
<Text
style={{
fontSize: 72,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueBold,
}}
>
{countdown}
</Text>
</View> </View>
)} )}
</View> </View>
{/* Start */} {/* Lyrics */}
{permissionsGranted && !isPreparing && !isRecording && ( <View
<GradientButton pointerEvents="box-none"
title="Lancer ma musique" style={{
containerStyle={{ width: "80%", alignSelf: "center" }} position: "absolute",
disabled={!songUrl || !canRecord} left: 0,
onPress={startCountdownThenRecord} right: 0,
/> bottom: bottom + 130,
)} paddingHorizontal: gutters,
}}
{/* Progress circulaire + restart */} >
{permissionsGranted && (isRecording || isPreparing) && ( <CreateLyricsHeader>
<View {isRecording && alignedWords.length > 0 ? (
style={{ <KaraokeLyrics alignedWords={alignedWords} currentTimeS={pos} />
position: "absolute", ) : (
left: 0, <Text
right: 0,
bottom: gutters,
alignItems: "center",
justifyContent: "center",
}}
>
<View
style={{
width: 120,
height: 120,
borderRadius: 120,
alignItems: "center",
justifyContent: "center",
}}
>
<ProgressRing
size={120}
strokeWidth={8}
progress={progressRatio}
/>
<TouchableOpacity
activeOpacity={0.9}
onPress={handleRestartRecording}
style={{ style={{
position: "absolute", fontSize: 16,
width: 70, color: Palette.white,
height: 70, fontFamily: FONT_FAMILY.InterMedium,
borderRadius: 80,
alignItems: "center",
justifyContent: "center",
backgroundColor: Palette.white,
shadowColor: "#000000",
shadowOpacity: 0.12,
shadowRadius: 10,
shadowOffset: { width: 0, height: 4 },
elevation: 4,
}} }}
> >
<RestartSpinnerIcon /> L'enregistrement de ta vidéo commencera lorsque tu lanceras ta
</TouchableOpacity> musique, et s'arrêtera à la fin du morceau.
</View> </Text>
</View> )}
)} </CreateLyricsHeader>
</View>
{/* Permission prompt */}
{!permissionsGranted && (
<View
style={{
position: "absolute",
left: 0,
right: 0,
bottom: gutters,
padding: gutters,
}}
>
<GradientButton
title="Autoriser la caméra"
onPress={async () => {
try {
if (!cameraPermission?.granted)
await requestCameraPermission();
} catch {}
}}
/>
</View>
)}
</View>
{/* Lyrics */}
<View
pointerEvents="box-none"
style={{
position: "absolute",
left: 0,
right: 0,
bottom: bottom + 130,
paddingHorizontal: gutters,
}}
>
<CreateLyricsHeader>
{isRecording && alignedWords.length > 0 ? (
<KaraokeLyrics alignedWords={alignedWords} currentTimeS={pos} />
) : (
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
}}
>
L'enregistrement de ta vidéo commencera lorsque tu lanceras ta
musique, et s'arrêtera à la fin du morceau.
</Text>
)}
</CreateLyricsHeader>
</View> </View>
</View> </View>
{/* </View> */}
</Page> </Page>
); );
}; };