fix global player web

This commit is contained in:
2025-10-20 16:44:29 +02:00
parent dfc9106f0a
commit c69afd93c8
+167 -70
View File
@@ -1,6 +1,12 @@
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import { Image as ExpoImage } from "expo-image"; import { Image as ExpoImage } from "expo-image";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { import {
Pressable, Pressable,
Image as RNImage, Image as RNImage,
@@ -26,7 +32,7 @@ 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 useTrackController from "../../hooks/useTrackController";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
import { Palette } from "../../styles"; import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
@@ -76,29 +82,40 @@ const MusicDetails = ({ route }) => {
const coverUrl = project?.coverUrl || null; const coverUrl = project?.coverUrl || null;
const songUrl = project?.songUrl || null; const songUrl = project?.songUrl || null;
const sharedPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, { const trackId = useMemo(() => {
id: projectId ? `project-${projectId}` : songUrl ? `song-${songUrl}` : undefined, if (projectId) return `project-${projectId}`;
if (songUrl) return `song-${songUrl}`;
return null;
}, [projectId, songUrl]);
const trackDescriptor = useMemo(() => {
if (!trackId || !songUrl) return null;
return {
id: trackId,
uri: songUrl,
songUrl,
title, title,
artist, artist,
artwork: coverUrl, artwork: coverUrl,
coverUrl, coverUrl,
metadata: { projectId, screen: "MusicDetails" }, metadata: { projectId, screen: "MusicDetails" },
}); context: { projectId, screen: "MusicDetails" },
const playAudio = useCallback(async () => { };
if (sharedPlayer?.play) await sharedPlayer.play(); }, [trackId, songUrl, title, artist, coverUrl, projectId]);
}, [sharedPlayer]);
const pauseAudio = useCallback(async () => { const {
if (sharedPlayer?.pause) await sharedPlayer.pause(); isCurrent: isCurrentTrack,
}, [sharedPlayer]); isPlaying: isTrackPlaying,
const seekAudio = useCallback( positionMs,
(seconds) => { durationMs,
if (sharedPlayer?.seekTo) sharedPlayer.seekTo(seconds); ensureLoaded,
}, pause: pauseTrack,
[sharedPlayer] resume: resumeTrack,
); seekTo: seekTrackTo,
const isPlaying = !!sharedPlayer?.playing; seekBy: seekTrackBy,
const positionMs = Math.max(0, (sharedPlayer?.currentTime || 0) * 1000); } = useTrackController(trackDescriptor);
const durationMs = Math.max(0, (sharedPlayer?.duration || 0) * 1000);
const isPlaying = isTrackPlaying;
// Karaoke aligned words (from timestamps) // Karaoke aligned words (from timestamps)
const alignedWords = useMemo(() => { const alignedWords = useMemo(() => {
@@ -120,7 +137,7 @@ const MusicDetails = ({ route }) => {
useEffect(() => { useEffect(() => {
listenedMsRef.current = 0; listenedMsRef.current = 0;
incrementDoneRef.current = false; incrementDoneRef.current = false;
}, [songUrl]); }, [trackId]);
// Start/stop a timer to accumulate listened milliseconds while playing // Start/stop a timer to accumulate listened milliseconds while playing
useEffect(() => { useEffect(() => {
@@ -165,59 +182,157 @@ const MusicDetails = ({ route }) => {
return `${m}:${s}`; return `${m}:${s}`;
}; };
const togglePlay = async () => { const togglePlay = useCallback(async () => {
if (!songUrl) return; if (!trackDescriptor) return;
try { try {
if (isPlaying) { if (!isCurrentTrack) {
await pauseAudio(); await ensureLoaded({
startPositionMs: positionMs,
autoPlay: true,
});
return;
}
if (isTrackPlaying) {
await pauseTrack();
} else { } else {
await playAudio(); await resumeTrack();
} }
} catch (e) { } catch (e) {
console.log("MusicDetails audio error", e?.message); console.log("MusicDetails audio error", e?.message);
} }
}; }, [
trackDescriptor,
isCurrentTrack,
ensureLoaded,
positionMs,
isTrackPlaying,
pauseTrack,
resumeTrack,
]);
const onSeek = async (ratio) => { const handleSliderSeekStart = useCallback(async () => {
if (!trackDescriptor) return;
try { try {
wasPlayingBeforeSeek.current = isTrackPlaying;
if (!isCurrentTrack) {
await ensureLoaded({
startPositionMs: positionMs,
autoPlay: false,
});
}
if (isTrackPlaying) {
await pauseTrack();
}
} catch (e) {
console.log("MusicDetails seek start error", e?.message);
}
}, [
trackDescriptor,
isTrackPlaying,
isCurrentTrack,
ensureLoaded,
positionMs,
pauseTrack,
]);
const handleSliderSeek = useCallback(
async (ratio) => {
const dur = durationMs || 0; const dur = durationMs || 0;
const pos = Math.floor(dur * ratio); if (!trackDescriptor || dur <= 0) return;
if (dur > 0) { const targetMs = Math.max(0, Math.floor(dur * ratio));
await seekAudio(Math.floor((pos || 0) / 1000)); try {
if (!isCurrentTrack) {
await ensureLoaded({ startPositionMs: targetMs, autoPlay: false });
} else {
await seekTrackTo(targetMs);
} }
} catch (e) { } catch (e) {
console.log("MusicDetails seek error", e?.message); console.log("MusicDetails seek error", e?.message);
} }
}; },
[
trackDescriptor,
durationMs,
isCurrentTrack,
ensureLoaded,
seekTrackTo,
]
);
const seekBy = async (deltaSeconds) => { const handleSliderSeekEnd = useCallback(async () => {
try { try {
const cur = Math.floor((positionMs || 0) / 1000); if (wasPlayingBeforeSeek.current) {
const next = Math.max(0, cur + deltaSeconds); if (!isCurrentTrack) {
await seekAudio(next); await ensureLoaded({
startPositionMs: positionMs,
autoPlay: true,
});
} else {
await resumeTrack();
}
}
} catch (e) {
console.log("MusicDetails seek end error", e?.message);
} finally {
wasPlayingBeforeSeek.current = false;
}
}, [ensureLoaded, isCurrentTrack, positionMs, resumeTrack]);
const handleSeekBySeconds = useCallback(
async (deltaSeconds) => {
if (!trackDescriptor) return;
const deltaMs = Number(deltaSeconds || 0) * 1000;
const target = Math.max(0, (positionMs || 0) + deltaMs);
try {
if (!isCurrentTrack) {
await ensureLoaded({ startPositionMs: target, autoPlay: true });
} else {
await seekTrackBy(deltaMs);
}
} catch (e) { } catch (e) {
console.log("MusicDetails seekBy error", e?.message); console.log("MusicDetails seekBy error", e?.message);
} }
}; },
[
trackDescriptor,
positionMs,
isCurrentTrack,
ensureLoaded,
seekTrackBy,
]
);
const handleLyricsSeek = React.useCallback( const handleLyricsSeek = useCallback(
async (timestampS) => { async (timestampS) => {
if (typeof timestampS !== "number" || Number.isNaN(timestampS)) return; if (
const seconds = Math.max(0, Number(timestampS) || 0); !trackDescriptor ||
const durationS = durationMs > 0 ? durationMs / 1000 : null; typeof timestampS !== "number" ||
const bounded = durationS ? Math.min(seconds, durationS) : seconds; Number.isNaN(timestampS)
)
return;
const targetMs = Math.max(0, Number(timestampS) * 1000);
try { try {
const wasPlaying = isPlaying; if (!isCurrentTrack) {
seekAudio(bounded); await ensureLoaded({ startPositionMs: targetMs, autoPlay: true });
if (wasPlaying) { return;
await playAudio(); }
await seekTrackTo(targetMs);
if (!isTrackPlaying) {
await resumeTrack();
} }
} catch (e) { } catch (e) {
console.log("MusicDetails lyrics seek error", e?.message); console.log("MusicDetails lyrics seek error", e?.message);
} }
}, },
[durationMs, isPlaying, playAudio, seekAudio] [
trackDescriptor,
isCurrentTrack,
ensureLoaded,
seekTrackTo,
isTrackPlaying,
resumeTrack,
]
); );
const description = useMemo(() => { const description = useMemo(() => {
// Build a readable text from lyrics with section labels // Build a readable text from lyrics with section labels
@@ -605,7 +720,7 @@ const MusicDetails = ({ route }) => {
}} }}
> >
{/* Previous (rewind 10s) */} {/* Previous (rewind 10s) */}
<Pressable onPress={() => seekBy(-10)}> <Pressable onPress={() => handleSeekBySeconds(-10)}>
<RNImage <RNImage
source={icons.forward} source={icons.forward}
style={{ style={{
@@ -629,7 +744,7 @@ const MusicDetails = ({ route }) => {
/> />
</PressableScale> </PressableScale>
{/* Next (forward 10s) */} {/* Next (forward 10s) */}
<PressableScale onPress={() => seekBy(10)}> <PressableScale onPress={() => handleSeekBySeconds(10)}>
<RNImage <RNImage
source={icons.forward} source={icons.forward}
style={{ style={{
@@ -644,27 +759,9 @@ const MusicDetails = ({ route }) => {
maxValue={fmt(durationMs)} maxValue={fmt(durationMs)}
progress={durationMs ? (positionMs || 0) / durationMs : 0} progress={durationMs ? (positionMs || 0) / durationMs : 0}
seekEnabled={!!songUrl} seekEnabled={!!songUrl}
onSeekStart={async () => { onSeekStart={handleSliderSeekStart}
try { onSeek={handleSliderSeek}
wasPlayingBeforeSeek.current = !!isPlaying; onSeekEnd={handleSliderSeekEnd}
if (isPlaying) {
await pauseAudio();
}
} catch (e) {
console.log("Pause on seek start error", e?.message);
}
}}
onSeek={onSeek}
onSeekEnd={async () => {
try {
if (wasPlayingBeforeSeek.current) {
await playAudio();
}
wasPlayingBeforeSeek.current = false;
} catch (e) {
console.log("Resume after seek error", e?.message);
}
}}
/> />
</View> </View>
)} )}