aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-client
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-client')
-rw-r--r--packages/meshbay-client/src/main.js170
1 files changed, 169 insertions, 1 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js
index 122a302..755f1f7 100644
--- a/packages/meshbay-client/src/main.js
+++ b/packages/meshbay-client/src/main.js
@@ -41,6 +41,19 @@ const { pathToFileURL } = require('node:url');
// runs (`electron .`) consistent with it.
app.commandLine.appendSwitch('class', 'MeshBay');
+// Chromium reads `--enable-features` and `--disable-features` as one
+// comma-joined list each, and `appendSwitch` **replaces** that list rather
+// than adding to it. Two call sites therefore cancel the first silently: no
+// error, nothing in a log, just a feature that is quietly not on. The
+// hardware-decode section further down is the second caller, so both go
+// through this — and so must the third.
+function addFeatures(kind, names) {
+ const current = app.commandLine.getSwitchValue(kind);
+ const merged = new Set(current ? current.split(',').filter(Boolean) : []);
+ for (const name of names) merged.add(name);
+ app.commandLine.appendSwitch(kind, [...merged].join(','));
+}
+
// Chromium publishes host candidates as random `.local` mDNS names rather
// than as the real local IP. Every peer this client talks to is a node running
// aiortc/aioice, and aioice has no mDNS resolver on any platform: it logs
@@ -56,7 +69,7 @@ app.commandLine.appendSwitch('class', 'MeshBay');
// *client*; the platform that matters is the peer's, not ours, and the peer is
// always a node. The trade is that our private address reaches the hub and the
// node in the SDP — both the user's own infrastructure, not an arbitrary page.
-app.commandLine.appendSwitch('disable-features', 'WebRtcHideLocalIpsWithMdns');
+addFeatures('disable-features', ['WebRtcHideLocalIpsWithMdns']);
const UI_DIR = path.join(__dirname, '..', 'ui');
const SCHEME = 'app';
@@ -245,6 +258,159 @@ function writeConfig(next) {
let config = readConfig();
+// ── Hardware video decoding (Linux) ─────────────────────────────────────────
+//
+// Chromium ships VA-API off on Linux; macOS and Windows decode H.264 in
+// hardware without being asked, so everything here is Linux-only.
+//
+// It pays off exactly where it is needed. A 1080p H.264 film is the streaming
+// path's normal case — the node hands the bytes over untouched (`-c:v copy`,
+// webrtc_server.py) and all the decoding happens here — and every Intel iGPU
+// of the last decade decodes that in fixed-function silicon, while an
+// Atom/Celeron-class CPU cannot hold 25 fps at 1080p in software.
+//
+// Three decisions, written down because each replaces something a person
+// would otherwise have to do by hand:
+//
+// - **Detected, not configured.** One package installs on every machine, so
+// the switches cannot be chosen at build time and there is no second
+// "low-power" package. The probe is two filesystem checks and runs before
+// `whenReady`, because Chromium reads its command line exactly once.
+//
+// - **The feature name is not stable, so it is not trusted.**
+// `VaapiVideoDecodeLinuxGL` has been renamed across Chromium releases, and
+// `build-client.sh` deliberately rebuilds against the *latest* Electron
+// every time — a name pinned here would stop matching one bump later with
+// nothing failing to say so. Both known names are passed (Chromium ignores
+// an unknown one) and the outcome is measured rather than assumed.
+//
+// - **The verdict is measured, remembered, and re-opened by a Chromium
+// bump.** `measureVideoDecoding` asks the renderer whether the codec the
+// node really streams decodes `powerEfficient`ly — Chromium's own answer to
+// "is this hardware", which is what reading it out of devtools would have
+// told you. A no moves to the next set of switches on the next launch; when
+// the list runs out they are dropped altogether, so `--ignore-gpu-blocklist`
+// cannot leave a machine with a broken GL stack paying for a feature it
+// never got.
+//
+// `videoAcceleration` in config.json overrides the lot: "off" never applies
+// anything, "on" applies it even where the probe finds no driver.
+
+const VAAPI_FEATURES = ['VaapiVideoDecodeLinuxGL', 'AcceleratedVideoDecodeLinuxGL'];
+
+// One per launch, in order, until the measurement says hardware. These are not
+// a guess about *this* Chromium — they are the ways its GL backend has had to
+// be named, and the machine picks which one is true for it.
+const VAAPI_ATTEMPTS = [
+ [],
+ [['use-gl', 'egl']],
+ [['use-gl', 'angle'], ['use-angle', 'gl']],
+];
+
+function vaapiDriverInstalled() {
+ const dirs = [process.env.LIBVA_DRIVERS_PATH, '/usr/lib/x86_64-linux-gnu/dri',
+ '/usr/lib/aarch64-linux-gnu/dri', '/usr/lib64/dri', '/usr/lib/dri'];
+ for (const dir of dirs) {
+ if (!dir) continue;
+ try {
+ if (fs.readdirSync(dir).some((f) => f.endsWith('_drv_video.so'))) return true;
+ } catch { /* no such directory on this distribution */ }
+ }
+ return false;
+}
+
+// Readable *and* writable: VA-API maps buffers on the render node. On a
+// desktop session logind grants that through an ACL rather than through group
+// membership, so asking the kernel answers "can this process use it", which is
+// the question — rather than "is this user in `render`", which is not.
+function renderNode() {
+ try {
+ for (const name of fs.readdirSync('/dev/dri')) {
+ if (!name.startsWith('renderD')) continue;
+ const dev = `/dev/dri/${name}`;
+ try {
+ fs.accessSync(dev, fs.constants.R_OK | fs.constants.W_OK);
+ return dev;
+ } catch { /* another GPU may still be usable */ }
+ }
+ } catch { /* no /dev/dri at all */ }
+ return null;
+}
+
+// Which attempt this launch is running, or null when no switches were applied
+// — which is also what stops a second window measuring the same launch twice.
+let vaapiAttempt = null;
+
+function configureVideoDecoding() {
+ if (process.platform !== 'linux') return;
+ const mode = config.videoAcceleration || 'auto';
+ if (mode === 'off') return;
+ const forced = mode === 'on';
+ if (!forced && !(renderNode() && vaapiDriverInstalled())) {
+ console.log('[video] no usable VA-API driver — decoding in software');
+ return;
+ }
+ const probe = config.videoDecodeProbe || {};
+ const known = probe.chrome === process.versions.chrome;
+ let attempt = (known && typeof probe.attempt === 'number') ? probe.attempt : 0;
+ if (attempt >= VAAPI_ATTEMPTS.length) {
+ if (!forced) {
+ console.log('[video] VA-API never took on this machine — decoding in software');
+ return;
+ }
+ // "on" is the person overruling the measurement, so the list running out
+ // is not an answer here — it keeps the last set rather than climbing an
+ // index nothing will ever read again.
+ attempt = VAAPI_ATTEMPTS.length - 1;
+ }
+ addFeatures('enable-features', VAAPI_FEATURES);
+ app.commandLine.appendSwitch('ignore-gpu-blocklist');
+ for (const [name, value] of VAAPI_ATTEMPTS[attempt])
+ app.commandLine.appendSwitch(name, value);
+ vaapiAttempt = attempt;
+}
+
+// `avc1.640029` is H.264 High 4.1 — what the node announces for a re-encode
+// (webrtc_server.py) and the profile a copied 1080p film carries. Asking about
+// the codec that is actually streamed is the point: a machine can decode
+// H.264 in hardware and still answer no for HEVC, and the reverse.
+const DECODE_PROBE = `navigator.mediaCapabilities.decodingInfo({
+ type: 'media-source',
+ video: {
+ contentType: 'video/mp4; codecs="avc1.640029"',
+ width: 1920, height: 1080, bitrate: 5000000, framerate: 25,
+ },
+}).then((r) => !!r.powerEfficient).catch(() => false)`;
+
+async function measureVideoDecoding(win) {
+ const attempt = vaapiAttempt;
+ vaapiAttempt = null;
+ if (attempt === null) return;
+ let hardware;
+ try {
+ hardware = await win.webContents.executeJavaScript(DECODE_PROBE);
+ } catch {
+ return; // the window went away; the question is asked again next launch
+ }
+ const gpu = app.getGPUFeatureStatus().video_decode;
+ console.log(`[video] hardware decoding: ${hardware} `
+ + `(attempt ${attempt}, gpu video_decode: ${gpu})`);
+ config = {
+ ...config,
+ videoDecodeProbe: {
+ chrome: process.versions.chrome,
+ attempt: hardware ? attempt : attempt + 1,
+ hardware,
+ gpu,
+ },
+ };
+ writeConfig(config);
+ if (!hardware && attempt + 1 < VAAPI_ATTEMPTS.length)
+ console.log('[video] another GL backend will be tried on the next launch');
+}
+
+configureVideoDecoding();
+
// ── Secrets ─────────────────────────────────────────────────────────────────
//
// The OS keychain, through safeStorage. What it protects and what it does not
@@ -482,6 +648,8 @@ function createWindow() {
});
win.once('ready-to-show', () => win.show());
+ // Once per launch, and only when the section above applied switches.
+ win.webContents.once('did-finish-load', () => { measureVideoDecoding(win); });
// Some Wayland compositors (observed under GNOME/Mutter on a VM with a
// virtio-gpu device whose command-buffer creation fails) never schedule a
// first paint for a surface that isn't mapped yet — but Electron won't map