mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -08:00
feat(embedded-mpv): Linux port of the frame-copy rendering engine (headless EGL) (#1171)
* feat(embedded-mpv): Linux frame-copy helper via headless EGL Port the frame-copy engine's native layer to Linux (PORTING.md items 1-4): - frame_helper_gl.h: platform GlContext abstraction. macOS keeps the CGL path (moved verbatim); Linux acquires an EGL display in order surfaceless-Mesa -> default display -> GBM render node, binds a 3.2 core desktop-GL context surfaceless (1x1 pbuffer fallback), and hands mpv eglGetProcAddress. The helper's own GL calls link against glvnd libOpenGL, so no display server is required. - frame_shm.h: portable frame_shm_now_ns() (CLOCK_MONOTONIC) shared by the helper and the reader addon, replacing the macOS-only clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); producer and consumer stay on the same clock. - embedded_mpv_frame_reader.c: real implementation now also on __linux__ (the code was already POSIX apart from the clock call). - binding.gyp: OS==linux executable branch for iptvnator_mpv_helper linking system libmpv (-lmpv) + EGL/OpenGL/gbm, with rpaths for $ORIGIN/lib and the build-time library dir. The in-process addon still does not link libmpv - the ban only binds in-process, the helper is out of process. - build-embedded-mpv.js: system-dev fallback on Linux (LIBMPV_INCLUDE_DIR or /usr/include) so a distro libmpv-dev install builds without staging a vendored runtime; a pre-set LINUX_NATIVE_LIBRARY_DIR now wins over the vendored lib dir. Verified on Ubuntu 25.04 / i7-1165G7 (Iris Xe): lavfi smoke per PORTING.md (idle->loading->playing snapshots at 4 Hz, aspect-fit generation bump g1 1280x720 -> g2 960x720 for a 4:3 source), reader probe 60 fps at 1080p60 with 0 torn reads, clean quit with no leaked processes or shm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(embedded-mpv): enable the frame-copy engine gates on Linux Flip the TypeScript side of the Linux port (PORTING.md item 5). A shared dependency-free predicate, isFrameCopyPlatformSupported() (linux any-arch, darwin arm64-only), now backs all four gates so they cannot drift: - main.ts: the persisted Settings toggle promotes to the env flag on Linux too (this runs before window creation and controls the sandbox relax). - EmbeddedMpvNativeService.isFrameCopyEngineActive/isFrameCopyAvailable. - EmbeddedMpvFrameCopyAdapter.isSupported. getSupport() ordering: the frame-copy branch moves above the Linux-only native-engine prerequisites - the X11/Xwayland display-server check and the system-mpv-on-PATH probe only bind the --wid native engine, while the frame-copy helper renders offscreen (headless EGL) and links libmpv itself. createSession() also skips resolving the native window handle for frame-copy sessions, which the adapter ignores anyway, so native-Wayland sessions no longer trip the window-handle assertion. Settings copy: the i18n frame-copy description now says macOS (Apple Silicon) and Linux in all 18 languages; stale macOS-only doc comments in the settings/support interfaces updated alongside. Tests: platform-gate matrix for the adapter (darwin arm64/x64, linux x64/arm64, win32) and service specs covering Linux activation under native Wayland, macOS arm64 staying active, macOS x64 staying native, and the skipped window handle for frame-copy sessions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(packaging): CI + package guards for the Linux frame-copy helper - build-and-make.yaml: install libegl-dev/libgl-dev/libgbm-dev on the Linux runner (the helper's EGL backend needs them now that the helper target builds on Linux), and verify the built helper exists and DOES link libmpv - the inverse of the addon's no-libmpv rule, which still holds and stays validated. - electron-after-pack.cjs: strip iptvnator_mpv_helper from packaged Linux apps. It links the build host's system libmpv, which end-user systems cannot be assumed to have; the support probe treats the missing helper as frame-copy-unavailable (dev-build-only engine until the bundled-runtime staging milestone). - frame_helper_gl.h: log the chosen EGL display tier to stderr (the adapter mirrors helper stderr), so bring-up problems on exotic setups are diagnosable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(embedded-mpv): document the Linux frame-copy port - architecture doc: frame-copy section covers Linux (EGL display tiers, build deps, package strip), Linux support matrix notes the frame-copy exception to the X11 + system-mpv requirements, Linux measured baseline. - RESULTS.md: Ubuntu 25.04 / i7-1165G7 (Iris Xe) measurement rows via the production helper + reader probe; viewport-size claim reproduced. - PORTING.md: Linux marked done with pointers to what changed; Windows remains the open port and its perf gate the open decision. - CLAUDE.md + tools/embedded-mpv/README.md: platform scope, Linux dev build requirements, system-headers fallback, helper strip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(embedded-mpv): commit the Linux frame-copy measurement probe linux-frame-probe.mjs reproduces the RESULTS.md Linux rows: spawns the production helper, attaches the frame-reader addon to the announced shm generation, and reports new-frame fps, copy wall time, produce->copy age, torn reads and pixel spread. Usage documented in RESULTS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): address multi-agent review findings on the Linux port Confirmed findings (each verified by 3 adversarial reviewers): - CI would fail to link the helper: -lOpenGL needs the unversioned glvnd libOpenGL.so, shipped only by libopengl-dev, which neither the runner images nor the previous apt line provide. Added to the workflow and to every documented Linux build-dep list. - The new 'test -x' dist guard could never pass: webpack's dist asset copy drops file modes (helper arrives as 0644). The guard is now 'test -f'; electron-after-pack.cjs restores the execute bit on packaged helpers (also fixes packaged-macOS spawns); the support probe now requires X_OK, so a mode-stripped helper reads as frame-copy-unavailable and falls back to native instead of failing spawn with EACCES. - The Settings frame-copy toggle was unreachable in exactly the Linux states the port targets: the native-Wayland and missing-system-mpv unsupported payloads omitted frameCopyAvailable, and toggle visibility derives solely from it. Both returns now advertise availability. Also from review: - build-embedded-mpv.js keeps the old graceful-skip contract when the new system-dev fallback finds libmpv-dev but the GL/EGL/gbm dev stack is missing (previously such machines skipped; a hard electron-build failure was a regression). - createSession derives the window-handle skip from the dispatched addon instead of re-evaluating the engine gate, so the two cannot disagree. - The render thread logs the GL renderer string (surfaceless Mesa can silently pick llvmpipe on non-Mesa-primary systems; now diagnosable — verified 'Mesa Intel Iris Xe' on this machine). - Specs pin the new semantics: frameCopyAvailable advertised while native is unsupported (Wayland / no mpv), frame-copy supported without a system mpv, and the handle-skip test disposes its session through the owning adapter. - Docs: PORTING.md file map reflects the frame_helper_gl.h seam for the Windows porter; helper-strip removal correctly gated on milestone 4 (bundled libmpv), not milestone 3; RESULTS.md preamble notes the RAW->MONOTONIC clock change; stale '(macOS)' scope comments updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): address Greptile/Codex review comments - Sandbox gate requires a usable helper (Greptile P1, security): the main.ts env promotion now also probes for an executable iptvnator_mpv_helper before relaxing the window sandbox — a stale opt-in on packaged Linux (helper deliberately stripped) or after a cleaned native build no longer costs a sandboxless launch for an engine that cannot activate. Helper discovery (addon candidate paths + X_OK probe) moved into embedded-mpv-frame-copy-platform.util.ts, shared by main.ts and the service; the service keeps thin instance wrappers so tests can stub per scenario. New util spec pins the platform matrix, candidate resolution, and the execute-bit semantics. - Stale frame-copy artifacts on skipped builds (Codex P2): cleanOutput() now also removes iptvnator_mpv_helper and embedded_mpv_frame_reader.node, so a failed/skipped rebuild cannot leave a previous helper advertising frame-copy support against a runtime the build just declared unavailable. - Multiarch default lib dir (Greptile P1, partially refuted): -l resolution never depended on our -L (the compiler's built-in search paths include the Debian/Ubuntu multiarch dir — proven by the green CI run linking with a nonexistent -L dir), but the system-dev fallback now defaults to /usr/lib/<multiarch-triple> when present so the -L flag and the helper's baked rpath point somewhere real. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): harden Linux frame-copy port --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
26271fc076
commit
7d75d989e8
45 files changed
+1387
-177
No files matched your search
@@ -182,6 +182,23 @@ function findWindowsLibMpv(runtimeRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Debian/Ubuntu install linker targets under the multiarch triple dir. The
|
||||
* compiler's built-in search paths cover it for -l resolution either way;
|
||||
* this keeps the -L flag and the helper's baked rpath pointing somewhere
|
||||
* real. */
|
||||
function defaultLinuxSystemLibDir() {
|
||||
const multiarchTriples = {
|
||||
arm: 'arm-linux-gnueabihf',
|
||||
arm64: 'aarch64-linux-gnu',
|
||||
x64: 'x86_64-linux-gnu',
|
||||
};
|
||||
const triple = multiarchTriples[targetArch];
|
||||
if (triple && fs.existsSync(`/usr/lib/${triple}`)) {
|
||||
return `/usr/lib/${triple}`;
|
||||
}
|
||||
return '/usr/lib';
|
||||
}
|
||||
|
||||
function findLinuxLibMpv(libDir) {
|
||||
for (const candidate of ['libmpv.so.2', 'libmpv.so.1', 'libmpv.so']) {
|
||||
const candidatePath = path.join(libDir, candidate);
|
||||
@@ -223,6 +240,30 @@ function resolveRuntime() {
|
||||
};
|
||||
}
|
||||
|
||||
if (targetPlatform === 'linux') {
|
||||
// Dev-first Linux flow (frame-copy helper links system libmpv): a
|
||||
// distro libmpv-dev install is a full runtime — no staging needed.
|
||||
// LIBMPV_INCLUDE_DIR / LINUX_NATIVE_LIBRARY_DIR override the system
|
||||
// paths for machines with a local (non-root) libmpv prefix.
|
||||
const systemIncludeDir =
|
||||
process.env.LIBMPV_INCLUDE_DIR || '/usr/include';
|
||||
if (fs.existsSync(path.join(systemIncludeDir, 'mpv', 'client.h'))) {
|
||||
return {
|
||||
origin: 'system-dev',
|
||||
includeDir: systemIncludeDir,
|
||||
libDir:
|
||||
process.env.LINUX_NATIVE_LIBRARY_DIR ||
|
||||
defaultLinuxSystemLibDir(),
|
||||
binDir: undefined,
|
||||
manifest: {
|
||||
warning:
|
||||
'Development-only system libmpv toolchain. Release packaging keeps the external-mpv-process contract.',
|
||||
},
|
||||
windowsImportLib: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (targetPlatform === 'darwin' && homebrewFallbackEnabled) {
|
||||
const homebrewLibMpv = findLibMpv(homebrewLibDir);
|
||||
const homebrewHeader = path.join(homebrewIncludeDir, 'mpv', 'client.h');
|
||||
@@ -454,7 +495,10 @@ function main() {
|
||||
npm_config_update_binary: 'false',
|
||||
LIBMPV_INCLUDE_DIR: runtime.includeDir,
|
||||
...(targetPlatform === 'linux'
|
||||
? { LINUX_NATIVE_LIBRARY_DIR: runtime.libDir }
|
||||
? {
|
||||
LINUX_NATIVE_LIBRARY_DIR:
|
||||
process.env.LINUX_NATIVE_LIBRARY_DIR || runtime.libDir,
|
||||
}
|
||||
: { LIBMPV_LIBRARY_DIR: outputLibDir }),
|
||||
...(runtime.windowsImportLib
|
||||
? {
|
||||
@@ -470,8 +514,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}.`);
|
||||
|
||||
@@ -117,6 +117,42 @@
|
||||
"<!(node -e \"const path = require('path'); const dir = process.env.LIBMPV_LIBRARY_DIR || '/opt/homebrew/lib'; process.stdout.write(path.join(dir, 'libmpv.2.dylib'))\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"OS==\"linux\"",
|
||||
{
|
||||
"type": "executable",
|
||||
"sources": [
|
||||
"helper/mpv_frame_helper.cpp"
|
||||
],
|
||||
"include_dirs": [
|
||||
"<!(node -p \"process.env.LIBMPV_INCLUDE_DIR || '/usr/include'\")",
|
||||
"helper"
|
||||
],
|
||||
"cflags_cc!": [
|
||||
"-fno-exceptions"
|
||||
],
|
||||
"cflags_cc": [
|
||||
"-std=c++17",
|
||||
"-fexceptions"
|
||||
],
|
||||
"cflags": [
|
||||
"-pthread"
|
||||
],
|
||||
"ldflags": [
|
||||
"-pthread",
|
||||
"-Wl,-rpath,'$$ORIGIN/lib'",
|
||||
"-Wl,-rpath,<!(node -p \"process.env.LINUX_NATIVE_LIBRARY_DIR || '/usr/lib'\")"
|
||||
],
|
||||
"libraries": [
|
||||
"-L<!(node -p \"process.env.LINUX_NATIVE_LIBRARY_DIR || '/usr/lib'\")",
|
||||
"-lmpv",
|
||||
"-lEGL",
|
||||
"-lOpenGL",
|
||||
"-lgbm",
|
||||
"-ldl"
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,519 @@
|
||||
/*
|
||||
* Headless GL context for the frame-copy helper, one implementation per
|
||||
* platform behind the same tiny GlContext surface.
|
||||
*
|
||||
* macOS: accelerated CGL 3.2 core context; GL entry points for mpv come
|
||||
* from the OpenGL framework via dlsym.
|
||||
*
|
||||
* Linux: EGL display candidates are fully probed in order — Mesa's
|
||||
* surfaceless platform (no display server needed), the session's default
|
||||
* display, then a GBM render node. The first hardware renderer wins; an
|
||||
* earlier software renderer is recreated only if no hardware tier works.
|
||||
* Each tier uses a desktop-GL 3.2 core context bound surfaceless (1x1 pbuffer
|
||||
* fallback for drivers without EGL_KHR_surfaceless_context). The helper's
|
||||
* own GL calls resolve at link time through libOpenGL (glvnd); mpv resolves
|
||||
* core symbols from that linked library and falls back to eglGetProcAddress
|
||||
* for extensions.
|
||||
*
|
||||
* Threading contract: create() runs on the main thread and must leave the
|
||||
* context unbound; the render thread calls makeCurrent() once and owns the
|
||||
* context until destroy().
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
#define GL_SILENCE_DEPRECATION
|
||||
#include <OpenGL/OpenGL.h>
|
||||
#include <OpenGL/gl3.h>
|
||||
|
||||
#include <dlfcn.h>
|
||||
|
||||
#elif defined(__linux__)
|
||||
|
||||
/* Keep eglplatform.h away from Xlib types: the helper never uses native
|
||||
* window/display handles, so X11 headers are an unnecessary build dep. */
|
||||
#define EGL_NO_X11 1
|
||||
#define MESA_EGL_NO_X11_HEADERS 1
|
||||
#define GL_GLEXT_PROTOTYPES 1
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glext.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <gbm.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <dlfcn.h>
|
||||
|
||||
#ifndef EGL_PLATFORM_SURFACELESS_MESA
|
||||
#define EGL_PLATFORM_SURFACELESS_MESA 0x31DD
|
||||
#endif
|
||||
#ifndef EGL_PLATFORM_GBM_KHR
|
||||
#define EGL_PLATFORM_GBM_KHR 0x31D7
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
namespace frame_helper {
|
||||
|
||||
/* Matches mpv_opengl_init_params.get_proc_address. */
|
||||
using GlGetProcAddressFn = void* (*)(void* ctx, const char* name);
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
inline void* glDlsymGetProcAddress(void* ctx, const char* name) {
|
||||
return dlsym(ctx, name);
|
||||
}
|
||||
|
||||
class GlContext {
|
||||
public:
|
||||
bool create(std::string& errorOut) {
|
||||
glDylib_ = dlopen(
|
||||
"/System/Library/Frameworks/OpenGL.framework/Versions/Current/"
|
||||
"OpenGL",
|
||||
RTLD_LAZY | RTLD_LOCAL);
|
||||
if (!glDylib_) {
|
||||
errorOut = "failed to open the OpenGL framework";
|
||||
return false;
|
||||
}
|
||||
|
||||
CGLPixelFormatAttribute attrs[] = {
|
||||
kCGLPFAOpenGLProfile,
|
||||
(CGLPixelFormatAttribute)kCGLOGLPVersion_3_2_Core,
|
||||
kCGLPFAAccelerated,
|
||||
kCGLPFAColorSize, (CGLPixelFormatAttribute)24,
|
||||
kCGLPFAAlphaSize, (CGLPixelFormatAttribute)8,
|
||||
(CGLPixelFormatAttribute)0,
|
||||
};
|
||||
CGLPixelFormatObj pixelFormat = nullptr;
|
||||
GLint matched = 0;
|
||||
if (CGLChoosePixelFormat(attrs, &pixelFormat, &matched) !=
|
||||
kCGLNoError ||
|
||||
!pixelFormat) {
|
||||
errorOut = "no accelerated CGL pixel format";
|
||||
return false;
|
||||
}
|
||||
const CGLError contextError =
|
||||
CGLCreateContext(pixelFormat, nullptr, &cgl_);
|
||||
CGLDestroyPixelFormat(pixelFormat);
|
||||
if (contextError != kCGLNoError || !cgl_) {
|
||||
errorOut = "failed to create a headless CGL context";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool makeCurrent(std::string& errorOut) {
|
||||
if (CGLSetCurrentContext(cgl_) != kCGLNoError) {
|
||||
errorOut = "failed to bind the CGL render context";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
CGLSetCurrentContext(nullptr);
|
||||
if (cgl_) CGLDestroyContext(cgl_);
|
||||
cgl_ = nullptr;
|
||||
/* glDylib_ stays mapped: mpv may still resolve symbols during its
|
||||
* own teardown, and the process exits right after anyway. */
|
||||
}
|
||||
|
||||
GlGetProcAddressFn procLoader() const { return glDlsymGetProcAddress; }
|
||||
void* procLoaderCtx() const { return glDylib_; }
|
||||
|
||||
private:
|
||||
CGLContextObj cgl_ = nullptr;
|
||||
void* glDylib_ = nullptr;
|
||||
};
|
||||
|
||||
#elif defined(__linux__)
|
||||
|
||||
inline void* eglWrapGetProcAddress(void* /*ctx*/, const char* name) {
|
||||
if (void* linkedSymbol = dlsym(RTLD_DEFAULT, name)) return linkedSymbol;
|
||||
return reinterpret_cast<void*>(eglGetProcAddress(name));
|
||||
}
|
||||
|
||||
class GlContext {
|
||||
public:
|
||||
bool create(std::string& errorOut) {
|
||||
const DisplayTier tiers[] = {
|
||||
DisplayTier::SurfacelessMesa,
|
||||
DisplayTier::Default,
|
||||
DisplayTier::Gbm,
|
||||
};
|
||||
|
||||
bool hasSoftwareFallback = false;
|
||||
DisplayTier softwareFallback = DisplayTier::SurfacelessMesa;
|
||||
std::string failures;
|
||||
|
||||
for (DisplayTier tier : tiers) {
|
||||
Candidate candidate;
|
||||
std::string candidateError;
|
||||
if (!tryCandidate(tier, candidate, candidateError)) {
|
||||
if (candidateOwnsResources(candidate)) {
|
||||
errorOut = candidateError +
|
||||
"; failed to safely tear down rejected EGL "
|
||||
"candidate";
|
||||
return false;
|
||||
}
|
||||
appendFailure(failures, candidateError);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!candidate.softwareRenderer) {
|
||||
adoptCandidate(candidate);
|
||||
std::fprintf(stderr, "egl display: %s\n", tierName(tier));
|
||||
return true;
|
||||
}
|
||||
|
||||
std::fprintf(stderr,
|
||||
"egl candidate %s uses software renderer %s; "
|
||||
"trying the next tier\n",
|
||||
tierName(tier), candidate.renderer.c_str());
|
||||
if (!hasSoftwareFallback) {
|
||||
softwareFallback = tier;
|
||||
hasSoftwareFallback = true;
|
||||
}
|
||||
destroyCandidate(candidate);
|
||||
if (candidateOwnsResources(candidate)) {
|
||||
errorOut = std::string(tierName(tier)) +
|
||||
": failed to safely release software EGL "
|
||||
"candidate";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Recreate rather than retain the software context while probing the
|
||||
* remaining tiers: EGL implementations may alias platform displays,
|
||||
* and terminating one alias can invalidate the retained context. */
|
||||
if (hasSoftwareFallback) {
|
||||
Candidate candidate;
|
||||
std::string candidateError;
|
||||
if (tryCandidate(softwareFallback, candidate, candidateError)) {
|
||||
adoptCandidate(candidate);
|
||||
std::fprintf(stderr,
|
||||
"egl display: %s (software fallback: %s)\n",
|
||||
tierName(softwareFallback), renderer_.c_str());
|
||||
return true;
|
||||
}
|
||||
if (candidateOwnsResources(candidate)) {
|
||||
errorOut = candidateError +
|
||||
"; failed to safely tear down software fallback";
|
||||
return false;
|
||||
}
|
||||
appendFailure(failures, candidateError);
|
||||
}
|
||||
|
||||
errorOut = "no usable EGL display (surfaceless/default/GBM)";
|
||||
if (!failures.empty()) errorOut += ": " + failures;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool makeCurrent(std::string& errorOut) {
|
||||
/* eglBindAPI is thread-local. create() runs on the main thread, so the
|
||||
* render thread must select desktop GL for both bind and teardown. */
|
||||
if (eglBindAPI(EGL_OPENGL_API) != EGL_TRUE) {
|
||||
errorOut = "failed to bind the desktop OpenGL EGL API";
|
||||
return false;
|
||||
}
|
||||
if (eglMakeCurrent(display_, surface_, surface_, context_) != EGL_TRUE) {
|
||||
errorOut = "failed to bind the EGL render context";
|
||||
eglReleaseThread();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
if (display_ != EGL_NO_DISPLAY) {
|
||||
bool released = true;
|
||||
if (eglBindAPI(EGL_OPENGL_API) != EGL_TRUE) {
|
||||
std::fprintf(stderr,
|
||||
"egl teardown: failed to bind desktop OpenGL\n");
|
||||
released = false;
|
||||
} else if (eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE,
|
||||
EGL_NO_CONTEXT) != EGL_TRUE) {
|
||||
std::fprintf(stderr,
|
||||
"egl teardown: failed to release current "
|
||||
"context\n");
|
||||
released = false;
|
||||
}
|
||||
if (eglReleaseThread() != EGL_TRUE) {
|
||||
std::fprintf(stderr,
|
||||
"egl teardown: eglReleaseThread failed\n");
|
||||
released = false;
|
||||
}
|
||||
/* Never tear down the display/GBM device under a context that EGL
|
||||
* may still consider current. The process is already exiting, so
|
||||
* leaking on this exceptional path is safer than use-after-free. */
|
||||
if (!released) return;
|
||||
|
||||
if (surface_ != EGL_NO_SURFACE)
|
||||
eglDestroySurface(display_, surface_);
|
||||
if (context_ != EGL_NO_CONTEXT)
|
||||
eglDestroyContext(display_, context_);
|
||||
eglTerminate(display_);
|
||||
}
|
||||
surface_ = EGL_NO_SURFACE;
|
||||
context_ = EGL_NO_CONTEXT;
|
||||
display_ = EGL_NO_DISPLAY;
|
||||
if (gbmDevice_) {
|
||||
gbm_device_destroy(gbmDevice_);
|
||||
gbmDevice_ = nullptr;
|
||||
}
|
||||
if (gbmFd_ >= 0) {
|
||||
close(gbmFd_);
|
||||
gbmFd_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
GlGetProcAddressFn procLoader() const { return eglWrapGetProcAddress; }
|
||||
void* procLoaderCtx() const { return nullptr; }
|
||||
|
||||
private:
|
||||
enum class DisplayTier { SurfacelessMesa, Default, Gbm };
|
||||
|
||||
struct Candidate {
|
||||
EGLDisplay display = EGL_NO_DISPLAY;
|
||||
EGLContext context = EGL_NO_CONTEXT;
|
||||
EGLSurface surface = EGL_NO_SURFACE;
|
||||
struct gbm_device* gbmDevice = nullptr;
|
||||
int gbmFd = -1;
|
||||
bool initialized = false;
|
||||
bool current = false;
|
||||
bool threadReleased = false;
|
||||
bool softwareRenderer = false;
|
||||
std::string renderer;
|
||||
};
|
||||
|
||||
static const char* tierName(DisplayTier tier) {
|
||||
switch (tier) {
|
||||
case DisplayTier::SurfacelessMesa:
|
||||
return "surfaceless-mesa";
|
||||
case DisplayTier::Default:
|
||||
return "default";
|
||||
case DisplayTier::Gbm:
|
||||
return "gbm render node";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
static void appendFailure(std::string& failures,
|
||||
const std::string& failure) {
|
||||
if (failure.empty()) return;
|
||||
if (!failures.empty()) failures += "; ";
|
||||
failures += failure;
|
||||
}
|
||||
|
||||
static bool candidateOwnsResources(const Candidate& candidate) {
|
||||
return candidate.display != EGL_NO_DISPLAY ||
|
||||
candidate.context != EGL_NO_CONTEXT ||
|
||||
candidate.surface != EGL_NO_SURFACE || candidate.gbmDevice ||
|
||||
candidate.gbmFd >= 0 || candidate.current;
|
||||
}
|
||||
|
||||
static bool isSoftwareRenderer(const std::string& renderer) {
|
||||
std::string normalized = renderer;
|
||||
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
|
||||
[](unsigned char value) {
|
||||
return static_cast<char>(std::tolower(value));
|
||||
});
|
||||
for (const char* marker : {"llvmpipe", "softpipe", "swrast",
|
||||
"software rasterizer", "lavapipe"}) {
|
||||
if (normalized.find(marker) != std::string::npos) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool openGbmDevice(Candidate& candidate) {
|
||||
for (int node = 128; node <= 131; node++) {
|
||||
char devicePath[32];
|
||||
std::snprintf(devicePath, sizeof(devicePath),
|
||||
"/dev/dri/renderD%d", node);
|
||||
const int fd = open(devicePath, O_RDWR | O_CLOEXEC);
|
||||
if (fd < 0) continue;
|
||||
struct gbm_device* device = gbm_create_device(fd);
|
||||
if (!device) {
|
||||
close(fd);
|
||||
continue;
|
||||
}
|
||||
candidate.gbmFd = fd;
|
||||
candidate.gbmDevice = device;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool chooseConfig(EGLDisplay display, EGLint surfaceType,
|
||||
EGLConfig* out) {
|
||||
const EGLint attrs[] = {
|
||||
EGL_SURFACE_TYPE, surfaceType,
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
|
||||
EGL_RED_SIZE, 8,
|
||||
EGL_GREEN_SIZE, 8,
|
||||
EGL_BLUE_SIZE, 8,
|
||||
EGL_ALPHA_SIZE, 8,
|
||||
EGL_NONE,
|
||||
};
|
||||
EGLint matched = 0;
|
||||
return eglChooseConfig(display, attrs, out, 1, &matched) == EGL_TRUE &&
|
||||
matched > 0;
|
||||
}
|
||||
|
||||
bool tryCandidate(DisplayTier tier, Candidate& candidate,
|
||||
std::string& errorOut) {
|
||||
const auto fail = [&](const std::string& detail) {
|
||||
errorOut = std::string(tierName(tier)) + ": " + detail;
|
||||
destroyCandidate(candidate);
|
||||
return false;
|
||||
};
|
||||
|
||||
switch (tier) {
|
||||
case DisplayTier::SurfacelessMesa:
|
||||
candidate.display = eglGetPlatformDisplay(
|
||||
EGL_PLATFORM_SURFACELESS_MESA, nullptr, nullptr);
|
||||
break;
|
||||
case DisplayTier::Default:
|
||||
candidate.display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
break;
|
||||
case DisplayTier::Gbm:
|
||||
if (!openGbmDevice(candidate)) {
|
||||
return fail("no accessible DRM render node");
|
||||
}
|
||||
candidate.display = eglGetPlatformDisplay(
|
||||
EGL_PLATFORM_GBM_KHR, candidate.gbmDevice, nullptr);
|
||||
break;
|
||||
}
|
||||
|
||||
if (candidate.display == EGL_NO_DISPLAY) {
|
||||
return fail("eglGetDisplay returned EGL_NO_DISPLAY");
|
||||
}
|
||||
EGLint major = 0;
|
||||
EGLint minor = 0;
|
||||
if (eglInitialize(candidate.display, &major, &minor) != EGL_TRUE) {
|
||||
return fail("eglInitialize failed");
|
||||
}
|
||||
candidate.initialized = true;
|
||||
if (eglBindAPI(EGL_OPENGL_API) != EGL_TRUE) {
|
||||
return fail("desktop OpenGL API unavailable");
|
||||
}
|
||||
|
||||
EGLConfig config = nullptr;
|
||||
if (!chooseConfig(candidate.display, EGL_PBUFFER_BIT, &config) &&
|
||||
!chooseConfig(candidate.display, 0, &config)) {
|
||||
return fail("no desktop-OpenGL EGLConfig");
|
||||
}
|
||||
const EGLint contextAttrs[] = {
|
||||
EGL_CONTEXT_MAJOR_VERSION,
|
||||
3,
|
||||
EGL_CONTEXT_MINOR_VERSION,
|
||||
2,
|
||||
EGL_CONTEXT_OPENGL_PROFILE_MASK,
|
||||
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
|
||||
EGL_NONE,
|
||||
};
|
||||
candidate.context = eglCreateContext(candidate.display, config,
|
||||
EGL_NO_CONTEXT, contextAttrs);
|
||||
if (candidate.context == EGL_NO_CONTEXT) {
|
||||
return fail("failed to create a 3.2 core context");
|
||||
}
|
||||
|
||||
if (eglMakeCurrent(candidate.display, EGL_NO_SURFACE, EGL_NO_SURFACE,
|
||||
candidate.context) != EGL_TRUE) {
|
||||
const EGLint pbufferAttrs[] = {
|
||||
EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE,
|
||||
};
|
||||
candidate.surface = eglCreatePbufferSurface(
|
||||
candidate.display, config, pbufferAttrs);
|
||||
if (candidate.surface == EGL_NO_SURFACE ||
|
||||
eglMakeCurrent(candidate.display, candidate.surface,
|
||||
candidate.surface, candidate.context) !=
|
||||
EGL_TRUE) {
|
||||
return fail(
|
||||
"eglMakeCurrent failed (surfaceless and pbuffer)");
|
||||
}
|
||||
}
|
||||
candidate.current = true;
|
||||
|
||||
const GLubyte* renderer = glGetString(GL_RENDERER);
|
||||
if (!renderer) return fail("GL_RENDERER unavailable");
|
||||
candidate.renderer = reinterpret_cast<const char*>(renderer);
|
||||
candidate.softwareRenderer = isSoftwareRenderer(candidate.renderer);
|
||||
|
||||
if (eglMakeCurrent(candidate.display, EGL_NO_SURFACE, EGL_NO_SURFACE,
|
||||
EGL_NO_CONTEXT) != EGL_TRUE) {
|
||||
return fail("failed to unbind the probe context");
|
||||
}
|
||||
candidate.current = false;
|
||||
if (eglReleaseThread() != EGL_TRUE) {
|
||||
return fail("eglReleaseThread failed after probing");
|
||||
}
|
||||
candidate.threadReleased = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool releaseCandidateThread(Candidate& candidate) {
|
||||
if (candidate.threadReleased) return true;
|
||||
if (candidate.current) {
|
||||
if (eglBindAPI(EGL_OPENGL_API) != EGL_TRUE ||
|
||||
eglMakeCurrent(candidate.display, EGL_NO_SURFACE,
|
||||
EGL_NO_SURFACE, EGL_NO_CONTEXT) != EGL_TRUE) {
|
||||
return false;
|
||||
}
|
||||
candidate.current = false;
|
||||
}
|
||||
if (eglReleaseThread() != EGL_TRUE) return false;
|
||||
candidate.threadReleased = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void destroyCandidate(Candidate& candidate) {
|
||||
if (!releaseCandidateThread(candidate)) {
|
||||
std::fprintf(stderr,
|
||||
"egl candidate teardown skipped: context release "
|
||||
"failed\n");
|
||||
return;
|
||||
}
|
||||
if (candidate.display != EGL_NO_DISPLAY) {
|
||||
if (candidate.surface != EGL_NO_SURFACE)
|
||||
eglDestroySurface(candidate.display, candidate.surface);
|
||||
if (candidate.context != EGL_NO_CONTEXT)
|
||||
eglDestroyContext(candidate.display, candidate.context);
|
||||
if (candidate.initialized) eglTerminate(candidate.display);
|
||||
}
|
||||
if (candidate.gbmDevice) gbm_device_destroy(candidate.gbmDevice);
|
||||
if (candidate.gbmFd >= 0) close(candidate.gbmFd);
|
||||
candidate = Candidate{};
|
||||
}
|
||||
|
||||
void adoptCandidate(Candidate& candidate) {
|
||||
display_ = candidate.display;
|
||||
context_ = candidate.context;
|
||||
surface_ = candidate.surface;
|
||||
gbmDevice_ = candidate.gbmDevice;
|
||||
gbmFd_ = candidate.gbmFd;
|
||||
renderer_ = candidate.renderer;
|
||||
candidate = Candidate{};
|
||||
}
|
||||
|
||||
EGLDisplay display_ = EGL_NO_DISPLAY;
|
||||
EGLContext context_ = EGL_NO_CONTEXT;
|
||||
EGLSurface surface_ = EGL_NO_SURFACE;
|
||||
struct gbm_device* gbmDevice_ = nullptr;
|
||||
int gbmFd_ = -1;
|
||||
std::string renderer_;
|
||||
};
|
||||
|
||||
#else
|
||||
#error "frame_helper_gl.h has no GL context implementation for this platform"
|
||||
#endif
|
||||
|
||||
} // namespace frame_helper
|
||||
@@ -1,23 +1,19 @@
|
||||
/*
|
||||
* Offscreen render + readback pipeline for the frame-copy helper (macOS).
|
||||
* Offscreen render + readback pipeline for the frame-copy helper.
|
||||
*
|
||||
* Headless CGL context, mpv render API into an FBO sized to the viewport,
|
||||
* async PBO readback ring, BGRA frames published into the FrameShm ring.
|
||||
* Validated in spikes/mpv-frame-copy (4K60 sustained on M1 Pro).
|
||||
* Headless GL context (per-platform, see frame_helper_gl.h), mpv render API
|
||||
* into an FBO sized to the viewport, async PBO readback ring, BGRA frames
|
||||
* published into the FrameShm ring. Validated in spikes/mpv-frame-copy
|
||||
* (4K60 sustained on M1 Pro).
|
||||
*
|
||||
* Threading: everything here runs on the render thread except
|
||||
* requestResize()/stop(), which only touch atomics/mutex-guarded state.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#define GL_SILENCE_DEPRECATION
|
||||
#include <OpenGL/OpenGL.h>
|
||||
#include <OpenGL/gl3.h>
|
||||
|
||||
#include <mpv/client.h>
|
||||
#include <mpv/render_gl.h>
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
@@ -30,12 +26,13 @@
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "frame_helper_gl.h"
|
||||
#include "frame_helper_io.h"
|
||||
#include "frame_shm.h"
|
||||
|
||||
namespace frame_helper {
|
||||
|
||||
inline uint64_t nowNs() { return clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); }
|
||||
inline uint64_t nowNs() { return frame_shm_now_ns(); }
|
||||
|
||||
struct ShmRing {
|
||||
FrameShmHeader* header = nullptr;
|
||||
@@ -129,9 +126,8 @@ private:
|
||||
void renderFrame();
|
||||
|
||||
mpv_handle* mpv_ = nullptr;
|
||||
CGLContextObj cgl_ = nullptr;
|
||||
GlContext gl_;
|
||||
mpv_render_context* renderContext_ = nullptr;
|
||||
void* glDylib_ = nullptr;
|
||||
|
||||
std::string shmBaseName_;
|
||||
ShmRing ring_;
|
||||
@@ -155,10 +151,6 @@ private:
|
||||
std::atomic<int> initState_{0};
|
||||
};
|
||||
|
||||
inline void* frameHelperGetProcAddress(void* ctx, const char* name) {
|
||||
return dlsym(ctx, name);
|
||||
}
|
||||
|
||||
inline bool RenderPipeline::start(mpv_handle* mpv,
|
||||
const std::string& shmBaseName, int width,
|
||||
int height, std::string& errorOut) {
|
||||
@@ -166,48 +158,26 @@ inline bool RenderPipeline::start(mpv_handle* mpv,
|
||||
shmBaseName_ = shmBaseName;
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
|
||||
glDylib_ = dlopen(
|
||||
"/System/Library/Frameworks/OpenGL.framework/Versions/Current/OpenGL",
|
||||
RTLD_LAZY | RTLD_LOCAL);
|
||||
if (!glDylib_) {
|
||||
errorOut = "failed to open the OpenGL framework";
|
||||
return false;
|
||||
}
|
||||
|
||||
CGLPixelFormatAttribute attrs[] = {
|
||||
kCGLPFAOpenGLProfile, (CGLPixelFormatAttribute)kCGLOGLPVersion_3_2_Core,
|
||||
kCGLPFAAccelerated,
|
||||
kCGLPFAColorSize, (CGLPixelFormatAttribute)24,
|
||||
kCGLPFAAlphaSize, (CGLPixelFormatAttribute)8,
|
||||
(CGLPixelFormatAttribute)0,
|
||||
};
|
||||
CGLPixelFormatObj pixelFormat = nullptr;
|
||||
GLint matched = 0;
|
||||
if (CGLChoosePixelFormat(attrs, &pixelFormat, &matched) != kCGLNoError ||
|
||||
!pixelFormat) {
|
||||
errorOut = "no accelerated CGL pixel format";
|
||||
return false;
|
||||
}
|
||||
const CGLError contextError =
|
||||
CGLCreateContext(pixelFormat, nullptr, &cgl_);
|
||||
CGLDestroyPixelFormat(pixelFormat);
|
||||
if (contextError != kCGLNoError || !cgl_) {
|
||||
errorOut = "failed to create a headless CGL context";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return gl_.create(errorOut);
|
||||
}
|
||||
|
||||
inline bool RenderPipeline::setupGl(std::string& errorOut) {
|
||||
CGLSetCurrentContext(cgl_);
|
||||
if (!gl_.makeCurrent(errorOut)) return false;
|
||||
|
||||
/* Keep the accepted renderer diagnosable. Linux already rejected earlier
|
||||
* software tiers when a later hardware-backed EGL candidate was usable. */
|
||||
const GLubyte* renderer = glGetString(GL_RENDERER);
|
||||
if (renderer) {
|
||||
std::fprintf(stderr, "gl renderer: %s\n",
|
||||
reinterpret_cast<const char*>(renderer));
|
||||
}
|
||||
|
||||
if (!rebuildTargets(width_, height_)) {
|
||||
errorOut = "framebuffer setup failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
mpv_opengl_init_params glInit = {frameHelperGetProcAddress, glDylib_};
|
||||
mpv_opengl_init_params glInit = {gl_.procLoader(), gl_.procLoaderCtx()};
|
||||
mpv_render_param params[] = {
|
||||
{MPV_RENDER_PARAM_API_TYPE,
|
||||
const_cast<char*>(MPV_RENDER_API_TYPE_OPENGL)},
|
||||
@@ -345,6 +315,8 @@ inline void RenderPipeline::runLoop() {
|
||||
.str("error", "render init failed: " + glError)
|
||||
.finish());
|
||||
initState_.store(-1);
|
||||
gl_.destroy();
|
||||
ring_.destroy();
|
||||
return;
|
||||
}
|
||||
initState_.store(1);
|
||||
@@ -399,8 +371,7 @@ inline void RenderPipeline::runLoop() {
|
||||
if (texture_) glDeleteTextures(1, &texture_);
|
||||
if (fbo_) glDeleteFramebuffers(1, &fbo_);
|
||||
if (pbos_[0]) glDeleteBuffers(FRAME_SHM_RING_SLOTS, pbos_);
|
||||
CGLSetCurrentContext(nullptr);
|
||||
if (cgl_) CGLDestroyContext(cgl_);
|
||||
gl_.destroy();
|
||||
ring_.destroy();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
* resize creates a fresh shm segment named `<base>-g<generation>` 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
|
||||
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <atomic>
|
||||
@@ -20,6 +23,16 @@ typedef std::atomic<uint64_t> frame_shm_atomic_u64;
|
||||
typedef _Atomic uint64_t frame_shm_atomic_u64;
|
||||
#endif
|
||||
|
||||
/* Monotonic clock for produce_time_ns/heartbeat_ns. Producer (helper) and
|
||||
* consumer (reader addon) MUST use this same clock so age math stays valid.
|
||||
* CLOCK_MONOTONIC is portable across macOS (10.12+) and Linux; the Windows
|
||||
* port will need a QueryPerformanceCounter shim here. */
|
||||
static inline uint64_t frame_shm_now_ns(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
|
||||
}
|
||||
|
||||
#define FRAME_SHM_MAGIC 0x564d5046u /* 'FPMV' */
|
||||
#define FRAME_SHM_VERSION 1u
|
||||
#define FRAME_SHM_RING_SLOTS 3u
|
||||
@@ -27,7 +40,7 @@ typedef _Atomic uint64_t frame_shm_atomic_u64;
|
||||
|
||||
typedef struct {
|
||||
frame_shm_atomic_u64 seq; /* 0 while the slot is being (re)written */
|
||||
uint64_t produce_time_ns; /* CLOCK_MONOTONIC_RAW after copy completes */
|
||||
uint64_t produce_time_ns; /* frame_shm_now_ns() after copy completes */
|
||||
} FrameShmSlot;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
* memory cage forbids external ArrayBuffers over foreign memory.
|
||||
*
|
||||
* Plain C N-API (no node-addon-api) so it stays ABI-stable and trivial.
|
||||
* macOS-only for now — other platforms export an empty object; the
|
||||
* TypeScript side gates on platform before requiring it.
|
||||
* macOS and Linux (POSIX shm) — other platforms export an empty object;
|
||||
* the TypeScript side gates on platform before requiring it.
|
||||
*/
|
||||
#define NAPI_VERSION 8
|
||||
#include <node_api.h>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#if defined(__APPLE__) || defined(__linux__)
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <stdatomic.h>
|
||||
@@ -30,9 +30,8 @@ static FrameShmHeader* g_header = NULL;
|
||||
static uint8_t* g_base = NULL;
|
||||
static size_t g_size = 0;
|
||||
|
||||
static uint64_t now_ns(void) {
|
||||
return clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW);
|
||||
}
|
||||
/* Same clock as the helper's produce_time_ns/heartbeat_ns writers. */
|
||||
static uint64_t now_ns(void) { return frame_shm_now_ns(); }
|
||||
|
||||
static void unmap_current(void) {
|
||||
if (g_base) {
|
||||
@@ -205,11 +204,11 @@ static napi_value Init(napi_env env, napi_value exports) {
|
||||
return exports;
|
||||
}
|
||||
|
||||
#else /* !__APPLE__ */
|
||||
#else /* neither __APPLE__ nor __linux__ */
|
||||
|
||||
static napi_value Init(napi_env env, napi_value exports) {
|
||||
/* Frame-copy is macOS-only for now; loading this module elsewhere
|
||||
* yields an empty exports object and the TS side treats it as
|
||||
/* Frame-copy is not ported to this platform yet; loading this module
|
||||
* here yields an empty exports object and the TS side treats it as
|
||||
* unsupported. */
|
||||
(void)env;
|
||||
return exports;
|
||||
|
||||
+2
-1
@@ -30,7 +30,8 @@ describe('embedded-mpv-frame-copy-platform.util', () => {
|
||||
it.each<[NodeJS.Platform, string, boolean]>([
|
||||
['darwin', 'arm64', true],
|
||||
['darwin', 'x64', false],
|
||||
['linux', 'x64', false],
|
||||
['linux', 'x64', true],
|
||||
['linux', 'arm64', true],
|
||||
['win32', 'x64', false],
|
||||
])('%s/%s -> %s', (platform, arch, expected) => {
|
||||
Object.defineProperty(process, 'platform', { value: platform });
|
||||
|
||||
@@ -4,11 +4,18 @@ import path from 'path';
|
||||
|
||||
/**
|
||||
* Platform gate + helper discovery for the embedded MPV frame-copy engine,
|
||||
* shared by startup and the runtime service so sandbox and engine decisions
|
||||
* cannot drift. These helpers must remain callable before app.whenReady().
|
||||
* shared by startup, the runtime service, and the frame-copy adapter so
|
||||
* sandbox and engine decisions cannot drift. These helpers must remain
|
||||
* callable before app.whenReady().
|
||||
*
|
||||
* macOS stays Apple-Silicon-only. Linux supports every architecture because
|
||||
* its helper renders through headless EGL and links libmpv out of process.
|
||||
*/
|
||||
export function isFrameCopyPlatformSupported(): boolean {
|
||||
return process.platform === 'darwin' && process.arch === 'arm64';
|
||||
return (
|
||||
process.platform === 'linux' ||
|
||||
(process.platform === 'darwin' && process.arch === 'arm64')
|
||||
);
|
||||
}
|
||||
|
||||
function dedupeDefinedPaths(paths: Array<string | undefined>): string[] {
|
||||
|
||||
@@ -173,4 +173,33 @@ describe('EmbeddedMpvFrameCopyAdapter', () => {
|
||||
const withoutHelper = createAdapter(null);
|
||||
expect(withoutHelper.isSupported()).toBe(false);
|
||||
});
|
||||
|
||||
describe('isSupported platform gate', () => {
|
||||
const originalPlatform = process.platform;
|
||||
const originalArch = process.arch;
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
Object.defineProperty(process, 'arch', { value: originalArch });
|
||||
});
|
||||
|
||||
it.each<[NodeJS.Platform, string, boolean]>([
|
||||
['darwin', 'arm64', true],
|
||||
['darwin', 'x64', false],
|
||||
['linux', 'x64', true],
|
||||
['linux', 'arm64', true],
|
||||
['win32', 'x64', false],
|
||||
])(
|
||||
'on %s/%s with a helper binary present -> %s',
|
||||
(platform, arch, expected) => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: platform,
|
||||
});
|
||||
Object.defineProperty(process, 'arch', { value: arch });
|
||||
expect(adapter.isSupported()).toBe(expected);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
EmbeddedMpvFrameSource,
|
||||
ResolvedPortalPlayback,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { isFrameCopyPlatformSupported } from './embedded-mpv-frame-copy-platform.util';
|
||||
import type {
|
||||
NativeEmbeddedMpvAddon,
|
||||
NativeEmbeddedMpvSessionSnapshot,
|
||||
@@ -81,8 +82,7 @@ export class EmbeddedMpvFrameCopyAdapter implements NativeEmbeddedMpvAddon {
|
||||
|
||||
isSupported(): boolean {
|
||||
return (
|
||||
process.platform === 'darwin' &&
|
||||
process.arch === 'arm64' &&
|
||||
isFrameCopyPlatformSupported() &&
|
||||
this.options.resolveHelperPath() !== null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,13 @@ describe('Embedded MPV native source recording invariants', () => {
|
||||
),
|
||||
'utf8'
|
||||
);
|
||||
const frameHelperGlSource = readFileSync(
|
||||
path.resolve(__dirname, '../../../native/helper/frame_helper_gl.h'),
|
||||
'utf8'
|
||||
);
|
||||
const linuxFrameHelperGlSource = frameHelperGlSource.slice(
|
||||
frameHelperGlSource.indexOf('inline void* eglWrapGetProcAddress')
|
||||
);
|
||||
|
||||
function functionBody(name: string): string {
|
||||
return sourceFunctionBody(nativeSource, `Napi::Value ${name}(`, name);
|
||||
@@ -590,7 +597,7 @@ describe('Embedded MPV native source recording invariants', () => {
|
||||
|
||||
it('requires Linux embedded MPV build inputs and validates process isolation in CI', () => {
|
||||
expect(buildAndMakeWorkflowSource).toContain(
|
||||
'libmpv-dev mpv pkg-config'
|
||||
'libmpv-dev mpv pkg-config libegl-dev libgl-dev libopengl-dev libgbm-dev'
|
||||
);
|
||||
expect(buildAndMakeWorkflowSource).toContain(
|
||||
'Stage Linux embedded MPV build inputs'
|
||||
@@ -602,11 +609,35 @@ describe('Embedded MPV native source recording invariants', () => {
|
||||
expect(buildAndMakeWorkflowSource).toContain(
|
||||
'Linux embedded MPV addon must not link directly to libmpv'
|
||||
);
|
||||
expect(buildAndMakeWorkflowSource).toContain(
|
||||
'test -f dist/apps/electron-backend/native/iptvnator_mpv_helper'
|
||||
);
|
||||
expect(buildAndMakeWorkflowSource).toContain(
|
||||
'Linux frame-copy helper must link libmpv'
|
||||
);
|
||||
expect(buildScriptSource).toContain("origin: 'external-mpv-process'");
|
||||
expect(buildScriptSource).toContain('writeLinuxProcessRuntimeManifest');
|
||||
expect(buildScriptSource).toContain('runtimeFiles: []');
|
||||
});
|
||||
|
||||
it('supports Linux system-development inputs without leaving stale frame-copy artifacts', () => {
|
||||
expect(buildScriptSource).toContain(
|
||||
"const systemIncludeDir =\n process.env.LIBMPV_INCLUDE_DIR || '/usr/include';"
|
||||
);
|
||||
expect(buildScriptSource).toContain(
|
||||
'process.env.LINUX_NATIVE_LIBRARY_DIR ||\n defaultLinuxSystemLibDir()'
|
||||
);
|
||||
expect(buildScriptSource).toContain("arm: 'arm-linux-gnueabihf'");
|
||||
expect(buildScriptSource).toContain("arm64: 'aarch64-linux-gnu'");
|
||||
expect(buildScriptSource).toContain("x64: 'x86_64-linux-gnu'");
|
||||
expect(buildScriptSource).toContain(
|
||||
"if (!embeddedMpvRequired && runtime.origin === 'system-dev')"
|
||||
);
|
||||
expect(buildScriptSource).toContain(
|
||||
'removeStaleFrameCopyArtifacts(outputDir);'
|
||||
);
|
||||
});
|
||||
|
||||
it('forces the current frame into a rebuilt shm generation after a paused resize', () => {
|
||||
const runLoop = sourceFunctionBody(
|
||||
frameHelperRenderSource,
|
||||
@@ -620,6 +651,85 @@ describe('Embedded MPV native source recording invariants', () => {
|
||||
'if (targetsRebuilt || (flags & MPV_RENDER_UPDATE_FRAME))'
|
||||
);
|
||||
});
|
||||
|
||||
it('validates complete EGL candidates and keeps software rendering as the final fallback', () => {
|
||||
const tryCandidate = sourceFunctionBody(
|
||||
linuxFrameHelperGlSource,
|
||||
'bool tryCandidate(',
|
||||
'GlContext::tryCandidate'
|
||||
);
|
||||
const create = sourceFunctionBody(
|
||||
linuxFrameHelperGlSource,
|
||||
'bool create(std::string& errorOut)',
|
||||
'GlContext::create'
|
||||
);
|
||||
|
||||
for (const requiredStep of [
|
||||
'eglInitialize(',
|
||||
'eglBindAPI(EGL_OPENGL_API)',
|
||||
'chooseConfig(',
|
||||
'eglCreateContext(',
|
||||
'eglMakeCurrent(',
|
||||
'glGetString(GL_RENDERER)',
|
||||
]) {
|
||||
expect(tryCandidate).toContain(requiredStep);
|
||||
}
|
||||
expect(create).toContain('softwareFallback');
|
||||
expect(create).toContain('candidate.softwareRenderer');
|
||||
expect(create.indexOf('DisplayTier::SurfacelessMesa')).toBeLessThan(
|
||||
create.indexOf('DisplayTier::Default')
|
||||
);
|
||||
expect(create.indexOf('DisplayTier::Default')).toBeLessThan(
|
||||
create.indexOf('DisplayTier::Gbm')
|
||||
);
|
||||
});
|
||||
|
||||
it('binds the desktop GL API on the render thread and releases it before EGL teardown', () => {
|
||||
const makeCurrent = sourceFunctionBody(
|
||||
linuxFrameHelperGlSource,
|
||||
'bool makeCurrent(std::string& errorOut)',
|
||||
'GlContext::makeCurrent'
|
||||
);
|
||||
const destroy = sourceFunctionBody(
|
||||
linuxFrameHelperGlSource,
|
||||
'void destroy()',
|
||||
'GlContext::destroy'
|
||||
);
|
||||
|
||||
expect(makeCurrent).toContain('eglBindAPI(EGL_OPENGL_API)');
|
||||
expect(makeCurrent).toContain('eglMakeCurrent(');
|
||||
expect(makeCurrent).toContain('return false;');
|
||||
|
||||
const bindIndex = destroy.indexOf('eglBindAPI(EGL_OPENGL_API)');
|
||||
const unbindIndex = destroy.indexOf('eglMakeCurrent(');
|
||||
const releaseIndex = destroy.indexOf('eglReleaseThread()');
|
||||
const destroySurfaceIndex = destroy.indexOf('eglDestroySurface(');
|
||||
expect(bindIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(unbindIndex).toBeGreaterThan(bindIndex);
|
||||
expect(releaseIndex).toBeGreaterThan(unbindIndex);
|
||||
expect(destroySurfaceIndex).toBeGreaterThan(releaseIndex);
|
||||
|
||||
const setupGl = sourceFunctionBody(
|
||||
frameHelperRenderSource,
|
||||
'inline bool RenderPipeline::setupGl(std::string& errorOut)',
|
||||
'RenderPipeline::setupGl'
|
||||
);
|
||||
expect(setupGl).toContain(
|
||||
'if (!gl_.makeCurrent(errorOut)) return false;'
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves linked core GL symbols before falling back to EGL extension lookup', () => {
|
||||
const dlsymIndex = linuxFrameHelperGlSource.indexOf(
|
||||
'dlsym(RTLD_DEFAULT, name)'
|
||||
);
|
||||
const eglIndex = linuxFrameHelperGlSource.indexOf(
|
||||
'eglGetProcAddress(name)'
|
||||
);
|
||||
|
||||
expect(dlsymIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(eglIndex).toBeGreaterThan(dlsymIndex);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Embedded MPV native build configuration', () => {
|
||||
@@ -633,6 +743,10 @@ describe('Embedded MPV native build configuration', () => {
|
||||
(candidate: { target_name?: string }) =>
|
||||
candidate.target_name === 'embedded_mpv'
|
||||
);
|
||||
const helperTarget = bindingGyp.targets.find(
|
||||
(candidate: { target_name?: string }) =>
|
||||
candidate.target_name === 'iptvnator_mpv_helper'
|
||||
);
|
||||
|
||||
it('declares platform-specific native sources for macOS, Windows, and Linux', () => {
|
||||
expect(target).toBeDefined();
|
||||
@@ -640,4 +754,24 @@ describe('Embedded MPV native build configuration', () => {
|
||||
expect(JSON.stringify(target)).toContain('src/embedded_mpv_win32.cc');
|
||||
expect(JSON.stringify(target)).toContain('src/embedded_mpv_linux.cc');
|
||||
});
|
||||
|
||||
it('links libmpv and headless GL only into the Linux helper process', () => {
|
||||
const linuxAddonConfig = target.conditions.find(
|
||||
([condition]: [string]) => condition === 'OS=="linux"'
|
||||
)?.[1];
|
||||
const linuxHelperConfig = helperTarget.conditions.find(
|
||||
([condition]: [string]) => condition === 'OS=="linux"'
|
||||
)?.[1];
|
||||
|
||||
expect(linuxAddonConfig?.libraries).not.toContain('-lmpv');
|
||||
expect(linuxHelperConfig?.libraries).toEqual(
|
||||
expect.arrayContaining([
|
||||
'-lmpv',
|
||||
'-lEGL',
|
||||
'-lOpenGL',
|
||||
'-lgbm',
|
||||
'-ldl',
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -297,6 +297,140 @@ describe('EmbeddedMpvNativeService power blocker', () => {
|
||||
}
|
||||
);
|
||||
|
||||
describe('frame-copy platform gate', () => {
|
||||
const originalArch = process.arch;
|
||||
|
||||
function mockHelperPresent(): void {
|
||||
jest.spyOn(
|
||||
service as unknown as {
|
||||
resolveFrameCopyHelperPath: () => string | null;
|
||||
},
|
||||
'resolveFrameCopyHelperPath'
|
||||
).mockReturnValue('/native/iptvnator_mpv_helper');
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'arch', { value: originalArch });
|
||||
delete process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY;
|
||||
});
|
||||
|
||||
it('activates the frame-copy engine on Linux, even under native Wayland', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
// Native Wayland session (no X11 ozone): blocks the --wid native
|
||||
// engine, but the frame-copy helper renders offscreen into shm
|
||||
// and must stay available.
|
||||
process.env.DISPLAY = ':0';
|
||||
process.env.WAYLAND_DISPLAY = 'wayland-0';
|
||||
process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY = '1';
|
||||
mockHelperPresent();
|
||||
|
||||
expect(service.getActiveEngine()).toBe('frame-copy');
|
||||
expect(service.isFrameCopyAvailable()).toBe(true);
|
||||
expect(service.getSupport()).toEqual(
|
||||
expect.objectContaining({
|
||||
supported: true,
|
||||
engine: 'frame-copy',
|
||||
frameCopyAvailable: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
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';
|
||||
mockHelperPresent();
|
||||
|
||||
expect(service.getActiveEngine()).toBe('frame-copy');
|
||||
expect(service.isFrameCopyAvailable()).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the frame-copy engine Apple-Silicon-only on macOS', () => {
|
||||
Object.defineProperty(process, 'arch', { value: 'x64' });
|
||||
process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY = '1';
|
||||
mockHelperPresent();
|
||||
|
||||
expect(service.getActiveEngine()).toBe('native');
|
||||
expect(service.isFrameCopyAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
it('skips the native window handle when creating a frame-copy session', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY = '1';
|
||||
mockHelperPresent();
|
||||
const frameCopyAddon = createMockAddon();
|
||||
frameCopyAddon.createSession.mockReturnValueOnce('s-fc');
|
||||
frameCopyAddon.getSessionSnapshot.mockReturnValueOnce(
|
||||
snapshot('loading')
|
||||
);
|
||||
(
|
||||
service as unknown as { frameCopyAdapter: MockAddon }
|
||||
).frameCopyAdapter = frameCopyAddon;
|
||||
|
||||
service.createSession(BOUNDS, '', 1);
|
||||
|
||||
expect(mainWindowGetNativeWindowHandleMock).not.toHaveBeenCalled();
|
||||
expect(frameCopyAddon.createSession).toHaveBeenCalledWith(
|
||||
Buffer.alloc(0),
|
||||
BOUNDS,
|
||||
'',
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not acquire a blocker for a loading session', () => {
|
||||
startSession('s1', snapshot('loading'));
|
||||
expect(powerSaveBlockerMock.start).not.toHaveBeenCalled();
|
||||
|
||||
@@ -111,9 +111,9 @@ export class EmbeddedMpvNativeService {
|
||||
|
||||
/**
|
||||
* Frame-copy engine: helper process + shm ring + renderer canvas.
|
||||
* Experimental, macOS Apple Silicon only (owner decision 2026-07-10),
|
||||
* opted into with IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY=1 on top of
|
||||
* the regular embedded MPV experiment flag.
|
||||
* Experimental, macOS Apple Silicon (owner decision 2026-07-10) and
|
||||
* Linux, opted into with IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY=1 on
|
||||
* top of the regular embedded MPV experiment flag.
|
||||
*/
|
||||
private isFrameCopyEngineRequested(): boolean {
|
||||
return ['1', 'true', 'yes', 'on'].includes(
|
||||
@@ -215,11 +215,21 @@ export class EmbeddedMpvNativeService {
|
||||
};
|
||||
}
|
||||
|
||||
if (this.isUnsupportedLinuxDisplayServer()) {
|
||||
// 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. 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()
|
||||
) {
|
||||
return {
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -231,16 +241,6 @@ export class EmbeddedMpvNativeService {
|
||||
};
|
||||
}
|
||||
|
||||
const missingLinuxMpvExecutableReason =
|
||||
this.getMissingLinuxMpvExecutableReason();
|
||||
if (missingLinuxMpvExecutableReason) {
|
||||
return {
|
||||
supported: false,
|
||||
platform: process.platform,
|
||||
reason: missingLinuxMpvExecutableReason,
|
||||
};
|
||||
}
|
||||
|
||||
// A requested-but-unavailable frame-copy engine (wrong platform or
|
||||
// missing helper) intentionally falls through to the native path so
|
||||
// embedded MPV keeps working and Settings can clear the opt-in.
|
||||
@@ -254,6 +254,17 @@ export class EmbeddedMpvNativeService {
|
||||
};
|
||||
}
|
||||
|
||||
const missingLinuxMpvExecutableReason =
|
||||
this.getMissingLinuxMpvExecutableReason();
|
||||
if (missingLinuxMpvExecutableReason) {
|
||||
return {
|
||||
supported: false,
|
||||
platform: process.platform,
|
||||
reason: missingLinuxMpvExecutableReason,
|
||||
frameCopyAvailable: this.isFrameCopyAvailable(),
|
||||
};
|
||||
}
|
||||
|
||||
if (this.addon) {
|
||||
try {
|
||||
if (!this.addon.isSupported()) {
|
||||
@@ -385,7 +396,16 @@ export class EmbeddedMpvNativeService {
|
||||
): EmbeddedMpvSession {
|
||||
this.assertEmbeddedMpvEnabled();
|
||||
const addon = this.getAddon();
|
||||
const windowHandle = this.getMainWindowHandle();
|
||||
// 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. 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,
|
||||
|
||||
@@ -9,7 +9,7 @@ export const VLC_PLAYER_ARGUMENTS = 'VLC_PLAYER_ARGUMENTS';
|
||||
export const MPV_REUSE_INSTANCE = 'MPV_REUSE_INSTANCE';
|
||||
export const VLC_REUSE_INSTANCE = 'VLC_REUSE_INSTANCE';
|
||||
/**
|
||||
* Embedded MPV frame-copy engine opt-in (macOS arm64). Lives in the main
|
||||
* Embedded MPV frame-copy engine opt-in (macOS arm64, Linux). Lives in the main
|
||||
* process config file because it must be readable synchronously before the
|
||||
* BrowserWindow is created — the engine relaxes the window sandbox for its
|
||||
* preload frame pump, which cannot change after window creation.
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "MPV المدمج: محرك نسخ الإطارات (تجريبي)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "يُرسم الفيديو على لوحة التطبيق بحيث تظهر القوائم ومربعات الحوار فوقه. متاح فقط على macOS بمعالج Apple Silicon.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "يُرسم الفيديو على لوحة التطبيق بحيث تظهر القوائم ومربعات الحوار فوقه. متاح فقط على macOS بمعالج Apple Silicon وعلى Linux.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "يسري المفعول بعد إعادة تشغيل التطبيق."
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "MPV المدمج: محرك نسخ الإطارات (تجريبي)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "الفيديو كيترسم على لوحة التطبيق باش القوائم والحوارات يبانو فوقو. غير على macOS ب Apple Silicon.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "الفيديو كيترسم على لوحة التطبيق باش القوائم والحوارات يبانو فوقو. غير على macOS ب Apple Silicon و Linux.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "غادي يخدم من بعد ما تعاود تشغل التطبيق."
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "Embedded MPV: рухавік frame-copy (эксперыментальна)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "Відэа малюецца на канвасе праграмы, таму меню і дыялогі паказваюцца па-над ім. Толькі macOS на Apple Silicon.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "Відэа малюецца на канвасе праграмы, таму меню і дыялогі паказваюцца па-над ім. Толькі macOS на Apple Silicon і Linux.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "Пачне дзейнічаць пасля перазапуску праграмы."
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "Embedded MPV: Frame-Copy-Engine (experimentell)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "Das Video wird auf die App-Leinwand gerendert, sodass Menüs und Dialoge darüber liegen. Nur macOS auf Apple Silicon.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "Das Video wird auf die App-Leinwand gerendert, sodass Menüs und Dialoge darüber liegen. Nur macOS auf Apple Silicon und Linux.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "Wird nach einem Neustart der App wirksam."
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "Embedded MPV: μηχανή frame-copy (πειραματικό)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "Το βίντεο σχεδιάζεται στον καμβά της εφαρμογής, ώστε τα μενού και τα παράθυρα διαλόγου να εμφανίζονται από πάνω του. Μόνο macOS με Apple Silicon.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "Το βίντεο σχεδιάζεται στον καμβά της εφαρμογής, ώστε τα μενού και τα παράθυρα διαλόγου να εμφανίζονται από πάνω του. Μόνο macOS με Apple Silicon και Linux.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "Ισχύει μετά την επανεκκίνηση της εφαρμογής."
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "Embedded MPV: frame-copy engine (experimental)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "Render video onto the app canvas so menus and dialogs stack above it. macOS Apple Silicon only.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "Render video onto the app canvas so menus and dialogs stack above it. macOS (Apple Silicon) and Linux only.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "Takes effect after restarting the app."
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "Embedded MPV: motor frame-copy (experimental)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "El vídeo se dibuja en el lienzo de la aplicación, de modo que los menús y diálogos quedan encima. Solo macOS con Apple Silicon.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "El vídeo se dibuja en el lienzo de la aplicación, de modo que los menús y diálogos quedan encima. Solo macOS con Apple Silicon y Linux.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "Se aplica tras reiniciar la aplicación."
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "Embedded MPV : moteur frame-copy (expérimental)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "La vidéo est rendue sur le canevas de l'application, les menus et boîtes de dialogue s'affichent donc au-dessus. macOS Apple Silicon uniquement.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "La vidéo est rendue sur le canevas de l'application, les menus et boîtes de dialogue s'affichent donc au-dessus. macOS Apple Silicon et Linux uniquement.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "Prend effet après le redémarrage de l'application."
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "Embedded MPV: motore frame-copy (sperimentale)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "Il video viene disegnato sul canvas dell'app, così menu e finestre di dialogo appaiono sopra di esso. Solo macOS con Apple Silicon.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "Il video viene disegnato sul canvas dell'app, così menu e finestre di dialogo appaiono sopra di esso. Solo macOS con Apple Silicon e Linux.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "Ha effetto dopo il riavvio dell'app."
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "埋め込みMPV:フレームコピーエンジン(実験的)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "映像をアプリのキャンバスに描画するため、メニューやダイアログをその上に表示できます。Apple Silicon搭載macOSのみ。",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "映像をアプリのキャンバスに描画するため、メニューやダイアログをその上に表示できます。Apple Silicon搭載macOSおよびLinuxのみ。",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "アプリの再起動後に有効になります。"
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "내장 MPV: 프레임 복사 엔진(실험적)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "비디오를 앱 캔버스에 렌더링하여 메뉴와 대화 상자가 그 위에 표시됩니다. Apple Silicon macOS 전용.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "비디오를 앱 캔버스에 렌더링하여 메뉴와 대화 상자가 그 위에 표시됩니다. Apple Silicon macOS 및 Linux 전용.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "앱을 다시 시작한 후 적용됩니다."
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "Embedded MPV: frame-copy-engine (experimenteel)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "De video wordt op het app-canvas getekend zodat menu's en dialoogvensters erboven verschijnen. Alleen macOS op Apple Silicon.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "De video wordt op het app-canvas getekend zodat menu's en dialoogvensters erboven verschijnen. Alleen macOS op Apple Silicon en Linux.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "Wordt van kracht na het herstarten van de app."
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "Embedded MPV: silnik frame-copy (eksperymentalny)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "Wideo jest rysowane na kanwie aplikacji, dzięki czemu menu i okna dialogowe wyświetlają się nad nim. Tylko macOS na Apple Silicon.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "Wideo jest rysowane na kanwie aplikacji, dzięki czemu menu i okna dialogowe wyświetlają się nad nim. Tylko macOS na Apple Silicon i Linux.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "Zacznie działać po ponownym uruchomieniu aplikacji."
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "Embedded MPV: motor frame-copy (experimental)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "O vídeo é desenhado na tela da aplicação, para que menus e diálogos fiquem por cima. Apenas macOS com Apple Silicon.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "O vídeo é desenhado na tela da aplicação, para que menus e diálogos fiquem por cima. Apenas macOS com Apple Silicon e Linux.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "Entra em vigor após reiniciar a aplicação."
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Открыть GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "Что нового",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "Embedded MPV: движок frame-copy (экспериментально)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "Видео рисуется на канвасе приложения, поэтому меню и диалоги отображаются поверх него. Только macOS на Apple Silicon.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "Видео рисуется на канвасе приложения, поэтому меню и диалоги отображаются поверх него. Только macOS на Apple Silicon и Linux.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "Вступит в силу после перезапуска приложения."
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "Embedded MPV: frame-copy motoru (deneysel)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "Video uygulama tuvaline çizilir, böylece menüler ve iletişim kutuları üzerinde görüntülenir. Yalnızca Apple Silicon macOS.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "Video uygulama tuvaline çizilir, böylece menüler ve iletişim kutuları üzerinde görüntülenir. Yalnızca Apple Silicon macOS ve Linux.",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "Uygulama yeniden başlatıldıktan sonra geçerli olur."
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "嵌入式 MPV:帧拷贝引擎(实验性)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "视频渲染到应用画布上,菜单和对话框可以显示在其上方。仅限 Apple Silicon 的 macOS。",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "视频渲染到应用画布上,菜单和对话框可以显示在其上方。仅限 Apple Silicon 的 macOS 和 Linux。",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "重启应用后生效。"
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
@@ -430,7 +430,7 @@
|
||||
"APP_UPDATE_OPEN_RELEASE": "Open GitHub release",
|
||||
"APP_UPDATE_RELEASE_NOTES": "What's new",
|
||||
"EMBEDDED_MPV_FRAME_COPY": "內嵌 MPV:幀複製引擎(實驗性)",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "影片會繪製到應用程式畫布上,選單和對話框可顯示在其上方。僅限 Apple Silicon 的 macOS。",
|
||||
"EMBEDDED_MPV_FRAME_COPY_DESCRIPTION": "影片會繪製到應用程式畫布上,選單和對話框可顯示在其上方。僅限 Apple Silicon 的 macOS 和 Linux。",
|
||||
"EMBEDDED_MPV_FRAME_COPY_RESTART": "重新啟動應用程式後生效。"
|
||||
},
|
||||
"THEMES": {
|
||||
|
||||
Reference in new issue
Block a user