mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
242 lines
9.3 KiB
TypeScript
242 lines
9.3 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
isVoiceEngineV2BridgePublishMicrophoneOptions,
|
|
type VoiceEngineV2BridgeAudioInputDevice,
|
|
type VoiceEngineV2BridgeAudioOutputDevice,
|
|
type VoiceEngineV2BridgeCameraDevice,
|
|
} from '@fluxer/voice_engine_v2/bridge';
|
|
import {
|
|
clampNativeMicrophoneMaxBitrateBps,
|
|
handleNativeVoiceEngineListAudioInputDevices,
|
|
handleNativeVoiceEngineListAudioOutputDevices,
|
|
handleNativeVoiceEngineListCameraDevices,
|
|
handleNativeVoiceEnginePublishMicrophone,
|
|
MICROPHONE_MAX_BITRATE_BPS_CAP,
|
|
MICROPHONE_MAX_BITRATE_BPS_FLOOR,
|
|
type NativeMicrophonePublishOptions,
|
|
type NativeVoiceEngineAudioIpcEngine,
|
|
type NativeVoiceEngineAudioQueryEngine,
|
|
NativeVoiceEngineUnavailableError,
|
|
} from './NativeVoiceEngineIpcCore';
|
|
|
|
function createEngine(overrides: Partial<NativeVoiceEngineAudioIpcEngine> = {}): NativeVoiceEngineAudioIpcEngine {
|
|
return {
|
|
publishMicrophone: async () => {},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createAudioQueryEngine(
|
|
overrides: Partial<NativeVoiceEngineAudioQueryEngine> = {},
|
|
): NativeVoiceEngineAudioQueryEngine {
|
|
return {
|
|
listAudioInputDevices: () => [],
|
|
listAudioOutputDevices: () => [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test('production microphone publish calls publishDeviceMicrophone with device and DSP options', async () => {
|
|
const deviceCalls: Array<NativeMicrophonePublishOptions> = [];
|
|
const pcmCalls: Array<[number, number]> = [];
|
|
const engine = createEngine({
|
|
publishMicrophone: async (sampleRate, channels) => {
|
|
pcmCalls.push([sampleRate, channels]);
|
|
},
|
|
publishDeviceMicrophone: async (options) => {
|
|
deviceCalls.push(options);
|
|
},
|
|
});
|
|
|
|
await handleNativeVoiceEnginePublishMicrophone(
|
|
{engine},
|
|
{
|
|
deviceId: 'recording-device-guid',
|
|
echoCancellation: false,
|
|
noiseSuppression: true,
|
|
autoGainControl: false,
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(deviceCalls, [
|
|
{
|
|
deviceId: 'recording-device-guid',
|
|
echoCancellation: false,
|
|
noiseSuppression: true,
|
|
autoGainControl: false,
|
|
},
|
|
]);
|
|
assert.deepEqual(pcmCalls, []);
|
|
});
|
|
|
|
test('production microphone publish forwards a clamped maxBitrateBps to the native addon', async () => {
|
|
const deviceCalls: Array<NativeMicrophonePublishOptions> = [];
|
|
const engine = createEngine({
|
|
publishDeviceMicrophone: async (options) => {
|
|
deviceCalls.push(options);
|
|
},
|
|
});
|
|
|
|
await handleNativeVoiceEnginePublishMicrophone({engine}, {deviceId: 'recording-device-guid', maxBitrateBps: 96_000});
|
|
await handleNativeVoiceEnginePublishMicrophone({engine}, {deviceId: 'recording-device-guid', maxBitrateBps: 1});
|
|
await handleNativeVoiceEnginePublishMicrophone(
|
|
{engine},
|
|
{deviceId: 'recording-device-guid', maxBitrateBps: 1_000_000},
|
|
);
|
|
|
|
assert.deepEqual(
|
|
deviceCalls.map((options) => options.maxBitrateBps),
|
|
[96_000, MICROPHONE_MAX_BITRATE_BPS_FLOOR, MICROPHONE_MAX_BITRATE_BPS_CAP],
|
|
);
|
|
});
|
|
|
|
test('microphone publish omits maxBitrateBps from the native call when absent', async () => {
|
|
const deviceCalls: Array<NativeMicrophonePublishOptions> = [];
|
|
const engine = createEngine({
|
|
publishDeviceMicrophone: async (options) => {
|
|
deviceCalls.push(options);
|
|
},
|
|
});
|
|
|
|
await handleNativeVoiceEnginePublishMicrophone({engine}, {deviceId: 'recording-device-guid'});
|
|
|
|
assert.equal(deviceCalls.length, 1);
|
|
assert.equal('maxBitrateBps' in (deviceCalls[0] ?? {}), false);
|
|
});
|
|
|
|
test('microphone publish rejects invalid maxBitrateBps values', async () => {
|
|
const engine = createEngine({
|
|
publishDeviceMicrophone: async () => {},
|
|
});
|
|
|
|
for (const maxBitrateBps of [0, -64_000, 64_000.5, Number.NaN, Number.POSITIVE_INFINITY]) {
|
|
await assert.rejects(
|
|
() => handleNativeVoiceEnginePublishMicrophone({engine}, {deviceId: 'recording-device-guid', maxBitrateBps}),
|
|
/Invalid voice-engine microphone maxBitrateBps/,
|
|
);
|
|
}
|
|
await assert.rejects(
|
|
() =>
|
|
handleNativeVoiceEnginePublishMicrophone(
|
|
{engine},
|
|
{deviceId: 'recording-device-guid', maxBitrateBps: '96000' as unknown as number},
|
|
),
|
|
/Invalid voice-engine microphone maxBitrateBps/,
|
|
);
|
|
});
|
|
|
|
test('clampNativeMicrophoneMaxBitrateBps pair-asserts the app-side floor and cap', () => {
|
|
assert.equal(clampNativeMicrophoneMaxBitrateBps(undefined), undefined);
|
|
assert.equal(clampNativeMicrophoneMaxBitrateBps(MICROPHONE_MAX_BITRATE_BPS_FLOOR), MICROPHONE_MAX_BITRATE_BPS_FLOOR);
|
|
assert.equal(clampNativeMicrophoneMaxBitrateBps(MICROPHONE_MAX_BITRATE_BPS_CAP), MICROPHONE_MAX_BITRATE_BPS_CAP);
|
|
assert.equal(clampNativeMicrophoneMaxBitrateBps(7_999), MICROPHONE_MAX_BITRATE_BPS_FLOOR);
|
|
assert.equal(clampNativeMicrophoneMaxBitrateBps(510_001), MICROPHONE_MAX_BITRATE_BPS_CAP);
|
|
assert.equal(clampNativeMicrophoneMaxBitrateBps(64_000), 64_000);
|
|
});
|
|
|
|
test('pcm-test microphone publish calls the legacy synthetic PCM method', async () => {
|
|
const deviceCalls: Array<NativeMicrophonePublishOptions> = [];
|
|
const pcmCalls: Array<[number, number]> = [];
|
|
const engine = createEngine({
|
|
publishMicrophone: async (sampleRate, channels) => {
|
|
pcmCalls.push([sampleRate, channels]);
|
|
},
|
|
publishDeviceMicrophone: async (options) => {
|
|
deviceCalls.push(options);
|
|
},
|
|
});
|
|
|
|
await handleNativeVoiceEnginePublishMicrophone({engine}, {mode: 'pcm-test', sampleRate: 48_000, numChannels: 2});
|
|
|
|
assert.deepEqual(pcmCalls, [[48_000, 2]]);
|
|
assert.deepEqual(deviceCalls, []);
|
|
});
|
|
|
|
test('audio device list handlers parse JSON strings returned by the native addon', async () => {
|
|
const inputDevices: Array<VoiceEngineV2BridgeAudioInputDevice> = [
|
|
{deviceId: 'input-default', label: 'Default microphone', isDefault: true},
|
|
{deviceId: 'input-usb', label: 'USB microphone', isDefault: false},
|
|
];
|
|
const outputDevices: Array<VoiceEngineV2BridgeAudioOutputDevice> = [
|
|
{deviceId: 'output-default', label: 'Default speaker', isDefault: true},
|
|
{deviceId: 'output-headset', label: 'Headset', isDefault: false},
|
|
];
|
|
const engine = createAudioQueryEngine({
|
|
listAudioInputDevices: () => JSON.stringify(inputDevices),
|
|
listAudioOutputDevices: () => JSON.stringify(outputDevices),
|
|
});
|
|
|
|
assert.deepEqual(await handleNativeVoiceEngineListAudioInputDevices(engine), inputDevices);
|
|
assert.deepEqual(await handleNativeVoiceEngineListAudioOutputDevices(engine), outputDevices);
|
|
});
|
|
|
|
test('audio device list handlers pass through arrays returned by the native addon', async () => {
|
|
const inputDevices: Array<VoiceEngineV2BridgeAudioInputDevice> = [
|
|
{deviceId: 'input-default', label: 'Default microphone', isDefault: true},
|
|
];
|
|
const outputDevices: Array<VoiceEngineV2BridgeAudioOutputDevice> = [
|
|
{deviceId: 'output-default', label: 'Default speaker', isDefault: true},
|
|
];
|
|
const engine = createAudioQueryEngine({
|
|
listAudioInputDevices: () => inputDevices,
|
|
listAudioOutputDevices: () => outputDevices,
|
|
});
|
|
|
|
assert.equal(await handleNativeVoiceEngineListAudioInputDevices(engine), inputDevices);
|
|
assert.equal(await handleNativeVoiceEngineListAudioOutputDevices(engine), outputDevices);
|
|
});
|
|
|
|
test('audio device listing throws when the native voice engine is unavailable', async () => {
|
|
await assert.rejects(() => handleNativeVoiceEngineListAudioInputDevices(null), NativeVoiceEngineUnavailableError);
|
|
await assert.rejects(() => handleNativeVoiceEngineListAudioOutputDevices(null), NativeVoiceEngineUnavailableError);
|
|
});
|
|
|
|
test('audio device listing rejects malformed native addon JSON', async () => {
|
|
const engine = createAudioQueryEngine({
|
|
listAudioInputDevices: () => '{',
|
|
listAudioOutputDevices: () => '{"devices":[]}',
|
|
});
|
|
|
|
await assert.rejects(() => handleNativeVoiceEngineListAudioInputDevices(engine), /Failed to parse/);
|
|
await assert.rejects(() => handleNativeVoiceEngineListAudioOutputDevices(engine), /must be a JSON array/);
|
|
});
|
|
|
|
test('microphone publish validation rejects invalid bridge arguments', async () => {
|
|
assert.equal(isVoiceEngineV2BridgePublishMicrophoneOptions(null), false);
|
|
assert.equal(isVoiceEngineV2BridgePublishMicrophoneOptions({mode: 'invalid'}), false);
|
|
assert.equal(
|
|
isVoiceEngineV2BridgePublishMicrophoneOptions({mode: 'pcm-test', sampleRate: '48000', numChannels: 1}),
|
|
false,
|
|
);
|
|
assert.equal(isVoiceEngineV2BridgePublishMicrophoneOptions({deviceId: 7}), false);
|
|
assert.equal(isVoiceEngineV2BridgePublishMicrophoneOptions({mode: 'device', deviceId: 'input-default'}), true);
|
|
|
|
await assert.rejects(
|
|
() => handleNativeVoiceEnginePublishMicrophone(null, {mode: 'pcm-test', sampleRate: 48_000, numChannels: 1}),
|
|
/Native voice engine is not connected/,
|
|
);
|
|
await assert.rejects(
|
|
() => handleNativeVoiceEnginePublishMicrophone({engine: createEngine()}, {mode: 'pcm-test', sampleRate: 48_000}),
|
|
/Invalid voice-engine pcm-test microphone args/,
|
|
);
|
|
await assert.rejects(
|
|
() => handleNativeVoiceEnginePublishMicrophone({engine: createEngine()}, {deviceId: 'input-default'}),
|
|
/Native voice engine addon does not support device microphone capture/,
|
|
);
|
|
});
|
|
|
|
test('camera device listing throws when the native voice engine is unavailable', async () => {
|
|
await assert.rejects(() => handleNativeVoiceEngineListCameraDevices(null), NativeVoiceEngineUnavailableError);
|
|
});
|
|
|
|
test('camera device listing returns the native device list without a connected session', async () => {
|
|
const devices: Array<VoiceEngineV2BridgeCameraDevice> = [
|
|
{deviceId: 'native-studio', label: 'Studio Display Camera', description: 'Apple Studio Display Camera', index: 0},
|
|
];
|
|
|
|
assert.deepEqual(await handleNativeVoiceEngineListCameraDevices({listCameraDevices: () => devices}), devices);
|
|
});
|