From 6cbcc18aab43f2f501ab8bd48c8d898f576b4301 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 16 Jul 2026 17:20:49 +0200 Subject: [PATCH] fix(frontend): wire hardware media-key next/previous track buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The player never registered Media Session action handlers, so the OS/keyboard next-track and previous-track keys had nothing to call — only play/pause worked via the browser's built-in audio-element heuristic. Now previoustrack/nexttrack are wired to the player, and metadata/playback state are kept in sync for the lock-screen/OS media widget. --- frontend/src/lib/player/player.svelte.ts | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/frontend/src/lib/player/player.svelte.ts b/frontend/src/lib/player/player.svelte.ts index a74e83b..5338b37 100644 --- a/frontend/src/lib/player/player.svelte.ts +++ b/frontend/src/lib/player/player.svelte.ts @@ -137,10 +137,12 @@ class PlayerState { audio.addEventListener('play', () => { this.playing = true; this.#startProgressLoop(); + this.#syncMediaSessionPlaybackState(); }); audio.addEventListener('pause', () => { this.playing = false; this.#stopProgressLoop(); + this.#syncMediaSessionPlaybackState(); }); audio.addEventListener('ended', () => { this.#stopProgressLoop(); @@ -162,9 +164,37 @@ class PlayerState { this.error = 'No se pudo reproducir la pista.'; }); this.#audio = audio; + this.#setupMediaSession(audio); return audio; } + #setupMediaSession(audio: HTMLAudioElement) { + if (!('mediaSession' in navigator)) return; + navigator.mediaSession.setActionHandler('play', () => audio.play().catch(() => {})); + navigator.mediaSession.setActionHandler('pause', () => audio.pause()); + navigator.mediaSession.setActionHandler('previoustrack', () => this.previous()); + navigator.mediaSession.setActionHandler('nexttrack', () => this.next()); + } + + #syncMediaSessionPlaybackState() { + if (!browser || !('mediaSession' in navigator)) return; + navigator.mediaSession.playbackState = this.playing ? 'playing' : 'paused'; + } + + #syncMediaSessionMetadata(track: PlayerTrack) { + if (!browser || !('mediaSession' in navigator)) return; + if (!track.id) { + navigator.mediaSession.metadata = null; + return; + } + const artwork = track.coverUrl ?? `/api/tracks/${track.id}/cover?size=small`; + navigator.mediaSession.metadata = new MediaMetadata({ + title: track.title, + artist: track.artist, + artwork: [{ src: artwork, sizes: '256x256' }] + }); + } + #tickProgress = () => { const audio = this.#audio; if (!audio) return; @@ -229,6 +259,7 @@ class PlayerState { const id = this.currentId; const track = this.tracks.find((t) => t.id === id); this.#applyAccent(id, track?.coverUrl); + if (track) this.#syncMediaSessionMetadata(track); const token = ++this.#loadToken; this.loading = true; this.error = '';