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
|
- **Built-in web player** at `http://localhost:8000/`: now playing (linked to
|
||||||
its source page), track history, skip button, per-track download, volume
|
its source page), track history, skip button, per-track download, volume
|
||||||
memory, live auto-reconnect, prefetch progress, and a synthwave look.
|
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
|
- **Robust in a container**: Docker healthcheck, graceful shutdown, retries on
|
||||||
transient HTTP errors.
|
transient HTTP errors.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,38 @@
|
||||||
if (wasPaused) { wasPaused = false; goLive(); }
|
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() {
|
async function poll() {
|
||||||
try {
|
try {
|
||||||
const r = await fetch("/nowplaying", { cache: "no-store" });
|
const r = await fetch("/nowplaying", { cache: "no-store" });
|
||||||
|
|
@ -240,6 +272,7 @@
|
||||||
: escapeHtml(label);
|
: escapeHtml(label);
|
||||||
artistEl.textContent = a;
|
artistEl.textContent = a;
|
||||||
document.title = t ? (a ? `${t} — ${a} · ${STATION_NAME}` : `${t} · ${STATION_NAME}`) : STATION_NAME;
|
document.title = t ? (a ? `${t} — ${a} · ${STATION_NAME}` : `${t} · ${STATION_NAME}`) : STATION_NAME;
|
||||||
|
updateMediaSession(t, a);
|
||||||
} catch (e) { /* keep last known values */ }
|
} catch (e) { /* keep last known values */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue