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/core": "^8.2.0",
|
||||||
"@capacitor/haptics": "^8.0.1",
|
"@capacitor/haptics": "^8.0.1",
|
||||||
"@capacitor/ios": "^8.2.0",
|
"@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/core": "^0.12.10",
|
||||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||||
"@ffmpeg/util": "^0.12.2",
|
"@ffmpeg/util": "^0.12.2",
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"serialize-javascript": "^7.0.3",
|
"serialize-javascript": "^7.0.3",
|
||||||
"source-map": "^0.7.4",
|
"source-map": "^0.7.4",
|
||||||
"sourcemap-codec": "npm:@jridgewell/sourcemap-codec@^1.4.14",
|
"sourcemap-codec": "^1.4.14",
|
||||||
},
|
},
|
||||||
"packages": {
|
"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=="],
|
"@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=="],
|
"@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=="],
|
"@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.shuffleActive = false;
|
||||||
this.repeatMode = REPEAT_MODE.OFF;
|
this.repeatMode = REPEAT_MODE.OFF;
|
||||||
this.preloadCache = new Map();
|
this.preloadCache = new Map();
|
||||||
|
this._pendingPreload = false;
|
||||||
|
setInterval(this.checkPreloadConditions.bind(this), 2000);
|
||||||
this.preloadAbortController = null;
|
this.preloadAbortController = null;
|
||||||
this.currentTrack = null;
|
this.currentTrack = null;
|
||||||
this.currentRgValues = null;
|
this.currentRgValues = null;
|
||||||
|
|
@ -473,7 +475,27 @@ export class Player {
|
||||||
this.quality = quality;
|
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) {
|
if (this.preloadAbortController) {
|
||||||
this.preloadAbortController.abort();
|
this.preloadAbortController.abort();
|
||||||
}
|
}
|
||||||
|
|
@ -482,7 +504,8 @@ export class Player {
|
||||||
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
|
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
|
||||||
const tracksToPreload = [];
|
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;
|
const nextIndex = this.currentQueueIndex + i;
|
||||||
if (nextIndex < currentQueue.length) {
|
if (nextIndex < currentQueue.length) {
|
||||||
tracksToPreload.push({ track: currentQueue[nextIndex], index: nextIndex });
|
tracksToPreload.push({ track: currentQueue[nextIndex], index: nextIndex });
|
||||||
|
|
@ -502,13 +525,52 @@ export class Player {
|
||||||
|
|
||||||
if (this.preloadAbortController.signal.aborted) break;
|
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);
|
this.preloadCache.set(track.id, streamInfo);
|
||||||
// Warm connection/cache
|
const streamUrl = streamInfo.url;
|
||||||
// For Blob URLs (DASH), this head request is not needed and can cause errors.
|
|
||||||
if (!streamInfo.url.startsWith('blob:')) {
|
// Warm connection and pre-fetch
|
||||||
fetch(streamInfo.url, { method: 'HEAD', signal: this.preloadAbortController.signal }).catch(
|
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) {
|
} catch (error) {
|
||||||
if (error.name !== 'AbortError') {
|
if (error.name !== 'AbortError') {
|
||||||
|
|
@ -720,9 +782,13 @@ export class Player {
|
||||||
this.hls.destroy();
|
this.hls.destroy();
|
||||||
this.hls = null;
|
this.hls = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Retain the initialized Shaka player if we are remaining on the same HTMLMediaElement
|
||||||
if (this.shakaInitialized && this.shakaPlayer) {
|
if (this.shakaInitialized && this.shakaPlayer) {
|
||||||
this.shakaPlayer.unload();
|
if (this.shakaPlayer.getMediaElement() !== activeElement) {
|
||||||
this.shakaInitialized = false;
|
this.shakaPlayer.unload();
|
||||||
|
this.shakaInitialized = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inactiveElement) {
|
if (inactiveElement) {
|
||||||
|
|
@ -736,9 +802,13 @@ export class Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
activeElement.pause();
|
// Let Shaka overwrite the activeElement's decoder pipeline gracefully if we're carrying it over.
|
||||||
activeElement.src = '';
|
// It manages its own buffering teardown implicitly when `load()` is executed.
|
||||||
activeElement.removeAttribute('src');
|
if (!this.shakaInitialized) {
|
||||||
|
activeElement.pause();
|
||||||
|
activeElement.src = '';
|
||||||
|
activeElement.removeAttribute('src');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
audioContextManager.changeSource(activeElement);
|
audioContextManager.changeSource(activeElement);
|
||||||
|
|
@ -925,7 +995,17 @@ export class Player {
|
||||||
await this.setupHlsVideo(activeElement, streamUrl, null);
|
await this.setupHlsVideo(activeElement, streamUrl, null);
|
||||||
} else if (streamUrl.startsWith('blob:') || streamUrl.includes('.mpd')) {
|
} else if (streamUrl.startsWith('blob:') || streamUrl.includes('.mpd')) {
|
||||||
await this.shakaPlayer.attach(activeElement);
|
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;
|
this.shakaInitialized = true;
|
||||||
|
|
||||||
const savedAdaptiveQuality = localStorage.getItem('adaptive-playback-quality') || 'auto';
|
const savedAdaptiveQuality = localStorage.getItem('adaptive-playback-quality') || 'auto';
|
||||||
|
|
@ -938,9 +1018,6 @@ export class Player {
|
||||||
|
|
||||||
this.applyAudioEffects();
|
this.applyAudioEffects();
|
||||||
|
|
||||||
const canPlay = await this.waitForCanPlayOrTimeout(activeElement);
|
|
||||||
if (!canPlay || this.playbackSequence !== currentSequence) return;
|
|
||||||
|
|
||||||
if (startTime > 0) {
|
if (startTime > 0) {
|
||||||
activeElement.currentTime = startTime;
|
activeElement.currentTime = startTime;
|
||||||
}
|
}
|
||||||
|
|
@ -961,6 +1038,9 @@ export class Player {
|
||||||
if (resolvedStreamInfo.rgInfo) {
|
if (resolvedStreamInfo.rgInfo) {
|
||||||
this.currentRgValues = resolvedStreamInfo.rgInfo;
|
this.currentRgValues = resolvedStreamInfo.rgInfo;
|
||||||
this.applyReplayGain();
|
this.applyReplayGain();
|
||||||
|
} else if (resolvedStreamInfo.rgInfoFallback) {
|
||||||
|
this.currentRgValues = resolvedStreamInfo.rgInfoFallback;
|
||||||
|
this.applyReplayGain();
|
||||||
} else {
|
} else {
|
||||||
// Fallback to legacy metadata if manifest lacked normalization data
|
// Fallback to legacy metadata if manifest lacked normalization data
|
||||||
const trackData = await this.api.getTrack(track.id, this.quality).catch(() => null);
|
const trackData = await this.api.getTrack(track.id, this.quality).catch(() => null);
|
||||||
|
|
@ -984,12 +1064,24 @@ export class Player {
|
||||||
// Handle playback
|
// Handle playback
|
||||||
if (streamUrl && (streamUrl.startsWith('blob:') || streamUrl.includes('.mpd')) && !track.isLocal) {
|
if (streamUrl && (streamUrl.startsWith('blob:') || streamUrl.includes('.mpd')) && !track.isLocal) {
|
||||||
// It's likely a DASH manifest URL
|
// It's likely a DASH manifest URL
|
||||||
await this.shakaPlayer.attach(activeElement);
|
if (this.shakaPlayer.getMediaElement() !== activeElement) {
|
||||||
if (startTime > 0) {
|
await this.shakaPlayer.attach(activeElement);
|
||||||
await this.shakaPlayer.load(streamUrl, startTime);
|
this.shakaInitialized = true;
|
||||||
} else {
|
|
||||||
await this.shakaPlayer.load(streamUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.shakaInitialized = true;
|
||||||
this.applyAudioEffects();
|
this.applyAudioEffects();
|
||||||
|
|
||||||
|
|
@ -998,18 +1090,14 @@ export class Player {
|
||||||
|
|
||||||
this.updateAdaptiveQualityBadge();
|
this.updateAdaptiveQualityBadge();
|
||||||
|
|
||||||
const canPlay = await this.waitForCanPlayOrTimeout(activeElement);
|
// Instantly trigger playback rather than explicitly waiting for 'canplay'
|
||||||
if (!canPlay || this.playbackSequence !== currentSequence) return;
|
// which delays the event loop and natively adds gap/latency
|
||||||
await this.safePlay(activeElement);
|
await this.safePlay(activeElement);
|
||||||
} else {
|
} else {
|
||||||
activeElement.src = streamUrl;
|
activeElement.src = streamUrl;
|
||||||
this.applyAudioEffects();
|
this.applyAudioEffects();
|
||||||
this.updateAdaptiveQualityBadge();
|
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) {
|
if (startTime > 0) {
|
||||||
activeElement.currentTime = startTime;
|
activeElement.currentTime = startTime;
|
||||||
}
|
}
|
||||||
|
|
@ -1046,10 +1134,6 @@ export class Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`Could not play track: ${trackTitle}`, error);
|
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"
|
"vite-plugin-pwa": "^1.2.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"sourcemap-codec": "npm:@jridgewell/sourcemap-codec@^1.4.14",
|
"sourcemap-codec": "^1.4.14",
|
||||||
"source-map": "^0.7.4",
|
"source-map": "^0.7.4",
|
||||||
"serialize-javascript": "^7.0.3"
|
"serialize-javascript": "^7.0.3"
|
||||||
},
|
},
|
||||||
|
|
@ -63,7 +63,7 @@
|
||||||
"@capacitor/core": "^8.2.0",
|
"@capacitor/core": "^8.2.0",
|
||||||
"@capacitor/haptics": "^8.0.1",
|
"@capacitor/haptics": "^8.0.1",
|
||||||
"@capacitor/ios": "^8.2.0",
|
"@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/core": "^0.12.10",
|
||||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||||
"@ffmpeg/util": "^0.12.2",
|
"@ffmpeg/util": "^0.12.2",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue