mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
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:
1 parent
91e5a88a5b
commit
5006858844
4 files changed
+89
-1
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 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)**:
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user