diff --git a/.github/workflows/build-and-make.yaml b/.github/workflows/build-and-make.yaml index 9032775f1..e5914bbe5 100644 --- a/.github/workflows/build-and-make.yaml +++ b/.github/workflows/build-and-make.yaml @@ -68,7 +68,7 @@ jobs: if: matrix.os == 'linux' run: | sudo apt-get update - sudo apt-get install --no-install-recommends -y rpm libarchive-tools flatpak flatpak-builder appstream libx11-dev libxext-dev libmpv-dev mpv pkg-config libegl-dev libgl-dev libgbm-dev + sudo apt-get install --no-install-recommends -y rpm libarchive-tools flatpak flatpak-builder appstream libx11-dev libxext-dev libmpv-dev mpv pkg-config libegl-dev libgl-dev libopengl-dev libgbm-dev # Configure Flatpak # 1. Add the Flathub repository (source of runtimes) @@ -339,7 +339,10 @@ jobs: # The frame-copy helper is the inverse: a separate process # that MUST link libmpv (dev-mode engine; stripped from # packages until the bundled-runtime staging lands). - test -x dist/apps/electron-backend/native/iptvnator_mpv_helper + # test -f, not -x: the webpack dist asset copy drops file + # modes; consumers restore the bit (after-pack) or require + # it via the X_OK support probe. + test -f dist/apps/electron-backend/native/iptvnator_mpv_helper if ! ldd dist/apps/electron-backend/native/iptvnator_mpv_helper | grep -q 'libmpv'; then echo "::error::Linux frame-copy helper must link libmpv" ldd dist/apps/electron-backend/native/iptvnator_mpv_helper diff --git a/CLAUDE.md b/CLAUDE.md index d42bcb81e..28af98791 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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=` 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; 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 (headless CGL on macOS, headless EGL on Linux — no window embedding, so native Wayland works and the X11/system-mpv requirements of the native Linux engine do not apply) and publishes BGRA frames into a shm ring; the preload frame pump uploads them onto a renderer ``, 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 runtime contains both an executable helper 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`, `libgbm-dev`) and is stripped from packaged apps until bundled-runtime staging lands. 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; 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 (headless CGL on macOS, headless EGL on Linux — no window embedding, so native Wayland works and the X11/system-mpv requirements of the native Linux engine do not apply) and publishes BGRA frames into a shm ring; the preload frame pump uploads them onto a renderer ``, 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 runtime contains both an executable helper 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 packaged apps until bundled-runtime staging lands. 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)**: diff --git a/apps/electron-backend/build-embedded-mpv.js b/apps/electron-backend/build-embedded-mpv.js index 4788329d4..769bcf7ee 100644 --- a/apps/electron-backend/build-embedded-mpv.js +++ b/apps/electron-backend/build-embedded-mpv.js @@ -495,8 +495,25 @@ function main() { `Building native addon against Electron ${electronVersion} using ${runtime.origin} runtime for ${targetPlatform}-${targetArch}...` ); cleanNativeBuildIntermediates(); - runNodeGyp('configure', env); - runNodeGyp('build', env); + try { + runNodeGyp('configure', env); + runNodeGyp('build', env); + } catch (error) { + if (!embeddedMpvRequired && runtime.origin === 'system-dev') { + // The system-dev fallback triggers on any machine with + // libmpv-dev installed; keep the old graceful-skip contract + // when the rest of the toolchain (EGL/GL/gbm dev packages) is + // missing instead of failing the whole electron build. + log( + `Embedded MPV native build failed with the system-dev toolchain; continuing without embedded MPV. ${ + error instanceof Error ? error.message : String(error) + }` + ); + cleanOutput(); + return; + } + throw error; + } if (!fs.existsSync(outputFile)) { throw new Error(`Build finished without producing ${outputFile}.`); diff --git a/apps/electron-backend/native/helper/frame_helper_render.h b/apps/electron-backend/native/helper/frame_helper_render.h index 872314e91..a9eb4704a 100644 --- a/apps/electron-backend/native/helper/frame_helper_render.h +++ b/apps/electron-backend/native/helper/frame_helper_render.h @@ -164,6 +164,15 @@ inline bool RenderPipeline::start(mpv_handle* mpv, inline bool RenderPipeline::setupGl(std::string& errorOut) { gl_.makeCurrent(); + /* Diagnosable renderer choice: e.g. Mesa's surfaceless platform can + * silently fall back to llvmpipe when the hardware driver is only + * reachable via another EGL display tier. */ + const GLubyte* renderer = glGetString(GL_RENDERER); + if (renderer) { + std::fprintf(stderr, "gl renderer: %s\n", + reinterpret_cast(renderer)); + } + if (!rebuildTargets(width_, height_)) { errorOut = "framebuffer setup failed"; return false; diff --git a/apps/electron-backend/native/helper/frame_shm.h b/apps/electron-backend/native/helper/frame_shm.h index 077feba2b..9b22001fd 100644 --- a/apps/electron-backend/native/helper/frame_shm.h +++ b/apps/electron-backend/native/helper/frame_shm.h @@ -6,7 +6,9 @@ * resize creates a fresh shm segment named `-g` and the * reader re-attaches when the helper announces the new generation. * - * Must compile as C11 (reader addon) and C++17 (helper). + * Must compile as C11 (reader addon) and C++17 (helper). Both consumers + * build with node-gyp's GNU dialects; frame_shm_now_ns() relies on POSIX + * clock_gettime, which strict -std=c11 (__STRICT_ANSI__) would hide. */ #pragma once diff --git a/apps/electron-backend/native/helper/mpv_frame_helper.cpp b/apps/electron-backend/native/helper/mpv_frame_helper.cpp index cff588dd6..a3909538b 100644 --- a/apps/electron-backend/native/helper/mpv_frame_helper.cpp +++ b/apps/electron-backend/native/helper/mpv_frame_helper.cpp @@ -1,5 +1,6 @@ /* - * iptvnator-mpv-helper — frame-copy embedded MPV helper process (macOS). + * iptvnator-mpv-helper — frame-copy embedded MPV helper process + * (macOS + Linux; platform GL context in frame_helper_gl.h). * * One process = one playback session. Owns libmpv end to end: decodes, * renders offscreen at viewport size, publishes BGRA frames into a shared diff --git a/apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts b/apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts index 1084767d8..3df3c7160 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts @@ -335,6 +335,51 @@ describe('EmbeddedMpvNativeService power blocker', () => { ); }); + 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'; @@ -375,6 +420,14 @@ describe('EmbeddedMpvNativeService power blocker', () => { '', 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' + ); }); }); diff --git a/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts b/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts index 890d84894..334da6dee 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts @@ -217,7 +217,10 @@ 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. + // 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() @@ -226,6 +229,7 @@ export class EmbeddedMpvNativeService { 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(), }; } @@ -257,6 +261,7 @@ export class EmbeddedMpvNativeService { supported: false, platform: process.platform, reason: missingLinuxMpvExecutableReason, + frameCopyAvailable: this.isFrameCopyAvailable(), }; } @@ -394,10 +399,13 @@ export class EmbeddedMpvNativeService { // 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. - const windowHandle = this.isFrameCopyEngineActive() - ? Buffer.alloc(0) - : this.getMainWindowHandle(); + // 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, diff --git a/docs/architecture/embedded-mpv-native.md b/docs/architecture/embedded-mpv-native.md index c75c54fa9..989947291 100644 --- a/docs/architecture/embedded-mpv-native.md +++ b/docs/architecture/embedded-mpv-native.md @@ -202,17 +202,22 @@ Trade-offs and constraints: - Scope: on macOS Apple Silicon only by owner decision (2026-07-10); Intel Macs keep the native-view engine. Linux (any arch) is ported — headless EGL, works under native Wayland since nothing embeds into a - window; dev builds need `libmpv-dev`, `libegl-dev`, `libgl-dev` and - `libgbm-dev` (the helper links system libmpv, which is legal - out-of-process — the in-process libmpv ban still binds the addon). The - Windows port of the helper (WGL) is future work — the shm protocol and - adapter are platform-agnostic. + window; dev builds need `libmpv-dev`, `libegl-dev`, `libgl-dev`, + `libopengl-dev` and `libgbm-dev` (the helper links system libmpv, which + is legal out-of-process — the in-process libmpv ban still binds the + addon). The helper logs the chosen EGL display tier and the GL renderer + string to stderr; on systems whose hardware driver is only reachable + via the default display (e.g. NVIDIA proprietary), the surfaceless tier + can select Mesa's software renderer — check that log line when + diagnosing performance. The Windows port of the helper (WGL) is future + work — the shm protocol and adapter are platform-agnostic. - Measured baseline (M1 Pro, spikes/mpv-frame-copy/RESULTS.md): 4K60 HEVC sustained end to end, ~1.2 ms shm copy + ~3.5 ms texture upload, ~10 ms produce-to-upload latency, zero torn frames over a 10-minute run. Linux (i7-1165G7/Iris Xe, same RESULTS.md): 1080p60 sustained with - ~1.2 ms copies; 4K rows are software-decode-limited on that hardware; - zero torn frames everywhere. + ~1.2 ms copies; the 4K rows are limited by software decode/source + generation on that hardware, not by the copy path; zero torn frames + everywhere. - Helper crash isolation: an unexpected helper exit surfaces as a session `error` (renderer falls back); it can never take down the Electron main process, unlike in-process libmpv. diff --git a/spikes/mpv-frame-copy/PORTING.md b/spikes/mpv-frame-copy/PORTING.md index df037b4e1..9c0eb0915 100644 --- a/spikes/mpv-frame-copy/PORTING.md +++ b/spikes/mpv-frame-copy/PORTING.md @@ -19,8 +19,8 @@ measurements in RESULTS.md. Verified end-to-end in-app on Ubuntu 25.04 (Wayland session) with the xtream mock portal. Dev-build-only on Linux: the helper links system libmpv and `electron-after-pack.cjs` strips it - from packages until milestone 3 (bundled runtime) — remove that strip - when milestone 3 lands. Windows is NOT ported yet — this branch on + from packages until milestone 4 (Linux bundled-libmpv runtime) — remove + that strip when milestone 4 lands. Windows is NOT ported yet — this branch on Windows behaves exactly like master (helper doesn't build there, engine can't activate, env flag falls back to native). - Coordination: PR #1169 credits larsemig's idea (#1154 comment 4932807350) @@ -35,17 +35,22 @@ The stdio protocol, shm layout, TS adapter, main-process service, preload pump, and Angular UI are shared and already shipped. ``` -apps/electron-backend/native/helper/ +apps/electron-backend/native/helper/ # state after the Linux port: ├── mpv_frame_helper.cpp # portable: protocol, mpv session, snapshots ├── frame_helper_io.h # portable: TSV-in/JSON-out, percent-encoding -├── frame_shm.h # layout portable; POSIX shm calls are not -└── frame_helper_render.h # macOS-ONLY: CGL headless GL + PBO + shm write +├── frame_shm.h # portable layout + shared CLOCK_MONOTONIC clock +│ # (POSIX shm calls still need a Windows twin) +├── frame_helper_render.h # portable: FBO/PBO readback + shm publish +└── frame_helper_gl.h # PLATFORM SEAM: GlContext — CGL (macOS) and + # EGL (Linux); Windows adds its WGL twin HERE apps/electron-backend/native/src/embedded_mpv_frame_reader.c - # real impl under #ifdef __APPLE__, stub elsewhere + # real impl on __APPLE__ + __linux__, stub + # elsewhere (Windows needs shm-open/clock twins) ``` -Porting = give `frame_helper_render.h` a WGL/EGL twin, give the shm -create/open a Windows twin, flip the TS gates, extend packaging. +Porting Windows = give `frame_helper_gl.h` a WGL GlContext twin, give the +shm create/open (+ `frame_shm_now_ns`) Windows twins, flip the TS gate in +`embedded-mpv-frame-copy-platform.util.ts`, extend packaging. ## Branching & merge strategy (do this, not "commit to the current branch") diff --git a/spikes/mpv-frame-copy/RESULTS.md b/spikes/mpv-frame-copy/RESULTS.md index 494231abc..40bb2f9f2 100644 --- a/spikes/mpv-frame-copy/RESULTS.md +++ b/spikes/mpv-frame-copy/RESULTS.md @@ -23,7 +23,9 @@ helper and the `Electron Helper (Renderer)` process during playback. Column meanings: *new fps* — frames actually reaching the canvas; *copy* — shm→ArrayBuffer memcpy in the addon; *upload* — `texSubImage2D` wall time; *age* — produce→uploaded latency (helper memcpy done → texture updated, -same CLOCK_MONOTONIC_RAW clock). +same monotonic clock on both sides — CLOCK_MONOTONIC_RAW in the original +spike harness used for the M1 rows; the production engine uses +CLOCK_MONOTONIC via `frame_shm_now_ns()` since the Linux port). ## MacBook Pro M1 Pro (arm64), macOS, 120 Hz internal display — 2026-07-10 diff --git a/tools/embedded-mpv/README.md b/tools/embedded-mpv/README.md index 99717cde0..33f53da1f 100644 --- a/tools/embedded-mpv/README.md +++ b/tools/embedded-mpv/README.md @@ -1,6 +1,6 @@ # Embedded MPV Runtime -This folder contains tooling for preparing MPV runtime/build inputs for IPTVnator's experimental embedded MPV player. macOS and Windows bundle `libmpv`; Linux uses staged MPV headers for compilation and launches the system `mpv` executable at runtime. On Linux, `apps/electron-backend/build-embedded-mpv.js` also falls back to system headers when nothing is staged (`libmpv-dev`; override with `LIBMPV_INCLUDE_DIR`/`LINUX_NATIVE_LIBRARY_DIR`), so a plain distro dev setup builds without staging. The frame-copy helper (`iptvnator_mpv_helper`) additionally needs `libegl-dev`, `libgl-dev`, and `libgbm-dev`, and links the system `libmpv` — allowed because it is a separate process; the in-process-libmpv ban still binds the addon. +This folder contains tooling for preparing MPV runtime/build inputs for IPTVnator's experimental embedded MPV player. macOS and Windows bundle `libmpv`; Linux uses staged MPV headers for compilation and launches the system `mpv` executable at runtime. On Linux, `apps/electron-backend/build-embedded-mpv.js` also falls back to system headers when nothing is staged (`libmpv-dev`; override with `LIBMPV_INCLUDE_DIR`/`LINUX_NATIVE_LIBRARY_DIR`), so a plain distro dev setup builds without staging. The frame-copy helper (`iptvnator_mpv_helper`) additionally needs `libegl-dev`, `libgl-dev`, `libopengl-dev` (for the unversioned glvnd `libOpenGL.so` the linker resolves `-lOpenGL` against), and `libgbm-dev`, and links the system `libmpv` — allowed because it is a separate process; the in-process-libmpv ban still binds the addon. ## Runtime Policy