mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-19 05:55:34 +09:00
refactor(voice): drop the native only surface from voice_engine_v2 and narrow the desktop bridge (#1788)
This commit is contained in:
1 parent
9731bac40f
commit
4f21430880
43 files changed
+196
-3682
No files matched your search
@@ -6,7 +6,6 @@ import inboundVideoFramesFixtureJson from '../../fixtures/event_logs/inbound_vid
|
||||
import participantsTracksFixtureJson from '../../fixtures/event_logs/participants_tracks.json';
|
||||
import subscriptionQualityFixtureJson from '../../fixtures/event_logs/subscription_quality.json';
|
||||
import watchedStreamsFixtureJson from '../../fixtures/event_logs/watched_streams.json';
|
||||
import {translateVoiceEngineV2BridgeEventToEvents, translateVoiceEngineV2BridgeVideoFrameToEvent} from '../bridge';
|
||||
import type {VoiceEngineV2Command} from '../protocol/commands';
|
||||
import type {VoiceEngineV2Event} from '../protocol/events';
|
||||
import type {
|
||||
@@ -136,39 +135,45 @@ describe('voice engine v2 participants, subscriptions, and inbound video fixture
|
||||
assertFixture(fixture);
|
||||
});
|
||||
|
||||
it('translates native bridge participant, track, and frame events into v2 projections', () => {
|
||||
let snapshot = createVoiceEngineV2InitialSnapshot(availableVoiceEngineV2Capabilities());
|
||||
const bridgeEvents = translateVoiceEngineV2BridgeEventToEvents({
|
||||
type: 'trackSubscribed',
|
||||
payload: {
|
||||
participantSid: 'PA_alice',
|
||||
identity: 'alice',
|
||||
participantName: 'Alice',
|
||||
trackSid: 'TR_screen',
|
||||
trackName: 'screen_share',
|
||||
kind: 'video',
|
||||
source: 'screen_share',
|
||||
muted: false,
|
||||
subscribed: true,
|
||||
subscriptionStatus: 'subscribed',
|
||||
it('projects a subscribed video track and its first frame into room and inbound-video state', () => {
|
||||
const events: Array<VoiceEngineV2Event> = [
|
||||
{
|
||||
type: 'room.trackPublished',
|
||||
track: {
|
||||
participantIdentity: 'alice',
|
||||
participantSid: 'PA_alice',
|
||||
trackSid: 'TR_screen',
|
||||
trackName: 'screen_share',
|
||||
kind: 'video',
|
||||
source: 'screen',
|
||||
muted: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
for (const event of bridgeEvents) {
|
||||
snapshot = transitionVoiceEngineV2(snapshot, event).snapshot;
|
||||
}
|
||||
snapshot = transitionVoiceEngineV2(
|
||||
snapshot,
|
||||
translateVoiceEngineV2BridgeVideoFrameToEvent({
|
||||
meta: {
|
||||
{
|
||||
type: 'inboundVideo.trackSubscribed',
|
||||
track: {
|
||||
participantSid: 'PA_alice',
|
||||
participantIdentity: 'alice',
|
||||
trackSid: 'TR_screen',
|
||||
source: 'screen',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'inboundVideo.frameReceived',
|
||||
frame: {
|
||||
participantSid: 'PA_alice',
|
||||
trackSid: 'TR_screen',
|
||||
width: 320,
|
||||
height: 180,
|
||||
timestampUs: 100,
|
||||
byteLength: 86_400,
|
||||
},
|
||||
data: new ArrayBuffer(86_400),
|
||||
}),
|
||||
).snapshot;
|
||||
},
|
||||
];
|
||||
let snapshot = createVoiceEngineV2InitialSnapshot(availableVoiceEngineV2Capabilities());
|
||||
for (const event of events) {
|
||||
snapshot = transitionVoiceEngineV2(snapshot, event).snapshot;
|
||||
}
|
||||
|
||||
expect(snapshot.room.tracks.TR_screen).toMatchObject({
|
||||
participantIdentity: 'alice',
|
||||
|
||||
@@ -121,94 +121,6 @@ describe('transitionVoiceEngineV2', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('defers native microphone publish until the audio device module is ready', () => {
|
||||
const queued = applyEvents(initialSnapshot(), [
|
||||
{type: 'nativeAudioDeviceModule.statusChanged', status: 'warming'},
|
||||
{type: 'microphone.publishRequested', options: {deviceId: 'default'}},
|
||||
]);
|
||||
|
||||
const established = transitionVoiceEngineV2(queued, {
|
||||
type: 'connection.externallyEstablished',
|
||||
options: {url: 'wss://voice', token: ''},
|
||||
});
|
||||
|
||||
expect(established.snapshot.connection.status).toBe('connected');
|
||||
expect(established.snapshot.microphone.status).toBe('idle');
|
||||
expect(established.snapshot.microphone.desired).toEqual({deviceId: 'default'});
|
||||
expect(established.commands).toEqual([]);
|
||||
|
||||
const ready = transitionVoiceEngineV2(established.snapshot, {
|
||||
type: 'nativeAudioDeviceModule.statusChanged',
|
||||
status: 'ready',
|
||||
});
|
||||
|
||||
expect(ready.snapshot.microphone.status).toBe('publishing');
|
||||
expect(ready.commands).toEqual([{type: 'microphone.publish', operationId: 1, options: {deviceId: 'default'}}]);
|
||||
});
|
||||
|
||||
it('fails deferred native microphone publish when the audio device module fails', () => {
|
||||
const established = applyEvents(initialSnapshot(), [
|
||||
{type: 'nativeAudioDeviceModule.statusChanged', status: 'warming'},
|
||||
{type: 'microphone.publishRequested', options: {deviceId: 'default'}},
|
||||
{type: 'connection.externallyEstablished', options: {url: 'wss://voice', token: ''}},
|
||||
]);
|
||||
|
||||
const failed = transitionVoiceEngineV2(established, {
|
||||
type: 'nativeAudioDeviceModule.statusChanged',
|
||||
status: 'failed',
|
||||
detail: 'ADM probe failed',
|
||||
});
|
||||
|
||||
expect(failed.snapshot.microphone.status).toBe('failed');
|
||||
expect(failed.snapshot.microphone.failure).toEqual({
|
||||
code: 'deviceUnavailable',
|
||||
capability: 'microphone',
|
||||
message: 'ADM probe failed',
|
||||
});
|
||||
expect(failed.commands).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not republish an already published microphone on repeated ADM ready events', () => {
|
||||
const established = applyEvents(initialSnapshot(), [
|
||||
{type: 'nativeAudioDeviceModule.statusChanged', status: 'warming'},
|
||||
{type: 'microphone.publishRequested', options: {deviceId: 'default'}},
|
||||
{type: 'connection.externallyEstablished', options: {url: 'wss://voice', token: ''}},
|
||||
]);
|
||||
const ready = transitionVoiceEngineV2(established, {
|
||||
type: 'nativeAudioDeviceModule.statusChanged',
|
||||
status: 'ready',
|
||||
});
|
||||
const operationId = ready.commands[0]?.operationId ?? -1;
|
||||
const published = transitionVoiceEngineV2(ready.snapshot, {
|
||||
type: 'microphone.publishSucceeded',
|
||||
operationId,
|
||||
}).snapshot;
|
||||
|
||||
const repeated = transitionVoiceEngineV2(published, {
|
||||
type: 'nativeAudioDeviceModule.statusChanged',
|
||||
status: 'ready',
|
||||
});
|
||||
|
||||
expect(repeated.snapshot.microphone.status).toBe('published');
|
||||
expect(repeated.commands).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not let set-microphone-enabled implicitly publish while native ADM is warming', () => {
|
||||
const established = applyEvents(initialSnapshot(), [
|
||||
{type: 'nativeAudioDeviceModule.statusChanged', status: 'warming'},
|
||||
{type: 'connection.externallyEstablished', options: {url: 'wss://voice', token: ''}},
|
||||
]);
|
||||
|
||||
const enabled = transitionVoiceEngineV2(established, {
|
||||
type: 'microphone.setEnabledRequested',
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
expect(enabled.snapshot.microphone.enabled).toBe(true);
|
||||
expect(enabled.snapshot.microphone.status).toBe('idle');
|
||||
expect(enabled.commands).toEqual([]);
|
||||
});
|
||||
|
||||
it('treats a repeated connection.externallyEstablished for the same endpoint as a no-op', () => {
|
||||
const established = applyEvents(initialSnapshot(), [
|
||||
{type: 'connection.externallyEstablished', options: {url: 'wss://voice', token: ''}},
|
||||
|
||||
@@ -17,7 +17,6 @@ import {transitionImplementation} from './implementation';
|
||||
import {transitionInboundVideo} from './inboundVideo';
|
||||
import {transitionLifecycle} from './lifecycle';
|
||||
import {transitionMicrophone} from './microphone';
|
||||
import {transitionNativeAudioDeviceModule} from './nativeAudioDeviceModule';
|
||||
import {transitionNativeAudioTap} from './nativeAudioTap';
|
||||
import {transitionNativeCapture} from './nativeCapture';
|
||||
import {transitionNativeFrameSink} from './nativeFrameSink';
|
||||
@@ -179,8 +178,6 @@ export function dispatchPlatformEvent(
|
||||
case 'devices.selectAudioOutputRequested':
|
||||
case 'devices.selectCameraRequested':
|
||||
return transitionDevices(snapshot, event);
|
||||
case 'nativeAudioDeviceModule.statusChanged':
|
||||
return transitionNativeAudioDeviceModule(snapshot, event);
|
||||
case 'nativeCapture.startRequested':
|
||||
case 'nativeCapture.updateRequested':
|
||||
case 'nativeCapture.stopRequested':
|
||||
|
||||
@@ -203,46 +203,6 @@ function microphoneOptionsWithAudioInputDevice(
|
||||
return {...options, deviceId};
|
||||
}
|
||||
|
||||
function nativeAudioDeviceModuleNotReadyError(snapshot: VoiceEngineV2Snapshot): VoiceEngineV2Error {
|
||||
assert.ok(snapshot != null, 'nativeAudioDeviceModuleNotReadyError snapshot must not be null');
|
||||
assert.ok(
|
||||
snapshot.nativeAudioDeviceModule.status === 'failed',
|
||||
'nativeAudioDeviceModuleNotReadyError requires failed ADM status',
|
||||
);
|
||||
return {
|
||||
code: 'deviceUnavailable',
|
||||
capability: 'microphone',
|
||||
message: snapshot.nativeAudioDeviceModule.detail ?? 'Native audio device module failed to become ready',
|
||||
};
|
||||
}
|
||||
|
||||
function planNativeAudioDeviceModuleGatedMicrophone(snapshot: VoiceEngineV2Snapshot): VoiceEngineV2Transition | null {
|
||||
assert.ok(snapshot != null, 'planNativeAudioDeviceModuleGatedMicrophone snapshot must not be null');
|
||||
assert.ok(snapshot.nativeAudioDeviceModule != null, 'snapshot.nativeAudioDeviceModule must not be null');
|
||||
switch (snapshot.nativeAudioDeviceModule.status) {
|
||||
case 'unsupported':
|
||||
case 'ready':
|
||||
return null;
|
||||
case 'unknown':
|
||||
case 'warming':
|
||||
return {
|
||||
snapshot: {...snapshot, microphone: {...snapshot.microphone, status: 'idle', failure: null}},
|
||||
commands: [],
|
||||
};
|
||||
case 'failed': {
|
||||
const error = nativeAudioDeviceModuleNotReadyError(snapshot);
|
||||
return {
|
||||
snapshot: {
|
||||
...snapshot,
|
||||
microphone: {...snapshot.microphone, status: 'failed', published: null, operationId: null, failure: error},
|
||||
lastFailure: error,
|
||||
},
|
||||
commands: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function planMicrophoneDeviceChange(
|
||||
snapshot: VoiceEngineV2Snapshot,
|
||||
deviceId: string | null,
|
||||
@@ -327,8 +287,6 @@ export function beginMicrophonePublish(snapshot: VoiceEngineV2Snapshot): VoiceEn
|
||||
};
|
||||
}
|
||||
if (!isConnected(snapshot)) return {snapshot, commands: []};
|
||||
const gated = planNativeAudioDeviceModuleGatedMicrophone(snapshot);
|
||||
if (gated) return gated;
|
||||
if (snapshot.microphone.status === 'publishing') return {snapshot, commands: []};
|
||||
if (snapshot.microphone.status === 'published' && sameMicrophoneOptions(snapshot.microphone.published, desired)) {
|
||||
return {snapshot, commands: []};
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import type {VoiceEngineV2Event} from '../../protocol/events';
|
||||
import type {VoiceEngineV2Snapshot, VoiceEngineV2Transition} from '../state';
|
||||
import {beginMicrophonePublish} from './microphone';
|
||||
|
||||
type VoiceEngineV2NativeAudioDeviceModuleEvent = Extract<
|
||||
VoiceEngineV2Event,
|
||||
{type: 'nativeAudioDeviceModule.statusChanged'}
|
||||
>;
|
||||
|
||||
export function transitionNativeAudioDeviceModule(
|
||||
snapshot: VoiceEngineV2Snapshot,
|
||||
event: VoiceEngineV2NativeAudioDeviceModuleEvent,
|
||||
): VoiceEngineV2Transition {
|
||||
assert.ok(snapshot != null, 'transitionNativeAudioDeviceModule snapshot must not be null');
|
||||
assert.ok(event != null, 'transitionNativeAudioDeviceModule event must not be null');
|
||||
assert.equal(event.type, 'nativeAudioDeviceModule.statusChanged', 'unexpected ADM event type');
|
||||
assert.equal(typeof event.status, 'string', 'ADM status must be a string');
|
||||
const next: VoiceEngineV2Snapshot = {
|
||||
...snapshot,
|
||||
nativeAudioDeviceModule: {
|
||||
status: event.status,
|
||||
detail: event.detail ?? null,
|
||||
},
|
||||
};
|
||||
return beginMicrophonePublish(next);
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import type {
|
||||
VoiceEngineV2LiveKitRoomState,
|
||||
VoiceEngineV2MediaStatus,
|
||||
VoiceEngineV2MicrophoneOptions,
|
||||
VoiceEngineV2NativeAudioDeviceModuleState,
|
||||
VoiceEngineV2NativeAudioTapOptions,
|
||||
VoiceEngineV2NativeCaptureOptions,
|
||||
VoiceEngineV2NativeFrameSinkOptions,
|
||||
@@ -162,7 +161,6 @@ export interface VoiceEngineV2Snapshot {
|
||||
connection: VoiceEngineV2ConnectionState;
|
||||
gateway: VoiceEngineV2GatewayState;
|
||||
liveKit: VoiceEngineV2LiveKitState;
|
||||
nativeAudioDeviceModule: VoiceEngineV2NativeAudioDeviceModuleState;
|
||||
microphone: VoiceEngineV2MicrophoneState;
|
||||
camera: VoiceEngineV2LocalMediaState<VoiceEngineV2CameraOptions>;
|
||||
screen: VoiceEngineV2LocalMediaState<VoiceEngineV2ScreenOptions>;
|
||||
@@ -286,10 +284,6 @@ export function createVoiceEngineV2InitialSnapshot(
|
||||
serverRegion: null,
|
||||
failure: null,
|
||||
},
|
||||
nativeAudioDeviceModule: {
|
||||
status: 'unsupported',
|
||||
detail: null,
|
||||
},
|
||||
microphone: {
|
||||
status: 'idle',
|
||||
desired: null,
|
||||
|
||||
Reference in new issue
Block a user