perf(web): preload restored audio before playback
This commit is contained in:
1 parent
20abe20686
commit
3a097c15c8
2 files changed
+119
-12
No files matched your search
@@ -97,6 +97,7 @@ export class Player {
|
||||
private timers: ReturnType<typeof setInterval>[] = [];
|
||||
private waveFeedback: { track_id: string; outcome: WaveOutcome }[] = [];
|
||||
private waveLoading = false;
|
||||
private restoreVersion = 0;
|
||||
/** Реакция сохранена на сервере — списки на экране нужно перечитать. */
|
||||
onReaction: (() => void) | null = null;
|
||||
|
||||
@@ -159,6 +160,7 @@ export class Player {
|
||||
const playable = tracks.filter((t) => t.available && t.stream_url);
|
||||
const target = tracks[index];
|
||||
if (!target || !playable.length) return;
|
||||
this.restoreVersion++;
|
||||
this.finishAttempt('skip');
|
||||
this.leaveWave();
|
||||
this.context = context;
|
||||
@@ -171,6 +173,7 @@ export class Player {
|
||||
|
||||
/** Запустить волну (или перезапустить с новым контекстом). */
|
||||
async startWave(activity: string | null, mood: string | null, seed?: { type: 'artist' | 'album' | 'track'; id: string; label: string }) {
|
||||
this.restoreVersion++;
|
||||
this.finishAttempt('skip');
|
||||
this.leaveWave();
|
||||
const res = unwrap(
|
||||
@@ -275,7 +278,7 @@ export class Player {
|
||||
|
||||
play() {
|
||||
if (!this.queue.current) return;
|
||||
if (!this.audio.src) {
|
||||
if (!this.audio.src || !this.attempt) {
|
||||
void this.load(true, this.state.positionMs);
|
||||
return;
|
||||
}
|
||||
@@ -388,9 +391,12 @@ export class Player {
|
||||
private async load(autoplay: boolean, startMs = 0) {
|
||||
const track = this.queue.current;
|
||||
if (!track?.stream_url) return;
|
||||
this.update({ loading: true, error: null, positionMs: startMs, durationMs: track.duration_ms });
|
||||
this.audio.src = track.stream_url;
|
||||
if (startMs > 0) this.audio.currentTime = startMs / 1000;
|
||||
this.update({ loading: autoplay, error: null, positionMs: startMs, durationMs: track.duration_ms });
|
||||
// Keep the restored element's buffer when the user presses Play.
|
||||
if (this.audio.getAttribute('src') !== track.stream_url) this.audio.src = track.stream_url;
|
||||
if (this.audio.currentTime !== startMs / 1000) this.audio.currentTime = startMs / 1000;
|
||||
this.updateMediaMetadata(track);
|
||||
if (!autoplay) return;
|
||||
this.applyLoudness();
|
||||
this.attempt = new PlayAttempt(track, startMs);
|
||||
this.preloaded = null;
|
||||
@@ -401,7 +407,6 @@ export class Player {
|
||||
context_type: this.context?.type,
|
||||
context_id: this.context?.id,
|
||||
});
|
||||
this.updateMediaMetadata(track);
|
||||
this.saveServer();
|
||||
void this.refillWave();
|
||||
if (autoplay) {
|
||||
@@ -597,22 +602,28 @@ export class Player {
|
||||
if (a.ended) return;
|
||||
this.update({ playing: false });
|
||||
const t = this.queue.current;
|
||||
if (t) this.events.push({ type: 'pause', track_id: t.id, position_ms: this.state.positionMs });
|
||||
this.saveServer();
|
||||
if (t && this.attempt) {
|
||||
this.events.push({ type: 'pause', track_id: t.id, position_ms: this.state.positionMs });
|
||||
this.saveServer();
|
||||
}
|
||||
});
|
||||
a.addEventListener('waiting', () => {
|
||||
if (this.attempt) this.update({ loading: true });
|
||||
});
|
||||
a.addEventListener('waiting', () => this.update({ loading: true }));
|
||||
a.addEventListener('timeupdate', () => {
|
||||
const ms = Math.round(a.currentTime * 1000);
|
||||
this.attempt?.tick(ms);
|
||||
this.update({ positionMs: ms });
|
||||
this.updatePositionState();
|
||||
this.maybePreload(ms);
|
||||
this.maybeCrossfade(ms);
|
||||
if (this.state.playing) {
|
||||
this.maybePreload(ms);
|
||||
this.maybeCrossfade(ms);
|
||||
}
|
||||
if (this.state.playing && Date.now() - this.lastServerSave > SAVE_EVERY_MS) this.saveServer();
|
||||
this.saveLocal();
|
||||
});
|
||||
a.addEventListener('ended', () => {
|
||||
if (this.ignoreEnded || this.fading) return;
|
||||
if (this.ignoreEnded || this.fading || !this.attempt) return;
|
||||
this.finishAttempt('play_end');
|
||||
if (this.queue.next(true)) {
|
||||
this.syncQueue();
|
||||
@@ -629,11 +640,18 @@ export class Player {
|
||||
private async onError() {
|
||||
const track = this.queue.current;
|
||||
if (!track) return;
|
||||
// A speculative load must never start playback or skip the saved track.
|
||||
if (!this.attempt) {
|
||||
this.audio.removeAttribute('src');
|
||||
this.audio.load();
|
||||
return;
|
||||
}
|
||||
if (this.refreshedFor !== track.id) {
|
||||
this.refreshedFor = track.id;
|
||||
try {
|
||||
const res = unwrap(await this.session.client.POST('/api/v1/tracks/batch', { body: { ids: [track.id] } }));
|
||||
const fresh = res.items[0];
|
||||
if (this.queue.current !== track || !this.attempt) return;
|
||||
if (fresh?.stream_url) {
|
||||
Object.assign(track, fresh);
|
||||
void this.load(true, this.state.positionMs);
|
||||
@@ -775,6 +793,7 @@ export class Player {
|
||||
* если она новее и с другого устройства. Автозапуска нет — плеер на паузе в сохранённой позиции.
|
||||
*/
|
||||
async restore() {
|
||||
const version = ++this.restoreVersion;
|
||||
let local: Saved | null = null;
|
||||
try {
|
||||
const raw = localStorage.getItem(LOCAL_KEY);
|
||||
@@ -804,8 +823,9 @@ export class Player {
|
||||
} catch {
|
||||
// офлайн — остаёмся с локальной копией
|
||||
}
|
||||
if (!saved?.queue.length) return;
|
||||
if (version !== this.restoreVersion || !saved?.queue.length) return;
|
||||
const res = unwrap(await this.session.client.POST('/api/v1/tracks/batch', { body: { ids: saved.queue } }));
|
||||
if (version !== this.restoreVersion) return;
|
||||
const wanted = saved.queue[saved.index];
|
||||
const tracks = res.items.filter((t) => t.available && t.stream_url);
|
||||
if (!tracks.length) return;
|
||||
@@ -819,12 +839,20 @@ export class Player {
|
||||
this.syncQueue();
|
||||
this.update({ positionMs: pos, durationMs: tracks[idx]!.duration_ms, volume: saved.volume });
|
||||
this.updateMediaMetadata(tracks[idx]!);
|
||||
// Prepare at the saved position without creating a listening attempt.
|
||||
await this.load(false, pos);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.restoreVersion++;
|
||||
this.fading = false;
|
||||
this.finishAttempt('skip');
|
||||
this.audio.pause();
|
||||
this.audio.removeAttribute('src');
|
||||
this.audio.load();
|
||||
this.overlap.pause();
|
||||
this.overlap.removeAttribute('src');
|
||||
this.overlap.load();
|
||||
for (const t of this.timers) clearInterval(t);
|
||||
this.timers = [];
|
||||
void this.events.flush();
|
||||
|
||||
Reference in new issue
Block a user