Files
musicland/src/screens/Playback/RecordedPlayback.web.js
T
Thomas Demirdjian 72852ad92b last tickets
2025-11-24 15:35:14 +01:00

380 lines
11 KiB
JavaScript

import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Image, Pressable, Text, View } from "react-native";
import { background, icons } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import Slider from "../../components/Slider";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
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)));
const m = Math.floor(total / 60).toString();
const sec = (total % 60).toString().padStart(2, "0");
return `${m}:${sec}`;
};
const toSeconds = (value) => {
const n = Number(value ?? 0);
if (!Number.isFinite(n) || n < 0) return 0;
return n > 10000 ? n / 1000 : n; // heuristique ms → s
};
const WEB_PREVIEW_WIDTH = 360;
const RecordedPlayback = ({ route }) => {
const { videoUri, project } = route.params || {};
const songUrl = project?.songUrl || null;
// AUDIO PLAYER (expo-audio → seconds)
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",
artist:
typeof project?.userName === "string" ? project.userName : "MusicLand",
artwork: project?.coverUrl || null,
coverUrl: project?.coverUrl || null,
metadata: { projectId: project?.id, screen: "RecordedPlayback" },
},
);
// 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
durS: 0, // secondes
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]);
useEffect(() => {
return () => {
if (!shouldPreserveBlobRef.current) {
releaseBlobUrl(videoUri || null);
}
};
}, [videoUri]);
// Démarrage / arrêt
useEffect(() => {
playbackEndedRef.current = false;
const start = async () => {
try {
if (audioPlayer && songUrl) {
await audioPlayer.play?.();
}
if (videoElRef.current && videoUri) {
// Lecture vidéo HTML5 (muet pour éviter les policies)
videoElRef.current.muted = true;
videoElRef.current.play().catch(() => {});
}
} catch {}
};
start();
return () => {
playbackEndedRef.current = false;
void stopPlayback();
};
}, [audioPlayer, songUrl, stopPlayback, videoUri]);
// Boucle de progression + éventuelle sync de la vidéo si fournie
useEffect(() => {
const id = setInterval(() => {
try {
const durS = toSeconds(audioPlayer?.duration); // secondes
const posS = toSeconds(audioPlayer?.currentTime); // secondes
setProgress({
posS,
durS,
playing: !!audioPlayer?.playing,
});
// Sync vidéo si on a une source vidéo
if (
videoElRef.current &&
videoUri &&
!Number.isNaN(videoElRef.current.currentTime)
) {
const v = Number(videoElRef.current.currentTime || 0);
const drift = Math.abs(v - posS);
if (drift > 0.35) {
videoElRef.current.currentTime = Math.max(0, posS);
}
}
} catch {}
}, 250);
return () => clearInterval(id);
}, [audioPlayer, videoUri]);
// Slider: ratio 0..1
const sliderProgress = useMemo(() => {
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);
const target = durS * ratio; // secondes
if (audioPlayer && durS > 0) {
await audioPlayer.seekTo?.(Math.floor(target)); // seek en secondes
}
if (videoElRef.current && videoUri) {
videoElRef.current.currentTime = Math.max(0, target);
}
} catch {}
};
const wasPlayingRef = useRef(false);
const isSeekingRef = useRef(false);
const onSeekStart = useCallback(async () => {
try {
if (isSeekingRef.current) return;
isSeekingRef.current = true;
wasPlayingRef.current = !!audioPlayer?.playing;
playbackEndedRef.current = false;
if (audioPlayer?.playing) await audioPlayer.pause?.();
if (videoElRef.current && !videoElRef.current.paused) {
videoElRef.current.pause();
}
} catch {}
}, [audioPlayer]);
const onSeekEnd = useCallback(async () => {
try {
if (!isSeekingRef.current) return;
isSeekingRef.current = false;
if (wasPlayingRef.current) {
if (audioPlayer) await audioPlayer.play?.();
if (videoElRef.current && videoUri)
videoElRef.current.play().catch(() => {});
}
} catch {}
}, [audioPlayer, videoUri]);
const handleTogglePlayback = useCallback(async () => {
try {
const duration = Number(progress?.durS || 0);
const position = Number(progress?.posS || 0);
const isAtEnd = duration > 0 && duration - position < 0.35;
const isCurrentlyPlaying = !!audioPlayer?.playing;
if (isCurrentlyPlaying) {
playbackEndedRef.current = false;
await stopPlayback();
return;
}
if (isAtEnd) {
if (audioPlayer) await audioPlayer.seekTo?.(0);
if (videoElRef.current && videoUri) {
videoElRef.current.currentTime = 0;
}
}
playbackEndedRef.current = false;
if (songUrl && audioPlayer) {
await audioPlayer.play?.();
}
if (videoElRef.current && videoUri) {
videoElRef.current.muted = true;
videoElRef.current.play().catch(() => {});
}
} catch {}
}, [audioPlayer, songUrl, stopPlayback, progress, videoUri]);
return (
<Page
containerStyle={{
alignItems: "none",
}}
backgroundImg={background.playbackBG2}
headerType="NONE"
>
<MusicLandHeader
progress={19}
onPressBack={goBack}
style={{
marginBottom: 40,
}}
/>
<View
style={{
width: "100%",
}}
>
{/* Bloc vidéo optionnel si jamais tu as un videoUri sur web */}
{videoUri ? (
<Pressable
onPress={handleTogglePlayback}
style={{
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
maxWidth: "100%",
aspectRatio: 9 / 16,
borderRadius: 12,
backgroundColor: "#00000066",
overflow: "hidden",
alignSelf: "center",
position: "relative",
}}
>
<video
ref={videoElRef}
src={videoUri}
muted
playsInline
style={{
width: "100%",
height: "100%",
objectFit: "cover",
backgroundColor: "black",
}}
controls={false}
/>
{!progress.playing && (
<View
pointerEvents="none"
style={{
position: "absolute",
left: "50%",
top: "50%",
transform: [{ translateX: -36 }, { translateY: -36 }],
width: 72,
height: 72,
borderRadius: 36,
alignItems: "center",
justifyContent: "center",
backgroundColor: "rgba(10, 5, 24, 0.75)",
borderWidth: 1,
borderColor: "#F94697",
}}
>
<Image source={icons.play} style={{ width: 26, height: 26 }} />
</View>
)}
</Pressable>
) : (
// Placeholder quand pas de vidéo sur web
<View
style={{
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
maxWidth: "100%",
aspectRatio: 9 / 16,
borderRadius: 12,
backgroundColor: "#00000066",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
alignSelf: "center",
}}
>
<Text style={{ color: Palette.white, opacity: 0.8 }}>
Aperçu vidéo non disponible sur le web
</Text>
</View>
)}
<View
style={{
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
maxWidth: "100%",
alignSelf: "center",
marginTop: 6,
alignItems: "center",
}}
>
<View style={{ width: "100%" }}>
<Slider
value={fmtSeconds(progress.posS)} // mm:ss (seconds)
maxValue={fmtSeconds(progress.durS)} // mm:ss (seconds)
progress={sliderProgress}
seekEnabled={!!songUrl}
onSeek={onSeek}
onSeekStart={onSeekStart}
onSeekEnd={onSeekEnd}
/>
</View>
</View>
</View>
<View
style={{
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
maxWidth: "100%",
alignSelf: "center",
gap: 12,
marginTop: 50,
}}
>
<GradientButton
title="Je valide"
onPress={() => {
shouldPreserveBlobRef.current = true;
navigate(Routes.PlaybackDownload, {
action: "playback",
uri: videoUri || null, // peut être null sur web
project,
});
}}
/>
<BorderGradientButton
title="Recommencer"
onPress={async () => {
try {
await stopPlayback();
} catch {}
navigate(Routes.RecordPlayback, { project });
}}
/>
</View>
</Page>
);
};
export default RecordedPlayback;