Merge branch 'background-music-playback'
This commit is contained in:
@@ -199,6 +199,89 @@ export function MusicPlayerProvider({ children }: { children: ReactNode }) {
|
|||||||
else audio.pause();
|
else audio.pause();
|
||||||
}, [isPlaying, currentTrack]);
|
}, [isPlaying, currentTrack]);
|
||||||
|
|
||||||
|
// OS-level media controls (lock screen, notification shade, media keys, etc.)
|
||||||
|
// via the Media Session API. Wire the transport actions to our state.
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof navigator === "undefined" || !("mediaSession" in navigator)) return;
|
||||||
|
const ms = navigator.mediaSession;
|
||||||
|
const handlers: [MediaSessionAction, MediaSessionActionHandler][] = [
|
||||||
|
["play", () => setIsPlaying(true)],
|
||||||
|
["pause", () => setIsPlaying(false)],
|
||||||
|
["previoustrack", () => stepRef.current(-1)],
|
||||||
|
["nexttrack", () => stepRef.current(1)],
|
||||||
|
[
|
||||||
|
"seekto",
|
||||||
|
(details) => {
|
||||||
|
if (typeof details.seekTime === "number") seek(details.seekTime);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"seekbackward",
|
||||||
|
(details) => seek((audioRef.current?.currentTime ?? 0) - (details.seekOffset ?? 10)),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"seekforward",
|
||||||
|
(details) => seek((audioRef.current?.currentTime ?? 0) + (details.seekOffset ?? 10)),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
for (const [action, handler] of handlers) {
|
||||||
|
try {
|
||||||
|
ms.setActionHandler(action, handler);
|
||||||
|
} catch {
|
||||||
|
// Action unsupported by this browser — ignore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
for (const [action] of handlers) {
|
||||||
|
try {
|
||||||
|
ms.setActionHandler(action, null);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [seek]);
|
||||||
|
|
||||||
|
// Keep the OS-visible metadata in sync with the current track.
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof navigator === "undefined" || !("mediaSession" in navigator)) return;
|
||||||
|
navigator.mediaSession.metadata = currentTrack
|
||||||
|
? new MediaMetadata({
|
||||||
|
title: currentTrack.title,
|
||||||
|
artist: "Gregor Lohaus",
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
}, [currentTrack]);
|
||||||
|
|
||||||
|
// Reflect play/pause state to the OS so the right button is shown.
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof navigator === "undefined" || !("mediaSession" in navigator)) return;
|
||||||
|
navigator.mediaSession.playbackState = currentTrack
|
||||||
|
? isPlaying
|
||||||
|
? "playing"
|
||||||
|
: "paused"
|
||||||
|
: "none";
|
||||||
|
}, [isPlaying, currentTrack]);
|
||||||
|
|
||||||
|
// Keep the scrubber position on the OS controls in sync.
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof navigator === "undefined" || !("mediaSession" in navigator)) return;
|
||||||
|
if (!("setPositionState" in navigator.mediaSession)) return;
|
||||||
|
try {
|
||||||
|
if (duration > 0 && Number.isFinite(duration)) {
|
||||||
|
navigator.mediaSession.setPositionState({
|
||||||
|
duration,
|
||||||
|
position: Math.min(currentTime, duration),
|
||||||
|
playbackRate: audioRef.current?.playbackRate ?? 1,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
navigator.mediaSession.setPositionState();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Some browsers throw on invalid state — ignore.
|
||||||
|
}
|
||||||
|
}, [currentTime, duration]);
|
||||||
|
|
||||||
const value = useMemo<MusicPlayerValue>(
|
const value = useMemo<MusicPlayerValue>(
|
||||||
() => ({
|
() => ({
|
||||||
tracks,
|
tracks,
|
||||||
|
|||||||
Reference in New Issue
Block a user