fix(web): polish social UI and prevent doubled shared audio
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m20s
release / desktop (Linux и Windows в релиз) (push) Successful in 19m22s
ci / ts / ts (packages + web) (push) Successful in 3m9s
release / release (версия и заметки) (push) Successful in 10s
release / android (APK в релиз) (push) Successful in 4m7s
release / prune (старые файлы и прогоны) (push) Successful in 11s

This commit is contained in:
mrleo1nid committed 2026-10-23 19:54:10 +03:00
1 parent 6450027b88
commit cb22f15912
17 files changed
+405 -63

No files matched your search

+29 -7
View File
@@ -49,6 +49,7 @@ export class Player {
private get audio() { return this.engine.audio; }
private get overlap() { return this.engine.overlap; }
private fading = false;
private fadeVersion = 0;
private ignoreEnded = false;
private readonly queue = new PlayQueue();
private readonly events: EventBuffer;
@@ -106,7 +107,7 @@ export class Player {
if (!this.listening) {
this.listening = new ListeningCoordinator(this.session, this.activity.deviceId, {
read: this.getState,
pause: () => { this.restoreVersion++; this.leaveWave(); this.audio.pause(); },
pause: () => { this.restoreVersion++; this.leaveWave(); this.cancelCrossfade(); this.audio.pause(); },
apply: (room, position, current) => this.syncListening(room, position, current),
changed: listening => this.update({ listening }),
});
@@ -291,6 +292,7 @@ export class Player {
pause() {
if (this.listening?.locked && !this.applyingSync) return;
this.cancelCrossfade();
this.audio.pause();
}
@@ -332,6 +334,7 @@ export class Player {
const track = this.queue.current;
if (!track) return;
const to = Math.min(Math.max(ms, 0), track.duration_ms);
this.cancelCrossfade();
const from = Math.round(this.audio.currentTime * 1000);
this.attempt?.seek(to);
this.audio.currentTime = to / 1000;
@@ -398,6 +401,7 @@ export class Player {
// --- загрузка трека ---
private async load(autoplay: boolean, startMs = 0, current = () => true) {
this.cancelCrossfade();
const track = this.queue.current;
if (!track?.stream_url) return;
const version = this.restoreVersion;
@@ -484,10 +488,25 @@ export class Player {
this.engine.applyLoudness(user * normalizeGain(this.state.track?.features?.loudness_lufs, this.normalizeOn()) * mix);
}
/** A cancelled transition must not keep a second source audible or change the new queue. */
private cancelCrossfade() {
this.fadeVersion++;
const wasFading = this.fading;
this.fading = false;
this.ignoreEnded = false;
this.overlap.pause();
this.overlap.volume = 0;
if (this.overlap.hasAttribute('src')) {
this.overlap.removeAttribute('src');
this.overlap.load();
}
if (wasFading) this.applyLoudness();
}
/** В конце трека накладывает следующий на crossfade_s секунд. */
private maybeCrossfade(positionMs: number) {
const cf = this.crossfadeSeconds();
if (this.fading || cf <= 0 || this.state.repeat === 'one') return;
if (this.listening?.locked || this.fading || cf <= 0 || this.state.repeat === 'one') return;
const dur = this.audio.duration;
if (!Number.isFinite(dur)) return;
const left = dur * 1000 - positionMs;
@@ -495,15 +514,16 @@ export class Player {
const next = this.state.queue[this.state.index + 1];
if (!next?.stream_url || !next.available) return;
this.fading = true;
const version = ++this.fadeVersion;
this.offline?.keep([this.queue.current!.id, next.id]);
this.overlap.src = this.offline?.local(next).stream_url ?? next.stream_url;
this.overlap.volume = 0;
void this.overlap.play().catch(() => {
this.fading = false;
if (version === this.fadeVersion) this.cancelCrossfade();
});
const started = performance.now();
const step = () => {
if (!this.fading) return;
if (!this.fading || version !== this.fadeVersion) return;
const p = Math.min(1, (performance.now() - started) / (cf * 1000));
this.applyLoudness(1 - p);
this.overlap.volume = Math.min(1, (this.state.muted ? 0 : this.state.volume) * normalizeGain(next.features?.loudness_lufs, this.normalizeOn()) * p);
@@ -511,12 +531,12 @@ export class Player {
requestAnimationFrame(step);
return;
}
this.finishCrossfade(next);
this.finishCrossfade(next, version);
};
requestAnimationFrame(step);
}
private finishCrossfade(next: Track) {
private finishCrossfade(next: Track, version: number) {
const at = this.overlap.currentTime;
this.ignoreEnded = true;
this.audio.pause();
@@ -545,6 +565,7 @@ export class Player {
this.media.updateMediaMetadata(next);
void this.offline?.save(next).catch(() => undefined);
void this.audio.play().finally(() => {
if (version !== this.fadeVersion) return;
this.fading = false;
this.ignoreEnded = false;
});
@@ -690,6 +711,7 @@ export class Player {
this.activity = null;
this.restoreVersion++;
this.wavePolicy.activate(null);
this.fadeVersion++;
this.fading = false;
this.finishAttempt('skip');
this.engine.dispose();
@@ -708,7 +730,7 @@ export class Player {
this.listening?.beginAudio();
}
private async syncListening(room: ListeningRoom, position: number, current: () => boolean) {
if (!room.track_id) { this.audio.pause(); return; }
if (!room.track_id) { this.cancelCrossfade(); this.audio.pause(); return; }
if (this.remoteRoom !== room.id || this.state.track?.id !== room.track_id || this.state.queue.length !== 1 || this.state.wave) {
const track = unwrap(await this.session.client.GET('/api/v1/tracks/{id}', { params: { path: { id: room.track_id } } }));
if (!current()) return;