Add native self-hosted instance connection to fluxer_desktop

Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 +
tools/ci) with a "Connect to a Different Server" menu item and popout
that lets the desktop app switch to any self-hosted Fluxer instance,
plus fixes for well-known discovery on single-domain self-hosted
deployments and a false-positive ERR_ABORTED on same-origin client
redirects during the switch. Defaults to chat.fluxr.chat and uses an
isolated userData directory from the official build.
This commit is contained in:
2026-07-01 18:22:43 -04:00
commit 682afacd30
1763 changed files with 613720 additions and 0 deletions
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {execFileSync} from 'node:child_process';
import {copyFileSync} from 'node:fs';
import {createRequire} from 'node:module';
import {setTimeout as sleep} from 'node:timers/promises';
const require = createRequire(import.meta.url);
const work = process.env.FLX_WORK;
const addonNode = `${work}/flx_direct_addon.node`;
copyFileSync(process.env.FLX_ADDON_SO, addonNode);
const addon = require(addonNode);
const results = [];
function record(name, ok, detail) {
results.push({name, ok});
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` -- ${detail}` : ''}`);
}
function pwDump() {
return JSON.parse(execFileSync('pw-dump', {encoding: 'utf8', maxBuffer: 64e6}));
}
function directSinkNode(dump) {
return dump.find(
(o) => o.type === 'PipeWire:Interface:Node' && /^fluxer-direct-capture-/.test(o.info?.props?.['node.name'] || ''),
);
}
async function waitFor(p, ms, step = 150) {
const end = Date.now() + ms;
let last;
while (Date.now() < end) {
last = p();
if (last) return last;
await sleep(step);
}
return last;
}
function rms(samples) {
if (samples.length === 0) return 0;
let sum = 0;
for (const s of samples) sum += s * s;
return Math.sqrt(sum / samples.length);
}
async function main() {
const dc = new addon.DirectAudioCapture();
let lifecycle = null;
dc.setLifecycleCallback((...args) => {
const flat = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
lifecycle = {kind: flat[0], msg: flat[1]};
});
const started = dc.start({include: [{'application.name': 'Music Player Demo'}]});
record('DirectAudioCapture.start(include rule) accepted', started === true);
const sinkUp = await waitFor(() => {
const d = pwDump();
return directSinkNode(d) ? d : null;
}, 8000);
record(
'hidden private sink fluxer-direct-capture-* created',
!!sinkUp,
sinkUp ? directSinkNode(sinkUp).info.props['node.name'] : 'timeout',
);
if (sinkUp) {
const sinkProps = directSinkNode(sinkUp).info.props;
record(
'private sink node.hidden=true (not user-visible)',
String(sinkProps['node.hidden']) === 'true',
`node.hidden=${sinkProps['node.hidden']}`,
);
record('private sink media.class=Audio/Sink', sinkProps['media.class'] === 'Audio/Sink', sinkProps['media.class']);
}
let maxRms = 0;
let frames = 0;
let totalSamples = 0;
const captureDeadline = Date.now() + 6000;
while (Date.now() < captureDeadline) {
const f = dc.read();
if (f) {
const samples = new Float32Array(f.samples);
frames += 1;
totalSamples += samples.length;
maxRms = Math.max(maxRms, rms(samples));
if (maxRms > 0.01 && frames > 5) break;
}
await sleep(20);
}
record('DirectAudioCapture yields real frames', frames > 0, `${frames} frames, ${totalSamples} samples`);
record('captured audio is non-silent (real 440Hz tone tapped)', maxRms > 0.01, `peak rms=${maxRms.toFixed(4)}`);
const dump = pwDump();
const sink = directSinkNode(dump);
const sinkId = Number(sink?.id);
const linkSrcNodes = new Set(
dump
.filter((o) => o.type === 'PipeWire:Interface:Link' && Number(o.info?.props?.['link.input.node']) === sinkId)
.map((o) => Number(o.info?.props?.['link.output.node'])),
);
const fluxerStreamIds = dump
.filter((o) => o.type === 'PipeWire:Interface:Node' && o.info?.props?.['application.name'] === 'Fluxer')
.map((o) => Number(o.id));
record(
'Fluxer-named app excluded from per-process capture',
fluxerStreamIds.every((id) => !linkSrcNodes.has(id)),
`fluxer=${fluxerStreamIds} linkedSrc=${[...linkSrcNodes]}`,
);
const musicIds = dump
.filter((o) => o.type === 'PipeWire:Interface:Node' && o.info?.props?.['application.name'] === 'Music Player Demo')
.map((o) => Number(o.id));
record(
'targeted app IS linked to the private sink',
musicIds.some((id) => linkSrcNodes.has(id)),
`music=${musicIds} linkedSrc=${[...linkSrcNodes]}`,
);
dc.stop();
await sleep(600);
record('stop() emits closed-clean lifecycle', lifecycle?.kind === 'closed-clean', JSON.stringify(lifecycle));
const afterStop = pwDump();
const sinkAfter = directSinkNode(afterStop);
const residualLinks = sinkAfter
? afterStop.filter(
(o) =>
o.type === 'PipeWire:Interface:Link' && Number(o.info?.props?.['link.input.node']) === Number(sinkAfter.id),
).length
: 0;
record('stop() removes capture links', residualLinks === 0, `residual=${residualLinks}`);
const failed = results.filter((r) => !r.ok).length;
console.log(`\n=== direct: ${results.length - failed}/${results.length} checks passed ===`);
process.exit(failed === 0 ? 0 : 1);
}
main().catch((e) => {
console.error('HARNESS ERROR:', e?.stack || e);
process.exit(2);
});
@@ -0,0 +1,233 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {execFileSync, spawn} from 'node:child_process';
import {copyFileSync} from 'node:fs';
import {createRequire} from 'node:module';
import {setTimeout as sleep} from 'node:timers/promises';
const require = createRequire(import.meta.url);
const addonSo = req('FLX_ADDON_SO');
const work = req('FLX_WORK');
const tone = req('FLX_TONE');
const addonNode = `${work}/flx_audio_addon.node`;
copyFileSync(addonSo, addonNode);
const addon = require(addonNode);
const SINK_NAME = 'fluxer-screen-share';
const SINK_DESC = 'Fluxer Screen Share Audio';
const results = [];
const children = [];
function req(name) {
const v = process.env[name];
if (!v) {
console.error(`HARNESS ERROR: ${name} not set`);
process.exit(2);
}
return v;
}
function record(name, ok, detail) {
results.push({name, ok});
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` -- ${detail}` : ''}`);
}
function pwDump() {
return JSON.parse(execFileSync('pw-dump', {encoding: 'utf8', maxBuffer: 64e6}));
}
function nodesByName(dump, name) {
return dump.filter((o) => o.type === 'PipeWire:Interface:Node' && o.info?.props?.['node.name'] === name);
}
function links(dump) {
return dump
.filter((o) => o.type === 'PipeWire:Interface:Link')
.map((o) => ({
inNode: Number(o.info?.props?.['link.input.node']),
outNode: Number(o.info?.props?.['link.output.node']),
}));
}
function streamNodes(dump) {
return dump.filter(
(o) => o.type === 'PipeWire:Interface:Node' && o.info?.props?.['media.class'] === 'Stream/Output/Audio',
);
}
async function waitFor(predicate, timeoutMs, stepMs = 150) {
const deadline = Date.now() + timeoutMs;
let last;
while (Date.now() < deadline) {
last = predicate();
if (last) return last;
await sleep(stepMs);
}
return last;
}
function killAll() {
for (const c of children) {
try {
c.kill('SIGKILL');
} catch {}
}
}
async function main() {
record('backend is pipewire', addon.audioBackend() === 'pipewire', addon.audioBackend());
const descendant = spawn(
'pw-play',
['--target', 'test_speakers', '-P', '{ application.name = "Descendant Player" }', tone],
{
stdio: 'ignore',
env: process.env,
},
);
children.push(descendant);
const speakerIdOf = (d) => nodesByName(d, 'test_speakers')[0]?.id;
const sourcesInto = (dump) => {
const sid = Number(speakerIdOf(dump));
return new Set(
links(dump)
.filter((l) => l.inNode === sid)
.map((l) => l.outNode),
).size;
};
const pre = await waitFor(() => {
const d = pwDump();
return streamNodes(d).length >= 5 && sourcesInto(d) >= 5 ? d : null;
}, 12000);
record(
'all 5 playback streams routed to speakers pre-capture',
!!pre,
pre ? `${streamNodes(pre).length} streams, ${sourcesInto(pre)} routed` : 'timeout',
);
const preDump = pre || pwDump();
const speakers = nodesByName(preDump, 'test_speakers');
record('default sink test_speakers exists', speakers.length === 1);
const preSpeakerSources = sourcesInto(preDump);
record(
'every app is playing to the real speakers pre-capture',
preSpeakerSources >= 5,
`${preSpeakerSources} distinct app streams -> speakers`,
);
const bridge = new addon.AudioBridge();
record('AudioBridge on pipewire', bridge.backend() === 'pipewire', bridge.backend());
record(
'apply(system rule) accepted',
bridge.apply({onlySpeakers: true, onlyDefaultSpeakers: true, ignoreDevices: true}) === true,
);
const after = await waitFor(() => {
const d = pwDump();
if (nodesByName(d, SINK_NAME).length !== 1) return null;
const g = bridge.routingGraph();
return g.ownedLinks.length >= 2 ? {d, g} : null;
}, 10000);
if (!after) {
record('fluxer sink + capture links established', false, 'timeout');
await finish(bridge);
return;
}
const {d: dump, g: graph} = after;
const sink = nodesByName(dump, SINK_NAME);
record('exactly one fluxer-screen-share node', sink.length === 1, `count=${sink.length}`);
const sp = sink[0]?.info?.props ?? {};
record(
'sink node.description is "Fluxer Screen Share Audio"',
sp['node.description'] === SINK_DESC,
sp['node.description'],
);
record('sink media.class is Audio/Source/Virtual', sp['media.class'] === 'Audio/Source/Virtual', sp['media.class']);
record('sink node.virtual=true', String(sp['node.virtual']) === 'true');
const sinkId = Number(sink[0]?.id);
record(
'all owned links are passive',
graph.ownedLinks.every((l) => l.passive === true),
`${graph.ownedLinks.length} links`,
);
record(
'all owned links terminate at the fluxer sink',
graph.ownedLinks.every((l) => Number(l.inputNodeId) === sinkId),
);
const captured = new Set(graph.ownedLinks.map((l) => Number(l.outputNodeId)));
const idsByPredicate = (pred) =>
streamNodes(dump)
.filter((o) => pred(o.info.props))
.map((o) => Number(o.id));
const normalIds = idsByPredicate(
(p) =>
['pw-play', 'Music Player Demo'].includes(p['application.name']) &&
p['application.name'] !== 'Fluxer' &&
p['application.name'] !== 'Descendant Player' &&
!(p['node.name'] || '').startsWith('Fluxer '),
);
const fluxerAppIds = idsByPredicate((p) => p['application.name'] === 'Fluxer');
const fluxerNodeIds = idsByPredicate((p) => (p['node.name'] || '').startsWith('Fluxer '));
const descendantIds = idsByPredicate((p) => p['application.name'] === 'Descendant Player');
record(
'normal external apps ARE captured',
normalIds.length >= 2 && normalIds.every((id) => captured.has(id)),
`normal=${normalIds} captured=${[...captured]}`,
);
record(
'Fluxer-named app (application.name) is EXCLUDED',
fluxerAppIds.length >= 1 && fluxerAppIds.every((id) => !captured.has(id)),
`fluxerApp=${fluxerAppIds}`,
);
record(
'Fluxer-named app (node.name prefix) is EXCLUDED',
fluxerNodeIds.length >= 1 && fluxerNodeIds.every((id) => !captured.has(id)),
`fluxerNode=${fluxerNodeIds}`,
);
record(
'descendant-PID player is EXCLUDED (self-process tree)',
descendantIds.length >= 1 && descendantIds.every((id) => !captured.has(id)),
`descendant=${descendantIds}`,
);
const afterSpeakerSources = sourcesInto(dump);
record(
'apps STILL play to real speakers during capture (tap, not move)',
afterSpeakerSources >= preSpeakerSources,
`before=${preSpeakerSources} after=${afterSpeakerSources} distinct app streams -> speakers`,
);
const meta = dump.find((o) => o.type === 'PipeWire:Interface:Metadata' && o.props?.['metadata.name'] === 'default');
const def = meta?.metadata?.find((m) => m.key === 'default.audio.sink')?.value?.name;
record('default audio sink unchanged (test_speakers)', def === 'test_speakers', `default=${def}`);
await finish(bridge);
}
async function finish(bridge) {
bridge.release();
const cleared = await waitFor(() => (bridge.routingGraph().ownedLinks.length === 0 ? true : null), 5000);
record(
'release()+settle removes all owned links',
!!cleared,
cleared ? '0 owned links' : `still ${bridge.routingGraph().ownedLinks.length}`,
);
await sleep(500);
const d = pwDump();
const sinkId = Number(nodesByName(d, SINK_NAME)[0]?.id);
const residual = Number.isNaN(sinkId) ? 0 : links(d).filter((l) => l.inNode === sinkId).length;
record('no residual links into fluxer sink after release', residual === 0, `residual=${residual}`);
killAll();
const failed = results.filter((r) => !r.ok).length;
console.log(`\n=== ${results.length - failed}/${results.length} checks passed ===`);
process.exit(failed === 0 ? 0 : 1);
}
process.on('exit', killAll);
main().catch((e) => {
killAll();
console.error('HARNESS ERROR:', e?.stack || e);
process.exit(2);
});
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-or-later
set -uo pipefail
WORK="${FLX_WORK:-/home/parallels/flx-vmtest}"
ADDON_SO="${FLX_ADDON_SO:-/home/parallels/flx-target/debug/libfluxer_linux_audio_capture.so}"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
mkdir -p "$WORK"
export XDG_RUNTIME_DIR="$WORK/xdg"
mkdir -p "$XDG_RUNTIME_DIR"; chmod 700 "$XDG_RUNTIME_DIR"
export PIPEWIRE_RUNTIME_DIR="$XDG_RUNTIME_DIR"
export PULSE_RUNTIME_PATH="$XDG_RUNTIME_DIR/pulse"
unset DISPLAY WAYLAND_DISPLAY DBUS_SESSION_BUS_ADDRESS
PIDS=()
cleanup() {
for p in "${PIDS[@]:-}"; do kill -9 "$p" 2>/dev/null; done
pkill -9 -u "$(id -un)" -f "pipewire" 2>/dev/null
pkill -9 -u "$(id -un)" -f "wireplumber" 2>/dev/null
}
trap cleanup EXIT
echo "=== starting private pipewire server (runtime=$XDG_RUNTIME_DIR) ==="
pipewire >"$WORK/pipewire.log" 2>&1 & PIDS+=($!)
sleep 0.8
pipewire-pulse >"$WORK/pipewire-pulse.log" 2>&1 & PIDS+=($!)
sleep 0.5
wireplumber >"$WORK/wireplumber.log" 2>&1 & PIDS+=($!)
for _ in $(seq 1 40); do pw-cli info 0 >/dev/null 2>&1 && break; sleep 0.25; done
if ! pw-cli info 0 >/dev/null 2>&1; then
echo "FATAL: private pipewire did not come up"; cat "$WORK/pipewire.log"; exit 2
fi
echo "=== creating virtual speakers (default sink) ==="
pactl load-module module-null-sink sink_name=test_speakers sink_properties='device.description=Test_Speakers' >/dev/null 2>&1
pactl set-default-sink test_speakers 2>/dev/null
sleep 0.4
echo "=== generating a real 600s stereo tone ==="
TONE="$WORK/tone.wav"
[ -f "$TONE" ] || ffmpeg -hide_banner -loglevel error -f lavfi -i "sine=frequency=440:duration=600" -ac 2 -ar 48000 "$TONE" </dev/null
export FLX_TONE="$TONE"
echo "=== spawning 4 external real-app players (outside the harness process tree) ==="
pw-play --target test_speakers "$TONE" >/dev/null 2>&1 & PIDS+=($!)
pw-play --target test_speakers -P '{ application.name = "Music Player Demo" }' "$TONE" >/dev/null 2>&1 & PIDS+=($!)
pw-play --target test_speakers -P '{ application.name = "Fluxer" }' "$TONE" >/dev/null 2>&1 & PIDS+=($!)
pw-play --target test_speakers -P '{ node.name = "Fluxer Helper Stream" }' "$TONE" >/dev/null 2>&1 & PIDS+=($!)
sleep 1.5
echo "=== pre-test graph (fluxer sink should be ABSENT) ==="
pw-dump | node -e 'const d=JSON.parse(require("fs").readFileSync(0));const f=d.filter(o=>o.type==="PipeWire:Interface:Node"&&/fluxer-screen-share/.test(o.info?.props?.["node.name"]||""));console.log("fluxer sink nodes pre-test:",f.length);'
echo "=== running napi SYSTEM-capture validation harness ==="
export FLX_ADDON_SO="$ADDON_SO" FLX_WORK="$WORK"
node "$HERE/pw_graph_validation.mjs"
HARNESS_RC=$?
echo "=== running napi DIRECT (per-process) capture validation harness ==="
node "$HERE/direct_capture_validation.mjs"
DIRECT_RC=$?
[ "$DIRECT_RC" = "0" ] || HARNESS_RC=$DIRECT_RC
echo "=== post-exit cleanup check (Drop must remove the fluxer sink) ==="
sleep 0.8
RESIDUAL=$(pw-dump | node -e 'const d=JSON.parse(require("fs").readFileSync(0));const f=d.filter(o=>(o.type==="PipeWire:Interface:Node"&&/fluxer/.test(o.info?.props?.["node.name"]||""))||(o.type==="PipeWire:Interface:Link"&&/fluxer/.test(JSON.stringify(o.info?.props||{}))));console.log(f.length);')
if [ "$RESIDUAL" = "0" ]; then
echo "PASS no fluxer nodes/links remain after addon process exit (clean teardown)"
else
echo "FAIL $RESIDUAL residual fluxer objects after addon process exit"
HARNESS_RC=1
fi
echo "=== DONE rc=$HARNESS_RC ==="
exit $HARNESS_RC