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.
849 lines
23 KiB
JavaScript
849 lines
23 KiB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import assert from 'node:assert/strict';
|
|
import {createRequire} from 'node:module';
|
|
import {describe, test} from 'node:test';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const webrtcSender = require('./index.js');
|
|
|
|
describe('webrtc-sender loader wrapper', () => {
|
|
test('resolves the native addon filename for every desktop OS/arch target', () => {
|
|
const cases = [
|
|
['win32', 'x64', 'webrtc-sender.win32-x64-msvc.node'],
|
|
['win32', 'arm64', 'webrtc-sender.win32-arm64-msvc.node'],
|
|
['darwin', 'x64', 'webrtc-sender.darwin-x64.node'],
|
|
['darwin', 'arm64', 'webrtc-sender.darwin-arm64.node'],
|
|
['linux', 'x64', 'webrtc-sender.linux-x64-gnu.node'],
|
|
['linux', 'arm64', 'webrtc-sender.linux-arm64-gnu.node'],
|
|
];
|
|
for (const [platform, arch, expected] of cases) {
|
|
assert.equal(webrtcSender.__nativeFileNameForTests(platform, arch), expected);
|
|
}
|
|
});
|
|
|
|
test('rejects unsupported platform/architecture pairs explicitly', () => {
|
|
assert.throws(() => webrtcSender.__nativeFileNameForTests('linux', 'ia32'), /not supported/);
|
|
assert.throws(() => webrtcSender.__nativeFileNameForTests('freebsd', 'x64'), /not supported/);
|
|
});
|
|
|
|
test('returns a native hardware encoder capability when the binding exports one', () => {
|
|
const expected = {
|
|
available: true,
|
|
backend: 'nvenc',
|
|
compiled: true,
|
|
runtime: true,
|
|
codecs: ['h264', 'h265'],
|
|
zeroCopy: true,
|
|
nativeInputs: ['dmabuf'],
|
|
};
|
|
webrtcSender.__setBindingForTests({
|
|
getHardwareEncoderCapability() {
|
|
return expected;
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(webrtcSender.getHardwareEncoderCapability(), expected);
|
|
assert.deepEqual(webrtcSender.getHardwareEncoderCapabilities(), expected);
|
|
});
|
|
|
|
test('delegates dropped video callback metrics through the VoiceEngine wrapper', () => {
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
inboundAudioFrames() {
|
|
return 2;
|
|
}
|
|
|
|
inboundVideoFrames() {
|
|
return 3;
|
|
}
|
|
|
|
droppedVideoFrameCallbacks() {
|
|
return 5;
|
|
}
|
|
},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
assert.equal(engine.inboundAudioFrames(), 2);
|
|
assert.equal(engine.inboundVideoFrames(), 3);
|
|
assert.equal(engine.droppedVideoFrameCallbacks(), 5);
|
|
});
|
|
|
|
test('reports VoiceEngine feature capabilities from the wrapped native prototype', () => {
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
publishDeviceMicrophone() {}
|
|
publishMicrophone() {}
|
|
publishCamera() {}
|
|
listCameraDevices() {}
|
|
publishScreenShare() {}
|
|
updateScreenShareEncoding() {}
|
|
unpublishScreenShare() {}
|
|
publishScreenShareAudio() {}
|
|
pushScreenSharePcm() {}
|
|
pushScreenShareFloat() {}
|
|
unpublishScreenShareAudio() {}
|
|
listAudioInputDevices() {}
|
|
listAudioOutputDevices() {}
|
|
setAudioOutputDevice() {}
|
|
setParticipantVolume() {}
|
|
setRemoteTrackSubscription() {}
|
|
publishData() {}
|
|
getConnectionStats() {}
|
|
setVideoFrameCallback() {}
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(webrtcSender.getCapabilities(), {
|
|
microphoneCapture: true,
|
|
syntheticMicrophonePcm: true,
|
|
cameraCapture: true,
|
|
nativeCameraBackgrounds: false,
|
|
screenShare: true,
|
|
screenShareEncodingUpdate: true,
|
|
screenShareAudio: true,
|
|
deviceLists: true,
|
|
outputDeviceSelection: true,
|
|
participantVolume: true,
|
|
remoteTrackSubscription: true,
|
|
dataChannel: true,
|
|
connectionStats: true,
|
|
nativeVideoFrames: true,
|
|
hardwareEncoderCapabilities: true,
|
|
});
|
|
});
|
|
|
|
test('does not report screen-share audio capability without the float push method', () => {
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
publishScreenShareAudio() {}
|
|
pushScreenSharePcm() {}
|
|
unpublishScreenShareAudio() {}
|
|
},
|
|
});
|
|
|
|
assert.equal(webrtcSender.getCapabilities().screenShareAudio, false);
|
|
});
|
|
|
|
test('reports native camera background support only from the explicit native probe', () => {
|
|
webrtcSender.__setBindingForTests({
|
|
hasNativeCameraBackgrounds() {
|
|
return true;
|
|
},
|
|
VoiceEngine: class {
|
|
publishCamera() {}
|
|
listCameraDevices() {}
|
|
},
|
|
});
|
|
|
|
assert.equal(webrtcSender.hasNativeCameraBackgrounds(), true);
|
|
assert.equal(webrtcSender.getCapabilities().nativeCameraBackgrounds, true);
|
|
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
publishCamera() {}
|
|
listCameraDevices() {}
|
|
},
|
|
});
|
|
|
|
assert.equal(webrtcSender.hasNativeCameraBackgrounds(), false);
|
|
assert.equal(webrtcSender.getCapabilities().nativeCameraBackgrounds, false);
|
|
});
|
|
|
|
test('delegates video-frame callback clearing to the native binding when exported', () => {
|
|
let clearCalls = 0;
|
|
const setCalls = [];
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
setVideoFrameCallback(callback) {
|
|
setCalls.push(callback);
|
|
}
|
|
|
|
clearVideoFrameCallback() {
|
|
clearCalls += 1;
|
|
}
|
|
},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
engine.clearVideoFrameCallback();
|
|
|
|
assert.equal(clearCalls, 1);
|
|
assert.deepEqual(setCalls, []);
|
|
});
|
|
|
|
test('delegates screen-share float audio through the VoiceEngine wrapper', async () => {
|
|
const calls = [];
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
pushScreenShareFloat(buffer, sampleRate, numChannels) {
|
|
calls.push([buffer, sampleRate, numChannels]);
|
|
return Promise.resolve(true);
|
|
}
|
|
},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
const buffer = Buffer.from(new Float32Array([0.25, -0.25]).buffer);
|
|
const accepted = await engine.pushScreenShareFloat(buffer, 48000, 2);
|
|
|
|
assert.equal(accepted, true);
|
|
assert.deepEqual(calls, [[buffer, 48000, 2]]);
|
|
});
|
|
|
|
test('falls back to a no-op video-frame callback for older native bindings', () => {
|
|
const setCalls = [];
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
setVideoFrameCallback(callback) {
|
|
setCalls.push(callback);
|
|
}
|
|
},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
engine.clearVideoFrameCallback();
|
|
|
|
assert.equal(setCalls.length, 1);
|
|
assert.equal(typeof setCalls[0], 'function');
|
|
assert.equal(setCalls[0]('{}', Buffer.alloc(0)), undefined);
|
|
});
|
|
|
|
test('treats video-frame callback clearing as a no-op when the binding exports neither method', () => {
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
|
|
assert.equal(engine.clearVideoFrameCallback(), undefined);
|
|
});
|
|
|
|
test('delegates engine bridge version reads and assertions to the native binding', () => {
|
|
const assertedVersions = [];
|
|
webrtcSender.__setBindingForTests({
|
|
getEngineBridgeVersion() {
|
|
return 9;
|
|
},
|
|
assertEngineBridgeVersion(version) {
|
|
assertedVersions.push(version);
|
|
if (version !== 9) {
|
|
throw new Error(`voice engine bridge version mismatch: host sent ${version}, native addon expects 9`);
|
|
}
|
|
},
|
|
});
|
|
|
|
assert.equal(webrtcSender.getEngineBridgeVersion(), 9);
|
|
webrtcSender.assertEngineBridgeVersion(9);
|
|
assert.throws(() => webrtcSender.assertEngineBridgeVersion(8), /bridge version mismatch/);
|
|
assert.deepEqual(assertedVersions, [9, 8]);
|
|
});
|
|
|
|
test('reports a null engine bridge version and throws on assertion when the binding lacks the exports', () => {
|
|
webrtcSender.__setBindingForTests({});
|
|
|
|
assert.equal(webrtcSender.getEngineBridgeVersion(), null);
|
|
assert.throws(() => webrtcSender.assertEngineBridgeVersion(9), /does not export assertEngineBridgeVersion/);
|
|
});
|
|
|
|
test('delegates native voice engine prewarm when the binding exports it', () => {
|
|
let prewarmCalls = 0;
|
|
webrtcSender.__setBindingForTests({
|
|
prewarmVoiceEngine() {
|
|
prewarmCalls += 1;
|
|
},
|
|
});
|
|
|
|
webrtcSender.prewarmVoiceEngine();
|
|
|
|
assert.equal(prewarmCalls, 1);
|
|
});
|
|
|
|
test('delegates device microphone publish through the VoiceEngine wrapper', async () => {
|
|
const calls = [];
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
publishDeviceMicrophone(opts) {
|
|
calls.push(opts);
|
|
return Promise.resolve();
|
|
}
|
|
},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
await engine.publishDeviceMicrophone({
|
|
deviceId: 'mic-guid',
|
|
echoCancellation: false,
|
|
noiseSuppression: true,
|
|
autoGainControl: false,
|
|
});
|
|
await engine.publishDeviceMicrophone({
|
|
deviceId: 'mic-guid',
|
|
echoCancellation: false,
|
|
noiseSuppression: true,
|
|
autoGainControl: false,
|
|
maxBitrateBps: 96_000,
|
|
});
|
|
|
|
assert.deepEqual(calls, [
|
|
{
|
|
deviceId: 'mic-guid',
|
|
echoCancellation: false,
|
|
noiseSuppression: true,
|
|
autoGainControl: false,
|
|
maxBitrateBps: undefined,
|
|
},
|
|
{
|
|
deviceId: 'mic-guid',
|
|
echoCancellation: false,
|
|
noiseSuppression: true,
|
|
autoGainControl: false,
|
|
maxBitrateBps: 96_000,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('delegates native camera devices and publish options through the VoiceEngine wrapper', async () => {
|
|
const publishCalls = [];
|
|
const processedPublishCalls = [];
|
|
const processedFrameCalls = [];
|
|
const nativeSinkPublishCalls = [];
|
|
const nativeSinkHandle = {};
|
|
const devices = [
|
|
{
|
|
deviceId: 'native-camera-id',
|
|
label: 'Studio Display Camera',
|
|
description: 'Apple Studio Display Camera',
|
|
index: 0,
|
|
deviceIdAliases: ['native-camera-id', '0'],
|
|
},
|
|
];
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
listCameraDevices() {
|
|
return devices;
|
|
}
|
|
|
|
publishCamera(opts) {
|
|
publishCalls.push(opts);
|
|
return Promise.resolve();
|
|
}
|
|
|
|
publishProcessedCamera(opts) {
|
|
processedPublishCalls.push(opts);
|
|
return Promise.resolve({trackSid: 'TR_processed_camera'});
|
|
}
|
|
|
|
publishNativeCameraSink(opts) {
|
|
nativeSinkPublishCalls.push(opts);
|
|
return Promise.resolve({trackSid: 'TR_native_camera'});
|
|
}
|
|
|
|
createCameraFrameSinkHandle() {
|
|
return nativeSinkHandle;
|
|
}
|
|
|
|
pushProcessedCameraFrame(frame) {
|
|
processedFrameCalls.push(frame);
|
|
return Promise.resolve(true);
|
|
}
|
|
},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
assert.equal(engine.listCameraDevices(), devices);
|
|
await engine.publishCamera({
|
|
deviceId: 'native-camera-id',
|
|
width: 1280,
|
|
height: 720,
|
|
frameRate: 30,
|
|
mirror: true,
|
|
codec: 'h265',
|
|
maxBitrateBps: 6_000_000,
|
|
maxFramerate: 30,
|
|
});
|
|
const processedPublishResult = await engine.publishProcessedCamera({
|
|
width: 1280,
|
|
height: 720,
|
|
frameRate: 30,
|
|
ignored: true,
|
|
});
|
|
const nativeSinkPublishResult = await engine.publishNativeCameraSink({
|
|
deviceId: 'native-camera-id',
|
|
width: 1280,
|
|
height: 720,
|
|
frameRate: 30,
|
|
backgroundMode: 'custom',
|
|
backgroundCustomMediaPath: '/tmp/bg.webp',
|
|
backgroundCustomMediaKind: 'animated',
|
|
codec: 'h264',
|
|
});
|
|
const cameraFrameSinkHandle = engine.createCameraFrameSinkHandle();
|
|
const frame = {
|
|
format: 'i420',
|
|
width: 4,
|
|
height: 2,
|
|
timestampUs: 12_345,
|
|
data: Buffer.alloc(12),
|
|
ignored: true,
|
|
};
|
|
const processedFrameResult = await engine.pushProcessedCameraFrame(frame);
|
|
|
|
assert.deepEqual(publishCalls, [
|
|
{
|
|
deviceId: 'native-camera-id',
|
|
width: 1280,
|
|
height: 720,
|
|
frameRate: 30,
|
|
mirror: true,
|
|
backgroundMode: undefined,
|
|
backgroundCustomMediaPath: undefined,
|
|
backgroundCustomMediaKind: undefined,
|
|
backgroundBlurStrength: undefined,
|
|
codec: 'h265',
|
|
maxBitrateBps: 6_000_000,
|
|
maxFramerate: 30,
|
|
},
|
|
]);
|
|
assert.deepEqual(processedPublishResult, {trackSid: 'TR_processed_camera'});
|
|
assert.deepEqual(processedPublishCalls, [
|
|
{
|
|
width: 1280,
|
|
height: 720,
|
|
frameRate: 30,
|
|
},
|
|
]);
|
|
assert.deepEqual(nativeSinkPublishResult, {trackSid: 'TR_native_camera'});
|
|
assert.deepEqual(nativeSinkPublishCalls, [
|
|
{
|
|
deviceId: 'native-camera-id',
|
|
width: 1280,
|
|
height: 720,
|
|
frameRate: 30,
|
|
mirror: undefined,
|
|
backgroundMode: 'custom',
|
|
backgroundCustomMediaPath: '/tmp/bg.webp',
|
|
backgroundCustomMediaKind: 'animated',
|
|
backgroundBlurStrength: undefined,
|
|
codec: 'h264',
|
|
maxBitrateBps: undefined,
|
|
maxFramerate: undefined,
|
|
},
|
|
]);
|
|
assert.equal(cameraFrameSinkHandle, nativeSinkHandle);
|
|
assert.equal(processedFrameResult, true);
|
|
assert.deepEqual(processedFrameCalls, [
|
|
{
|
|
format: 'i420',
|
|
width: 4,
|
|
height: 2,
|
|
timestampUs: 12_345,
|
|
data: frame.data,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('reports processed camera publishing as unavailable for older native bindings', async () => {
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
assert.throws(
|
|
() => engine.publishProcessedCamera({width: 1280, height: 720, frameRate: 30}),
|
|
/native binding does not export publishProcessedCamera/,
|
|
);
|
|
assert.throws(
|
|
() => engine.publishNativeCameraSink({width: 1280, height: 720, frameRate: 30}),
|
|
/native binding does not export publishNativeCameraSink/,
|
|
);
|
|
assert.equal(engine.createCameraFrameSinkHandle(), null);
|
|
assert.throws(
|
|
() =>
|
|
engine.pushProcessedCameraFrame({
|
|
format: 'i420',
|
|
width: 4,
|
|
height: 2,
|
|
timestampUs: 12_345,
|
|
data: Buffer.alloc(12),
|
|
}),
|
|
/native binding does not export pushProcessedCameraFrame/,
|
|
);
|
|
});
|
|
|
|
test('delegates device screen-share publish through the VoiceEngine wrapper', async () => {
|
|
const publishCalls = [];
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
publishDeviceScreenShare(opts) {
|
|
publishCalls.push(opts);
|
|
return Promise.resolve();
|
|
}
|
|
},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
await engine.publishDeviceScreenShare({
|
|
deviceId: 'studio-display-camera',
|
|
width: 1920,
|
|
height: 1080,
|
|
frameRate: 60,
|
|
codec: 'h264',
|
|
maxBitrateBps: 8_000_000,
|
|
maxFramerate: 60,
|
|
});
|
|
|
|
assert.deepEqual(publishCalls, [
|
|
{
|
|
deviceId: 'studio-display-camera',
|
|
width: 1920,
|
|
height: 1080,
|
|
frameRate: 60,
|
|
mirror: undefined,
|
|
backgroundMode: undefined,
|
|
backgroundCustomMediaPath: undefined,
|
|
backgroundCustomMediaKind: undefined,
|
|
backgroundBlurStrength: undefined,
|
|
codec: 'h264',
|
|
maxBitrateBps: 8_000_000,
|
|
maxFramerate: 60,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('delegates camera capture updates with effect strengths through the VoiceEngine wrapper', async () => {
|
|
const updateCalls = [];
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
updateCameraCapture(opts) {
|
|
updateCalls.push(opts);
|
|
return Promise.resolve();
|
|
}
|
|
},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
await engine.updateCameraCapture({
|
|
deviceId: 'native-camera-id',
|
|
width: 1280,
|
|
height: 720,
|
|
frameRate: 30,
|
|
mirror: false,
|
|
backgroundMode: 'blur',
|
|
backgroundBlurStrength: 90,
|
|
ignored: true,
|
|
});
|
|
|
|
assert.deepEqual(updateCalls, [
|
|
{
|
|
deviceId: 'native-camera-id',
|
|
width: 1280,
|
|
height: 720,
|
|
frameRate: 30,
|
|
mirror: false,
|
|
backgroundMode: 'blur',
|
|
backgroundCustomMediaPath: undefined,
|
|
backgroundCustomMediaKind: undefined,
|
|
backgroundBlurStrength: 90,
|
|
codec: undefined,
|
|
maxBitrateBps: undefined,
|
|
maxFramerate: undefined,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('reports camera capture updates as unavailable for older native bindings', () => {
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
assert.throws(
|
|
() => engine.updateCameraCapture({deviceId: 'native-camera-id'}),
|
|
/native binding does not export updateCameraCapture/,
|
|
);
|
|
});
|
|
|
|
test('reports device screen-share publish as unavailable for older native bindings', async () => {
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
assert.throws(
|
|
() => engine.publishDeviceScreenShare({deviceId: 'studio-display-camera'}),
|
|
/native binding does not export publishDeviceScreenShare/,
|
|
);
|
|
});
|
|
|
|
test('delegates screen-share simulcast selection through the VoiceEngine wrapper', async () => {
|
|
const calls = [];
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
publishScreenShare(width, height, codec, maxBitrateBps, maxFramerate, simulcast, options) {
|
|
calls.push({width, height, codec, maxBitrateBps, maxFramerate, simulcast, options});
|
|
return Promise.resolve();
|
|
}
|
|
},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
await engine.publishScreenShare(3840, 2160, 'h265', 50_000_000, 60, false, {
|
|
adaptiveSend: true,
|
|
minVideoFps: 15,
|
|
minResolutionScale: 0.5,
|
|
maxAudioBufferMs: 750,
|
|
captureId: 'screen-harness-primary',
|
|
});
|
|
assert.deepEqual(calls, [
|
|
{
|
|
width: 3840,
|
|
height: 2160,
|
|
codec: 'h265',
|
|
maxBitrateBps: 50_000_000,
|
|
maxFramerate: 60,
|
|
simulcast: false,
|
|
options: {
|
|
adaptiveSend: true,
|
|
minVideoFps: 15,
|
|
minResolutionScale: 0.5,
|
|
maxAudioBufferMs: 750,
|
|
captureId: 'screen-harness-primary',
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('rejects screen-share publish and encoding update without capture IDs', async () => {
|
|
const calls = [];
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
publishScreenShare(...args) {
|
|
calls.push(['publish', args]);
|
|
return Promise.resolve();
|
|
}
|
|
|
|
updateScreenShareEncoding(...args) {
|
|
calls.push(['update', args]);
|
|
return Promise.resolve();
|
|
}
|
|
},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
assert.throws(
|
|
() => engine.publishScreenShare(1280, 720, 'h264', undefined, 30, false, undefined),
|
|
/screen-share publish requires a non-empty captureId/,
|
|
);
|
|
assert.throws(
|
|
() => engine.publishScreenShare(1280, 720, 'h264', undefined, 30, false, {captureId: ''}),
|
|
/screen-share publish requires a non-empty captureId/,
|
|
);
|
|
assert.throws(
|
|
() => engine.updateScreenShareEncoding(1280, 720, undefined, 30, undefined),
|
|
/screen-share encoding update requires a non-empty captureId/,
|
|
);
|
|
assert.throws(
|
|
() => engine.updateScreenShareEncoding(1280, 720, undefined, 30, {captureId: ''}),
|
|
/screen-share encoding update requires a non-empty captureId/,
|
|
);
|
|
assert.deepEqual(calls, []);
|
|
});
|
|
|
|
test('delegates connect options through the VoiceEngine wrapper', async () => {
|
|
const calls = [];
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
connect(url, token, e2eeKey, options) {
|
|
calls.push({url, token, e2eeKey, options});
|
|
return Promise.resolve();
|
|
}
|
|
},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
const key = Buffer.from('secret');
|
|
await engine.connect('ws://localhost:7880', 'token', key, {
|
|
autoSubscribe: false,
|
|
adaptiveStream: true,
|
|
dynacast: true,
|
|
});
|
|
assert.deepEqual(calls, [
|
|
{
|
|
url: 'ws://localhost:7880',
|
|
token: 'token',
|
|
e2eeKey: key,
|
|
options: {
|
|
autoSubscribe: false,
|
|
adaptiveStream: true,
|
|
dynacast: true,
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('delegates native screen frame sink handle creation through the VoiceEngine wrapper', () => {
|
|
const calls = [];
|
|
const handle = {native: true};
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
createScreenFrameSinkHandle(captureId) {
|
|
calls.push(captureId);
|
|
return handle;
|
|
}
|
|
},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
assert.equal(engine.createScreenFrameSinkHandle('capture-1'), handle);
|
|
assert.deepEqual(calls, ['capture-1']);
|
|
});
|
|
|
|
test('treats native screen frame sink handles as unavailable for older voice bindings', () => {
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
assert.equal(engine.createScreenFrameSinkHandle('capture-1'), null);
|
|
});
|
|
|
|
test('defaults dropped video callback metrics to zero for older native bindings', () => {
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
inboundAudioFrames() {
|
|
return 2;
|
|
}
|
|
|
|
inboundVideoFrames() {
|
|
return 3;
|
|
}
|
|
},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
assert.equal(engine.droppedVideoFrameCallbacks(), 0);
|
|
});
|
|
|
|
test('delegates remote track subscription updates through the VoiceEngine wrapper', async () => {
|
|
const calls = [];
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
setRemoteTrackSubscription(participantIdentity, source, subscribed, enabled, quality) {
|
|
calls.push({participantIdentity, source, subscribed, enabled, quality});
|
|
return Promise.resolve();
|
|
}
|
|
},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
await engine.setRemoteTrackSubscription({
|
|
participantIdentity: 'user_1_conn',
|
|
source: 'screen_share',
|
|
subscribed: true,
|
|
enabled: false,
|
|
quality: 'high',
|
|
});
|
|
|
|
assert.deepEqual(calls, [
|
|
{
|
|
participantIdentity: 'user_1_conn',
|
|
source: 'screen_share',
|
|
subscribed: true,
|
|
enabled: false,
|
|
quality: 'high',
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('ignores remote track subscription updates for older native bindings', async () => {
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
await engine.setRemoteTrackSubscription({
|
|
participantIdentity: 'user_1_conn',
|
|
source: 'camera',
|
|
subscribed: true,
|
|
});
|
|
});
|
|
|
|
test('delegates data packets through the VoiceEngine wrapper', async () => {
|
|
const calls = [];
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
publishData(payload, reliable, topic, destinationIdentities) {
|
|
calls.push({payload, reliable, topic, destinationIdentities});
|
|
return Promise.resolve();
|
|
}
|
|
},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
await engine.publishData(new Uint8Array([1, 2, 3]), {
|
|
reliable: true,
|
|
topic: 'screen-share-codec',
|
|
destinationIdentities: ['user_1_conn'],
|
|
});
|
|
|
|
assert.equal(Buffer.isBuffer(calls[0].payload), true);
|
|
assert.deepEqual([...calls[0].payload], [1, 2, 3]);
|
|
assert.deepEqual(calls, [
|
|
{
|
|
payload: calls[0].payload,
|
|
reliable: true,
|
|
topic: 'screen-share-codec',
|
|
destinationIdentities: ['user_1_conn'],
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('rejects invalid data packet payloads in the wrapper', async () => {
|
|
webrtcSender.__setBindingForTests({
|
|
VoiceEngine: class {
|
|
publishData() {
|
|
return Promise.resolve();
|
|
}
|
|
},
|
|
});
|
|
|
|
const engine = new webrtcSender.VoiceEngine();
|
|
await assert.rejects(() => engine.publishData('not-bytes'), /payload must be/);
|
|
});
|
|
|
|
test('returns an unavailable hardware encoder capability without a binding', () => {
|
|
webrtcSender.__setBindingForTests(null);
|
|
|
|
assert.deepEqual(webrtcSender.getHardwareEncoderCapability(), {
|
|
available: false,
|
|
backend: 'none',
|
|
compiled: false,
|
|
runtime: false,
|
|
codecs: [],
|
|
zeroCopy: false,
|
|
nativeInputs: [],
|
|
reason: 'native_binding_unavailable',
|
|
detail: '@fluxer/webrtc-sender binding unavailable',
|
|
});
|
|
});
|
|
|
|
test('exposes every VoiceEngine method declared in index.d.ts on the wrapper', async () => {
|
|
const {readFileSync} = await import('node:fs');
|
|
const dts = readFileSync(new URL('./index.d.ts', import.meta.url), 'utf8');
|
|
const classStart = dts.indexOf('export declare class VoiceEngine {');
|
|
assert.ok(classStart >= 0);
|
|
const classEnd = dts.indexOf('\n}', classStart);
|
|
assert.ok(classEnd > classStart);
|
|
const classBody = dts.slice(classStart, classEnd);
|
|
const declaredMethods = [...classBody.matchAll(/^\t([A-Za-z0-9_]+)\(/gm)]
|
|
.map((match) => match[1])
|
|
.filter((name) => name !== 'constructor');
|
|
assert.ok(declaredMethods.length >= 30);
|
|
const wrapperMethods = new Set(Object.getOwnPropertyNames(webrtcSender.VoiceEngine.prototype));
|
|
const missing = declaredMethods.filter((name) => !wrapperMethods.has(name));
|
|
assert.deepEqual(missing, []);
|
|
});
|
|
});
|