feat(embedded-mpv): add Windows frame-copy support (#1175)

Port the embedded mpv frame-copy pipeline to Windows with WGL rendering and named shared memory. Includes packaging validation, platform gates, tests, and architecture documentation.
This commit is contained in:
4gray authored and GitHub committed 2026-07-15 21:27:56 +02:00
1 parent 7d75d989e8
commit 59e08fd2d6
41 files changed
+1284 -157

No files matched your search

@@ -32,7 +32,8 @@ describe('embedded-mpv-frame-copy-platform.util', () => {
['darwin', 'x64', false],
['linux', 'x64', true],
['linux', 'arm64', true],
['win32', 'x64', false],
['win32', 'x64', true],
['freebsd', 'x64', false],
])('%s/%s -> %s', (platform, arch, expected) => {
Object.defineProperty(process, 'platform', { value: platform });
Object.defineProperty(process, 'arch', { value: arch });
@@ -54,8 +55,14 @@ describe('embedded-mpv-frame-copy-platform.util', () => {
'build',
'Release'
);
// The resolver looks for the host platform's binary name, so the
// fixture must follow it for the spec to stay host-agnostic.
const helperFileName = () =>
process.platform === 'win32'
? 'iptvnator_mpv_helper.exe'
: 'iptvnator_mpv_helper';
const helperPath = () =>
path.join(releaseDir(), 'iptvnator_mpv_helper');
path.join(releaseDir(), helperFileName());
const readerPath = () =>
path.join(releaseDir(), 'embedded_mpv_frame_reader.node');
@@ -134,7 +141,7 @@ describe('embedded-mpv-frame-copy-platform.util', () => {
mkdirSync(packagedNativeDir, { recursive: true });
const packagedHelper = path.join(
packagedNativeDir,
'iptvnator_mpv_helper'
helperFileName()
);
writeFileSync(packagedHelper, '#!/bin/sh\n');
chmodSync(packagedHelper, 0o755);
@@ -8,12 +8,17 @@ import path from 'path';
* 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.
* macOS: Apple Silicon only (owner decision 2026-07-10) — Intel Macs keep
* the docked native engine. Linux: any arch — the helper renders offscreen
* through headless EGL and links libmpv out of process, so neither window
* embedding nor the in-process-libmpv ban constrains it. Windows: any arch
* with a helper binary (WGL offscreen render; in practice x64, the only
* vendored runtime) — the helper-presence check below is the real gate.
*/
export function isFrameCopyPlatformSupported(): boolean {
return (
process.platform === 'linux' ||
process.platform === 'win32' ||
(process.platform === 'darwin' && process.arch === 'arm64')
);
}
@@ -80,7 +85,8 @@ export function getEmbeddedMpvAddonCandidatePaths(): string[] {
/**
* Resolve the first executable frame-copy helper with a readable regular-file
* frame reader beside it. Both artifacts are required before the engine may
* relax the renderer sandbox.
* relax the renderer sandbox. Windows uses the `.exe` helper name; X_OK is an
* existence check there, while POSIX platforms also require the execute bit.
*/
export function resolveFrameCopyHelperPath(): string | null {
// Packaged startup must never fall through to cwd/dist development
@@ -89,12 +95,15 @@ export function resolveFrameCopyHelperPath(): string | null {
const addonCandidates = app.isPackaged
? getPackagedAddonPaths()
: [getLocalBuildAddonPath(), ...getDistAddonPaths()];
const helperFileName =
process.platform === 'win32'
? 'iptvnator_mpv_helper.exe'
: 'iptvnator_mpv_helper';
return (
addonCandidates
.map((candidatePath) => path.dirname(candidatePath))
.map((nativeDir) => ({
helper: path.join(nativeDir, 'iptvnator_mpv_helper'),
helper: path.join(nativeDir, helperFileName),
reader: path.join(
nativeDir,
'embedded_mpv_frame_reader.node'
@@ -1,4 +1,5 @@
import { EventEmitter } from 'events';
import path from 'path';
const spawnMock = jest.fn();
jest.mock('child_process', () => ({
@@ -117,8 +118,10 @@ describe('EmbeddedMpvFrameCopyAdapter', () => {
expect(frameSourceChanges).toEqual([
{ sessionId, shmName: `/${sessionId}-g1` },
]);
// path.join output is host-specific; build the expectation the
// same way so the spec passes on Windows checkouts too.
expect(adapter.getFrameSource(sessionId)?.readerPath).toBe(
'/native/embedded_mpv_frame_reader.node'
path.join('/native', 'embedded_mpv_frame_reader.node')
);
});
@@ -190,7 +193,8 @@ describe('EmbeddedMpvFrameCopyAdapter', () => {
['darwin', 'x64', false],
['linux', 'x64', true],
['linux', 'arm64', true],
['win32', 'x64', false],
['win32', 'x64', true],
['freebsd', 'x64', false],
])(
'on %s/%s with a helper binary present -> %s',
(platform, arch, expected) => {
@@ -40,19 +40,23 @@ describe('Embedded MPV native source recording invariants', () => {
'utf8'
);
const frameHelperRenderSource = readFileSync(
path.resolve(
__dirname,
'../../../native/helper/frame_helper_render.h'
),
path.resolve(__dirname, '../../../native/helper/frame_helper_render.h'),
'utf8'
);
const frameHelperGlSource = readFileSync(
path.resolve(__dirname, '../../../native/helper/frame_helper_gl.h'),
'utf8'
);
const frameShmSource = readFileSync(
path.resolve(__dirname, '../../../native/helper/frame_shm.h'),
'utf8'
);
const linuxFrameHelperGlSource = frameHelperGlSource.slice(
frameHelperGlSource.indexOf('inline void* eglWrapGetProcAddress')
);
const windowsFrameHelperGlSource = frameHelperGlSource.slice(
frameHelperGlSource.indexOf('#define FRAME_HELPER_WGL_ENTRY_POINTS')
);
function functionBody(name: string): string {
return sourceFunctionBody(nativeSource, `Napi::Value ${name}(`, name);
@@ -304,9 +308,7 @@ describe('Embedded MPV native source recording invariants', () => {
});
it('copies Windows runtime DLLs next to the addon for Windows loader lookup', () => {
expect(buildScriptSource).toContain(
"'libmpv-2.dll',"
);
expect(buildScriptSource).toContain("'libmpv-2.dll',");
expect(buildScriptSource).toContain(
'path.join(outputDir, windowsDllName)'
);
@@ -719,6 +721,57 @@ describe('Embedded MPV native source recording invariants', () => {
);
});
it('checks WGL context handoff and never deletes a context after failed unbind', () => {
const create = sourceFunctionBody(
windowsFrameHelperGlSource,
'bool create(std::string& errorOut)',
'WGL GlContext::create'
);
const makeCurrent = sourceFunctionBody(
windowsFrameHelperGlSource,
'bool makeCurrent(std::string& errorOut)',
'WGL GlContext::makeCurrent'
);
const destroy = sourceFunctionBody(
windowsFrameHelperGlSource,
'void destroy()',
'WGL GlContext::destroy'
);
expect(create).toContain('wglMakeCurrent(dc_, core) != TRUE');
expect(create).toContain('wglMakeCurrent(nullptr, nullptr) != TRUE');
expect(create).toContain('wglLoaderGetProcAddress(opengl32_,');
expect(create).toContain('"wglCreateContextAttribsARB")');
expect(create).not.toContain(
'wglGetProcAddress("wglCreateContextAttribsARB")'
);
expect(makeCurrent).toContain('wglMakeCurrent(dc_, context_) != TRUE');
expect(makeCurrent).toContain('return false;');
const unbindIndex = destroy.indexOf(
'wglMakeCurrent(nullptr, nullptr) != TRUE'
);
const deleteIndex = destroy.indexOf(
'wglDeleteContext(context_) != TRUE'
);
expect(unbindIndex).toBeGreaterThanOrEqual(0);
expect(deleteIndex).toBeGreaterThan(unbindIndex);
expect(destroy.slice(0, deleteIndex)).toContain('return;');
});
it('initializes the Windows QPC frequency without a cross-thread data race', () => {
const nowNs = sourceFunctionBody(
frameShmSource,
'static inline uint64_t frame_shm_now_ns(void)',
'frame_shm_now_ns'
);
expect(nowNs).toContain(
'static const uint64_t frequency = []() -> uint64_t'
);
expect(nowNs).not.toContain('static uint64_t frequency;');
});
it('resolves linked core GL symbols before falling back to EGL extension lookup', () => {
const dlsymIndex = linuxFrameHelperGlSource.indexOf(
'dlsym(RTLD_DEFAULT, name)'
@@ -111,9 +111,9 @@ export class EmbeddedMpvNativeService {
/**
* Frame-copy engine: helper process + shm ring + renderer canvas.
* 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.
* Experimental, macOS Apple Silicon (owner decision 2026-07-10), Linux
* and Windows, 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,9 +215,9 @@ export class EmbeddedMpvNativeService {
};
}
// 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
// The frame-copy engine renders offscreen (headless EGL on Linux,
// WGL on Windows) 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.