diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html
index dee4665..8e7b5dd 100644
--- a/web/zebra-spaces.html
+++ b/web/zebra-spaces.html
@@ -1680,6 +1680,33 @@ function attachListenerStreamViaAudioContext(uuid, stream){
gain.connect(audioCtx.destination);
listenerAudioNodes.set(uuid, { src, gain, stream });
logLine('', 'listener audio via AudioContext '+uuid.slice(0,4)+' ctxState='+audioCtx.state);
+ /* Media Session API — tell the OS this tab is playing media. On
+ * Android Firefox + iOS Safari, this:
+ * - keeps the tab in media-priority mode (less aggressive JS
+ * throttling, AudioContext stays running)
+ * - surfaces lock-screen / notification-area transport controls
+ * - signals "do not freeze this tab" to the OS scheduler
+ * Combined with the server-side skip of alive-ttl for listeners,
+ * a mobile user can tab away to email / browser / chat and the
+ * zebra-spaces audio keeps playing in the background. Fox
+ * 2026-06-04. */
+ try {
+ if (navigator.mediaSession){
+ const title = 'zebra-spaces';
+ const artist = roomID ? ('room ' + roomID.slice(0,8)) : 'live';
+ navigator.mediaSession.metadata = new MediaMetadata({ title, artist });
+ navigator.mediaSession.playbackState = 'playing';
+ /* Provide a no-op pause handler so the OS knows we accept the
+ * 'pause' action — without one, some platforms refuse to keep
+ * the session active. */
+ navigator.mediaSession.setActionHandler('pause', () => {
+ navigator.mediaSession.playbackState = 'paused';
+ });
+ navigator.mediaSession.setActionHandler('play', () => {
+ navigator.mediaSession.playbackState = 'playing';
+ });
+ }
+ } catch(_){}
return true;
}
function detachListenerStream(uuid){
@@ -5475,8 +5502,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');