Files
iptvnator/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged.e2e.ts
T
4gray 8fdac824fd feat(packaging): ship Linux embedded MPV frame-copy runtime (#1200)
* docs: design Linux frame-copy packaging

* docs: plan Linux frame-copy packaging

* feat(packaging): define Linux frame-copy profiles

* fix(packaging): reject inherited profile names

* feat(embedded-mpv): validate staged Linux runtime

* fix(embedded-mpv): require Linux source packages

* fix(embedded-mpv): harden Linux runtime staging

* feat(embedded-mpv): build LGPL Linux runtime

* fix(embedded-mpv): pin Linux runtime inputs

* feat(embedded-mpv): build relocatable Linux helper

* fix(embedded-mpv): require bundled Linux runtime

* fix(embedded-mpv): make Linux runtime portable

* feat(packaging): ship Linux frame-copy artifacts

* fix(embedded-mpv): verify Linux helper linkage

* fix(packaging): enforce Linux frame-copy isolation

* fix(embedded-mpv): pin Linux display data

* docs(embedded-mpv): document Linux frame-copy packaging

* feat(embedded-mpv): probe Linux frame-copy runtime

* test(embedded-mpv): smoke packaged Linux frame-copy

* docs(embedded-mpv): clarify Linux system runtime baseline

* fix(embedded-mpv): harden Linux runtime capability gate

* ci: verify Linux frame-copy packages

* test(embedded-mpv): harden packaged Linux smoke

* test(embedded-mpv): preserve packaged GL mode

* test(packaging): harden Linux package probes

* fix(embedded-mpv): enable private Snap shared memory

* fix(embedded-mpv): sanitize Linux helper environment

* fix(packaging): enforce private Snap memory semantics

* fix(packaging): reject ambiguous Snap memory metadata

* fix(embedded-mpv): prioritize trusted Snap GL

* fix(packaging): reject advanced Snap YAML semantics

* fix(packaging): reject arbitrary Snap YAML aliases

* feat(packaging): ship Linux runtime license notices

* docs(embedded-mpv): document Linux runtime distribution

* fix(packaging): parse Snap trailing comments safely

* fix(release): gate Snap publish on public source release

* fix(packaging): strip VCS metadata from source bundle

* docs(packaging): clarify Linux source release gate

* test(embedded-mpv): smoke missing bundled libmpv

* style(embedded-mpv): format final validation inputs

* fix(e2e): satisfy fixture index signature typing

* fix(ci): declare fontconfig gperf generator

* fix(embedded-mpv): hash runtime cache identities

* fix(packaging): harden Linux frame-copy delivery

* fix(packaging): tighten runtime delivery gates

* fix(ci): decouple Linux runtime matrix

* fix(packaging): harden Linux frame-copy delivery

* fix(packaging): validate Linux frame-copy runtimes

* fix(packaging): scope Snap Electron library checks

* feat(packaging): ship Linux frame-copy runtimes

* fix(packaging): improve Linux runtime smoke diagnostics

* fix(packaging): expose bounded helper probe details

* test(packaging): trace Snap EGL probe failures

* fix(packaging): prefer core22 ABI in Snap helper

* fix(packaging): bound helper probe capture

* fix(packaging): harden Linux frame-copy releases

* fix(packaging): canonicalize libplacebo submodule identity

* fix(packaging): make source archive inspection portable

* fix(packaging): harden Snap release verification
2026-07-18 17:28:22 +02:00

312 lines
12 KiB
TypeScript

import {
expect,
launchPackagedElectronApp,
resolvePackagedLinuxExecutable,
test,
type LaunchedElectronApp,
} from './electron-test-fixtures';
import { join } from 'path';
import {
assertNativeFallbackPrerequisites,
cleanupPackagedFrameCopySmoke,
closeAndWaitForExit,
createLocalMediaServer,
getEmbeddedMpvSupport,
getLatestSession,
installEmbeddedMpvSessionCapture,
installFrameCanvasAndSessionCapture,
isMeaningfulNativePlaybackSnapshot,
renderedFrameSignal,
type LocalMediaServer,
} from './embedded-mpv-frame-copy-packaged-fixtures';
import {
createDisposablePackagedLinuxApp,
createPackagedEntryGuard,
readPackagedRuntimeIdentity,
type DisposablePackagedLinuxApp,
type PackagedEntryGuard,
} from './embedded-mpv-frame-copy-packaged-filesystem';
const PACKAGED_FRAME_COPY_REQUIRED_ENV =
'IPTVNATOR_E2E_REQUIRE_PACKAGED_FRAME_COPY';
const FRAME_COPY_OPT_IN_ENV = 'IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY';
const packagedExecutable = resolvePackagedLinuxExecutable();
const packagedFrameCopyRequired = isTruthy(
process.env[PACKAGED_FRAME_COPY_REQUIRED_ENV]
);
function isTruthy(value: string | undefined): boolean {
return ['1', 'true', 'yes', 'on'].includes(
(value ?? '').trim().toLowerCase()
);
}
// This smoke drives the public preload API directly so it exercises the real
// packaged main process, helper, frame reader, WebGL pump, and pause controls
// without coupling runtime validation to playlist import/settings UI state.
test.describe('Packaged Linux embedded MPV frame-copy runtime', () => {
test.skip(
process.platform !== 'linux',
'The packaged frame-copy runtime is Linux-only.'
);
test.skip(
!packagedExecutable && !packagedFrameCopyRequired,
`Set IPTVNATOR_E2E_PACKAGED_EXECUTABLE or ${PACKAGED_FRAME_COPY_REQUIRED_ENV}=1 in the dedicated packaged-runtime job.`
);
test('@critical @electron @embedded-mpv uses packaged frame-copy and fails closed to native-view', async ({
dataDir,
}) => {
expect(
process.arch,
'The official Linux frame-copy runtime is x64-only.'
).toBe('x64');
expect(
packagedExecutable,
`The dedicated packaged-runtime job must provide a real unpacked x64 executable with IPTVNATOR_E2E_PACKAGED_EXECUTABLE when ${PACKAGED_FRAME_COPY_REQUIRED_ENV}=1.`
).toBeTruthy();
const sourceExecutablePath = packagedExecutable as string;
assertNativeFallbackPrerequisites();
let packageClone: DisposablePackagedLinuxApp | undefined;
let hiddenRuntimeEntry: PackagedEntryGuard | undefined;
let media: LocalMediaServer | undefined;
let frameCopyApp: LaunchedElectronApp | undefined;
let fallbackApp: LaunchedElectronApp | undefined;
try {
packageClone =
createDisposablePackagedLinuxApp(sourceExecutablePath);
const executablePath = packageClone.executablePath;
const nativeDir = packageClone.nativeDir;
const runtimeIdentity = readPackagedRuntimeIdentity(nativeDir);
hiddenRuntimeEntry = createPackagedEntryGuard(
join(nativeDir, 'lib', runtimeIdentity.libmpvSoname),
{
expectedKind: 'regular-file',
hiddenDirectory: packageClone.temporaryRoot,
}
);
const mediaServer = await createLocalMediaServer();
media = mediaServer;
expect(runtimeIdentity).toMatchObject({
arch: 'x64',
platform: 'linux',
runtimeMode: 'bundled',
});
expect(['portable', 'flatpak']).toContain(runtimeIdentity.profile);
const launchedFrameCopyApp = await launchPackagedElectronApp(
executablePath,
dataDir,
{
env: {
[FRAME_COPY_OPT_IN_ENV]: '1',
},
}
);
frameCopyApp = launchedFrameCopyApp;
await expect
.poll(() => getEmbeddedMpvSupport(launchedFrameCopyApp))
.toMatchObject({
engine: 'frame-copy',
frameCopyAvailable: true,
platform: 'linux',
supported: true,
});
await installFrameCanvasAndSessionCapture(launchedFrameCopyApp);
const created = await launchedFrameCopyApp.mainWindow.evaluate(
async () => {
return window.electron.createEmbeddedMpvSession(
{ x: 0, y: 0, width: 320, height: 180 },
'Packaged frame-copy smoke',
0
);
}
);
await launchedFrameCopyApp.mainWindow.evaluate(
async ({ sessionId, streamUrl }) => {
await window.electron.setEmbeddedMpvPaused(sessionId, true);
await window.electron.loadEmbeddedMpvPlayback(sessionId, {
streamUrl,
title: 'Two-second generated Y4M fixture',
isLive: false,
});
},
{ sessionId: created.id, streamUrl: mediaServer.url }
);
await expect
.poll(
() => getLatestSession(launchedFrameCopyApp, created.id),
{
timeout: 15000,
}
)
.toMatchObject({
status: 'paused',
streamUrl: mediaServer.url,
videoHeight: 36,
videoWidth: 64,
});
const attached = await launchedFrameCopyApp.mainWindow.evaluate(
async (sessionId) => {
return window.electron.attachEmbeddedMpvFrameView?.(
sessionId
);
},
created.id
);
expect(attached).toBe(true);
await expect(
launchedFrameCopyApp.mainWindow.getByTestId(
'packaged-embedded-mpv-frame'
)
).toHaveAttribute('width', '320');
await expect(
launchedFrameCopyApp.mainWindow.getByTestId(
'packaged-embedded-mpv-frame'
)
).toHaveAttribute('height', '180');
await expect
.poll(() => renderedFrameSignal(launchedFrameCopyApp), {
timeout: 15000,
})
.toBeGreaterThan(0);
await launchedFrameCopyApp.mainWindow.evaluate(
(sessionId) =>
window.electron.setEmbeddedMpvPaused(sessionId, false),
created.id
);
await expect
.poll(
async () =>
(
await getLatestSession(
launchedFrameCopyApp,
created.id
)
)?.status,
{ timeout: 10000 }
)
.toBe('playing');
await launchedFrameCopyApp.mainWindow.evaluate(
(sessionId) =>
window.electron.setEmbeddedMpvPaused(sessionId, true),
created.id
);
await expect
.poll(
async () =>
(
await getLatestSession(
launchedFrameCopyApp,
created.id
)
)?.status,
{ timeout: 10000 }
)
.toBe('paused');
await launchedFrameCopyApp.mainWindow.evaluate(
async (sessionId) => {
window.electron.detachEmbeddedMpvFrameView?.();
await window.electron.disposeEmbeddedMpvSession(sessionId);
window.__packagedEmbeddedMpvUnsubscribe?.();
},
created.id
);
await closeAndWaitForExit(launchedFrameCopyApp);
frameCopyApp = undefined;
hiddenRuntimeEntry.hide();
expect(readPackagedRuntimeIdentity(nativeDir)).toEqual(
runtimeIdentity
);
const launchedFallbackApp = await launchPackagedElectronApp(
executablePath,
dataDir,
{
env: {
[FRAME_COPY_OPT_IN_ENV]: '1',
},
}
);
fallbackApp = launchedFallbackApp;
const fallbackSupport =
await getEmbeddedMpvSupport(launchedFallbackApp);
expect(fallbackSupport).toMatchObject({
engine: 'native',
frameCopyAvailable: false,
frameCopyUnavailableReason: 'runtime-library-missing',
platform: 'linux',
supported: true,
});
await installEmbeddedMpvSessionCapture(launchedFallbackApp);
const nativeSession = await launchedFallbackApp.mainWindow.evaluate(
async () => {
return window.electron.createEmbeddedMpvSession(
{ x: 0, y: 0, width: 320, height: 180 },
'Native-view fallback smoke',
0
);
}
);
expect(nativeSession.id).toMatch(/^embedded-mpv-/);
await launchedFallbackApp.mainWindow.evaluate(
async ({ sessionId, streamUrl }) => {
await window.electron.loadEmbeddedMpvPlayback(sessionId, {
streamUrl,
title: 'Native-view generated Y4M fixture',
isLive: false,
});
},
{ sessionId: nativeSession.id, streamUrl: mediaServer.url }
);
await expect
.poll(
async () =>
isMeaningfulNativePlaybackSnapshot(
await getLatestSession(
launchedFallbackApp,
nativeSession.id
),
mediaServer.url
),
{ timeout: 15000 }
)
.toBe(true);
expect(
launchedFallbackApp.electronApp.process().exitCode
).toBeNull();
expect(
launchedFallbackApp.electronApp.process().signalCode
).toBeNull();
await launchedFallbackApp.mainWindow.evaluate(async (sessionId) => {
await window.electron.disposeEmbeddedMpvSession(sessionId);
window.__packagedEmbeddedMpvUnsubscribe?.();
}, nativeSession.id);
await expect
.poll(() => launchedFallbackApp.mainWindow.title())
.toContain('IPTVnator');
} finally {
await cleanupPackagedFrameCopySmoke({
apps: [frameCopyApp, fallbackApp],
hiddenRuntimeEntry,
media,
packageClone,
});
}
});
});