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:
@@ -0,0 +1,248 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {afterEach, describe, test} from 'node:test';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const macScreenCapture = require('./index.js');
|
||||
|
||||
function makeFakeBinding({sources = [], availability = {sck: {supported: true}, screenPermission: 'authorized'}} = {}) {
|
||||
const calls = [];
|
||||
const frameSinkHandleCalls = [];
|
||||
const natives = [];
|
||||
const frameSinkDiagnostics = {
|
||||
accepted: 0,
|
||||
coalesced: 0,
|
||||
rejected: 0,
|
||||
mediaFramesDroppedWithoutSink: 0,
|
||||
};
|
||||
class FakeNative {
|
||||
constructor() {
|
||||
this.lifecycleCallback = undefined;
|
||||
this.stopCount = 0;
|
||||
natives.push(this);
|
||||
}
|
||||
|
||||
setLifecycleCallback(callback) {
|
||||
this.lifecycleCallback = callback;
|
||||
}
|
||||
|
||||
setFrameSinkHandle(handle) {
|
||||
frameSinkHandleCalls.push(handle);
|
||||
}
|
||||
|
||||
async start(sourceId, sourceKind, width, height, frameRate, captureId, captureOptions) {
|
||||
calls.push({sourceId, sourceKind, width, height, frameRate, captureId, captureOptions});
|
||||
return {width: width || 1920, height: height || 1080, frameRate: frameRate || 30, pixelFormat: 'nv12'};
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.stopCount += 1;
|
||||
}
|
||||
|
||||
getFrameSinkDiagnostics() {
|
||||
return {...frameSinkDiagnostics};
|
||||
}
|
||||
}
|
||||
return {
|
||||
binding: {
|
||||
ScreenCapture: FakeNative,
|
||||
listSources: async () => sources,
|
||||
getBackendAvailability: async () => availability,
|
||||
getBackendInfo: () => ({
|
||||
backend: 'mac-screen-capture',
|
||||
supported: true,
|
||||
reason: '',
|
||||
minMacosVersion: '12.3',
|
||||
detectedMacosVersion: '14.0',
|
||||
sckAvailable: true,
|
||||
}),
|
||||
},
|
||||
calls,
|
||||
frameSinkHandleCalls,
|
||||
frameSinkDiagnostics,
|
||||
natives,
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
macScreenCapture.__setBindingForTests(null);
|
||||
});
|
||||
|
||||
describe('mac-screen-capture loader wrapper', () => {
|
||||
test('forwards source id, kind, and dimensions to native binding', async () => {
|
||||
const {binding, calls} = makeFakeBinding();
|
||||
macScreenCapture.__setBindingForTests(binding);
|
||||
const capture = new macScreenCapture.ScreenCapture({
|
||||
sourceId: '12345',
|
||||
sourceKind: 'window',
|
||||
width: 1280,
|
||||
height: 720,
|
||||
frameRate: 30,
|
||||
colorRange: 'full',
|
||||
colorSpace: 'rec709',
|
||||
showCursorClicks: true,
|
||||
captureRect: {x: 10, y: 20, width: 300, height: 200},
|
||||
});
|
||||
capture.on('error', () => {});
|
||||
const result = await capture.start();
|
||||
assert.deepEqual(calls, [
|
||||
{
|
||||
sourceId: '12345',
|
||||
sourceKind: 'window',
|
||||
width: 1280,
|
||||
height: 720,
|
||||
frameRate: 30,
|
||||
captureId: undefined,
|
||||
captureOptions: {
|
||||
colorRange: 'full',
|
||||
colorSpace: 'rec709',
|
||||
showCursorClicks: true,
|
||||
captureRect: {x: 10, y: 20, width: 300, height: 200},
|
||||
},
|
||||
},
|
||||
]);
|
||||
assert.equal(result.pixelFormat, 'nv12');
|
||||
assert.equal(result.width, 1280);
|
||||
assert.equal(result.height, 720);
|
||||
});
|
||||
test('defaults sourceKind to screen and frameRate to 30', async () => {
|
||||
const {binding, calls} = makeFakeBinding();
|
||||
macScreenCapture.__setBindingForTests(binding);
|
||||
const capture = new macScreenCapture.ScreenCapture({sourceId: '1'});
|
||||
capture.on('error', () => {});
|
||||
await capture.start();
|
||||
assert.equal(calls[0].sourceKind, 'screen');
|
||||
assert.equal(calls[0].frameRate, 30);
|
||||
});
|
||||
test('forwards display and window sources from native binding without rewriting ids', async () => {
|
||||
const {binding} = makeFakeBinding({
|
||||
sources: [
|
||||
{kind: 'screen', id: 'display:69733632', name: 'Studio Display', width: 5120, height: 2880},
|
||||
{
|
||||
kind: 'window',
|
||||
id: 'window:4242',
|
||||
name: 'Fluxer',
|
||||
width: 1440,
|
||||
height: 900,
|
||||
appName: 'Fluxer',
|
||||
bundleId: 'app.fluxer.desktop',
|
||||
targetPid: 1234,
|
||||
},
|
||||
],
|
||||
});
|
||||
macScreenCapture.__setBindingForTests(binding);
|
||||
|
||||
const sources = await macScreenCapture.listSources();
|
||||
|
||||
assert.deepEqual(sources, [
|
||||
{kind: 'screen', id: 'display:69733632', name: 'Studio Display', width: 5120, height: 2880},
|
||||
{
|
||||
kind: 'window',
|
||||
id: 'window:4242',
|
||||
name: 'Fluxer',
|
||||
width: 1440,
|
||||
height: 900,
|
||||
appName: 'Fluxer',
|
||||
bundleId: 'app.fluxer.desktop',
|
||||
targetPid: 1234,
|
||||
},
|
||||
]);
|
||||
});
|
||||
test('reports ScreenCaptureKit support and permission from native binding', async () => {
|
||||
const {binding} = makeFakeBinding({
|
||||
availability: {
|
||||
sck: {supported: true, macosVersion: '15.0'},
|
||||
screenPermission: 'authorized',
|
||||
},
|
||||
});
|
||||
macScreenCapture.__setBindingForTests(binding);
|
||||
|
||||
assert.deepEqual(await macScreenCapture.getBackendAvailability(), {
|
||||
sck: {supported: true, macosVersion: '15.0'},
|
||||
screenPermission: 'authorized',
|
||||
});
|
||||
});
|
||||
test('installs a native frame sink handle once before start', async () => {
|
||||
const {binding, calls, frameSinkHandleCalls} = makeFakeBinding();
|
||||
macScreenCapture.__setBindingForTests(binding);
|
||||
const frameSinkHandle = {native: true};
|
||||
const capture = new macScreenCapture.ScreenCapture({
|
||||
sourceId: '12345',
|
||||
sourceKind: 'window',
|
||||
frameSinkHandle,
|
||||
nativeFrameSinkRequired: true,
|
||||
});
|
||||
capture.on('error', () => {});
|
||||
|
||||
await capture.start();
|
||||
|
||||
assert.deepEqual(frameSinkHandleCalls, [frameSinkHandle]);
|
||||
assert.equal(calls.length, 1);
|
||||
});
|
||||
test('fails before native start when a native frame sink is required but missing', async () => {
|
||||
const {binding, calls, frameSinkHandleCalls} = makeFakeBinding();
|
||||
macScreenCapture.__setBindingForTests(binding);
|
||||
const capture = new macScreenCapture.ScreenCapture({
|
||||
sourceId: '12345',
|
||||
sourceKind: 'window',
|
||||
nativeFrameSinkRequired: true,
|
||||
});
|
||||
capture.on('error', () => {});
|
||||
|
||||
await assert.rejects(() => capture.start(), /native frame sink handle is required/i);
|
||||
assert.deepEqual(frameSinkHandleCalls, []);
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
test('frame sink diagnostics are forwarded by the wrapper', () => {
|
||||
const {binding, frameSinkDiagnostics} = makeFakeBinding();
|
||||
macScreenCapture.__setBindingForTests(binding);
|
||||
const capture = new macScreenCapture.ScreenCapture({sourceId: '1'});
|
||||
|
||||
frameSinkDiagnostics.accepted = 5;
|
||||
frameSinkDiagnostics.coalesced = 1;
|
||||
frameSinkDiagnostics.rejected = 2;
|
||||
frameSinkDiagnostics.mediaFramesDroppedWithoutSink = 3;
|
||||
|
||||
assert.deepEqual(capture.getFrameSinkDiagnostics(), {
|
||||
accepted: 5,
|
||||
coalesced: 1,
|
||||
rejected: 2,
|
||||
mediaFramesDroppedWithoutSink: 3,
|
||||
});
|
||||
});
|
||||
test('emits closed once when native lifecycle closes and stop is called later', async () => {
|
||||
const {binding, natives} = makeFakeBinding();
|
||||
macScreenCapture.__setBindingForTests(binding);
|
||||
const capture = new macScreenCapture.ScreenCapture({sourceId: '1'});
|
||||
let closed = 0;
|
||||
capture.on('closed', () => {
|
||||
closed += 1;
|
||||
});
|
||||
natives[0].lifecycleCallback('closed', '');
|
||||
await Promise.resolve();
|
||||
await capture.stop();
|
||||
assert.equal(closed, 1);
|
||||
assert.equal(natives[0].stopCount, 1);
|
||||
});
|
||||
test('lifecycle error emits Error event', async () => {
|
||||
const {binding, natives} = makeFakeBinding();
|
||||
macScreenCapture.__setBindingForTests(binding);
|
||||
const capture = new macScreenCapture.ScreenCapture({sourceId: '1'});
|
||||
const errors = [];
|
||||
capture.on('error', (err) => errors.push(err));
|
||||
natives[0].lifecycleCallback('error', 'permission lost mid-stream');
|
||||
assert.equal(errors.length, 1);
|
||||
assert.equal(errors[0].message, 'permission lost mid-stream');
|
||||
});
|
||||
test('lifecycle diagnostic emits diagnostic event', () => {
|
||||
const {binding, natives} = makeFakeBinding();
|
||||
macScreenCapture.__setBindingForTests(binding);
|
||||
const capture = new macScreenCapture.ScreenCapture({sourceId: '1'});
|
||||
const diagnostics = [];
|
||||
capture.on('diagnostic', (message) => diagnostics.push(message));
|
||||
natives[0].lifecycleCallback('diagnostic', 'frame sink rejected a frame');
|
||||
assert.deepEqual(diagnostics, ['frame sink rejected a frame']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user