Files
fluxer/fluxer_app/src/features/voice/engine/MediaEngineFacade.ts
T

2909 lines
113 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
import assert from 'node:assert/strict';
import {showGenericErrorModal} from '@app/features/app/components/alerts/GenericErrorModalCommands';
import Authentication from '@app/features/auth/state/Authentication';
import type {Channel} from '@app/features/channel/models/Channel';
import Channels from '@app/features/channel/state/Channels';
import GatewayConnection from '@app/features/gateway/transport/GatewayConnection';
import type {GatewayErrorData} from '@app/features/gateway/transport/GatewaySocket';
import type {GuildReadyData} from '@app/features/gateway/types/GatewayGuildTypes';
import type {VoiceState} from '@app/features/gateway/types/GatewayVoiceTypes';
import GuildMatureContentAgree from '@app/features/guild/state/GuildMatureContentAgree';
import Guilds from '@app/features/guild/state/Guilds';
import {SOMETHING_WENT_WRONG_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import Keybind from '@app/features/input/state/InputKeybind';
import GuildMembers from '@app/features/member/state/GuildMembers';
import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands';
import {SoundType, setSoundOutputDeviceIdResolver} from '@app/features/notification/utils/SoundUtils';
import MediaPermission from '@app/features/permissions/system/state/MediaPermission';
import {Logger} from '@app/features/platform/utils/AppLogger';
import * as SoundCommands from '@app/features/ui/commands/SoundCommands';
import * as ToastCommands from '@app/features/ui/commands/ToastCommands';
import Idle from '@app/features/ui/state/Idle';
import Sound from '@app/features/ui/state/Sound';
import {getElectronAPI} from '@app/features/ui/utils/NativeUtils';
import Users from '@app/features/user/state/Users';
import {getStreamKey, parseStreamKey} from '@app/features/voice/components/StreamKeys';
import {voiceStatsDB} from '@app/features/voice/diagnostics/VoiceStatsDB';
import AdaptiveScreenShareEngine from '@app/features/voice/engine/AdaptiveScreenShareEngine';
import {
createMediaEngineFacadeSnapshot,
type MediaEngineFacadeEvent,
type MediaEngineFacadePendingSessionRestore,
type MediaEngineFacadeSnapshot,
selectMediaEngineConnectPreflightDecision,
selectMediaEngineConnectRequestDecision,
selectMediaEngineGatewayErrorDecision,
shouldCancelMediaEngineReconnectForServerVoiceStateRemoval,
shouldImmediatelyDisconnectMediaEngineForServerVoiceStateRemoval,
shouldNotifyCameraUserLimitRejection,
shouldRunMediaEngineDeferredDisconnect,
transitionMediaEngineFacadeSnapshot,
} from '@app/features/voice/engine/MediaEngineFacadeStateMachine';
import {
AFK_CHECK_INTERVAL_MS,
CLAIM_YOUR_ACCOUNT_TO_JOIN_THIS_VOICE_CHANNEL_DESCRIPTOR,
CLAIM_YOUR_ACCOUNT_TO_JOIN_VOICE_CHANNELS_YOU_DESCRIPTOR,
CLAIM_YOUR_ACCOUNT_TO_START_OR_JOIN_1_DESCRIPTOR,
DEFERRED_DISCONNECT_TIMEOUT_MS,
RECONNECT_SUCCEEDED_PICK_A_SCREEN_AGAIN_IF_YOU_DESCRIPTOR,
VOICE_CAMERA_USER_LIMIT_REACHED_DESCRIPTOR,
VOICE_CHANNEL_NO_LONGER_AVAILABLE_DESCRIPTOR,
VOICE_CONNECTION_FAILED_DESCRIPTOR,
VOICE_CONNECTION_LIMIT_REACHED_DESCRIPTOR,
YOU_CAN_T_JOIN_WHILE_YOU_RE_ON_DESCRIPTOR,
} from '@app/features/voice/engine/media_engine_facade/shared';
import {
createVoiceSessionRestoreSync,
saveCurrentVoiceSessionRestoreSnapshot,
type VoiceSessionRestoreSyncHandle,
} from '@app/features/voice/engine/media_engine_facade/VoiceSessionRestoreSync';
import ScreenShareCodecNegotiation from '@app/features/voice/engine/ScreenShareCodecNegotiation';
import ScreenSharePublicationMigration from '@app/features/voice/engine/ScreenSharePublicationMigration';
import {Store, useStoreVersion} from '@app/features/voice/engine/Store';
import {shouldMoveToAfkOnTick} from '@app/features/voice/engine/VoiceAfkTracking';
import {
checkChannelLimit,
checkMultipleConnections,
sendVoiceStateConnect,
sendVoiceStateDisconnect,
syncVoiceStateToServer,
} from '@app/features/voice/engine/VoiceChannelConnector';
import type {VoiceConnectionFailureReason} from '@app/features/voice/engine/VoiceConnectionStateMachine';
import VoiceDevicePermissionState from '@app/features/voice/engine/VoiceDevicePermissionState';
import {getEffectiveAudioState} from '@app/features/voice/engine/VoiceEffectiveAudioState';
import type {NormalizedVoiceState} from '@app/features/voice/engine/VoiceGatewayStateMachine';
import {
noteLocalVoiceActivity,
noteLocalVoiceActivityFromSnapshot,
} from '@app/features/voice/engine/VoiceIdleActivityBridge';
import {
createVoiceLocalAudioReconcileCoalescerSnapshot,
selectVoiceLocalAudioEffectiveSelfMute,
selectVoiceLocalAudioReconcileCoalescedCount,
selectVoiceLocalAudioReconcileRunReasons,
shouldStartVoiceLocalAudioReconcileRun,
shouldWarnAboutVoiceLocalAudioReconcileFollowUpRuns,
transitionVoiceLocalAudioReconcileCoalescerSnapshot,
type VoiceLocalAudioReconcileCoalescerSnapshot,
} from '@app/features/voice/engine/VoiceLocalAudioReconcilePolicy';
import {normalizeVoiceMediaGraphViewerStreamKeys} from '@app/features/voice/engine/VoiceMediaGraph';
import {startVoiceMediaGraphTimerScheduler} from '@app/features/voice/engine/VoiceMediaGraphTimerScheduler';
import {bindOutputDeviceSync} from '@app/features/voice/engine/VoiceOutputDeviceSync';
import type {LivekitParticipantSnapshot} from '@app/features/voice/engine/VoiceParticipantStateMachine';
import {bindRoomEvents, type RoomEventDependencies} from '@app/features/voice/engine/VoiceRoomEventBinder';
import type {VoiceStateSyncPartial, VoiceStateSyncPayload} from '@app/features/voice/engine/VoiceStateSyncTypes';
import {
deferStopWatchingStreamKey,
normalizeStreamGuildId,
replaceWatchedStreamKeys,
stopWatchingStreamKey,
} from '@app/features/voice/engine/VoiceStreamWatchState';
import {type VoiceConnectionQuality, VoiceTrackSource} from '@app/features/voice/engine/VoiceTrackSource';
import {bindVoiceEngineV2AppAudioPreferencesSync} from '@app/features/voice/engine/v2/VoiceEngineV2AppAudioPreferencesSyncBinding';
import {getLocalDecodableVideoCodecs} from '@app/features/voice/engine/v2/VoiceEngineV2AppCodecCapability';
import {
computeVoiceEngineV2WatchedStreamGossip,
ingestVoiceEngineV2CodecGossip,
} from '@app/features/voice/engine/v2/VoiceEngineV2AppCodecGossipAdapter';
import voiceEngineV2AppConnectionHostAdapter, {
type VoiceServerUpdateData,
} from '@app/features/voice/engine/v2/VoiceEngineV2AppConnectionHostAdapter';
import {
createVoiceEngineV2AppControllerHost,
type VoiceEngineV2AppControllerHost,
} from '@app/features/voice/engine/v2/VoiceEngineV2AppControllerHost';
import voiceEngineV2AppDebugEventSinkHostAdapter from '@app/features/voice/engine/v2/VoiceEngineV2AppDebugEventSinkHostAdapter';
import {createVoiceEngineV2AppEventLogSpillLoggerSink} from '@app/features/voice/engine/v2/VoiceEngineV2AppEventLogSpillLoggerSink';
import {createVoiceEngineV2AppIngestionPort} from '@app/features/voice/engine/v2/VoiceEngineV2AppHostPorts';
import type {VoiceEngineV2AppLifecycleDisposable} from '@app/features/voice/engine/v2/VoiceEngineV2AppLifecycleAdapter';
import voiceEngineV2AppMediaExecutionAdapter from '@app/features/voice/engine/v2/VoiceEngineV2AppMediaExecutionAdapter';
import voiceEngineV2AppMediaStateAdapter from '@app/features/voice/engine/v2/VoiceEngineV2AppMediaStateAdapter';
import {
createVoiceEngineV2AppParticipantAdapter,
type VoiceEngineV2AppParticipantAdapter,
} from '@app/features/voice/engine/v2/VoiceEngineV2AppParticipantAdapter';
import VoiceEngineV2AppPermissionAdapter from '@app/features/voice/engine/v2/VoiceEngineV2AppPermissionAdapter';
import {createVoiceEngineV2AppProductionHostPorts} from '@app/features/voice/engine/v2/VoiceEngineV2AppProductionHostPorts';
import {
createVoiceEngineV2AppProjectionStore,
type VoiceEngineV2AppProjectionStore,
} from '@app/features/voice/engine/v2/VoiceEngineV2AppProjectionStore';
import VoiceEngineV2AppRemoteSpeakingAdapter from '@app/features/voice/engine/v2/VoiceEngineV2AppRemoteSpeakingAdapter';
import type {VoiceEngineV2AppScreenShareControllerGateway} from '@app/features/voice/engine/v2/VoiceEngineV2AppScreenShareControllerRouting';
import voiceEngineV2AppScreenShareExecutionAdapter, {
type DeviceScreenShareCaptureOptions,
type ScreenShareReconnectSnapshot,
} from '@app/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter';
import {selectVoiceEngineV2AppIntentSelfMuteForVoiceStatePayload} from '@app/features/voice/engine/v2/VoiceEngineV2AppSelectors';
import {VoiceEngineV2AppSourceLifecycleBridge} from '@app/features/voice/engine/v2/VoiceEngineV2AppSourceLifecycleBridge';
import {VoiceEngineV2AppStatsHostAdapter} from '@app/features/voice/engine/v2/VoiceEngineV2AppStatsHostAdapter';
import VoiceEngineV2AppSubscriptionAdapter from '@app/features/voice/engine/v2/VoiceEngineV2AppSubscriptionAdapter';
import voiceEngineV2AppVoiceStateAdapter from '@app/features/voice/engine/v2/VoiceEngineV2AppVoiceStateAdapter';
import type {DisplayScreenShareCaptureContext} from '@app/features/voice/engine/voice_screen_share_manager/shared';
import type {VoiceStateAckPayload} from '@app/features/voice/events/VoiceStateAck';
import CallMediaPrefs from '@app/features/voice/state/CallMediaPrefs';
import {type ChannelE2EEStatus, computeChannelE2EEStatus} from '@app/features/voice/state/ChannelE2EEStatus';
import LocalVoiceState from '@app/features/voice/state/LocalVoiceState';
import VoiceCallLayout from '@app/features/voice/state/VoiceCallLayout';
import VoiceRegionTeleport from '@app/features/voice/state/VoiceRegionTeleport';
import VoiceSessionRestore, {type VoiceSessionRestoreSnapshot} from '@app/features/voice/state/VoiceSessionRestore';
import VoiceSettings from '@app/features/voice/state/VoiceSettings';
import type {CodecPreference} from '@app/features/voice/utils/CodecCapabilityDetector';
import {setNativeAudioCaptureBridgeLifecycleBridge} from '@app/features/voice/utils/NativeAudioCaptureBridge';
import {areOrderedStringArraysEqual} from '@app/features/voice/utils/StringArrayUtils';
import {buildVoiceParticipantIdentity} from '@app/features/voice/utils/VoiceParticipantIdentity';
import {
isVoiceServerMuteActive,
resolveVoiceStateSelfMute,
shouldPrepareMicrophoneForVoiceConnect,
} from '@app/features/voice/utils/VoicePermissionUtils';
import {getActiveVoiceProcessingMode} from '@app/features/voice/utils/VoiceProcessingProfile';
import type {NativeAudioStartOptions} from '@app/types/electron.d';
import {ME} from '@fluxer/constants/src/AppConstants';
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
import {VOICE_CHANNEL_CAMERA_USER_LIMIT} from '@fluxer/constants/src/LimitConstants';
import type {
VoiceEngineV2AudioMode,
VoiceEngineV2CameraOptions,
VoiceEngineV2Command,
VoiceEngineV2Controller,
VoiceEngineV2DisconnectReason,
VoiceEngineV2GatewayVoiceState,
VoiceEngineV2LatencyDataPoint,
VoiceEngineV2LocalStreamSource,
VoiceEngineV2MicrophoneOptions,
VoiceEngineV2Model,
VoiceEngineV2PerTrackStats,
VoiceEngineV2Snapshot,
VoiceEngineV2StatsSample,
VoiceEngineV2TransportInfo,
VoiceEngineV2VideoCodec,
VoiceEngineV2VoiceStats,
} from '@fluxer/voice_engine_v2';
import {
encodeVoiceEngineV2CodecGossip,
shouldApplyGatewayVoiceStateEcho,
VOICE_ENGINE_V2_CODEC_GOSSIP_TOPIC,
} from '@fluxer/voice_engine_v2';
import {type I18n, i18n as linguiI18n, type MessageDescriptor} from '@lingui/core';
import {
type ConnectionQuality as LiveKitConnectionQuality,
type Participant,
type Room,
RoomEvent,
type ScreenShareCaptureOptions,
Track,
type TrackPublishOptions,
} from 'livekit-client';
import {makeObservable, observable} from 'mobx';
const logger = new Logger('MediaEngineFacade');
function isCameraPermissionDeniedCommandFailure(error: unknown): boolean {
if (!(error instanceof Error)) return false;
const code = (error as Error & {code?: unknown}).code;
if (code === 'permissionDenied') return true;
return error.message.includes('permissionDenied');
}
function buildCameraCommandOptions(options?: {deviceId?: string; sendUpdate?: boolean}): VoiceEngineV2CameraOptions {
const commandOptions: VoiceEngineV2CameraOptions = {};
if (options?.deviceId) {
commandOptions.deviceId = options.deviceId;
}
if (options?.sendUpdate === false) {
commandOptions.sendUpdate = false;
}
return commandOptions;
}
function buildRoomEventDependencies(facade: {
resetStreamTracking: () => void;
participants: VoiceEngineV2AppParticipantAdapter;
sourceLifecycleBridge: VoiceEngineV2AppSourceLifecycleBridge | null;
isUserMovePending: () => boolean;
}): RoomEventDependencies {
const sourceLifecycleBridge = facade.sourceLifecycleBridge;
return {
connection: {
createGuardedHandler: (guardedAttemptId, handler) =>
voiceEngineV2AppConnectionHostAdapter.createGuardedHandler(guardedAttemptId, handler),
isDisconnecting: () => voiceEngineV2AppConnectionHostAdapter.disconnecting,
isUserMovePending: () => facade.isUserMovePending(),
markConnected: () => voiceEngineV2AppConnectionHostAdapter.markConnected(),
markDisconnected: (reason) => voiceEngineV2AppConnectionHostAdapter.markDisconnected(reason),
markReconnecting: () => voiceEngineV2AppConnectionHostAdapter.markReconnecting(),
markReconnected: () => voiceEngineV2AppConnectionHostAdapter.markReconnected(),
},
media: {
applyAllLocalAudioPreferences: (activeRoom) =>
voiceEngineV2AppMediaExecutionAdapter.applyAllLocalAudioPreferences(activeRoom),
ensureMicrophone: (activeRoom, activeChannelId) =>
voiceEngineV2AppMediaExecutionAdapter.ensureMicrophone(activeRoom, activeChannelId),
playEntranceSound: () => voiceEngineV2AppMediaExecutionAdapter.playEntranceSound(),
resetStreamTracking: () => facade.resetStreamTracking(),
},
mediaState: {
handleLocalTrackStateChange: (source, isPublished) =>
voiceEngineV2AppMediaStateAdapter.handleLocalTrackStateChange(source, isPublished),
resetLocalMediaState: (reason) => voiceEngineV2AppMediaStateAdapter.resetLocalMediaState(reason),
},
participants: {
clear: () => facade.participants.clear(),
hydrateFromRoom: (activeRoom) => facade.participants.hydrateFromRoom(activeRoom),
removeParticipant: (identity) => facade.participants.removeParticipant(identity),
updateActiveSpeakers: (speakers) => facade.participants.updateActiveSpeakers(speakers),
upsertParticipant: (participant) => facade.participants.upsertParticipant(participant),
},
permissions: {
applyDeafen: (activeRoom, deafened) => VoiceEngineV2AppPermissionAdapter.applyDeafen(activeRoom, deafened),
syncWithPermissionState: (activeGuildId, activeChannelId, activeRoom) =>
VoiceEngineV2AppPermissionAdapter.syncWithPermissionState(activeGuildId, activeChannelId, activeRoom),
},
remoteSpeaking: {
attachIfApplicable: (participant, publication, track) =>
VoiceEngineV2AppRemoteSpeakingAdapter.attachIfApplicable(participant, publication, track),
clear: () => VoiceEngineV2AppRemoteSpeakingAdapter.clear(),
detachByIdentity: (identity) => VoiceEngineV2AppRemoteSpeakingAdapter.detachByIdentity(identity),
detachIfTrackMatches: (participant, publication) =>
VoiceEngineV2AppRemoteSpeakingAdapter.detachIfTrackMatches(participant, publication),
hydrateFromRoom: (activeRoom) => VoiceEngineV2AppRemoteSpeakingAdapter.hydrateFromRoom(activeRoom),
},
screenShare: {
cleanupLingeringScreenShareTracks: (participant) =>
voiceEngineV2AppScreenShareExecutionAdapter.cleanupLingeringScreenShareTracks(participant),
handleLocalScreenShareTrackUnpublished: (activeRoom, didChangeLocalState, publication) =>
voiceEngineV2AppScreenShareExecutionAdapter.handleLocalScreenShareTrackUnpublished(
activeRoom,
didChangeLocalState,
publication,
),
isScreenShareCodecRepublishInFlight: () =>
voiceEngineV2AppScreenShareExecutionAdapter.isScreenShareCodecRepublishInFlight(),
renegotiateActiveScreenShareCodec: (activeRoom, selection) =>
voiceEngineV2AppScreenShareExecutionAdapter.renegotiateActiveScreenShareCodec(
activeRoom,
selection.codec,
selection.reason,
),
},
subscriptions: {
isScreenShareSubscribed: (participantIdentity) =>
VoiceEngineV2AppSubscriptionAdapter.isScreenShareSubscribed(participantIdentity),
reattachScreenShareAfterPublish: (participantIdentity, publication) =>
VoiceEngineV2AppSubscriptionAdapter.reattachScreenShareAfterPublish(participantIdentity, publication),
reconcileSubscriptions: () => VoiceEngineV2AppSubscriptionAdapter.reconcileSubscriptions(),
},
remoteTrackLifecycle: sourceLifecycleBridge
? {
bind: (track, options) => sourceLifecycleBridge.bindRemoteTrackLifecycle(track, options),
}
: undefined,
};
}
interface ConnectToVoiceChannelOptions {
skipChannelGate?: boolean;
deferNavigationUntilConnected?: boolean;
initialViewerStreamKeys?: Array<string>;
}
interface ConnectDirectlyOptions {
deferNavigationUntilConnected?: boolean;
initialViewerStreamKeys?: Array<string>;
}
type TerminalUnloadVoiceDisconnectReason = 'pagehide' | 'beforeunload' | 'unload';
type VoiceMuteReason = 'guild' | 'permission' | 'voice_push_to_talk' | 'self' | null;
export interface VoiceEngineConnectionContext {
guildId: string | null;
channelId: string | null;
connectionId: string | null;
connected: boolean;
connecting: boolean;
reconnecting: boolean;
}
class MediaEngineFacade extends Store {
private readonly voiceEngineV2Host: VoiceEngineV2AppControllerHost;
private readonly voiceEngineV2ProjectionStore: VoiceEngineV2AppProjectionStore;
private readonly voiceEngineV2Participants: VoiceEngineV2AppParticipantAdapter;
private readonly voiceEngineV2SourceLifecycleBridge: VoiceEngineV2AppSourceLifecycleBridge | null;
private statsHostAdapter: VoiceEngineV2AppStatsHostAdapter;
private afkIntervalId: NodeJS.Timeout | null = null;
private voiceSessionRestoreSync: VoiceSessionRestoreSyncHandle | null = null;
private outputDeviceSyncDisposer: (() => void) | null = null;
private audioPreferencesSyncDisposer: (() => void) | null = null;
private videoCodecDecodeCapResyncDisposer: (() => void) | null = null;
private videoCodecPublishOverrideSyncDisposer: (() => void) | null = null;
private localStreamCodecReconcileScheduled = false;
private localAudioReconcileCoalescer: VoiceLocalAudioReconcileCoalescerSnapshot =
createVoiceLocalAudioReconcileCoalescerSnapshot();
private applyingGatewayVoiceStateEcho = false;
private previousWatchedStreamCodecGossip = new Map<
string,
{identity: string; source: VoiceEngineV2LocalStreamSource}
>();
private videoCodecGossipReceiverDisposer: (() => void) | null = null;
private i18n: I18n | null = linguiI18n;
private pendingServerDisconnectTimeout: NodeJS.Timeout | null = null;
private facadeSnapshot: MediaEngineFacadeSnapshot = createMediaEngineFacadeSnapshot();
private disconnectPromise: Promise<void> | null = null;
private terminalUnloadVoiceDisconnectSent = false;
private voiceEngineV2EstablishedConnectionKey: string | null = null;
private lastVoiceConnectFailed = false;
constructor() {
super();
this.statsHostAdapter = new VoiceEngineV2AppStatsHostAdapter();
const voiceEngineV2Ingestion = createVoiceEngineV2AppIngestionPort();
this.voiceEngineV2Host = createVoiceEngineV2AppControllerHost({
eventLogSpillSink: createVoiceEngineV2AppEventLogSpillLoggerSink({logger}),
ports: createVoiceEngineV2AppProductionHostPorts({
gateway: {
async writeVoiceState(options): Promise<void> {
const connectionId = voiceEngineV2AppConnectionHostAdapter.connectionId;
if (!connectionId) return;
if (!options.channelId) {
sendVoiceStateDisconnect(options.guildId, connectionId);
return;
}
syncVoiceStateToServer(options.guildId, options.channelId, connectionId, {
self_mute: options.selfMute,
self_deaf: options.selfDeaf,
self_video: options.selfVideo,
self_stream: options.selfStream,
});
},
async clearVoiceState(guildId): Promise<void> {
const connectionId = voiceEngineV2AppConnectionHostAdapter.connectionId;
if (!connectionId) return;
sendVoiceStateDisconnect(guildId, connectionId);
},
},
connection: {
startConnection: (guildId, channelId) =>
voiceEngineV2AppConnectionHostAdapter.startConnection(guildId, channelId),
disconnectFromVoiceChannel: (reason) =>
voiceEngineV2AppConnectionHostAdapter.disconnectFromVoiceChannel(reason),
},
media: voiceEngineV2AppMediaExecutionAdapter,
screenShare: voiceEngineV2AppScreenShareExecutionAdapter,
getRoom: () => this.room,
getActiveGuildId: () => voiceEngineV2AppConnectionHostAdapter.guildId,
getActiveChannelId: () => voiceEngineV2AppConnectionHostAdapter.channelId,
audioOutputStore: {
async setOutputDevice(deviceId): Promise<void> {
VoiceSettings.updateSettings({outputDeviceId: deviceId});
},
},
ingestion: voiceEngineV2Ingestion,
subscriptions: VoiceEngineV2AppSubscriptionAdapter,
stats: this.statsHostAdapter,
lifecycleDisposables: this.createVoiceEngineV2LifecycleDisposables(),
logger,
}),
});
this.voiceEngineV2ProjectionStore = createVoiceEngineV2AppProjectionStore(this.voiceEngineV2Host);
this.voiceEngineV2Participants = createVoiceEngineV2AppParticipantAdapter({
controller: this.voiceEngineV2Controller,
getModel: () => this.voiceEngineV2Model,
ingest: (event) => voiceEngineV2Ingestion.ingest(event),
getCurrentConnectionId: () => voiceEngineV2AppConnectionHostAdapter.connectionId,
});
this.voiceEngineV2SourceLifecycleBridge = this.createSourceLifecycleBridge();
voiceEngineV2AppScreenShareExecutionAdapter.setControllerGateway(this.createScreenShareControllerGateway());
voiceEngineV2AppScreenShareExecutionAdapter.setSourceLifecycleBridge(this.voiceEngineV2SourceLifecycleBridge);
voiceEngineV2AppMediaExecutionAdapter.setSourceLifecycleBridge(this.voiceEngineV2SourceLifecycleBridge);
setNativeAudioCaptureBridgeLifecycleBridge(this.voiceEngineV2SourceLifecycleBridge);
this.syncVoiceEngineV2AudioControlsFromAppState();
makeObservable<this, 'facadeSnapshot'>(this, {
facadeSnapshot: observable.ref,
});
this.initializeEngineStoreSync();
this.initializeVoiceEngineV2ConnectionLifecycleSync();
this.initializeLocalAudioStateSync();
this.initializeVoiceSessionRestoreSync();
this.initializeE2EEStatusSync();
this.initializeTerminalUnloadVoiceDisconnect();
Sound.setSelfDeafenedResolver(() => getEffectiveAudioState().effectiveDeaf);
setSoundOutputDeviceIdResolver(() => VoiceSettings.getOutputDeviceId());
this.prewarmVoiceEngineV2InBackground('startup');
(
window as typeof window & {
_mediaEngine?: MediaEngineFacade;
}
)._mediaEngine = this;
logger.debug('MediaEngineFacade initialized');
}
setI18n(instance: I18n): void {
this.i18n = instance;
}
private createSourceLifecycleBridge(): VoiceEngineV2AppSourceLifecycleBridge | null {
const api = getElectronAPI()?.nativeScreenCapture;
if (!api || typeof api.onLifecycleEvent !== 'function') return null;
const subscribe = api.onLifecycleEvent.bind(api);
const host = this.voiceEngineV2Host;
return new VoiceEngineV2AppSourceLifecycleBridge({
dispatch: (event) => {
host.dispatch(event);
},
subscribe,
});
}
private createVoiceEngineV2LifecycleDisposables(): ReadonlyArray<VoiceEngineV2AppLifecycleDisposable> {
return [
{
name: 'app-tracking-and-stores',
dispose: async () => {
this.stopTracking();
this.statsHostAdapter.reset();
VoiceEngineV2AppRemoteSpeakingAdapter.clear();
VoiceEngineV2AppSubscriptionAdapter.cleanup();
VoiceEngineV2AppPermissionAdapter.reset();
this.resetLocalMediaAndScreenShareTracking();
this.voiceEngineV2Participants.clear();
},
},
];
}
private transitionFacadeState(event: MediaEngineFacadeEvent): void {
this.update(() => {
this.facadeSnapshot = transitionMediaEngineFacadeSnapshot(this.facadeSnapshot, event);
});
}
private prewarmVoiceEngineV2InBackground(reason: string): void {
void this.voiceEngineV2Host
.runAndWait(
() => {
this.voiceEngineV2Controller.prewarm();
},
{description: `voice engine v2 prewarm (${reason})`},
)
.then(() => {
logger.info('Voice engine v2 prewarm completed', {reason});
})
.catch((error) => {
logger.warn('Voice engine v2 prewarm failed', {reason, error});
});
}
private initializeEngineStoreSync(): void {
const forwardChange = () => {
this.scheduleLocalStreamCodecReconcile();
this.emitChange();
};
this.voiceEngineV2ProjectionStore.subscribe(forwardChange);
voiceEngineV2AppConnectionHostAdapter.subscribe(forwardChange);
voiceEngineV2AppVoiceStateAdapter.subscribe(forwardChange);
this.statsHostAdapter.subscribe(forwardChange);
AdaptiveScreenShareEngine.subscribe(forwardChange);
voiceEngineV2AppMediaExecutionAdapter.subscribe(forwardChange);
voiceEngineV2AppScreenShareExecutionAdapter.subscribe(forwardChange);
VoiceEngineV2AppSubscriptionAdapter.subscribe(forwardChange);
VoiceEngineV2AppPermissionAdapter.subscribe(forwardChange);
ScreenSharePublicationMigration.subscribe(forwardChange);
}
private initializeVoiceEngineV2ConnectionLifecycleSync(): void {
assert.ok(this.voiceEngineV2Host != null, 'connection lifecycle sync requires the v2 host');
assert.equal(
this.voiceEngineV2EstablishedConnectionKey,
null,
'connection lifecycle sync must initialize before any establishment',
);
voiceEngineV2AppConnectionHostAdapter.subscribe(() => this.syncVoiceEngineV2ConnectionLifecycle());
voiceEngineV2AppConnectionHostAdapter.subscribe(() => this.syncVoiceConnectFailureToast());
this.syncVoiceEngineV2ConnectionLifecycle();
}
private syncVoiceConnectFailureToast(): void {
const failed = voiceEngineV2AppConnectionHostAdapter.connectFailed;
if (failed === this.lastVoiceConnectFailed) return;
this.lastVoiceConnectFailed = failed;
if (!failed) return;
if (!this.i18n) return;
ToastCommands.createToast({
type: 'error',
children: this.i18n._(VOICE_CONNECTION_FAILED_DESCRIPTOR),
});
}
private syncVoiceEngineV2ConnectionLifecycle(): void {
const state = voiceEngineV2AppConnectionHostAdapter.connectionState;
assert.equal(typeof state.connected, 'boolean', 'connection state must expose a boolean connected flag');
assert.equal(typeof state.reconnecting, 'boolean', 'connection state must expose a boolean reconnecting flag');
if (state.reconnecting) return;
const key = state.connected ? `${state.connectionId ?? ''}|${state.voiceServerEndpoint ?? ''}` : null;
const previousKey = this.voiceEngineV2EstablishedConnectionKey;
if (key === previousKey) return;
this.voiceEngineV2EstablishedConnectionKey = key;
if (key === null) {
const transition = this.voiceEngineV2Host.dispatch({
type: 'connection.remoteDisconnected',
reason: this.resolveVoiceEngineV2RemoteDisconnectReason(),
});
assert.notEqual(
transition.snapshot.connection.status,
'connected',
'v2 snapshot must not stay connected after external disconnect',
);
return;
}
if (previousKey !== null) {
this.voiceEngineV2Host.dispatch({type: 'connection.remoteDisconnected', reason: 'replaced'});
}
const transition = this.voiceEngineV2Host.dispatch({
type: 'connection.externallyEstablished',
options: {url: state.voiceServerEndpoint ?? '', token: ''},
});
if (transition.snapshot.lifecycle.tearingDown) {
this.voiceEngineV2EstablishedConnectionKey = null;
logger.info('Voice engine v2 external establishment observed during lifecycle teardown', {
connectionStatus: transition.snapshot.connection.status,
});
return;
}
assert.equal(
transition.snapshot.connection.status,
'connected',
'v2 snapshot must be connected after external establishment',
);
this.syncVoiceEngineV2AudioControlsFromAppState();
}
private resolveVoiceEngineV2RemoteDisconnectReason(): VoiceEngineV2DisconnectReason {
const reason = voiceEngineV2AppConnectionHostAdapter.localDisconnectReason;
assert.ok(reason === null || typeof reason === 'string', 'local disconnect reason must be a string or null');
switch (reason) {
case 'error':
case 'abort':
return 'network';
case 'server':
return 'server';
case 'channelMove':
case 'replaced':
return 'replaced';
case 'user':
case null:
return 'user';
}
}
private get pendingSessionRestore(): MediaEngineFacadePendingSessionRestore | null {
return this.facadeSnapshot.context.pendingSessionRestore;
}
private get pendingUserMove(): {guildId: string | null; channelId: string} | null {
return this.facadeSnapshot.context.pendingUserMove;
}
private get pendingServerDisconnectConnectionId(): string | null {
return this.facadeSnapshot.context.pendingServerDisconnectConnectionId;
}
private get pendingScreenShareReconnect(): ScreenShareReconnectSnapshot | null {
return this.facadeSnapshot.context.pendingScreenShareReconnect as ScreenShareReconnectSnapshot | null;
}
get voiceEngineV2Controller(): VoiceEngineV2Controller {
return this.voiceEngineV2Host.controller;
}
get voiceEngineV2Snapshot(): VoiceEngineV2Snapshot {
return this.voiceEngineV2ProjectionStore.snapshot;
}
get voiceEngineV2Model(): VoiceEngineV2Model {
return this.voiceEngineV2ProjectionStore.model;
}
get room(): Room | null {
return voiceEngineV2AppConnectionHostAdapter.room;
}
get guildId(): string | null {
return voiceEngineV2AppConnectionHostAdapter.guildId;
}
get channelId(): string | null {
return voiceEngineV2AppConnectionHostAdapter.channelId;
}
get connectionId(): string | null {
return voiceEngineV2AppConnectionHostAdapter.connectionId;
}
get connected(): boolean {
return voiceEngineV2AppConnectionHostAdapter.connected;
}
get connecting(): boolean {
return voiceEngineV2AppConnectionHostAdapter.connecting;
}
get reconnecting(): boolean {
return voiceEngineV2AppConnectionHostAdapter.reconnecting;
}
get connectFailed(): boolean {
return voiceEngineV2AppConnectionHostAdapter.connectFailed;
}
get connectFailureReason(): VoiceConnectionFailureReason {
return voiceEngineV2AppConnectionHostAdapter.connectFailureReason;
}
get connectFailedTarget(): {guildId: string | null; channelId: string} | null {
return voiceEngineV2AppConnectionHostAdapter.connectFailedTarget;
}
get voiceServerEndpoint(): string | null {
return voiceEngineV2AppConnectionHostAdapter.voiceServerEndpoint;
}
get connectionContext(): VoiceEngineConnectionContext {
return {
guildId: this.guildId,
channelId: this.channelId,
connectionId: this.connectionId,
connected: this.connected,
connecting: this.connecting,
reconnecting: this.reconnecting,
};
}
get participants(): Readonly<Record<string, LivekitParticipantSnapshot>> {
return this.voiceEngineV2Participants.participants;
}
get localConnectionQuality(): VoiceConnectionQuality | LiveKitConnectionQuality {
return this.room?.localParticipant?.connectionQuality ?? 'unknown';
}
get connectionVoiceStates(): Readonly<Record<string, NormalizedVoiceState>> {
return voiceEngineV2AppVoiceStateAdapter.getConnectionVoiceStates();
}
get currentLatency(): number | null {
return this.statsHostAdapter.currentLatency;
}
get averageLatency(): number | null {
return this.statsHostAdapter.averageLatency;
}
get latencyHistory(): Array<VoiceEngineV2LatencyDataPoint> {
return this.statsHostAdapter.latencyHistory;
}
get voiceStats(): VoiceEngineV2VoiceStats {
return this.statsHostAdapter.voiceStats;
}
get perTrackStats(): Array<VoiceEngineV2PerTrackStats> {
return this.statsHostAdapter.perTrackStats;
}
get statsTimeSeries(): Array<VoiceEngineV2StatsSample> {
return this.statsHostAdapter.statsTimeSeries;
}
get publisherTransport(): VoiceEngineV2TransportInfo | null {
return this.statsHostAdapter.publisherTransport;
}
get subscriberTransport(): VoiceEngineV2TransportInfo | null {
return this.statsHostAdapter.subscriberTransport;
}
get reconnectionCount(): number {
return this.statsHostAdapter.reconnectionCount;
}
get estimatedLatency(): number | null {
return this.statsHostAdapter.estimatedLatency;
}
get displayLatency(): number | null {
const measured = this.currentLatency;
return measured !== null ? measured : this.estimatedLatency;
}
refreshMicrophoneFromSettings(): void {
void this.refreshMicrophoneFromCurrentEngine().catch((error) => {
logger.warn('Failed to refresh microphone from settings', {error});
});
}
private async refreshMicrophoneFromCurrentEngine(): Promise<void> {
await voiceEngineV2AppMediaExecutionAdapter.refreshMicrophone(this.room, {forceRepublish: true});
}
refreshCameraBackgroundFromSettings(): void {
void this.refreshCameraBackgroundFromCurrentEngine().catch((error) => {
logger.warn('Failed to refresh camera background from settings', {error});
});
}
private async refreshCameraBackgroundFromCurrentEngine(): Promise<void> {
await voiceEngineV2AppMediaExecutionAdapter.refreshCameraBackground();
}
refreshCameraCaptureFromSettings(): void {
void this.refreshCameraCaptureFromCurrentEngine().catch((error) => {
logger.warn('Failed to refresh camera capture from settings', {error});
});
}
private async refreshCameraCaptureFromCurrentEngine(): Promise<void> {
await voiceEngineV2AppMediaExecutionAdapter.refreshCameraCapture();
}
async refreshActiveScreenShareCodecNegotiation(): Promise<void> {
const room = this.room;
const selection = await ScreenShareCodecNegotiation.publishLocalCapabilities(room, 'manual', {});
const codec =
selection?.codec ??
ScreenShareCodecNegotiation.selectScreenShareCodec(VoiceSettings.getPreferredScreenShareCodec());
await voiceEngineV2AppScreenShareExecutionAdapter.renegotiateActiveScreenShareCodec(room, codec, 'manual', {
force: true,
});
}
private reconcileLocalAudioStateInBackground(reason: string): void {
const previous = this.localAudioReconcileCoalescer;
const next = transitionVoiceLocalAudioReconcileCoalescerSnapshot(previous, {type: 'run.requested', reason});
this.localAudioReconcileCoalescer = next;
if (!shouldStartVoiceLocalAudioReconcileRun(previous, next)) return;
void this.drainLocalAudioReconcile();
}
private async drainLocalAudioReconcile(): Promise<void> {
try {
for (;;) {
const reasons = selectVoiceLocalAudioReconcileRunReasons(this.localAudioReconcileCoalescer);
try {
await this.reconcileLocalAudioState(reasons.join(' + '));
} catch (error) {
logger.warn('Local audio reconciliation failed', {reasons, error});
}
const previous = this.localAudioReconcileCoalescer;
const next = transitionVoiceLocalAudioReconcileCoalescerSnapshot(previous, {type: 'run.settled'});
this.localAudioReconcileCoalescer = next;
if (!shouldStartVoiceLocalAudioReconcileRun(previous, next)) return;
if (
shouldWarnAboutVoiceLocalAudioReconcileFollowUpRuns(next) &&
!shouldWarnAboutVoiceLocalAudioReconcileFollowUpRuns(previous)
) {
logger.warn('Local audio reconciliation keeps re-queueing itself', {
reasons: selectVoiceLocalAudioReconcileRunReasons(next),
coalescedCount: selectVoiceLocalAudioReconcileCoalescedCount(previous),
});
}
}
} catch (error) {
logger.error('Local audio reconciliation loop aborted; resetting the coalescer', {error});
this.localAudioReconcileCoalescer = createVoiceLocalAudioReconcileCoalescerSnapshot();
}
}
private async restoreLocalMedia(args: {
reason: string;
room: Room | null;
restoreCamera: boolean;
restoreStream: boolean;
restoreStreamFromSnapshot: (() => Promise<boolean>) | null;
streamSnapshotFallback: boolean;
streamPlaySound: boolean;
settleStreamStateWhenNotRestored: boolean;
toastWhenStreamNotRestored: boolean;
}): Promise<void> {
assert.equal(typeof args.restoreCamera, 'boolean', 'restoreLocalMedia requires a camera flag');
assert.equal(typeof args.restoreStream, 'boolean', 'restoreLocalMedia requires a stream flag');
if (args.restoreCamera) {
try {
await this.setCameraEnabled(true, {
deviceId: VoiceSettings.getVideoDeviceId(),
sendUpdate: false,
});
} catch (error) {
logger.warn('Failed to restore camera during local media restore', {reason: args.reason, error});
}
}
if (!args.restoreStream) return;
let restored = false;
if (args.restoreStreamFromSnapshot) {
restored = await args.restoreStreamFromSnapshot();
if (!restored && !args.streamSnapshotFallback) {
voiceEngineV2AppMediaStateAdapter.applyScreenShareState(false, {sendUpdate: false});
return;
}
}
let streamRestoreFailed = false;
if (!restored) {
try {
await voiceEngineV2AppScreenShareExecutionAdapter.setScreenShareEnabled(args.room, true, {
sendUpdate: false,
...(args.streamPlaySound ? {} : {playSound: false}),
});
restored = LocalVoiceState.getSelfStream();
} catch (error) {
streamRestoreFailed = true;
logger.warn('Failed to restore screen share during local media restore', {reason: args.reason, error});
}
}
if (restored) return;
if (streamRestoreFailed || args.settleStreamStateWhenNotRestored) {
voiceEngineV2AppMediaStateAdapter.applyScreenShareState(false, {sendUpdate: false});
}
if (args.toastWhenStreamNotRestored && this.i18n) {
ToastCommands.createToast({
type: 'info',
children: this.i18n._(RECONNECT_SUCCEEDED_PICK_A_SCREEN_AGAIN_IF_YOU_DESCRIPTOR),
});
}
}
private initializeLocalAudioStateSync(): void {
let previousState = {
selfMute: LocalVoiceState.getSelfMute(),
selfDeaf: LocalVoiceState.getSelfDeaf(),
};
LocalVoiceState.subscribe(() => {
const currentState = {
selfMute: LocalVoiceState.getSelfMute(),
selfDeaf: LocalVoiceState.getSelfDeaf(),
};
if (currentState.selfMute === previousState.selfMute && currentState.selfDeaf === previousState.selfDeaf) {
return;
}
logger.debug('Local audio state changed, reconciling active voice session', {
previousState,
currentState,
});
const unmutedLocally = previousState.selfMute && !currentState.selfMute && !this.applyingGatewayVoiceStateEcho;
previousState = currentState;
if (unmutedLocally) {
voiceEngineV2AppMediaExecutionAdapter.noteUserMuteIntentChanged();
}
this.syncVoiceEngineV2AudioControlsFromAppState();
this.reconcileLocalAudioStateInBackground('local audio state change');
});
}
private initializeE2EEStatusSync(): void {
let previousRoom: Room | null = null;
let previousStatus: ChannelE2EEStatus | null = null;
const syncE2EEStatus = () => {
const room = voiceEngineV2AppConnectionHostAdapter.room;
const channelId = voiceEngineV2AppConnectionHostAdapter.channelId;
const status =
voiceEngineV2AppConnectionHostAdapter.connected && channelId
? computeChannelE2EEStatus(voiceEngineV2AppConnectionHostAdapter.guildId, channelId)
: null;
if (room === previousRoom && status === previousStatus) {
return;
}
previousRoom = room;
previousStatus = status;
if (!room) return;
if (status === 'encrypted') {
void room.setE2EEEnabled(true).catch((error) => {
logger.warn('Failed to enable E2EE on room', error);
});
} else if (status === 'broken') {
void room.setE2EEEnabled(false).catch((error) => {
logger.warn('Failed to disable E2EE on room after capability mix detected', error);
});
}
};
voiceEngineV2AppConnectionHostAdapter.subscribe(syncE2EEStatus);
voiceEngineV2AppVoiceStateAdapter.subscribe(syncE2EEStatus);
}
private initializeVoiceSessionRestoreSync(): void {
this.voiceSessionRestoreSync?.dispose();
this.voiceSessionRestoreSync = createVoiceSessionRestoreSync();
}
private initializeTerminalUnloadVoiceDisconnect(): void {
const handlePageHide = (event: PageTransitionEvent) => {
if (event.persisted) return;
this.disconnectVoiceForTerminalUnload('pagehide');
};
const handleBeforeUnload = () => this.disconnectVoiceForTerminalUnload('beforeunload');
const handleUnload = () => this.disconnectVoiceForTerminalUnload('unload');
window.addEventListener('pagehide', handlePageHide, {capture: true});
window.addEventListener('beforeunload', handleBeforeUnload, {capture: true});
window.addEventListener('unload', handleUnload, {capture: true});
}
private disconnectVoiceTransportForTerminalUnload(): void {
voiceEngineV2AppConnectionHostAdapter.disconnectTransportsForTerminalUnload();
}
private discardVoiceConnection(connectionId: string, options: {clearLocalState?: boolean} = {}): void {
voiceEngineV2AppVoiceStateAdapter.removeVoiceStateConnection(connectionId);
this.voiceEngineV2Participants.discardConnection(connectionId);
if (options.clearLocalState) {
LocalVoiceState.clearConnectionState(connectionId);
}
}
private handleCurrentLocalVoiceStateRemoval(guildId: string | null, voiceState: VoiceState): boolean {
const connectionId = voiceState.connection_id;
if (!connectionId) return false;
const removalInput = {
voiceStateConnectionId: connectionId,
voiceStateChannelId: voiceState.channel_id,
currentConnectionId: voiceEngineV2AppConnectionHostAdapter.connectionId,
currentChannelId: voiceEngineV2AppConnectionHostAdapter.channelId,
connected: voiceEngineV2AppConnectionHostAdapter.connected,
connecting: voiceEngineV2AppConnectionHostAdapter.connecting,
};
if (shouldImmediatelyDisconnectMediaEngineForServerVoiceStateRemoval(removalInput)) {
logger.info('Server removed current voice connection, tearing down local voice transport', {
guildId,
connectionId,
});
this.cancelPendingServerDisconnect();
this.applyEngineGatewayEcho(null);
void this.disconnectFromVoiceChannel('server');
return true;
}
if (!shouldCancelMediaEngineReconnectForServerVoiceStateRemoval(removalInput)) return false;
logger.info('Server confirmed current voice connection removal, cancelling auto reconnect', {
guildId,
connectionId,
});
this.cancelPendingServerDisconnect();
this.applyEngineGatewayEcho(null);
this.clearViewerStreamKeys();
VoiceSessionRestore.clearSnapshot();
this.discardVoiceConnection(connectionId, {clearLocalState: true});
voiceEngineV2AppConnectionHostAdapter.markDisconnected('server');
CallMediaPrefs.clearForCall(connectionId);
return true;
}
disconnectVoiceForTerminalUnload(reason: TerminalUnloadVoiceDisconnectReason): void {
if (this.terminalUnloadVoiceDisconnectSent) return;
const {guildId, channelId, connectionId, connected, connecting, room} =
voiceEngineV2AppConnectionHostAdapter.connectionState;
const hasVoiceTransport = voiceEngineV2AppConnectionHostAdapter.hasTerminalUnloadTransports();
if (!connected && !connecting && !channelId && !connectionId && !room && !hasVoiceTransport) return;
this.terminalUnloadVoiceDisconnectSent = true;
saveCurrentVoiceSessionRestoreSnapshot();
logger.info('Terminal page unload voice disconnect', {
reason,
guildId,
channelId,
connectionId,
connected,
connecting,
hasRoom: room != null,
hasVoiceTransport,
});
voiceEngineV2AppScreenShareExecutionAdapter.stopNativeScreenShareForTerminalUnload();
const hasSpecificConnection = connectionId != null;
GatewayConnection.sendTerminalVoiceDisconnect(
{
guild_id: hasSpecificConnection ? guildId : null,
channel_id: null,
self_mute: true,
self_deaf: true,
self_video: false,
self_stream: false,
viewer_stream_keys: [],
connection_id: connectionId ?? null,
},
`Terminal voice disconnect: ${reason}`,
);
this.disconnectVoiceTransportForTerminalUnload();
}
private resolveVoiceConnectChannel(guildId: string | null, channelId: string): Channel | null {
const channel = Channels.getChannel(channelId) ?? null;
if (!channel) return null;
if (channel.type === ChannelTypes.GUILD_VOICE) {
if (guildId && channel.guildId !== guildId) return null;
return channel;
}
if (guildId) return null;
if (channel.type === ChannelTypes.DM || channel.type === ChannelTypes.GROUP_DM) {
return channel;
}
return null;
}
private showVoiceErrorModal(message: MessageDescriptor, dataFlx: string, values?: Record<string, unknown>): void {
const i18n = this.i18n;
if (!i18n) return;
showGenericErrorModal({
title: () => i18n._(SOMETHING_WENT_WRONG_DESCRIPTOR),
message: () => i18n._(message, values),
dataFlx,
});
}
private showVoiceChannelUnavailableErrorModal(): void {
if (!this.i18n) return;
this.showVoiceErrorModal(
VOICE_CHANNEL_NO_LONGER_AVAILABLE_DESCRIPTOR,
'voice.media-engine-facade.channel-unavailable-error-modal',
);
}
private clearUnavailableVoiceTarget(
channelId: string,
reason: 'preflight' | 'gateway-error' | 'channel-delete',
options: {showToast?: boolean} = {},
): void {
logger.info('Clearing unavailable voice target', {channelId, reason});
this.transitionFacadeState({type: 'unavailableTarget.clear', channelId});
VoiceSessionRestore.clearSnapshotForChannel(channelId);
voiceEngineV2AppConnectionHostAdapter.forgetReconnectChannel(channelId);
if (options.showToast) {
this.showVoiceChannelUnavailableErrorModal();
}
}
handleChannelDelete(channelId: string): void {
this.clearUnavailableVoiceTarget(channelId, 'channel-delete');
if (voiceEngineV2AppConnectionHostAdapter.channelId === channelId) {
void this.disconnectFromVoiceChannel('server');
}
}
async retryFailedVoiceConnection(): Promise<void> {
const target = voiceEngineV2AppConnectionHostAdapter.connectFailedTarget;
if (!target) {
logger.warn('Retry requested with no failed voice connection target');
return;
}
this.lastVoiceConnectFailed = false;
voiceEngineV2AppConnectionHostAdapter.resetConnectionState();
await this.connectToVoiceChannel(target.guildId, target.channelId, {skipChannelGate: true});
}
dismissFailedVoiceConnection(): void {
if (!voiceEngineV2AppConnectionHostAdapter.connectFailed) return;
this.lastVoiceConnectFailed = false;
voiceEngineV2AppConnectionHostAdapter.resetConnectionState();
}
async connectToVoiceChannel(
guildId: string | null,
channelId: string,
options: ConnectToVoiceChannelOptions = {},
): Promise<void> {
this.terminalUnloadVoiceDisconnectSent = false;
VoiceCallLayout.reset();
const voiceChannel = this.resolveVoiceConnectChannel(guildId, channelId);
const resolvedGuildId = voiceChannel ? (guildId ?? voiceChannel.guildId ?? null) : guildId;
const currentUserId = Authentication.currentUserId;
const isTimedOut =
voiceChannel && resolvedGuildId && currentUserId
? (GuildMembers.getMember(resolvedGuildId, currentUserId)?.isTimedOut() ?? false)
: false;
const currentUser = Users.getCurrentUser();
const isUnclaimed = !(currentUser?.isClaimed() ?? false);
const guild = resolvedGuildId ? Guilds.getGuild(resolvedGuildId) : null;
const isOwner = guild?.isOwner(currentUserId) ?? false;
const channel = voiceChannel ?? Channels.getChannel(channelId);
const blockedByMatureGate =
Boolean(voiceChannel) &&
!options.skipChannelGate &&
GuildMatureContentAgree.shouldShowGate({
channelId,
guildId: resolvedGuildId,
});
const preflightDecision = selectMediaEngineConnectPreflightDecision({
targetAvailable: Boolean(voiceChannel),
isTimedOut: Boolean(isTimedOut),
isUnclaimed,
isGuildOwner: resolvedGuildId ? isOwner : true,
isDirectMessage: channel?.type === ChannelTypes.DM,
hasGatewaySocket: Boolean(GatewayConnection.socket),
blockedByMatureGate,
channelLimitAllowed: true,
});
if (preflightDecision.type === 'cleanup-unavailable-target') {
this.clearUnavailableVoiceTarget(channelId, 'preflight', {showToast: preflightDecision.showToast});
return;
}
if (preflightDecision.type === 'toast') {
if (!this.i18n) {
throw new Error('MediaEngineFacade: i18n not initialized');
}
const descriptor =
preflightDecision.reason === 'timed-out'
? YOU_CAN_T_JOIN_WHILE_YOU_RE_ON_DESCRIPTOR
: preflightDecision.reason === 'claim-account-direct'
? CLAIM_YOUR_ACCOUNT_TO_START_OR_JOIN_1_DESCRIPTOR
: CLAIM_YOUR_ACCOUNT_TO_JOIN_VOICE_CHANNELS_YOU_DESCRIPTOR;
this.showVoiceErrorModal(
descriptor,
`voice.media-engine-facade.preflight-${preflightDecision.reason}-error-modal`,
);
return;
}
if (preflightDecision.type === 'abort') {
if (preflightDecision.reason === 'missing-gateway-socket') {
logger.warn('No socket');
}
return;
}
if (preflightDecision.type === 'navigate-channel-gate') {
NavigationCommands.selectChannel(resolvedGuildId ?? undefined, channelId);
return;
}
const channelLimitAllowed = checkChannelLimit(resolvedGuildId, channelId);
const channelLimitDecision = selectMediaEngineConnectPreflightDecision({
targetAvailable: true,
isTimedOut: false,
isUnclaimed: false,
isGuildOwner: true,
isDirectMessage: false,
hasGatewaySocket: true,
blockedByMatureGate: false,
channelLimitAllowed,
});
if (channelLimitDecision.type === 'abort') {
return;
}
const shouldProceed = checkMultipleConnections(
resolvedGuildId,
channelId,
async () =>
this.connectDirectly(resolvedGuildId, channelId, {
deferNavigationUntilConnected: options.deferNavigationUntilConnected,
initialViewerStreamKeys: options.initialViewerStreamKeys,
}),
() =>
this.connectDirectly(resolvedGuildId, channelId, {
deferNavigationUntilConnected: options.deferNavigationUntilConnected,
initialViewerStreamKeys: options.initialViewerStreamKeys,
}),
() => voiceEngineV2AppConnectionHostAdapter.clearInFlightConnect(),
);
if (!shouldProceed) return;
await this.connectDirectly(resolvedGuildId, channelId, {
deferNavigationUntilConnected: options.deferNavigationUntilConnected,
initialViewerStreamKeys: options.initialViewerStreamKeys,
});
}
private async connectDirectly(
guildId: string | null,
channelId: string,
options: ConnectDirectlyOptions = {},
): Promise<void> {
VoiceCallLayout.reset();
if (this.disconnectPromise) {
logger.debug('Waiting for voice teardown before starting a new connection', {guildId, channelId});
await this.disconnectPromise;
}
const initialDecision = selectMediaEngineConnectRequestDecision(this.facadeSnapshot, {
guildId,
channelId,
connected: voiceEngineV2AppConnectionHostAdapter.connected,
connecting: voiceEngineV2AppConnectionHostAdapter.connecting,
currentGuildId: voiceEngineV2AppConnectionHostAdapter.guildId,
currentChannelId: voiceEngineV2AppConnectionHostAdapter.channelId,
});
if (initialDecision.type === 'noop') return;
if (
shouldPrepareMicrophoneForVoiceConnect({
guildId,
channelId,
selfMute: LocalVoiceState.getSelfMute(),
selfDeaf: LocalVoiceState.getSelfDeaf(),
hasUserSetMute: LocalVoiceState.getHasUserSetMute(),
mutedByPermission: LocalVoiceState.getMutedByPermission(),
})
) {
void voiceEngineV2AppMediaExecutionAdapter.prepareMicrophonePermissionForConnect().catch((error) => {
logger.warn('Microphone permission warm-up before voice connect failed; joining listen-only', {error});
});
}
const connectDecision = selectMediaEngineConnectRequestDecision(this.facadeSnapshot, {
guildId,
channelId,
connected: voiceEngineV2AppConnectionHostAdapter.connected,
connecting: voiceEngineV2AppConnectionHostAdapter.connecting,
currentGuildId: voiceEngineV2AppConnectionHostAdapter.guildId,
currentChannelId: voiceEngineV2AppConnectionHostAdapter.channelId,
});
if (connectDecision.type === 'noop') return;
if (connectDecision.type === 'move-user') {
this.transitionFacadeState({
type: 'userMove.requested',
guildId: connectDecision.guildId,
channelId: connectDecision.channelId,
});
await this.disconnectForChannelMove('user');
}
if (!voiceEngineV2AppConnectionHostAdapter.startConnection(guildId, channelId)) return;
const initialViewerStreamKeys = replaceWatchedStreamKeys(options.initialViewerStreamKeys ?? [], {
sync: false,
}).keys;
if (!options.deferNavigationUntilConnected) {
this.navigateToVoiceChannel(guildId, channelId);
}
sendVoiceStateConnect(guildId, channelId, initialViewerStreamKeys);
}
async restoreVoiceSession(
snapshot: VoiceSessionRestoreSnapshot,
options?: {
restoreVideo?: boolean;
restoreStream?: boolean;
},
): Promise<void> {
this.prepareVoiceSessionRestore(snapshot, options);
await this.connectToVoiceChannel(snapshot.guildId, snapshot.channelId, {deferNavigationUntilConnected: true});
if (
!voiceEngineV2AppConnectionHostAdapter.connecting &&
!(
voiceEngineV2AppConnectionHostAdapter.connected &&
voiceEngineV2AppConnectionHostAdapter.guildId === snapshot.guildId &&
voiceEngineV2AppConnectionHostAdapter.channelId === snapshot.channelId
)
) {
this.clearPreparedVoiceSessionRestore(snapshot);
}
}
prepareVoiceSessionRestore(
snapshot: VoiceSessionRestoreSnapshot,
options?: {
restoreVideo?: boolean;
restoreStream?: boolean;
},
): void {
this.transitionFacadeState({
type: 'sessionRestore.prepare',
guildId: snapshot.guildId,
channelId: snapshot.channelId,
restoreVideo: Boolean(options?.restoreVideo && snapshot.selfVideo),
restoreStream: Boolean(options?.restoreStream && snapshot.selfStream),
});
}
clearPreparedVoiceSessionRestore(snapshot?: Pick<VoiceSessionRestoreSnapshot, 'guildId' | 'channelId'>): void {
if (!snapshot) {
this.transitionFacadeState({type: 'sessionRestore.clear'});
return;
}
this.transitionFacadeState({
type: 'sessionRestore.clearTarget',
guildId: snapshot.guildId,
channelId: snapshot.channelId,
});
}
private resetLocalMediaAndScreenShareTracking(): void {
voiceEngineV2AppScreenShareExecutionAdapter.resetStreamTracking();
voiceEngineV2AppMediaExecutionAdapter.resetStreamTracking();
}
private async stopActiveScreenShareForTeardown(context: 'disconnect' | 'channel_move'): Promise<void> {
if (!LocalVoiceState.getSelfStream()) return;
try {
await voiceEngineV2AppScreenShareExecutionAdapter.setScreenShareEnabled(this.room, false, {
sendUpdate: false,
playSound: false,
});
} catch (error) {
logger.warn('Failed to stop screen share during teardown', {context, error});
}
}
async disconnectFromVoiceChannel(reason: 'user' | 'error' | 'server' = 'user'): Promise<void> {
if (this.disconnectPromise) {
logger.debug('Voice teardown already in progress', {reason});
return this.disconnectPromise;
}
const disconnectPromise = this.runDisconnectFromVoiceChannel(reason);
this.disconnectPromise = disconnectPromise;
try {
await disconnectPromise;
} finally {
if (this.disconnectPromise === disconnectPromise) {
this.disconnectPromise = null;
}
}
}
private async runDisconnectFromVoiceChannel(reason: 'user' | 'error' | 'server'): Promise<void> {
const {guildId, connectionId, connected, connecting, channelId} =
voiceEngineV2AppConnectionHostAdapter.connectionState;
if (!connected && !connecting && !channelId) {
return;
}
logger.debug('Voice teardown starting', {guildId, channelId, reason});
this.cancelPendingServerDisconnect();
this.transitionFacadeState({type: 'disconnect.cleanupStarted', reason});
await this.stopActiveScreenShareForTeardown('disconnect');
this.clearViewerStreamKeys();
this.stopTracking();
this.statsHostAdapter.reset();
if (reason === 'user' || reason === 'server') {
VoiceSessionRestore.clearSnapshot();
}
if (reason !== 'server' && connectionId) {
sendVoiceStateDisconnect(guildId, connectionId);
}
SoundCommands.playSound(SoundType.VoiceDisconnect);
VoiceCallLayout.reset();
voiceEngineV2AppMediaStateAdapter.resetLocalMediaState('disconnect');
voiceEngineV2AppMediaExecutionAdapter.resetMicrophoneFailureLatch();
this.resetLocalMediaAndScreenShareTracking();
this.voiceEngineV2Participants.clear();
if (connectionId) {
this.discardVoiceConnection(connectionId, {clearLocalState: true});
} else {
LocalVoiceState.clearConnectionState(connectionId);
}
voiceEngineV2AppConnectionHostAdapter.disconnectFromVoiceChannel(reason);
if (reason === 'user' || reason === 'server') {
VoiceSessionRestore.clearSnapshot();
}
if (connectionId) CallMediaPrefs.clearForCall(connectionId);
logger.info('Voice teardown complete', {channelId, reason});
this.transitionFacadeState({type: 'cleanup.complete'});
}
private async disconnectForChannelMove(reason: 'user' | 'server'): Promise<void> {
const {guildId, connectionId, connected, connecting, channelId} =
voiceEngineV2AppConnectionHostAdapter.connectionState;
if (!connected && !connecting && !channelId) return;
logger.debug('Voice teardown for channel move', {guildId, channelId});
this.cancelPendingServerDisconnect();
this.transitionFacadeState({type: 'channelMove.cleanupStarted', reason});
await this.stopActiveScreenShareForTeardown('channel_move');
this.clearViewerStreamKeys();
this.stopTracking();
this.statsHostAdapter.reset();
if (reason === 'user' && connectionId) {
sendVoiceStateDisconnect(guildId, connectionId);
}
VoiceCallLayout.reset();
voiceEngineV2AppMediaStateAdapter.resetLocalMediaState('disconnect');
voiceEngineV2AppMediaExecutionAdapter.resetMicrophoneFailureLatch();
this.resetLocalMediaAndScreenShareTracking();
this.voiceEngineV2Participants.clear();
if (reason === 'user' && connectionId) {
this.discardVoiceConnection(connectionId, {clearLocalState: true});
} else {
LocalVoiceState.clearConnectionState(connectionId);
}
voiceEngineV2AppConnectionHostAdapter.disconnectForChannelMove();
if (connectionId) CallMediaPrefs.clearForCall(connectionId);
SoundCommands.playSound(SoundType.UserMove);
this.transitionFacadeState({type: 'cleanup.complete'});
}
private scheduleDeferredServerDisconnect(connectionId: string): void {
this.cancelPendingServerDisconnect();
this.transitionFacadeState({type: 'serverDisconnect.schedule', connectionId});
this.pendingServerDisconnectTimeout = setTimeout(() => {
const currentVoiceState = voiceEngineV2AppVoiceStateAdapter.getVoiceStateByConnectionId(connectionId);
const currentVoiceStateChannelId = currentVoiceState?.channel_id ?? null;
if (
shouldRunMediaEngineDeferredDisconnect(this.facadeSnapshot, {
connectionId,
currentConnectionId: voiceEngineV2AppConnectionHostAdapter.connectionId,
connected: voiceEngineV2AppConnectionHostAdapter.connected,
currentVoiceStateChannelId,
})
) {
logger.info('Deferred disconnect executing - no VOICE_SERVER_UPDATE received', {connectionId});
void this.disconnectFromVoiceChannel('server');
} else if (currentVoiceStateChannelId) {
logger.debug('Deferred disconnect ignored because voice state is active again', {
connectionId,
channelId: currentVoiceStateChannelId,
});
}
this.pendingServerDisconnectTimeout = null;
this.transitionFacadeState({type: 'serverDisconnect.timeoutElapsed', connectionId});
}, DEFERRED_DISCONNECT_TIMEOUT_MS);
logger.debug('Scheduled deferred disconnect', {connectionId, timeoutMs: DEFERRED_DISCONNECT_TIMEOUT_MS});
}
private isPushToTalkActive(): boolean {
if (getActiveVoiceProcessingMode(VoiceSettings) === 'studio') return false;
return Keybind.isPushToTalkEffective();
}
private isPushToMuteActive(): boolean {
if (getActiveVoiceProcessingMode(VoiceSettings) === 'studio') return false;
return Keybind.isPushToMuteEffective();
}
private getVoiceEngineV2AudioMode(): VoiceEngineV2AudioMode {
if (this.isPushToTalkActive()) return 'pushToTalk';
if (this.isPushToMuteActive()) return 'pushToMute';
return 'voiceActivity';
}
private syncVoiceEngineV2AudioControlsFromAppState(): void {
this.voiceEngineV2Controller.setAudioControls({
mode: this.getVoiceEngineV2AudioMode(),
locallyMuted: selectVoiceLocalAudioEffectiveSelfMute({
localSelfMute: LocalVoiceState.getSelfMute(),
microphoneFailureLatched: voiceEngineV2AppMediaExecutionAdapter.isMicrophoneFailureLatched(),
}),
preferredLocallyMuted: LocalVoiceState.getSelfMute(),
locallyDeafened: LocalVoiceState.getSelfDeaf(),
mutedByPermission: LocalVoiceState.getMutedByPermission(),
hasUserSetMute: LocalVoiceState.getHasUserSetMute(),
hasUserSetDeaf: LocalVoiceState.getHasUserSetDeaf(),
shouldUnmuteOnUndeafen: LocalVoiceState.shouldUnmuteOnUndeafen,
pushToTalkActive: this.isPushToTalkActive() && Keybind.pushToTalkHeld,
pushToMuteActive: this.isPushToMuteActive() && Keybind.pushToMuteHeld,
inputVolume: VoiceSettings.inputVolume,
outputVolume: VoiceSettings.outputVolume,
});
}
private getEffectiveSelfMuteForVoiceStatePayload(): boolean {
this.syncVoiceEngineV2AudioControlsFromAppState();
return selectVoiceEngineV2AppIntentSelfMuteForVoiceStatePayload(this.voiceEngineV2Snapshot);
}
private cancelPendingServerDisconnect(): void {
if (this.pendingServerDisconnectTimeout) {
clearTimeout(this.pendingServerDisconnectTimeout);
this.pendingServerDisconnectTimeout = null;
logger.debug('Cancelled pending server disconnect', {
connectionId: this.pendingServerDisconnectConnectionId,
});
}
this.transitionFacadeState({type: 'serverDisconnect.cancel'});
}
private seedLocalVoiceStateFromServerUpdate(raw: VoiceServerUpdateData): void {
const previousConnectionId = voiceEngineV2AppConnectionHostAdapter.connectionId;
const currentVoiceState = raw.connection_id
? voiceEngineV2AppVoiceStateAdapter.getVoiceStateByConnectionId(raw.connection_id)
: null;
const resolvedGuildId = raw.guild_id ?? voiceEngineV2AppConnectionHostAdapter.guildId ?? null;
const resolvedChannelId = raw.channel_id ?? voiceEngineV2AppConnectionHostAdapter.channelId ?? null;
if (!raw.connection_id) return;
const viewerStreamKeys = currentVoiceState?.viewer_stream_keys ?? LocalVoiceState.getViewerStreamKeys();
const seededViewerStreamKeys = this.replaceViewerStreamConnectionId(
viewerStreamKeys,
resolvedGuildId,
resolvedChannelId,
previousConnectionId,
raw.connection_id,
);
LocalVoiceState.seedConnectionState(raw.connection_id, {
selfMute: currentVoiceState?.self_mute ?? LocalVoiceState.getSelfMute(),
selfDeaf: currentVoiceState?.self_deaf ?? LocalVoiceState.getSelfDeaf(),
selfVideo: currentVoiceState?.self_video ?? LocalVoiceState.getSelfVideo(),
selfStream: currentVoiceState?.self_stream ?? LocalVoiceState.getSelfStream(),
viewerStreamKeys: seededViewerStreamKeys,
});
replaceWatchedStreamKeys(seededViewerStreamKeys, {sync: false});
const currentUserId = Users.getCurrentUser()?.id;
if (currentUserId) {
this.migratePinnedScreenShareIdentity(currentUserId, previousConnectionId, raw.connection_id);
}
}
handleVoiceServerUpdate(raw: VoiceServerUpdateData): void {
if (raw.connection_id && raw.connection_id === this.pendingServerDisconnectConnectionId) {
this.cancelPendingServerDisconnect();
}
this.handleVoiceServerUpdateViaJs(raw);
}
private handleVoiceServerUpdateViaJs(raw: VoiceServerUpdateData): void {
const expectedChannelId = voiceEngineV2AppConnectionHostAdapter.channelId;
const currentVoiceState = raw.connection_id
? voiceEngineV2AppVoiceStateAdapter.getVoiceStateByConnectionId(raw.connection_id)
: null;
if (
voiceEngineV2AppConnectionHostAdapter.connected &&
raw.channel_id &&
currentVoiceState?.channel_id &&
currentVoiceState.channel_id !== raw.channel_id
) {
logger.warn('Ignoring VOICE_SERVER_UPDATE that conflicts with known voice state', {
expectedChannelId,
incomingChannelId: raw.channel_id,
connectionId: raw.connection_id,
voiceStateChannelId: currentVoiceState.channel_id,
});
return;
}
if (
raw.channel_id &&
expectedChannelId &&
raw.channel_id !== expectedChannelId &&
this.pendingUserMove?.channelId === expectedChannelId &&
voiceEngineV2AppConnectionHostAdapter.connecting
) {
logger.warn('Ignoring VOICE_SERVER_UPDATE during user-initiated move', {
expectedChannelId,
incomingChannelId: raw.channel_id,
connectionId: raw.connection_id,
});
return;
}
const resolvedGuildId = raw.guild_id ?? voiceEngineV2AppConnectionHostAdapter.guildId ?? null;
const resolvedChannelId = raw.channel_id ?? voiceEngineV2AppConnectionHostAdapter.channelId ?? null;
this.seedLocalVoiceStateFromServerUpdate(raw);
const shouldPreserveLocalMedia =
Boolean(this.facadeSnapshot.context.activeConnection) &&
this.facadeSnapshot.context.activeConnection?.guildId === resolvedGuildId &&
this.facadeSnapshot.context.activeConnection?.channelId === resolvedChannelId;
const shouldApplyPendingSessionRestore =
Boolean(this.pendingSessionRestore) &&
this.pendingSessionRestore?.guildId === resolvedGuildId &&
this.pendingSessionRestore?.channelId === resolvedChannelId;
voiceEngineV2AppConnectionHostAdapter.handleVoiceServerUpdate(
raw,
(room, attemptId, guildId, channelId) => {
const roomEventDependencies = buildRoomEventDependencies({
resetStreamTracking: () => this.resetLocalMediaAndScreenShareTracking(),
participants: this.voiceEngineV2Participants,
sourceLifecycleBridge: this.voiceEngineV2SourceLifecycleBridge,
isUserMovePending: () => this.pendingUserMove !== null,
});
bindRoomEvents(
room,
attemptId,
guildId,
channelId,
{
onConnected: async () => {
this.transitionFacadeState({type: 'connection.connected', guildId, channelId});
this.navigateToVoiceChannel(guildId, channelId);
this.startTracking(room);
if (shouldPreserveLocalMedia) {
await this.restoreLocalMediaState(room);
}
if (shouldApplyPendingSessionRestore) {
await this.restorePendingSessionMedia();
}
voiceEngineV2AppMediaExecutionAdapter.resetMicrophoneFailureLatch();
await this.reconcileLocalAudioState('voice room connected');
},
onDisconnected: () => {
AdaptiveScreenShareEngine.stop();
this.stopTracking();
this.statsHostAdapter.reset();
},
onReconnecting: () => {
AdaptiveScreenShareEngine.stop();
this.statsHostAdapter.stopLatencyTracking();
this.statsHostAdapter.stopStatsTracking();
},
onReconnected: () => {
const room = voiceEngineV2AppConnectionHostAdapter.room;
if (room?.localParticipant?.getTrackPublication(Track.Source.ScreenShare)?.videoTrack) {
AdaptiveScreenShareEngine.start(room);
}
this.statsHostAdapter.incrementReconnectionCount();
this.statsHostAdapter.startLatencyTracking();
this.statsHostAdapter.startStatsTracking();
voiceEngineV2AppMediaExecutionAdapter.resetMicrophoneFailureLatch();
this.reconcileLocalAudioStateInBackground('voice room reconnected');
},
},
roomEventDependencies,
);
},
(isChannelMove, previousRoom) => {
this.cancelPendingServerDisconnect();
this.stopTracking();
if (isChannelMove) {
this.statsHostAdapter.reset();
}
this.voiceEngineV2Participants.clear();
let shouldKeepPreviousRoomTracks = false;
if (!isChannelMove && shouldPreserveLocalMedia && LocalVoiceState.getSelfStream()) {
const pendingScreenShareReconnect =
voiceEngineV2AppScreenShareExecutionAdapter.prepareScreenShareReconnect(previousRoom);
if (pendingScreenShareReconnect) {
this.transitionFacadeState({
type: 'screenShareReconnect.prepare',
snapshot: pendingScreenShareReconnect,
});
this.syncLocalVoiceStateWithServer({self_stream: true});
shouldKeepPreviousRoomTracks = true;
} else {
this.transitionFacadeState({type: 'screenShareReconnect.clear'});
}
} else {
this.transitionFacadeState({type: 'screenShareReconnect.clear'});
}
if (!shouldPreserveLocalMedia && this.facadeSnapshot.context.activeConnection) {
voiceEngineV2AppMediaStateAdapter.resetLocalMediaState('disconnect');
this.resetLocalMediaAndScreenShareTracking();
}
return shouldKeepPreviousRoomTracks;
},
(_room, _attemptId) => {},
(newRoom, attemptId, guildId, channelId) => {
logger.info('Region hot-swap complete, rebinding tracking', {guildId, channelId, attemptId});
this.stopTracking();
this.startTracking(newRoom);
this.voiceEngineV2Participants.hydrateFromRoom(newRoom);
VoiceEngineV2AppRemoteSpeakingAdapter.hydrateFromRoom(newRoom);
VoiceEngineV2AppSubscriptionAdapter.reconcileSubscriptions();
VoiceEngineV2AppPermissionAdapter.applyDeafen(newRoom, getEffectiveAudioState().effectiveDeaf);
voiceEngineV2AppMediaExecutionAdapter.applyAllLocalAudioPreferences(newRoom);
if (newRoom.localParticipant?.getTrackPublication(Track.Source.ScreenShare)?.videoTrack) {
AdaptiveScreenShareEngine.start(newRoom);
} else {
AdaptiveScreenShareEngine.stop();
}
if (guildId && channelId) {
VoiceEngineV2AppPermissionAdapter.syncWithPermissionState(guildId, channelId, newRoom);
}
voiceEngineV2AppMediaExecutionAdapter.resetMicrophoneFailureLatch();
this.reconcileLocalAudioStateInBackground('region hot-swap complete');
},
(_guildId, _channelId, _connectionId, _attemptId, error) => {
logger.warn('LiveKit connect failed, clearing gateway voice state', {error});
void this.disconnectFromVoiceChannel('error');
},
);
}
handleGatewayReady(guilds: Array<GuildReadyData>): void {
voiceEngineV2AppVoiceStateAdapter.handleGatewayReady(guilds);
}
handleGuildCreate(guild: GuildReadyData): void {
voiceEngineV2AppVoiceStateAdapter.handleGuildCreate(guild);
}
handleGuildDelete(guildId: string): void {
voiceEngineV2AppVoiceStateAdapter.handleGuildDelete(guildId);
if (voiceEngineV2AppConnectionHostAdapter.connected && voiceEngineV2AppConnectionHostAdapter.guildId === guildId) {
void this.disconnectFromVoiceChannel('server');
}
}
handlePassiveVoiceStates(guildId: string, voiceStates: Array<VoiceState>): void {
for (const voiceState of voiceStates) {
const stateWithGuild = {
...voiceState,
guild_id: voiceState.guild_id ?? guildId,
};
voiceEngineV2AppVoiceStateAdapter.handleGatewayVoiceStateUpdate(guildId, stateWithGuild);
}
}
handleGatewayVoiceStateUpdate(guildId: string | null, voiceState: VoiceState): void {
const user = Users.getCurrentUser();
const isLocalConnection =
user &&
voiceState.user_id === user.id &&
voiceState.connection_id === voiceEngineV2AppConnectionHostAdapter.connectionId;
if (voiceEngineV2AppVoiceStateAdapter.isConnectionIgnored(voiceState.connection_id)) {
voiceEngineV2AppVoiceStateAdapter.handleGatewayVoiceStateUpdate(guildId, voiceState);
if (isLocalConnection) {
this.handleCurrentLocalVoiceStateRemoval(guildId, voiceState);
}
return;
}
const previousConnectionState = voiceState.connection_id
? voiceEngineV2AppVoiceStateAdapter.getVoiceStateByConnectionId(voiceState.connection_id)
: null;
const incomingViewerStreamKeys = normalizeVoiceMediaGraphViewerStreamKeys(voiceState.viewer_stream_keys);
const previousLocalMediaState = isLocalConnection
? {
selfVideo: LocalVoiceState.getSelfVideo(),
selfStream: LocalVoiceState.getSelfStream(),
}
: null;
const previousLocalAudioState =
isLocalConnection && previousConnectionState
? {
serverMute: isVoiceServerMuteActive(
previousConnectionState,
previousConnectionState.guild_id,
previousConnectionState.channel_id,
),
serverDeaf: previousConnectionState.deaf,
selfMute: previousConnectionState.self_mute,
selfDeaf: previousConnectionState.self_deaf,
}
: null;
const previousViewerStreamKeys: ReadonlyArray<string> = previousConnectionState?.viewer_stream_keys ?? [];
if (!isLocalConnection && voiceState.connection_id) {
this.migrateWatchedRemoteStreamConnection(guildId, voiceState);
}
const localServerPayload: VoiceStateSyncPayload | null =
isLocalConnection && voiceState.channel_id && voiceState.connection_id
? {
guild_id: guildId,
channel_id: voiceState.channel_id,
connection_id: voiceState.connection_id,
self_mute: voiceState.self_mute,
self_deaf: voiceState.self_deaf,
self_video: voiceState.self_video,
self_stream: voiceState.self_stream,
viewer_stream_keys: incomingViewerStreamKeys,
}
: null;
const shouldApplyIncomingLocalState =
localServerPayload === null
? true
: shouldApplyGatewayVoiceStateEcho(
this.voiceEngineV2Snapshot,
this.toEngineGatewayVoiceState(voiceState, guildId),
);
const projectedVoiceState =
isLocalConnection && !shouldApplyIncomingLocalState
? {
...voiceState,
self_mute: LocalVoiceState.getSelfMute(),
self_deaf: LocalVoiceState.getSelfDeaf(),
self_video: LocalVoiceState.getSelfVideo(),
self_stream: LocalVoiceState.getSelfStream(),
viewer_stream_keys: LocalVoiceState.getViewerStreamKeys(),
}
: voiceState;
voiceEngineV2AppVoiceStateAdapter.handleGatewayVoiceStateUpdate(guildId, projectedVoiceState);
if (!isLocalConnection && voiceEngineV2AppConnectionHostAdapter.connected && voiceState.channel_id) {
if (!areOrderedStringArraysEqual(previousViewerStreamKeys, incomingViewerStreamKeys)) {
this.playSpectatorSounds(previousViewerStreamKeys, incomingViewerStreamKeys);
}
}
if (!isLocalConnection && voiceState.connection_id) {
const previousGuildId = normalizeStreamGuildId(previousConnectionState?.guild_id);
const nextGuildId = normalizeStreamGuildId(guildId ?? voiceState.guild_id);
const previousChannelId = previousConnectionState?.channel_id ?? null;
const nextChannelId = voiceState.channel_id ?? null;
const leftPreviousStreamChannel =
previousChannelId != null && (previousChannelId !== nextChannelId || previousGuildId !== nextGuildId);
const streamEnded = voiceState.self_stream === false;
const streamChannelId = leftPreviousStreamChannel ? previousChannelId : nextChannelId;
const streamGuildId = leftPreviousStreamChannel ? previousGuildId : nextGuildId;
if (streamChannelId && (streamEnded || leftPreviousStreamChannel)) {
const streamKey = getStreamKey(streamGuildId, streamChannelId, voiceState.connection_id);
if (streamEnded) {
deferStopWatchingStreamKey(streamKey, {
guildId: streamGuildId,
channelId: streamChannelId,
});
} else {
stopWatchingStreamKey(streamKey, {
guildId: streamGuildId,
channelId: streamChannelId,
});
}
}
}
if (isLocalConnection) {
const currentConnectionId = voiceEngineV2AppConnectionHostAdapter.connectionId;
const isCurrentConnection = voiceState.connection_id === currentConnectionId;
let shouldApplyCurrentServerState = true;
const serverPayload = localServerPayload;
if (this.handleCurrentLocalVoiceStateRemoval(guildId, voiceState)) {
return;
}
if (voiceState.channel_id && voiceState.connection_id && isCurrentConnection) {
const ptmActive = this.isPushToMuteActive();
const pttActive = this.isPushToTalkActive();
const applyServerState = shouldApplyIncomingLocalState;
shouldApplyCurrentServerState = applyServerState;
this.applyingGatewayVoiceStateEcho = true;
try {
LocalVoiceState.syncConnectionState(voiceState.connection_id, {
selfMute: ptmActive || pttActive || !applyServerState ? undefined : voiceState.self_mute,
selfDeaf: applyServerState ? voiceState.self_deaf : undefined,
selfVideo: applyServerState ? voiceState.self_video : undefined,
selfStream: applyServerState ? voiceState.self_stream : undefined,
viewerStreamKeys: applyServerState ? incomingViewerStreamKeys : undefined,
});
} finally {
this.applyingGatewayVoiceStateEcho = false;
}
if (applyServerState) {
replaceWatchedStreamKeys([...incomingViewerStreamKeys], {sync: false});
}
}
this.applyEngineGatewayEcho(serverPayload ? this.toEngineGatewayVoiceState(voiceState, guildId) : null);
if (voiceState.channel_id && voiceState.connection_id) {
if (isCurrentConnection && this.pendingServerDisconnectConnectionId === voiceState.connection_id) {
this.cancelPendingServerDisconnect();
}
if (
isCurrentConnection &&
voiceEngineV2AppConnectionHostAdapter.connected &&
!voiceEngineV2AppConnectionHostAdapter.connecting &&
voiceState.channel_id !== voiceEngineV2AppConnectionHostAdapter.channelId
) {
const updatedGuildId = guildId ?? voiceState.guild_id ?? null;
void this.disconnectForChannelMove('server').finally(() => {
voiceEngineV2AppConnectionHostAdapter.recoverConnectionExpectation(updatedGuildId, voiceState.channel_id!);
});
return;
}
const shouldRecoverConnectionExpectation =
!voiceEngineV2AppConnectionHostAdapter.connected &&
!voiceEngineV2AppConnectionHostAdapter.connecting &&
voiceEngineV2AppConnectionHostAdapter.guildId === null &&
voiceEngineV2AppConnectionHostAdapter.channelId === null;
if (shouldRecoverConnectionExpectation) {
const recoveredGuildId = guildId ?? voiceState.guild_id ?? null;
voiceEngineV2AppConnectionHostAdapter.recoverConnectionExpectation(recoveredGuildId, voiceState.channel_id);
logger.info('Recovered connection expectation from voice state update', {
guildId: recoveredGuildId,
channelId: voiceState.channel_id,
connectionId: voiceState.connection_id,
});
}
const nextServerMute = isVoiceServerMuteActive(
voiceState,
guildId ?? voiceState.guild_id,
voiceState.channel_id,
);
const audioStateChanged =
previousLocalAudioState &&
(previousLocalAudioState.serverMute !== nextServerMute ||
previousLocalAudioState.serverDeaf !== voiceState.deaf ||
previousLocalAudioState.selfMute !== voiceState.self_mute ||
previousLocalAudioState.selfDeaf !== voiceState.self_deaf);
if (isCurrentConnection && audioStateChanged) {
this.reconcileLocalAudioStateInBackground('voice state update');
}
}
if (previousLocalMediaState) {
const videoDisabled =
shouldApplyCurrentServerState && previousLocalMediaState.selfVideo && voiceState.self_video === false;
const streamDisabled =
shouldApplyCurrentServerState && previousLocalMediaState.selfStream && voiceState.self_stream === false;
if (videoDisabled) {
logger.info('Server disabled camera for local connection, unpublishing video track');
void this.setCameraEnabled(false, {sendUpdate: false});
}
if (streamDisabled) {
logger.info('Server disabled screen share for local connection, unpublishing stream track');
void this.setScreenShareEnabled(false, {sendUpdate: false});
}
}
if (
voiceState.channel_id === null &&
voiceEngineV2AppConnectionHostAdapter.connected &&
voiceState.connection_id &&
isCurrentConnection
) {
this.scheduleDeferredServerDisconnect(voiceState.connection_id);
}
}
}
handleGatewayVoiceStateDelete(guildId: string, userId: string): void {
voiceEngineV2AppVoiceStateAdapter.handleGatewayVoiceStateDelete(guildId, userId);
}
getCurrentUserVoiceState(guildId?: string | null): NormalizedVoiceState | null {
return voiceEngineV2AppVoiceStateAdapter.getCurrentUserVoiceState(
guildId,
Users.getCurrentUser()?.id,
voiceEngineV2AppConnectionHostAdapter.connectionId,
);
}
getVoiceState(guildId: string | null, userId?: string): NormalizedVoiceState | null {
const currentUserId = Users.getCurrentUser()?.id;
const connectionId =
!userId || userId === currentUserId ? voiceEngineV2AppConnectionHostAdapter.connectionId : null;
return voiceEngineV2AppVoiceStateAdapter.getVoiceState(guildId, userId, currentUserId, connectionId);
}
getVoiceStateByConnectionId(connectionId: string): NormalizedVoiceState | null {
return voiceEngineV2AppVoiceStateAdapter.getVoiceStateByConnectionId(connectionId);
}
isVoiceConnectionIgnored(connectionId: string | null | undefined): boolean {
return voiceEngineV2AppVoiceStateAdapter.isConnectionIgnored(connectionId);
}
getAllVoiceStatesInChannel(guildId: string, channelId: string): Readonly<Record<string, NormalizedVoiceState>> {
return voiceEngineV2AppVoiceStateAdapter.getAllVoiceStatesInChannel(guildId, channelId);
}
getAllVoiceStatesInGuild(
guildId: string,
): Readonly<Record<string, Readonly<Record<string, NormalizedVoiceState>>>> | undefined {
return voiceEngineV2AppVoiceStateAdapter.getAllVoiceStatesInGuild(guildId);
}
getAllVoiceStates(): Readonly<
Record<string, Readonly<Record<string, Readonly<Record<string, NormalizedVoiceState>>>>>
> {
return voiceEngineV2AppVoiceStateAdapter.getAllVoiceStates();
}
handleGatewayVoiceStateAck(data: VoiceStateAckPayload): void {
if (shouldNotifyCameraUserLimitRejection({status: data.status, errorCode: data.error_code})) {
this.handleCameraUserLimitRejection();
}
const canonicalState = data.canonical_state;
if (!canonicalState?.channel_id || !canonicalState.connection_id) {
logger.debug('Ignoring voice state ack without canonical connection state', {
mutationId: data.mutation_id,
status: data.status,
guildId: data.guild_id,
channelId: data.channel_id,
connectionId: data.connection_id,
});
return;
}
this.applyEngineGatewayEcho(
this.toEngineGatewayVoiceState(canonicalState, canonicalState.guild_id ?? data.guild_id ?? null),
);
}
private handleCameraUserLimitRejection(): void {
logger.warn('Camera enable rejected by the gateway camera user limit', {
limit: VOICE_CHANNEL_CAMERA_USER_LIMIT,
});
void this.setCameraEnabled(false, {sendUpdate: false}).catch((error) => {
logger.warn('Failed to turn the camera back off after a camera user limit rejection', {error});
});
if (!this.i18n) return;
this.showVoiceErrorModal(
VOICE_CAMERA_USER_LIMIT_REACHED_DESCRIPTOR,
'voice.media-engine-facade.camera-user-limit-error-modal',
{voiceChannelCameraUserLimit: VOICE_CHANNEL_CAMERA_USER_LIMIT},
);
}
private toEngineGatewayVoiceState(
source: {
guild_id?: string | null;
channel_id: string | null;
user_id: string;
session_id?: string;
self_mute: boolean;
self_deaf: boolean;
self_video: boolean;
self_stream: boolean;
suppress?: boolean;
},
guildId: string | null,
): VoiceEngineV2GatewayVoiceState {
return {
guildId: guildId ?? source.guild_id ?? null,
channelId: source.channel_id,
userId: source.user_id,
sessionId: source.session_id ?? null,
selfMute: source.self_mute,
selfDeaf: source.self_deaf,
selfVideo: source.self_video,
selfStream: source.self_stream,
suppress: source.suppress ?? false,
requestToSpeakTimestamp: null,
};
}
private applyEngineGatewayEcho(voiceState: VoiceEngineV2GatewayVoiceState | null): void {
this.voiceEngineV2Controller.dispatch({type: 'gateway.voiceStateUpdated', voiceState});
if (voiceState) {
this.voiceEngineV2Controller.reconcileGatewayVoiceState();
}
}
syncLocalVoiceStateWithServer(partial?: VoiceStateSyncPartial): void {
const devicePermission = VoiceDevicePermissionState.getState().permissionStatus;
const micGranted = MediaPermission.isMicrophoneGranted() || devicePermission.audio === 'granted';
if (!micGranted || LocalVoiceState.getMutedByPermission()) {
LocalVoiceState.ensurePermissionMute();
}
const {guildId, channelId, connectionId} = voiceEngineV2AppConnectionHostAdapter.connectionState;
if (!channelId || !connectionId) return;
const selfMute = resolveVoiceStateSelfMute({
guildId,
channelId,
microphoneGranted: micGranted,
requestedSelfMute: partial?.self_mute,
effectiveSelfMute: this.getEffectiveSelfMuteForVoiceStatePayload(),
});
const payload: VoiceStateSyncPayload = {
guild_id: guildId,
channel_id: channelId,
connection_id: connectionId,
self_mute: selfMute,
self_deaf: partial?.self_deaf ?? LocalVoiceState.getSelfDeaf(),
self_video: partial?.self_video ?? LocalVoiceState.getSelfVideo(),
self_stream: partial?.self_stream ?? LocalVoiceState.getSelfStream(),
viewer_stream_keys: partial?.viewer_stream_keys ?? LocalVoiceState.getViewerStreamKeys(),
};
if (!micGranted && !LocalVoiceState.getSelfMute()) {
LocalVoiceState.updateSelfMute(true);
}
this.voiceEngineV2Controller.setDesiredGatewayVoiceState({
guildId,
channelId,
selfMute: payload.self_mute,
selfDeaf: payload.self_deaf,
selfVideo: payload.self_video,
selfStream: payload.self_stream,
});
if (partial?.viewer_stream_keys !== undefined) {
syncVoiceStateToServer(guildId, channelId, connectionId, {viewer_stream_keys: payload.viewer_stream_keys});
}
}
private async reconcileLocalAudioState(reason: string): Promise<void> {
const {channelId, connectionId} = voiceEngineV2AppConnectionHostAdapter.connectionState;
if (!channelId || !connectionId) {
logger.debug('Skipping local audio reconciliation without an active voice connection', {
reason,
channelId,
connectionId,
});
return;
}
const serverVoiceState = voiceEngineV2AppVoiceStateAdapter.getVoiceStateByConnectionId(connectionId);
const serverMute = isVoiceServerMuteActive(
serverVoiceState,
voiceEngineV2AppConnectionHostAdapter.guildId,
channelId,
);
const serverDeaf = serverVoiceState?.deaf ?? false;
await voiceEngineV2AppMediaExecutionAdapter.reconcileEffectiveAudioState(
voiceEngineV2AppConnectionHostAdapter.room,
{
channelId,
serverMute,
serverDeaf,
},
);
this.syncLocalVoiceStateWithServer();
}
getParticipantByUserIdAndConnectionId(
userId: string,
connectionId: string | null,
): LivekitParticipantSnapshot | undefined {
return this.voiceEngineV2Participants.getParticipantByUserIdAndConnectionId(userId, connectionId);
}
upsertParticipant(participant: Participant): void {
this.voiceEngineV2Participants.upsertParticipant(participant);
}
async setCameraEnabled(
enabled: boolean,
options?: {
deviceId?: string;
sendUpdate?: boolean;
},
): Promise<void> {
const selectedOptions = enabled
? {
deviceId: options?.deviceId ?? VoiceSettings.getVideoDeviceId(),
...(options?.sendUpdate === false ? {sendUpdate: false} : {}),
}
: options;
const commandOptions = buildCameraCommandOptions(selectedOptions);
try {
await this.voiceEngineV2Host.runAndWait(
() => {
if (enabled) {
this.voiceEngineV2Controller.publishCamera(commandOptions);
} else {
this.voiceEngineV2Controller.unpublishCamera(commandOptions);
}
},
{description: enabled ? 'publish camera' : 'unpublish camera'},
);
} catch (error) {
if (isCameraPermissionDeniedCommandFailure(error)) {
logger.info('Camera enable denied by permission; denial already surfaced to the user', {enabled});
return;
}
throw error;
}
}
async enableMicrophone(
room: Room,
channelId: string | null,
options: VoiceEngineV2MicrophoneOptions = {},
): Promise<void> {
await voiceEngineV2AppMediaExecutionAdapter.enableMicrophone(room, channelId, options);
}
async refreshMicrophonePublishSettingsForChannel(channelId: string): Promise<void> {
assert.ok(channelId.length > 0, 'microphone publish settings refresh requires a channelId');
if (channelId !== voiceEngineV2AppConnectionHostAdapter.channelId) return;
await voiceEngineV2AppMediaExecutionAdapter.refreshMicrophonePublishSettings(
voiceEngineV2AppConnectionHostAdapter.room,
channelId,
);
}
async updateActiveCameraCapture(options?: {deviceId?: string}): Promise<void> {
await this.setCameraEnabled(true, options);
}
async setScreenShareEnabled(
enabled: boolean,
options?: ScreenShareCaptureOptions & {
sendUpdate?: boolean;
playSound?: boolean;
restartIfEnabled?: boolean;
preserveStreamAudioPreferences?: boolean;
},
publishOptions?: TrackPublishOptions,
): Promise<void> {
await voiceEngineV2AppScreenShareExecutionAdapter.setScreenShareEnabled(
this.room,
enabled,
options,
publishOptions,
);
}
async startDeviceScreenShare(
options?: DeviceScreenShareCaptureOptions,
publishOptions?: TrackPublishOptions,
): Promise<void> {
await voiceEngineV2AppScreenShareExecutionAdapter.startDeviceScreenShare(this.room, options, publishOptions);
}
private createScreenShareControllerGateway(): VoiceEngineV2AppScreenShareControllerGateway {
const plannedScreenOperationIds = (commands: ReadonlyArray<VoiceEngineV2Command>): Array<number> => {
const operationIds: Array<number> = [];
for (const command of commands) {
if (command.type === 'screen.publish') operationIds.push(command.operationId);
if (command.type === 'screen.updateEncoding') operationIds.push(command.operationId);
if (command.type === 'screen.unpublish') operationIds.push(command.operationId);
}
return operationIds;
};
return {
isScreenCommandRoutable: () => this.voiceEngineV2Snapshot.connection.status === 'connected',
hasScreenPublication: () => {
const screen = this.voiceEngineV2Snapshot.screen;
if (screen.published != null) return true;
return screen.status === 'publishing' || screen.status === 'unpublishing';
},
hasScreenDesired: () => this.voiceEngineV2Snapshot.screen.desired != null,
clearScreenDesired: () => {
if (this.voiceEngineV2Snapshot.screen.desired == null) return;
this.voiceEngineV2Controller.unpublishScreen();
},
executingScreenOperationId: () => this.voiceEngineV2Host.executingCommand('screen')?.operationId ?? null,
isScreenOperationPending: (operationId) => this.voiceEngineV2Host.isOperationPending(operationId),
publishScreen: (options, onPlanned) =>
this.voiceEngineV2Host.runAndWait(
() => {
this.voiceEngineV2Controller.publishScreen(options);
},
{
description: 'publish screen share',
staleCompletion: 'resolve',
onCommandsPlanned: (commands) => onPlanned(plannedScreenOperationIds(commands)),
},
),
unpublishScreen: (onPlanned) =>
this.voiceEngineV2Host.runAndWait(
() => {
this.voiceEngineV2Controller.unpublishScreen();
},
{
description: 'unpublish screen share',
staleCompletion: 'resolve',
onCommandsPlanned: (commands) => onPlanned(plannedScreenOperationIds(commands)),
},
),
};
}
async replaceActiveDisplayScreenShare(
options?: ScreenShareCaptureOptions,
publishOptions?: TrackPublishOptions,
captureContext?: DisplayScreenShareCaptureContext,
): Promise<boolean> {
return voiceEngineV2AppScreenShareExecutionAdapter.replaceActiveDisplayScreenShare(
this.room,
options,
publishOptions,
captureContext,
);
}
async replaceActiveDeviceScreenShare(
options?: DeviceScreenShareCaptureOptions,
publishOptions?: TrackPublishOptions,
): Promise<boolean> {
return voiceEngineV2AppScreenShareExecutionAdapter.replaceActiveDeviceScreenShare(
this.room,
options,
publishOptions,
);
}
async ensureLinuxScreenShareAudioPublication(
linuxRule?: NonNullable<NativeAudioStartOptions['linuxRule']>,
options?: {includeSelfWindowAudio?: boolean; replaceExisting?: boolean},
): Promise<boolean> {
return voiceEngineV2AppScreenShareExecutionAdapter.ensureLinuxScreenShareAudioPublication(
this.room,
linuxRule,
options,
);
}
setScreenShareAudioMuted(muted: boolean): void {
voiceEngineV2AppScreenShareExecutionAdapter.setScreenShareAudioMuted(this.room, muted);
}
async updateActiveScreenShareSettings(
options?: ScreenShareCaptureOptions,
publishOptions?: TrackPublishOptions,
): Promise<boolean> {
return voiceEngineV2AppScreenShareExecutionAdapter.updateActiveScreenShareSettings(
this.room,
options,
publishOptions,
);
}
applyLocalAudioPreferencesForUser(userId: string): void {
voiceEngineV2AppMediaExecutionAdapter.applyLocalAudioPreferencesForUser(userId, this.room);
}
applyAllLocalAudioPreferences(): void {
voiceEngineV2AppMediaExecutionAdapter.applyAllLocalAudioPreferences(this.room);
}
applyLocalInputVolume(): void {
voiceEngineV2AppMediaExecutionAdapter.applyLocalInputVolume(this.room);
}
setLocalVideoDisabled(identity: string, disabled: boolean): void {
voiceEngineV2AppMediaExecutionAdapter.setLocalVideoDisabled(identity, disabled, this.room, this.connectionId);
}
applyPushToTalkHold(held: boolean): void {
voiceEngineV2AppMediaExecutionAdapter.applyPushToTalkHold(held, this.room, () => this.getCurrentUserVoiceState());
this.syncVoiceEngineV2AudioControlsFromAppState();
}
applyPushToMuteHold(held: boolean): void {
voiceEngineV2AppMediaExecutionAdapter.applyPushToMuteHold(held, this.room, () => this.getCurrentUserVoiceState());
this.syncVoiceEngineV2AudioControlsFromAppState();
}
handlePushToTalkModeChange(): void {
voiceEngineV2AppMediaExecutionAdapter.handlePushToTalkModeChange(this.room, () => this.getCurrentUserVoiceState());
this.syncVoiceEngineV2AudioControlsFromAppState();
}
getMuteReason(voiceState: VoiceState | null): VoiceMuteReason {
return voiceEngineV2AppMediaExecutionAdapter.getMuteReason(voiceState, this.guildId, this.channelId);
}
isMicrophoneFailureLatched(): boolean {
return voiceEngineV2AppMediaExecutionAdapter.isMicrophoneFailureLatched();
}
clearMicrophoneFailureLatch(): void {
if (!voiceEngineV2AppMediaExecutionAdapter.isMicrophoneFailureLatched()) return;
voiceEngineV2AppMediaExecutionAdapter.noteUserMuteIntentChanged();
this.syncVoiceEngineV2AudioControlsFromAppState();
this.reconcileLocalAudioStateInBackground('microphone failure latch cleared');
}
async toggleCameraFromKeybind(): Promise<void> {
const current = LocalVoiceState.getSelfVideo();
await this.setCameraEnabled(!current, {deviceId: VoiceSettings.getVideoDeviceId()});
}
async toggleScreenShareFromKeybind(): Promise<void> {
await voiceEngineV2AppScreenShareExecutionAdapter.toggleScreenShareFromKeybind(this.room);
}
private startTracking(roomOverride?: Room | null): void {
const room = roomOverride ?? voiceEngineV2AppConnectionHostAdapter.room;
if (!room) {
logger.warn('No room available');
return;
}
startVoiceMediaGraphTimerScheduler();
this.statsHostAdapter.setRoom(room);
this.statsHostAdapter.startLatencyTracking();
this.statsHostAdapter.startStatsTracking();
VoiceEngineV2AppSubscriptionAdapter.setRoom(room);
VoiceEngineV2AppPermissionAdapter.initializeSubscriptions(room);
this.outputDeviceSyncDisposer?.();
this.outputDeviceSyncDisposer = bindOutputDeviceSync(room);
this.audioPreferencesSyncDisposer?.();
this.audioPreferencesSyncDisposer = this.bindAudioPreferencesSync(room);
this.videoCodecDecodeCapResyncDisposer?.();
this.videoCodecDecodeCapResyncDisposer = this.bindVideoDecodeCapResync();
this.videoCodecPublishOverrideSyncDisposer?.();
this.videoCodecPublishOverrideSyncDisposer = this.bindVideoCodecPublishOverrideSync();
this.videoCodecGossipReceiverDisposer?.();
this.videoCodecGossipReceiverDisposer = this.bindVideoCodecGossipReceiver(room);
this.startAfkTracking();
const channelId = voiceEngineV2AppConnectionHostAdapter.channelId;
if (channelId) {
voiceEngineV2AppDebugEventSinkHostAdapter.start({
guildId: voiceEngineV2AppConnectionHostAdapter.guildId,
channelId,
connectionId: voiceEngineV2AppConnectionHostAdapter.connectionId,
room,
});
}
logger.info('All tracking started');
}
private stopTracking(): void {
voiceEngineV2AppDebugEventSinkHostAdapter.stop('tracking-stopped');
this.statsHostAdapter.stopLatencyTracking();
this.statsHostAdapter.stopStatsTracking();
VoiceEngineV2AppRemoteSpeakingAdapter.clear();
VoiceEngineV2AppSubscriptionAdapter.cleanup();
VoiceEngineV2AppPermissionAdapter.reset();
this.outputDeviceSyncDisposer?.();
this.outputDeviceSyncDisposer = null;
this.audioPreferencesSyncDisposer?.();
this.audioPreferencesSyncDisposer = null;
this.videoCodecDecodeCapResyncDisposer?.();
this.videoCodecDecodeCapResyncDisposer = null;
this.videoCodecPublishOverrideSyncDisposer?.();
this.videoCodecPublishOverrideSyncDisposer = null;
this.videoCodecGossipReceiverDisposer?.();
this.videoCodecGossipReceiverDisposer = null;
this.previousWatchedStreamCodecGossip = new Map();
this.stopAfkTracking();
logger.info('All tracking stopped');
}
private abortVoiceConnection(): void {
this.transitionFacadeState({type: 'screenShareReconnect.clear'});
voiceEngineV2AppConnectionHostAdapter.abortConnection();
}
private bindVideoCodecPublishOverrideSync(): () => void {
assert.ok(this.voiceEngineV2Host != null, 'video codec publish override sync requires the v2 host');
const apply = (source: VoiceEngineV2LocalStreamSource, preference: CodecPreference): void => {
this.voiceEngineV2Controller.setVideoCodecOverride(source, preference === 'auto' ? null : preference);
};
let previousScreen: CodecPreference = VoiceSettings.getPreferredScreenShareCodec();
let previousCamera: CodecPreference = VoiceSettings.getPreferredVideoCodec();
apply('screen', previousScreen);
apply('camera', previousCamera);
return VoiceSettings.subscribe(() => {
const screen = VoiceSettings.getPreferredScreenShareCodec();
if (screen !== previousScreen) {
previousScreen = screen;
apply('screen', screen);
}
const camera = VoiceSettings.getPreferredVideoCodec();
if (camera !== previousCamera) {
previousCamera = camera;
apply('camera', camera);
}
});
}
private bindVideoDecodeCapResync(): () => void {
assert.ok(this.voiceEngineV2Host != null, 'video decode cap resync requires the v2 host');
let previous: CodecPreference = VoiceSettings.getEmulatedDecodeVideoCodecCap();
return VoiceSettings.subscribe(() => {
const current = VoiceSettings.getEmulatedDecodeVideoCodecCap();
if (current === previous) return;
previous = current;
this.previousWatchedStreamCodecGossip = new Map();
this.syncWatchedStreamCodecGossip();
});
}
private scheduleLocalStreamCodecReconcile(): void {
if (this.localStreamCodecReconcileScheduled) return;
this.localStreamCodecReconcileScheduled = true;
queueMicrotask(() => {
this.localStreamCodecReconcileScheduled = false;
if (this.voiceEngineV2Host == null) return;
this.reconcileLocalStreamCodec('camera', this.voiceEngineV2Snapshot.camera);
this.reconcileLocalStreamCodec('screen', this.voiceEngineV2Snapshot.screen);
this.syncWatchedStreamCodecGossip();
});
}
private syncWatchedStreamCodecGossip(): void {
const result = computeVoiceEngineV2WatchedStreamGossip(
this.previousWatchedStreamCodecGossip,
this.voiceEngineV2Snapshot.watchedStreams,
getLocalDecodableVideoCodecs(VoiceSettings.getEmulatedDecodeVideoCodecCap()),
);
this.previousWatchedStreamCodecGossip = result.next;
for (const {destinationIdentity, message} of result.messages) {
this.voiceEngineV2Controller.publishData({
payload: encodeVoiceEngineV2CodecGossip(message),
reliable: true,
topic: VOICE_ENGINE_V2_CODEC_GOSSIP_TOPIC,
destinationIdentities: [destinationIdentity],
});
}
}
private bindVideoCodecGossipReceiver(room: Room): () => void {
const handler = (payload: Uint8Array, participant?: Participant, _kind?: unknown, topic?: string): void => {
if (topic !== VOICE_ENGINE_V2_CODEC_GOSSIP_TOPIC) return;
const identity = participant?.identity;
if (!identity) return;
ingestVoiceEngineV2CodecGossip(this.voiceEngineV2Controller, identity, payload);
};
room.on(RoomEvent.DataReceived, handler);
return () => {
room.off(RoomEvent.DataReceived, handler);
};
}
private reconcileLocalStreamCodec(
source: VoiceEngineV2LocalStreamSource,
media: {
status: string;
published: {codec?: VoiceEngineV2VideoCodec} | null;
desired: {codec?: VoiceEngineV2VideoCodec} | null;
},
): void {
const registered = this.voiceEngineV2Snapshot.codecNegotiation.streams[source] != null;
const active = media.published != null || media.status === 'publishing';
if (!active) {
if (registered) this.voiceEngineV2Controller.unregisterLocalStreamCodec(source);
return;
}
if (registered) return;
const codec: VoiceEngineV2VideoCodec = media.published?.codec ?? media.desired?.codec ?? '';
const streamIdentity = `${source}:${crypto.randomUUID()}`;
this.voiceEngineV2Controller.registerLocalStreamCodec(source, streamIdentity, codec);
}
private bindAudioPreferencesSync(room: Room): () => void {
return bindVoiceEngineV2AppAudioPreferencesSync(
room,
{
refreshMicrophone: async () => this.refreshMicrophoneFromCurrentEngine(),
refreshLocalVoiceInputProcessor: async () => {
await voiceEngineV2AppMediaExecutionAdapter.refreshLocalVoiceInputProcessor(room);
},
applyLocalInputVolume: () => voiceEngineV2AppMediaExecutionAdapter.applyLocalInputVolume(room),
applyAllLocalAudioPreferences: () => voiceEngineV2AppMediaExecutionAdapter.applyAllLocalAudioPreferences(room),
},
logger,
);
}
private startAfkTracking(): void {
this.stopAfkTracking();
this.afkIntervalId = setInterval(() => {
if (
!voiceEngineV2AppConnectionHostAdapter.connected ||
!voiceEngineV2AppConnectionHostAdapter.guildId ||
!voiceEngineV2AppConnectionHostAdapter.channelId
)
return;
const guild = Guilds.getGuild(voiceEngineV2AppConnectionHostAdapter.guildId);
if (
shouldMoveToAfkOnTick({
hasRecentVoiceActivity: this.noteCurrentLocalVoiceActivity(),
channelId: voiceEngineV2AppConnectionHostAdapter.channelId,
afkChannelId: guild?.afkChannelId,
afkTimeoutSeconds: guild?.afkTimeout,
inactiveDurationMs: Idle.getInactiveDurationMs(),
})
) {
void this.moveToAfkChannel();
}
}, AFK_CHECK_INTERVAL_MS);
}
private noteCurrentLocalVoiceActivity(): boolean {
const livekitParticipant = voiceEngineV2AppConnectionHostAdapter.room?.localParticipant;
if (noteLocalVoiceActivity(livekitParticipant)) return true;
return noteLocalVoiceActivityFromSnapshot(this.voiceEngineV2Participants.getLocalParticipant());
}
private stopAfkTracking(): void {
if (this.afkIntervalId !== null) {
clearInterval(this.afkIntervalId);
this.afkIntervalId = null;
}
}
private playSpectatorSounds(oldStreamKeys: ReadonlyArray<string>, newStreamKeys: ReadonlyArray<string>): void {
if (VoiceRegionTeleport.shouldSuppressRejoinSounds()) return;
const myConnectionId = voiceEngineV2AppConnectionHostAdapter.connectionId;
if (!myConnectionId) return;
const myViewerStreamKeys = LocalVoiceState.getViewerStreamKeys();
const isRelevantStream = (key: string): boolean => {
const parsed = parseStreamKey(key);
if (!parsed) return false;
if (parsed.connectionId === myConnectionId) return true;
if (myViewerStreamKeys.includes(key)) return true;
return false;
};
const isEndedLocalStream = (key: string): boolean => {
const parsed = parseStreamKey(key);
return parsed?.connectionId === myConnectionId && !LocalVoiceState.getSelfStream();
};
const oldRelevant = new Set(oldStreamKeys.filter(isRelevantStream));
const newRelevant = new Set(newStreamKeys.filter(isRelevantStream));
const joined = newStreamKeys.filter((k) => isRelevantStream(k) && !oldRelevant.has(k));
const left = oldStreamKeys.filter((k) => isRelevantStream(k) && !newRelevant.has(k) && !isEndedLocalStream(k));
if (joined.length > 0) {
SoundCommands.playSound(SoundType.ViewerJoin);
} else if (left.length > 0) {
SoundCommands.playSound(SoundType.ViewerLeave);
}
}
private replaceViewerStreamConnectionId(
keys: ReadonlyArray<string>,
guildId: string | null | undefined,
channelId: string | null | undefined,
previousConnectionId: string | null | undefined,
nextConnectionId: string | null | undefined,
): Array<string> {
if (!channelId || !previousConnectionId || !nextConnectionId || previousConnectionId === nextConnectionId) {
return [...keys];
}
const normalizedGuildId = normalizeStreamGuildId(guildId);
const previousKey = getStreamKey(normalizedGuildId, channelId, previousConnectionId);
const nextKey = getStreamKey(normalizedGuildId, channelId, nextConnectionId);
let changed = false;
const updated: Array<string> = [];
for (const key of keys) {
const migratedKey = key === previousKey ? nextKey : key;
changed ||= migratedKey !== key;
if (!updated.includes(migratedKey)) {
updated.push(migratedKey);
}
}
return changed ? updated : [...keys];
}
private migratePinnedScreenShareIdentity(
userId: string,
previousConnectionId: string | null | undefined,
nextConnectionId: string | null | undefined,
): void {
if (!previousConnectionId || !nextConnectionId || previousConnectionId === nextConnectionId) return;
if (VoiceCallLayout.pinnedParticipantSource !== VoiceTrackSource.ScreenShare) return;
if (VoiceCallLayout.pinnedParticipantIdentity !== buildVoiceParticipantIdentity(userId, previousConnectionId))
return;
VoiceCallLayout.setPinnedParticipant(
buildVoiceParticipantIdentity(userId, nextConnectionId),
VoiceTrackSource.ScreenShare,
);
}
private migrateWatchedRemoteStreamConnection(guildId: string | null, voiceState: VoiceState): void {
const channelId = voiceState.channel_id;
const nextConnectionId = voiceState.connection_id;
if (!channelId || !nextConnectionId || !voiceState.self_stream) return;
const normalizedGuildId = normalizeStreamGuildId(guildId ?? voiceState.guild_id);
const previousStates = voiceEngineV2AppVoiceStateAdapter.getAllVoiceStatesInChannel(
normalizedGuildId ?? ME,
channelId,
);
const currentKeys = LocalVoiceState.getViewerStreamKeys();
let updatedKeys: Array<string> = [...currentKeys];
let migrated = false;
for (const previousConnectionKey in previousStates) {
const previousState = previousStates[previousConnectionKey];
if (!previousState) continue;
const previousConnectionId = previousState.connection_id;
if (
previousState.user_id !== voiceState.user_id ||
!previousConnectionId ||
previousConnectionId === nextConnectionId ||
!previousState.self_stream
) {
continue;
}
const nextKeys = this.replaceViewerStreamConnectionId(
updatedKeys,
normalizedGuildId,
channelId,
previousConnectionId,
nextConnectionId,
);
if (!areOrderedStringArraysEqual(nextKeys, updatedKeys)) {
this.migratePinnedScreenShareIdentity(voiceState.user_id, previousConnectionId, nextConnectionId);
updatedKeys = nextKeys;
migrated = true;
}
}
const result = replaceWatchedStreamKeys(updatedKeys, {sync: false});
if (!result.changed) return;
this.syncLocalVoiceStateWithServer({viewer_stream_keys: result.keys});
if (migrated) {
logger.info('Migrated watched stream connection id', {
userId: voiceState.user_id,
channelId,
nextConnectionId,
});
}
}
disconnectRemoteDevice(guildId: string, connectionId: string): void {
if (connectionId === voiceEngineV2AppConnectionHostAdapter.connectionId) {
void this.disconnectFromVoiceChannel('user');
return;
}
this.discardVoiceConnection(connectionId);
sendVoiceStateDisconnect(guildId, connectionId);
}
disconnectAllRemoteDevices(
devices: ReadonlyArray<{
guildId: string;
connectionId: string;
}>,
): void {
let shouldDisconnectCurrentDevice = false;
const currentConnectionId = voiceEngineV2AppConnectionHostAdapter.connectionId;
for (const device of devices) {
if (device.connectionId === currentConnectionId) {
shouldDisconnectCurrentDevice = true;
continue;
}
this.discardVoiceConnection(device.connectionId);
sendVoiceStateDisconnect(device.guildId, device.connectionId);
}
if (shouldDisconnectCurrentDevice) {
void this.disconnectFromVoiceChannel('user');
}
}
async moveToAfkChannel(): Promise<void> {
const {guildId, channelId, connected} = voiceEngineV2AppConnectionHostAdapter.connectionState;
if (!connected || !guildId || !channelId) return;
const guild = Guilds.getGuild(guildId);
if (!guild?.afkChannelId || channelId === guild.afkChannelId) return;
await this.connectToVoiceChannel(guildId, guild.afkChannelId);
}
getLastConnectedChannel(): {
guildId: string;
channelId: string;
} | null {
return voiceEngineV2AppConnectionHostAdapter.lastConnectedChannel;
}
getShouldReconnect(): boolean {
return voiceEngineV2AppConnectionHostAdapter.shouldAutoReconnect;
}
markReconnectionAttempted(): void {
voiceEngineV2AppConnectionHostAdapter.markReconnectionAttempted();
}
async handleLogout(): Promise<void> {
this.terminalUnloadVoiceDisconnectSent = false;
this.cancelPendingServerDisconnect();
this.transitionFacadeState({type: 'cleanup.logoutStarted'});
this.clearViewerStreamKeys();
this.stopTracking();
await this.stopActiveScreenShareForTeardown('disconnect');
VoiceSessionRestore.clearSnapshot();
voiceEngineV2AppConnectionHostAdapter.cleanup();
voiceEngineV2AppVoiceStateAdapter.clearAllVoiceStates();
this.voiceEngineV2Participants.clear();
VoiceEngineV2AppPermissionAdapter.reset();
this.resetLocalMediaAndScreenShareTracking();
voiceEngineV2AppMediaStateAdapter.resetLocalMediaState('logout');
try {
await voiceStatsDB.clear();
} catch (error) {
logger.error('Failed to clear voice stats DB during logout', error);
throw error;
} finally {
logger.info('Cleanup complete');
this.transitionFacadeState({type: 'cleanup.complete'});
}
}
handleGatewayError(error: GatewayErrorData): void {
const decision = selectMediaEngineGatewayErrorDecision(this.facadeSnapshot, {
code: error.code,
connecting: voiceEngineV2AppConnectionHostAdapter.connecting,
connected: voiceEngineV2AppConnectionHostAdapter.connected,
channelId: voiceEngineV2AppConnectionHostAdapter.channelId,
});
if (decision.type === 'ignore') return;
logger.warn(`Voice-related gateway error: [${error.code}] ${error.message}`);
if (decision.clearPendingSessionRestore) {
this.transitionFacadeState({type: 'sessionRestore.clear'});
}
if (decision.unavailableChannelId) {
this.clearUnavailableVoiceTarget(decision.unavailableChannelId, 'gateway-error', {
showToast: decision.showUnavailableToast,
});
}
if (decision.clearViewerStreamKeys) {
this.clearViewerStreamKeys();
}
if (decision.abortConnection) {
logger.info('Gateway voice error while connecting, aborting', {code: error.code});
this.abortVoiceConnection();
}
if (decision.disconnectReason) {
void this.disconnectFromVoiceChannel(decision.disconnectReason);
}
if (decision.toast) {
if (!this.i18n) {
throw new Error('MediaEngineFacade: i18n not initialized');
}
const descriptor =
decision.toast === 'timed-out'
? YOU_CAN_T_JOIN_WHILE_YOU_RE_ON_DESCRIPTOR
: decision.toast === 'connection-limit'
? VOICE_CONNECTION_LIMIT_REACHED_DESCRIPTOR
: CLAIM_YOUR_ACCOUNT_TO_JOIN_THIS_VOICE_CHANNEL_DESCRIPTOR;
this.showVoiceErrorModal(descriptor, `voice.media-engine-facade.gateway-${decision.toast}-error-modal`);
}
}
cleanup(): void {
this.terminalUnloadVoiceDisconnectSent = false;
this.cancelPendingServerDisconnect();
this.transitionFacadeState({type: 'cleanup.cleanupStarted'});
this.clearViewerStreamKeys();
this.stopTracking();
this.statsHostAdapter.cleanup();
VoiceEngineV2AppSubscriptionAdapter.cleanup();
VoiceEngineV2AppPermissionAdapter.reset();
this.resetLocalMediaAndScreenShareTracking();
voiceEngineV2AppConnectionHostAdapter.cleanup();
voiceEngineV2AppVoiceStateAdapter.clearAllVoiceStates();
this.voiceEngineV2Participants.clear();
voiceEngineV2AppMediaStateAdapter.resetLocalMediaState('cleanup');
this.transitionFacadeState({type: 'cleanup.complete'});
}
reset(): void {
this.terminalUnloadVoiceDisconnectSent = false;
this.cancelPendingServerDisconnect();
this.clearViewerStreamKeys();
this.statsHostAdapter.reset();
this.transitionFacadeState({type: 'cleanup.reset'});
voiceEngineV2AppConnectionHostAdapter.resetConnectionState();
voiceEngineV2AppConnectionHostAdapter.resetReconnectState();
VoiceEngineV2AppRemoteSpeakingAdapter.clear();
VoiceEngineV2AppPermissionAdapter.reset();
this.resetLocalMediaAndScreenShareTracking();
this.voiceEngineV2Participants.clear();
voiceEngineV2AppMediaStateAdapter.resetLocalMediaState('cleanup');
}
private async restoreLocalMediaState(roomOverride?: Room | null): Promise<void> {
const room = roomOverride ?? voiceEngineV2AppConnectionHostAdapter.room;
const participant = room?.localParticipant ?? null;
if (!participant) return;
const pendingScreenShareReconnect = this.pendingScreenShareReconnect;
this.transitionFacadeState({type: 'screenShareReconnect.consume'});
const shouldRestoreScreenShare =
(LocalVoiceState.getSelfStream() || pendingScreenShareReconnect !== null) && !participant.isScreenShareEnabled;
await this.restoreLocalMedia({
reason: 'voice room connected',
room,
restoreCamera: LocalVoiceState.getSelfVideo() && !participant.isCameraEnabled,
restoreStream: shouldRestoreScreenShare,
restoreStreamFromSnapshot: pendingScreenShareReconnect
? () =>
voiceEngineV2AppScreenShareExecutionAdapter.restoreScreenShareReconnect(room, pendingScreenShareReconnect)
: null,
streamSnapshotFallback: false,
streamPlaySound: true,
settleStreamStateWhenNotRestored: false,
toastWhenStreamNotRestored: false,
});
}
private async restorePendingSessionMedia(): Promise<void> {
const pendingRestore = this.pendingSessionRestore;
this.transitionFacadeState({type: 'sessionRestore.consume'});
if (!pendingRestore) {
return;
}
await this.restoreLocalMedia({
reason: 'startup voice restore',
room: this.room,
restoreCamera: pendingRestore.restoreVideo,
restoreStream: pendingRestore.restoreStream,
restoreStreamFromSnapshot: null,
streamSnapshotFallback: false,
streamPlaySound: true,
settleStreamStateWhenNotRestored: false,
toastWhenStreamNotRestored: true,
});
}
private navigateToVoiceChannel(guildId: string | null, channelId: string): void {
const targetGuildId = guildId ?? ME;
NavigationCommands.selectChannel(targetGuildId, channelId);
}
private clearViewerStreamKeys(): void {
if (LocalVoiceState.getViewerStreamKeys().length === 0) {
return;
}
replaceWatchedStreamKeys([], {sync: false});
}
}
const instance = new MediaEngineFacade();
export function useMediaEngineVersion(): number {
return useStoreVersion(instance);
}
export function getVoiceEngineV2Model(): VoiceEngineV2Model {
return instance.voiceEngineV2Model;
}
export function getVoiceEngineV2Snapshot(): VoiceEngineV2Snapshot {
return instance.voiceEngineV2Snapshot;
}
export function useVoiceEngineV2Model(): VoiceEngineV2Model {
useMediaEngineVersion();
return instance.voiceEngineV2Model;
}
export function useVoiceEngineV2Snapshot(): VoiceEngineV2Snapshot {
useMediaEngineVersion();
return instance.voiceEngineV2Snapshot;
}
(
window as typeof window & {
_mediaEngineFacade?: MediaEngineFacade;
}
)._mediaEngineFacade = instance;
export default instance;