refactor(desktop): replace the native voice engine with a standalone hardware encoder (#1677)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-16 19:24:31 +02:00
1 parent 98c40c6979
commit ccdd85b099
893 files changed
+620 -371790

No files matched your search

-37
View File
@@ -27,7 +27,6 @@ import {
isValidTargetPid,
normalizeTimestampUs,
} from './NativeAudioValidation';
import {createScreenAudioSinkHandleForSender, hasActiveNativeEngineForSender} from './NativeVoiceEngine';
const logger = createChildLogger('NativeAudio');
const requireModule = createRequire(import.meta.url);
@@ -41,8 +40,6 @@ interface NativeCaptureInstance {
removeListener(event: 'closed', listener: () => void): this;
start(): Promise<void> | void;
stop(): Promise<void> | void;
setScreenAudioSink?: (handle: unknown) => boolean;
clearScreenAudioSink?: () => void;
routingGraph?: () => VirtmicRoutingGraph | null;
}
@@ -749,8 +746,6 @@ async function startNativeAudioCapture(
throw new Error('Native audio addon unavailable for this platform');
}
let firstFrameLogged = false;
let nativeSinkAttached = false;
let slowPathReported = false;
const session: ActiveNativeAudioSession = {
captureId,
capture,
@@ -774,19 +769,6 @@ async function startNativeAudioCapture(
);
return;
}
if (nativeSinkAttached) {
if (!slowPathReported) {
slowPathReported = true;
logger.error(
'Native screen-audio fast path stopped engaging: a captured frame reached the JS bridge even though the native engine sink is attached. Failing the capture so this critical regression surfaces immediately instead of silently downgrading to the crackle-prone path.',
{captureId, senderId: sender.id, sampleRate: frame.sampleRate, channels: frame.channels},
);
stopActiveSession(session, 'addon-error', 'native screen-audio fast path stopped engaging').catch((error) =>
logger.warn('stopActiveSession failed after native fast-path regression', {captureId, error}),
);
}
return;
}
try {
sender.send('native-audio:frame', {
captureId,
@@ -830,25 +812,6 @@ async function startNativeAudioCapture(
activeSessions.set(captureId, session);
rememberSenderSession(sender.id, captureId);
try {
if (hasActiveNativeEngineForSender(sender.id)) {
if (typeof capture.setScreenAudioSink !== 'function') {
throw new Error(
`Native screen-audio fast path unavailable for capture ${captureId}: capture binding lacks setScreenAudioSink (stale native build)`,
);
}
const screenAudioSink = createScreenAudioSinkHandleForSender(sender.id);
if (!screenAudioSink) {
throw new Error(
`Native screen-audio fast path unavailable for capture ${captureId}: native voice engine produced no sink handle for the active session`,
);
}
if (capture.setScreenAudioSink(screenAudioSink) !== true) {
throw new Error(
`Native screen-audio fast path unavailable for capture ${captureId}: native capture binding does not implement setScreenAudioSink (stale native build)`,
);
}
nativeSinkAttached = true;
}
await Promise.resolve(capture.start());
} catch (error) {
logger.error('Native audio capture failed to start', {captureId, error});
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createRequire} from 'node:module';
import {createChildLogger} from '@electron/common/Logger';
import {
normalizeVoiceEngineV2BridgeHardwareEncoderCapabilities,
unavailableVoiceEngineV2BridgeHardwareEncoderCapabilities,
VOICE_ENGINE_V2_IPC_CHANNELS,
type VoiceEngineV2BridgeHardwareEncoderCapabilities,
} from '@fluxer/voice_engine_v2/bridge';
import {ipcMain} from 'electron';
const logger = createChildLogger('NativeHardwareEncoder');
const requireModule = createRequire(import.meta.url);
interface HardwareEncoderModule {
getHardwareEncoderCapabilities?: () => unknown;
}
let cachedModule: HardwareEncoderModule | null | undefined;
let cachedModuleLoadErrorDetail: string | undefined;
function loadHardwareEncoderModule(): HardwareEncoderModule | null {
if (cachedModule !== undefined) return cachedModule;
cachedModuleLoadErrorDetail = undefined;
try {
cachedModule = requireModule('@fluxer/hardware-encoder') as HardwareEncoderModule;
} catch (error) {
logger.warn('Failed to load @fluxer/hardware-encoder', error);
cachedModuleLoadErrorDetail = error instanceof Error ? error.message : String(error);
cachedModule = null;
}
return cachedModule;
}
function getHardwareEncoderCapabilities(): VoiceEngineV2BridgeHardwareEncoderCapabilities {
const mod = loadHardwareEncoderModule();
if (!mod) {
return unavailableVoiceEngineV2BridgeHardwareEncoderCapabilities('load-failed', cachedModuleLoadErrorDetail);
}
if (typeof mod.getHardwareEncoderCapabilities !== 'function') {
return unavailableVoiceEngineV2BridgeHardwareEncoderCapabilities('unsupported-addon-version');
}
try {
return normalizeVoiceEngineV2BridgeHardwareEncoderCapabilities(mod.getHardwareEncoderCapabilities());
} catch (error) {
logger.warn('Native hardware encoder getHardwareEncoderCapabilities() threw', error);
const detail = error instanceof Error ? error.message : String(error);
return unavailableVoiceEngineV2BridgeHardwareEncoderCapabilities('query-failed', detail);
}
}
let handlersRegistered = false;
export function registerNativeHardwareEncoderHandlers(): void {
if (handlersRegistered) return;
handlersRegistered = true;
ipcMain.handle(
VOICE_ENGINE_V2_IPC_CHANNELS.getHardwareEncoderCapabilities,
(): VoiceEngineV2BridgeHardwareEncoderCapabilities => getHardwareEncoderCapabilities(),
);
}
export function cleanupNativeHardwareEncoderHandlers(): void {
if (!handlersRegistered) return;
ipcMain.removeHandler(VOICE_ENGINE_V2_IPC_CHANNELS.getHardwareEncoderCapabilities);
handlersRegistered = false;
}
@@ -86,12 +86,7 @@ function makeNativeAddon({
};
}
function loadNativeScreenCapture({
platform = 'linux',
addon,
tccStatus = 'not-determined',
frameSinkHandle = null,
} = {}) {
function loadNativeScreenCapture({platform = 'linux', addon, tccStatus = 'not-determined'} = {}) {
const handlers = new Map();
const calls = {
logs: {debug: [], warn: []},
@@ -149,12 +144,6 @@ function loadNativeScreenCapture({
if (specifier === './MacTcc') {
return {getTccStatus: () => tccStatus};
}
if (specifier === './NativeVoiceEngine') {
return {
createNativeVoiceEngineScreenFrameSinkHandle: (captureId) =>
typeof frameSinkHandle === 'function' ? frameSinkHandle(captureId) : frameSinkHandle,
};
}
if (specifier === './NativeScreenCaptureValidation') {
return {
isValidStartOptions: () => true,
@@ -242,15 +231,10 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
});
test('starts display and window captures with exact source id and kind and reports diagnostics', async () => {
const frameSinkHandles = {
'capture-1': {native: true, captureId: 'capture-1'},
'capture-2': {native: true, captureId: 'capture-2'},
};
const {addon, captures} = makeNativeAddon();
const harness = loadNativeScreenCapture({
platform: 'linux',
addon,
frameSinkHandle: (captureId) => frameSinkHandles[captureId] ?? null,
});
const {sender, sent} = makeSender();
harness.module.registerNativeScreenCaptureHandlers();
@@ -296,7 +280,6 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
showCursorClicks: true,
captureRect: {x: 10, y: 20, width: 300, height: 200},
nativeFrameSinkRequired: true,
frameSinkHandle: frameSinkHandles['capture-1'],
},
{
sourceId: 'window:4242',
@@ -311,7 +294,6 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
showCursorClicks: false,
captureRect: undefined,
nativeFrameSinkRequired: true,
frameSinkHandle: frameSinkHandles['capture-2'],
},
]);
assert.equal(displayResult.captureId, 'capture-1');
@@ -342,37 +324,11 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
assert.equal(captures[1].stopCount, 1);
});
test('fails fast when native frame sink is required but unavailable', async () => {
const {addon, captures} = makeNativeAddon();
const harness = loadNativeScreenCapture({platform: 'linux', addon});
const {sender} = makeSender();
harness.module.registerNativeScreenCaptureHandlers();
await assert.rejects(
() =>
harness.handlers.get('native-screen-capture:start')(
{sender},
{
sourceId: 'window:4242',
sourceKind: 'window',
width: 1280,
height: 720,
captureId: 'preselected-capture-id',
nativeFrameSinkRequired: true,
},
),
/requires a native frame sink handle/,
);
assert.equal(captures.length, 0);
});
test('passes caller-provided capture id and native sink handle to the platform wrapper', async () => {
const frameSinkHandle = {native: true};
test('passes caller-provided capture id to the platform wrapper', async () => {
const {addon, captures} = makeNativeAddon();
const harness = loadNativeScreenCapture({
platform: 'linux',
addon,
frameSinkHandle: (captureId) => (captureId === 'preselected-capture-id' ? frameSinkHandle : null),
});
const {sender, sent} = makeSender();
harness.module.registerNativeScreenCaptureHandlers();
@@ -403,7 +359,6 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
showCursorClicks: false,
captureRect: undefined,
nativeFrameSinkRequired: true,
frameSinkHandle,
});
assert.deepEqual(sent, []);
});
@@ -518,7 +473,6 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
const harness = loadNativeScreenCapture({
platform: 'win32',
addon,
frameSinkHandle: (captureId) => ({native: true, captureId}),
});
harness.module.registerNativeScreenCaptureHandlers();
const {sender} = makeSender();
@@ -19,7 +19,6 @@ import type {
import {ipcMain} from 'electron';
import {getTccStatus} from './MacTcc';
import {isValidStartOptions, normalizeScreenCaptureDimension} from './NativeScreenCaptureValidation';
import {createNativeVoiceEngineScreenFrameSinkHandle} from './NativeVoiceEngine';
const logger = createChildLogger('NativeScreenCapture');
const requireModule = createRequire(import.meta.url);
@@ -822,10 +821,6 @@ async function startNativeScreenCapture(
if (activeSessions.has(captureId)) {
throw new Error('Native screen capture id is already active');
}
const frameSinkHandle = createNativeVoiceEngineScreenFrameSinkHandle(captureId);
if (!frameSinkHandle) {
throw new Error('Native screen capture requires a native frame sink handle, but none is active');
}
const capture = new loadResult.addon.ScreenCapture({
sourceId: options.sourceId,
sourceKind: options.sourceKind,
@@ -839,7 +834,6 @@ async function startNativeScreenCapture(
showCursorClicks: options.showCursorClicks === true,
captureRect: options.captureRect,
nativeFrameSinkRequired: true,
frameSinkHandle,
});
const session: ActiveNativeScreenSession = {
captureId,
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,241 +0,0 @@
// 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);
});
@@ -1,197 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import assert from 'node:assert/strict';
import type {
VoiceEngineV2BridgeAudioInputDevice,
VoiceEngineV2BridgeAudioOutputDevice,
VoiceEngineV2BridgeCameraDevice,
VoiceEngineV2BridgePublishMicrophoneOptions,
} from '@fluxer/voice_engine_v2/bridge';
export const MICROPHONE_MAX_BITRATE_BPS_FLOOR = 8_000;
export const MICROPHONE_MAX_BITRATE_BPS_CAP = 510_000;
export interface NativeMicrophonePublishOptions {
deviceId?: string;
echoCancellation?: boolean;
noiseSuppression?: boolean;
autoGainControl?: boolean;
deepFilter?: boolean;
deepFilterNoiseReductionLevel?: number;
maxBitrateBps?: number;
}
interface NativeVoiceEnginePublishMicrophoneArgs extends VoiceEngineV2BridgePublishMicrophoneOptions {
maxBitrateBps?: number;
}
export interface NativeVoiceEngineAudioIpcEngine {
publishMicrophone(sampleRate: number, channels: number): Promise<void>;
publishDeviceMicrophone?(opts: NativeMicrophonePublishOptions): Promise<void>;
}
export interface NativeVoiceEngineAudioQueryEngine {
listAudioInputDevices():
| Array<VoiceEngineV2BridgeAudioInputDevice>
| string
| Promise<Array<VoiceEngineV2BridgeAudioInputDevice> | string>;
listAudioOutputDevices():
| Array<VoiceEngineV2BridgeAudioOutputDevice>
| string
| Promise<Array<VoiceEngineV2BridgeAudioOutputDevice> | string>;
}
interface NativeVoiceEngineAudioIpcSession {
engine: NativeVoiceEngineAudioIpcEngine;
}
export class NativeVoiceEngineCapabilityError extends Error {
readonly capability: string;
constructor(capability: string, message: string) {
super(message);
this.name = 'NativeVoiceEngineCapabilityError';
this.capability = capability;
}
}
export class NativeVoiceEngineNotConnectedError extends Error {
constructor() {
super('Native voice engine is not connected');
this.name = 'NativeVoiceEngineNotConnectedError';
}
}
export class NativeVoiceEngineUnavailableError extends Error {
constructor() {
super('Native voice engine is unavailable');
this.name = 'NativeVoiceEngineUnavailableError';
}
}
export class NativeVoiceEngineInvalidArgsError extends Error {
constructor(message: string) {
super(message);
this.name = 'NativeVoiceEngineInvalidArgsError';
}
}
function parseNativeJsonArray<T>(value: unknown, label: string): Array<T> {
if (Array.isArray(value)) return value as Array<T>;
if (typeof value !== 'string') {
throw new Error(`Native voice engine ${label} must be a JSON array`);
}
let parsed: unknown;
try {
parsed = JSON.parse(value) as unknown;
} catch (error) {
throw new Error(`Failed to parse native voice engine ${label}: ${String(error)}`);
}
if (!Array.isArray(parsed)) {
throw new Error(`Native voice engine ${label} must be a JSON array`);
}
return parsed as Array<T>;
}
function clampNativeMicrophoneDeepFilterNoiseReductionLevel(level: number | undefined): number | undefined {
if (level === undefined) {
return undefined;
}
if (!Number.isFinite(level)) {
throw new NativeVoiceEngineInvalidArgsError('Invalid voice-engine microphone deepFilterNoiseReductionLevel');
}
const clamped = Math.min(Math.max(level, 0), 100);
assert.ok(clamped >= 0, 'microphone deepFilterNoiseReductionLevel must be at least 0');
assert.ok(clamped <= 100, 'microphone deepFilterNoiseReductionLevel must be at most 100');
return clamped;
}
export function clampNativeMicrophoneMaxBitrateBps(maxBitrateBps: number | undefined): number | undefined {
if (maxBitrateBps === undefined) {
return undefined;
}
if (!Number.isInteger(maxBitrateBps)) {
throw new NativeVoiceEngineInvalidArgsError('Invalid voice-engine microphone maxBitrateBps');
}
if (maxBitrateBps <= 0) {
throw new NativeVoiceEngineInvalidArgsError('Invalid voice-engine microphone maxBitrateBps');
}
const clamped = Math.min(Math.max(maxBitrateBps, MICROPHONE_MAX_BITRATE_BPS_FLOOR), MICROPHONE_MAX_BITRATE_BPS_CAP);
assert.ok(clamped >= MICROPHONE_MAX_BITRATE_BPS_FLOOR, 'microphone maxBitrateBps must respect the floor');
assert.ok(clamped <= MICROPHONE_MAX_BITRATE_BPS_CAP, 'microphone maxBitrateBps must respect the cap');
return clamped;
}
export async function handleNativeVoiceEnginePublishMicrophone(
session: NativeVoiceEngineAudioIpcSession | null,
args: NativeVoiceEnginePublishMicrophoneArgs,
): Promise<void> {
if (!session) {
throw new NativeVoiceEngineNotConnectedError();
}
if (args.mode === 'pcm-test') {
if (typeof args.sampleRate !== 'number' || typeof args.numChannels !== 'number') {
throw new NativeVoiceEngineInvalidArgsError('Invalid voice-engine pcm-test microphone args');
}
await session.engine.publishMicrophone(args.sampleRate, args.numChannels);
return;
}
if (typeof session.engine.publishDeviceMicrophone !== 'function') {
throw new NativeVoiceEngineCapabilityError(
'microphoneCapture',
'Native voice engine addon does not support device microphone capture',
);
}
const maxBitrateBps = clampNativeMicrophoneMaxBitrateBps(args.maxBitrateBps);
const deepFilterNoiseReductionLevel = clampNativeMicrophoneDeepFilterNoiseReductionLevel(
args.deepFilterNoiseReductionLevel,
);
await session.engine.publishDeviceMicrophone({
deviceId: args.deviceId,
echoCancellation: args.echoCancellation,
noiseSuppression: args.noiseSuppression,
autoGainControl: args.autoGainControl,
...(args.deepFilter !== undefined ? {deepFilter: args.deepFilter} : {}),
...(deepFilterNoiseReductionLevel !== undefined ? {deepFilterNoiseReductionLevel} : {}),
...(maxBitrateBps !== undefined ? {maxBitrateBps} : {}),
});
}
interface NativeVoiceEngineCameraQueryEngine {
listCameraDevices(): Array<VoiceEngineV2BridgeCameraDevice> | Promise<Array<VoiceEngineV2BridgeCameraDevice>>;
}
export async function handleNativeVoiceEngineListCameraDevices(
engine: NativeVoiceEngineCameraQueryEngine | null,
): Promise<Array<VoiceEngineV2BridgeCameraDevice>> {
if (!engine) {
throw new NativeVoiceEngineUnavailableError();
}
const devices = await engine.listCameraDevices();
assert.ok(Array.isArray(devices), 'native voice engine listCameraDevices must return an array');
return devices;
}
export async function handleNativeVoiceEngineListAudioOutputDevices(
engine: NativeVoiceEngineAudioQueryEngine | null,
): Promise<Array<VoiceEngineV2BridgeAudioOutputDevice>> {
if (!engine) {
throw new NativeVoiceEngineUnavailableError();
}
return parseNativeJsonArray<VoiceEngineV2BridgeAudioOutputDevice>(
await engine.listAudioOutputDevices(),
'audio output devices',
);
}
export async function handleNativeVoiceEngineListAudioInputDevices(
engine: NativeVoiceEngineAudioQueryEngine | null,
): Promise<Array<VoiceEngineV2BridgeAudioInputDevice>> {
if (!engine) {
throw new NativeVoiceEngineUnavailableError();
}
return parseNativeJsonArray<VoiceEngineV2BridgeAudioInputDevice>(
await engine.listAudioInputDevices(),
'audio input devices',
);
}
@@ -1,210 +0,0 @@
{
"version": 1,
"name": "native_voice_engine_ipc_requests",
"cases": [
{
"name": "valid connect reaches the native engine",
"channel": "connect",
"event": "sender",
"args": [{"url": "wss://voice.example.test", "token": "token-connect"}],
"expected": {"result": "void", "engineCalls": {"connect": 1}}
},
{
"name": "invalid connect args throw",
"channel": "connect",
"event": "sender",
"args": [{"url": "wss://voice.example.test", "token": ""}],
"expected": {"throws": "Invalid voice-engine connect args"}
},
{
"name": "invalid mic enabled args return typed operation failure",
"channel": "setMicEnabled",
"args": ["yes"],
"expected": {
"result": {
"ok": false,
"error": {
"code": "invalid-args",
"message": "Invalid voice-engine set-mic-enabled args"
}
}
}
},
{
"name": "valid microphone publish returns typed success",
"setup": "connected",
"channel": "publishMicrophone",
"args": [{"mode": "pcm-test", "sampleRate": 48000, "numChannels": 2}],
"expected": {"result": {"ok": true}}
},
{
"name": "invalid microphone publish returns typed operation failure",
"channel": "publishMicrophone",
"args": [{"mode": "pcm-test", "sampleRate": "48000", "numChannels": 2}],
"expected": {
"result": {
"ok": false,
"error": {
"code": "invalid-args",
"message": "Invalid voice-engine publish-microphone args",
"capability": "microphoneCapture"
}
}
}
},
{
"name": "valid zero-copy screen publish reaches native engine",
"setup": "connected",
"channel": "publishScreen",
"args": [
{
"captureId": "capture-zero-copy",
"width": 1920,
"height": 1080,
"codec": "h264",
"maxBitrateBps": 6000000,
"maxFramerate": 60,
"hardwareEncoding": true,
"zeroCopyRequired": true
}
],
"expected": {"result": "void", "engineCalls": {"publishScreenShare": 1}}
},
{
"name": "explicit non-zero-copy screen publish is rejected",
"setup": "connected",
"channel": "publishScreen",
"args": [
{
"captureId": "capture-copying",
"width": 1920,
"height": 1080,
"zeroCopyRequired": false
}
],
"expected": {"throws": "requires zero-copy transport"}
},
{
"name": "hardware screen encoding without zero-copy is rejected",
"setup": "screen",
"channel": "updateScreenShareEncoding",
"args": [
{
"captureId": "fixture-screen",
"width": 1280,
"height": 720,
"hardwareEncoding": true,
"zeroCopyRequired": false
}
],
"expected": {"throws": "requires zero-copy transport"}
},
{
"name": "valid native screen audio publish reaches native engine",
"setup": "connected",
"channel": "publishScreenAudio",
"args": [{"sampleRate": 48000, "numChannels": 2, "route": "native", "captureId": "capture-zero-copy"}],
"expected": {"result": "void", "engineCalls": {"publishScreenShareAudio": 1}}
},
{
"name": "invalid screen audio route throws",
"setup": "connected",
"channel": "publishScreenAudio",
"args": [{"sampleRate": 48000, "numChannels": 2, "route": "loopback"}],
"expected": {"throws": "Invalid voice-engine publish-screen-audio args"}
},
{
"name": "valid camera publish reaches native engine",
"setup": "connected",
"channel": "publishCamera",
"args": [{"deviceId": "camera-1", "width": 1280, "height": 720, "frameRate": 30}],
"expected": {"result": "void", "engineCalls": {"publishCamera": 1}}
},
{
"name": "valid native camera sink publish reaches native engine",
"setup": "connected",
"channel": "publishNativeCameraSink",
"args": [{"deviceId": "camera-1", "width": 1280, "height": 720, "frameRate": 30, "backgroundMode": "blur"}],
"expected": {"result": {"trackSid": "TR_native_camera"}, "engineCalls": {"publishNativeCameraSink": 1}}
},
{
"name": "valid processed camera publish reaches native engine",
"setup": "connected",
"channel": "publishProcessedCamera",
"args": [{"width": 1280, "height": 720, "frameRate": 30}],
"expected": {"result": {"trackSid": "TR_processed_camera"}, "engineCalls": {"publishProcessedCamera": 1}}
},
{
"name": "valid processed camera frame reaches native engine",
"setup": "connected",
"channel": "pushProcessedCameraFrame",
"args": [
{"format": "i420", "width": 2, "height": 2, "timestampUs": 1, "data": {"$arrayBuffer": [1, 1, 1, 1, 2, 3]}}
],
"expected": {"result": true, "engineCalls": {"processedCameraFrames": 1}}
},
{
"name": "invalid processed camera frame throws",
"setup": "connected",
"channel": "pushProcessedCameraFrame",
"args": [{"format": "i420", "width": 2, "height": 2, "timestampUs": 1, "data": {"$arrayBuffer": [1, 2, 3]}}],
"expected": {"throws": "Invalid voice-engine push-processed-camera-frame args"}
},
{
"name": "invalid camera publish throws",
"setup": "connected",
"channel": "publishCamera",
"args": [{"deviceId": "camera-1", "frameRate": 0}],
"expected": {"throws": "Invalid voice-engine publish-camera args"}
},
{
"name": "connected stats return normalized empty stats",
"setup": "connected",
"channel": "getConnectionStats",
"args": [],
"expected": {"result": {"rttMs": null, "outbound": [], "inbound": []}}
},
{
"name": "valid participant volume reaches native engine",
"setup": "connected",
"channel": "setParticipantVolume",
"args": [{"participantSid": "participant-1", "volume": 0.75}],
"expected": {"result": "void", "engineCalls": {"setParticipantVolume": 1}}
},
{
"name": "invalid participant volume throws",
"setup": "connected",
"channel": "setParticipantVolume",
"args": [{"participantSid": "", "volume": 0.75}],
"expected": {"throws": "Invalid voice-engine set-participant-volume args"}
},
{
"name": "valid remote subscription reaches native engine",
"setup": "connected",
"channel": "setRemoteTrackSubscription",
"args": [{"participantIdentity": "alice", "source": "screen", "subscribed": true, "quality": "high"}],
"expected": {"result": "void", "engineCalls": {"remoteTrackSubscription": 1}}
},
{
"name": "invalid remote subscription quality throws",
"setup": "connected",
"channel": "setRemoteTrackSubscription",
"args": [{"participantIdentity": "alice", "source": "screen", "subscribed": true, "quality": "ultra"}],
"expected": {"throws": "Invalid voice-engine set-remote-track-subscription args"}
},
{
"name": "valid data publish reaches native engine",
"setup": "connected",
"channel": "publishData",
"args": [{"payload": {"$arrayBuffer": [1, 2, 3, 4]}, "reliable": true, "topic": "fixture"}],
"expected": {"result": "void", "engineCalls": {"publishData": 1}}
},
{
"name": "invalid data payload throws",
"setup": "connected",
"channel": "publishData",
"args": [{"payload": [1, 2, 3, 4], "reliable": true}],
"expected": {"throws": "Invalid voice-engine publish-data args"}
}
]
}
+8 -4
View File
@@ -61,9 +61,12 @@ import {cleanupVirtmic, registerVirtmicHandlers} from '@electron/main/LinuxAudio
import {initializeMainI18n} from '@electron/main/MainI18n';
import {createApplicationMenu} from '@electron/main/Menu';
import {cleanupNativeAudio, registerNativeAudioHandlers} from '@electron/main/NativeAudio';
import {
cleanupNativeHardwareEncoderHandlers,
registerNativeHardwareEncoderHandlers,
} from '@electron/main/NativeHardwareEncoder';
import {runNativeModulePreflight} from '@electron/main/NativeModulePreflight';
import {cleanupNativeScreenCapture, registerNativeScreenCaptureHandlers} from '@electron/main/NativeScreenCapture';
import {cleanupNativeVoiceEngine, registerNativeVoiceEngineHandlers} from '@electron/main/NativeVoiceEngine';
import {appendOpenH264Switches} from '@electron/main/OpenH264Manager';
import {startRpcServer, stopRpcServer} from '@electron/main/RpcServer';
import {cleanupLinuxChromiumSpellcheckDictionaries} from '@electron/main/Spellcheck';
@@ -386,9 +389,9 @@ if (launchConfigurationError) {
log.error('[Init] Failed to register native screen capture handlers:', error);
}
try {
runStartupPhase('native-voice-engine-handlers', registerNativeVoiceEngineHandlers);
runStartupPhase('native-hardware-encoder-handlers', registerNativeHardwareEncoderHandlers);
} catch (error: unknown) {
log.error('[Init] Failed to register native voice engine handlers:', error);
log.error('[Init] Failed to register native hardware encoder handlers:', error);
}
try {
runStartupPhase('application-menu', createApplicationMenu);
@@ -472,9 +475,10 @@ if (launchConfigurationError) {
cleanupGlobalKeyHook();
cleanupNativeAudio();
cleanupNativeScreenCapture();
cleanupNativeHardwareEncoderHandlers();
cleanupVirtmic();
destroyDesktopTray();
const asyncCleanups: Array<Promise<unknown>> = [cleanupNativeVoiceEngine(), stopRpcServer()];
const asyncCleanups: Array<Promise<unknown>> = [stopRpcServer()];
if (netLog.currentlyLogging) {
asyncCleanups.push(
netLog.stopLogging().catch((error) => {