diff --git a/apps/electron-backend/src/app/events/embedded-mpv.events.spec.ts b/apps/electron-backend/src/app/events/embedded-mpv.events.spec.ts index 7d23e105c..e9d235011 100644 --- a/apps/electron-backend/src/app/events/embedded-mpv.events.spec.ts +++ b/apps/electron-backend/src/app/events/embedded-mpv.events.spec.ts @@ -6,7 +6,9 @@ jest.mock('electron', () => ({ const mockEmbeddedMpvService = { createSession: jest.fn(), + prepareAddon: jest.fn(), getSupport: jest.fn(), + willProbeLinuxMpvExecutable: jest.fn(() => false), setPaused: jest.fn(), }; const mockSessionOptions = { @@ -29,6 +31,7 @@ jest.mock('../startup/login-shell-path', () => ({ import { ipcMain } from 'electron'; import { EMBEDDED_MPV_CREATE_SESSION, + EMBEDDED_MPV_PREPARE, EMBEDDED_MPV_SET_PAUSED, EMBEDDED_MPV_SUPPORT, } from '@iptvnator/shared/interfaces'; @@ -60,45 +63,55 @@ describe('EmbeddedMpvEvents IPC handlers', () => { }); describe('support checks and the login shell PATH', () => { - const originalPlatform = process.platform; - afterEach(() => { - Object.defineProperty(process, 'platform', { - value: originalPlatform, - }); mockWaitForLoginShellPath.mockClear(); - }); - - it('on Linux, checks support only after the lookup settled', async () => { - Object.defineProperty(process, 'platform', { value: 'linux' }); - let settle: () => void = () => undefined; - mockWaitForLoginShellPath.mockReturnValueOnce( - new Promise((resolve) => { - settle = resolve; - }) + mockEmbeddedMpvService.willProbeLinuxMpvExecutable.mockReset(); + mockEmbeddedMpvService.willProbeLinuxMpvExecutable.mockReturnValue( + false ); - mockEmbeddedMpvService.getSupport.mockReturnValue({ - supported: true, - }); - - const support = getIpcMainHandler(EMBEDDED_MPV_SUPPORT)({}); - await new Promise((resolve) => setImmediate(resolve)); - // Linux caches the result of a bare-name `mpv --version`. - expect(mockEmbeddedMpvService.getSupport).not.toHaveBeenCalled(); - - settle(); - await expect(support).resolves.toEqual({ supported: true }); }); - it('elsewhere, and for session calls, never waits', async () => { - Object.defineProperty(process, 'platform', { value: 'darwin' }); + it.each([EMBEDDED_MPV_SUPPORT, EMBEDDED_MPV_PREPARE])( + '%s waits for the lookup before the bare-name mpv probe', + async (channel) => { + mockEmbeddedMpvService.willProbeLinuxMpvExecutable.mockReturnValue( + true + ); + let settle: () => void = () => undefined; + mockWaitForLoginShellPath.mockReturnValueOnce( + new Promise((resolve) => { + settle = resolve; + }) + ); + mockEmbeddedMpvService.getSupport.mockReturnValue({ + supported: true, + }); + mockEmbeddedMpvService.prepareAddon.mockReturnValue({ + supported: true, + }); + + const support = getIpcMainHandler(channel)({}); + await new Promise((resolve) => setImmediate(resolve)); + expect( + mockEmbeddedMpvService.getSupport + ).not.toHaveBeenCalled(); + expect( + mockEmbeddedMpvService.prepareAddon + ).not.toHaveBeenCalled(); + + settle(); + await expect(support).resolves.toEqual({ supported: true }); + } + ); + + it('does not wait when no probe runs, nor for session calls', async () => { mockEmbeddedMpvService.getSupport.mockReturnValue({ supported: true, + engine: 'frame-copy', }); mockEmbeddedMpvService.createSession.mockReturnValue({ id: 's' }); await getIpcMainHandler(EMBEDDED_MPV_SUPPORT)({}); - Object.defineProperty(process, 'platform', { value: 'linux' }); await getIpcMainHandler(EMBEDDED_MPV_CREATE_SESSION)( {}, { x: 0, y: 0, width: 1, height: 1 } diff --git a/apps/electron-backend/src/app/events/embedded-mpv.events.ts b/apps/electron-backend/src/app/events/embedded-mpv.events.ts index f40d20008..de4286ad7 100644 --- a/apps/electron-backend/src/app/events/embedded-mpv.events.ts +++ b/apps/electron-backend/src/app/events/embedded-mpv.events.ts @@ -66,23 +66,23 @@ function handleEmbeddedMpv( } /** - * On Linux, support (and prepare, which checks support first) runs - * `mpv --version` by bare name and caches the result, so it must see the - * login shell PATH. Nothing else here spawns by name. + * On Linux, support (and prepare, which checks support first) may run + * `mpv --version` by bare name and cache the result, so it must see the + * login shell PATH. Only that probe waits; nothing else here spawns by name. */ -async function afterLinuxLoginShellPath(check: () => T): Promise { - if (process.platform === 'linux') { +async function afterLoginShellPathIfProbing(check: () => T): Promise { + if (getService().willProbeLinuxMpvExecutable()) { await waitForLoginShellPath(); } return check(); } handleEmbeddedMpv(EMBEDDED_MPV_SUPPORT, () => - afterLinuxLoginShellPath(() => getService().getSupport()) + afterLoginShellPathIfProbing(() => getService().getSupport()) ); handleEmbeddedMpv(EMBEDDED_MPV_PREPARE, () => - afterLinuxLoginShellPath(() => getService().prepareAddon()) + afterLoginShellPathIfProbing(() => getService().prepareAddon()) ); handleEmbeddedMpv( diff --git a/apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts b/apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts index c4b43007d..7b98e7d57 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts @@ -375,6 +375,40 @@ describe('EmbeddedMpvNativeService power blocker', () => { expect(support.frameCopyAvailable).toBe(true); }); + it('predicts the bare-name mpv probe exactly when getSupport runs it', () => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + process.env.DISPLAY = ':0'; + delete process.env.WAYLAND_DISPLAY; + mockSpawnSync.mockClear(); + mockSpawnSync.mockReturnValue({ status: 0 }); + mockRuntimeUsable(); + + expect(service.willProbeLinuxMpvExecutable()).toBe(true); + service.getSupport(); + expect(mockSpawnSync).toHaveBeenCalledWith( + 'mpv', + ['--version'], + expect.anything() + ); + // Cached afterwards: no further probe, so no further wait. + expect(service.willProbeLinuxMpvExecutable()).toBe(false); + }); + + it('predicts no probe for the frame-copy engine or native Wayland', () => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + process.env.DISPLAY = ':0'; + process.env.WAYLAND_DISPLAY = 'wayland-0'; + mockSpawnSync.mockClear(); + mockRuntimeUsable(); + expect(service.willProbeLinuxMpvExecutable()).toBe(false); + + delete process.env.WAYLAND_DISPLAY; + process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY = '1'; + expect(service.willProbeLinuxMpvExecutable()).toBe(false); + service.getSupport(); + expect(mockSpawnSync).not.toHaveBeenCalled(); + }); + it('keeps frame-copy supported on Linux without a system mpv executable', () => { // The helper links libmpv itself; the mpv-on-PATH probe only // binds the native --wid engine. diff --git a/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts b/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts index b53e3be7d..dbccfd910 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts @@ -377,6 +377,22 @@ export class EmbeddedMpvNativeService { }; } + /** + * Whether `getSupport()` would now run the uncached Linux bare-name + * `mpv --version` probe, i.e. reach the native-engine checks: no + * frame-copy engine, a usable display server and the feature enabled. + * The IPC layer waits for the login shell PATH only then. + */ + willProbeLinuxMpvExecutable(): boolean { + return ( + process.platform === 'linux' && + this.cachedLinuxMpvExecutableReason === undefined && + !this.isUnsupportedLinuxDisplayServer() && + isEmbeddedMpvFeatureEnabled() && + !this.isFrameCopyEngineActive() + ); + } + getSupport(): EmbeddedMpvSupport { if (!SUPPORTED_EMBEDDED_MPV_PLATFORMS.has(process.platform)) { return {