zebra-spaces: Media Session API on listener attach — OS keeps backgrounded tab alive

Pairs with the signal-server listener alive-ttl exemption (c16f6c1).
When the AudioContext listener path binds a remote stream, declare
a MediaSession to the OS: metadata + playbackState=playing + no-op
play/pause action handlers.

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 to the OS scheduler not to freeze this tab

Together with the server exemption, mobile listeners can tab away
to email / browser / chat and zebra-spaces audio keeps playing.
This commit is contained in:
Russell Ballestrini 2026-06-04 13:01:37 -04:00
parent cd0b6a74d9
commit d71a37f84a
No known key found for this signature in database

View file

@ -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');
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-04</span><br>
md5 <span class="stamp-md5">9b675279aab82d1a7a07d9708bbd8805</span><br>
sha256 <span class="stamp-sha">60456caf735c84bb4703458a042a8d5710babfb81cd13fb24b2284f4a207e6df</span><br>
md5 <span class="stamp-md5">0c3035d2de972b24edd97e8b6130f1f3</span><br>
sha256 <span class="stamp-sha">7aa8d55df2a764566d1b1af276961c46109189f992b357a00b6201be94559339</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>