Changeset b5bae24 in Klonkt for src/assets/js/audio-player.js
- Timestamp:
- 06/14/2026 01:25:19 AM (3 months ago)
- Branches:
- main
- Children:
- 9b36f45
- Parents:
- c88783e
- File:
-
- 1 edited
-
src/assets/js/audio-player.js (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/js/audio-player.js
rc88783e rb5bae24 165 165 // earlier fetch resolves. Only the latest load may set audio.src. 166 166 let loadSeq = 0; 167 // Next-track prefetch. While the current track plays we download the *next* 168 // track's bytes into a held blob, so `ended` → next() can swap src instantly 169 // (no silent gap, and no long async window where the browser's autoplay 170 // activation can lapse and reject play()). At most one track ahead is held. 171 // Shape: { url, objUrl } — objUrl is null while the fetch is still in flight. 172 let preload = null; 167 173 168 174 // Hide initially … … 186 192 // Fetch the track bytes and hand back a blob: object URL. The X-Audio-Player 187 193 // 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); 194 // Retries a few times with backoff: a single transient network blip used to 195 // bump the error counter and SKIP the song (auto-advance past it). Now one 196 // hiccup just costs a retry, and we only give up after genuinely failing. 197 async function fetchAsObjectUrl(url, attempts) { 198 attempts = attempts || 1; 199 let lastErr; 200 for (let i = 0; i < attempts; i++) { 201 try { 202 const r = await fetch(url, { 203 credentials: 'same-origin', 204 headers: { 'X-Audio-Player': '1' }, 205 }); 206 if (!r.ok) throw new Error('HTTP ' + r.status); 207 const blob = await r.blob(); 208 return URL.createObjectURL(blob); 209 } catch (e) { 210 lastErr = e; 211 if (i < attempts - 1) { 212 await new Promise((res) => setTimeout(res, 350 * (i + 1))); 213 } 214 } 215 } 216 throw lastErr; 217 } 218 219 // Apply a ready blob URL to the <audio> element. Single source of truth for 220 // "swap the playing source": used by both the cached-preload path and the 221 // fresh-fetch path so there's one place that touches audio.src. 222 function applyBlob(objUrl, autoplay, mySeq) { 223 if (mySeq !== loadSeq) { try { URL.revokeObjectURL(objUrl); } catch (e) {} return; } // superseded 224 root.classList.remove('audio-loading'); 225 // Free the previously-playing track's blob — otherwise each track leaks a 226 // copy. Never the same handle as objUrl (createObjectURL is unique), so this 227 // can't revoke the source we're about to play. 228 if (currentObjectUrl && currentObjectUrl !== objUrl) { 229 try { URL.revokeObjectURL(currentObjectUrl); } catch (e) {} 230 } 231 currentObjectUrl = objUrl; 232 // Schone overgang: pause + load forceert reset van internal state na 233 // meerdere src-changes (voorkomt state-corruption van het audio-element). 234 try { audio.pause(); } catch (e) {} 235 audio.src = objUrl; 236 try { audio.load(); } catch (e) {} 237 if (autoplay) play(); 238 } 239 240 function onLoadError(err, mySeq) { 241 if (mySeq !== loadSeq) return; // superseded — ignore stale failure 242 root.classList.remove('audio-loading'); 243 console.error('[pcms-audio] track load failed after retries', err); 244 // Genuine failure (after retries): bump the counter and auto-skip, but stop 245 // after 3 in a row so a fully-broken queue can't loop "next" forever. 246 consecutiveErrors++; 247 if (consecutiveErrors < 3 && queue.length > 1) setTimeout(next, 400); 248 } 249 250 // Discard any held/in-flight preload and free its blob if resolved. 251 function dropPreload() { 252 if (preload && preload.objUrl) { try { URL.revokeObjectURL(preload.objUrl); } catch (e) {} } 253 preload = null; 254 } 255 256 // Prefetch the *next* track's bytes in the background. Idempotent: re-calling 257 // while the same track is already cached / in flight is a no-op. Called from 258 // the `playing` event so the network is otherwise idle. 259 function preloadNext() { 260 if (queue.length < 2) return; 261 const ni = (currentIndex + 1) % queue.length; 262 const t = queue[ni]; 263 if (!t || !t.url) return; 264 if (preload && preload.url === t.url) return; // already held or in flight 265 dropPreload(); // different track queued before → free it 266 const marker = { url: t.url, objUrl: null }; 267 preload = marker; 268 fetchAsObjectUrl(t.url, 2).then((obj) => { 269 // Only keep it if this is still the track we want next; otherwise free it. 270 if (preload === marker) { marker.objUrl = obj; } 271 else { try { URL.revokeObjectURL(obj); } catch (e) {} } 272 }).catch(() => { if (preload === marker) preload = null; }); 196 273 } 197 274 … … 227 304 228 305 const mySeq = ++loadSeq; 306 307 // Fast path: the bytes for this exact track were already prefetched while 308 // the previous track played → swap in instantly, no gap, no fetch window. 309 if (preload && preload.url === t.url && preload.objUrl) { 310 const obj = preload.objUrl; 311 preload = null; // ownership moves to applyBlob (becomes currentObjectUrl) 312 applyBlob(obj, autoplay, mySeq); 313 return; 314 } 315 316 // Not preloaded (or preload still in flight) → drop any stale preload and 317 // fetch fresh, retrying transient failures before giving up. 318 dropPreload(); 229 319 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 }); 320 fetchAsObjectUrl(t.url, 3) 321 .then((objUrl) => applyBlob(objUrl, autoplay, mySeq)) 322 .catch((err) => onLoadError(err, mySeq)); 251 323 } 252 324 … … 298 370 albumName = ''; 299 371 loadSeq++; // cancel any in-flight load 372 dropPreload(); // free any prefetched next-track blob 300 373 if (currentObjectUrl) { try { URL.revokeObjectURL(currentObjectUrl); } catch (e) {} } 301 374 currentObjectUrl = null; … … 345 418 // een kapotte track → infinite "next"-loop. `playing` vuurt alleen als er 346 419 // daadwerkelijk audio speelt. 347 audio.addEventListener('playing', () => { consecutiveErrors = 0; });420 audio.addEventListener('playing', () => { consecutiveErrors = 0; preloadNext(); }); 348 421 audio.addEventListener('pause', () => { isPlaying = false; root.classList.remove('is-playing'); }); 349 422 audio.addEventListener('ended', next);
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)