clear lot of tickets

This commit is contained in:
Thomas Demirdjian
2025-11-07 17:33:06 +01:00
parent 4365f1e46d
commit d7d9211fbe
30 changed files with 970 additions and 355 deletions
+4 -4
View File
@@ -30,15 +30,15 @@ const Playback = ({ route }) => {
}}
>
<View style={{ width: "80%", alignSelf: "center", gap: 12 }}>
<GradientButton
title="Enregistrer mon Playback"
onPress={onPressRecord}
/>
<BorderGradientButton
title="Guide du Playbacker"
onPress={() => setShowIntro(true)}
/>
{/* <BorderGradientButton title="Importer une vidéo" /> */}
<GradientButton
title="Enregistrer mon Playback"
onPress={onPressRecord}
/>
</View>
</View>
<FullscreenIntroVideo
+41 -3
View File
@@ -15,6 +15,7 @@ import GradientButton from "../../components/GradientButton";
import KaraokeLyrics from "../../components/KaraokeLyrics";
import MusicLandHeader from "../../components/MusicLandHeader";
import { increment, projectsRef } from "../../config/firebase";
import usePlayer from "../../hooks/usePlayer";
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
@@ -35,6 +36,7 @@ const RecordPlayback = ({ route }) => {
log("Route params received", { hasProject: !!project });
const projectId = project?.id;
const songIndex = project?.songIndex;
const { setLooping, isLooping } = usePlayer() || {};
// Permissions
const [cameraPermission, requestCameraPermission] = useCameraPermissions();
@@ -48,6 +50,8 @@ const RecordPlayback = ({ route }) => {
const countdownActiveRef = useRef(false); // évite le déclenchement avant 1er tick
const playbackStartedRef = useRef(false); // devient vrai lorsque l'audio progresse réellement
const progressLogRef = useRef({ bucket: -1, lastPos: -1, lastDur: -1 });
const originalLoopingValueRef = useRef({ hasValue: false, value: false });
const latestLoopingValueRef = useRef(isLooping ?? false);
// Compteurs vues
const listenedMsRef = useRef(0);
@@ -80,6 +84,10 @@ const RecordPlayback = ({ route }) => {
log("Screen params", { projectId, songUrl, songIndex });
}, [projectId, songUrl, songIndex]);
useEffect(() => {
latestLoopingValueRef.current = isLooping ?? false;
}, [isLooping]);
const musicIndex = useMemo(() => {
const i = Number(songIndex);
return Number.isFinite(i) && i >= 0 ? i : 0;
@@ -280,12 +288,42 @@ const RecordPlayback = ({ route }) => {
} catch (_) {}
}, [player]);
const resetSessionRef = useRef(resetSession);
useEffect(() => {
resetSessionRef.current = resetSession;
}, [resetSession]);
useFocusEffect(
useCallback(() => {
log("Screen focused, resetting session");
void resetSession();
return () => {};
}, [resetSession])
void resetSessionRef.current?.();
return () => {
log("Screen blurred, stopping playback");
void resetSessionRef.current?.();
};
}, [])
);
useFocusEffect(
useCallback(() => {
if (typeof setLooping === "function") {
originalLoopingValueRef.current = {
hasValue: true,
value: latestLoopingValueRef.current,
};
if (latestLoopingValueRef.current) {
setLooping(false);
}
}
return () => {
if (
typeof setLooping === "function" &&
originalLoopingValueRef.current?.hasValue
) {
setLooping(!!originalLoopingValueRef.current.value);
}
};
}, [setLooping])
);
// Lancer le compte à rebours (le tick décrémente uniquement)
+41 -3
View File
@@ -16,6 +16,7 @@ import KaraokeLyrics from "../../components/KaraokeLyrics";
import MusicLandHeader from "../../components/MusicLandHeader";
import { increment, projectsRef } from "../../config/firebase";
import { isWeb } from "../../hooks/useLayoutType";
import usePlayer from "../../hooks/usePlayer";
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
@@ -59,6 +60,7 @@ const RecordPlayback = ({ route }) => {
const { project } = route.params || {};
const songIndex = Number(project?.songIndex ?? 0) || 0;
const [cameraPermission, requestCameraPermission] = useCameraPermissions();
const { setLooping, isLooping } = usePlayer() || {};
// Player
const songUrl = project?.songUrl || null;
@@ -80,6 +82,8 @@ const RecordPlayback = ({ route }) => {
const stopRecordingPromiseRef = useRef(null);
const stopRecordingResolveRef = useRef(null);
const recordedUrlRef = useRef(null);
const originalLoopingValueRef = useRef({ hasValue: false, value: false });
const latestLoopingValueRef = useRef(isLooping ?? false);
const [mediaReady, setMediaReady] = useState(false);
const [mediaError, setMediaError] = useState(null);
@@ -100,6 +104,10 @@ const RecordPlayback = ({ route }) => {
const correctedInitialJumpRef = useRef(false);
const progressDebugCounterRef = useRef(0);
useEffect(() => {
latestLoopingValueRef.current = isLooping ?? false;
}, [isLooping]);
// Lyrics
const alignedWords = useMemo(() => {
const ts = project?.musicTimestamps?.[songIndex];
@@ -328,11 +336,41 @@ const RecordPlayback = ({ route }) => {
console.log(LOG_PREFIX, "resetSession:end");
}, [player, releaseRecordingUrl, stopRecorderAndGetUrl]);
const resetSessionRef = useRef(resetSession);
useEffect(() => {
resetSessionRef.current = resetSession;
}, [resetSession]);
useFocusEffect(
useCallback(() => {
void resetSession();
return () => {};
}, [resetSession])
if (typeof setLooping === "function") {
originalLoopingValueRef.current = {
hasValue: true,
value: latestLoopingValueRef.current,
};
if (latestLoopingValueRef.current) {
setLooping(false);
}
}
return () => {
if (
typeof setLooping === "function" &&
originalLoopingValueRef.current?.hasValue
) {
setLooping(!!originalLoopingValueRef.current.value);
}
};
}, [setLooping])
);
useFocusEffect(
useCallback(() => {
void resetSessionRef.current?.();
return () => {
console.log(LOG_PREFIX, "focusEffect:cleanup");
void resetSessionRef.current?.();
};
}, [])
);
useEffect(() => {
+87 -12
View File
@@ -1,9 +1,9 @@
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
import * as FileSystem from "expo-file-system";
import { VideoView, useVideoPlayer } from "expo-video";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { View } from "react-native";
import { background } from "../../assets";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Image, Pressable, View } from "react-native";
import { background, icons } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
@@ -30,8 +30,13 @@ const RecordedPlayback = ({ route }) => {
p.timeUpdateEventInterval = 0.2;
});
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 });
const [progressInfo, setProgressInfo] = useState({
pos: 0,
dur: 0,
isPlaying: false,
});
const wasPlayingBeforeSeek = useRef(false);
const playbackEndedRef = useRef(false);
// Format mm:ss
const fmt = (ms) => {
@@ -44,7 +49,17 @@ const RecordedPlayback = ({ route }) => {
};
// Start both players on mount
const stopPlayback = useCallback(async () => {
try {
if (audioPlayer?.playing) await audioPlayer.pause?.();
} catch (e) {}
try {
if (videoPlayer?.playing) videoPlayer.pause();
} catch (e) {}
}, [audioPlayer, videoPlayer]);
useEffect(() => {
playbackEndedRef.current = false;
const start = async () => {
try {
if (audioPlayer && songUrl) await audioPlayer.play?.();
@@ -53,12 +68,10 @@ const RecordedPlayback = ({ route }) => {
};
start();
return () => {
try {
if (audioPlayer?.playing) audioPlayer.pause?.();
if (videoPlayer?.playing) videoPlayer.pause();
} catch (e) {}
playbackEndedRef.current = false;
void stopPlayback();
};
}, [audioPlayer, videoPlayer, songUrl]);
}, [audioPlayer, songUrl, stopPlayback, videoPlayer]);
// Poll from audio player for progress display; keep video in sync if drifting
useEffect(() => {
@@ -66,7 +79,8 @@ const RecordedPlayback = ({ route }) => {
try {
const dur = (audioPlayer?.duration || 0) * 1000;
const pos = (audioPlayer?.currentTime || 0) * 1000;
setProgressInfo({ pos, dur });
const playing = !!audioPlayer?.playing || !!videoPlayer?.playing;
setProgressInfo({ pos, dur, isPlaying: playing });
// basic drift correction: if desync > 300ms, align video
if (videoPlayer && !Number.isNaN(videoPlayer.currentTime)) {
@@ -94,6 +108,7 @@ const RecordedPlayback = ({ route }) => {
const onSeekStart = async () => {
try {
wasPlayingBeforeSeek.current = !!audioPlayer?.playing;
playbackEndedRef.current = false;
if (audioPlayer?.playing) await audioPlayer.pause?.();
if (videoPlayer?.playing) videoPlayer.pause();
} catch (e) {}
@@ -113,6 +128,47 @@ const RecordedPlayback = ({ route }) => {
: 0;
}, [progressInfo]);
useEffect(() => {
const duration = progressInfo?.dur || 0;
if (!duration) return;
const position = progressInfo?.pos || 0;
if (playbackEndedRef.current && duration - position > 1000) {
playbackEndedRef.current = false;
return;
}
const remaining = Math.max(0, duration - position);
if (remaining <= 400 && !playbackEndedRef.current) {
playbackEndedRef.current = true;
void stopPlayback();
}
}, [progressInfo, stopPlayback]);
const handleTogglePlayback = async () => {
try {
const duration = progressInfo?.dur || 0;
const position = progressInfo?.pos || 0;
const isAtEnd = duration > 0 && duration - position < 350;
const isCurrentlyPlaying =
!!audioPlayer?.playing || !!videoPlayer?.playing;
if (isCurrentlyPlaying) {
playbackEndedRef.current = false;
if (audioPlayer?.playing) await audioPlayer.pause?.();
if (videoPlayer?.playing) videoPlayer.pause();
return;
}
if (isAtEnd) {
if (audioPlayer) await audioPlayer.seekTo?.(0);
if (videoPlayer) videoPlayer.currentTime = 0;
}
playbackEndedRef.current = false;
if (songUrl && audioPlayer) await audioPlayer.play?.();
if (videoPlayer) videoPlayer.play();
} catch (e) {}
};
return (
<Page backgroundImg={background.playbackBG2} headerType="NONE">
<MusicLandHeader progress={19} onPressBack={goBack} />
@@ -144,6 +200,26 @@ const RecordedPlayback = ({ route }) => {
onSeekStart={onSeekStart}
onSeekEnd={onSeekEnd}
/>
<Pressable
onPress={handleTogglePlayback}
style={{
width: 72,
height: 72,
borderRadius: 36,
alignSelf: "center",
marginTop: 4,
alignItems: "center",
justifyContent: "center",
backgroundColor: "rgba(10, 5, 24, 0.65)",
borderWidth: 1,
borderColor: "#F94697",
}}
>
<Image
source={progressInfo.isPlaying ? icons.pause : icons.play}
style={{ width: 26, height: 26 }}
/>
</Pressable>
</View>
<View
style={{ width: "80%", alignSelf: "center", marginTop: 4, gap: 12 }}
@@ -162,8 +238,7 @@ const RecordedPlayback = ({ route }) => {
title="Recommencer"
onPress={async () => {
try {
if (audioPlayer?.playing) await audioPlayer.pause?.();
if (videoPlayer?.playing) videoPlayer.pause();
await stopPlayback();
} catch (e) {}
try {
if (videoUri) {
+35 -18
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Text, View } from "react-native";
import { background } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
@@ -43,6 +43,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 [progress, setProgress] = useState({
posS: 0, // secondes
@@ -50,9 +51,20 @@ const RecordedPlayback = ({ route }) => {
playing: false,
});
const stopPlayback = useCallback(async () => {
try {
if (audioPlayer?.playing) await audioPlayer.pause?.();
} catch {}
try {
if (videoElRef.current && !videoElRef.current.paused) {
videoElRef.current.pause();
}
} catch {}
}, [audioPlayer]);
// Démarrage / arrêt
useEffect(() => {
let mounted = true;
playbackEndedRef.current = false;
const start = async () => {
try {
if (audioPlayer && songUrl) {
@@ -68,17 +80,10 @@ const RecordedPlayback = ({ route }) => {
start();
return () => {
mounted = false;
try {
if (audioPlayer?.playing) audioPlayer.pause?.();
} catch {}
try {
if (videoElRef.current) {
videoElRef.current.pause();
}
} catch {}
playbackEndedRef.current = false;
void stopPlayback();
};
}, [audioPlayer, songUrl, videoUri]);
}, [audioPlayer, songUrl, stopPlayback, videoUri]);
// Boucle de progression + éventuelle sync de la vidéo si fournie
useEffect(() => {
@@ -115,6 +120,21 @@ const RecordedPlayback = ({ route }) => {
return progress.durS > 0 ? Math.min(1, progress.posS / progress.durS) : 0;
}, [progress]);
useEffect(() => {
const duration = progress?.durS || 0;
if (!duration) return;
const position = progress?.posS || 0;
if (playbackEndedRef.current && duration - position > 1) {
playbackEndedRef.current = false;
return;
}
const remaining = Math.max(0, duration - position);
if (remaining <= 0.35 && !playbackEndedRef.current) {
playbackEndedRef.current = true;
void stopPlayback();
}
}, [progress, stopPlayback]);
const onSeek = async (ratio) => {
try {
const durS = Number(progress.durS || 0);
@@ -132,6 +152,7 @@ const RecordedPlayback = ({ route }) => {
const onSeekStart = async () => {
try {
wasPlayingRef.current = !!audioPlayer?.playing;
playbackEndedRef.current = false;
if (audioPlayer?.playing) await audioPlayer.pause?.();
if (videoElRef.current && !videoElRef.current.paused) {
videoElRef.current.pause();
@@ -244,13 +265,9 @@ const RecordedPlayback = ({ route }) => {
/>
<BorderGradientButton
title="Recommencer"
onPress={() => {
onPress={async () => {
try {
if (audioPlayer?.playing) audioPlayer.pause?.();
} catch {}
try {
if (videoElRef.current && !videoElRef.current.paused)
videoElRef.current.pause();
await stopPlayback();
} catch {}
navigate(Routes.RecordPlayback, { project });
}}