diff --git a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts index e9001a79b..3c907b858 100644 --- a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts +++ b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts @@ -295,12 +295,18 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy { if (!session) { return; } - this.volume.set(session.volume); - this.timeUpdate.emit({ - currentTime: session.positionSeconds, - duration: session.durationSeconds ?? 0, + // Side effects must not pull in transitive signal deps via + // scheduleControlsHide — otherwise opening a popover, pausing, or + // hovering would re-run this body and re-emit timeUpdate (which + // could feed back into playback inputs and restart the stream). + untracked(() => { + this.volume.set(session.volume); + this.timeUpdate.emit({ + currentTime: session.positionSeconds, + duration: session.durationSeconds ?? 0, + }); + this.scheduleControlsHide(); }); - this.scheduleControlsHide(); }); } diff --git a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts index 9a9757099..0038f45c5 100644 --- a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts +++ b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts @@ -1,6 +1,7 @@ import { DestroyRef, Injectable, + computed, effect, inject, signal, @@ -26,6 +27,10 @@ export class EmbeddedMpvSessionController { readonly stalled = signal(false); readonly retryToken = signal(0); + private readonly sessionStatus = computed( + () => this.session()?.status ?? null + ); + private readonly destroyRef = inject(DestroyRef); private readonly unsubscribeSessionUpdate?: () => void; @@ -54,8 +59,11 @@ export class EmbeddedMpvSessionController { }); } + // Track the narrowest possible signal — status only — so this effect + // does not re-run on every position-poll snapshot (~2 Hz during play) + // even though handleStalledTracking would be a no-op for those. effect(() => { - const status = this.session()?.status ?? null; + const status = this.sessionStatus(); untracked(() => this.handleStalledTracking(status)); });