fix(playback): keep asking for Embedded MPV support while the answer is inconclusive

Keeping the saved player on an inconclusive answer left a mounted player
stuck on it: the session controller asked for support once, in its
constructor, and the session effect never starts while unsupported, so the
player did not recover after the login shell answered. The settings page
held its one answer the same way.

`watchEmbeddedMpvSupport()` asks again every 3 s until the answer is final
or the surface is destroyed. The player controller and the settings page
facade load support through it, so playback starts by itself and the
Embedded MPV option appears without reopening the page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-10-03 19:52:28 +02:00
1 parent 417eaf5005
commit d143fb85f5
8 files changed
+407 -33

No files matched your search

@@ -1,5 +1,6 @@
import { TestBed } from '@angular/core/testing';
import {
EMBEDDED_MPV_SUPPORT_RECHECK_MS,
EmbeddedMpvEngine,
EmbeddedMpvSession,
ResolvedPortalPlayback,
@@ -106,6 +107,58 @@ describe('EmbeddedMpvSessionController (lifecycle & support edges)', () => {
});
});
describe('an inconclusive support answer', () => {
// A slow login shell: mpv was looked up before its PATH arrived.
const inconclusive = {
supported: false,
platform: 'linux',
reason: 'mpv executable missing',
inconclusive: true,
};
it('is asked for again, so a mounted player recovers by itself', async () => {
const settled = createSupport('native');
electron.getEmbeddedMpvSupport
.mockResolvedValueOnce(inconclusive)
.mockResolvedValue(settled);
const controller = TestBed.inject(EmbeddedMpvSessionController);
await waitFor(
() => controller.support() !== null,
'the first support answer'
);
expect(controller.support()).toBe(inconclusive);
await jest.advanceTimersByTimeAsync(
EMBEDDED_MPV_SUPPORT_RECHECK_MS
);
expect(controller.support()).toBe(settled);
expect(electron.getEmbeddedMpvSupport).toHaveBeenCalledTimes(2);
// A final answer is kept.
await jest.advanceTimersByTimeAsync(
EMBEDDED_MPV_SUPPORT_RECHECK_MS * 3
);
expect(electron.getEmbeddedMpvSupport).toHaveBeenCalledTimes(2);
});
it('is no longer asked for once the player is gone', async () => {
electron.getEmbeddedMpvSupport.mockResolvedValue(inconclusive);
const controller = TestBed.inject(EmbeddedMpvSessionController);
await waitFor(
() => controller.support() !== null,
'the first support answer'
);
TestBed.resetTestingModule();
await jest.advanceTimersByTimeAsync(
EMBEDDED_MPV_SUPPORT_RECHECK_MS * 3
);
expect(electron.getEmbeddedMpvSupport).toHaveBeenCalledTimes(1);
});
});
it('preserves the constructor support probe after preparing a session', async () => {
const probedSupport = createSupport('native');
const preparedSupport = createSupport('frame-copy');
@@ -15,6 +15,7 @@ import {
EmbeddedMpvSupport,
RecordingStartMetadata,
ResolvedPortalPlayback,
watchEmbeddedMpvSupport,
} from '@iptvnator/shared/interfaces';
import { EmbeddedMpvCommandRunner } from './embedded-mpv-command-runner';
import { measureBounds } from './embedded-mpv-format.utils';
@@ -95,8 +96,9 @@ export class EmbeddedMpvSessionController {
this.session.set(session);
});
let stopSupportWatch: (() => void) | undefined;
if (typeof window.electron?.getEmbeddedMpvSupport === 'function') {
void this.loadSupport();
stopSupportWatch = this.watchSupport();
} else {
this.support.set({
supported: false,
@@ -114,6 +116,7 @@ export class EmbeddedMpvSessionController {
this.destroyRef.onDestroy(() => {
this.unsubscribeSessionUpdate?.();
stopSupportWatch?.();
this.stalledTracker.cancel();
if (this.boundsAnimationFrame !== null) {
cancelAnimationFrame(this.boundsAnimationFrame);
@@ -381,22 +384,31 @@ export class EmbeddedMpvSessionController {
EmbeddedMpvSession['recording'] | null
> => this.commands.stopRecording();
private async loadSupport(): Promise<void> {
try {
const electron = this.getElectronBridge();
if (!electron?.getEmbeddedMpvSupport) {
throw new Error(
'Embedded MPV requires the Electron desktop build.'
);
}
this.support.set(await electron.getEmbeddedMpvSupport());
} catch (error) {
this.support.set({
supported: false,
platform: window.electron?.platform ?? 'unknown',
reason: error instanceof Error ? error.message : String(error),
});
}
/**
* Loads support and keeps asking while the answer is inconclusive: the
* session effect starts playback as soon as `support` turns supported,
* so a player mounted during that window recovers by itself.
*/
private watchSupport(): () => void {
return watchEmbeddedMpvSupport(
() => {
const electron = this.getElectronBridge();
if (!electron?.getEmbeddedMpvSupport) {
throw new Error(
'Embedded MPV requires the Electron desktop build.'
);
}
return electron.getEmbeddedMpvSupport();
},
(support) => this.support.set(support),
(error) =>
this.support.set({
supported: false,
platform: window.electron?.platform ?? 'unknown',
reason:
error instanceof Error ? error.message : String(error),
})
);
}
private getElectronBridge(): ElectronBridge | undefined {