chore(player): log preload load errors
This commit is contained in:
parent
f461aee942
commit
8a377d5332
3 changed files with 121 additions and 37 deletions
6
bun.lock
6
bun.lock
|
|
@ -10,7 +10,7 @@
|
|||
"@capacitor/core": "^8.2.0",
|
||||
"@capacitor/haptics": "^8.0.1",
|
||||
"@capacitor/ios": "^8.2.0",
|
||||
"@dantheman827/taglib-ts": "https://github.com/DanTheMan827/taglib-ts/archive/ebd0e369b706c127a280d4ad631977f8d12ff88f.tar.gz",
|
||||
"@dantheman827/taglib-ts": "^0.1.5",
|
||||
"@ffmpeg/core": "^0.12.10",
|
||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||
"@ffmpeg/util": "^0.12.2",
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
"overrides": {
|
||||
"serialize-javascript": "^7.0.3",
|
||||
"source-map": "^0.7.4",
|
||||
"sourcemap-codec": "npm:@jridgewell/sourcemap-codec@^1.4.14",
|
||||
"sourcemap-codec": "^1.4.14",
|
||||
},
|
||||
"packages": {
|
||||
"@apideck/better-ajv-errors": ["@apideck/better-ajv-errors@0.3.7", "", { "dependencies": { "jsonpointer": "^5.0.1", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-TajUJwGWbDwkCx/CZi7tRE8PVB7simCvKJfHUsSdvps+aTM/PDPP4gkLmKnc+x3CE//y9i/nj74GqdL/hwk7Iw=="],
|
||||
|
|
@ -300,7 +300,7 @@
|
|||
|
||||
"@csstools/selector-specificity": ["@csstools/selector-specificity@6.0.0", "", { "peerDependencies": { "postcss-selector-parser": "^7.1.1" } }, "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA=="],
|
||||
|
||||
"@dantheman827/taglib-ts": ["@dantheman827/taglib-ts@https://github.com/DanTheMan827/taglib-ts/archive/ebd0e369b706c127a280d4ad631977f8d12ff88f.tar.gz", {}, "sha512-QPl5eWhFqP716VKIX5t7x39eRswRHG+5RpQ9wW6cNbDh1QSa/gWpPyvwP55O+/qCV7VjLepZI1/XFoxMO8jK7w=="],
|
||||
"@dantheman827/taglib-ts": ["@dantheman827/taglib-ts@0.1.5", "", {}, "sha512-uOMzKccEE0AUsMZ6rTPSUB0ZqAvuRxih9ECn8N62lMNi0UsXQumuKN86F3CxNaF4y3LOkvtp45vplTjwEEUOlQ=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
|
||||
|
||||
|
|
|
|||
148
js/player.js
148
js/player.js
|
|
@ -47,6 +47,8 @@ export class Player {
|
|||
this.shuffleActive = false;
|
||||
this.repeatMode = REPEAT_MODE.OFF;
|
||||
this.preloadCache = new Map();
|
||||
this._pendingPreload = false;
|
||||
setInterval(this.checkPreloadConditions.bind(this), 2000);
|
||||
this.preloadAbortController = null;
|
||||
this.currentTrack = null;
|
||||
this.currentRgValues = null;
|
||||
|
|
@ -473,7 +475,27 @@ export class Player {
|
|||
this.quality = quality;
|
||||
}
|
||||
|
||||
async preloadNextTracks() {
|
||||
preloadNextTracks() {
|
||||
this._pendingPreload = true;
|
||||
}
|
||||
|
||||
async checkPreloadConditions() {
|
||||
if (!this._pendingPreload || !this.activeElement || this.activeElement.paused) return;
|
||||
|
||||
const currentTime = this.activeElement.currentTime || 0;
|
||||
const duration = this.activeElement.duration || 0;
|
||||
const timeRemaining = duration - currentTime;
|
||||
|
||||
// Preload if we are in last 30 seconds of song
|
||||
const shouldPreload = (duration > 0 && timeRemaining <= 30);
|
||||
|
||||
if (shouldPreload) {
|
||||
this._pendingPreload = false;
|
||||
void this._executePreloadNextTracks().catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
async _executePreloadNextTracks() {
|
||||
if (this.preloadAbortController) {
|
||||
this.preloadAbortController.abort();
|
||||
}
|
||||
|
|
@ -482,7 +504,8 @@ export class Player {
|
|||
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
|
||||
const tracksToPreload = [];
|
||||
|
||||
for (let i = 1; i <= 2; i++) {
|
||||
// Only preload the next 1 song to prevent data waste
|
||||
for (let i = 1; i <= 1; i++) {
|
||||
const nextIndex = this.currentQueueIndex + i;
|
||||
if (nextIndex < currentQueue.length) {
|
||||
tracksToPreload.push({ track: currentQueue[nextIndex], index: nextIndex });
|
||||
|
|
@ -502,13 +525,52 @@ export class Player {
|
|||
|
||||
if (this.preloadAbortController.signal.aborted) break;
|
||||
|
||||
// Also preload ReplayGain legacy metadata if the fast manifest endpoint failed to provide it
|
||||
if (track.type !== 'video' && !streamInfo.rgInfo) {
|
||||
try {
|
||||
const trackData = await this.api.getTrack(track.id, this.quality);
|
||||
if (trackData && trackData.info) {
|
||||
streamInfo.rgInfoFallback = {
|
||||
trackReplayGain: trackData.info.trackReplayGain,
|
||||
trackPeakAmplitude: trackData.info.trackPeakAmplitude,
|
||||
albumReplayGain: trackData.info.albumReplayGain,
|
||||
albumPeakAmplitude: trackData.info.albumPeakAmplitude,
|
||||
};
|
||||
}
|
||||
} catch(e) {} // Fail silently
|
||||
}
|
||||
|
||||
this.preloadCache.set(track.id, streamInfo);
|
||||
// Warm connection/cache
|
||||
// For Blob URLs (DASH), this head request is not needed and can cause errors.
|
||||
if (!streamInfo.url.startsWith('blob:')) {
|
||||
fetch(streamInfo.url, { method: 'HEAD', signal: this.preloadAbortController.signal }).catch(
|
||||
() => {}
|
||||
);
|
||||
const streamUrl = streamInfo.url;
|
||||
|
||||
// Warm connection and pre-fetch
|
||||
if (!streamUrl.startsWith('blob:')) {
|
||||
if (streamUrl.includes('.mpd') || streamUrl.includes('.m3u8')) {
|
||||
if (this.shakaInitialized && this.shakaPlayer && typeof this.shakaPlayer.preload === 'function') {
|
||||
try {
|
||||
const preloadManager = await this.shakaPlayer.preload(streamUrl);
|
||||
streamInfo.preloadManager = preloadManager;
|
||||
} catch (e) {
|
||||
// Ignore preload errors, will just load fresh
|
||||
}
|
||||
} else {
|
||||
fetch(streamUrl, { method: 'GET', signal: this.preloadAbortController.signal }).catch(
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// For static files (FLAC, MP3), standard fetch of the first ~5MB completely primes the cache.
|
||||
const preloader = new Audio();
|
||||
preloader.preload = 'auto';
|
||||
preloader.muted = true;
|
||||
preloader.src = streamUrl;
|
||||
streamInfo.preloader = preloader; // Hold reference
|
||||
|
||||
fetch(streamUrl, {
|
||||
headers: { Range: 'bytes=0-5242880' },
|
||||
signal: this.preloadAbortController.signal,
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
|
|
@ -720,9 +782,13 @@ export class Player {
|
|||
this.hls.destroy();
|
||||
this.hls = null;
|
||||
}
|
||||
|
||||
// Retain the initialized Shaka player if we are remaining on the same HTMLMediaElement
|
||||
if (this.shakaInitialized && this.shakaPlayer) {
|
||||
this.shakaPlayer.unload();
|
||||
this.shakaInitialized = false;
|
||||
if (this.shakaPlayer.getMediaElement() !== activeElement) {
|
||||
this.shakaPlayer.unload();
|
||||
this.shakaInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (inactiveElement) {
|
||||
|
|
@ -736,9 +802,13 @@ export class Player {
|
|||
}
|
||||
|
||||
if (activeElement) {
|
||||
activeElement.pause();
|
||||
activeElement.src = '';
|
||||
activeElement.removeAttribute('src');
|
||||
// Let Shaka overwrite the activeElement's decoder pipeline gracefully if we're carrying it over.
|
||||
// It manages its own buffering teardown implicitly when `load()` is executed.
|
||||
if (!this.shakaInitialized) {
|
||||
activeElement.pause();
|
||||
activeElement.src = '';
|
||||
activeElement.removeAttribute('src');
|
||||
}
|
||||
}
|
||||
|
||||
audioContextManager.changeSource(activeElement);
|
||||
|
|
@ -925,7 +995,17 @@ export class Player {
|
|||
await this.setupHlsVideo(activeElement, streamUrl, null);
|
||||
} else if (streamUrl.startsWith('blob:') || streamUrl.includes('.mpd')) {
|
||||
await this.shakaPlayer.attach(activeElement);
|
||||
await this.shakaPlayer.load(streamUrl);
|
||||
|
||||
const loadTarget = track.type == 'video' && this.preloadCache.has(track.id) ?
|
||||
(this.preloadCache.get(track.id).preloadManager || streamUrl) : streamUrl;
|
||||
|
||||
try {
|
||||
await this.shakaPlayer.load(loadTarget);
|
||||
} catch (e) {
|
||||
console.error("PreloadManager load Error:", e); if (loadTarget !== streamUrl) await this.shakaPlayer.load(streamUrl);
|
||||
else throw e;
|
||||
}
|
||||
|
||||
this.shakaInitialized = true;
|
||||
|
||||
const savedAdaptiveQuality = localStorage.getItem('adaptive-playback-quality') || 'auto';
|
||||
|
|
@ -938,9 +1018,6 @@ export class Player {
|
|||
|
||||
this.applyAudioEffects();
|
||||
|
||||
const canPlay = await this.waitForCanPlayOrTimeout(activeElement);
|
||||
if (!canPlay || this.playbackSequence !== currentSequence) return;
|
||||
|
||||
if (startTime > 0) {
|
||||
activeElement.currentTime = startTime;
|
||||
}
|
||||
|
|
@ -961,6 +1038,9 @@ export class Player {
|
|||
if (resolvedStreamInfo.rgInfo) {
|
||||
this.currentRgValues = resolvedStreamInfo.rgInfo;
|
||||
this.applyReplayGain();
|
||||
} else if (resolvedStreamInfo.rgInfoFallback) {
|
||||
this.currentRgValues = resolvedStreamInfo.rgInfoFallback;
|
||||
this.applyReplayGain();
|
||||
} else {
|
||||
// Fallback to legacy metadata if manifest lacked normalization data
|
||||
const trackData = await this.api.getTrack(track.id, this.quality).catch(() => null);
|
||||
|
|
@ -984,12 +1064,24 @@ export class Player {
|
|||
// Handle playback
|
||||
if (streamUrl && (streamUrl.startsWith('blob:') || streamUrl.includes('.mpd')) && !track.isLocal) {
|
||||
// It's likely a DASH manifest URL
|
||||
await this.shakaPlayer.attach(activeElement);
|
||||
if (startTime > 0) {
|
||||
await this.shakaPlayer.load(streamUrl, startTime);
|
||||
} else {
|
||||
await this.shakaPlayer.load(streamUrl);
|
||||
if (this.shakaPlayer.getMediaElement() !== activeElement) {
|
||||
await this.shakaPlayer.attach(activeElement);
|
||||
this.shakaInitialized = true;
|
||||
}
|
||||
|
||||
const loadTarget = resolvedStreamInfo.preloadManager || streamUrl;
|
||||
|
||||
try {
|
||||
if (startTime > 0) {
|
||||
await this.shakaPlayer.load(loadTarget, startTime);
|
||||
} else {
|
||||
await this.shakaPlayer.load(loadTarget);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("PreloadManager load Error:", e); if (loadTarget !== streamUrl) await this.shakaPlayer.load(streamUrl);
|
||||
else throw e;
|
||||
}
|
||||
|
||||
this.shakaInitialized = true;
|
||||
this.applyAudioEffects();
|
||||
|
||||
|
|
@ -998,18 +1090,14 @@ export class Player {
|
|||
|
||||
this.updateAdaptiveQualityBadge();
|
||||
|
||||
const canPlay = await this.waitForCanPlayOrTimeout(activeElement);
|
||||
if (!canPlay || this.playbackSequence !== currentSequence) return;
|
||||
// Instantly trigger playback rather than explicitly waiting for 'canplay'
|
||||
// which delays the event loop and natively adds gap/latency
|
||||
await this.safePlay(activeElement);
|
||||
} else {
|
||||
activeElement.src = streamUrl;
|
||||
this.applyAudioEffects();
|
||||
this.updateAdaptiveQualityBadge();
|
||||
|
||||
// Wait for audio to be ready before playing
|
||||
const canPlay = await this.waitForCanPlayOrTimeout(activeElement);
|
||||
if (!canPlay || this.playbackSequence !== currentSequence) return;
|
||||
|
||||
if (startTime > 0) {
|
||||
activeElement.currentTime = startTime;
|
||||
}
|
||||
|
|
@ -1046,10 +1134,6 @@ export class Player {
|
|||
}
|
||||
|
||||
console.error(`Could not play track: ${trackTitle}`, error);
|
||||
// Skip to next track on unexpected error
|
||||
if (recursiveCount < currentQueue.length) {
|
||||
setTimeout(() => this.playNext(recursiveCount + 1), 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
"vite-plugin-pwa": "^1.2.0"
|
||||
},
|
||||
"overrides": {
|
||||
"sourcemap-codec": "npm:@jridgewell/sourcemap-codec@^1.4.14",
|
||||
"sourcemap-codec": "^1.4.14",
|
||||
"source-map": "^0.7.4",
|
||||
"serialize-javascript": "^7.0.3"
|
||||
},
|
||||
|
|
@ -63,7 +63,7 @@
|
|||
"@capacitor/core": "^8.2.0",
|
||||
"@capacitor/haptics": "^8.0.1",
|
||||
"@capacitor/ios": "^8.2.0",
|
||||
"@dantheman827/taglib-ts": "https://github.com/DanTheMan827/taglib-ts/archive/ebd0e369b706c127a280d4ad631977f8d12ff88f.tar.gz",
|
||||
"@dantheman827/taglib-ts": "^0.1.5",
|
||||
"@ffmpeg/core": "^0.12.10",
|
||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||
"@ffmpeg/util": "^0.12.2",
|
||||
|
|
|
|||
Loading…
Reference in a new issue