Files
iptvnator/docs/superpowers/specs/2026-07-17-linux-embedded-mpv-frame-copy-packaging-design.md
T
4gray c266eaa680 fix(packaging): preserve Flatpak Electron ELF for Zypak (#1205)
Fixes the launcher/Zypak regression reported in #1203. The additional GPU/video.js behavior remains tracked separately in that issue.
2026-07-18 22:42:45 +02:00

14 KiB

Linux Embedded MPV Frame-Copy Packaging Design

Date: 2026-07-17

Status: Approved

Goal

Ship a genuinely usable Embedded MPV frame-copy runtime in every official Linux x64 package format produced by IPTVnator: AppImage, DEB, RPM, Pacman, Snap, and Flatpak. Keep libmpv outside the Electron process, retain the existing native-view engine as a safe fallback, and never advertise frame-copy from artifact presence alone.

Linux arm64 and armv7l remain out of scope for native Embedded MPV artifacts. The current CI cross-packages those architectures from an x64 host and cannot produce or exercise matching native addons. Those packages must continue to carry an explicit unavailable marker and must not contain x64 native binaries.

Evidence From The Existing Implementation

  • apps/electron-backend/native/binding.gyp builds three distinct artifacts: the native-view addon, the N-API shared-memory frame reader, and the iptvnator_mpv_helper process. On Linux only the helper links -lmpv; the addon uses X11/Xext/dlopen and must remain free of libmpv linkage.
  • tools/packaging/embedded-mpv-frame-copy-files.cjs deliberately deletes the helper from every Linux package.
  • tools/packaging/embedded-mpv-packaging.cjs rejects Linux packages that contain either the helper or libmpv, and accepts only the external-mpv-process manifest origin.
  • resolveFrameCopyHelperPath() currently treats an executable helper plus a readable reader addon as a usable runtime. It does not prove that the ELF loader can resolve libmpv or that libmpv/EGL initialization works.
  • .github/workflows/build-and-make.yaml builds and verifies the Linux helper against Ubuntu's system libmpv, then relies on the after-pack hook to remove it. The same x64 build output is used for foreign-architecture Linux packages, which receive the unavailable marker.
  • Electron Builder creates one unpacked application layout before producing multiple distributable targets. A system-runtime layout and a bundled portable-runtime layout therefore cannot safely share one packaging pass.

Selected Distribution Strategy

Linux packaging is split into explicit profiles:

Profile Formats libmpv strategy
system DEB, RPM, Pacman Depend on the distribution package and resolve libmpv.so.2 from the host
portable AppImage, Snap Bundle the pinned LGPL-compatible runtime closure under native/lib
flatpak Flatpak Bundle the same pinned LGPL-compatible runtime closure under native/lib

The official CI matrix must run these profiles independently. The packaging hook receives the profile through a required environment value and validates that the selected target set matches the runtime mode. It must fail closed if an official x64 package is requested with an absent, incomplete, or ambiguous profile.

Flatpak is an isolated packaging pass and keeps iptvnator as the real Electron ELF so Electron Builder's electron-wrapper passes it directly to Zypak. Other Linux targets retain the conditional iptvnator wrapper and iptvnator.bin. Mixed Flatpak/non-Flatpak target sets fail before mutation.

System package dependencies are:

  • DEB: libmpv2, libegl1, libgl1, libgbm1
  • RPM: mpv-libs, libglvnd-egl, libglvnd-glx, mesa-libgbm
  • Pacman: mpv, libglvnd, mesa

These names match the current Debian, Fedora, and Arch package databases and cover every direct helper interface: libmpv, EGL, GL, and GBM. The helper links libGL.so.1 rather than libOpenGL.so.0, matching both the distro contracts and Snap's graphics provider. System packages do not copy libmpv into IPTVnator. The helper keeps an $ORIGIN/lib RUNPATH first for a consistent binary, but naturally resolves the system SONAME when the private directory is absent.

Portable and sandboxed packages use a source-built runtime rather than copying the Ubuntu runner's mpv package. The runtime build is checksum/version pinned, uses FFmpeg without GPL/nonfree switches and mpv with -Dgpl=false, records sources and exact flags in the manifest, and keeps shared libraries replaceable under the LGPL. The minimal codec baseline is FFmpeg's built-in LGPL decoders, demuxers, protocols, and software scaling/resampling plus libass text subtitles. Hardware decoding remains opportunistic through host Mesa/driver interfaces and must fall back to software decoding.

The source build also pins the hwdata v0.409 archive and its SHA-256 because libdisplay-info 0.1.1 compiles pnp.ids into its generated vendor lookup table. The builder stages that file with private hwdata.pc metadata and restricts libdisplay-info's native pkg-config search to the staged prefix, so Meson's /usr/share/hwdata/pnp.ids fallback cannot make the runtime depend on unrecorded host data. The runtime manifest records this build-input relationship. Release source bundles must include the exact hwdata archive and its dual-license notice (GPL-2.0-or-later OR XFree86-1.0) alongside the MIT-licensed libdisplay-info source.

The strict Snap uses base: core22, a private shared-memory plug, and an exact graphics-core22 content plug targeting a real empty mode-0755 $SNAP/graphics with external mesa-core22 as default provider. The graphics provider supplies EGL/GL/GLX/GBM/DRM/VA; Electron Builder's GNOME content runtime supplies ALSA/PulseAudio. Those shared providers are not copied into IPTVnator's Snap or source/notices archive. Because core22 does not synthesize $SNAP content targets, the package hook creates the empty directory and extracted-artifact validation checks its type and emptiness. The metadata also declares exactly the canonical graphics layouts: /usr/share/libdrm binds from $SNAP/graphics/libdrm, and /usr/share/drirc.d symlinks to $SNAP/graphics/drirc.d. Locally installed --dangerous artifacts explicitly install and connect both providers in CI, disconnect graphics-core22 to require an unavailable application diagnostic with exit code 1, then reconnect it and require success.

Runtime Layout And Linkage

The x64 packaged native directory is:

resources/app.asar.unpacked/electron-backend/native/
  embedded_mpv.node
  embedded_mpv_frame_reader.node
  iptvnator_mpv_helper
  embedded-mpv-runtime.json
  lib/
    libmpv.so.2
    libavcodec.so.*
    libavformat.so.*
    libavutil.so.*
    libavfilter.so.*
    libswresample.so.*
    libswscale.so.*
    libass.so.*
    ...other non-system runtime dependencies

For system, lib/ is absent and the manifest declares the required SONAME and package-family dependency. For portable and flatpak, lib/ contains the complete non-system dependency closure. ELF dependencies inside that closure and the helper use only SONAMEs plus $ORIGIN-relative RPATH/RUNPATH; they may not retain build-prefix paths.

embedded_mpv.node, the Electron executable (iptvnator for Flatpak; iptvnator.bin for other Linux targets), and Electron's shipped libraries must not have a direct DT_NEEDED entry for libmpv. iptvnator_mpv_helper must have one. Process isolation is an invariant, not a profile-specific choice. The source electron-backend/native{,/**/*} tree is excluded from app.asar; afterPack is the sole owner of the normalized unpacked native directory, and package checks reject every archived native entry. The pristine Electron tree is scanned recursively before target packaging. Because Snap later overlays package-manager lib/**/usr/lib/** trees into the payload root, its extracted-target scan excludes exactly those two target-provided trees while remaining recursive everywhere else. Electron-library symlinks outside those roots fail closed.

The manifest records:

  • schema version, platform, architecture, profile, and runtime origin;
  • required helper/reader names and executable/readable expectations;
  • libmpv SONAME and either system package requirements or bundled files;
  • source package versions, URLs/checksums, license identifiers, and exact FFmpeg/mpv build flags for bundled profiles;
  • the pinned hwdata pnp.ids build input consumed by libdisplay-info;
  • runtime closure and total byte size;
  • the native-view backend contract and the fact that only the helper links libmpv.

Honest Capability Detection

The helper gains a side-effect-free --runtime-probe mode. It must:

  1. load through the normal ELF loader and therefore prove that all DT_NEEDED dependencies resolve;
  2. create and initialize an idle libmpv handle with vo=libmpv;
  3. create the platform render pipeline far enough to prove EGL/OpenGL/GBM availability without opening media;
  4. create, map, validate, and destroy the minimal shared-memory ring required by playback;
  5. emit one versioned JSON result and exit promptly with status zero only on success.

The main process invokes this probe synchronously with a bounded timeout before BrowserWindow creation. Probe success is cached for the process lifetime. The probe environment prepends the packaged native/lib directory only when the manifest declares a bundled runtime. The system profile does not inject a private loader path. Snap additionally rebuilds its loader and graphics-driver variables from validated host GL, $SNAP/graphics, exact GNOME-platform, and generic core22 roots; ambient preload/audit/library/driver overrides and caller-provided architecture triplets are not inherited. The direct provider wrapper launch also removes shell startup/options, tracing hooks, and exported functions and fixes PATH to immutable core22 system directories.

Packaging CI invokes the full main-process gate with the exact --embedded-mpv-runtime-probe application switch. It executes before BrowserWindow startup, writes one availability JSON line, and exits zero only for a usable runtime. CI does not treat a direct helper invocation or an environment opt-in as proof of packaged capability.

Frame-copy is usable only when all of the following are true:

  • platform and architecture are supported;
  • helper and reader are regular files with correct access modes;
  • the runtime manifest is present, parses, matches Linux/x64, names the actual artifacts, and uses an allowed profile/origin;
  • every manifest-declared bundled file exists as a readable regular file;
  • --runtime-probe succeeds and returns the expected protocol version.

Any failure returns false, keeps the renderer sandbox enabled, and makes the native service choose native-view. The capability result includes a stable reason code for tracing and diagnostics but does not crash startup.

If dependencies disappear after startup, helper spawn/early-exit remains a session error and follows the existing renderer fallback path. The helper is never loaded into Electron as a library.

Packaging And CI Validation

Unit and packaging tests cover:

  • profile-to-target mapping and rejection of mixed system/bundled passes;
  • Linux runtime staging, manifest normalization, closure collection, RPATH, file modes, and stale-artifact cleanup;
  • package validation for system, portable, Flatpak, foreign architecture, and malformed/incomplete manifests;
  • capability probe timeout, nonzero exit, invalid JSON, manifest mismatch, missing dependency, and successful result caching;
  • exclusion of all native payloads from app.asar, including marker-only ARM and system-package stale x64 artifacts;
  • helper probe protocol and failure behavior;
  • package metadata dependencies for DEB/RPM/Pacman.

Linux CI must:

  1. build or restore the pinned x64 LGPL runtime;
  2. build the addon, reader, and helper once against that staged runtime;
  3. prove with readelf/ldd that Electron and embedded_mpv.node do not link libmpv and the helper does;
  4. package the three profiles independently;
  5. unpack or mount each produced format and validate its real payload, modes, manifest, RPATH, dependency closure, and profile;
  6. install/run the application-level packaged gate inside the actual Snap and Flatpak (with the exact Freedesktop 24.08 EGL external-platform path reconstructed inside /app), and probe the AppImage payload;
  7. install system packages in matching disposable distro containers and run the helper probe after the declared libmpv dependency is installed;
  8. run a packaged Electron smoke test that confirms frame-copy capability, creates a helper session against a deterministic local media fixture, sees at least one frame/snapshot, and then repeats with libmpv hidden or removed to prove non-crashing native-view fallback.

Checks that require Linux kernel/package tooling or GPU/EGL are CI-only. macOS development can run all pure Node/Jest tests and static source checks, but cannot establish Linux ELF, package-manager, sandbox, or rendering behavior.

Documentation And Release Compliance

Update docs/architecture/embedded-mpv-native.md, tools/embedded-mpv/README.md, vendor/embedded-mpv/README.md, AGENTS.md, and CLAUDE.md. The docs must describe the x64 format matrix, profile selection, manifest/probe contract, process-isolation invariant, fallback behavior, source-distribution obligations, codec baseline, and ARM status.

Release artifacts must publish the generated runtime manifest and exact source archives/metadata required by the recorded LGPL source-distribution statement. The libplacebo payload is a VCS-metadata-free working-tree snapshot with exact commit/submodule records, so clone-local .git state cannot perturb the compliance tar. Automated Snap publication must wait for a public v* release that already contains both the Snap assets and the exact source archive. Snap Store publication remains outside this implementation and requires its separate release workflow; repository integration follows explicit maintainer authorization.