fix(playback): release the radio override when a new selection mounts no player

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 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-02 09:49:06 +02:00
1 parent 02969dca26
commit 67e7161d65
3 files changed
+41

No files matched your search

@@ -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 =
@@ -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 {
@@ -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();