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,249 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
const {EventEmitter} = require('node:events');
const {existsSync} = require('node:fs');
const {join, sep} = require('node:path');
const {createNativeLoadError, loadNativeBinding} = require('./loader-diagnostics.cjs');
const MODULE_NAME = '@fluxer/win-process-loopback';
function resolveNativeRoot() {
const asarSegment = `${sep}app.asar${sep}`;
if (!__dirname.includes(asarSegment)) return __dirname;
const unpackedDir = __dirname.replace(asarSegment, `${sep}app.asar.unpacked${sep}`);
return existsSync(unpackedDir) ? unpackedDir : __dirname;
}
function nativeFileName(arch) {
switch (arch) {
case 'x64':
return 'win-process-loopback.win32-x64-msvc.node';
case 'arm64':
return 'win-process-loopback.win32-arm64-msvc.node';
default:
return null;
}
}
let binding = null;
let loadError = null;
if (process.platform === 'win32') {
const nativeRoot = resolveNativeRoot();
const fileName = nativeFileName(process.arch);
if (!fileName) {
loadError = createNativeLoadError({
moduleName: MODULE_NAME,
nativeRoot,
packageDir: __dirname,
reason: `unsupported Windows architecture: ${process.arch}`,
});
throw loadError;
} else {
const nativePath = join(nativeRoot, fileName);
const loaded = loadNativeBinding({
moduleName: MODULE_NAME,
nativePath,
nativeRoot,
packageDir: __dirname,
probe: false,
});
binding = loaded.binding;
loadError = loaded.loadError;
if (loadError) throw loadError;
}
} else {
loadError = createNativeLoadError({
moduleName: MODULE_NAME,
nativeRoot: resolveNativeRoot(),
packageDir: __dirname,
reason: `not supported on platform ${process.platform}`,
});
}
function isSupported() {
if (!binding) return false;
try {
return Boolean(binding.isSupported());
} catch {
return false;
}
}
function getBackendInfo() {
if (!binding) {
const reason =
process.platform !== 'win32'
? `@fluxer/win-process-loopback only supports Windows, got ${process.platform}. Microsoft's documented process-loopback minimum is Windows build 20348.`
: `@fluxer/win-process-loopback native binary unavailable: ${loadError?.message ?? 'unknown reason'}`;
return {
backend: 'win-process-loopback',
supported: false,
reason,
processSupported: false,
systemSupported: false,
systemExcludesSelf: false,
processIncludeSupported: false,
processExcludeSupported: false,
sessionMixerSupported: false,
systemLoopbackMode: 'unavailable',
minWindowsBuild: 20348,
minWindowsVersionLabel: 'Windows build 20348 (documented)',
detectedWindowsBuild: undefined,
};
}
try {
return binding.getBackendInfo();
} catch (err) {
return {
backend: 'win-process-loopback',
supported: false,
reason: `@fluxer/win-process-loopback getBackendInfo failed: ${err?.message ?? String(err)}`,
processSupported: false,
systemSupported: false,
systemExcludesSelf: false,
processIncludeSupported: false,
processExcludeSupported: false,
sessionMixerSupported: false,
systemLoopbackMode: 'unavailable',
minWindowsBuild: 20348,
minWindowsVersionLabel: 'Windows build 20348 (documented)',
detectedWindowsBuild: undefined,
};
}
}
function pidFromHwnd(hwnd) {
if (!binding) return 0;
try {
return binding.pidFromHwnd(hwnd);
} catch {
return 0;
}
}
function resolveAudioRootPid(pid) {
if (!binding) return pid;
try {
return binding.resolveAudioRootPid(pid);
} catch {
return pid;
}
}
class ProcessLoopback extends EventEmitter {
constructor(pid, opts = {}) {
super();
if (!binding) {
throw loadError ?? new Error('@fluxer/win-process-loopback binding unavailable');
}
const captureScope = opts.captureScope ?? opts.winCaptureScope ?? opts.scope ?? 'process';
const includeProcessTree =
captureScope === 'system' || captureScope === 'session-mixer' ? false : opts.includeProcessTree !== false;
const sampleRate = opts.sampleRate;
const channels = opts.channels;
this._stopped = false;
this._started = false;
this._startPromise = null;
this._resolveStart = null;
this._rejectStart = null;
this._native = new binding.ProcessLoopback(
pid,
{includeProcessTree, sampleRate, channels, captureScope},
(frame) => this.emit('frame', frame),
(err) => {
if (!this._started && this._rejectStart) {
this._rejectStart(err);
this._clearStartWaiters();
}
this.emit('error', err);
},
() => {
if (this._stopped) return;
this._stopped = true;
if (!this._started && this._rejectStart) {
this._rejectStart(new Error('ProcessLoopback closed before audio capture started'));
this._clearStartWaiters();
}
queueMicrotask(() => {
try {
this._native.dispose();
} catch {}
});
this.emit('closed');
},
() => {
this._started = true;
if (this._resolveStart) {
this._resolveStart();
this._clearStartWaiters();
}
this.emit('started');
},
);
}
_clearStartWaiters() {
this._resolveStart = null;
this._rejectStart = null;
}
setScreenAudioSink(handle) {
if (typeof this._native.setScreenAudioSink !== 'function') return false;
try {
return this._native.setScreenAudioSink(handle) !== false;
} catch {
return false;
}
}
clearScreenAudioSink() {
if (typeof this._native.clearScreenAudioSink === 'function') {
this._native.clearScreenAudioSink();
}
}
start() {
if (this._stopped) return Promise.resolve();
if (this._started) return Promise.resolve();
if (this._startPromise) return this._startPromise;
this._startPromise = new Promise((resolve, reject) => {
this._resolveStart = resolve;
this._rejectStart = reject;
try {
this._native.start();
} catch (error) {
this._clearStartWaiters();
reject(error);
}
});
return this._startPromise;
}
stop() {
if (this._stopped) return;
this._stopped = true;
this.clearScreenAudioSink();
if (!this._started && this._rejectStart) {
this._rejectStart(new Error('ProcessLoopback stopped before audio capture started'));
this._clearStartWaiters();
}
this._native.dispose();
}
}
function __setBindingForTests(nextBinding, nextLoadError = null) {
binding = nextBinding;
loadError = nextLoadError;
}
module.exports = {
isSupported,
getBackendInfo,
pidFromHwnd,
resolveAudioRootPid,
ProcessLoopback,
get loadError() {
return loadError;
},
__setBindingForTests,
};