mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
fix(embedded-mpv): harden Windows frame-copy port
This commit is contained in:
1 parent
1a0e03cb93
commit
972c4cc3b7
11 files changed
+609
-110
No files matched your search
@@ -617,7 +617,7 @@ This project uses modern Angular signal-based APIs and patterns. **ALWAYS** use
|
||||
- Built-in HTML5 player with HLS.js or Video.js
|
||||
- External players: MPV, VLC (via IPC to Electron backend)
|
||||
- Embedded MPV (experimental, macOS/Windows/Linux): renders mpv video inside the Electron window through a native addon. macOS uses the libmpv render API in an `NSOpenGLView`; Windows uses in-process libmpv with `--wid` against an app-owned child `HWND`; Linux spawns an out-of-process `mpv --wid=<x11-window>` controlled over a JSON IPC socket (X11/XWayland only, requires system `mpv` on PATH; subtitles/speed/aspect/recording are not exported there). mpv's own screensaver inhibition does not apply to any of these paths, so `EmbeddedMpvNativeService` holds an Electron `powerSaveBlocker` (`prevent-display-sleep`) whenever any session's status is `playing`, and releases it on pause, dispose, or shutdown. Service: `apps/electron-backend/src/app/services/embedded-mpv-native.service.ts`; full architecture: `docs/architecture/embedded-mpv-native.md`.
|
||||
- Embedded MPV frame-copy engine (experimental, macOS Apple Silicon + Linux + Windows; enabled via `Settings > Playback > Embedded MPV: frame-copy engine` (restart required) or `IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY=1` on top of the embedded MPV experiment flag): a per-session helper renders mpv offscreen at viewport size (headless CGL on macOS, headless EGL on Linux, WGL against a hidden window on Windows) and publishes BGRA frames into a shm ring (POSIX shm; a `Local\` named file mapping on Windows); the preload frame pump uploads them onto a renderer `<canvas data-embedded-mpv-frame>`, so controls/dialogs are ordinary DOM above the video. Stored and explicit opt-ins relax the sandbox only while the base embedded-MPV feature is enabled and a platform-supported packaged runtime contains both the regular-file helper (`iptvnator_mpv_helper` / `.exe`) and readable regular frame-reader addon; packaged discovery is restricted to packaged resources. A disabled base experiment keeps embedded MPV unavailable with the sandbox intact, while a missing, mode-stripped, or incomplete frame-copy runtime falls back to the native engine without relaxing the sandbox. On Linux the engine is dev-build-only for now: the helper links system libmpv (build deps: `libmpv-dev`, `libegl-dev`, `libgl-dev`, `libopengl-dev`, `libgbm-dev`) and is stripped from packages until bundled-runtime staging lands. On Windows the helper links vendored libmpv and packages must place an accepted mpv DLL beside the helper executable. Adapter: `apps/electron-backend/src/app/services/embedded-mpv-frame-copy.adapter.ts`; helper: `apps/electron-backend/native/helper/`; details in `docs/architecture/embedded-mpv-native.md` ("Frame-Copy Engine").
|
||||
- Embedded MPV frame-copy engine (experimental, macOS Apple Silicon + Linux + Windows; enabled via `Settings > Playback > Embedded MPV: frame-copy engine` (restart required) or `IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY=1` on top of the embedded MPV experiment flag): a per-session helper renders mpv offscreen at viewport size (headless CGL on macOS, headless EGL on Linux, WGL against a hidden window on Windows) and publishes BGRA frames into a shm ring (POSIX shm; a `Local\` named file mapping on Windows); the preload frame pump uploads them onto a renderer `<canvas data-embedded-mpv-frame>`, so controls/dialogs are ordinary DOM above the video. Stored and explicit opt-ins relax the sandbox only while the base embedded-MPV feature is enabled and a platform-supported packaged runtime contains both the regular-file helper (`iptvnator_mpv_helper` / `.exe`) and readable regular frame-reader addon; packaged discovery is restricted to packaged resources. A disabled base experiment keeps embedded MPV unavailable with the sandbox intact, while a missing, mode-stripped, or incomplete frame-copy runtime falls back to the native engine without relaxing the sandbox. On Linux the engine is dev-build-only for now: the helper links system libmpv (build deps: `libmpv-dev`, `libegl-dev`, `libgl-dev`, `libopengl-dev`, `libgbm-dev`) and is stripped from packages until bundled-runtime staging lands. On Windows the helper links vendored libmpv and package validation requires the exact MPV DLL named in the helper's PE import table beside the executable. Adapter: `apps/electron-backend/src/app/services/embedded-mpv-frame-copy.adapter.ts`; helper: `apps/electron-backend/native/helper/`; details in `docs/architecture/embedded-mpv-native.md` ("Frame-Copy Engine").
|
||||
|
||||
**VOD/Series Detail Pages (two-state layout)**:
|
||||
|
||||
|
||||
@@ -666,14 +666,19 @@ public:
|
||||
return false;
|
||||
}
|
||||
if (wglMakeCurrent(dc_, legacy) != TRUE) {
|
||||
wglDeleteContext(legacy);
|
||||
if (wglDeleteContext(legacy) != TRUE) {
|
||||
std::fprintf(stderr,
|
||||
"wgl create: failed to delete unbound legacy "
|
||||
"context\n");
|
||||
}
|
||||
errorOut = "failed to bind the legacy WGL context";
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto createContextAttribs =
|
||||
reinterpret_cast<FrameHelperWglCreateContextAttribsFn>(
|
||||
wglGetProcAddress("wglCreateContextAttribsARB"));
|
||||
wglLoaderGetProcAddress(opengl32_,
|
||||
"wglCreateContextAttribsARB"));
|
||||
if (createContextAttribs) {
|
||||
const int attribs[] = {
|
||||
WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
|
||||
@@ -683,9 +688,29 @@ public:
|
||||
};
|
||||
HGLRC core = createContextAttribs(dc_, nullptr, attribs);
|
||||
if (core) {
|
||||
wglMakeCurrent(dc_, core);
|
||||
wglDeleteContext(legacy);
|
||||
context_ = core;
|
||||
if (wglMakeCurrent(dc_, core) != TRUE) {
|
||||
if (wglDeleteContext(core) != TRUE) {
|
||||
std::fprintf(
|
||||
stderr,
|
||||
"wgl create: failed to delete unbound core "
|
||||
"context\n");
|
||||
}
|
||||
if (wglGetCurrentContext() != legacy &&
|
||||
wglMakeCurrent(dc_, legacy) != TRUE) {
|
||||
errorOut =
|
||||
"failed to restore the legacy WGL context";
|
||||
return false;
|
||||
}
|
||||
context_ = legacy;
|
||||
} else {
|
||||
context_ = core;
|
||||
if (wglDeleteContext(legacy) != TRUE) {
|
||||
std::fprintf(
|
||||
stderr,
|
||||
"wgl create: failed to delete superseded legacy "
|
||||
"context\n");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Pre-3.2 driver: keep the legacy (compatibility) context;
|
||||
* the entry-point loading below decides adequacy. */
|
||||
@@ -708,20 +733,43 @@ public:
|
||||
#undef FRAME_HELPER_WGL_LOAD
|
||||
if (!loaded) {
|
||||
errorOut = std::string("missing GL entry point: ") + missing;
|
||||
wglMakeCurrent(nullptr, nullptr);
|
||||
if (wglMakeCurrent(nullptr, nullptr) != TRUE) {
|
||||
errorOut += "; failed to release the WGL context";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Unbind before returning — see the threading contract above. */
|
||||
wglMakeCurrent(nullptr, nullptr);
|
||||
if (wglMakeCurrent(nullptr, nullptr) != TRUE) {
|
||||
errorOut =
|
||||
"failed to release the WGL context for render-thread handoff";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void makeCurrent() { wglMakeCurrent(dc_, context_); }
|
||||
bool makeCurrent(std::string& errorOut) {
|
||||
if (!dc_ || !context_) {
|
||||
errorOut = "WGL render context is unavailable";
|
||||
return false;
|
||||
}
|
||||
if (wglMakeCurrent(dc_, context_) != TRUE) {
|
||||
errorOut = "failed to bind the WGL render context";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
wglMakeCurrent(nullptr, nullptr);
|
||||
if (context_) wglDeleteContext(context_);
|
||||
if (context_ && wglMakeCurrent(nullptr, nullptr) != TRUE) {
|
||||
std::fprintf(stderr,
|
||||
"wgl teardown: failed to release current context\n");
|
||||
return;
|
||||
}
|
||||
if (context_ && wglDeleteContext(context_) != TRUE) {
|
||||
std::fprintf(stderr, "wgl teardown: failed to delete context\n");
|
||||
return;
|
||||
}
|
||||
context_ = nullptr;
|
||||
/* The hidden window/class belong to the main thread (DestroyWindow
|
||||
* is thread-affine and this runs on the render thread); the process
|
||||
|
||||
@@ -44,14 +44,17 @@ typedef _Atomic uint64_t frame_shm_atomic_u64;
|
||||
* without overflow by splitting whole seconds from the remainder. */
|
||||
static inline uint64_t frame_shm_now_ns(void) {
|
||||
#if defined(_WIN32)
|
||||
/* QPF is fixed after boot, so the benign init race writes one value. */
|
||||
static uint64_t frequency;
|
||||
/* C++11 function-local initialization is synchronized, so the helper's
|
||||
* render/event threads cannot race while caching the boot-stable QPF. */
|
||||
static const uint64_t frequency = []() -> uint64_t {
|
||||
LARGE_INTEGER value;
|
||||
if (QueryPerformanceFrequency(&value) != TRUE ||
|
||||
value.QuadPart <= 0) {
|
||||
return 1;
|
||||
}
|
||||
return (uint64_t)value.QuadPart;
|
||||
}();
|
||||
LARGE_INTEGER counter;
|
||||
if (frequency == 0) {
|
||||
LARGE_INTEGER f;
|
||||
QueryPerformanceFrequency(&f);
|
||||
frequency = (uint64_t)f.QuadPart;
|
||||
}
|
||||
QueryPerformanceCounter(&counter);
|
||||
const uint64_t ticks = (uint64_t)counter.QuadPart;
|
||||
return (ticks / frequency) * 1000000000ull +
|
||||
|
||||
@@ -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)'
|
||||
|
||||
@@ -114,10 +114,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (
|
||||
isDesktop() &&
|
||||
(frameCopyAvailable() || form().value.embeddedMpvFrameCopy)
|
||||
) {
|
||||
@if (frameCopyAvailable() || form().value.embeddedMpvFrameCopy) {
|
||||
<div
|
||||
class="setting-item"
|
||||
data-test-id="embedded-mpv-frame-copy-setting"
|
||||
@@ -187,7 +184,9 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (supportsExternalPlayerPathSettings() && form().value.player === 'mpv') {
|
||||
@if (
|
||||
supportsExternalPlayerPathSettings() && form().value.player === 'mpv'
|
||||
) {
|
||||
<div class="setting-item">
|
||||
<div class="setting-item__meta">
|
||||
<h4>{{ 'SETTINGS.MPV_PLAYER_PATH_LABEL' | translate }}</h4>
|
||||
@@ -259,7 +258,9 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (supportsExternalPlayerPathSettings() && form().value.player === 'vlc') {
|
||||
@if (
|
||||
supportsExternalPlayerPathSettings() && form().value.player === 'vlc'
|
||||
) {
|
||||
<div class="setting-item">
|
||||
<div class="setting-item__meta">
|
||||
<h4>{{ 'SETTINGS.VLC_PLAYER_PATH_LABEL' | translate }}</h4>
|
||||
|
||||
@@ -171,47 +171,29 @@ describe('SettingsPlaybackSectionComponent', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('shows the frame-copy toggle when the engine is available on desktop', () => {
|
||||
fixture.componentRef.setInput('isDesktop', true);
|
||||
fixture.componentRef.setInput('frameCopyAvailable', true);
|
||||
fixture.detectChanges();
|
||||
it.each<[string, boolean, boolean, boolean, boolean]>([
|
||||
['shows the available desktop engine', true, true, false, true],
|
||||
['keeps a stale desktop opt-in clearable', true, false, true, true],
|
||||
['hides a stale opt-in in the PWA', false, false, true, false],
|
||||
])(
|
||||
'%s',
|
||||
(_description, isDesktop, frameCopyAvailable, stored, expected) => {
|
||||
const form = createForm();
|
||||
form.controls['embeddedMpvFrameCopy'].setValue(stored);
|
||||
fixture.componentRef.setInput('form', form);
|
||||
fixture.componentRef.setInput('isDesktop', isDesktop);
|
||||
fixture.componentRef.setInput(
|
||||
'frameCopyAvailable',
|
||||
frameCopyAvailable
|
||||
);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
fixture.nativeElement.querySelector(
|
||||
const setting = fixture.nativeElement.querySelector(
|
||||
'[data-test-id="embedded-mpv-frame-copy-setting"]'
|
||||
)
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('keeps a stale frame-copy opt-in clearable on desktop without the helper', () => {
|
||||
const form = createForm();
|
||||
form.controls['embeddedMpvFrameCopy'].setValue(true);
|
||||
fixture.componentRef.setInput('form', form);
|
||||
fixture.componentRef.setInput('isDesktop', true);
|
||||
fixture.componentRef.setInput('frameCopyAvailable', false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
fixture.nativeElement.querySelector(
|
||||
'[data-test-id="embedded-mpv-frame-copy-setting"]'
|
||||
)
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('never shows the frame-copy toggle in the PWA, even with a stored opt-in', () => {
|
||||
const form = createForm();
|
||||
form.controls['embeddedMpvFrameCopy'].setValue(true);
|
||||
fixture.componentRef.setInput('form', form);
|
||||
fixture.componentRef.setInput('isDesktop', false);
|
||||
fixture.componentRef.setInput('frameCopyAvailable', false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
fixture.nativeElement.querySelector(
|
||||
'[data-test-id="embedded-mpv-frame-copy-setting"]'
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
);
|
||||
expect(Boolean(setting)).toBe(expected);
|
||||
}
|
||||
);
|
||||
|
||||
it('shows the recording folder setting only in desktop builds', () => {
|
||||
fixture.componentRef.setInput('isDesktop', true);
|
||||
|
||||
@@ -41,7 +41,11 @@ Windows packaged runtimes must preserve the MPV DLL basename referenced by the
|
||||
import library used at native-addon link time. For example, an archive that
|
||||
ships `libmpv.dll.a` and `libmpv-2.dll` must package `libmpv-2.dll`; renaming it
|
||||
to `mpv-2.dll` leaves `embedded_mpv.node` with an unresolved DLL dependency at
|
||||
startup, so the Settings support probe hides the Embedded MPV option.
|
||||
startup, so the Settings support probe hides the Embedded MPV option. The
|
||||
frame-copy helper is a separate executable and resolves that same import from
|
||||
its own directory. Package validation therefore reads the helper's PE import
|
||||
table and requires the exact referenced MPV DLL beside
|
||||
`iptvnator_mpv_helper.exe`, not only under `native/lib/`.
|
||||
|
||||
On Linux, `embedded_mpv.node` must not link directly to `libmpv` or load libmpv in-process. Electron loads its own `libffmpeg` and Chromium graphics stack; in-process libmpv can resolve FFmpeg/GL symbols against incompatible Electron symbols, while isolated dynamic-loader namespaces introduce thread/runtime ownership problems. The Linux addon therefore owns only the X11 child-window embedding, process lifecycle, and a private MPV JSON IPC socket. It starts `mpv --wid=<window> --input-ipc-server=<socket>`, polls `time-pos`, `duration`, `volume`, and `pause`, and forwards pause/seek/volume/audio-track commands through that socket. The Linux MPV JSON IPC polling runs on an addon-owned background thread; `getSessionSnapshot()` returns the last cached snapshot and must not perform socket round trips on Electron's main thread. Linux MPV process teardown sends `SIGTERM` on the caller path, then waits and escalates to `SIGKILL` on a detached cleanup thread. A healthy Linux build lists X11/Xext as addon dependencies, but `ldd apps/electron-backend/native/build/Release/embedded_mpv.node` must not list `libmpv`. Runtime support also requires an `mpv` executable on `PATH`.
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# Frame-copy engine — Windows/Linux porting handoff
|
||||
|
||||
> Handoff for future Claude/dev sessions on Windows and Linux machines
|
||||
> (they won't have the originating Mac's local session memory — this file
|
||||
> is the transfer). Written 2026-07-11 by the macOS session that built the
|
||||
> engine; fold into DESIGN.md once both ports land.
|
||||
> Historical handoff and maintenance notes for the completed Linux and
|
||||
> Windows ports. The canonical runtime contract now lives in
|
||||
> `docs/architecture/embedded-mpv-native.md`.
|
||||
|
||||
## State as of 2026-07-15
|
||||
|
||||
@@ -70,16 +69,11 @@ twins, the TS gate in `embedded-mpv-frame-copy-platform.util.ts`, packaging.
|
||||
|
||||
## Branching & merge strategy
|
||||
|
||||
- Merge order is #1169 → #1171 → #1175. #1169 is already merged; #1171 is
|
||||
based directly on that `master`, while #1175 remains stacked on the Linux
|
||||
port until #1171 lands.
|
||||
- Rewrite only the platform-specific commit range when moving a stacked PR;
|
||||
do not replay the old parent history after its squash merge. Retarget the PR
|
||||
explicitly and keep the parent branch until its child has been rewritten.
|
||||
- Keep the stack at most one unmerged level deep. New follow-up work branches
|
||||
from the latest landed platform base on `master`.
|
||||
- Commit incrementally within the port branch; land each platform's
|
||||
measurement rows in `RESULTS.md` in the same PR as its port.
|
||||
- The platform stack landed in order #1169 → #1171 → #1175.
|
||||
- Each child was rewritten onto the latest squash-merged `master` using only
|
||||
its platform-specific commit range; the old parent history was not replayed.
|
||||
- New frame-copy work branches directly from `master`. Keep future stacks at
|
||||
most one unmerged level deep and land platform measurements with their port.
|
||||
|
||||
## Per-platform task lists
|
||||
|
||||
@@ -217,10 +211,10 @@ display-capable DC.
|
||||
`node -e "const r=require('.../embedded_mpv_frame_reader.node'); const i=r.open('/impv-t-g2'); ..."`
|
||||
→ `latestSeq()` advancing + pixel min/max spread.
|
||||
- **In-app**: `IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY=1 pnpm run
|
||||
serve:backend:embedded-mpv` or the Settings toggle (+restart). Second
|
||||
serve:backend:embedded-mpv` or the Settings toggle (+restart). Second
|
||||
parallel instance for CDP testing: build, then run
|
||||
`electron dist/apps/electron-backend/main.js --remote-debugging-port=9223
|
||||
--user-data-dir=/tmp/x` with `ELECTRON_IS_DEV=0` for the file:// renderer
|
||||
--user-data-dir=/tmp/x` with `ELECTRON_IS_DEV=0` for the file:// renderer
|
||||
(dist package.json has no `main` field — point at main.js explicitly;
|
||||
a separate user-data-dir avoids the Chromium profile singleton).
|
||||
- **Perf gate**: follow `RESULTS.md` methodology (STATS/LONGRUN lines,
|
||||
|
||||
@@ -110,7 +110,10 @@ libmpv import library, and `mpv-2.dll`/`mpv.dll` or
|
||||
(`lib/` and `bin/`) or the common `mpv-dev-lgpl` flat layout with the import
|
||||
library and DLL in the archive root. The staged runtime preserves the DLL
|
||||
basename from the archive because Windows import libraries encode the DLL name
|
||||
that `embedded_mpv.node` must load at runtime. If
|
||||
that native binaries must load at runtime. Package validation reads the
|
||||
frame-copy helper's PE imports and requires that exact DLL basename beside
|
||||
`iptvnator_mpv_helper.exe`; a different accepted MPV DLL name or a copy only
|
||||
under `native/lib/` is not sufficient. If
|
||||
`runtime-manifest.json` is missing, CI generates a minimal manifest from the
|
||||
archive URL/path and checksum; release-ready runtime archives should still
|
||||
provide full source/build metadata.
|
||||
|
||||
@@ -102,6 +102,37 @@ const embeddedMpvWin32Source = fs.readFileSync(
|
||||
);
|
||||
const { validatePackagedEmbeddedMpv } = require('./embedded-mpv-packaging.cjs');
|
||||
|
||||
function writeWindowsHelperFixture(helperPath, importedDllName) {
|
||||
const peOffset = 0x80;
|
||||
const optionalHeaderOffset = peOffset + 24;
|
||||
const optionalHeaderSize = 0xf0;
|
||||
const sectionTableOffset = optionalHeaderOffset + optionalHeaderSize;
|
||||
const importRva = 0x1000;
|
||||
const importRawOffset = 0x200;
|
||||
const importNameOffset = 0x30;
|
||||
const image = Buffer.alloc(0x400);
|
||||
|
||||
image.write('MZ', 0, 'ascii');
|
||||
image.writeUInt32LE(peOffset, 0x3c);
|
||||
image.write('PE\0\0', peOffset, 'ascii');
|
||||
image.writeUInt16LE(0x8664, peOffset + 4);
|
||||
image.writeUInt16LE(1, peOffset + 6);
|
||||
image.writeUInt16LE(optionalHeaderSize, peOffset + 20);
|
||||
image.writeUInt16LE(0x20b, optionalHeaderOffset);
|
||||
image.writeUInt32LE(0x200, optionalHeaderOffset + 60);
|
||||
image.writeUInt32LE(16, optionalHeaderOffset + 108);
|
||||
image.writeUInt32LE(importRva, optionalHeaderOffset + 120);
|
||||
image.writeUInt32LE(40, optionalHeaderOffset + 124);
|
||||
image.write('.idata\0\0', sectionTableOffset, 'ascii');
|
||||
image.writeUInt32LE(0x200, sectionTableOffset + 8);
|
||||
image.writeUInt32LE(importRva, sectionTableOffset + 12);
|
||||
image.writeUInt32LE(0x200, sectionTableOffset + 16);
|
||||
image.writeUInt32LE(importRawOffset, sectionTableOffset + 20);
|
||||
image.writeUInt32LE(importRva + importNameOffset, importRawOffset + 12);
|
||||
image.write(`${importedDllName}\0`, importRawOffset + importNameOffset);
|
||||
fs.writeFileSync(helperPath, image);
|
||||
}
|
||||
|
||||
test('Linux package identity does not expose the internal Electron backend project name', () => {
|
||||
assert.equal(electronBuilderConfig.productName, 'IPTVnator');
|
||||
assert.equal(electronBuilderConfig.extraMetadata?.name, 'iptvnator');
|
||||
@@ -306,8 +337,8 @@ test('embedded MPV package validation accepts Windows runtime files and Linux pr
|
||||
for (const [platform, runtimeFile] of [
|
||||
['windows', 'mpv-2.dll'],
|
||||
['windows', 'libmpv-2.dll'],
|
||||
['windows', join('lib', 'mpv.dll')],
|
||||
['windows', join('lib', 'libmpv.dll')],
|
||||
['windows', 'mpv.dll'],
|
||||
['windows', 'libmpv.dll'],
|
||||
]) {
|
||||
// One fixture dir per runtime-file scenario: the frame-copy
|
||||
// artifacts written below must not leak into the next
|
||||
@@ -347,7 +378,10 @@ test('embedded MPV package validation accepts Windows runtime files and Linux pr
|
||||
)
|
||||
);
|
||||
|
||||
fs.writeFileSync(join(nativeDir, 'iptvnator_mpv_helper.exe'), '');
|
||||
writeWindowsHelperFixture(
|
||||
join(nativeDir, 'iptvnator_mpv_helper.exe'),
|
||||
runtimeFile
|
||||
);
|
||||
fs.writeFileSync(
|
||||
join(nativeDir, 'embedded_mpv_frame_reader.node'),
|
||||
''
|
||||
@@ -402,10 +436,10 @@ test('embedded MPV package validation accepts Windows runtime files and Linux pr
|
||||
// Host-agnostic assertion: on non-macOS hosts the validator also
|
||||
// reports that link validation needs a macOS host, so only the
|
||||
// frame-copy artifact requirement is asserted here.
|
||||
const remainingErrors = validatePackagedEmbeddedMpv(
|
||||
darwinResourceDir,
|
||||
{ platform: 'darwin', required: true }
|
||||
);
|
||||
const remainingErrors = validatePackagedEmbeddedMpv(darwinResourceDir, {
|
||||
platform: 'darwin',
|
||||
required: true,
|
||||
});
|
||||
assert.ok(
|
||||
!remainingErrors.some((error) =>
|
||||
error.includes('frame-copy artifact')
|
||||
@@ -439,6 +473,138 @@ test('embedded MPV package validation accepts Windows runtime files and Linux pr
|
||||
}
|
||||
});
|
||||
|
||||
test('Windows frame-copy packages reject an mpv DLL that exists only under native/lib', () => {
|
||||
const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-'));
|
||||
|
||||
try {
|
||||
for (const runtimeFile of [
|
||||
'mpv-2.dll',
|
||||
'libmpv-2.dll',
|
||||
'mpv.dll',
|
||||
'libmpv.dll',
|
||||
]) {
|
||||
const resourceDir = join(tempDir, runtimeFile);
|
||||
const nativeDir = join(
|
||||
resourceDir,
|
||||
'app.asar.unpacked',
|
||||
'electron-backend',
|
||||
'native'
|
||||
);
|
||||
fs.mkdirSync(join(nativeDir, 'lib'), { recursive: true });
|
||||
fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), '');
|
||||
fs.writeFileSync(
|
||||
join(nativeDir, 'embedded-mpv-runtime.json'),
|
||||
JSON.stringify({ origin: 'vendored-lgpl' })
|
||||
);
|
||||
writeWindowsHelperFixture(
|
||||
join(nativeDir, 'iptvnator_mpv_helper.exe'),
|
||||
runtimeFile
|
||||
);
|
||||
fs.writeFileSync(
|
||||
join(nativeDir, 'embedded_mpv_frame_reader.node'),
|
||||
''
|
||||
);
|
||||
fs.writeFileSync(join(nativeDir, 'lib', runtimeFile), '');
|
||||
|
||||
const errors = validatePackagedEmbeddedMpv(resourceDir, {
|
||||
platform: 'windows',
|
||||
required: true,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
errors.some(
|
||||
(error) =>
|
||||
error.includes(
|
||||
'beside the Windows frame-copy helper'
|
||||
) && error.includes(nativeDir)
|
||||
),
|
||||
`${runtimeFile} under native/lib must not satisfy the helper DLL requirement: ${errors.join('; ')}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('Windows frame-copy packages require the DLL imported by the helper', () => {
|
||||
const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-'));
|
||||
|
||||
try {
|
||||
const nativeDir = join(
|
||||
tempDir,
|
||||
'app.asar.unpacked',
|
||||
'electron-backend',
|
||||
'native'
|
||||
);
|
||||
fs.mkdirSync(nativeDir, { recursive: true });
|
||||
fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), '');
|
||||
fs.writeFileSync(
|
||||
join(nativeDir, 'embedded-mpv-runtime.json'),
|
||||
JSON.stringify({ origin: 'vendored-lgpl' })
|
||||
);
|
||||
writeWindowsHelperFixture(
|
||||
join(nativeDir, 'iptvnator_mpv_helper.exe'),
|
||||
'mpv-2.dll'
|
||||
);
|
||||
fs.writeFileSync(join(nativeDir, 'embedded_mpv_frame_reader.node'), '');
|
||||
fs.writeFileSync(join(nativeDir, 'libmpv.dll'), '');
|
||||
|
||||
const errors = validatePackagedEmbeddedMpv(tempDir, {
|
||||
platform: 'windows',
|
||||
required: true,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
errors.some(
|
||||
(error) =>
|
||||
error.includes('imports mpv-2.dll') &&
|
||||
error.includes(join(nativeDir, 'mpv-2.dll'))
|
||||
),
|
||||
`a different accepted DLL must not satisfy the helper import: ${errors.join('; ')}`
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('Windows frame-copy package validation fails closed for a malformed helper', () => {
|
||||
const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-'));
|
||||
|
||||
try {
|
||||
const nativeDir = join(
|
||||
tempDir,
|
||||
'app.asar.unpacked',
|
||||
'electron-backend',
|
||||
'native'
|
||||
);
|
||||
fs.mkdirSync(nativeDir, { recursive: true });
|
||||
fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), '');
|
||||
fs.writeFileSync(
|
||||
join(nativeDir, 'embedded-mpv-runtime.json'),
|
||||
JSON.stringify({ origin: 'vendored-lgpl' })
|
||||
);
|
||||
fs.writeFileSync(join(nativeDir, 'iptvnator_mpv_helper.exe'), 'MZ');
|
||||
fs.writeFileSync(join(nativeDir, 'embedded_mpv_frame_reader.node'), '');
|
||||
fs.writeFileSync(join(nativeDir, 'mpv-2.dll'), '');
|
||||
|
||||
const errors = validatePackagedEmbeddedMpv(tempDir, {
|
||||
platform: 'windows',
|
||||
required: true,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
errors.some((error) =>
|
||||
error.includes(
|
||||
'Unable to inspect Windows frame-copy helper imports'
|
||||
)
|
||||
),
|
||||
`malformed helper must fail package validation: ${errors.join('; ')}`
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('Windows embedded MPV staging preserves import-library DLL basenames', () => {
|
||||
assert.match(
|
||||
embeddedMpvStageRuntimeSource,
|
||||
@@ -503,10 +669,7 @@ test('frame-copy packaging file operations enforce modes and remove stale artifa
|
||||
|
||||
try {
|
||||
const helperPath = join(tempDir, 'iptvnator_mpv_helper');
|
||||
const windowsHelperPath = join(
|
||||
tempDir,
|
||||
'iptvnator_mpv_helper.exe'
|
||||
);
|
||||
const windowsHelperPath = join(tempDir, 'iptvnator_mpv_helper.exe');
|
||||
const readerPath = join(tempDir, 'embedded_mpv_frame_reader.node');
|
||||
fs.writeFileSync(helperPath, '#!/bin/sh\n');
|
||||
fs.chmodSync(helperPath, 0o644);
|
||||
|
||||
@@ -4,6 +4,12 @@ const { spawnSync } = require('child_process');
|
||||
|
||||
const forbiddenRuntimePathPrefixes = ['/opt/homebrew/', '/usr/local/'];
|
||||
const systemRuntimePathPrefixes = ['/System/Library/', '/usr/lib/'];
|
||||
const windowsMpvRuntimeNames = [
|
||||
'mpv-2.dll',
|
||||
'libmpv-2.dll',
|
||||
'mpv.dll',
|
||||
'libmpv.dll',
|
||||
];
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
@@ -67,6 +73,177 @@ function listRuntimeFiles(directoryPath) {
|
||||
.sort();
|
||||
}
|
||||
|
||||
function assertPeRange(image, offset, size, label) {
|
||||
if (
|
||||
!Number.isSafeInteger(offset) ||
|
||||
!Number.isSafeInteger(size) ||
|
||||
offset < 0 ||
|
||||
size < 0 ||
|
||||
offset + size > image.length
|
||||
) {
|
||||
throw new Error(`Invalid PE image: ${label} is out of range.`);
|
||||
}
|
||||
}
|
||||
|
||||
function readPeImportedDllNames(binaryPath) {
|
||||
const image = fs.readFileSync(binaryPath);
|
||||
assertPeRange(image, 0, 0x40, 'DOS header');
|
||||
if (image.toString('ascii', 0, 2) !== 'MZ') {
|
||||
throw new Error('Invalid PE image: missing DOS signature.');
|
||||
}
|
||||
|
||||
const peOffset = image.readUInt32LE(0x3c);
|
||||
assertPeRange(image, peOffset, 24, 'PE header');
|
||||
if (image.toString('ascii', peOffset, peOffset + 4) !== 'PE\0\0') {
|
||||
throw new Error('Invalid PE image: missing PE signature.');
|
||||
}
|
||||
|
||||
const sectionCount = image.readUInt16LE(peOffset + 6);
|
||||
const optionalHeaderSize = image.readUInt16LE(peOffset + 20);
|
||||
const optionalHeaderOffset = peOffset + 24;
|
||||
assertPeRange(
|
||||
image,
|
||||
optionalHeaderOffset,
|
||||
optionalHeaderSize,
|
||||
'optional header'
|
||||
);
|
||||
|
||||
const optionalMagic = image.readUInt16LE(optionalHeaderOffset);
|
||||
const dataDirectoryOffset =
|
||||
optionalMagic === 0x20b
|
||||
? optionalHeaderOffset + 112
|
||||
: optionalMagic === 0x10b
|
||||
? optionalHeaderOffset + 96
|
||||
: null;
|
||||
const directoryCountOffset =
|
||||
optionalMagic === 0x20b
|
||||
? optionalHeaderOffset + 108
|
||||
: optionalMagic === 0x10b
|
||||
? optionalHeaderOffset + 92
|
||||
: null;
|
||||
if (dataDirectoryOffset === null || directoryCountOffset === null) {
|
||||
throw new Error(
|
||||
`Invalid PE image: unsupported optional-header magic 0x${optionalMagic.toString(16)}.`
|
||||
);
|
||||
}
|
||||
const optionalHeaderEnd = optionalHeaderOffset + optionalHeaderSize;
|
||||
if (
|
||||
directoryCountOffset + 4 > optionalHeaderEnd ||
|
||||
dataDirectoryOffset + 16 > optionalHeaderEnd
|
||||
) {
|
||||
throw new Error(
|
||||
'Invalid PE image: data directories exceed the optional header.'
|
||||
);
|
||||
}
|
||||
assertPeRange(image, directoryCountOffset, 4, 'data-directory count');
|
||||
if (image.readUInt32LE(directoryCountOffset) < 2) {
|
||||
return [];
|
||||
}
|
||||
assertPeRange(image, dataDirectoryOffset + 8, 8, 'import directory');
|
||||
|
||||
const importRva = image.readUInt32LE(dataDirectoryOffset + 8);
|
||||
const importSize = image.readUInt32LE(dataDirectoryOffset + 12);
|
||||
if (importRva === 0 && importSize === 0) {
|
||||
return [];
|
||||
}
|
||||
if (importRva === 0 || importSize < 20) {
|
||||
throw new Error('Invalid PE image: malformed import directory.');
|
||||
}
|
||||
|
||||
const sectionTableOffset = optionalHeaderOffset + optionalHeaderSize;
|
||||
assertPeRange(
|
||||
image,
|
||||
sectionTableOffset,
|
||||
sectionCount * 40,
|
||||
'section table'
|
||||
);
|
||||
const sections = Array.from({ length: sectionCount }, (_, index) => {
|
||||
const offset = sectionTableOffset + index * 40;
|
||||
return {
|
||||
virtualSize: image.readUInt32LE(offset + 8),
|
||||
virtualAddress: image.readUInt32LE(offset + 12),
|
||||
rawSize: image.readUInt32LE(offset + 16),
|
||||
rawOffset: image.readUInt32LE(offset + 20),
|
||||
};
|
||||
});
|
||||
|
||||
const rvaToLocation = (rva, label) => {
|
||||
const section = sections.find(
|
||||
(candidate) =>
|
||||
rva >= candidate.virtualAddress &&
|
||||
rva <
|
||||
candidate.virtualAddress +
|
||||
Math.max(candidate.virtualSize, candidate.rawSize)
|
||||
);
|
||||
if (!section) {
|
||||
throw new Error(
|
||||
`Invalid PE image: ${label} RVA 0x${rva.toString(16)} is not mapped.`
|
||||
);
|
||||
}
|
||||
|
||||
const sectionOffset = rva - section.virtualAddress;
|
||||
if (sectionOffset >= section.rawSize) {
|
||||
throw new Error(
|
||||
`Invalid PE image: ${label} is outside section file data.`
|
||||
);
|
||||
}
|
||||
const offset = section.rawOffset + sectionOffset;
|
||||
assertPeRange(image, offset, 1, label);
|
||||
return {
|
||||
offset,
|
||||
sectionEnd: section.rawOffset + section.rawSize,
|
||||
};
|
||||
};
|
||||
|
||||
const importedDllNames = [];
|
||||
const descriptorCount = Math.floor(importSize / 20);
|
||||
let foundTerminator = false;
|
||||
for (let index = 0; index < descriptorCount; index += 1) {
|
||||
const descriptorLocation = rvaToLocation(
|
||||
importRva + index * 20,
|
||||
'import descriptor'
|
||||
);
|
||||
const descriptorOffset = descriptorLocation.offset;
|
||||
if (descriptorOffset + 20 > descriptorLocation.sectionEnd) {
|
||||
throw new Error(
|
||||
'Invalid PE image: import descriptor crosses section file data.'
|
||||
);
|
||||
}
|
||||
assertPeRange(image, descriptorOffset, 20, 'import descriptor');
|
||||
const descriptor = image.subarray(
|
||||
descriptorOffset,
|
||||
descriptorOffset + 20
|
||||
);
|
||||
if (descriptor.every((value) => value === 0)) {
|
||||
foundTerminator = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const nameRva = image.readUInt32LE(descriptorOffset + 12);
|
||||
const nameLocation = rvaToLocation(nameRva, 'imported DLL name');
|
||||
const nameOffset = nameLocation.offset;
|
||||
const nameEnd = image.indexOf(0, nameOffset);
|
||||
if (nameEnd < 0 || nameEnd >= nameLocation.sectionEnd) {
|
||||
throw new Error(
|
||||
'Invalid PE image: imported DLL name is not terminated.'
|
||||
);
|
||||
}
|
||||
const name = image.toString('ascii', nameOffset, nameEnd);
|
||||
if (!name) {
|
||||
throw new Error('Invalid PE image: imported DLL name is empty.');
|
||||
}
|
||||
importedDllNames.push(name);
|
||||
}
|
||||
|
||||
if (!foundTerminator) {
|
||||
throw new Error(
|
||||
'Invalid PE image: import descriptor table has no terminator.'
|
||||
);
|
||||
}
|
||||
|
||||
return [...new Set(importedDllNames)];
|
||||
}
|
||||
|
||||
function parseOtoolDependencies(binaryPath) {
|
||||
const output = run('otool', ['-L', binaryPath]);
|
||||
return output
|
||||
@@ -426,16 +603,12 @@ function getPackagedRuntimeCandidates(libDir, platform, nativeDir) {
|
||||
path.join(libDir, 'libmpv.dylib'),
|
||||
];
|
||||
case 'win32':
|
||||
return [
|
||||
nativeDir ? path.join(nativeDir, 'mpv-2.dll') : null,
|
||||
nativeDir ? path.join(nativeDir, 'libmpv-2.dll') : null,
|
||||
nativeDir ? path.join(nativeDir, 'mpv.dll') : null,
|
||||
nativeDir ? path.join(nativeDir, 'libmpv.dll') : null,
|
||||
path.join(libDir, 'mpv-2.dll'),
|
||||
path.join(libDir, 'libmpv-2.dll'),
|
||||
path.join(libDir, 'mpv.dll'),
|
||||
path.join(libDir, 'libmpv.dll'),
|
||||
].filter(Boolean);
|
||||
return windowsMpvRuntimeNames.flatMap((name) =>
|
||||
[
|
||||
nativeDir ? path.join(nativeDir, name) : null,
|
||||
path.join(libDir, name),
|
||||
].filter(Boolean)
|
||||
);
|
||||
case 'linux':
|
||||
return [];
|
||||
default:
|
||||
@@ -480,7 +653,11 @@ function getEmbeddedMpvAddonArch(env = process.env) {
|
||||
* dist output. Those packages must not ship a foreign-architecture
|
||||
* `embedded_mpv.node` — it can never load and produces a cryptic error.
|
||||
*/
|
||||
function isForeignLinuxEmbeddedMpvArch(platform, targetArch, env = process.env) {
|
||||
function isForeignLinuxEmbeddedMpvArch(
|
||||
platform,
|
||||
targetArch,
|
||||
env = process.env
|
||||
) {
|
||||
if (normalizeEmbeddedMpvPlatform(platform) !== 'linux') {
|
||||
return false;
|
||||
}
|
||||
@@ -560,6 +737,77 @@ function validatePackagedEmbeddedMpv(resourceDir, options = {}) {
|
||||
`Missing embedded MPV frame-copy artifact: ${artifactPath}`
|
||||
)
|
||||
);
|
||||
|
||||
if (platform === 'win32') {
|
||||
// The helper is a separate executable. Windows resolves its
|
||||
// imported libmpv DLL from the executable directory, so a copy
|
||||
// under native/lib may satisfy addon bookkeeping but cannot
|
||||
// start iptvnator_mpv_helper.exe.
|
||||
const helperPath = path.join(
|
||||
unpackedNativeDir,
|
||||
'iptvnator_mpv_helper.exe'
|
||||
);
|
||||
const helperRuntimeCandidates = windowsMpvRuntimeNames.map((name) =>
|
||||
path.join(unpackedNativeDir, name)
|
||||
);
|
||||
if (
|
||||
!helperRuntimeCandidates.some((candidate) =>
|
||||
fs.existsSync(candidate)
|
||||
)
|
||||
) {
|
||||
errors.push(
|
||||
[
|
||||
`Missing bundled MPV DLL beside the Windows frame-copy helper in ${unpackedNativeDir}.`,
|
||||
'Expected one of:',
|
||||
...helperRuntimeCandidates.map(
|
||||
(candidate) => `- ${candidate}`
|
||||
),
|
||||
].join('\n')
|
||||
);
|
||||
}
|
||||
|
||||
if (fs.existsSync(helperPath)) {
|
||||
try {
|
||||
const acceptedRuntimeNames = new Set(
|
||||
windowsMpvRuntimeNames.map((name) => name.toLowerCase())
|
||||
);
|
||||
const importedRuntimeNames = readPeImportedDllNames(
|
||||
helperPath
|
||||
).filter((name) =>
|
||||
acceptedRuntimeNames.has(name.toLowerCase())
|
||||
);
|
||||
if (importedRuntimeNames.length === 0) {
|
||||
errors.push(
|
||||
`Windows frame-copy helper does not import a supported MPV DLL: ${helperPath}`
|
||||
);
|
||||
}
|
||||
|
||||
const packagedRuntimeNames = new Set(
|
||||
fs
|
||||
.readdirSync(unpackedNativeDir, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => entry.name.toLowerCase())
|
||||
);
|
||||
for (const importedRuntimeName of importedRuntimeNames) {
|
||||
if (
|
||||
!packagedRuntimeNames.has(
|
||||
importedRuntimeName.toLowerCase()
|
||||
)
|
||||
) {
|
||||
errors.push(
|
||||
`Windows frame-copy helper imports ${importedRuntimeName}, but the matching DLL is missing beside it: ${path.join(unpackedNativeDir, importedRuntimeName)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Unable to inspect Windows frame-copy helper imports at ${helperPath}: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
|
||||
Reference in new issue
Block a user