Changeset 9e6b54f in Klonkt
- Timestamp:
- 06/20/2026 03:53:58 AM (3 months ago)
- Branches:
- main
- Children:
- 976d36d
- Parents:
- 73b41eb
- Location:
- src
- Files:
-
- 4 edited
-
assets/css/embed.css (modified) (1 diff)
-
assets/js/audio-player.js (modified) (11 diffs)
-
assets/js/embed-player.js (modified) (2 diffs)
-
views/shell.ejs (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/embed.css
r73b41eb r9e6b54f 122 122 pointer-events: none; 123 123 } 124 /* Verborgen YouTube-audio-speler ván de site-audiospeler (onderbalk). Off-screen 125 * met echte grootte zodat de browser 'm niet pauzeert; alleen het geluid telt. */ 126 #pcms-yt-host { 127 position: absolute; 128 left: -99999px; 129 top: 0; 130 width: 320px; 131 height: 180px; 132 opacity: 0; 133 pointer-events: none; 134 } 135 /* Audio-only embed-kaart die nu via de onderbalk speelt = launcher-hint. */ 136 .pcms-embed-elsewhere { opacity: .9; } 137 .pcms-embed-elsewhere .pcms-embed-title::after { 138 content: ' — speelt in de speler ↓'; 139 font-weight: normal; 140 opacity: .6; 141 font-size: .85em; 142 } 143 124 144 /* Audio-only YouTube: de YT-speler draait verborgen (off-screen) maar moet een 125 145 * echte grootte houden, anders pauzeert de browser 'm. Alleen het geluid telt. */ -
src/assets/js/audio-player.js
r73b41eb r9e6b54f 289 289 }); 290 290 291 // ============================================================ 292 // 4a. YouTube-backend — speelt YT-AUDIO via een verborgen IFrame-speler in de 293 // (persistente) speler-root. Zo speelt YouTube net als de site-tracks door bij 294 // htmx-navigatie en is 't bedienbaar in de onderbalk. Video wordt niet getoond. 295 // ============================================================ 296 let ytMode = false, ytPlayer = null, ytReady = false, ytApiLoading = null, ytTick = null; 297 298 function loadYTApi() { 299 if (window.YT && window.YT.Player) return Promise.resolve(window.YT); 300 if (ytApiLoading) return ytApiLoading; 301 ytApiLoading = new Promise((resolve) => { 302 const prev = window.onYouTubeIframeAPIReady; 303 window.onYouTubeIframeAPIReady = function () { if (prev) { try { prev(); } catch (e) {} } resolve(window.YT); }; 304 if (!document.querySelector('script[src*="youtube.com/iframe_api"]')) { 305 const s = document.createElement('script'); s.src = 'https://www.youtube.com/iframe_api'; s.async = true; document.head.appendChild(s); 306 } 307 setTimeout(() => { if (window.YT && window.YT.Player) resolve(window.YT); }, 8000); 308 }); 309 return ytApiLoading; 310 } 311 312 function ensureYT() { 313 if (ytPlayer) return Promise.resolve(ytPlayer); 314 return loadYTApi().then((YT) => new Promise((resolve) => { 315 if (!YT || !YT.Player) { resolve(null); return; } 316 let host = document.getElementById('pcms-yt-host'); 317 if (!host) { 318 host = document.createElement('div'); host.id = 'pcms-yt-host'; 319 const m = document.createElement('div'); m.id = 'pcms-yt-mount'; host.appendChild(m); 320 root.appendChild(host); 321 } 322 ytPlayer = new YT.Player('pcms-yt-mount', { 323 host: 'https://www.youtube-nocookie.com', 324 playerVars: { controls: 0, playsinline: 1, rel: 0, modestbranding: 1, iv_load_policy: 3, origin: window.location.origin }, 325 events: { 326 onReady: () => { ytReady = true; try { ytPlayer.setVolume(audio.muted ? 0 : Math.round(audio.volume * 100)); } catch (e) {} resolve(ytPlayer); }, 327 onStateChange: onYTState, 328 }, 329 }); 330 setTimeout(() => resolve(ytPlayer), 8000); 331 })); 332 } 333 334 function onYTState(e) { 335 if (!ytMode) return; 336 const s = e.data; // 1 playing, 2 paused, 0 ended 337 if (s === 1) { 338 isPlaying = true; root.classList.add('is-playing'); root.classList.remove('audio-needs-tap'); 339 mediaRegistry().setActive(registrySelf); startYTTick(); pullYTMeta(); 340 } else if (s === 2) { 341 isPlaying = false; root.classList.remove('is-playing'); stopYTTick(); 342 } else if (s === 0) { 343 stopYTTick(); next(); 344 } 345 } 346 347 function startYTTick() { 348 if (ytTick) return; 349 ytTick = setInterval(() => { 350 if (!ytPlayer || !ytMode) return; 351 try { updateProgressUI(ytPlayer.getCurrentTime() || 0, ytPlayer.getDuration() || 0); } catch (e) {} 352 }, 250); 353 } 354 function stopYTTick() { if (ytTick) { clearInterval(ytTick); ytTick = null; } } 355 356 function pullYTMeta() { 357 try { 358 const t = queue[currentIndex]; if (!t || !t.yt || !ytPlayer.getVideoData) return; 359 const vd = ytPlayer.getVideoData(); 360 if (vd && vd.title && (!t.title || t.title === 'YouTube')) { 361 t.title = vd.title; titleEl.textContent = vd.title; sheetTitle.textContent = vd.title; renderQueue(); markPlaying(t.id); 362 } 363 } catch (e) {} 364 } 365 366 // Gedeelde voortgang-UI (beide backends). Audio gebruikt 'timeupdate', YT de tick. 367 function updateProgressUI(cur, dur) { 368 if (!dur || isNaN(dur)) return; 369 const pct = Math.max(0, Math.min(100, (cur / dur) * 100)); 370 seekFill.style.width = pct + '%'; sheetSeekFill.style.width = pct + '%'; 371 currentEl.textContent = formatTime(cur); totalEl.textContent = formatTime(dur); 372 sheetCurrent.textContent = formatTime(cur); sheetTotal.textContent = formatTime(dur); 373 } 374 291 375 function loadTrack(index, autoplay, metaOnly) { 292 376 if (!queue[index]) { … … 296 380 currentIndex = index; 297 381 const t = queue[index]; 298 if (!t.url) { 299 console.error('[pcms-audio] track has no url', t); 300 return; 301 } 302 console.log('[pcms-audio] loading', t.title, t.url, metaOnly ? '(meta only)' : ''); 303 // Metadata + chrome update synchronously so the UI reacts instantly while 304 // the bytes download. 305 titleEl.textContent = t.title || 'Untitled'; 382 // Metadata + chrome update synchronously (geldt voor beide backends). 383 titleEl.textContent = t.title || (t.yt ? 'YouTube' : 'Untitled'); 306 384 artistEl.textContent = t.artist || ''; 307 sheetTitle.textContent = t.title || 'Untitled';385 sheetTitle.textContent = t.title || (t.yt ? 'YouTube' : 'Untitled'); 308 386 sheetArtist.textContent = t.artist || ''; 309 387 sheetAlbum.textContent = albumName || ''; … … 316 394 317 395 if (metaOnly) return; 396 397 // YouTube-bron → verborgen YT-speler i.p.v. de blob-<audio>. 398 if (t.yt) { 399 ytMode = true; 400 try { audio.pause(); } catch (e) {} 401 loadSeq++; dropPreload(); 402 updateProgressUI(0, 1); currentEl.textContent = '0:00'; totalEl.textContent = '0:00'; 403 seekFill.style.width = '0%'; sheetSeekFill.style.width = '0%'; 404 root.classList.add('audio-loading'); 405 ensureYT().then((p) => { 406 if (queue[currentIndex] !== t) return; // inmiddels andere track gekozen 407 root.classList.remove('audio-loading'); 408 if (!p) { onLoadError(new Error('YT API niet beschikbaar'), loadSeq); return; } 409 try { 410 if (autoplay) p.loadVideoById(t.yt); else p.cueVideoById(t.yt); 411 p.setVolume(audio.muted ? 0 : Math.round(audio.volume * 100)); 412 } catch (e) {} 413 }); 414 return; 415 } 416 417 // Normale blob-audio track. 418 ytMode = false; 419 stopYTTick(); 420 try { if (ytPlayer && ytPlayer.stopVideo) ytPlayer.stopVideo(); } catch (e) {} 421 if (!t.url) { console.error('[pcms-audio] track has no url', t); return; } 318 422 319 423 const mySeq = ++loadSeq; … … 367 471 368 472 function play() { 473 if (ytMode) { 474 if (ytPlayer && ytReady) { try { ytPlayer.playVideo(); } catch (e) {} } 475 else if (queue[currentIndex]) loadTrack(currentIndex, true); 476 return; 477 } 369 478 if (!audio.src) { 370 479 // Nothing fetched yet (pre-seed showed metadata only, or a load is still … … 388 497 } 389 498 } 390 function pause() { audio.pause(); }391 function togglePlay() { audio.paused ? play() : pause(); }499 function pause() { if (ytMode) { try { ytPlayer && ytPlayer.pauseVideo(); } catch (e) {} return; } audio.pause(); } 500 function togglePlay() { if (ytMode) { isPlaying ? pause() : play(); return; } audio.paused ? play() : pause(); } 392 501 function next() { 393 502 if (!queue.length) return; … … 404 513 document.body.classList.remove('has-audio-player'); 405 514 closeSheet(); 515 ytMode = false; stopYTTick(); 516 try { if (ytPlayer && ytPlayer.stopVideo) ytPlayer.stopVideo(); } catch (e) {} 406 517 queue = []; 407 518 albumName = ''; … … 462 573 return r; 463 574 } 464 const registrySelf = { pause() { try { audio.pause(); } catch (e) {} } };575 const registrySelf = { pause() { try { audio.pause(); } catch (e) {} try { if (ytPlayer && ytPlayer.pauseVideo) ytPlayer.pauseVideo(); } catch (e) {} } }; 465 576 466 577 // ============================================================ … … 498 609 audio.addEventListener('volumechange', () => { root.classList.toggle('is-muted', audio.muted || audio.volume === 0); }); 499 610 audio.addEventListener('timeupdate', () => { 500 if (!audio.duration || isNaN(audio.duration)) return; 501 const pct = (audio.currentTime / audio.duration) * 100; 502 seekFill.style.width = pct + '%'; 503 sheetSeekFill.style.width = pct + '%'; 504 currentEl.textContent = formatTime(audio.currentTime); 505 totalEl.textContent = formatTime(audio.duration); 506 sheetCurrent.textContent = formatTime(audio.currentTime); 507 sheetTotal.textContent = formatTime(audio.duration); 611 if (ytMode) return; // YT-voortgang loopt via de tick 612 updateProgressUI(audio.currentTime, audio.duration); 508 613 }); 509 614 … … 516 621 function attachSeek(seekEl) { 517 622 seekEl.addEventListener('click', (e) => { 518 if (!audio.duration) return;519 623 const rect = seekEl.getBoundingClientRect(); 520 624 const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); 625 if (ytMode) { 626 try { const d = ytPlayer.getDuration() || 0; if (d) ytPlayer.seekTo(ratio * d, true); } catch (e2) {} 627 return; 628 } 629 if (!audio.duration) return; 521 630 audio.currentTime = ratio * audio.duration; 522 631 }); … … 527 636 audio.volume = 0.8; 528 637 volumeSlider.addEventListener('input', () => { 529 audio.volume = volumeSlider.value / 100; 530 if (volumeSlider.value > 0) audio.muted = false; 531 }); 532 muteBtn.addEventListener('click', () => { audio.muted = !audio.muted; }); 638 const v = parseInt(volumeSlider.value, 10) || 0; 639 audio.volume = v / 100; 640 if (v > 0) audio.muted = false; 641 if (ytPlayer) { try { ytPlayer.setVolume(v); if (v > 0 && ytPlayer.unMute) ytPlayer.unMute(); } catch (e) {} } 642 }); 643 muteBtn.addEventListener('click', () => { 644 audio.muted = !audio.muted; 645 if (ytPlayer) { try { audio.muted ? ytPlayer.mute() : ytPlayer.unMute(); } catch (e) {} } 646 root.classList.toggle('is-muted', audio.muted); 647 }); 533 648 534 649 // ============================================================ … … 797 912 // 9. Public API 798 913 // ============================================================ 914 // Speel een YouTube-bron als AUDIO af in deze (persistente) site-speler — door 915 // de embed-speler aangeroepen voor een audio-only YouTube-embed. Zo verschijnt 916 // 'ie in de onderbalk én speelt 'ie door bij navigeren. 917 function playYouTube(opts) { 918 if (!opts || !opts.id) return; 919 setQueue([{ 920 yt: opts.id, 921 title: opts.title || 'YouTube', 922 artist: opts.artist || '', 923 cover: opts.cover || '', 924 postUrl: opts.postUrl || '', 925 }], 0); 926 } 927 799 928 window.pcmsAudioPlayer = { 800 setQueue, play, pause, next, prev, close, openSheet, closeSheet, 929 setQueue, play, pause, next, prev, close, openSheet, closeSheet, playYouTube, 801 930 isPlaying: () => isPlaying, 802 931 currentTrack: () => queue[currentIndex] || null, -
src/assets/js/embed-player.js
r73b41eb r9e6b54f 189 189 if (audioOnly) el.classList.add('pcms-embed-audio-mode'); 190 190 const isVideo = provider === 'youtube' && !audioOnly; 191 const custom = provider === 'youtube' || provider === 'soundcloud'; // eigen controls 191 // Audio-only YT speelt via de site-onderbalk → geen eigen in-kaart controlebar. 192 const custom = (provider === 'youtube' || provider === 'soundcloud') && !audioOnly; 192 193 const poster = provider === 'youtube' 193 194 ? `https://i.ytimg.com/vi/${ytId(ref, url)}/hqdefault.jpg` : ''; … … 306 307 // Eerste interactie → adapter mounten + spelen. Daarna toggelt de knop. 307 308 async function ensureMountedAndPlay() { 309 // Audio-only YouTube → speel via de site-audiospeler (onderbalk): zichtbaar 310 // én doorspelen bij navigeren. De inline-kaart is enkel de launcher. 311 if (audioOnly && window.pcmsAudioPlayer && typeof window.pcmsAudioPlayer.playYouTube === 'function') { 312 window.pcmsAudioPlayer.playYouTube({ id: ytId(ref, url), cover: poster, postUrl: location.pathname + location.search }); 313 el.classList.add('pcms-embed-elsewhere'); 314 return; 315 } 308 316 if (mounted) { if (adapter) adapter.play(); return; } 309 317 mounted = true; -
src/views/shell.ejs
r73b41eb r9e6b54f 170 170 <link rel="stylesheet" href="/assets/css/audio.css?v=6"> 171 171 <!-- Eigen custom media-embeds (YouTube/SoundCloud/Spotify) in huisstijl. --> 172 <link rel="stylesheet" href="/assets/css/embed.css?v= 5">172 <link rel="stylesheet" href="/assets/css/embed.css?v=6"> 173 173 174 174 <%- include('partials/shared-styles') %> … … 300 300 ?v=N — cache-buster: bump bij elke audio-player.js wijziging zodat 301 301 Cloudflare (max-age=1y) niet de oude versie blijft serveren. --> 302 <script src="/assets/js/audio-player.js?v=2 0"></script>302 <script src="/assets/js/audio-player.js?v=21"></script> 303 303 <!-- Eigen custom media-embeds (YouTube/SoundCloud/Spotify) via de echte 304 304 player-API's + gedeelde mutual-exclusion registry met de site-speler. --> 305 <script src="/assets/js/embed-player.js?v= 8" defer></script>305 <script src="/assets/js/embed-player.js?v=9" defer></script> 306 306 <% if (site && site.enable_audio_player && audioTracks && audioTracks.length > 0) { %> 307 307 <script>window.PCMS_SITE_TRACKS = <%- JSON.stringify(audioTracks) %>;</script>
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)