mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
* 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
312 lines
12 KiB
TypeScript
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,
|
|
});
|
|
}
|
|
});
|
|
});
|