mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 21:56:18 -08:00
feat(embedded-mpv): Linux port of the frame-copy rendering engine (headless EGL) (#1171)
* feat(embedded-mpv): Linux frame-copy helper via headless EGL Port the frame-copy engine's native layer to Linux (PORTING.md items 1-4): - frame_helper_gl.h: platform GlContext abstraction. macOS keeps the CGL path (moved verbatim); Linux acquires an EGL display in order surfaceless-Mesa -> default display -> GBM render node, binds a 3.2 core desktop-GL context surfaceless (1x1 pbuffer fallback), and hands mpv eglGetProcAddress. The helper's own GL calls link against glvnd libOpenGL, so no display server is required. - frame_shm.h: portable frame_shm_now_ns() (CLOCK_MONOTONIC) shared by the helper and the reader addon, replacing the macOS-only clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); producer and consumer stay on the same clock. - embedded_mpv_frame_reader.c: real implementation now also on __linux__ (the code was already POSIX apart from the clock call). - binding.gyp: OS==linux executable branch for iptvnator_mpv_helper linking system libmpv (-lmpv) + EGL/OpenGL/gbm, with rpaths for $ORIGIN/lib and the build-time library dir. The in-process addon still does not link libmpv - the ban only binds in-process, the helper is out of process. - build-embedded-mpv.js: system-dev fallback on Linux (LIBMPV_INCLUDE_DIR or /usr/include) so a distro libmpv-dev install builds without staging a vendored runtime; a pre-set LINUX_NATIVE_LIBRARY_DIR now wins over the vendored lib dir. Verified on Ubuntu 25.04 / i7-1165G7 (Iris Xe): lavfi smoke per PORTING.md (idle->loading->playing snapshots at 4 Hz, aspect-fit generation bump g1 1280x720 -> g2 960x720 for a 4:3 source), reader probe 60 fps at 1080p60 with 0 torn reads, clean quit with no leaked processes or shm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(embedded-mpv): enable the frame-copy engine gates on Linux Flip the TypeScript side of the Linux port (PORTING.md item 5). A shared dependency-free predicate, isFrameCopyPlatformSupported() (linux any-arch, darwin arm64-only), now backs all four gates so they cannot drift: - main.ts: the persisted Settings toggle promotes to the env flag on Linux too (this runs before window creation and controls the sandbox relax). - EmbeddedMpvNativeService.isFrameCopyEngineActive/isFrameCopyAvailable. - EmbeddedMpvFrameCopyAdapter.isSupported. getSupport() ordering: the frame-copy branch moves above the Linux-only native-engine prerequisites - the X11/Xwayland display-server check and the system-mpv-on-PATH probe only bind the --wid native engine, while the frame-copy helper renders offscreen (headless EGL) and links libmpv itself. createSession() also skips resolving the native window handle for frame-copy sessions, which the adapter ignores anyway, so native-Wayland sessions no longer trip the window-handle assertion. Settings copy: the i18n frame-copy description now says macOS (Apple Silicon) and Linux in all 18 languages; stale macOS-only doc comments in the settings/support interfaces updated alongside. Tests: platform-gate matrix for the adapter (darwin arm64/x64, linux x64/arm64, win32) and service specs covering Linux activation under native Wayland, macOS arm64 staying active, macOS x64 staying native, and the skipped window handle for frame-copy sessions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(packaging): CI + package guards for the Linux frame-copy helper - build-and-make.yaml: install libegl-dev/libgl-dev/libgbm-dev on the Linux runner (the helper's EGL backend needs them now that the helper target builds on Linux), and verify the built helper exists and DOES link libmpv - the inverse of the addon's no-libmpv rule, which still holds and stays validated. - electron-after-pack.cjs: strip iptvnator_mpv_helper from packaged Linux apps. It links the build host's system libmpv, which end-user systems cannot be assumed to have; the support probe treats the missing helper as frame-copy-unavailable (dev-build-only engine until the bundled-runtime staging milestone). - frame_helper_gl.h: log the chosen EGL display tier to stderr (the adapter mirrors helper stderr), so bring-up problems on exotic setups are diagnosable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(embedded-mpv): document the Linux frame-copy port - architecture doc: frame-copy section covers Linux (EGL display tiers, build deps, package strip), Linux support matrix notes the frame-copy exception to the X11 + system-mpv requirements, Linux measured baseline. - RESULTS.md: Ubuntu 25.04 / i7-1165G7 (Iris Xe) measurement rows via the production helper + reader probe; viewport-size claim reproduced. - PORTING.md: Linux marked done with pointers to what changed; Windows remains the open port and its perf gate the open decision. - CLAUDE.md + tools/embedded-mpv/README.md: platform scope, Linux dev build requirements, system-headers fallback, helper strip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(embedded-mpv): commit the Linux frame-copy measurement probe linux-frame-probe.mjs reproduces the RESULTS.md Linux rows: spawns the production helper, attaches the frame-reader addon to the announced shm generation, and reports new-frame fps, copy wall time, produce->copy age, torn reads and pixel spread. Usage documented in RESULTS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): address multi-agent review findings on the Linux port Confirmed findings (each verified by 3 adversarial reviewers): - CI would fail to link the helper: -lOpenGL needs the unversioned glvnd libOpenGL.so, shipped only by libopengl-dev, which neither the runner images nor the previous apt line provide. Added to the workflow and to every documented Linux build-dep list. - The new 'test -x' dist guard could never pass: webpack's dist asset copy drops file modes (helper arrives as 0644). The guard is now 'test -f'; electron-after-pack.cjs restores the execute bit on packaged helpers (also fixes packaged-macOS spawns); the support probe now requires X_OK, so a mode-stripped helper reads as frame-copy-unavailable and falls back to native instead of failing spawn with EACCES. - The Settings frame-copy toggle was unreachable in exactly the Linux states the port targets: the native-Wayland and missing-system-mpv unsupported payloads omitted frameCopyAvailable, and toggle visibility derives solely from it. Both returns now advertise availability. Also from review: - build-embedded-mpv.js keeps the old graceful-skip contract when the new system-dev fallback finds libmpv-dev but the GL/EGL/gbm dev stack is missing (previously such machines skipped; a hard electron-build failure was a regression). - createSession derives the window-handle skip from the dispatched addon instead of re-evaluating the engine gate, so the two cannot disagree. - The render thread logs the GL renderer string (surfaceless Mesa can silently pick llvmpipe on non-Mesa-primary systems; now diagnosable — verified 'Mesa Intel Iris Xe' on this machine). - Specs pin the new semantics: frameCopyAvailable advertised while native is unsupported (Wayland / no mpv), frame-copy supported without a system mpv, and the handle-skip test disposes its session through the owning adapter. - Docs: PORTING.md file map reflects the frame_helper_gl.h seam for the Windows porter; helper-strip removal correctly gated on milestone 4 (bundled libmpv), not milestone 3; RESULTS.md preamble notes the RAW->MONOTONIC clock change; stale '(macOS)' scope comments updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): address Greptile/Codex review comments - Sandbox gate requires a usable helper (Greptile P1, security): the main.ts env promotion now also probes for an executable iptvnator_mpv_helper before relaxing the window sandbox — a stale opt-in on packaged Linux (helper deliberately stripped) or after a cleaned native build no longer costs a sandboxless launch for an engine that cannot activate. Helper discovery (addon candidate paths + X_OK probe) moved into embedded-mpv-frame-copy-platform.util.ts, shared by main.ts and the service; the service keeps thin instance wrappers so tests can stub per scenario. New util spec pins the platform matrix, candidate resolution, and the execute-bit semantics. - Stale frame-copy artifacts on skipped builds (Codex P2): cleanOutput() now also removes iptvnator_mpv_helper and embedded_mpv_frame_reader.node, so a failed/skipped rebuild cannot leave a previous helper advertising frame-copy support against a runtime the build just declared unavailable. - Multiarch default lib dir (Greptile P1, partially refuted): -l resolution never depended on our -L (the compiler's built-in search paths include the Debian/Ubuntu multiarch dir — proven by the green CI run linking with a nonexistent -L dir), but the system-dev fallback now defaults to /usr/lib/<multiarch-triple> when present so the -L flag and the helper's baked rpath point somewhere real. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): harden Linux frame-copy port --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
26271fc076
commit
7d75d989e8
45 files changed
+1387
-177
No files matched your search
+2
-1
@@ -30,7 +30,8 @@ describe('embedded-mpv-frame-copy-platform.util', () => {
|
||||
it.each<[NodeJS.Platform, string, boolean]>([
|
||||
['darwin', 'arm64', true],
|
||||
['darwin', 'x64', false],
|
||||
['linux', 'x64', false],
|
||||
['linux', 'x64', true],
|
||||
['linux', 'arm64', true],
|
||||
['win32', 'x64', false],
|
||||
])('%s/%s -> %s', (platform, arch, expected) => {
|
||||
Object.defineProperty(process, 'platform', { value: platform });
|
||||
|
||||
@@ -4,11 +4,18 @@ import path from 'path';
|
||||
|
||||
/**
|
||||
* Platform gate + helper discovery for the embedded MPV frame-copy engine,
|
||||
* shared by startup and the runtime service so sandbox and engine decisions
|
||||
* cannot drift. These helpers must remain callable before app.whenReady().
|
||||
* shared by startup, the runtime service, and the frame-copy adapter so
|
||||
* sandbox and engine decisions cannot drift. These helpers must remain
|
||||
* callable before app.whenReady().
|
||||
*
|
||||
* macOS stays Apple-Silicon-only. Linux supports every architecture because
|
||||
* its helper renders through headless EGL and links libmpv out of process.
|
||||
*/
|
||||
export function isFrameCopyPlatformSupported(): boolean {
|
||||
return process.platform === 'darwin' && process.arch === 'arm64';
|
||||
return (
|
||||
process.platform === 'linux' ||
|
||||
(process.platform === 'darwin' && process.arch === 'arm64')
|
||||
);
|
||||
}
|
||||
|
||||
function dedupeDefinedPaths(paths: Array<string | undefined>): string[] {
|
||||
|
||||
@@ -173,4 +173,33 @@ describe('EmbeddedMpvFrameCopyAdapter', () => {
|
||||
const withoutHelper = createAdapter(null);
|
||||
expect(withoutHelper.isSupported()).toBe(false);
|
||||
});
|
||||
|
||||
describe('isSupported platform gate', () => {
|
||||
const originalPlatform = process.platform;
|
||||
const originalArch = process.arch;
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
Object.defineProperty(process, 'arch', { value: originalArch });
|
||||
});
|
||||
|
||||
it.each<[NodeJS.Platform, string, boolean]>([
|
||||
['darwin', 'arm64', true],
|
||||
['darwin', 'x64', false],
|
||||
['linux', 'x64', true],
|
||||
['linux', 'arm64', true],
|
||||
['win32', 'x64', false],
|
||||
])(
|
||||
'on %s/%s with a helper binary present -> %s',
|
||||
(platform, arch, expected) => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: platform,
|
||||
});
|
||||
Object.defineProperty(process, 'arch', { value: arch });
|
||||
expect(adapter.isSupported()).toBe(expected);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
EmbeddedMpvFrameSource,
|
||||
ResolvedPortalPlayback,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { isFrameCopyPlatformSupported } from './embedded-mpv-frame-copy-platform.util';
|
||||
import type {
|
||||
NativeEmbeddedMpvAddon,
|
||||
NativeEmbeddedMpvSessionSnapshot,
|
||||
@@ -81,8 +82,7 @@ export class EmbeddedMpvFrameCopyAdapter implements NativeEmbeddedMpvAddon {
|
||||
|
||||
isSupported(): boolean {
|
||||
return (
|
||||
process.platform === 'darwin' &&
|
||||
process.arch === 'arm64' &&
|
||||
isFrameCopyPlatformSupported() &&
|
||||
this.options.resolveHelperPath() !== null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,13 @@ describe('Embedded MPV native source recording invariants', () => {
|
||||
),
|
||||
'utf8'
|
||||
);
|
||||
const frameHelperGlSource = readFileSync(
|
||||
path.resolve(__dirname, '../../../native/helper/frame_helper_gl.h'),
|
||||
'utf8'
|
||||
);
|
||||
const linuxFrameHelperGlSource = frameHelperGlSource.slice(
|
||||
frameHelperGlSource.indexOf('inline void* eglWrapGetProcAddress')
|
||||
);
|
||||
|
||||
function functionBody(name: string): string {
|
||||
return sourceFunctionBody(nativeSource, `Napi::Value ${name}(`, name);
|
||||
@@ -590,7 +597,7 @@ describe('Embedded MPV native source recording invariants', () => {
|
||||
|
||||
it('requires Linux embedded MPV build inputs and validates process isolation in CI', () => {
|
||||
expect(buildAndMakeWorkflowSource).toContain(
|
||||
'libmpv-dev mpv pkg-config'
|
||||
'libmpv-dev mpv pkg-config libegl-dev libgl-dev libopengl-dev libgbm-dev'
|
||||
);
|
||||
expect(buildAndMakeWorkflowSource).toContain(
|
||||
'Stage Linux embedded MPV build inputs'
|
||||
@@ -602,11 +609,35 @@ describe('Embedded MPV native source recording invariants', () => {
|
||||
expect(buildAndMakeWorkflowSource).toContain(
|
||||
'Linux embedded MPV addon must not link directly to libmpv'
|
||||
);
|
||||
expect(buildAndMakeWorkflowSource).toContain(
|
||||
'test -f dist/apps/electron-backend/native/iptvnator_mpv_helper'
|
||||
);
|
||||
expect(buildAndMakeWorkflowSource).toContain(
|
||||
'Linux frame-copy helper must link libmpv'
|
||||
);
|
||||
expect(buildScriptSource).toContain("origin: 'external-mpv-process'");
|
||||
expect(buildScriptSource).toContain('writeLinuxProcessRuntimeManifest');
|
||||
expect(buildScriptSource).toContain('runtimeFiles: []');
|
||||
});
|
||||
|
||||
it('supports Linux system-development inputs without leaving stale frame-copy artifacts', () => {
|
||||
expect(buildScriptSource).toContain(
|
||||
"const systemIncludeDir =\n process.env.LIBMPV_INCLUDE_DIR || '/usr/include';"
|
||||
);
|
||||
expect(buildScriptSource).toContain(
|
||||
'process.env.LINUX_NATIVE_LIBRARY_DIR ||\n defaultLinuxSystemLibDir()'
|
||||
);
|
||||
expect(buildScriptSource).toContain("arm: 'arm-linux-gnueabihf'");
|
||||
expect(buildScriptSource).toContain("arm64: 'aarch64-linux-gnu'");
|
||||
expect(buildScriptSource).toContain("x64: 'x86_64-linux-gnu'");
|
||||
expect(buildScriptSource).toContain(
|
||||
"if (!embeddedMpvRequired && runtime.origin === 'system-dev')"
|
||||
);
|
||||
expect(buildScriptSource).toContain(
|
||||
'removeStaleFrameCopyArtifacts(outputDir);'
|
||||
);
|
||||
});
|
||||
|
||||
it('forces the current frame into a rebuilt shm generation after a paused resize', () => {
|
||||
const runLoop = sourceFunctionBody(
|
||||
frameHelperRenderSource,
|
||||
@@ -620,6 +651,85 @@ describe('Embedded MPV native source recording invariants', () => {
|
||||
'if (targetsRebuilt || (flags & MPV_RENDER_UPDATE_FRAME))'
|
||||
);
|
||||
});
|
||||
|
||||
it('validates complete EGL candidates and keeps software rendering as the final fallback', () => {
|
||||
const tryCandidate = sourceFunctionBody(
|
||||
linuxFrameHelperGlSource,
|
||||
'bool tryCandidate(',
|
||||
'GlContext::tryCandidate'
|
||||
);
|
||||
const create = sourceFunctionBody(
|
||||
linuxFrameHelperGlSource,
|
||||
'bool create(std::string& errorOut)',
|
||||
'GlContext::create'
|
||||
);
|
||||
|
||||
for (const requiredStep of [
|
||||
'eglInitialize(',
|
||||
'eglBindAPI(EGL_OPENGL_API)',
|
||||
'chooseConfig(',
|
||||
'eglCreateContext(',
|
||||
'eglMakeCurrent(',
|
||||
'glGetString(GL_RENDERER)',
|
||||
]) {
|
||||
expect(tryCandidate).toContain(requiredStep);
|
||||
}
|
||||
expect(create).toContain('softwareFallback');
|
||||
expect(create).toContain('candidate.softwareRenderer');
|
||||
expect(create.indexOf('DisplayTier::SurfacelessMesa')).toBeLessThan(
|
||||
create.indexOf('DisplayTier::Default')
|
||||
);
|
||||
expect(create.indexOf('DisplayTier::Default')).toBeLessThan(
|
||||
create.indexOf('DisplayTier::Gbm')
|
||||
);
|
||||
});
|
||||
|
||||
it('binds the desktop GL API on the render thread and releases it before EGL teardown', () => {
|
||||
const makeCurrent = sourceFunctionBody(
|
||||
linuxFrameHelperGlSource,
|
||||
'bool makeCurrent(std::string& errorOut)',
|
||||
'GlContext::makeCurrent'
|
||||
);
|
||||
const destroy = sourceFunctionBody(
|
||||
linuxFrameHelperGlSource,
|
||||
'void destroy()',
|
||||
'GlContext::destroy'
|
||||
);
|
||||
|
||||
expect(makeCurrent).toContain('eglBindAPI(EGL_OPENGL_API)');
|
||||
expect(makeCurrent).toContain('eglMakeCurrent(');
|
||||
expect(makeCurrent).toContain('return false;');
|
||||
|
||||
const bindIndex = destroy.indexOf('eglBindAPI(EGL_OPENGL_API)');
|
||||
const unbindIndex = destroy.indexOf('eglMakeCurrent(');
|
||||
const releaseIndex = destroy.indexOf('eglReleaseThread()');
|
||||
const destroySurfaceIndex = destroy.indexOf('eglDestroySurface(');
|
||||
expect(bindIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(unbindIndex).toBeGreaterThan(bindIndex);
|
||||
expect(releaseIndex).toBeGreaterThan(unbindIndex);
|
||||
expect(destroySurfaceIndex).toBeGreaterThan(releaseIndex);
|
||||
|
||||
const setupGl = sourceFunctionBody(
|
||||
frameHelperRenderSource,
|
||||
'inline bool RenderPipeline::setupGl(std::string& errorOut)',
|
||||
'RenderPipeline::setupGl'
|
||||
);
|
||||
expect(setupGl).toContain(
|
||||
'if (!gl_.makeCurrent(errorOut)) return false;'
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves linked core GL symbols before falling back to EGL extension lookup', () => {
|
||||
const dlsymIndex = linuxFrameHelperGlSource.indexOf(
|
||||
'dlsym(RTLD_DEFAULT, name)'
|
||||
);
|
||||
const eglIndex = linuxFrameHelperGlSource.indexOf(
|
||||
'eglGetProcAddress(name)'
|
||||
);
|
||||
|
||||
expect(dlsymIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(eglIndex).toBeGreaterThan(dlsymIndex);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Embedded MPV native build configuration', () => {
|
||||
@@ -633,6 +743,10 @@ describe('Embedded MPV native build configuration', () => {
|
||||
(candidate: { target_name?: string }) =>
|
||||
candidate.target_name === 'embedded_mpv'
|
||||
);
|
||||
const helperTarget = bindingGyp.targets.find(
|
||||
(candidate: { target_name?: string }) =>
|
||||
candidate.target_name === 'iptvnator_mpv_helper'
|
||||
);
|
||||
|
||||
it('declares platform-specific native sources for macOS, Windows, and Linux', () => {
|
||||
expect(target).toBeDefined();
|
||||
@@ -640,4 +754,24 @@ describe('Embedded MPV native build configuration', () => {
|
||||
expect(JSON.stringify(target)).toContain('src/embedded_mpv_win32.cc');
|
||||
expect(JSON.stringify(target)).toContain('src/embedded_mpv_linux.cc');
|
||||
});
|
||||
|
||||
it('links libmpv and headless GL only into the Linux helper process', () => {
|
||||
const linuxAddonConfig = target.conditions.find(
|
||||
([condition]: [string]) => condition === 'OS=="linux"'
|
||||
)?.[1];
|
||||
const linuxHelperConfig = helperTarget.conditions.find(
|
||||
([condition]: [string]) => condition === 'OS=="linux"'
|
||||
)?.[1];
|
||||
|
||||
expect(linuxAddonConfig?.libraries).not.toContain('-lmpv');
|
||||
expect(linuxHelperConfig?.libraries).toEqual(
|
||||
expect.arrayContaining([
|
||||
'-lmpv',
|
||||
'-lEGL',
|
||||
'-lOpenGL',
|
||||
'-lgbm',
|
||||
'-ldl',
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -297,6 +297,140 @@ describe('EmbeddedMpvNativeService power blocker', () => {
|
||||
}
|
||||
);
|
||||
|
||||
describe('frame-copy platform gate', () => {
|
||||
const originalArch = process.arch;
|
||||
|
||||
function mockHelperPresent(): void {
|
||||
jest.spyOn(
|
||||
service as unknown as {
|
||||
resolveFrameCopyHelperPath: () => string | null;
|
||||
},
|
||||
'resolveFrameCopyHelperPath'
|
||||
).mockReturnValue('/native/iptvnator_mpv_helper');
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'arch', { value: originalArch });
|
||||
delete process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY;
|
||||
});
|
||||
|
||||
it('activates the frame-copy engine on Linux, even under native Wayland', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
// Native Wayland session (no X11 ozone): blocks the --wid native
|
||||
// engine, but the frame-copy helper renders offscreen into shm
|
||||
// and must stay available.
|
||||
process.env.DISPLAY = ':0';
|
||||
process.env.WAYLAND_DISPLAY = 'wayland-0';
|
||||
process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY = '1';
|
||||
mockHelperPresent();
|
||||
|
||||
expect(service.getActiveEngine()).toBe('frame-copy');
|
||||
expect(service.isFrameCopyAvailable()).toBe(true);
|
||||
expect(service.getSupport()).toEqual(
|
||||
expect.objectContaining({
|
||||
supported: true,
|
||||
engine: 'frame-copy',
|
||||
frameCopyAvailable: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('advertises frame-copy availability while native Wayland blocks the native engine', () => {
|
||||
// Pre-opt-in discoverability: without frameCopyAvailable on the
|
||||
// unsupported payload the Settings toggle never appears in
|
||||
// exactly the states the frame-copy engine exists to fix.
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
process.env.DISPLAY = ':0';
|
||||
process.env.WAYLAND_DISPLAY = 'wayland-0';
|
||||
mockHelperPresent();
|
||||
|
||||
const support = service.getSupport();
|
||||
expect(support.supported).toBe(false);
|
||||
expect(support.reason).toContain('Native Wayland embedding');
|
||||
expect(support.frameCopyAvailable).toBe(true);
|
||||
});
|
||||
|
||||
it('advertises frame-copy availability when the system mpv executable is missing', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
process.env.DISPLAY = ':0';
|
||||
delete process.env.WAYLAND_DISPLAY;
|
||||
mockSpawnSync.mockReturnValue({ status: 1 });
|
||||
mockHelperPresent();
|
||||
|
||||
const support = service.getSupport();
|
||||
expect(support.supported).toBe(false);
|
||||
expect(support.frameCopyAvailable).toBe(true);
|
||||
});
|
||||
|
||||
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.
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
process.env.DISPLAY = ':0';
|
||||
delete process.env.WAYLAND_DISPLAY;
|
||||
process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY = '1';
|
||||
mockSpawnSync.mockReturnValue({ status: 1 });
|
||||
mockHelperPresent();
|
||||
|
||||
expect(service.getSupport()).toEqual(
|
||||
expect.objectContaining({
|
||||
supported: true,
|
||||
engine: 'frame-copy',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('activates the frame-copy engine on macOS arm64', () => {
|
||||
Object.defineProperty(process, 'arch', { value: 'arm64' });
|
||||
process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY = '1';
|
||||
mockHelperPresent();
|
||||
|
||||
expect(service.getActiveEngine()).toBe('frame-copy');
|
||||
expect(service.isFrameCopyAvailable()).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the frame-copy engine Apple-Silicon-only on macOS', () => {
|
||||
Object.defineProperty(process, 'arch', { value: 'x64' });
|
||||
process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY = '1';
|
||||
mockHelperPresent();
|
||||
|
||||
expect(service.getActiveEngine()).toBe('native');
|
||||
expect(service.isFrameCopyAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
it('skips the native window handle when creating a frame-copy session', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY = '1';
|
||||
mockHelperPresent();
|
||||
const frameCopyAddon = createMockAddon();
|
||||
frameCopyAddon.createSession.mockReturnValueOnce('s-fc');
|
||||
frameCopyAddon.getSessionSnapshot.mockReturnValueOnce(
|
||||
snapshot('loading')
|
||||
);
|
||||
(
|
||||
service as unknown as { frameCopyAdapter: MockAddon }
|
||||
).frameCopyAdapter = frameCopyAddon;
|
||||
|
||||
service.createSession(BOUNDS, '', 1);
|
||||
|
||||
expect(mainWindowGetNativeWindowHandleMock).not.toHaveBeenCalled();
|
||||
expect(frameCopyAddon.createSession).toHaveBeenCalledWith(
|
||||
Buffer.alloc(0),
|
||||
BOUNDS,
|
||||
'',
|
||||
1
|
||||
);
|
||||
|
||||
// Dispose while the frame-copy env is still set so teardown
|
||||
// dispatches to the adapter that owns the session, not the
|
||||
// native addon the outer afterEach shutdown would pick.
|
||||
service.disposeSession('s-fc');
|
||||
expect(frameCopyAddon.disposeSession).toHaveBeenCalledWith(
|
||||
's-fc'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not acquire a blocker for a loading session', () => {
|
||||
startSession('s1', snapshot('loading'));
|
||||
expect(powerSaveBlockerMock.start).not.toHaveBeenCalled();
|
||||
|
||||
@@ -111,9 +111,9 @@ export class EmbeddedMpvNativeService {
|
||||
|
||||
/**
|
||||
* Frame-copy engine: helper process + shm ring + renderer canvas.
|
||||
* Experimental, macOS Apple Silicon only (owner decision 2026-07-10),
|
||||
* opted into with IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY=1 on top of
|
||||
* the regular embedded MPV experiment flag.
|
||||
* Experimental, macOS Apple Silicon (owner decision 2026-07-10) and
|
||||
* Linux, opted into with IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY=1 on
|
||||
* top of the regular embedded MPV experiment flag.
|
||||
*/
|
||||
private isFrameCopyEngineRequested(): boolean {
|
||||
return ['1', 'true', 'yes', 'on'].includes(
|
||||
@@ -215,11 +215,21 @@ export class EmbeddedMpvNativeService {
|
||||
};
|
||||
}
|
||||
|
||||
if (this.isUnsupportedLinuxDisplayServer()) {
|
||||
// The frame-copy engine renders offscreen (headless EGL on Linux)
|
||||
// into a renderer canvas: the Linux X11/Xwayland and system-mpv
|
||||
// requirements below only bind the native --wid engine. Both
|
||||
// native-engine failure returns still advertise frameCopyAvailable
|
||||
// so the Settings toggle stays reachable — otherwise the states the
|
||||
// frame-copy engine exists to fix would hide the way to enable it.
|
||||
if (
|
||||
this.isUnsupportedLinuxDisplayServer() &&
|
||||
!this.isFrameCopyEngineActive()
|
||||
) {
|
||||
return {
|
||||
supported: false,
|
||||
platform: process.platform,
|
||||
reason: 'Embedded MPV on Linux currently requires X11 or Xwayland. Native Wayland embedding is not supported yet.',
|
||||
frameCopyAvailable: this.isFrameCopyAvailable(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -231,16 +241,6 @@ export class EmbeddedMpvNativeService {
|
||||
};
|
||||
}
|
||||
|
||||
const missingLinuxMpvExecutableReason =
|
||||
this.getMissingLinuxMpvExecutableReason();
|
||||
if (missingLinuxMpvExecutableReason) {
|
||||
return {
|
||||
supported: false,
|
||||
platform: process.platform,
|
||||
reason: missingLinuxMpvExecutableReason,
|
||||
};
|
||||
}
|
||||
|
||||
// A requested-but-unavailable frame-copy engine (wrong platform or
|
||||
// missing helper) intentionally falls through to the native path so
|
||||
// embedded MPV keeps working and Settings can clear the opt-in.
|
||||
@@ -254,6 +254,17 @@ export class EmbeddedMpvNativeService {
|
||||
};
|
||||
}
|
||||
|
||||
const missingLinuxMpvExecutableReason =
|
||||
this.getMissingLinuxMpvExecutableReason();
|
||||
if (missingLinuxMpvExecutableReason) {
|
||||
return {
|
||||
supported: false,
|
||||
platform: process.platform,
|
||||
reason: missingLinuxMpvExecutableReason,
|
||||
frameCopyAvailable: this.isFrameCopyAvailable(),
|
||||
};
|
||||
}
|
||||
|
||||
if (this.addon) {
|
||||
try {
|
||||
if (!this.addon.isSupported()) {
|
||||
@@ -385,7 +396,16 @@ export class EmbeddedMpvNativeService {
|
||||
): EmbeddedMpvSession {
|
||||
this.assertEmbeddedMpvEnabled();
|
||||
const addon = this.getAddon();
|
||||
const windowHandle = this.getMainWindowHandle();
|
||||
// The frame-copy adapter ignores the native window handle (frames go
|
||||
// through shm to a DOM canvas), so skip resolving it — under native
|
||||
// Wayland the handle assertion would reject an engine that does not
|
||||
// embed into the window at all. Derive the skip from the dispatched
|
||||
// addon rather than re-evaluating the engine gate, so the two
|
||||
// decisions cannot disagree.
|
||||
const windowHandle =
|
||||
this.frameCopyAdapter && addon === this.frameCopyAdapter
|
||||
? Buffer.alloc(0)
|
||||
: this.getMainWindowHandle();
|
||||
const startedAt = new Date().toISOString();
|
||||
const sessionId = addon.createSession(
|
||||
windowHandle,
|
||||
|
||||
@@ -9,7 +9,7 @@ export const VLC_PLAYER_ARGUMENTS = 'VLC_PLAYER_ARGUMENTS';
|
||||
export const MPV_REUSE_INSTANCE = 'MPV_REUSE_INSTANCE';
|
||||
export const VLC_REUSE_INSTANCE = 'VLC_REUSE_INSTANCE';
|
||||
/**
|
||||
* Embedded MPV frame-copy engine opt-in (macOS arm64). Lives in the main
|
||||
* Embedded MPV frame-copy engine opt-in (macOS arm64, Linux). Lives in the main
|
||||
* process config file because it must be readable synchronously before the
|
||||
* BrowserWindow is created — the engine relaxes the window sandbox for its
|
||||
* preload frame pump, which cannot change after window creation.
|
||||
|
||||
Reference in new issue
Block a user