stream: expose OS media controls via the Media Session API
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Wire the web player into system media controls (MPRIS, Control Center, lock screen) and keyboard media keys: push track metadata and handle play/pause/next, with seek neutralized on the live stream. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a1fed6b4e3
commit
622210197f
2 changed files with 37 additions and 0 deletions
|
|
@ -32,6 +32,10 @@ simultaneous listeners), not for public broadcasting.
|
|||
- **Built-in web player** at `http://localhost:8000/`: now playing (linked to
|
||||
its source page), track history, skip button, per-track download, volume
|
||||
memory, live auto-reconnect, prefetch progress, and a synthwave look.
|
||||
- **OS media controls**: the player exposes current track metadata and
|
||||
play/pause/next through the Media Session API, so it wires into system media
|
||||
controls (MPRIS on Linux, macOS Control Center, Windows, mobile lock screen)
|
||||
and keyboard media keys.
|
||||
- **Robust in a container**: Docker healthcheck, graceful shutdown, retries on
|
||||
transient HTTP errors.
|
||||
|
||||
|
|
|
|||
|
|
@ -225,6 +225,38 @@
|
|||
if (wasPaused) { wasPaused = false; goLive(); }
|
||||
});
|
||||
|
||||
// Intégration aux contrôles média du système (MPRIS sous Linux, centre de
|
||||
// contrôle macOS/Windows, écran verrouillé mobile, touches multimédia du
|
||||
// clavier) via la Media Session API. On expose les métadonnées du morceau
|
||||
// courant et on branche play / pause / suivant sur les mêmes actions que
|
||||
// l'interface. Absente sur quelques navigateurs : on garde un no-op.
|
||||
const media = navigator.mediaSession;
|
||||
function updateMediaSession(title, artist) {
|
||||
if (!media || !window.MediaMetadata) return;
|
||||
media.metadata = new MediaMetadata({
|
||||
title: title || STATION_NAME,
|
||||
artist: artist || "",
|
||||
album: STATION_NAME,
|
||||
artwork: [{ src: "/favicon.svg", type: "image/svg+xml" }],
|
||||
});
|
||||
}
|
||||
if (media) {
|
||||
const safe = (fn) => { try { fn(); } catch (e) { /* action non supportée */ } };
|
||||
safe(() => media.setActionHandler("play", () => player.play().catch(() => {})));
|
||||
safe(() => media.setActionHandler("pause", () => player.pause()));
|
||||
safe(() => media.setActionHandler("stop", () => player.pause()));
|
||||
// Pas de reprise possible sur un direct : « précédent » comme « suivant »
|
||||
// passent au morceau suivant côté serveur.
|
||||
safe(() => media.setActionHandler("nexttrack", () => { skipBtn.click(); }));
|
||||
// Le direct n'est pas déplaçable : on neutralise les actions de seek pour
|
||||
// éviter que le système n'affiche des boutons d'avance/recul inopérants.
|
||||
safe(() => media.setActionHandler("seekbackward", null));
|
||||
safe(() => media.setActionHandler("seekforward", null));
|
||||
safe(() => media.setActionHandler("seekto", null));
|
||||
player.addEventListener("playing", () => { media.playbackState = "playing"; });
|
||||
player.addEventListener("pause", () => { media.playbackState = "paused"; });
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
try {
|
||||
const r = await fetch("/nowplaying", { cache: "no-store" });
|
||||
|
|
@ -240,6 +272,7 @@
|
|||
: escapeHtml(label);
|
||||
artistEl.textContent = a;
|
||||
document.title = t ? (a ? `${t} — ${a} · ${STATION_NAME}` : `${t} · ${STATION_NAME}`) : STATION_NAME;
|
||||
updateMediaSession(t, a);
|
||||
} catch (e) { /* keep last known values */ }
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue