fix(playback): keep the saved Embedded MPV player when the mpv check is inconclusive

On Linux native-view the support check runs `mpv --version` by bare name
and waits for the login shell PATH first. Since #1784 that lookup is
asynchronous with a 10 s budget; when it ran out, the check ran on the
inherited PATH and answered a plain `supported: false`. The settings store
took that as a verdict and persisted the default player over a saved
Embedded MPV selection. The main process probed again once the shell
answered, but nothing restored the setting.

`EmbeddedMpvSupport` now carries `inconclusive`. The native service sets it
on a missing mpv while its probe has only seen the inherited PATH; the IPC
handler declares that state before probing and registers the re-probe
before the check, so a throwing check cannot leave it stuck. Every other
answer stays final.

Consumers no longer settle on an inconclusive answer: the settings store
keeps the saved player, and the command palette and the settings search
probe again on their next use instead of caching it for the session.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-10-03 19:39:41 +02:00
1 parent 8e1cb05913
commit 417eaf5005
14 files changed
+545 -16

No files matched your search

@@ -0,0 +1,98 @@
import { Injector } from '@angular/core';
import { StorageMap } from '@ngx-pwa/local-storage';
import { of } from 'rxjs';
import {
EmbeddedMpvSupport,
Settings,
STORE_KEY,
VideoPlayer,
} from '@iptvnator/shared/interfaces';
import { EpgSourceSettingsService } from './epg-source-settings.service';
import { SettingsStore } from './settings-store.service';
/** What the main process answers when the Linux `mpv` probe finds nothing. */
const MPV_MISSING: EmbeddedMpvSupport = {
supported: false,
platform: 'linux',
reason: 'Embedded MPV on Linux requires the mpv executable on PATH.',
frameCopyAvailable: false,
frameCopyUnavailableReason: 'helper-probe-failed',
};
describe('SettingsStore saved Embedded MPV selection', () => {
const testWindow = window as unknown as {
electron?: { getEmbeddedMpvSupport: jest.Mock };
};
const originalElectron = testWindow.electron;
let storage: { get: jest.Mock; set: jest.Mock };
let injector: Injector;
/** Loads settings as on startup and lets the support check finish. */
async function startWithSavedEmbeddedMpv(
support: EmbeddedMpvSupport
): Promise<InstanceType<typeof SettingsStore>> {
testWindow.electron = {
getEmbeddedMpvSupport: jest.fn().mockResolvedValue(support),
};
const store = injector.get(SettingsStore);
await store.loadSettings();
await new Promise((resolve) => setTimeout(resolve));
expect(testWindow.electron.getEmbeddedMpvSupport).toHaveBeenCalled();
return store;
}
beforeEach(() => {
const saved: Partial<Settings> = { player: VideoPlayer.EmbeddedMpv };
storage = {
get: jest.fn(() => of(saved)),
set: jest.fn(() => of(undefined)),
};
injector = Injector.create({
providers: [
SettingsStore,
EpgSourceSettingsService,
{ provide: StorageMap, useValue: storage },
],
});
jest.spyOn(
injector.get(EpgSourceSettingsService),
'synchronize'
).mockResolvedValue(undefined);
});
afterEach(() => {
testWindow.electron = originalElectron;
});
it('keeps the saved player when the support check is inconclusive', async () => {
// A slow login shell: mpv was looked up before its PATH arrived.
const store = await startWithSavedEmbeddedMpv({
...MPV_MISSING,
inconclusive: true,
});
expect(store.player()).toBe(VideoPlayer.EmbeddedMpv);
expect(storage.set).not.toHaveBeenCalled();
});
it('falls back to the default player on a final unsupported answer', async () => {
const store = await startWithSavedEmbeddedMpv(MPV_MISSING);
expect(store.player()).toBe(VideoPlayer.VideoJs);
expect(storage.set).toHaveBeenCalledWith(
STORE_KEY.Settings,
expect.objectContaining({ player: VideoPlayer.VideoJs })
);
});
it('keeps the saved player when Embedded MPV is supported', async () => {
const store = await startWithSavedEmbeddedMpv({
supported: true,
platform: 'linux',
engine: 'native',
});
expect(store.player()).toBe(VideoPlayer.EmbeddedMpv);
expect(storage.set).not.toHaveBeenCalled();
});
});
@@ -338,14 +338,15 @@ export const SettingsStore = signalStore(
try {
const support =
await window.electron.getEmbeddedMpvSupport();
if (!support.supported) {
if (support.supported) {
scheduleEmbeddedMpvPrepare();
} else if (!support.inconclusive) {
// An inconclusive answer is no verdict on this
// machine: the saved player stays as it is.
await this.updateSettings({
player: DEFAULT_SETTINGS.player,
});
return;
}
scheduleEmbeddedMpvPrepare();
} catch (error) {
console.warn(
'Failed to verify embedded MPV support; reverting to the default inline player.',