Changeset 4c9f29a in Klonkt for src/assets/js


Ignore:
Timestamp:
06/15/2026 03:23:14 AM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
3e86f1c
Parents:
650b601
git-author:
roboburr <roboburr@…> (06/15/2026 03:22:47 AM)
git-committer:
roboburr <roboburr@…> (06/15/2026 03:23:14 AM)
Message:

feat: on-brand media embeds via the real player APIs

Replaces bare platform iframes with on-brand cards, powered by the
official JS APIs so play/pause/progress are in our own hands:

  • YouTube (IFrame Player API) + SoundCloud (Widget API): fully custom controls, native chrome hidden.
  • Spotify (iFrame API): our frame around it + controls (their player UI remains; restyling not possible without Premium+OAuth).
  • Shared PlaybackRegistry: mutual exclusion -- only 1 thing plays at a time (incl. the site audio player). Replaces the focus/blur heuristic with real play events (blur stays as fallback for iframe-only embeds).
  • Progressive enhancement: if an ad-blocker blocks the platform API, falls back seamlessly to the bare platform iframe (autoplay). The resting-state card is our brand for everyone.

AudioEmbedService now renders a placeholder div (data-embed-*) for YT/SC/
Spotify instead of an iframe; embed-player.js builds the card client-side.
CSP scriptSrc extended with the player API hosts.

Adversarial review (workflow) -> 6 bugs fixed: HTMX swap leak (poll timers/
adapters -> MutationObserver teardown + adapter.destroy()), javascript: URL XSS
(scheme guard in detectProvider + safeHref client-side), Spotify ended
misdetection (no more reset-to-0), ytId/server regex on exact 11, blur scope
limited to .folio-embed.

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

Location:
src/assets/js
Files:
1 added
1 edited

Legend:

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

    r650b601 r4c9f29a  
    386386  function close() {
    387387    pause();
     388    mediaRegistry().release(registrySelf);
    388389    root.classList.add('audio-player-hidden');
    389390    document.body.classList.remove('has-audio-player');
     
    425426
    426427  // ============================================================
     428  // 4b. Mutual exclusion — gedeelde media-registry (zie embed-player.js).
     429  // ============================================================
     430  // Alle spelers (deze site-speler + de YouTube/SoundCloud/Spotify-embeds)
     431  // registreren zich in window.pcmsMediaRegistry. Start er één, dan pauzeert de
     432  // vorige. Dit is de precieze vervanger van de oude focus/blur-heuristiek voor
     433  // de embeds met een echte JS-API. (De blur-fallback hieronder blijft staan
     434  // voor iframe-only embeds zonder API: Bandcamp/Apple Music/Vimeo.)
     435  function mediaRegistry() {
     436    if (window.pcmsMediaRegistry) return window.pcmsMediaRegistry;
     437    const r = {
     438      _active: null,
     439      setActive(player) {
     440        if (this._active && this._active !== player && this._active.pause) {
     441          try { this._active.pause(); } catch (e) {}
     442        }
     443        this._active = player;
     444      },
     445      release(player) { if (this._active === player) this._active = null; },
     446    };
     447    window.pcmsMediaRegistry = r;
     448    return r;
     449  }
     450  const registrySelf = { pause() { try { audio.pause(); } catch (e) {} } };
     451
     452  // ============================================================
    427453  // 5. Audio element events → UI sync
    428454  // ============================================================
     
    434460    root.classList.add('is-playing');
    435461    root.classList.remove('audio-needs-tap');  // verstop tap-hint
     462    mediaRegistry().setActive(registrySelf);   // pauzeer eventueel spelende embeds
    436463  });
    437464  // Reset de error-teller pas bij ECHTE playback-start (`playing`), niet bij
     
    547574  });
    548575
    549   // Geen dubbel geluid: élke embed (YouTube/Vimeo/SoundCloud/Spotify/Apple Music/
    550   // Bandcamp) is een iframe. Zodra de gebruiker er een aanklikt om af te spelen,
    551   // gaat de focus naar dat iframe -> window 'blur'. Speelt onze speler dan?
    552   // Pauzeer 'm. (Op de site zijn iframes per definitie embeds.)
     576  // Vangnet voor mutual exclusion. Voor YouTube/SoundCloud/Spotify-embeds doet de
     577  // registry dit al precies (echte play-events). Maar voor iframe-only embeds
     578  // ZONDER JS-API (Bandcamp/Apple/Vimeo) én voor de iframe-FALLBACK (als een
     579  // ad-blocker de player-API blokkeert) is er geen play-event: daar vangen we het
     580  // af via focus. Klikt de gebruiker zo'n iframe aan → window 'blur' → pauzeer
     581  // onze speler. (Voor de API-embeds is dit hooguit een onschadelijke dubbele
     582  // pauze.)
    553583  window.addEventListener('blur', () => {
    554584    setTimeout(() => {
    555585      const el = document.activeElement;
    556       if (el && el.tagName === 'IFRAME' && audio.src && !audio.paused) {
     586      // Alleen embed-iframes (binnen .folio-embed) pauzeren de speler — niet een
     587      // willekeurig iframe (captcha/reclame/kaart) dat per ongeluk focus krijgt.
     588      if (el && el.tagName === 'IFRAME' && el.closest('.folio-embed') && audio.src && !audio.paused) {
    557589        pause();
    558590      }
Note: See TracChangeset for help on using the changeset viewer.