Changeset 995b100 in Klonkt for src/assets/js/embed-player.js
- Timestamp:
- 08/17/2026 07:32:49 AM (3 weeks ago)
- Branches:
- main
- Children:
- e854ace
- Parents:
- d97c58c
- git-author:
- Robin <roboburr@…> (08/17/2026 07:30:41 AM)
- git-committer:
- Robin <roboburr@…> (08/17/2026 07:32:49 AM)
- File:
-
- 1 edited
-
src/assets/js/embed-player.js (modified) (8 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/js/embed-player.js
rd97c58c r995b100 113 113 function fallbackIframe(provider, ref, url) { 114 114 if (provider === 'youtube') { 115 const id = ytId(ref, url);116 return { src: 'https://www.youtube-nocookie.com/embed/' + encodeURIComponent(id) + '?autoplay=1&rel=0', ratio: true, fs: true };115 const src = ytEmbedSrc(ref, url, 'autoplay=1&rel=0'); 116 if (src) return { src, ratio: true, fs: true }; 117 117 } 118 118 if (provider === 'soundcloud') { … … 134 134 return m + ':' + (s < 10 ? '0' : '') + s; 135 135 } 136 // Three ref shapes, the same ones AudioEmbedService.detectProvider produces 137 // and the same ones the Klonkt hub uses, so a ref travels between them 138 // unchanged: "<video>" | "<video>?list=<L>" | "list:<L>" 139 // 140 // ytId answers only "which VIDEO", because that is what a poster thumbnail 141 // needs; for a bare playlist there is no video and it returns null. 136 142 function ytId(ref, url) { 137 if (ref && /^[A-Za-z0-9_-]{11}$/.test(ref)) return ref; 143 const r = String(ref || ''); 144 if (r.indexOf('list:') === 0) return null; // a playlist has no single video 145 const bare = r.split('?list=')[0]; 146 if (/^[A-Za-z0-9_-]{11}$/.test(bare)) return bare; 138 147 const m = (url || '').match(/(?:youtube\.com\/(?:watch\?(?:.*&)?v=|embed\/|shorts\/|live\/)|youtu\.be\/)([A-Za-z0-9_-]{11})/); 139 148 if (m) return m[1]; 140 149 return null; // no blind slice — an invalid ref returns nothing rather than a broken id 150 } 151 /** The playlist id of a ref (or of the URL it came from), else null. */ 152 function ytList(ref, url) { 153 const r = String(ref || ''); 154 if (r.indexOf('list:') === 0) return r.slice(5) || null; 155 const i = r.indexOf('?list='); 156 if (i > 0) return r.slice(i + 6) || null; 157 const m = (url || '').match(/[?&](?:amp;)?list=([A-Za-z0-9_-]{10,60})/); 158 return m ? m[1] : null; 159 } 160 /** The /embed/ URL for a ref. A bare playlist embeds as `videoseries`. */ 161 function ytEmbedSrc(ref, url, query) { 162 const base = 'https://www.youtube-nocookie.com/embed/'; 163 const id = ytId(ref, url); 164 const list = ytList(ref, url); 165 let src; 166 if (!id && list) src = base + 'videoseries?list=' + encodeURIComponent(list); 167 else if (id && list) src = base + encodeURIComponent(id) + '?list=' + encodeURIComponent(list); 168 else if (id) src = base + encodeURIComponent(id); 169 else return null; 170 return src + (src.indexOf('?') > 0 ? '&' : '?') + (query || ''); 141 171 } 142 172 // Only allow http(s) as href (defense-in-depth against javascript:/data: URIs). … … 166 196 let html = ''; 167 197 if (provider === 'youtube') { 168 const id = ytId(ref, url);169 html = id170 ? '<div class="pcms-embed-ratio"><iframe src=" https://www.youtube-nocookie.com/embed/' + encodeURIComponent(id) + '?rel=0" title="YouTube" loading="lazy" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>'198 const src = ytEmbedSrc(ref, url, 'rel=0'); 199 html = src 200 ? '<div class="pcms-embed-ratio"><iframe src="' + escAttr(src) + '" title="YouTube" loading="lazy" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>' 171 201 : '<a class="pcms-embed-plain-link" href="' + escAttr(safeHref(url)) + '" target="_blank" rel="noopener">YouTube</a>'; 172 202 } else if (provider === 'soundcloud') { … … 215 245 const isVideo = provider === 'youtube'; 216 246 const custom = provider === 'youtube' || provider === 'soundcloud'; // eigen controls 217 const poster = provider === 'youtube' 218 ? `https://i.ytimg.com/vi/${ytId(ref, url)}/hqdefault.jpg` : ''; 247 // A bare playlist has no video id, and interpolating null gave 248 // i.ytimg.com/vi/null/hqdefault.jpg — a 404 painted as the poster. 249 const posterId = provider === 'youtube' ? ytId(ref, url) : null; 250 const poster = posterId ? `https://i.ytimg.com/vi/${posterId}/hqdefault.jpg` : ''; 219 251 220 252 el.innerHTML = '' … … 388 420 const YT = await ytApi(); 389 421 const id = ytId(ref, url); 422 const list = ytList(ref, url); 390 423 return new Promise((resolve, reject) => { 391 424 let pollTimer = null; 425 let endTimer = null; // see onStateChange: a list "ends" between items 392 426 const player = new YT.Player(mountEl, { 393 videoId: id, 427 // With a video AND a list, `list` makes the player continue into the 428 // playlist after this song. Without a video it is the playlist 429 // itself, and then listType says so -- videoId must stay absent, or 430 // the API loads that one video and forgets the list. 431 ...(id ? { videoId: id } : {}), 394 432 host: 'https://www.youtube-nocookie.com', 395 433 playerVars: { 396 434 controls: 0, modestbranding: 1, rel: 0, playsinline: 1, fs: 0, 397 435 disablekb: 1, iv_load_policy: 3, origin: window.location.origin, 436 ...(list ? (id ? { list } : { list, listType: 'playlist' }) : {}), 398 437 }, 399 438 events: { … … 408 447 destroy() { 409 448 if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } 449 if (endTimer) { clearTimeout(endTimer); endTimer = null; } 410 450 try { player.destroy(); } catch (e) {} 411 451 }, … … 415 455 // -1 unstarted, 0 ended, 1 playing, 2 paused, 3 buffering, 5 cued 416 456 if (e.data === 1) { 457 // A next playlist item started, so the pending "it is over" was 458 // false alarm. 459 if (endTimer) { clearTimeout(endTimer); endTimer = null; } 417 460 hooks.onPlay(); 418 461 if (!pollTimer) pollTimer = setInterval(() => { … … 423 466 if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } 424 467 } else if (e.data === 0) { 425 hooks.onEnded();426 468 if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } 469 // INSIDE a playlist, YouTube reports "ended" between every two 470 // songs as well. Firing onEnded there hands the queue on after 471 // song one and cuts the album off. So on a list we wait, and 472 // only a silence that is not interrupted by the next song 473 // counts as the end. Same rule as the hub (2.5s). 474 if (list) { 475 if (!endTimer) endTimer = setTimeout(() => { endTimer = null; hooks.onEnded(); }, 2500); 476 } else { 477 hooks.onEnded(); 478 } 427 479 } 428 480 },
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)