Changeset 9e6b54f in Klonkt for src/assets/js/audio-player.js
- Timestamp:
- 06/20/2026 03:53:58 AM (3 months ago)
- Branches:
- main
- Children:
- 976d36d
- Parents:
- 73b41eb
- File:
-
- 1 edited
-
src/assets/js/audio-player.js (modified) (11 diffs)
Legend:
- Unmodified
- Added
- Removed
-
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,
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)