diff --git a/src/app/music/_components/MusicPlayerProvider.tsx b/src/app/music/_components/MusicPlayerProvider.tsx index 3a695b1..39d704a 100644 --- a/src/app/music/_components/MusicPlayerProvider.tsx +++ b/src/app/music/_components/MusicPlayerProvider.tsx @@ -199,6 +199,89 @@ export function MusicPlayerProvider({ children }: { children: ReactNode }) { else audio.pause(); }, [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( () => ({ tracks,