playback
This commit is contained in:
+102
-137
@@ -1,5 +1,5 @@
|
||||
import { useRoute } from "@react-navigation/core";
|
||||
import { Audio } from "expo-audio";
|
||||
import { useAudioPlayer } from "expo-audio";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
@@ -11,10 +11,14 @@ import {
|
||||
View,
|
||||
} from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import {
|
||||
responsiveHeight,
|
||||
responsiveWidth,
|
||||
} from "react-native-responsive-dimensions";
|
||||
import { useGlobal } from "reactn";
|
||||
import { background, icons, img } from "../../assets";
|
||||
import { background, icons } from "../../assets";
|
||||
import Slider from "../../components/Slider";
|
||||
import firebase, {
|
||||
import {
|
||||
arrayRemove,
|
||||
arrayUnion,
|
||||
increment,
|
||||
@@ -44,9 +48,7 @@ const MusicDetails = () => {
|
||||
const timerRef = React.useRef(null);
|
||||
|
||||
const { data: project } = useDataFromRef({
|
||||
ref: projectId
|
||||
? firebase.firestore().collection("projects").doc(projectId)
|
||||
: null,
|
||||
ref: projectId ? projectsRef.doc(projectId) : null,
|
||||
simpleRef: true,
|
||||
listener: true,
|
||||
condition: !!projectId,
|
||||
@@ -71,61 +73,27 @@ const MusicDetails = () => {
|
||||
const title = project?.title || "Sans titre";
|
||||
const artist = owner?.userName || "MusicLand";
|
||||
const coverUrl = project?.coverUrl || null;
|
||||
const songUrl = useMemo(() => {
|
||||
if (project?.song?.url) return project.song.url;
|
||||
const arr = Array.isArray(project?.musicUrls) ? project.musicUrls : [];
|
||||
return arr[0] || null;
|
||||
}, [project]);
|
||||
const songUrl = project?.songUrl || null;
|
||||
|
||||
const soundRef = React.useRef(null);
|
||||
const player = useAudioPlayer(songUrl ? { uri: songUrl } : undefined);
|
||||
|
||||
// Load/unload audio with expo-audio for reliable status updates on iOS/Android
|
||||
// Reset counters when the track changes
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const load = async () => {
|
||||
try {
|
||||
// Unload previous
|
||||
if (soundRef.current) {
|
||||
await soundRef.current.unloadAsync();
|
||||
soundRef.current.setOnPlaybackStatusUpdate(null);
|
||||
soundRef.current = null;
|
||||
}
|
||||
// Reset listen tracking per song load
|
||||
listenedMsRef.current = 0;
|
||||
incrementDoneRef.current = false;
|
||||
if (!songUrl) return;
|
||||
const { sound } = await Audio.Sound.createAsync(
|
||||
{ uri: songUrl },
|
||||
{ shouldPlay: false }
|
||||
);
|
||||
sound.setOnPlaybackStatusUpdate((status) => {
|
||||
if (!isMounted) return;
|
||||
if (!status || !status.isLoaded) return;
|
||||
const pos = status.positionMillis || 0;
|
||||
const dur = status.durationMillis || 0;
|
||||
setProgressInfo({ pos, dur });
|
||||
setIsPlaying(!!status.isPlaying);
|
||||
});
|
||||
soundRef.current = sound;
|
||||
} catch (e) {
|
||||
console.log("Audio load error", e?.message);
|
||||
}
|
||||
};
|
||||
load();
|
||||
return () => {
|
||||
isMounted = false;
|
||||
(async () => {
|
||||
try {
|
||||
if (soundRef.current) {
|
||||
await soundRef.current.unloadAsync();
|
||||
soundRef.current.setOnPlaybackStatusUpdate(null);
|
||||
soundRef.current = null;
|
||||
}
|
||||
} catch (_) {}
|
||||
})();
|
||||
};
|
||||
listenedMsRef.current = 0;
|
||||
incrementDoneRef.current = false;
|
||||
}, [songUrl]);
|
||||
|
||||
// Poll player state to update progress and play state
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
const dur = (player?.duration || 0) * 1000;
|
||||
const pos = (player?.currentTime || 0) * 1000;
|
||||
setProgressInfo({ pos, dur });
|
||||
setIsPlaying(!!player?.playing);
|
||||
}, 300);
|
||||
return () => clearInterval(id);
|
||||
}, [player]);
|
||||
|
||||
// Start/stop a timer to accumulate listened milliseconds while playing
|
||||
useEffect(() => {
|
||||
const clearTimer = () => {
|
||||
@@ -170,15 +138,13 @@ const MusicDetails = () => {
|
||||
};
|
||||
|
||||
const togglePlay = async () => {
|
||||
const sound = soundRef.current;
|
||||
if (!sound || !songUrl) return;
|
||||
if (!player || !songUrl) return;
|
||||
try {
|
||||
const status = await sound.getStatusAsync();
|
||||
if (status?.isLoaded && status.isPlaying) {
|
||||
await sound.pauseAsync();
|
||||
if (player.playing) {
|
||||
await player.pause?.();
|
||||
setIsPlaying(false);
|
||||
} else {
|
||||
await sound.playAsync();
|
||||
await player.play?.();
|
||||
setIsPlaying(true);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -190,9 +156,8 @@ const MusicDetails = () => {
|
||||
try {
|
||||
const dur = progressInfo.dur || 0;
|
||||
const pos = Math.floor(dur * ratio);
|
||||
const sound = soundRef.current;
|
||||
if (sound && dur > 0) {
|
||||
await sound.setPositionAsync(pos);
|
||||
if (player && dur > 0) {
|
||||
await player.seekTo?.(Math.floor((pos || 0) / 1000));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("MusicDetails seek error", e?.message);
|
||||
@@ -203,8 +168,7 @@ const MusicDetails = () => {
|
||||
try {
|
||||
const cur = Math.floor((progressInfo.pos || 0) / 1000);
|
||||
const next = Math.max(0, cur + deltaSeconds);
|
||||
const sound = soundRef.current;
|
||||
if (sound) await sound.setPositionAsync(next * 1000);
|
||||
if (player) await player.seekTo?.(next);
|
||||
} catch (e) {
|
||||
console.log("MusicDetails seekBy error", e?.message);
|
||||
}
|
||||
@@ -248,10 +212,9 @@ const MusicDetails = () => {
|
||||
paddingTop: 20,
|
||||
paddingBottom: gutters * 2,
|
||||
}}
|
||||
// stickyHeaderIndices={[1]}
|
||||
>
|
||||
<View style={{ gap: 28 }}>
|
||||
{coverUrl ? (
|
||||
{coverUrl && (
|
||||
<ExpoImage
|
||||
source={{ uri: coverUrl }}
|
||||
cachePolicy="memory-disk"
|
||||
@@ -260,8 +223,6 @@ const MusicDetails = () => {
|
||||
transition={150}
|
||||
style={styles.img}
|
||||
/>
|
||||
) : (
|
||||
<RNImage source={img.placeholder4} style={styles.img} />
|
||||
)}
|
||||
<View style={{ ...Style.containerSpaceBetween }}>
|
||||
<View>
|
||||
@@ -305,79 +266,81 @@ const MusicDetails = () => {
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ paddingTop: 22 }}>
|
||||
<Slider
|
||||
value={fmt(progressInfo.pos)}
|
||||
maxValue={fmt(progressInfo.dur)}
|
||||
progress={
|
||||
progressInfo.dur ? (progressInfo.pos || 0) / progressInfo.dur : 0
|
||||
}
|
||||
seekEnabled={!!songUrl}
|
||||
onSeekStart={async () => {
|
||||
try {
|
||||
const sound = soundRef.current;
|
||||
const status = await sound?.getStatusAsync?.();
|
||||
wasPlayingBeforeSeek.current =
|
||||
!!status?.isLoaded && !!status?.isPlaying;
|
||||
if (status?.isLoaded && status?.isPlaying) {
|
||||
await sound.pauseAsync();
|
||||
setIsPlaying(false);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Pause on seek start error", e?.message);
|
||||
{songUrl && (
|
||||
<View style={{ paddingTop: 22 }}>
|
||||
<Slider
|
||||
value={fmt(progressInfo.pos)}
|
||||
maxValue={fmt(progressInfo.dur)}
|
||||
progress={
|
||||
progressInfo.dur
|
||||
? (progressInfo.pos || 0) / progressInfo.dur
|
||||
: 0
|
||||
}
|
||||
}}
|
||||
onSeek={onSeek}
|
||||
onSeekEnd={async () => {
|
||||
try {
|
||||
const sound = soundRef.current;
|
||||
if (sound && wasPlayingBeforeSeek.current) {
|
||||
await sound.playAsync();
|
||||
setIsPlaying(true);
|
||||
seekEnabled={!!songUrl}
|
||||
onSeekStart={async () => {
|
||||
try {
|
||||
wasPlayingBeforeSeek.current = !!player?.playing;
|
||||
if (player?.playing) {
|
||||
await player.pause?.();
|
||||
setIsPlaying(false);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Pause on seek start error", e?.message);
|
||||
}
|
||||
wasPlayingBeforeSeek.current = false;
|
||||
} catch (e) {
|
||||
console.log("Resume after seek error", e?.message);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<View style={{ ...Style.containerRow, gap: 24, alignSelf: "center" }}>
|
||||
{/* Previous (rewind 10s) */}
|
||||
<Pressable onPress={() => seekBy(-10)}>
|
||||
<RNImage
|
||||
source={icons.forward}
|
||||
style={{
|
||||
...size({ size: 30 }),
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
{/* Play / Pause */}
|
||||
<Pressable
|
||||
style={{
|
||||
...size({ size: 40 }),
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onPress={togglePlay}
|
||||
onSeek={onSeek}
|
||||
onSeekEnd={async () => {
|
||||
try {
|
||||
if (player && wasPlayingBeforeSeek.current) {
|
||||
await player.play?.();
|
||||
setIsPlaying(true);
|
||||
}
|
||||
wasPlayingBeforeSeek.current = false;
|
||||
} catch (e) {
|
||||
console.log("Resume after seek error", e?.message);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{ ...Style.containerRow, gap: 24, alignSelf: "center" }}
|
||||
>
|
||||
<RNImage
|
||||
resizeMode={"contain"}
|
||||
source={isPlaying ? icons.pause : icons.play}
|
||||
style={size({ size: 34 })}
|
||||
/>
|
||||
</Pressable>
|
||||
{/* Next (forward 10s) */}
|
||||
<Pressable onPress={() => seekBy(10)}>
|
||||
<RNImage
|
||||
source={icons.forward}
|
||||
{/* Previous (rewind 10s) */}
|
||||
<Pressable onPress={() => seekBy(-10)}>
|
||||
<RNImage
|
||||
source={icons.forward}
|
||||
style={{
|
||||
...size({ size: 30 }),
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
{/* Play / Pause */}
|
||||
<Pressable
|
||||
style={{
|
||||
...size({ size: 30 }),
|
||||
transform: [{ rotate: "180deg" }],
|
||||
...size({ size: 40 }),
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
onPress={togglePlay}
|
||||
>
|
||||
<RNImage
|
||||
resizeMode={"contain"}
|
||||
source={isPlaying ? icons.pause : icons.play}
|
||||
style={size({ size: 34 })}
|
||||
/>
|
||||
</Pressable>
|
||||
{/* Next (forward 10s) */}
|
||||
<Pressable onPress={() => seekBy(10)}>
|
||||
<RNImage
|
||||
source={icons.forward}
|
||||
style={{
|
||||
...size({ size: 30 }),
|
||||
transform: [{ rotate: "180deg" }],
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{description?.length > 0 && (
|
||||
<View style={{ marginTop: 30, gap: 20 }}>
|
||||
<Text
|
||||
@@ -409,6 +372,8 @@ const styles = StyleSheet.create({
|
||||
fontSize: 20,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.HelveticaNeueMedium,
|
||||
width: responsiveWidth(70),
|
||||
marginBottom: responsiveHeight(1),
|
||||
},
|
||||
name: {
|
||||
fontSize: 16,
|
||||
|
||||
Reference in New Issue
Block a user