Changeset 6a51968 in Klonkt


Ignore:
Timestamp:
06/25/2026 02:02:59 PM (3 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
7a235bc
Parents:
c648b04
Message:

audio: Media Session API for reliable mobile auto-advance + lock-screen controls

On mobile, auto-advancing to the next track started then stopped immediately: iOS
pauses a programmatic play() with no active media session. Wire navigator.mediaSession
— per-track metadata (title/artist/album/artwork), play/pause/next/prev/seek action
handlers, and playbackState — so the session stays live (continuous playback works) and
the OS shows lock-screen / notification / headset controls. audio-player.js?v=30.

Co-Authored-By: Claude <noreply@…>

Location:
src
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/audio-player.js

    rc648b04 r6a51968  
    289289  });
    290290
     291  // Media Session metadata (lock-screen / notification info + artwork). Set per
     292  // track; the action handlers are wired once below. Keeping a live media session
     293  // is what lets iOS continue a programmatic auto-advance play() instead of
     294  // pausing it immediately.
     295  function updateMediaMetadata(t) {
     296    if (!('mediaSession' in navigator) || typeof MediaMetadata === 'undefined') return;
     297    try {
     298      const art = [];
     299      if (t && t.cover) {
     300        let u = t.cover; try { u = new URL(t.cover, location.href).href; } catch (e) {}
     301        art.push({ src: u, sizes: '512x512', type: '' });
     302      }
     303      navigator.mediaSession.metadata = new MediaMetadata({
     304        title: (t && t.title) || 'Untitled',
     305        artist: (t && t.artist) || '',
     306        album: albumName || '',
     307        artwork: art,
     308      });
     309    } catch (e) { /* non-fatal */ }
     310  }
     311
    291312  function loadTrack(index, autoplay, metaOnly) {
    292313    if (!queue[index]) {
     
    314335    renderQueue();
    315336    markPlaying(t.id);
     337    updateMediaMetadata(t);
    316338
    317339    if (metaOnly) return;
     
    475497    root.classList.remove('audio-needs-tap');  // hide tap hint
    476498    mediaRegistry().setActive(registrySelf);   // pause any currently playing embeds
    477   });
     499    if ('mediaSession' in navigator) { try { navigator.mediaSession.playbackState = 'playing'; } catch (e) {} }
     500  });
     501
     502  // Media Session action handlers (wired once): lock-screen / headset / car
     503  // controls, and — crucially — an active session so iOS keeps a programmatic
     504  // auto-advance playing instead of pausing it the instant it starts.
     505  if ('mediaSession' in navigator) {
     506    const ms = navigator.mediaSession;
     507    const wire = (action, fn) => { try { ms.setActionHandler(action, fn); } catch (e) { /* unsupported action */ } };
     508    wire('play', () => play());
     509    wire('pause', () => pause());
     510    wire('previoustrack', () => prev());
     511    wire('nexttrack', () => next());
     512    wire('seekto', (e) => { if (e && e.seekTime != null && audio.duration) { try { audio.currentTime = e.seekTime; } catch (er) {} } });
     513  }
    478514  // Reset the error counter only on a REAL playback start (`playing`), not the
    479515  // eager `play` event. `play` fires before any network/decode error, so resetting
     
    481517  // track → infinite "next" loop. `playing` only fires when audio is actually playing.
    482518  audio.addEventListener('playing', () => { consecutiveErrors = 0; preloadNext(); });
    483   audio.addEventListener('pause', () => { isPlaying = false; root.classList.remove('is-playing'); });
     519  audio.addEventListener('pause', () => { isPlaying = false; root.classList.remove('is-playing'); if ('mediaSession' in navigator) { try { navigator.mediaSession.playbackState = 'paused'; } catch (e) {} } });
    484520  audio.addEventListener('ended', next);
    485521  audio.addEventListener('error', (e) => {
  • src/views/shell.ejs

    rc648b04 r6a51968  
    335335     ?v=N — cache-buster: bump bij elke audio-player.js wijziging zodat
    336336     Cloudflare (max-age=1y) niet de oude versie blijft serveren. -->
    337 <script src="/assets/js/audio-player.js?v=29"></script>
     337<script src="/assets/js/audio-player.js?v=30"></script>
    338338<!-- Eigen custom media-embeds (YouTube/SoundCloud/Spotify) via de echte
    339339     player-API's + gedeelde mutual-exclusion registry met de site-speler. -->
Note: See TracChangeset for help on using the changeset viewer.