fix global player web

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