fix double-click external playback handling

This commit is contained in:
4gray committed 2026-05-08 10:59:52 +02:00
1 parent 626948f2f5
commit e350a2ea96
10 files changed
+184 -27

No files matched your search

@@ -0,0 +1,20 @@
import { ChannelActions } from 'm3u-state';
import { Channel } from 'shared-interfaces';
import { createM3uChannelPlaybackRequest } from './m3u-channel-playback-actions';
describe('createM3uChannelPlaybackRequest', () => {
it('marks channel activation as an explicit playback request', () => {
const channel = {
id: 'channel-1',
name: 'Sample TV',
url: 'http://localhost/live.m3u8',
} as Channel;
expect(createM3uChannelPlaybackRequest(channel)).toEqual(
ChannelActions.setActiveChannel({
channel,
startPlayback: true,
})
);
});
});
@@ -0,0 +1,9 @@
import { ChannelActions } from 'm3u-state';
import { Channel } from 'shared-interfaces';
export function createM3uChannelPlaybackRequest(channel: Channel) {
return ChannelActions.setActiveChannel({
channel,
startPlayback: true,
});
}
@@ -495,14 +495,44 @@ describe('VideoPlayerComponent', () => {
expect(overlayRef.attach).toHaveBeenCalledTimes(1);
});
it('switches channels by number through the store action', () => {
it('switches channels by number through a playback request', () => {
syncStoreState(sampleChannel);
fixture.detectChanges();
component.switchToChannelByNumber(1);
expect(storeMock.dispatch).toHaveBeenCalledWith(
ChannelActions.setActiveChannel({ channel: sampleChannel })
ChannelActions.setActiveChannel({
channel: sampleChannel,
startPlayback: true,
})
);
});
it('changes channels from remote navigation through a playback request', () => {
const nextChannel = {
...sampleChannel,
id: 'channel-2',
url: 'http://localhost/next.m3u8',
name: 'Next TV',
};
activeChannel.set(sampleChannel);
activeChannel$.next(sampleChannel);
channels.set([sampleChannel, nextChannel]);
channels$.next([sampleChannel, nextChannel]);
fixture.detectChanges();
(
component as unknown as {
handleRemoteChannelChange(direction: 'up' | 'down'): void;
}
).handleRemoteChannelChange('down');
expect(storeMock.dispatch).toHaveBeenCalledWith(
ChannelActions.setActiveChannel({
channel: nextChannel,
startPlayback: true,
})
);
});
});
@@ -88,6 +88,7 @@ import {
Settings,
VideoPlayer,
} from 'shared-interfaces';
import { createM3uChannelPlaybackRequest } from './m3u-channel-playback-actions';
const M3U_MULTI_EPG_HEADER_ACTION_ID = 'm3u-multi-epg';
const M3U_SIDEBAR_STORAGE_KEY = 'm3u-sidebar-width';
@@ -459,9 +460,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
}
this.store.dispatch(
ChannelActions.setActiveChannel({
channel: nextChannel,
})
createM3uChannelPlaybackRequest(nextChannel)
);
},
error: (err) => {
@@ -739,7 +738,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
.subscribe((channel) => {
if (channel) {
this.store.dispatch(
ChannelActions.setActiveChannel({ channel })
createM3uChannelPlaybackRequest(channel)
);
}
});
@@ -403,6 +403,17 @@ describe('StalkerLiveStreamLayoutComponent', () => {
);
});
it('does not attach a dangling finally cleanup to failed playback resolution', async () => {
const playbackError = new Error('nothing_to_play');
const playbackPromise = Promise.reject(playbackError);
const finallySpy = jest.spyOn(playbackPromise, 'finally');
resolveItvPlayback.mockReturnValue(playbackPromise);
await component.playChannel(itvChannels()[0]);
expect(finallySpy).not.toHaveBeenCalled();
});
it('starts external playback from remote channel navigation when double-click opening is enabled', async () => {
settingsStore.openStreamOnDoubleClick.set(true);
portalPlayer.isEmbeddedPlayer.mockReturnValue(false);
@@ -404,11 +404,13 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy {
const promise = this.stalkerStore.resolveItvPlayback(item);
this.playbackResolution = { channelId, promise };
void promise.finally(() => {
const cleanup = () => {
if (this.playbackResolution?.promise === promise) {
this.playbackResolution = null;
}
});
};
void promise.then(cleanup, cleanup);
return promise;
}