From 93523a1d3c32b48e29af19008adf2c480a029faa Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 3 Oct 2026 16:15:54 +0200 Subject: [PATCH] fix(portals): a failed MPV/VLC movie launch is reported to the viewer The launch settlement swallowed a rejected launch together with a superseded one, so the menu's launch had nothing to report. It now hands a failure that nothing superseded to the page, which logs it and shows the playback error. Co-Authored-By: Claude Fable 5.1 --- .../vod-details-external-launch.ts | 7 +++- .../vod-details-external-playback.spec.ts | 39 +++++++++++++++++++ .../vod-details-playback.service.ts | 4 ++ .../vod-details-route.component.ts | 8 ++++ 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-external-launch.ts b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-external-launch.ts index 0940db2aa..7575eee04 100644 --- a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-external-launch.ts +++ b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-external-launch.ts @@ -5,6 +5,8 @@ interface OwnedExternalLaunchOptions { owns: () => boolean; close: (session: ExternalPlayerSession) => Promise; warnCloseFailure: (error: unknown) => void; + /** The launch itself failed while nothing superseded it: the user's to hear about. */ + reportLaunchFailure?: (error: unknown) => void; clearPending: () => void; clearOwnership: () => void; } @@ -38,7 +40,10 @@ export async function settleOwnedExternalLaunch( options.clearPending(); options.clearOwnership(); return false; - } catch { + } catch (error) { + if (options.owns()) { + options.reportLaunchFailure?.(error); + } // A partial reuse can reject while Electron retains a matching // closable error session. Keep its credential-free identity so the // next source start can still find and close that exact process. diff --git a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-external-playback.spec.ts b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-external-playback.spec.ts index 574e03817..b4c3ace44 100644 --- a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-external-playback.spec.ts +++ b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-external-playback.spec.ts @@ -211,6 +211,45 @@ describe('VodDetailsPlaybackService — external playback handoff', () => { expect(openResolvedPlayback).toHaveBeenCalledTimes(1); }); + it('reports a failed launch to the page unless another start superseded it', async () => { + const reportExternalLaunchFailure = jest.fn(); + service.bind({ + vodId: routeVodId, + vodInfo: signal(null), + activeSource, + supersedePendingSwitch: jest.fn(), + reportExternalLaunchFailure, + }); + const failure = new Error('mpv is not installed'); + const playback = { + streamUrl: 'https://example.com/alt.mkv', + title: 'Example Movie', + contentInfo: { + playlistId: 'playlist-1', + contentXtreamId: 1, + contentType: 'vod' as const, + }, + }; + + openResolvedPlayback.mockRejectedValueOnce(failure); + await service.startResolvedPlayback(playback); + expect(reportExternalLaunchFailure).toHaveBeenCalledWith(failure); + + // The route moved on before the failure arrived: the error belongs + // to a launch nothing shows any more. + let shown = true; + let rejectLaunch: (error: Error) => void = () => undefined; + openResolvedPlayback.mockImplementationOnce( + () => new Promise((_, reject) => (rejectLaunch = reject)) + ); + const stale = service.startResolvedPlayback(playback, () => shown); + await new Promise((resolve) => setTimeout(resolve)); + shown = false; + rejectLaunch(failure); + await expect(stale).resolves.toBe(false); + expect(reportExternalLaunchFailure).toHaveBeenCalledTimes(1); + }); + it('closes a closable error before starting a replacement source', async () => { activeSession.set({ ...sessionFor(ROUTE_PLAYLIST, ROUTE_VOD_ID), diff --git a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-playback.service.ts b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-playback.service.ts index 49f806d41..a2becf9fa 100644 --- a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-playback.service.ts +++ b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-playback.service.ts @@ -57,6 +57,8 @@ export interface VodDetailsPlaybackBindings { activeSource?: Signal; /** Retires a source resolution that the accepted fallback now supersedes. */ supersedePendingSwitch: () => void; + /** An MPV/VLC launch failed while nothing superseded it; the page tells the user. */ + reportExternalLaunchFailure?: (error: unknown) => void; } /** @@ -532,6 +534,8 @@ export class VodDetailsPlaybackService { 'Closing a superseded external player failed.', error ), + reportLaunchFailure: (error) => + this.bindings()?.reportExternalLaunchFailure?.(error), clearPending: () => this.clearExternalLaunchPending(generation), clearOwnership: () => this.clearExternalLaunchOwnership(generation), }); diff --git a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.ts b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.ts index 4fbdb3128..5d1e2f0c8 100644 --- a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.ts +++ b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.ts @@ -420,6 +420,14 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy { activeSource: this.msUi.activeAlternativeSource, supersedePendingSwitch: () => this.multiSource.supersedePendingSwitch(), + reportExternalLaunchFailure: (error) => { + this.logger.error('External launch failed', error); + this.snackBar.open( + this.translateService.instant('PORTALS.PLAYBACK_ERROR'), + undefined, + { duration: 3000 } + ); + }, }); effect(() => {