Changeset 834bcc3 in Klonkt for src/assets/js/embed-player.js
- Timestamp:
- 06/23/2026 06:14:27 PM (3 months ago)
- Branches:
- main
- Children:
- d774679
- Parents:
- bb42dfb
- File:
-
- 1 edited
-
src/assets/js/embed-player.js (modified) (27 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/js/embed-player.js
rbb42dfb r834bcc3 1 1 /** 2 * Klonkt Embed Player — eigen, in-huisstijl media-embeds bovenop de ÉCHTE3 * player -API's (YouTube IFrame API, SoundCloud Widget API, Spotify iFrame API).2 * Klonkt Embed Player — custom, on-brand media embeds on top of the REAL 3 * player APIs (YouTube IFrame API, SoundCloud Widget API, Spotify iFrame API). 4 4 * 5 * De server (AudioEmbedService) rendert per embed een placeholder:5 * The server (AudioEmbedService) renders a placeholder per embed: 6 6 * <div class="folio-embed folio-embed--<provider> pcms-embed-card" 7 7 * data-embed-provider data-embed-ref data-embed-type data-embed-url></div> 8 * Dit script bouwt daar onze eigen kaart omheen (cover/poster + onze play-knop+9 * voortgangsbalk in huisstijl) en bestuurt de onderliggende speler via de10 * platform -API, zodat play/pause/voortgang in ÓNZE handen liggen.8 * This script wraps it in our own card (cover/poster + our play button + 9 * progress bar in brand style) and controls the underlying player via the 10 * platform API, so play/pause/progress are in OUR hands. 11 11 * 12 * Capability -eerlijkheid:13 * - youtube/soundcloud → volledig eigen controls (native chrome verborgen).14 * - spotify → besturing + onze frame eromheen; Spotify's eigen UI15 * blijft binnenin (kan niet anders zonderPremium+OAuth).12 * Capability honesty: 13 * - youtube/soundcloud → fully custom controls (native chrome hidden). 14 * - spotify → controls + our frame around it; Spotify's own UI 15 * stays inside (no alternative without Premium+OAuth). 16 16 * 17 * Mutual exclusion: e lke speler (incl. de site-audiospeler in audio-player.js)18 * regist reert zich in window.pcmsMediaRegistry. Start er één → de vorige pauzeert.19 * Dit vervangt de oude focus/blur-heuristiek door echte play-events.17 * Mutual exclusion: every player (incl. the site audio player in audio-player.js) 18 * registers itself in window.pcmsMediaRegistry. Starting one pauses the previous. 19 * This replaces the old focus/blur heuristic with real play events. 20 20 * 21 * Singleton — guard tegen dubbel-init (HTMX laadt scripts soms opnieuw).21 * Singleton — guard against double-init (HTMX can reload scripts). 22 22 */ 23 23 (function () { … … 25 25 26 26 // ============================================================ 27 // 0. Gedeelde playback-registry (ook door audio-player.js gebruikt)27 // 0. Shared playback registry (also used by audio-player.js) 28 28 // ============================================================ 29 29 function registry() { … … 31 31 const r = { 32 32 _active: null, 33 // Mark eer `player` als de enige actieve; pauzeer de vorige.33 // Mark `player` as the sole active one; pause the previous. 34 34 setActive(player) { 35 35 if (this._active && this._active !== player && this._active.pause) { … … 45 45 46 46 // ============================================================ 47 // 1. Lazy script -loaders — 1 promise per platform, gedeeld overN embeds.48 // We laden een platform-script PAS als er een embed van dat platform op de49 // pagina daadwerkelijk wordt gestart.47 // 1. Lazy script loaders — 1 promise per platform, shared across N embeds. 48 // A platform script is only loaded when an embed from that platform 49 // is actually started on the page. 50 50 // ============================================================ 51 51 const scripts = {}; … … 55 55 s.src = src; s.async = true; 56 56 s.onload = () => resolve(); 57 s.onerror = () => reject(new Error('embed -script faalde: ' + src));57 s.onerror = () => reject(new Error('embed script failed: ' + src)); 58 58 document.head.appendChild(s); 59 59 }); 60 60 } 61 // Belangrijk: alle loaders REJECTEN bij een script-fout (bv. een ad-blocker die62 // het API-script blokkeert) én na een time-out — zodat de aanroeper netjes kan63 // t erugvallen op het kale platform-iframe i.p.v. eeuwig te hangen.61 // Important: all loaders REJECT on script errors (e.g. an ad-blocker blocking 62 // the API script) and on timeout — so the caller can gracefully fall back to 63 // the plain platform iframe instead of hanging indefinitely. 64 64 const API_TIMEOUT = 8000; 65 65 66 // YouTube: global e callback onYouTubeIframeAPIReady (1×) → in promise wikkelen.66 // YouTube: global callback onYouTubeIframeAPIReady (once) → wrap in a promise. 67 67 function ytApi() { 68 68 if (scripts.yt) return scripts.yt; … … 79 79 return scripts.yt; 80 80 } 81 // SoundCloud: geen globale ready-callback; resolve op script-onload, daarna82 // wacht elke widget op z'n eigen SC.Widget.Events.READY.81 // SoundCloud: no global ready callback; resolve on script onload, then 82 // each widget waits for its own SC.Widget.Events.READY. 83 83 function scApi() { 84 84 if (scripts.sc) return scripts.sc; … … 91 91 return scripts.sc; 92 92 } 93 // Spotify: global e callback onSpotifyIframeApiReady(IFrameAPI) (1×) → wrappen.93 // Spotify: global callback onSpotifyIframeApiReady(IFrameAPI) (once) → wrap in a promise. 94 94 function spotifyApi() { 95 95 if (scripts.sp) return scripts.sp; … … 101 101 }; 102 102 loadScript('https://open.spotify.com/embed/iframe-api/v1').catch(reject); 103 // Korter dan API_TIMEOUT: Spotify's bundle initialiseert snel óf helemaal104 // niet (CDN 503 / origin-gating). Niet 8s wachten vóór de iframe-fallback.103 // Shorter than API_TIMEOUT: Spotify's bundle initialises quickly or not at all 104 // (CDN 503 / origin-gating). No need to wait 8 s before the iframe fallback. 105 105 setTimeout(() => reject(new Error('Spotify API timeout')), 4000); 106 106 }); … … 108 108 } 109 109 110 // Kaal platform-iframe als fallback wanneer de JS-API geblokkeerd/onbereikbaar111 // is. Ad-blockers laten de embed-iframes doorgaans wél door. Autoplay-param112 // omdat we hier altijd in een user-gesture-context zitten (klik op play).110 // Plain platform iframe as fallback when the JS API is blocked/unreachable. 111 // Ad-blockers generally let embed iframes through. Autoplay parameter 112 // is safe here because we're always in a user-gesture context (play click). 113 113 function fallbackIframe(provider, ref, url) { 114 114 if (provider === 'youtube') { … … 138 138 const m = (url || '').match(/(?:youtube\.com\/(?:watch\?(?:.*&)?v=|embed\/|shorts\/|live\/)|youtu\.be\/)([A-Za-z0-9_-]{11})/); 139 139 if (m) return m[1]; 140 return null; // geen blinde slice — een ongeldige ref geeft liever niets dan een kapotteid141 } 142 // Alleen http(s) als href toelaten (defense-in-depth tegen javascript:/data:).140 return null; // no blind slice — an invalid ref returns nothing rather than a broken id 141 } 142 // Only allow http(s) as href (defense-in-depth against javascript:/data: URIs). 143 143 function safeHref(u) { 144 144 try { const p = new URL(u, location.href); return (p.protocol === 'http:' || p.protocol === 'https:') ? u : '#'; } … … 154 154 155 155 // ============================================================ 156 // 3. Card controller — b ouwt de huisstijl-kaart + delegeert naar een adapter156 // 3. Card controller — builds the on-brand card + delegates to an adapter 157 157 // ============================================================ 158 158 function buildCard(el) { … … 165 165 el.classList.remove('pcms-embed-loading'); 166 166 167 // O nze speler-zijde (registry-peer). pause() wijst naar de adapter zodra die168 // gemount is; ervoor is 't eenno-op.167 // Our player side (registry peer). pause() points to the adapter once 168 // mounted; before that it's a no-op. 169 169 let adapter = null; 170 170 const self = { pause() { if (adapter && adapter.pause) { try { adapter.pause(); } catch (e) {} } } }; 171 171 172 // Teardown -hook: aangeroepen door de MutationObserver als deze kaart uit de173 // DOM verdwijnt (HTMX-swap) → adapter opruimen (timers/iframes) + registry174 // vrijgeven, zodat er geen poll-timers of spelers blijven lekken.172 // Teardown hook: called by the MutationObserver when this card leaves the 173 // DOM (HTMX swap) → clean up the adapter (timers/iframes) + release the registry, 174 // so no poll timers or players keep leaking. 175 175 el._pcmsDestroy = function () { 176 176 try { if (adapter && adapter.destroy) adapter.destroy(); } catch (e) {} … … 182 182 let dur = 0; 183 183 184 // --- UI ophangen (verschilt per provider-type) ---184 // --- Mount the UI (differs per provider type) --- 185 185 const isVideo = provider === 'youtube'; 186 186 const custom = provider === 'youtube' || provider === 'soundcloud'; // eigen controls … … 220 220 const volEl = el.querySelector('.pcms-embed-vol'); 221 221 const muteBtn = el.querySelector('.pcms-embed-mute'); 222 let vol = 100; // huidige volume 0-100 (wordt op de adapter toegepast)222 let vol = 100; // current volume 0-100 (applied to the adapter) 223 223 let preMuteVol = 100; 224 224 … … 251 251 }; 252 252 253 // API geblokkeerd/onbereikbaar → kaal platform-iframe (graceful degradation).254 // De mutual-exclusion loopt voor deze fallback via de blur-heuristiek255 // (audio-player.js), want de kaart krijgt geen.is-mounted.253 // API blocked/unreachable → plain platform iframe (graceful degradation). 254 // Mutual exclusion for this fallback runs via the blur heuristic 255 // (audio-player.js), because the card never gets .is-mounted. 256 256 function renderFallback() { 257 257 const fb = fallbackIframe(provider, ref, url); … … 271 271 el.appendChild(iframe); 272 272 273 // Mutual exclusion óók voor de fallback-iframe. Eencross-origin iframe274 // kunnen we niet via een API pauzeren, dus 'pauze' = herladen ZONDER275 // autoplay (= stop t het geluid, speler blijft zichtbaar/herstartbaar).276 // We regist reren 'm als actief: nu (= gebruiker start de embed) pauzeert de277 // site -speler/andere embeds; en als de site-speler later start, pauzeert de278 // registry dezefallback.273 // Mutual exclusion also for the fallback iframe. A cross-origin iframe 274 // cannot be paused via an API, so 'pause' = reload WITHOUT 275 // autoplay (= stops the audio, player stays visible/restartable). 276 // We register it as active: now (= user starts the embed) the 277 // site player/other embeds pause; and when the site player starts later, 278 // the registry pauses this fallback. 279 279 self.pause = function () { 280 280 try { … … 294 294 } 295 295 296 // Eerste interactie → adapter mounten + spelen. Daarna toggelt de knop.296 // First interaction → mount the adapter + play. The button toggles after that. 297 297 async function ensureMountedAndPlay() { 298 298 if (mounted) { if (adapter) adapter.play(); return; } 299 299 mounted = true; 300 // Spotify: hun speler is toch niet te skinnen (besturing-only) én de301 // iFrame -API-bundle initialiseert in de praktijk vaak niet(CDN-gating/503)302 // → niet op de API wachten, meteen het kale Spotify-iframe tonen. Instant.300 // Spotify: their player cannot be skinned (controls-only) and the 301 // iFrame API bundle often fails to initialise in practice (CDN-gating/503) 302 // → don't wait for the API, show the plain Spotify iframe immediately. 303 303 if (provider === 'spotify') { renderFallback(); return; } 304 304 el.classList.add('pcms-embed-busy'); … … 306 306 adapter = await MOUNTERS[provider](mountEl, { ref, url }, hooks); 307 307 if (el._pcmsApplyVol) el._pcmsApplyVol(); // onthouden volume toepassen 308 // is-mounted: CSS verbergt de poster (video) of de Spotify-facade en309 // toont de echte speler. Voor SoundCloud blijft onze kaart+balk staan en310 // blijft het (functionele) iframe off-screen verborgen.308 // is-mounted: CSS hides the poster (video) or Spotify facade and 309 // shows the real player. For SoundCloud our card+bar stays visible and 310 // the (functional) iframe remains hidden off-screen. 311 311 el.classList.add('is-mounted'); 312 312 adapter.play(); 313 313 } catch (err) { 314 console.warn('[pcms-embed] API niet beschikbaar, val terug op kaaliframe', provider, err);314 console.warn('[pcms-embed] API unavailable, falling back to plain iframe', provider, err); 315 315 renderFallback(); 316 316 } … … 328 328 adapter.seek(ratio * dur); 329 329 }); 330 // Volume: s chuif zet vol (0-100) en past 'm toe op de adapter (YT/SC hebben331 // setVolume). De waarde wordt onthouden en na het mounten opnieuw toegepast.330 // Volume: slider sets vol (0-100) and applies it to the adapter (YT/SC support 331 // setVolume). The value is remembered and reapplied after mounting. 332 332 function applyVol() { 333 333 if (adapter && adapter.setVolume) { try { adapter.setVolume(vol); } catch (e) {} } … … 341 341 applyVol(); 342 342 }); 343 el._pcmsApplyVol = applyVol; // door ensureMountedAndPlay aangeroepen namount343 el._pcmsApplyVol = applyVol; // called by ensureMountedAndPlay after mount 344 344 } 345 345 … … 349 349 350 350 // ============================================================ 351 // 4. Adapters — normalise ren de 3 zeer verschillende API's naar 1 vorm.352 // E lke mounter geeft { play, pause, seek } terug en roept hooks aan met353 // second en (units worden hier rechtgetrokken).351 // 4. Adapters — normalise the 3 very different APIs to one common shape. 352 // Each mounter returns { play, pause, seek } and calls hooks with 353 // seconds (units are normalised here). 354 354 // ============================================================ 355 355 const MOUNTERS = { … … 403 403 }, 404 404 405 // ---- SoundCloud: Widget API, visual=false, eigencontrols ----405 // ---- SoundCloud: Widget API, visual=false, custom controls ---- 406 406 async soundcloud(mountEl, { ref, url }, hooks) { 407 407 const SC = await scApi(); 408 // Iframe zelf bouwen (kale balk) en daarna SC.Widget eraan hangen.408 // Build the iframe ourselves (bare bar) and then attach SC.Widget to it. 409 409 const iframe = document.createElement('iframe'); 410 410 iframe.allow = 'autoplay'; … … 453 453 }, 454 454 455 // ---- Spotify: iFrame API — besturing + onze frame; geen eigenskin ----455 // ---- Spotify: iFrame API — controls + our frame; no custom skin ---- 456 456 async spotify(mountEl, { ref, url }, hooks) { 457 457 const IFrameAPI = await spotifyApi(); … … 473 473 const d = e && e.data ? e.data : {}; 474 474 hooks.onProgress((d.position || 0) / 1000, (d.duration || 0) / 1000); 475 // Geen betrouwbare 'ended'-event bij Spotify; we behandelen elke476 // isPaused -overgang als play/pause. Einde = gewoon een pauze (de balk477 // blijft op de eindpositie i.p.v. misleidend naar 0 te springen).475 // No reliable 'ended' event from Spotify; we treat every 476 // isPaused transition as play/pause. End = just a pause (the bar 477 // stays at the end position rather than misleadingly jumping to 0). 478 478 if (d.isPaused === false && lastPaused) { lastPaused = false; hooks.onPlay(); } 479 479 else if (d.isPaused === true && !lastPaused) { lastPaused = true; hooks.onPause(); } … … 486 486 487 487 // ============================================================ 488 // 5. Scan + HTMX -/DOM-mutatie-aware (de site navigeert deels via HTMX-swaps)488 // 5. Scan + HTMX/DOM-mutation-aware (the site partially navigates via HTMX swaps) 489 489 // ============================================================ 490 490 function scan(root) { … … 492 492 if (el.dataset.embedInit) return; 493 493 el.dataset.embedInit = '1'; 494 try { buildCard(el); } catch (e) { console.error('[pcms-embed] buildCard fa alde', e); }494 try { buildCard(el); } catch (e) { console.error('[pcms-embed] buildCard failed', e); } 495 495 }); 496 496 } … … 501 501 scan(document); 502 502 } 503 // HTMX vervangt #pcms-main bij interne navigatie → opnieuw scannen.503 // HTMX replaces #pcms-main on internal navigation → rescan. 504 504 document.body.addEventListener('htmx:afterSwap', (e) => scan(e.target || document)); 505 505 document.body.addEventListener('htmx:load', (e) => scan(e.target || document)); 506 506 507 // Teardown bij verwijdering uit de DOM (HTMX vervangt #pcms-main innerHTML, of 508 // een SPA-achtige swap). Zonder dit blijven YouTube-poll-timers + adapters/ 509 // iframes hangen als je wegnavigeert terwijl een embed speelt → CPU/geheugenlek 510 // dat per navigatie opstapelt. We roepen el._pcmsDestroy() aan voor elke kaart 511 // die echt uit het document verdwijnt. 507 // Teardown on DOM removal (HTMX replaces #pcms-main innerHTML, or an SPA-like 508 // swap). Without this, YouTube poll timers + adapters/iframes linger when 509 // navigating away while an embed is playing → CPU/memory leak that accumulates 510 // per navigation. We call el._pcmsDestroy() for every card that truly leaves the document. 512 511 const teardownObserver = new MutationObserver((muts) => { 513 512 for (const m of muts) {
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)