Changeset 21522ae in Klonkt for src/assets/js/audio-player.js
- Timestamp:
- 05/20/2026 10:14:01 PM (4 months ago)
- Branches:
- main
- Children:
- 353c39c
- Parents:
- 46f23fd
- git-author:
- Robin Genis <roboburr@…> (05/20/2026 10:13:26 PM)
- git-committer:
- Robin Genis <roboburr@…> (05/20/2026 10:14:01 PM)
- File:
-
- 1 edited
-
src/assets/js/audio-player.js (modified) (10 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/js/audio-player.js
r46f23fd r21522ae 158 158 let isPlaying = false; 159 159 let albumName = ''; 160 // Blob playback state. We fetch each track's bytes and play from a blob: 161 // object URL — no plain media URL is ever exposed to the page. currentObjectUrl 162 // is revoked when we move on, so we don't leak one Blob per track in memory. 163 let currentObjectUrl = null; 164 // Monotonic load token: a fast prev/next can fire several loads before an 165 // earlier fetch resolves. Only the latest load may set audio.src. 166 let loadSeq = 0; 160 167 161 168 // Hide initially … … 177 184 } 178 185 179 function loadTrack(index) { 186 // Fetch the track bytes and hand back a blob: object URL. The X-Audio-Player 187 // header + same-origin credentials get us past the stream route's access gate. 188 async function fetchAsObjectUrl(url) { 189 const r = await fetch(url, { 190 credentials: 'same-origin', 191 headers: { 'X-Audio-Player': '1' }, 192 }); 193 if (!r.ok) throw new Error('HTTP ' + r.status); 194 const blob = await r.blob(); 195 return URL.createObjectURL(blob); 196 } 197 198 // metaOnly: show the track in the UI but DON'T download its bytes yet. 199 // Used by the site pre-seed so opening a page doesn't auto-download audio; 200 // the blob is fetched lazily on the first play(). 201 function loadTrack(index, autoplay, metaOnly) { 180 202 if (!queue[index]) { 181 203 console.warn('[pcms-audio] loadTrack: no track at index', index); … … 188 210 return; 189 211 } 190 console.log('[pcms-audio] loading', t.title, t.url); 191 // Schone overgang: pause + reset voorkomt state-corruption van het 192 // audio-element na meerdere src-changes (bug die continuous playback 193 // brak na 3-4 tracks). audio.load() forceert reset van internal state. 194 try { audio.pause(); } catch (e) {} 195 audio.src = t.url; 196 try { audio.load(); } catch (e) {} 212 console.log('[pcms-audio] loading', t.title, t.url, metaOnly ? '(meta only)' : ''); 213 // Metadata + chrome update synchronously so the UI reacts instantly while 214 // the bytes download. 197 215 titleEl.textContent = t.title || 'Untitled'; 198 216 artistEl.textContent = t.artist || ''; … … 205 223 document.body.classList.add('has-audio-player'); 206 224 renderQueue(); 225 226 if (metaOnly) return; 227 228 const mySeq = ++loadSeq; 229 root.classList.add('audio-loading'); 230 fetchAsObjectUrl(t.url).then((objUrl) => { 231 if (mySeq !== loadSeq) { URL.revokeObjectURL(objUrl); return; } // superseded 232 root.classList.remove('audio-loading'); 233 // Free the previous track's blob — otherwise every track leaks a copy. 234 if (currentObjectUrl) { try { URL.revokeObjectURL(currentObjectUrl); } catch (e) {} } 235 currentObjectUrl = objUrl; 236 // Schone overgang: pause + load forceert reset van internal state na 237 // meerdere src-changes (voorkomt state-corruption van het audio-element). 238 try { audio.pause(); } catch (e) {} 239 audio.src = objUrl; 240 try { audio.load(); } catch (e) {} 241 if (autoplay) play(); 242 }).catch((err) => { 243 if (mySeq !== loadSeq) return; // superseded — ignore stale failure 244 root.classList.remove('audio-loading'); 245 console.error('[pcms-audio] track fetch failed', err); 246 // Treat a failed download like a playback error: bump the counter and 247 // auto-skip, but stop after 3 in a row so we never loop forever. 248 consecutiveErrors++; 249 if (consecutiveErrors < 3 && queue.length > 1) setTimeout(next, 400); 250 }); 207 251 } 208 252 … … 211 255 albumName = (opts && opts.albumName) || ''; 212 256 if (!queue.length) return; 213 loadTrack(typeof startIdx === 'number' ? Math.max(0, Math.min(startIdx, queue.length - 1)) : 0); 214 play(); 257 loadTrack(typeof startIdx === 'number' ? Math.max(0, Math.min(startIdx, queue.length - 1)) : 0, true); 215 258 } 216 259 217 260 function play() { 218 if (!audio.src) return; 261 if (!audio.src) { 262 // Nothing fetched yet (pre-seed showed metadata only, or a load is still 263 // in flight). Kick off the blob load for the current track and autoplay. 264 if (queue[currentIndex]) loadTrack(currentIndex, true); 265 return; 266 } 219 267 const p = audio.play(); 220 268 if (p && typeof p.catch === 'function') { … … 236 284 function next() { 237 285 if (!queue.length) return; 238 loadTrack((currentIndex + 1) % queue.length); 239 play(); 286 loadTrack((currentIndex + 1) % queue.length, true); 240 287 } 241 288 function prev() { 242 289 if (!queue.length) return; 243 loadTrack(currentIndex === 0 ? queue.length - 1 : currentIndex - 1); 244 play(); 290 loadTrack(currentIndex === 0 ? queue.length - 1 : currentIndex - 1, true); 245 291 } 246 292 function close() { … … 251 297 queue = []; 252 298 albumName = ''; 299 loadSeq++; // cancel any in-flight load 300 if (currentObjectUrl) { try { URL.revokeObjectURL(currentObjectUrl); } catch (e) {} } 301 currentObjectUrl = null; 302 try { audio.removeAttribute('src'); audio.load(); } catch (e) {} 253 303 } 254 304 … … 268 318 const idx = parseInt(li.dataset.idx, 10); 269 319 if (!isNaN(idx) && idx !== currentIndex) { 270 loadTrack(idx); 271 play(); 320 loadTrack(idx, true); 272 321 } 273 322 }); … … 288 337 audio.addEventListener('play', () => { 289 338 isPlaying = true; 290 consecutiveErrors = 0; // reset bij succesvolle play291 339 root.classList.add('is-playing'); 292 340 root.classList.remove('audio-needs-tap'); // verstop tap-hint 293 341 }); 342 // Reset de error-teller pas bij ECHTE playback-start (`playing`), niet bij 343 // het eager `play`-event. `play` vuurt vóór een eventuele netwerk-/decode- 344 // fout, dus resetten daar zou de 3-strikes-stop nooit laten triggeren bij 345 // een kapotte track → infinite "next"-loop. `playing` vuurt alleen als er 346 // daadwerkelijk audio speelt. 347 audio.addEventListener('playing', () => { consecutiveErrors = 0; }); 294 348 audio.addEventListener('pause', () => { isPlaying = false; root.classList.remove('is-playing'); }); 295 349 audio.addEventListener('ended', next); … … 539 593 cover: t.cover_url || t.cover || '', 540 594 })); 541 if (queue.length) loadTrack(0 );595 if (queue.length) loadTrack(0, false, true); // metadata only — fetch on first play 542 596 } 543 597 })();
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)