global player
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import { BlurView } from "expo-blur";
|
||||
import React, { useMemo } from "react";
|
||||
import {
|
||||
Image,
|
||||
Platform,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
import { icons, img } from "../../assets";
|
||||
import usePlayer from "../../hooks/usePlayer";
|
||||
import { Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
|
||||
const formatDuration = (ms) => {
|
||||
const totalSeconds = Math.max(0, Math.floor((ms || 0) / 1000));
|
||||
const minutes = Math.floor(totalSeconds / 60)
|
||||
.toString()
|
||||
.padStart(1, "0");
|
||||
const seconds = (totalSeconds % 60).toString().padStart(2, "0");
|
||||
return `${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const GlobalAudioPlayer = () => {
|
||||
const { currentTrack, isPlaying, positionMs, durationMs, pause, resume } =
|
||||
usePlayer();
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const hasTrack = !!currentTrack?.source;
|
||||
if (!hasTrack) return null;
|
||||
|
||||
const title = currentTrack?.title || "Lecture en cours";
|
||||
const artist = currentTrack?.artist || "";
|
||||
const progress =
|
||||
durationMs > 0 ? Math.min(1, Math.max(0, positionMs / durationMs)) : 0;
|
||||
|
||||
const artworkSource = useMemo(() => {
|
||||
if (typeof currentTrack?.artwork === "number") return currentTrack.artwork;
|
||||
if (currentTrack?.artwork && typeof currentTrack.artwork === "string") {
|
||||
return { uri: currentTrack.artwork };
|
||||
}
|
||||
if (currentTrack?.coverUrl && typeof currentTrack.coverUrl === "string") {
|
||||
return { uri: currentTrack.coverUrl };
|
||||
}
|
||||
return img.placeholder;
|
||||
}, [currentTrack?.artwork, currentTrack?.coverUrl]);
|
||||
|
||||
const handleToggle = async () => {
|
||||
if (isPlaying) {
|
||||
await pause();
|
||||
} else {
|
||||
await resume();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View pointerEvents="box-none" style={styles.host}>
|
||||
<BlurView
|
||||
intensity={Platform.OS === "ios" ? 40 : 15}
|
||||
tint="dark"
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
paddingBottom: Math.max(insets.bottom, 12),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Image source={artworkSource} style={styles.cover} />
|
||||
<View style={styles.details}>
|
||||
<Text numberOfLines={1} style={styles.title}>
|
||||
{title}
|
||||
</Text>
|
||||
{!!artist && (
|
||||
<Text numberOfLines={1} style={styles.artist}>
|
||||
{artist}
|
||||
</Text>
|
||||
)}
|
||||
<View style={styles.progressContainer}>
|
||||
<View style={styles.progressTrack}>
|
||||
<View
|
||||
style={[styles.progressFill, { width: `${progress * 100}%` }]}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.timeContainer}>
|
||||
<Text style={styles.time}>{formatDuration(positionMs)}</Text>
|
||||
<Text style={styles.time}>{formatDuration(durationMs)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.actions}>
|
||||
<Pressable
|
||||
onPress={handleToggle}
|
||||
hitSlop={12}
|
||||
style={styles.toggleButton}
|
||||
>
|
||||
<Image
|
||||
source={isPlaying ? icons.pause : icons.play}
|
||||
style={styles.toggleIcon}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
</BlurView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
host: {
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
pointerEvents: "box-none",
|
||||
},
|
||||
container: {
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 12,
|
||||
borderRadius: 20,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 12,
|
||||
backgroundColor: "rgba(18, 11, 29, 0.85)",
|
||||
borderWidth: 1,
|
||||
borderColor: "#372152",
|
||||
},
|
||||
cover: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 12,
|
||||
backgroundColor: "#1f1829",
|
||||
},
|
||||
details: {
|
||||
flex: 1,
|
||||
gap: 6,
|
||||
},
|
||||
title: {
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontSize: 14,
|
||||
},
|
||||
artist: {
|
||||
color: "#C2B7D6",
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
fontSize: 12,
|
||||
},
|
||||
progressContainer: {
|
||||
gap: 6,
|
||||
},
|
||||
progressTrack: {
|
||||
height: 4,
|
||||
borderRadius: 8,
|
||||
backgroundColor: "rgba(255,255,255,0.1)",
|
||||
overflow: "hidden",
|
||||
},
|
||||
progressFill: {
|
||||
height: 4,
|
||||
backgroundColor: "#F94697",
|
||||
},
|
||||
timeContainer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
time: {
|
||||
color: "#C2B7D6",
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
fontSize: 10,
|
||||
},
|
||||
actions: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
toggleButton: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
},
|
||||
toggleIcon: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
tintColor: Palette.white,
|
||||
resizeMode: "contain",
|
||||
},
|
||||
});
|
||||
|
||||
export default GlobalAudioPlayer;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useContext } from "react";
|
||||
import { PlayerContext } from "../providers/PlayerProvider";
|
||||
|
||||
const usePlayer = () => {
|
||||
return useContext(PlayerContext);
|
||||
};
|
||||
|
||||
export default usePlayer;
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import useTrackController from "./useTrackController";
|
||||
|
||||
const buildDescriptorFromSource = (source) => {
|
||||
if (!source) return null;
|
||||
|
||||
const descriptor = {};
|
||||
if (typeof source === "number") {
|
||||
descriptor.source = source;
|
||||
descriptor.id = `asset-${source}`;
|
||||
} else if (typeof source === "string") {
|
||||
descriptor.uri = source;
|
||||
descriptor.id = `uri-${source}`;
|
||||
} else if (typeof source === "object") {
|
||||
descriptor.source = source.source;
|
||||
descriptor.uri = source.uri;
|
||||
descriptor.assetId = source.assetId;
|
||||
descriptor.headers = source.headers;
|
||||
descriptor.id = source.id;
|
||||
|
||||
if (!descriptor.id) {
|
||||
if (typeof source.uri === "string") {
|
||||
descriptor.id = `uri-${source.uri}`;
|
||||
} else if (typeof source.assetId === "number") {
|
||||
descriptor.id = `asset-${source.assetId}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!descriptor.id) {
|
||||
try {
|
||||
descriptor.id = `src-${JSON.stringify(source)}`;
|
||||
} catch (e) {
|
||||
descriptor.id = `src-${Date.now()}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!descriptor.uri && !descriptor.source && typeof descriptor.assetId === "number") {
|
||||
descriptor.source = descriptor.assetId;
|
||||
}
|
||||
|
||||
if (!descriptor.uri && !descriptor.source) return null;
|
||||
return descriptor;
|
||||
};
|
||||
|
||||
const useSharedAudioPlayer = (source, options = null) => {
|
||||
const descriptor = useMemo(() => {
|
||||
const base = buildDescriptorFromSource(source);
|
||||
if (!base) return null;
|
||||
if (!options || typeof options !== "object") return base;
|
||||
|
||||
const extended = { ...base };
|
||||
if (options.id) extended.id = options.id;
|
||||
if (options.title) extended.title = options.title;
|
||||
if (options.artist) extended.artist = options.artist;
|
||||
if (options.artwork) extended.artwork = options.artwork;
|
||||
if (options.coverUrl) extended.coverUrl = options.coverUrl;
|
||||
if (options.metadata) extended.metadata = options.metadata;
|
||||
if (options.context) extended.context = options.context;
|
||||
return extended;
|
||||
}, [source, options]);
|
||||
|
||||
const {
|
||||
play,
|
||||
pause,
|
||||
resume,
|
||||
seekTo,
|
||||
isPlaying,
|
||||
positionMs,
|
||||
durationMs,
|
||||
} = useTrackController(descriptor);
|
||||
|
||||
const stateRef = useRef({
|
||||
isPlaying: false,
|
||||
positionMs: 0,
|
||||
durationMs: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
stateRef.current = {
|
||||
isPlaying,
|
||||
positionMs,
|
||||
durationMs,
|
||||
};
|
||||
}, [isPlaying, positionMs, durationMs]);
|
||||
|
||||
const controlsRef = useRef({ play, pause, resume, seekTo });
|
||||
useEffect(() => {
|
||||
controlsRef.current = { play, pause, resume, seekTo };
|
||||
}, [play, pause, resume, seekTo]);
|
||||
|
||||
const playerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!descriptor) {
|
||||
playerRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!playerRef.current || playerRef.current.__trackId !== descriptor.id) {
|
||||
const player = {};
|
||||
Object.defineProperty(player, "__trackId", {
|
||||
value: descriptor.id,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
});
|
||||
|
||||
player.play = async () => {
|
||||
const { positionMs: currentPosition } = stateRef.current;
|
||||
await controlsRef.current.play({ startPositionMs: currentPosition });
|
||||
};
|
||||
|
||||
player.pause = async () => {
|
||||
await controlsRef.current.pause();
|
||||
};
|
||||
|
||||
player.resume = async () => {
|
||||
await controlsRef.current.resume();
|
||||
};
|
||||
|
||||
player.seekTo = async (seconds) => {
|
||||
const target = Math.max(0, Number(seconds) || 0) * 1000;
|
||||
await controlsRef.current.seekTo(target);
|
||||
};
|
||||
|
||||
Object.defineProperties(player, {
|
||||
playing: {
|
||||
enumerable: true,
|
||||
get: () => !!stateRef.current.isPlaying,
|
||||
},
|
||||
duration: {
|
||||
enumerable: true,
|
||||
get: () => stateRef.current.durationMs / 1000,
|
||||
},
|
||||
currentTime: {
|
||||
enumerable: true,
|
||||
get: () => stateRef.current.positionMs / 1000,
|
||||
},
|
||||
});
|
||||
|
||||
playerRef.current = player;
|
||||
}
|
||||
}, [descriptor]);
|
||||
|
||||
return playerRef.current;
|
||||
};
|
||||
|
||||
export default useSharedAudioPlayer;
|
||||
@@ -0,0 +1,143 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import usePlayer from "./usePlayer";
|
||||
|
||||
const useTrackController = (descriptor) => {
|
||||
const {
|
||||
currentTrack,
|
||||
play,
|
||||
pause,
|
||||
resume,
|
||||
isPlaying,
|
||||
positionMs,
|
||||
durationMs,
|
||||
seekTo,
|
||||
seekBy,
|
||||
} = usePlayer();
|
||||
|
||||
const normalizedDescriptor = useMemo(() => {
|
||||
if (!descriptor || !descriptor.id) return null;
|
||||
return descriptor;
|
||||
}, [descriptor]);
|
||||
|
||||
const trackId = normalizedDescriptor?.id || null;
|
||||
const isCurrent = trackId ? currentTrack?.id === trackId : false;
|
||||
const effectivePositionMs = isCurrent ? positionMs : 0;
|
||||
const effectiveDurationMs = isCurrent ? durationMs : 0;
|
||||
const effectiveIsPlaying = isCurrent && isPlaying;
|
||||
|
||||
const ensureLoaded = useCallback(
|
||||
async ({ startPositionMs = 0, autoPlay = true } = {}) => {
|
||||
if (!normalizedDescriptor) return;
|
||||
await play(normalizedDescriptor, {
|
||||
startPositionMs,
|
||||
autoPlay,
|
||||
context: normalizedDescriptor.context,
|
||||
});
|
||||
},
|
||||
[normalizedDescriptor, play]
|
||||
);
|
||||
|
||||
const playNow = useCallback(
|
||||
async ({ startPositionMs = 0 } = {}) => {
|
||||
await ensureLoaded({ startPositionMs, autoPlay: true });
|
||||
},
|
||||
[ensureLoaded]
|
||||
);
|
||||
|
||||
const pauseNow = useCallback(async () => {
|
||||
if (isCurrent) {
|
||||
await pause();
|
||||
}
|
||||
}, [isCurrent, pause]);
|
||||
|
||||
const resumeNow = useCallback(
|
||||
async ({ startPositionMs } = {}) => {
|
||||
if (!normalizedDescriptor) return;
|
||||
if (!isCurrent) {
|
||||
await ensureLoaded({
|
||||
startPositionMs: startPositionMs ?? effectivePositionMs,
|
||||
autoPlay: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (typeof startPositionMs === "number") {
|
||||
await seekTo(Math.max(0, startPositionMs));
|
||||
}
|
||||
await resume();
|
||||
},
|
||||
[
|
||||
normalizedDescriptor,
|
||||
isCurrent,
|
||||
ensureLoaded,
|
||||
effectivePositionMs,
|
||||
seekTo,
|
||||
resume,
|
||||
]
|
||||
);
|
||||
|
||||
const seekToPosition = useCallback(
|
||||
async (targetMs, { autoPlay = false } = {}) => {
|
||||
const bounded = Math.max(0, Number(targetMs) || 0);
|
||||
if (!normalizedDescriptor) return;
|
||||
if (!isCurrent) {
|
||||
await ensureLoaded({ startPositionMs: bounded, autoPlay });
|
||||
return;
|
||||
}
|
||||
await seekTo(bounded);
|
||||
if (autoPlay && !effectiveIsPlaying) {
|
||||
await resume();
|
||||
}
|
||||
},
|
||||
[
|
||||
normalizedDescriptor,
|
||||
isCurrent,
|
||||
ensureLoaded,
|
||||
seekTo,
|
||||
effectiveIsPlaying,
|
||||
resume,
|
||||
]
|
||||
);
|
||||
|
||||
const seekByDelta = useCallback(
|
||||
async (deltaMs, { autoPlay = false } = {}) => {
|
||||
const delta = Number(deltaMs || 0);
|
||||
if (!normalizedDescriptor) return;
|
||||
if (!isCurrent) {
|
||||
const next = Math.max(0, effectivePositionMs + delta);
|
||||
await ensureLoaded({ startPositionMs: next, autoPlay });
|
||||
return;
|
||||
}
|
||||
await seekBy(delta);
|
||||
if (autoPlay && !effectiveIsPlaying) {
|
||||
await resume();
|
||||
}
|
||||
},
|
||||
[
|
||||
normalizedDescriptor,
|
||||
isCurrent,
|
||||
ensureLoaded,
|
||||
effectivePositionMs,
|
||||
seekBy,
|
||||
effectiveIsPlaying,
|
||||
resume,
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
trackId,
|
||||
descriptor: normalizedDescriptor,
|
||||
isCurrent,
|
||||
isPlaying: effectiveIsPlaying,
|
||||
positionMs: effectivePositionMs,
|
||||
durationMs: effectiveDurationMs,
|
||||
ensureLoaded,
|
||||
play: playNow,
|
||||
pause: pauseNow,
|
||||
resume: resumeNow,
|
||||
seekTo: seekToPosition,
|
||||
seekBy: seekByDelta,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTrackController;
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { useAudioPlayer, useAudioPlayerStatus } from "expo-audio";
|
||||
|
||||
import GlobalAudioPlayer from "../components/player/GlobalAudioPlayer";
|
||||
|
||||
const noopAsync = async () => {};
|
||||
const noop = () => {};
|
||||
|
||||
const DEFAULT_CONTEXT = {
|
||||
currentTrack: null,
|
||||
queue: [],
|
||||
status: "idle",
|
||||
isPlaying: false,
|
||||
isBuffering: false,
|
||||
positionMs: 0,
|
||||
durationMs: 0,
|
||||
error: null,
|
||||
play: noopAsync,
|
||||
togglePlay: noopAsync,
|
||||
pause: noopAsync,
|
||||
resume: noopAsync,
|
||||
seekTo: noopAsync,
|
||||
seekBy: noopAsync,
|
||||
stop: noopAsync,
|
||||
setQueue: noop,
|
||||
};
|
||||
|
||||
export const PlayerContext = createContext(DEFAULT_CONTEXT);
|
||||
|
||||
const convertToMs = (value) => {
|
||||
const numeric = Number(value ?? 0);
|
||||
if (!Number.isFinite(numeric) || numeric <= 0) return 0;
|
||||
if (Platform.OS === "web") return numeric;
|
||||
if (numeric > 100000) return numeric;
|
||||
return numeric * 1000;
|
||||
};
|
||||
|
||||
const ensureSource = (track = {}, options = {}) => {
|
||||
const candidate = track?.source ?? options?.source ?? null;
|
||||
if (typeof candidate === "number") return candidate;
|
||||
if (candidate && typeof candidate === "object") return candidate;
|
||||
if (typeof candidate === "string") return { uri: candidate };
|
||||
|
||||
const uriCandidate =
|
||||
track?.uri ??
|
||||
track?.url ??
|
||||
track?.songUrl ??
|
||||
track?.audioUrl ??
|
||||
track?.musicUrl ??
|
||||
(typeof track === "string" ? track : null) ??
|
||||
options?.uri ??
|
||||
options?.url ??
|
||||
options?.songUrl ??
|
||||
options?.audioUrl ??
|
||||
null;
|
||||
|
||||
if (typeof uriCandidate === "number") return uriCandidate;
|
||||
if (uriCandidate && typeof uriCandidate === "object") return uriCandidate;
|
||||
if (typeof uriCandidate === "string" && uriCandidate.length) {
|
||||
return { uri: uriCandidate };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getTrackId = (track = {}, options = {}) => {
|
||||
if (typeof track === "string") return track;
|
||||
return (
|
||||
track?.id ??
|
||||
options?.id ??
|
||||
track?.projectId ??
|
||||
track?.songId ??
|
||||
track?.uri ??
|
||||
track?.url ??
|
||||
track?.songUrl ??
|
||||
track?.musicUrl ??
|
||||
options?.uri ??
|
||||
options?.url ??
|
||||
options?.songUrl ??
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
const normalizeTrack = (trackInput = {}, options = {}) => {
|
||||
const track =
|
||||
typeof trackInput === "string"
|
||||
? { uri: trackInput }
|
||||
: trackInput
|
||||
? { ...trackInput }
|
||||
: {};
|
||||
|
||||
const source = ensureSource(track, options);
|
||||
|
||||
const explicitId = getTrackId(track, options);
|
||||
let id = explicitId;
|
||||
|
||||
if (!id && typeof source === "string") {
|
||||
id = source;
|
||||
}
|
||||
if (!id && typeof source === "number") {
|
||||
id = String(source);
|
||||
}
|
||||
if (!id && source && typeof source === "object" && typeof source.uri === "string") {
|
||||
id = source.uri;
|
||||
}
|
||||
if (!id) {
|
||||
id = `track-${Date.now()}`;
|
||||
}
|
||||
|
||||
const titleCandidate =
|
||||
typeof track?.title === "string"
|
||||
? track.title
|
||||
: typeof options?.title === "string"
|
||||
? options.title
|
||||
: "";
|
||||
|
||||
const artistCandidate =
|
||||
typeof track?.artist === "string"
|
||||
? track.artist
|
||||
: typeof options?.artist === "string"
|
||||
? options.artist
|
||||
: "";
|
||||
|
||||
const artwork = track?.artwork ?? track?.coverUrl ?? options?.artwork ?? options?.coverUrl ?? null;
|
||||
|
||||
return {
|
||||
id,
|
||||
title: titleCandidate,
|
||||
artist: artistCandidate,
|
||||
artwork,
|
||||
coverUrl: track?.coverUrl ?? options?.coverUrl ?? null,
|
||||
source,
|
||||
metadata: {
|
||||
...(options?.metadata || {}),
|
||||
...(track?.metadata || {}),
|
||||
},
|
||||
context: options?.context ?? track?.context ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
const PlayerProvider = ({ children }) => {
|
||||
const [currentTrack, setCurrentTrack] = useState(null);
|
||||
const [queue, setQueue] = useState([]);
|
||||
const [playback, setPlayback] = useState({
|
||||
status: "idle",
|
||||
isPlaying: false,
|
||||
isBuffering: false,
|
||||
positionMs: 0,
|
||||
durationMs: 0,
|
||||
});
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const autoPlayRef = useRef(false);
|
||||
const pendingSeekSecondsRef = useRef(null);
|
||||
|
||||
const source = useMemo(() => {
|
||||
if (!currentTrack?.source) return null;
|
||||
return currentTrack.source;
|
||||
}, [currentTrack?.source]);
|
||||
|
||||
const player = useAudioPlayer(source || null, 200);
|
||||
const status = useAudioPlayerStatus(player);
|
||||
|
||||
useEffect(() => {
|
||||
if (!status) return;
|
||||
|
||||
const durationMs = convertToMs(status.duration);
|
||||
const positionMs = convertToMs(status.currentTime);
|
||||
const isLoaded = !!status.isLoaded;
|
||||
const isPlaying = !!status.playing;
|
||||
const isBuffering = !!status.isBuffering;
|
||||
const didJustFinish = !!status.didJustFinish;
|
||||
|
||||
setPlayback((prev) => {
|
||||
let nextStatus = prev.status;
|
||||
if (!currentTrack) {
|
||||
nextStatus = "idle";
|
||||
} else if (!isLoaded) {
|
||||
nextStatus = "loading";
|
||||
} else if (didJustFinish) {
|
||||
nextStatus = "ended";
|
||||
} else if (isPlaying) {
|
||||
nextStatus = "playing";
|
||||
} else if (prev.status === "loading" && isLoaded) {
|
||||
nextStatus = "ready";
|
||||
} else if (!isPlaying && isLoaded) {
|
||||
nextStatus = "paused";
|
||||
}
|
||||
|
||||
return {
|
||||
status: nextStatus,
|
||||
isPlaying,
|
||||
isBuffering,
|
||||
positionMs,
|
||||
durationMs,
|
||||
};
|
||||
});
|
||||
}, [status, currentTrack]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!player || !currentTrack) return;
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
const pendingSeek = pendingSeekSecondsRef.current;
|
||||
pendingSeekSecondsRef.current = null;
|
||||
|
||||
if (typeof pendingSeek === "number" && pendingSeek >= 0) {
|
||||
await player.seekTo?.(pendingSeek);
|
||||
}
|
||||
|
||||
if (autoPlayRef.current) {
|
||||
autoPlayRef.current = false;
|
||||
await player.play?.();
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
}, [player, currentTrack?.id]);
|
||||
|
||||
const play = useCallback(
|
||||
async (trackInput, options = {}) => {
|
||||
const normalized = normalizeTrack(trackInput, options);
|
||||
if (!normalized.source) {
|
||||
console.warn("PlayerProvider: impossible de lancer la lecture, source audio manquante");
|
||||
return;
|
||||
}
|
||||
|
||||
const sameTrack = currentTrack?.id && normalized.id === currentTrack.id;
|
||||
const targetPositionMs =
|
||||
typeof options?.startPositionMs === "number"
|
||||
? options.startPositionMs
|
||||
: typeof options?.positionMs === "number"
|
||||
? options.positionMs
|
||||
: 0;
|
||||
const targetSeconds = Math.max(0, targetPositionMs) / 1000;
|
||||
const shouldAutoPlay = options?.autoPlay !== false;
|
||||
|
||||
setError(null);
|
||||
|
||||
if (sameTrack) {
|
||||
setCurrentTrack((prev) => ({ ...prev, ...normalized }));
|
||||
try {
|
||||
if (targetSeconds > 0) {
|
||||
await player?.seekTo?.(targetSeconds);
|
||||
}
|
||||
if (shouldAutoPlay) {
|
||||
await player?.play?.();
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
if (!shouldAutoPlay) {
|
||||
setPlayback((prev) => ({
|
||||
...prev,
|
||||
status: "ready",
|
||||
isPlaying: false,
|
||||
positionMs: targetPositionMs,
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
autoPlayRef.current = shouldAutoPlay;
|
||||
pendingSeekSecondsRef.current = targetSeconds > 0 ? targetSeconds : null;
|
||||
|
||||
setPlayback((prev) => ({
|
||||
...prev,
|
||||
status: "loading",
|
||||
isPlaying: false,
|
||||
positionMs: targetPositionMs,
|
||||
}));
|
||||
setCurrentTrack(normalized);
|
||||
},
|
||||
[currentTrack?.id, player]
|
||||
);
|
||||
|
||||
const resume = useCallback(async () => {
|
||||
if (!currentTrack) return;
|
||||
try {
|
||||
await player?.play?.();
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
}, [player, currentTrack]);
|
||||
|
||||
const pause = useCallback(async () => {
|
||||
try {
|
||||
autoPlayRef.current = false;
|
||||
pendingSeekSecondsRef.current = null;
|
||||
await player?.pause?.();
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
}, [player]);
|
||||
|
||||
const togglePlay = useCallback(
|
||||
async (trackInput, options = {}) => {
|
||||
if (!trackInput && !currentTrack) return;
|
||||
const targetId = getTrackId(
|
||||
typeof trackInput === "string" ? { uri: trackInput } : trackInput,
|
||||
options
|
||||
);
|
||||
|
||||
if (trackInput && (!currentTrack || (targetId && targetId !== currentTrack.id))) {
|
||||
await play(trackInput, options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (playback.isPlaying) {
|
||||
await pause();
|
||||
} else if (currentTrack) {
|
||||
await resume();
|
||||
} else if (trackInput) {
|
||||
await play(trackInput, options);
|
||||
}
|
||||
},
|
||||
[currentTrack, playback.isPlaying, pause, play, resume]
|
||||
);
|
||||
|
||||
const seekTo = useCallback(
|
||||
async (positionMs) => {
|
||||
if (!player) return;
|
||||
const bounded = Math.max(0, Number(positionMs) || 0);
|
||||
const seconds = bounded / 1000;
|
||||
|
||||
try {
|
||||
if (status?.isLoaded) {
|
||||
await player.seekTo?.(seconds);
|
||||
} else {
|
||||
pendingSeekSecondsRef.current = seconds;
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
},
|
||||
[player, status?.isLoaded]
|
||||
);
|
||||
|
||||
const seekBy = useCallback(
|
||||
async (deltaMs) => {
|
||||
const next = Math.max(0, playback.positionMs + Number(deltaMs || 0));
|
||||
await seekTo(next);
|
||||
},
|
||||
[playback.positionMs, seekTo]
|
||||
);
|
||||
|
||||
const stop = useCallback(async () => {
|
||||
try {
|
||||
autoPlayRef.current = false;
|
||||
pendingSeekSecondsRef.current = null;
|
||||
await player?.pause?.();
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setCurrentTrack(null);
|
||||
setPlayback({
|
||||
status: "idle",
|
||||
isPlaying: false,
|
||||
isBuffering: false,
|
||||
positionMs: 0,
|
||||
durationMs: 0,
|
||||
});
|
||||
}
|
||||
}, [player]);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
currentTrack,
|
||||
queue,
|
||||
status: playback.status,
|
||||
isPlaying: playback.isPlaying,
|
||||
isBuffering: playback.isBuffering,
|
||||
positionMs: playback.positionMs,
|
||||
durationMs: playback.durationMs,
|
||||
error,
|
||||
play,
|
||||
togglePlay,
|
||||
pause,
|
||||
resume,
|
||||
seekTo,
|
||||
seekBy,
|
||||
stop,
|
||||
setQueue,
|
||||
}),
|
||||
[
|
||||
currentTrack,
|
||||
error,
|
||||
pause,
|
||||
play,
|
||||
playback.durationMs,
|
||||
playback.isBuffering,
|
||||
playback.isPlaying,
|
||||
playback.positionMs,
|
||||
playback.status,
|
||||
queue,
|
||||
resume,
|
||||
seekBy,
|
||||
seekTo,
|
||||
stop,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<PlayerContext.Provider value={contextValue}>
|
||||
{children}
|
||||
<GlobalAudioPlayer />
|
||||
</PlayerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlayerProvider;
|
||||
@@ -4,6 +4,7 @@ import React from "react";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
|
||||
import { SheetProvider } from "react-native-actions-sheet";
|
||||
import PlayerProvider from "./PlayerProvider";
|
||||
import SplashAnimationProvider from "./SplashAnimationProvider";
|
||||
import StripeEmbeddedProvider from "./StripeEmbeddedProvider";
|
||||
import UniversalLinkProvider from "./UniversalLinkProvider";
|
||||
@@ -21,6 +22,7 @@ const SharedProviders = ({ children }) => {
|
||||
[UniversalLinkProvider, {}],
|
||||
[BottomSheetModalProvider, {}],
|
||||
[SheetProvider, {}],
|
||||
[PlayerProvider, {}],
|
||||
];
|
||||
|
||||
// Dynamically nest the providers using reduce
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAudioPlayer } from "expo-audio";
|
||||
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
@@ -75,7 +75,14 @@ const MusicDetails = ({ route }) => {
|
||||
const coverUrl = project?.coverUrl || null;
|
||||
const songUrl = project?.songUrl || null;
|
||||
|
||||
const player = useAudioPlayer(songUrl ? { uri: songUrl } : undefined);
|
||||
const player = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, {
|
||||
id: projectId ? `project-${projectId}` : songUrl ? `song-${songUrl}` : undefined,
|
||||
title,
|
||||
artist,
|
||||
artwork: coverUrl,
|
||||
coverUrl,
|
||||
metadata: { projectId, screen: "MusicDetails" },
|
||||
});
|
||||
|
||||
// Karaoke aligned words (from timestamps)
|
||||
const alignedWords = useMemo(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BlurView } from "expo-blur";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Pressable,
|
||||
Image as RNImage,
|
||||
@@ -26,119 +26,12 @@ import {
|
||||
usersRef,
|
||||
} from "../../config/firebase";
|
||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
||||
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import Style, { gutters, size } from "../../styles/Style";
|
||||
|
||||
const useHtmlAudioPlayer = (source) => {
|
||||
const src = typeof source === "string" ? source : source?.uri || null;
|
||||
const audioRef = useRef(null);
|
||||
const [state, setState] = useState({
|
||||
durationMs: 0,
|
||||
positionMs: 0,
|
||||
isPlaying: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setState({ durationMs: 0, positionMs: 0, isPlaying: false });
|
||||
|
||||
if (!src) {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
audioRef.current.src = "";
|
||||
audioRef.current.load();
|
||||
audioRef.current = null;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const audio = new Audio(src);
|
||||
audio.preload = "auto";
|
||||
audio.crossOrigin = "anonymous";
|
||||
audioRef.current = audio;
|
||||
|
||||
const syncDuration = () => {
|
||||
const duration = Number.isFinite(audio.duration)
|
||||
? audio.duration * 1000
|
||||
: 0;
|
||||
setState((prev) => ({ ...prev, durationMs: duration }));
|
||||
};
|
||||
|
||||
const handleLoadedMetadata = () => syncDuration();
|
||||
const handleDurationChange = () => syncDuration();
|
||||
const handleTimeUpdate = () => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
positionMs: audio.currentTime * 1000,
|
||||
}));
|
||||
};
|
||||
const handlePlay = () => setState((prev) => ({ ...prev, isPlaying: true }));
|
||||
const handlePause = () =>
|
||||
setState((prev) => ({ ...prev, isPlaying: false }));
|
||||
const handleEnded = () =>
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
positionMs: Number.isFinite(audio.duration)
|
||||
? audio.duration * 1000
|
||||
: prev.positionMs,
|
||||
isPlaying: false,
|
||||
}));
|
||||
|
||||
audio.addEventListener("loadedmetadata", handleLoadedMetadata);
|
||||
audio.addEventListener("durationchange", handleDurationChange);
|
||||
audio.addEventListener("timeupdate", handleTimeUpdate);
|
||||
audio.addEventListener("play", handlePlay);
|
||||
audio.addEventListener("pause", handlePause);
|
||||
audio.addEventListener("ended", handleEnded);
|
||||
|
||||
return () => {
|
||||
audio.pause();
|
||||
audio.removeEventListener("loadedmetadata", handleLoadedMetadata);
|
||||
audio.removeEventListener("durationchange", handleDurationChange);
|
||||
audio.removeEventListener("timeupdate", handleTimeUpdate);
|
||||
audio.removeEventListener("play", handlePlay);
|
||||
audio.removeEventListener("pause", handlePause);
|
||||
audio.removeEventListener("ended", handleEnded);
|
||||
audio.src = "";
|
||||
audio.load();
|
||||
audioRef.current = null;
|
||||
};
|
||||
}, [src]);
|
||||
|
||||
const play = React.useCallback(async () => {
|
||||
if (!audioRef.current) return;
|
||||
try {
|
||||
await audioRef.current.play();
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const pause = React.useCallback(() => {
|
||||
if (!audioRef.current) return;
|
||||
audioRef.current.pause();
|
||||
}, []);
|
||||
|
||||
const seekTo = React.useCallback((seconds) => {
|
||||
if (!audioRef.current) return;
|
||||
const next = Math.max(0, Number(seconds) || 0);
|
||||
const duration = audioRef.current.duration;
|
||||
const bounded = Number.isFinite(duration) ? Math.min(next, duration) : next;
|
||||
audioRef.current.currentTime = bounded;
|
||||
setState((prev) => ({ ...prev, positionMs: bounded * 1000 }));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
play,
|
||||
pause,
|
||||
seekTo,
|
||||
playing: state.isPlaying,
|
||||
positionMs: state.positionMs,
|
||||
durationMs: state.durationMs,
|
||||
};
|
||||
};
|
||||
|
||||
// 20 secondes
|
||||
const timeBeforeIncrement = 20000;
|
||||
|
||||
@@ -183,14 +76,29 @@ const MusicDetails = ({ route }) => {
|
||||
const coverUrl = project?.coverUrl || null;
|
||||
const songUrl = project?.songUrl || null;
|
||||
|
||||
const {
|
||||
play: playAudio,
|
||||
pause: pauseAudio,
|
||||
seekTo: seekAudio,
|
||||
playing: isPlaying,
|
||||
positionMs,
|
||||
durationMs,
|
||||
} = useHtmlAudioPlayer(songUrl ? { uri: songUrl } : undefined);
|
||||
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);
|
||||
|
||||
// Karaoke aligned words (from timestamps)
|
||||
const alignedWords = useMemo(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { useAudioPlayer } from "expo-audio";
|
||||
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
|
||||
import { CameraView, useCameraPermissions } from "expo-camera";
|
||||
import React, {
|
||||
useCallback,
|
||||
@@ -54,7 +54,14 @@ const RecordPlayback = ({ route }) => {
|
||||
|
||||
// Musique
|
||||
const songUrl = project?.songUrl || null;
|
||||
const player = useAudioPlayer(songUrl ? { uri: songUrl } : undefined);
|
||||
const player = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, {
|
||||
id: projectId ? `record-${projectId}` : songUrl ? `record-${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, screen: "RecordPlayback" },
|
||||
});
|
||||
|
||||
const musicIndex = useMemo(() => {
|
||||
const i = Number(songIndex);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { useAudioPlayer } from "expo-audio";
|
||||
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
|
||||
import { useCameraPermissions } from "expo-camera";
|
||||
import React, {
|
||||
useCallback,
|
||||
@@ -61,7 +61,14 @@ const RecordPlayback = ({ route }) => {
|
||||
|
||||
// Player
|
||||
const songUrl = project?.songUrl || null;
|
||||
const player = useAudioPlayer(songUrl ? { uri: songUrl } : undefined);
|
||||
const player = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, {
|
||||
id: projectId ? `record-${projectId}` : songUrl ? `record-${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, screen: "RecordPlayback" },
|
||||
});
|
||||
|
||||
// MediaStream / Recorder (web only)
|
||||
const previewVideoRef = useRef(null);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAudioPlayer } from "expo-audio";
|
||||
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import { VideoView, useVideoPlayer } from "expo-video";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
@@ -16,7 +16,14 @@ const RecordedPlayback = ({ route }) => {
|
||||
const { videoUri, project } = route.params || {};
|
||||
const songUrl = project?.songUrl || null;
|
||||
console.log("video uri is : ", videoUri);
|
||||
const audioPlayer = useAudioPlayer(songUrl ? { uri: songUrl } : undefined);
|
||||
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" },
|
||||
});
|
||||
const videoPlayer = useVideoPlayer(videoUri || null, (p) => {
|
||||
p.loop = false;
|
||||
p.muted = true; // recorded video has no audio; keep muted anyway
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAudioPlayer } from "expo-audio";
|
||||
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import { background } from "../../assets";
|
||||
@@ -29,7 +29,14 @@ const RecordedPlayback = ({ route }) => {
|
||||
const songUrl = project?.songUrl || null;
|
||||
console.log("video uri is : ", videoUri);
|
||||
// AUDIO PLAYER (expo-audio → seconds)
|
||||
const audioPlayer = useAudioPlayer(songUrl ? { uri: songUrl } : undefined);
|
||||
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);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useIsFocused, useRoute } from "@react-navigation/native";
|
||||
import { useAudioPlayer } from "expo-audio";
|
||||
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { VideoView, useVideoPlayer } from "expo-video";
|
||||
import React, {
|
||||
@@ -115,7 +115,14 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
||||
|
||||
const creatorName = item?.userName;
|
||||
|
||||
const audioPlayer = useAudioPlayer(audioSource || undefined);
|
||||
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) => {
|
||||
p.loop = false;
|
||||
p.muted = true;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAudioPlayer } from "expo-audio";
|
||||
import useSharedAudioPlayer from "../../../hooks/useSharedAudioPlayer";
|
||||
import { BlurView } from "expo-blur";
|
||||
import React, {
|
||||
useCallback,
|
||||
@@ -144,7 +144,14 @@ const PlaybackItem = ({
|
||||
setIsLiked(currentUID ? lb.includes(currentUID) : false);
|
||||
}, [item?.likedBy, currentUID]);
|
||||
|
||||
const audioPlayer = useAudioPlayer(audioSource || undefined);
|
||||
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 },
|
||||
});
|
||||
|
||||
// Helpers to normalize and map clocks
|
||||
const getAudioTimeSeconds = useCallback(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* global setInterval, clearInterval */
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { useAudioPlayer } from "expo-audio";
|
||||
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
|
||||
import { BlurView } from "expo-blur";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Image, Platform, Pressable, Text, View } from "react-native";
|
||||
@@ -34,11 +34,31 @@ const SongReady = () => {
|
||||
0: { pos: 0, dur: 0 },
|
||||
1: { pos: 0, dur: 0 },
|
||||
});
|
||||
const player0 = useAudioPlayer(
|
||||
musicUrls[0] ? { uri: musicUrls[0] } : undefined
|
||||
const player0 = useSharedAudioPlayer(
|
||||
musicUrls[0] ? { uri: musicUrls[0] } : undefined,
|
||||
{
|
||||
id: musicUrls[0] ? `songready-${musicUrls[0]}` : undefined,
|
||||
title:
|
||||
(Array.isArray(selectedProject?.musicTitles)
|
||||
? selectedProject?.musicTitles?.[0]
|
||||
: null) || selectedProject?.title || "Option 1",
|
||||
artwork: selectedProject?.coverUrl || null,
|
||||
coverUrl: selectedProject?.coverUrl || null,
|
||||
metadata: { index: 0, projectId },
|
||||
}
|
||||
);
|
||||
const player1 = useAudioPlayer(
|
||||
musicUrls[1] ? { uri: musicUrls[1] } : undefined
|
||||
const player1 = useSharedAudioPlayer(
|
||||
musicUrls[1] ? { uri: musicUrls[1] } : undefined,
|
||||
{
|
||||
id: musicUrls[1] ? `songready-${musicUrls[1]}` : undefined,
|
||||
title:
|
||||
(Array.isArray(selectedProject?.musicTitles)
|
||||
? selectedProject?.musicTitles?.[1]
|
||||
: null) || selectedProject?.title || "Option 2",
|
||||
artwork: selectedProject?.coverUrl || null,
|
||||
coverUrl: selectedProject?.coverUrl || null,
|
||||
metadata: { index: 1, projectId },
|
||||
}
|
||||
);
|
||||
const wasPlayingBeforeSeek = useRef({ 0: false, 1: false });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user