From 67e7161d65b82df2e21153f7947282750326ba98 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 2 Aug 2026 09:49:06 +0200 Subject: [PATCH] fix(playback): release the radio override when a new selection mounts no player MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex round-4 P2: after radio installed its credentials, selecting an item that never mounts a player surface (external video playback, failed resolution) left the old Cookie/Authorization installed — no WebPlayerViewComponent, close, or destroy cleanup runs on that path. Both radio hosts (unified collection tab and the Stalker live layout, which has the identical hole) now release the previously owned radio scope at the start of every new selection; the slot-ownership semantics keep this a no-op when another playback already owns the override. Regression test in the live-layout spec pins the failed-selection path. Co-Authored-By: Claude Fable 5 --- .../unified-live-tab.component.ts | 6 ++++ ...alker-live-stream-layout.component.spec.ts | 29 +++++++++++++++++++ .../stalker-live-stream-layout.component.ts | 6 ++++ 3 files changed, 41 insertions(+) diff --git a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.ts b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.ts index a84606938..677c90f73 100644 --- a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.ts +++ b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.ts @@ -591,6 +591,12 @@ export class UnifiedLiveTabComponent { this.activeDetail.set(null); this.activeTimeshift.set(null); this.isSelecting.set(true); + // A previously owned radio override must not survive into a + // selection that never mounts a player surface of its own — external + // video playback and failed resolutions would otherwise keep the old + // radio credentials installed for that origin. + this.streamHeaders.clear(this.radioHeaderScopeUrl); + this.radioHeaderScopeUrl = null; try { const detail = diff --git a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts index 259726302..5f53d3eb7 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts @@ -1214,6 +1214,35 @@ describe('StalkerLiveStreamLayoutComponent', () => { ); expect(component.activePlayback()).toBeNull(); }); + + it('releases the radio override when the next selection never mounts a player', async () => { + // Switching from a playing radio to a channel whose resolution fails + // (or plays externally) mounts no player surface — the selection + // itself must release the previously installed credentials. + stalkerStore.selectedContentType.set('radio'); + selectedCategoryId.set('radio-all'); + selectedItem.set(null); + selectedItvId.set(undefined); + fixture.detectChanges(); + resolveRadioPlayback.mockResolvedValue({ + streamUrl: 'http://portal.example/radio_2.mpg', + title: 'Portal FM', + headers: { Cookie: 'mac=00:1A:79:00:00:01' }, + }); + await component.playChannel(radioChannels()[0]); + await fixture.whenStable(); + + stalkerStore.selectedContentType.set('itv'); + resolveItvPlayback.mockRejectedValueOnce(new Error('portal down')); + await component.playChannel(itvChannels()[0]); + await fixture.whenStable(); + + expect(window.electron?.setUserAgent).toHaveBeenLastCalledWith( + undefined, + undefined, + 'http://portal.example/radio_2.mpg' + ); + }); }); function buildProgram(channelId: string, title: string): EpgProgram { diff --git a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts index a2f0c470f..56af7e4da 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts @@ -582,6 +582,12 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { const channelId = normalizeStalkerEntityId(item.id); this.stalkerStore.setSelectedItem(item); this.ensureChannelWithinRenderWindow(channelId); + // A previously owned radio override must not survive into a + // selection that never mounts a player surface of its own — external + // video playback and failed resolutions would otherwise keep the old + // radio credentials installed for that origin. + this.streamHeaders.clear(this.radioHeaderScopeUrl); + this.radioHeaderScopeUrl = null; try { const isRadioMode = this.isRadioMode();