feat(embedded-mpv): require frame-copy artifacts in macOS package validation + docs

macOS packages that ship embedded_mpv.node must also ship the
iptvnator_mpv_helper binary and the embedded_mpv_frame_reader.node addon —
they come out of the same binding.gyp run, and a package missing them would
silently lose the frame-copy engine. Covered in the package-identity test.
Architecture doc and CLAUDE.md document the Settings toggle, aspect-fit
rendering, audio-delay passthrough, and the renderer-reload session reaping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-07-15 18:20:45 +02:00
1 parent 91e5a88a5b
commit 5006858844
4 files changed
+89 -1

No files matched your search

+1 -1
View File
@@ -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 only, `IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY=1` on top of the embedded MPV experiment flag): a per-session `iptvnator_mpv_helper` process renders mpv offscreen at viewport size and publishes BGRA frames into a shm ring; the preload frame pump uploads them onto a renderer `<canvas data-embedded-mpv-frame>`, so controls/dialogs are ordinary DOM above the video (no native-surface compositing workarounds; the flag relaxes the window sandbox for the preload's native reader addon). 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 only; 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 `iptvnator_mpv_helper` process renders mpv offscreen at viewport size and publishes BGRA frames into a shm ring; the preload frame pump uploads them onto a renderer `<canvas data-embedded-mpv-frame>`, so controls/dialogs are ordinary DOM above the video (no native-surface compositing workarounds; the flag relaxes the window sandbox for the preload's native reader addon). 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)**:
+25
View File
@@ -124,6 +124,31 @@ engine that replaces the native-view compositing entirely:
runs: the helper re-renders at the new viewport size (device pixels via
the display scale factor).
Enabling it: the `Settings > Playback > Embedded MPV: frame-copy engine`
checkbox (shown only when support reports `frameCopyAvailable`) persists to
the main-process config store (`electron-conf`), which `main.ts` reads
before creating the window and translates into the env flag; an explicitly
set env var (including `0`) always wins. Changing the toggle requires an
app restart because the sandbox relaxation is fixed at window creation.
Rendering size: the helper renders at the **aspect-fit** size of the video
(observed `dwidth`/`dheight`) inside the requested viewport and bumps a shm
generation when it changes — letterbox bars are never baked into frames,
frames stay as small as possible, and the canvas letterboxes with a
transparent background (app surface shows at the sides; fullscreen keeps a
black backdrop). Snapshots carry `videoWidth`/`videoHeight`.
`IPTVNATOR_EMBEDDED_MPV_AUDIO_DELAY=<seconds>` passes through to mpv's
`audio-delay` for lip-sync tuning until a calibration flow exists.
Lifecycle safety: `EmbeddedMpvNativeService` watches the main window for
`render-process-gone` and `did-navigate` (full reloads) and disposes every
session — Angular teardown never runs on a renderer crash/hard reload, and
without the watch helper processes (or native mpv handles) would leak until
app shutdown. Unexpected helper exits surface as a session `error`. macOS
package validation requires `iptvnator_mpv_helper` and
`embedded_mpv_frame_reader.node` next to the addon whenever the addon
ships.
Trade-offs and constraints:
- The experiment flag relaxes the BrowserWindow sandbox (preload must
@@ -327,6 +327,51 @@ test('embedded MPV package validation accepts Windows runtime files and Linux pr
);
}
const darwinResourceDir = join(tempDir, 'darwin');
const darwinNativeDir = join(
darwinResourceDir,
'app.asar.unpacked',
'electron-backend',
'native'
);
fs.mkdirSync(join(darwinNativeDir, 'lib'), { recursive: true });
fs.writeFileSync(join(darwinNativeDir, 'embedded_mpv.node'), '');
fs.writeFileSync(
join(darwinNativeDir, 'embedded-mpv-runtime.json'),
JSON.stringify({ origin: 'vendored-lgpl' })
);
fs.writeFileSync(join(darwinNativeDir, 'lib', 'libmpv.2.dylib'), '');
// macOS packages that ship the addon must also ship the frame-copy
// engine artifacts built by the same binding.gyp run.
const missingFrameCopyErrors = validatePackagedEmbeddedMpv(
darwinResourceDir,
{ platform: 'darwin', required: true }
);
assert.ok(
missingFrameCopyErrors.some((error) =>
error.includes('iptvnator_mpv_helper')
)
);
assert.ok(
missingFrameCopyErrors.some((error) =>
error.includes('embedded_mpv_frame_reader.node')
)
);
fs.writeFileSync(join(darwinNativeDir, 'iptvnator_mpv_helper'), '');
fs.writeFileSync(
join(darwinNativeDir, 'embedded_mpv_frame_reader.node'),
''
);
assert.deepEqual(
validatePackagedEmbeddedMpv(darwinResourceDir, {
platform: 'darwin',
required: true,
}),
[]
);
const linuxResourceDir = join(tempDir, 'linux');
const linuxNativeDir = join(
linuxResourceDir,
@@ -540,6 +540,24 @@ function validatePackagedEmbeddedMpv(resourceDir, options = {}) {
return errors;
}
if (platform === 'darwin') {
// The frame-copy engine artifacts are built by the same binding.gyp
// run as the addon; a macOS package that ships the addon without
// them would silently lose the engine (support probe hides it).
const missingFrameCopyArtifacts = [
'iptvnator_mpv_helper',
'embedded_mpv_frame_reader.node',
]
.map((name) => path.join(unpackedNativeDir, name))
.filter((artifactPath) => !fs.existsSync(artifactPath));
errors.push(
...missingFrameCopyArtifacts.map(
(artifactPath) =>
`Missing embedded MPV frame-copy artifact: ${artifactPath}`
)
);
}
if (!fs.existsSync(manifestPath)) {
errors.push(`Missing embedded MPV runtime manifest: ${manifestPath}`);
} else {