fix(playback): clarify external player launch feedback (#1388)

This commit is contained in:
4gray authored and GitHub committed 2026-08-09 13:33:07 +02:00
1 parent 8442747c37
commit d73acd6bfc
101 files changed
+8819 -850

No files matched your search

@@ -2,9 +2,7 @@ import { ExternalPlayerSession } from '@iptvnator/shared/interfaces';
import { ExternalPlaybackService } from './external-playback.service';
describe('ExternalPlaybackService', () => {
let listener:
| ((session: ExternalPlayerSession) => void)
| undefined;
let listener: ((session: ExternalPlayerSession) => void) | undefined;
let closeExternalPlayerSession: jest.Mock;
let service: ExternalPlaybackService;
@@ -94,7 +92,9 @@ describe('ExternalPlaybackService', () => {
contentType: 'vod',
})
).toBeNull();
expect(service.visibleSession()).toBeNull();
expect(service.visibleSession()).toEqual(
expect.objectContaining({ status: 'error' })
);
});
it('delegates close requests for closable sessions', async () => {
@@ -133,18 +133,48 @@ describe('ExternalPlaybackService', () => {
expect(closeExternalPlayerSession).toHaveBeenCalledWith('session-2');
});
it('hides terminal sessions from the dock', () => {
it('hides closed sessions but keeps errors visible until dismissed', () => {
listener?.(createSession({ status: 'closed', canClose: false }));
expect(service.visibleSession()).toBeNull();
listener?.(
createSession({
id: 'session-3',
status: 'error',
error: 'Launch failed',
canClose: false,
})
);
const failed = createSession({
id: 'session-3',
status: 'error',
error: 'Launch failed',
canClose: false,
});
listener?.(createSession({ id: failed.id }));
listener?.(failed);
expect(service.visibleSession()).toEqual(failed);
service.dismissActiveSession();
expect(service.visibleSession()).toBeNull();
});
it('adopts an exactly correlated restored session after replacement teardown fails', () => {
const previous = createSession({
id: 'session-previous',
status: 'opened',
title: 'Previous stream',
});
const replacement = createSession({
id: 'session-replacement',
title: 'Replacement stream',
});
listener?.(previous);
listener?.(replacement);
listener?.({
...previous,
restoredFromSessionId: replacement.id,
updatedAt: '2026-03-07T10:00:20.000Z',
});
expect(service.activeSession()).toMatchObject({
id: previous.id,
status: 'opened',
});
expect(service.visibleSession()?.id).toBe(previous.id);
});
});
@@ -1,5 +1,8 @@
import { computed, Injectable, signal } from '@angular/core';
import { ExternalPlayerSession, PlayerContentInfo } from '@iptvnator/shared/interfaces';
import {
ExternalPlayerSession,
PlayerContentInfo,
} from '@iptvnator/shared/interfaces';
@Injectable({
providedIn: 'root',
@@ -10,11 +13,7 @@ export class ExternalPlaybackService {
readonly visibleSession = computed(() => {
const session = this.activeSession();
if (
!session ||
session.status === 'closed' ||
session.status === 'error'
) {
if (!session || session.status === 'closed') {
return null;
}
@@ -60,9 +59,8 @@ export class ExternalPlaybackService {
this.dismissedSessionId.set(session.id);
try {
const updatedSession = await window.electron.closeExternalPlayerSession(
session.id
);
const updatedSession =
await window.electron.closeExternalPlayerSession(session.id);
if (updatedSession) {
this.handleSessionUpdate(updatedSession);
return;
@@ -105,12 +103,19 @@ export class ExternalPlaybackService {
private handleSessionUpdate(session: ExternalPlayerSession): void {
const current = this.activeSession();
const restoresCurrentReplacement =
current?.id === session.restoredFromSessionId;
if (!current || current.id === session.id || session.status === 'launching') {
if (
!current ||
current.id === session.id ||
session.status === 'launching' ||
restoresCurrentReplacement
) {
this.activeSession.set(session);
}
if (session.status === 'launching') {
if (session.status === 'launching' || restoresCurrentReplacement) {
this.dismissedSessionId.set(null);
}
}