clean new logique

This commit is contained in:
2025-10-23 12:06:36 +02:00
parent 02dbf98928
commit efa6c07cef
4 changed files with 144 additions and 356 deletions
+1 -1
View File
@@ -141,7 +141,7 @@ const MusicCard = ({
// Platform.OS !== "ios" ? "dimezisBlurView" : "none" // Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// } // }
> >
<View style={{ gap: 3, flexShrink: 1 }}> <View style={{ gap: 3, flexShrink: 1, paddingVertical: 11 }}>
<Text numberOfLines={2} style={styles.title}> <Text numberOfLines={2} style={styles.title}>
{title || "Sans titre"} {title || "Sans titre"}
</Text> </Text>
@@ -89,15 +89,9 @@ const SearchResultsList = ({
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
{shouldShowMusics && ( {shouldShowMusics && (
<View style={{ paddingHorizontal: 2 }}> <View style={{ gap: 10 }}>
<CreateLyricsHeader
gradientProps={{
start: { x: 0, y: 0 },
end: { x: 0, y: 1 },
}}
>
{musics.length > 0 && ( {musics.length > 0 && (
<View style={{ gap: 1 }}> <>
{Array.isArray(musics) && {Array.isArray(musics) &&
musics.slice(0, 6).map((project) => ( musics.slice(0, 6).map((project) => (
<MusicCard <MusicCard
@@ -131,24 +125,17 @@ const SearchResultsList = ({
{"Chargement…"} {"Chargement…"}
</Text> </Text>
)} )}
</View> </>
)} )}
{musics.length === 0 && !musicsLoading && ( {musics.length === 0 && !musicsLoading && (
<EmptyText text={"Aucune musique trouvée"} /> <EmptyText text={"Aucune musique trouvée"} />
)} )}
</CreateLyricsHeader>
</View> </View>
)} )}
{shouldShowPlaybacks && ( {shouldShowPlaybacks && (
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}> <View style={{ gap: 10, paddingTop: 10 }}>
<CreateLyricsHeader
gradientProps={{
start: { x: 0, y: 0 },
end: { x: 0, y: 1 },
}}
>
{playbacks.length > 0 && ( {playbacks.length > 0 && (
<View style={{ gap: 10 }}> <>
{Array.isArray(playbacks) && {Array.isArray(playbacks) &&
playbacks.slice(0, 6).map((project) => ( playbacks.slice(0, 6).map((project) => (
<MusicCard <MusicCard
@@ -182,12 +169,11 @@ const SearchResultsList = ({
{"Chargement…"} {"Chargement…"}
</Text> </Text>
)} )}
</View> </>
)} )}
{playbacks.length === 0 && !playbacksLoading && ( {playbacks.length === 0 && !playbacksLoading && (
<EmptyText text={"Aucun playback trouvé"} /> <EmptyText text={"Aucun playback trouvé"} />
)} )}
</CreateLyricsHeader>
</View> </View>
)} )}
{shouldShowUsers && ( {shouldShowUsers && (
+5 -51
View File
@@ -21,7 +21,6 @@ import {
usersRef, usersRef,
} from "../../config/firebase"; } from "../../config/firebase";
import useDataFromRef from "../../hooks/useDataFromRef"; import useDataFromRef from "../../hooks/useDataFromRef";
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
import { Routes } from "../../navigation"; import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService"; import { navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider"; import { useUser } from "../../providers/UserDataProvider";
@@ -32,13 +31,6 @@ import { size } from "../../styles/Style";
const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => { const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
const { currentUID, followUser, unfollowUser } = useUser() || {}; const { currentUID, followUser, unfollowUser } = useUser() || {};
const videoUrl = item?.playbackUrl || null; const videoUrl = item?.playbackUrl || null;
const audioSource = useMemo(() => {
const fromSong = item?.songUrl ? { uri: item.songUrl } : null;
return fromSong;
}, [item]);
const hasExternalAudio = !!audioSource;
const initialLikedBy = Array.isArray(item?.likedBy) ? item.likedBy : []; const initialLikedBy = Array.isArray(item?.likedBy) ? item.likedBy : [];
const [isLiked, setIsLiked] = useState( const [isLiked, setIsLiked] = useState(
currentUID ? initialLikedBy.includes(currentUID) : false currentUID ? initialLikedBy.includes(currentUID) : false
@@ -116,21 +108,9 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
const creatorName = item?.userName; const creatorName = item?.userName;
const audioPlayer = useSharedAudioPlayer(audioSource || undefined, {
id: item?.id
? `playback-${item.id}`
: audioSource?.uri
? `playback-${audioSource.uri}`
: undefined,
title: typeof item?.title === "string" ? item.title : "",
artist: typeof item?.userName === "string" ? item.userName : "",
artwork: item?.coverUrl || null,
coverUrl: item?.coverUrl || null,
metadata: { playbackId: item?.id },
});
const videoPlayer = useVideoPlayer(videoUrl || null, (p) => { const videoPlayer = useVideoPlayer(videoUrl || null, (p) => {
p.loop = false; p.loop = false;
p.muted = true; p.muted = false;
p.timeUpdateEventInterval = 0.2; p.timeUpdateEventInterval = 0.2;
}); });
@@ -138,9 +118,6 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
const toggle = async () => { const toggle = async () => {
try { try {
if (isActive) { if (isActive) {
try {
if (audioPlayer && hasExternalAudio) await audioPlayer.seekTo?.(0);
} catch (e) {}
try { try {
if (videoPlayer) videoPlayer.currentTime = 0; if (videoPlayer) videoPlayer.currentTime = 0;
} catch (e) {} } catch (e) {}
@@ -149,41 +126,21 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
try { try {
if (videoPlayer) videoPlayer.play(); if (videoPlayer) videoPlayer.play();
} catch (e) {} } catch (e) {}
try {
if (audioPlayer && hasExternalAudio) audioPlayer.play?.();
} catch (e) {}
} else { } else {
if (audioPlayer?.playing) await audioPlayer.pause?.();
if (videoPlayer?.playing) videoPlayer.pause(); if (videoPlayer?.playing) videoPlayer.pause();
} }
} catch (e) {} } catch (e) {}
}; };
toggle(); toggle();
}, [isActive, audioPlayer, videoPlayer, hasExternalAudio]); }, [isActive, videoPlayer]);
// Micro-correction initiale uniquement pour absorber un léger décalage réseau
useEffect(() => {
if (!isActive || !hasExternalAudio) return;
const t = global.setTimeout(() => {
try {
const a = audioPlayer?.currentTime || 0;
const v = videoPlayer?.currentTime || 0;
if (Math.abs(a - v) > 0.2 && videoPlayer) {
videoPlayer.currentTime = Math.max(0, a);
}
} catch (e) {}
}, 300);
return () => global.clearTimeout(t);
}, [isActive, hasExternalAudio, audioPlayer, videoPlayer]);
useEffect(() => { useEffect(() => {
return () => { return () => {
try { try {
if (audioPlayer?.playing) audioPlayer.pause?.();
if (videoPlayer?.playing) videoPlayer.pause(); if (videoPlayer?.playing) videoPlayer.pause();
} catch (e) {} } catch (e) {}
}; };
}, [audioPlayer, videoPlayer]); }, [videoPlayer]);
// Build alignedWords from item musicTimestamps // Build alignedWords from item musicTimestamps
const alignedWords = useMemo(() => { const alignedWords = useMemo(() => {
@@ -204,14 +161,11 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
if (!isActive) return; if (!isActive) return;
const id = global.setInterval(() => { const id = global.setInterval(() => {
try { try {
const t = hasExternalAudio setCurrentTimeS(Number(videoPlayer?.currentTime || 0));
? Number(audioPlayer?.currentTime || 0)
: Number(videoPlayer?.currentTime || 0);
setCurrentTimeS(t);
} catch (e) {} } catch (e) {}
}, 250); }, 250);
return () => global.clearInterval(id); return () => global.clearInterval(id);
}, [isActive, hasExternalAudio, audioPlayer, videoPlayer]); }, [isActive, videoPlayer]);
// partage: géré via l'API native Share directement dans l'UI // partage: géré via l'API native Share directement dans l'UI
return ( return (
@@ -23,7 +23,6 @@ import {
projectsRef, projectsRef,
usersRef, usersRef,
} from "../../../config/firebase"; } from "../../../config/firebase";
import useSharedAudioPlayer from "../../../hooks/useSharedAudioPlayer";
import { Routes } from "../../../navigation"; import { Routes } from "../../../navigation";
import { navigate } from "../../../navigation/NavigationService"; import { navigate } from "../../../navigation/NavigationService";
import { useUser } from "../../../providers/UserDataProvider"; import { useUser } from "../../../providers/UserDataProvider";
@@ -37,8 +36,7 @@ const DEBUG_PLAYBACK_WEB = true;
// NOTE: Web-only implementation that uses a native <video> element instead of expo-video // NOTE: Web-only implementation that uses a native <video> element instead of expo-video
// - No Platform checks // - No Platform checks
// - Always relies on an external audio track for playback // - Audio now comes directly from the MP4 playbackUrl
// - Keeps the muted video visuals in sync with the external audio clock
const PlaybackItem = ({ const PlaybackItem = ({
item, item,
@@ -54,7 +52,7 @@ const PlaybackItem = ({
width: viewportWidth, width: viewportWidth,
height: viewportHeight, height: viewportHeight,
}); });
const [openComments, setOpenComments] = useState(true); const [openComments] = useState(true);
const commentInputRef = useRef(null); const commentInputRef = useRef(null);
const { currentUID, followUser, unfollowUser } = useUser() || {}; const { currentUID, followUser, unfollowUser } = useUser() || {};
@@ -62,13 +60,6 @@ const PlaybackItem = ({
const videoRef = useRef(null); // HTMLVideoElement const videoRef = useRef(null); // HTMLVideoElement
const wasActiveRef = useRef(false); const wasActiveRef = useRef(false);
const startedRef = useRef(false); const startedRef = useRef(false);
const debugTickRef = useRef(0);
const pendingSeekRef = useRef(false);
const audioSource = useMemo(() => {
const fromSong = item?.songUrl ? { uri: item.songUrl } : null;
return fromSong;
}, [item?.songUrl]);
const initialLikedBy = Array.isArray(item?.likedBy) ? item.likedBy : []; const initialLikedBy = Array.isArray(item?.likedBy) ? item.likedBy : [];
const [isLiked, setIsLiked] = useState( const [isLiked, setIsLiked] = useState(
@@ -144,185 +135,77 @@ const PlaybackItem = ({
setIsLiked(currentUID ? lb.includes(currentUID) : false); setIsLiked(currentUID ? lb.includes(currentUID) : false);
}, [item?.likedBy, currentUID]); }, [item?.likedBy, currentUID]);
const audioPlayer = useSharedAudioPlayer(audioSource || undefined, { useEffect(() => {
id: item?.id startedRef.current = false;
? `playback-${item.id}` }, [videoUrl]);
: audioSource?.uri
? `playback-${audioSource.uri}`
: undefined,
title: typeof item?.title === "string" ? item.title : "",
artist: typeof item?.userName === "string" ? item.userName : "",
artwork: item?.coverUrl || null,
coverUrl: item?.coverUrl || null,
metadata: { playbackId: item?.id },
});
// Helpers to normalize and map clocks // Start/reset when active, using the video's own audio
const getAudioTimeSeconds = useCallback(() => {
const raw = Number(audioPlayer?.currentTime || 0);
// On web, expo-audio currentTime can be in ms; convert if it looks like ms (>1000)
const seconds = raw > 1000 ? raw / 1000 : raw;
return Number.isFinite(seconds) ? seconds : 0;
}, [audioPlayer]);
const getTargetVideoTime = useCallback(
(el) => {
const sec = getAudioTimeSeconds();
const dur = Number(el?.duration || NaN);
if (Number.isFinite(dur) && dur > 0.3) {
let t = sec % dur;
// Avoid setting time extremely close to the end to prevent immediate loop
const guard = Math.max(0, dur - 0.25);
if (t > guard) t = guard;
return t;
}
return sec;
},
[getAudioTimeSeconds]
);
// --- Playback control helpers (web <video>) ---
const playVideo = useCallback(async () => {
try {
const el = videoRef.current;
if (!el) return;
// Allow autoplay by keeping it muted initially
el.muted = true;
if (el.paused) {
await el.play().catch(() => {});
}
} catch (_e) {}
}, []);
const syncVideoClockToAudio = useCallback(() => {
try {
const el = videoRef.current;
if (!el) return;
const audioTimeS = getAudioTimeSeconds();
const audioIsPlaying = !!audioPlayer?.playing || audioTimeS > 0.05;
if (!audioIsPlaying) return;
const target = getTargetVideoTime(el);
// If not enough data loaded to seek accurately, attach one-time seek on loadeddata
if (el.readyState < 2) {
if (!pendingSeekRef.current) {
pendingSeekRef.current = true;
const handler = () => {
try {
const t = getTargetVideoTime(el);
el.currentTime = t;
if (el.paused) el.play().catch(() => {});
if (DEBUG_PLAYBACK_WEB) {
console.log("[PlaybackItem.web] deferred seek on loadeddata", {
t,
rs: el.readyState,
});
}
} catch (_) {}
pendingSeekRef.current = false;
};
el.addEventListener("loadeddata", handler, { once: true });
}
return;
}
const diff = Math.abs((el.currentTime || 0) - target);
if (diff > 0.5) {
if (DEBUG_PLAYBACK_WEB) {
console.log("[PlaybackItem.web] sync video", {
audioTimeS,
target,
videoTime: Number(el.currentTime || 0),
duration: Number(el.duration || 0),
});
}
el.currentTime = target;
if (el.paused) el.play().catch(() => {});
}
} catch (_e) {}
}, [audioPlayer, getAudioTimeSeconds, getTargetVideoTime]);
// Start/reset when active
useEffect(() => { useEffect(() => {
const el = videoRef.current;
if (!isActive || !el) return;
if (startedRef.current) return;
const start = async () => {
try {
if (DEBUG_PLAYBACK_WEB) {
console.log("[PlaybackItem.web] start()", {
isActive,
audioPlaying: !!audioPlayer?.playing,
audioTime: Number(audioPlayer?.currentTime || 0),
videoPaused: el.paused,
videoTime: Number(el.currentTime || 0),
readyState: el.readyState,
});
}
// 1) Start the visual first to guarantee motion
await playVideo();
// 2) Try to start audio, but don't force sync until it's actually playing
if (audioPlayer && !audioPlayer.playing) {
try {
await audioPlayer.play?.();
} catch (_e) {}
}
// 3) Choose the best clock available for initial background sync
const audioTimeS = getAudioTimeSeconds();
const audioIsPlaying = !!audioPlayer?.playing || audioTimeS > 0.05;
const videoTime = Number(el.currentTime || 0);
const clockTime = audioIsPlaying ? getTargetVideoTime(el) : videoTime;
onBackgroundSync?.({
currentTime: Number.isFinite(clockTime) ? clockTime : 0,
isPlaying: audioIsPlaying || !el.paused,
});
startedRef.current = true;
} catch (_e) {}
};
start();
return () => {};
}, [
isActive,
playVideo,
audioPlayer,
syncVideoClockToAudio,
onBackgroundSync,
]);
useEffect(() => {
if (isActive) return;
try {
if (audioPlayer?.playing) audioPlayer.pause?.();
} catch (_e) {}
const el = videoRef.current; const el = videoRef.current;
if (!el) return; if (!el) return;
if (!isActive) {
try { try {
if (!el.paused) el.pause(); if (!el.paused) el.pause();
} catch (_e) {}
try {
el.muted = true; el.muted = true;
} catch (_e) {} } catch (_e) {}
startedRef.current = false; startedRef.current = false;
}, [isActive, audioPlayer]); onBackgroundSync?.({ isPlaying: false });
return;
}
const start = async () => {
try {
if (!startedRef.current) {
startedRef.current = true;
const resetToStart = () => {
try {
el.currentTime = 0;
} catch (_e) {}
};
if (el.readyState >= 1) {
resetToStart();
} else {
const handleLoaded = () => {
resetToStart();
};
el.addEventListener("loadeddata", handleLoaded, { once: true });
}
}
el.muted = false;
await el.play().catch((error) => {
if (DEBUG_PLAYBACK_WEB) {
console.log("[PlaybackItem.web] play() rejected", error);
}
});
onBackgroundSync?.({
currentTime: Number(el.currentTime || 0),
isPlaying: !el.paused,
});
} catch (_e) {
if (DEBUG_PLAYBACK_WEB) {
console.log("[PlaybackItem.web] start error", _e);
}
}
};
start();
}, [isActive, videoUrl, onBackgroundSync]);
// Pause all on unmount // Pause all on unmount
useEffect(() => { useEffect(() => {
const el = videoRef.current; // snapshot ref for cleanup const el = videoRef.current;
return () => { return () => {
try { try {
if (audioPlayer?.playing) audioPlayer.pause?.();
if (el && !el.paused) el.pause(); if (el && !el.paused) el.pause();
} catch (_e) {} } catch (_e) {}
onBackgroundSync?.({ isPlaying: false }); onBackgroundSync?.({ isPlaying: false });
}; };
}, [audioPlayer, onBackgroundSync]); }, [onBackgroundSync]);
// Lyrics timing // Lyrics timing based on video clock
const alignedWords = useMemo(() => { const alignedWords = useMemo(() => {
const idx = Number(item?.songIndex) || 0; const idx = Number(item?.songIndex) || 0;
const ts = item?.musicTimestamps?.[idx]; const ts = item?.musicTimestamps?.[idx];
@@ -340,53 +223,18 @@ const PlaybackItem = ({
const id = setInterval(() => { const id = setInterval(() => {
try { try {
const el = videoRef.current; const el = videoRef.current;
const audioTimeS = getAudioTimeSeconds();
const audioIsPlaying = !!audioPlayer?.playing || audioTimeS > 0.05;
const videoTime = Number(el?.currentTime || 0); const videoTime = Number(el?.currentTime || 0);
const safeTime = Number.isFinite(videoTime) ? videoTime : 0;
// Lyrics: real audio seconds (normalized), not modulo setCurrentTimeS(safeTime);
const timeForLyrics =
audioIsPlaying && Number.isFinite(audioTimeS)
? audioTimeS
: Number.isFinite(videoTime)
? videoTime
: 0;
setCurrentTimeS(timeForLyrics);
// Keep video roughly in sync with audio only when audio is genuinely playing
if (audioIsPlaying) {
syncVideoClockToAudio();
}
onBackgroundSync?.({ onBackgroundSync?.({
// Background video: use the same modulo/clamped target as the main video to avoid loop-thrashing currentTime: safeTime,
currentTime: getTargetVideoTime(el), isPlaying: !!el && !el.paused,
isPlaying: audioIsPlaying || (!!el && !el.paused),
}); });
if (DEBUG_PLAYBACK_WEB) {
const n = (debugTickRef.current = (debugTickRef.current + 1) % 4);
if (n === 0) {
console.log("[PlaybackItem.web] tick", {
audioPlaying: !!audioPlayer?.playing,
audioTimeS,
videoPaused: !!el?.paused,
videoTime,
started: startedRef.current,
});
}
}
} catch (_e) {} } catch (_e) {}
}, 250); }, 250);
return () => clearInterval(id); return () => clearInterval(id);
}, [ }, [isActive, onBackgroundSync]);
isActive,
audioPlayer,
syncVideoClockToAudio,
onBackgroundSync,
getAudioTimeSeconds,
getTargetVideoTime,
]);
useEffect(() => { useEffect(() => {
if (!onBackgroundSync) return undefined; if (!onBackgroundSync) return undefined;
@@ -497,7 +345,7 @@ const PlaybackItem = ({
ref={videoRef} ref={videoRef}
src={videoUrl} src={videoUrl}
playsInline playsInline
muted muted={!isActive}
preload="auto" preload="auto"
// Using web CSS properties here on purpose // Using web CSS properties here on purpose
style={{ style={{