mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 04:26:16 -08:00
* 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>
140 lines
4.8 KiB
JavaScript
140 lines
4.8 KiB
JavaScript
/*
|
|
* Linux frame-copy probe: spawns iptvnator_mpv_helper, attaches the
|
|
* embedded_mpv_frame_reader addon to the announced shm generation, and
|
|
* reports producer fps, copy latency (ageMs), copy wall time, torn reads
|
|
* and pixel spread. Usage:
|
|
* node frame-probe.mjs <url> <width> <height> <seconds> [--hwdec auto]
|
|
*/
|
|
import { spawn } from 'node:child_process';
|
|
import { createRequire } from 'node:module';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const releaseDir = process.env.RELEASE_DIR;
|
|
const [url, width = '1280', height = '720', seconds = '5', ...rest] =
|
|
process.argv.slice(2);
|
|
if (!url || !releaseDir) {
|
|
console.error('usage: RELEASE_DIR=<native build dir> node frame-probe.mjs <url> [w] [h] [s]');
|
|
process.exit(1);
|
|
}
|
|
const hwdecIdx = rest.indexOf('--hwdec');
|
|
const hwdec = hwdecIdx >= 0 ? rest[hwdecIdx + 1] : null;
|
|
|
|
const reader = require(path.join(releaseDir, 'embedded_mpv_frame_reader.node'));
|
|
const helperPath = path.join(releaseDir, 'iptvnator_mpv_helper');
|
|
const shmBase = `/impv-probe-${process.pid}`;
|
|
|
|
const args = ['--shm-base', shmBase, '--width', width, '--height', height];
|
|
if (hwdec) args.push('--hwdec', hwdec);
|
|
const child = spawn(helperPath, args, { stdio: ['pipe', 'pipe', 'inherit'] });
|
|
|
|
let buffer = '';
|
|
let currentSource = null;
|
|
const generations = [];
|
|
let statusLog = [];
|
|
|
|
child.stdout.on('data', (chunk) => {
|
|
buffer += chunk.toString();
|
|
let i;
|
|
while ((i = buffer.indexOf('\n')) >= 0) {
|
|
const line = buffer.slice(0, i);
|
|
buffer = buffer.slice(i + 1);
|
|
if (!line.trim()) continue;
|
|
let ev;
|
|
try { ev = JSON.parse(line); } catch { continue; }
|
|
if (ev.event === 'shm') {
|
|
generations.push(`${ev.name} ${ev.width}x${ev.height}`);
|
|
currentSource = ev;
|
|
} else if (ev.event === 'snapshot') {
|
|
if (statusLog[statusLog.length - 1] !== ev.status) statusLog.push(ev.status);
|
|
} else if (ev.event === 'fatal') {
|
|
console.error('FATAL:', ev.error);
|
|
process.exit(2);
|
|
}
|
|
}
|
|
});
|
|
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
await sleep(300);
|
|
child.stdin.write(`load\turl=${url.replace(/%/g, '%25')}\n`);
|
|
|
|
// Wait for playback + a possibly-resized generation to settle.
|
|
const settleDeadline = Date.now() + 10000;
|
|
while (Date.now() < settleDeadline) {
|
|
await sleep(100);
|
|
if (statusLog.includes('playing') && currentSource) {
|
|
await sleep(700); // allow the aspect-fit resize generation to land
|
|
break;
|
|
}
|
|
}
|
|
if (!currentSource) {
|
|
console.error('no shm source announced');
|
|
child.kill('SIGKILL');
|
|
process.exit(2);
|
|
}
|
|
|
|
const info = reader.open(currentSource.name);
|
|
console.log(`attached ${currentSource.name}: ${info.width}x${info.height} stride=${info.stride} gen=${info.generation}`);
|
|
|
|
const frameBuf = new ArrayBuffer(info.frameBytes);
|
|
let lastSeq = 0;
|
|
let frames = 0;
|
|
let torn = 0;
|
|
const ages = [];
|
|
const copyTimes = [];
|
|
const durationMs = Number(seconds) * 1000;
|
|
const start = Date.now();
|
|
let pixelMin = 255, pixelMax = 0;
|
|
|
|
while (Date.now() - start < durationMs) {
|
|
const seq = reader.latestSeq();
|
|
if (seq !== lastSeq) {
|
|
const t0 = process.hrtime.bigint();
|
|
const result = reader.copyLatest(frameBuf);
|
|
const t1 = process.hrtime.bigint();
|
|
if (result) {
|
|
frames += 1;
|
|
lastSeq = result.seq;
|
|
ages.push(result.ageMs);
|
|
copyTimes.push(Number(t1 - t0) / 1e6);
|
|
if (result.torn) torn += 1;
|
|
}
|
|
}
|
|
await sleep(2);
|
|
}
|
|
const elapsed = (Date.now() - start) / 1000;
|
|
|
|
// pixel spread over the last frame
|
|
const view = new Uint8Array(frameBuf);
|
|
for (let i = 0; i < view.length; i += 4001 * 4) {
|
|
for (let c = 0; c < 3; c++) {
|
|
const v = view[i + c];
|
|
if (v < pixelMin) pixelMin = v;
|
|
if (v > pixelMax) pixelMax = v;
|
|
}
|
|
}
|
|
|
|
const pct = (arr, p) => {
|
|
if (!arr.length) return NaN;
|
|
const sorted = [...arr].sort((a, b) => a - b);
|
|
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
|
|
};
|
|
const avg = (arr) => arr.reduce((a, b) => a + b, 0) / (arr.length || 1);
|
|
|
|
console.log(`generations seen: ${generations.join(' | ')}`);
|
|
console.log(`status transitions: ${statusLog.join(' -> ')}`);
|
|
console.log(`producerAliveMs: ${reader.producerAliveMs().toFixed(1)}`);
|
|
console.log(`new frames: ${frames} in ${elapsed.toFixed(1)}s = ${(frames / elapsed).toFixed(1)} fps`);
|
|
console.log(`copy ms avg/p95: ${avg(copyTimes).toFixed(2)} / ${pct(copyTimes, 0.95).toFixed(2)}`);
|
|
console.log(`age ms avg/p95: ${avg(ages).toFixed(2)} / ${pct(ages, 0.95).toFixed(2)}`);
|
|
console.log(`torn: ${torn}`);
|
|
console.log(`pixel spread (BGR sampled): min=${pixelMin} max=${pixelMax}`);
|
|
|
|
child.stdin.write('quit\n');
|
|
child.stdin.end();
|
|
await sleep(500);
|
|
if (child.exitCode === null) child.kill('SIGKILL');
|
|
reader.close();
|