// SPDX-License-Identifier: AGPL-3.0-or-later import assert from 'node:assert/strict'; import i18n from '@app/app/I18n'; import {GenericErrorModal} from '@app/features/app/components/alerts/GenericErrorModal'; import {SoundType} from '@app/features/notification/utils/SoundUtils'; import * as ModalCommands from '@app/features/ui/commands/ModalCommands'; import * as SoundCommands from '@app/features/ui/commands/SoundCommands'; import {getElectronAPI} from '@app/features/ui/utils/NativeUtils'; import * as VoiceSettingsCommands from '@app/features/voice/commands/VoiceSettingsCommands'; import {getStreamKey} from '@app/features/voice/components/StreamKeys'; import AdaptiveScreenShareEngine from '@app/features/voice/engine/AdaptiveScreenShareEngine'; import type {NegotiationReason} from '@app/features/voice/engine/ScreenShareCodecNegotiation'; import ScreenShareCodecNegotiation from '@app/features/voice/engine/ScreenShareCodecNegotiation'; import {Store} from '@app/features/voice/engine/Store'; import {getVoiceConnectionContextFromMediaEngine} from '@app/features/voice/engine/VoiceMediaEngineBridge'; import { normalizeVoiceMediaGraphViewerStreamKeys, selectVoiceMediaGraphViewerStreamKeys, } from '@app/features/voice/engine/VoiceMediaGraph'; import {voiceMediaGraphStore} from '@app/features/voice/engine/VoiceMediaGraphStore'; import {VoiceScreenShareLifecycleStore} from '@app/features/voice/engine/VoiceScreenShareLifecycleStore'; import type { VoiceScreenShareEvent, VoiceScreenShareSourceType, } from '@app/features/voice/engine/VoiceScreenShareStateMachine'; import {addWatchedStreamKey, stopWatchingStreamKey} from '@app/features/voice/engine/VoiceStreamWatchState'; import { getLocalScreenSharePublications, getLocalScreenShareVideoPublications, unpublishLocalMediaPublications, } from '@app/features/voice/engine/VoiceTrackPublicationUtils'; import {VoiceEngineV2AppScreenShareCodecMigration} from '@app/features/voice/engine/v2/VoiceEngineV2AppScreenShareCodecMigration'; import { selectVoiceEngineV2AppScreenShareSetEnabledOptions, type VoiceEngineV2AppScreenShareControllerGateway, VoiceEngineV2AppScreenShareControllerRouting, } from '@app/features/voice/engine/v2/VoiceEngineV2AppScreenShareControllerRouting'; import {VoiceEngineV2AppScreenShareLiveKitFlows} from '@app/features/voice/engine/v2/VoiceEngineV2AppScreenShareLiveKitFlows'; import { applyVoiceEngineV2AppScreenShareState, type VoiceScreenShareStateOptions, } from '@app/features/voice/engine/v2/VoiceEngineV2AppScreenShareStateSync'; import {VoiceEngineV2AppScreenShareTrackPlumbing} from '@app/features/voice/engine/v2/VoiceEngineV2AppScreenShareTrackPlumbing'; import type {VoiceEngineV2AppSourceLifecycleBridge} from '@app/features/voice/engine/v2/VoiceEngineV2AppSourceLifecycleBridge'; import { captureScreenSharePublicationCleanup, type DeviceScreenShareCaptureOptions, logger, mergeScreenShareCaptureCleanupSnapshots, releaseScreenShareCaptureCleanup, getEffectivePublishOptions as resolveEffectivePublishOptions, type ScreenShareCaptureCleanupSnapshot, type ScreenShareCodecReadinessStatus, type SimulcastTrackInfoLike, scheduleScreenShareEncoderVerification, stopMediaTrack, } from '@app/features/voice/engine/voice_screen_share_manager/shared'; import ActiveScreenShareSource from '@app/features/voice/state/ActiveScreenShareSource'; import LocalVoiceState from '@app/features/voice/state/LocalVoiceState'; import VoiceSettings from '@app/features/voice/state/VoiceSettings'; import { prepareHighFidelityScreenShareAudioTrack, SCREEN_SHARE_AUDIO_PUBLISH_OPTIONS, } from '@app/features/voice/utils/AudioPublishOptions'; import { markScreenShareCodecEncodeRuntimeFailure, type ScreenShareContentSource, } from '@app/features/voice/utils/CodecCapabilityDetector'; import {disarmVirtmic} from '@app/features/voice/utils/LinuxScreenShareAudio'; import { captureNativeAudioTrackForLinuxRouting, commitNativeAudioBridgeReplacement, disarmNativeAudio, } from '@app/features/voice/utils/NativeAudioCaptureBridge'; import type {NativeAudioStartOptions} from '@app/types/electron.d'; import type {VoiceEngineV2ScreenOptions} from '@fluxer/voice_engine_v2'; import {msg} from '@lingui/core/macro'; import { type LocalAudioTrack, type LocalParticipant, type LocalTrackPublication, type LocalVideoTrack, type Room, type ScreenShareCaptureOptions, Track, type TrackPublishOptions, type VideoCodec, } from 'livekit-client'; export type {DeviceScreenShareCaptureOptions} from '@app/features/voice/engine/voice_screen_share_manager/shared'; const SCREEN_SHARE_ENDED_MODAL_KEY = 'voice-screen-share-ended'; const SCREEN_SHARE_ENDED_DESCRIPTOR = msg({ message: 'Screen share ended', comment: 'Title of a modal shown when an active screen share stops unexpectedly.', context: 'screen-share', }); const SCREEN_SHARE_SOURCE_STOPPED_DESCRIPTOR = msg({ message: 'The screen share source stopped sending video, so your screen share was stopped.', comment: 'Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls.', context: 'screen-share', }); export interface ScreenShareReconnectSnapshot { videoTrack: MediaStreamTrack; audioTrack?: MediaStreamTrack; audioMuted: boolean; contentSource: ScreenShareContentSource; } const selectScreenShareSetEnabledOptions = selectVoiceEngineV2AppScreenShareSetEnabledOptions; class VoiceEngineV2AppScreenShareExecutionAdapter extends Store { private readonly lifecycle: VoiceScreenShareLifecycleStore; private readonly trackPlumbing: VoiceEngineV2AppScreenShareTrackPlumbing; private activeScreenShareEndListener: (() => void) | null = null; private endedScreenShareStopInFlight: Promise | null = null; encoderVerificationTimer: NodeJS.Timeout | null = null; screenShareMigrationGeneration = 0; sourceLifecycleBridge: VoiceEngineV2AppSourceLifecycleBridge | null = null; readonly codecMigration: VoiceEngineV2AppScreenShareCodecMigration; readonly liveKitFlows: VoiceEngineV2AppScreenShareLiveKitFlows; readonly controllerRouting: VoiceEngineV2AppScreenShareControllerRouting; constructor() { super(); this.lifecycle = new VoiceScreenShareLifecycleStore({update: (fn) => this.update(fn)}); this.trackPlumbing = new VoiceEngineV2AppScreenShareTrackPlumbing({ getActiveContentSource: () => this.getActiveScreenShareContentSourceInternal(), }); this.codecMigration = new VoiceEngineV2AppScreenShareCodecMigration(this); this.liveKitFlows = new VoiceEngineV2AppScreenShareLiveKitFlows(this); this.controllerRouting = new VoiceEngineV2AppScreenShareControllerRouting(this); } setControllerGateway(gateway: VoiceEngineV2AppScreenShareControllerGateway | null): void { this.controllerRouting.setGateway(gateway); } get isScreenSharePending(): boolean { return this.lifecycle.pendingOperationActive; } get streamingPriorityHeld(): boolean { return this.lifecycle.streamingPriorityHeld; } getIsScreenSharePending(): boolean { return this.isScreenSharePending; } setSourceLifecycleBridge(bridge: VoiceEngineV2AppSourceLifecycleBridge | null): void { this.sourceLifecycleBridge = bridge; } stopNativeScreenShareForTerminalUnload(): void { if (this.streamingPriorityHeld) { this.transitionScreenShareLifecycleInternal({type: 'share.streamingPriority.set', active: false}); try { getElectronAPI()?.releaseStreamingPriority?.(); } catch (error) { logger.warn('Failed to release streaming priority during terminal unload', {error}); } } } setStreamingPriorityInternal(active: boolean): void { if (active === this.streamingPriorityHeld) return; this.transitionScreenShareLifecycleInternal({type: 'share.streamingPriority.set', active}); try { const api = getElectronAPI(); if (active) { api?.acquireStreamingPriority?.(); } else { api?.releaseStreamingPriority?.(); } } catch (error) { logger.warn('Failed to update native streaming priority', {active, error}); } } ensureScreenShareKeepAliveSinkInternal(participant: LocalParticipant, preferredTrack?: LocalVideoTrack): void { this.trackPlumbing.ensureKeepAliveSink(participant, preferredTrack); } clearScreenShareKeepAliveSinkInternal(): void { this.trackPlumbing.clearKeepAliveSink(); } isScreenShareCodecRepublishInFlight(): boolean { return this.lifecycle.codecRepublishInFlight; } transitionScreenShareLifecycleInternal(event: VoiceScreenShareEvent): void { this.lifecycle.transition(event); } private transitionScreenShareCodecReadiness(status: ScreenShareCodecReadinessStatus): void { this.transitionScreenShareLifecycleInternal({ type: status === 'loading' ? 'share.codecReadiness.loading' : status === 'ready' ? 'share.codecReadiness.ready' : 'share.codecReadiness.timeout', }); } async getEffectivePublishOptionsInternal( enabled: boolean, publishOptions?: TrackPublishOptions, ): Promise { return resolveEffectivePublishOptions(enabled, publishOptions, { onCodecReadiness: (status) => this.transitionScreenShareCodecReadiness(status), }); } queuePendingStopRequestInternal(options?: {sendUpdate?: boolean; playSound?: boolean}): void { this.lifecycle.queueStopRequest(options); } queuePendingCodecRepublishRequestInternal( codec: VideoCodec, reason: NegotiationReason, options: {force?: boolean} = {}, ): void { this.lifecycle.queueCodecRepublishRequest(codec, reason, options); } deferActiveCodecRepublishRequestInternal( codec: VideoCodec, reason: NegotiationReason, options: {force?: boolean} = {}, ): void { this.lifecycle.deferCodecRepublishRequest(codec, reason, options); } async applyPendingScreenShareRequestsInternal(room: Room | null, participant: LocalParticipant): Promise { await this.lifecycle.drainQueuedRequests({ isScreenShareEnabled: () => participant.isScreenShareEnabled, applyStop: (request) => this.setScreenShareEnabled(room, false, request), applyCodecRepublish: async (request) => { await this.renegotiateActiveScreenShareCodec(room, request.codec, request.reason, {force: request.force}); }, }); } getActiveScreenShareContentSourceInternal(): ScreenShareContentSource { const sourceId = ActiveScreenShareSource.getSourceId(); if (sourceId?.startsWith('window:')) return 'app'; return 'display'; } getActiveScreenShareSourceTypeInternal(): VoiceScreenShareSourceType { const contentSource = this.getActiveScreenShareContentSourceInternal(); if (contentSource === 'device') return 'device'; return 'display'; } getScreenShareSourceTypeForContentSourceInternal( contentSource: ScreenShareContentSource, ): VoiceScreenShareSourceType { if (contentSource === 'device') return 'device'; return 'display'; } applyScreenShareContentHintInternal( participant: LocalParticipant, contentSource: ScreenShareContentSource = this.getActiveScreenShareContentSourceInternal(), preferredTrack?: LocalVideoTrack, ): void { this.trackPlumbing.applyContentHint(participant, contentSource, preferredTrack); } applyScreenShareContentHintToMediaTrackInternal( mediaStreamTrack: MediaStreamTrack | undefined, contentSource: ScreenShareContentSource = this.getActiveScreenShareContentSourceInternal(), ): void { this.trackPlumbing.applyContentHintToMediaTrack(mediaStreamTrack, contentSource); } async enforceScreenShareSenderParametersInternal( participant: LocalParticipant, publishOptions?: TrackPublishOptions, ): Promise { await this.trackPlumbing.enforceSenderParameters(participant, publishOptions); } async enforceScreenShareTrackSenderParametersInternal( track: LocalVideoTrack | undefined, publishOptions?: TrackPublishOptions, ): Promise { await this.trackPlumbing.enforceTrackSenderParameters(track, publishOptions); } bindScreenShareSenderParameterReapplyInternal( participant: LocalParticipant, publishOptions?: TrackPublishOptions, preferredTrack?: LocalVideoTrack, ): void { this.trackPlumbing.bindSenderParameterReapply(participant, publishOptions, preferredTrack); } applyScreenShareAudioContentHintInternal(participant: LocalParticipant): void { this.trackPlumbing.applyAudioContentHint(participant); } private getLocalStreamKey(): string | null { const connection = getVoiceConnectionContextFromMediaEngine(); const {guildId, channelId, connectionId} = connection ?? {}; if (!connectionId) { logger.debug('Skipping local stream watcher sync without an active voice connection', { connectionState: connection, }); return null; } return getStreamKey(guildId ?? null, channelId ?? null, connectionId); } applyScreenShareStateInternal(enabled: boolean, options: VoiceScreenShareStateOptions): void { applyVoiceEngineV2AppScreenShareState(enabled, options); } prepareScreenShareReconnect(room: Room | null): ScreenShareReconnectSnapshot | null { const participant = room?.localParticipant; if (!participant?.isScreenShareEnabled) { return null; } const screenSharePublication = participant.getTrackPublication(Track.Source.ScreenShare); const videoTrack = screenSharePublication?.videoTrack?.mediaStreamTrack; if (!videoTrack || videoTrack.readyState === 'ended') { return null; } const screenShareAudioPublication = participant.getTrackPublication(Track.Source.ScreenShareAudio); const audioTrack = screenShareAudioPublication?.audioTrack?.mediaStreamTrack ?? (screenShareAudioPublication?.track as LocalAudioTrack | undefined)?.mediaStreamTrack; const liveAudioTrack = audioTrack && audioTrack.readyState !== 'ended' ? audioTrack : undefined; return { videoTrack, ...(liveAudioTrack ? {audioTrack: liveAudioTrack} : {}), audioMuted: screenShareAudioPublication?.isMuted ?? false, contentSource: this.getActiveScreenShareContentSourceInternal(), }; } syncLocalScreenShareAudioStateInternal(participant: LocalParticipant, enabled: boolean): void { const hasAudioTrack = Boolean(participant.getTrackPublication(Track.Source.ScreenShareAudio)); LocalVoiceState.updateSelfStreamAudio(enabled && hasAudioTrack); } syncPersistedScreenShareAudioPreferenceInternal(participant: LocalParticipant): void { const screenShareAudioPublication = participant.getTrackPublication(Track.Source.ScreenShareAudio); const muteStreamAudio = !(screenShareAudioPublication && !screenShareAudioPublication.isMuted); if (VoiceSettings.getMuteStreamAudio() !== muteStreamAudio) { VoiceSettings.updateSettings({muteStreamAudio}); } } async unmuteScreenShareAudioPublicationInternal(participant: LocalParticipant, reason: string): Promise { const publication = participant.getTrackPublication(Track.Source.ScreenShareAudio); if (!publication || !publication.isMuted) { return; } try { await publication.unmute(); } catch (error) { logger.warn('Failed to unmute screen-share audio publication', {error, reason}); } } private cleanupScreenShareAudioRoutingState(): void { this.cleanupScreenShareAudioCaptureRouting(); ActiveScreenShareSource.clear(); } private cleanupScreenShareAudioCaptureRouting(): void { void getElectronAPI() ?.virtmic?.stop() ?.catch((error) => { logger.warn('Failed to stop virtmic during screen-share audio routing cleanup', {error}); }); disarmVirtmic(); disarmNativeAudio(); } getScreenShareCaptureCleanupSnapshotInternal(participant: LocalParticipant): ScreenShareCaptureCleanupSnapshot { return captureScreenSharePublicationCleanup(...getLocalScreenSharePublications(participant)); } private async releaseScreenShareCapture( participant: LocalParticipant, snapshot?: ScreenShareCaptureCleanupSnapshot, ): Promise { await releaseScreenShareCaptureCleanup( mergeScreenShareCaptureCleanupSnapshots(snapshot, this.getScreenShareCaptureCleanupSnapshotInternal(participant)), ); } getScreenShareSimulcastCleanupSnapshotInternal(screenShareTrack: LocalVideoTrack): ScreenShareCaptureCleanupSnapshot { const snapshot: ScreenShareCaptureCleanupSnapshot = {mediaTracks: [], senders: []}; const simulcastCodecs = ( screenShareTrack as LocalVideoTrack & { simulcastCodecs?: Map; } ).simulcastCodecs; for (const simulcastTrackInfo of simulcastCodecs?.values() ?? []) { snapshot.mediaTracks.push(simulcastTrackInfo.mediaStreamTrack); if (simulcastTrackInfo.sender) { snapshot.senders.push(simulcastTrackInfo.sender); } } return snapshot; } cleanupActiveScreenShareEndListenerInternal(): void { this.activeScreenShareEndListener?.(); this.activeScreenShareEndListener = null; } cancelEncoderVerificationInternal(): void { if (this.encoderVerificationTimer != null) { clearTimeout(this.encoderVerificationTimer); this.encoderVerificationTimer = null; } this.trackPlumbing.cleanupKeyFrameRequests(); this.transitionScreenShareLifecycleInternal({type: 'share.encoderVerification.cleared'}); } showScreenShareEndedModalInternal(description: string): void { ModalCommands.pushWithKey( ModalCommands.modal(() => ( )), SCREEN_SHARE_ENDED_MODAL_KEY, ); } startEncoderVerificationInternal( room: Room | null, participant: LocalParticipant, codec?: VideoCodec, preferredTrack?: LocalVideoTrack, ): void { this.cancelEncoderVerificationInternal(); if (!codec) return; const publication = preferredTrack ? undefined : participant.getTrackPublication(Track.Source.ScreenShare); const track = preferredTrack ?? (publication?.videoTrack as LocalVideoTrack | undefined); const sender = track?.sender; if (!sender) { logger.warn('No sender found for screen share encoder verification'); return; } this.encoderVerificationTimer = scheduleScreenShareEncoderVerification( () => sender.getStats(), codec, (failure) => { const failureReason = failure.reason === 'codec-mismatch' ? 'screen-share-codec-mismatch' : 'screen-share-encode-stalled'; if (!markScreenShareCodecEncodeRuntimeFailure(failure.codec, failureReason)) return; void this.recoverActiveScreenShareAfterEncoderFailure(room, failure.codec).catch((error) => { logger.warn('Failed to recover screen share after encoder verification failure', { error, codec: failure.codec, failureReason, }); }); }, ); this.trackPlumbing.bindKeyFrameRequests(room, participant, track); this.transitionScreenShareLifecycleInternal({type: 'share.encoderVerification.scheduled'}); } private async recoverActiveScreenShareAfterEncoderFailure(room: Room | null, failedCodec: VideoCodec): Promise { await ScreenShareCodecNegotiation.publishLocalCapabilities(room, 'manual', {}); const codec = ScreenShareCodecNegotiation.selectScreenShareCodec(VoiceSettings.getPreferredScreenShareCodec()); if (codec === failedCodec) return; await this.renegotiateActiveScreenShareCodec(room, codec, 'manual', {force: true}); } monitorActiveScreenShareEndInternal( room: Room | null, participant: LocalParticipant, preferredTrack?: LocalVideoTrack, ): void { this.cleanupActiveScreenShareEndListenerInternal(); const publication = preferredTrack ? undefined : participant.getTrackPublication(Track.Source.ScreenShare); const videoTrack = preferredTrack ?? publication?.videoTrack; const mediaStreamTrack = videoTrack?.mediaStreamTrack; if (!mediaStreamTrack) { return; } const isCurrentScreenShareTrack = (): boolean => getLocalScreenShareVideoPublications(participant).some((screenSharePublication) => { const track = (screenSharePublication.videoTrack as LocalVideoTrack | undefined) ?? (screenSharePublication.track as LocalVideoTrack | undefined); return track === videoTrack && track.mediaStreamTrack === mediaStreamTrack; }); const stopEndedScreenShare = (trigger: string): void => { if (this.endedScreenShareStopInFlight) return; if (!isCurrentScreenShareTrack()) { return; } logger.info('Screen share media track ended; disabling screen share', { trigger, readyState: mediaStreamTrack.readyState, }); this.showScreenShareEndedModalInternal(i18n._(SCREEN_SHARE_SOURCE_STOPPED_DESCRIPTOR)); this.transitionScreenShareLifecycleInternal({type: 'share.endedStop.start'}); const stopPromise = this.setScreenShareEnabled(room, false, {sendUpdate: true, playSound: true}) .catch((error) => { logger.warn('Failed to disable screen share after media track ended', {error}); }) .finally(() => { if (this.endedScreenShareStopInFlight === stopPromise) { this.endedScreenShareStopInFlight = null; } this.transitionScreenShareLifecycleInternal({type: 'share.endedStop.finish'}); }); this.endedScreenShareStopInFlight = stopPromise; }; const onEnded = (): void => stopEndedScreenShare('ended-event'); mediaStreamTrack.addEventListener('ended', onEnded); this.activeScreenShareEndListener = () => { mediaStreamTrack.removeEventListener('ended', onEnded); }; if (mediaStreamTrack.readyState === 'ended') { queueMicrotask(() => stopEndedScreenShare('already-ended')); } } async restoreScreenShareReconnect( room: Room | null, snapshot: ScreenShareReconnectSnapshot, publishOptions?: TrackPublishOptions, ): Promise { return this.liveKitFlows.restoreReconnect(room, snapshot, publishOptions); } async cleanupLingeringScreenShareTracks( participant: LocalParticipant, snapshot?: ScreenShareCaptureCleanupSnapshot, ): Promise { this.clearScreenShareKeepAliveSinkInternal(); this.trackPlumbing.cleanupSenderParameterReapply(); this.cleanupScreenShareAudioRoutingState(); const publications = getLocalScreenSharePublications(participant); const cleanupSnapshot = mergeScreenShareCaptureCleanupSnapshots( snapshot, captureScreenSharePublicationCleanup(...publications), ); const cleanupResult = await unpublishLocalMediaPublications(participant, publications); for (const failure of cleanupResult.failedPublications) { logger.warn('Failed to unpublish lingering screen share track', { error: failure.error, source: failure.publication.source, }); } await this.releaseScreenShareCapture(participant, cleanupSnapshot); } handleLocalScreenShareTrackUnpublished(room: Room, playSound: boolean, publication?: LocalTrackPublication): void { this.clearScreenShareKeepAliveSinkInternal(); this.cleanupActiveScreenShareEndListenerInternal(); AdaptiveScreenShareEngine.stop(); const participant = room.localParticipant; const cleanupSnapshot = captureScreenSharePublicationCleanup( publication, participant.getTrackPublication(Track.Source.ScreenShareAudio), ); this.syncLocalStreamWatchStateInternal(false); this.syncLocalScreenShareAudioStateInternal(participant, false); if (!this.isScreenSharePending) { this.applyScreenShareStateInternal(false, {reason: 'user', sendUpdate: true}); } void this.cleanupLingeringScreenShareTracks(participant, cleanupSnapshot).catch((error) => { logger.warn('Failed to clean up screen-share audio after video unpublish', {error}); }); if (playSound && !this.isScreenSharePending) { SoundCommands.playSound(SoundType.ScreenShareStop); } } syncLocalStreamWatchStateInternal(enabled: boolean): void { const streamKey = this.getLocalStreamKey(); if (!streamKey) { return; } const graphKeys = selectVoiceMediaGraphViewerStreamKeys(voiceMediaGraphStore.getGraphSnapshot()); const current = graphKeys.length > 0 ? graphKeys : normalizeVoiceMediaGraphViewerStreamKeys(LocalVoiceState.getViewerStreamKeys()); this.transitionScreenShareLifecycleInternal({ type: 'share.localWatcher.sync', enabled, streamKey, currentViewerStreamKeys: current, }); for (const command of this.lifecycle.snapshot.context.watchCommands) { if (command.type === 'watch.add') { addWatchedStreamKey(command.key); } else { stopWatchingStreamKey(command.key, {clearPinned: false}); } } this.transitionScreenShareLifecycleInternal({type: 'share.clearWatchCommands'}); if (!enabled && !current.includes(streamKey)) { logger.debug('Local stream watcher already absent while disabling screen share', { current, expected: streamKey, }); } } async replaceActiveScreenShareAudioTrackInternal( participant: LocalParticipant, audioTrack: MediaStreamTrack | undefined, ): Promise { const screenShareAudioPublication = participant.getTrackPublication(Track.Source.ScreenShareAudio); const existingAudioTrack = screenShareAudioPublication?.audioTrack ?? (screenShareAudioPublication?.track as LocalAudioTrack | undefined); const previousAudioMediaTrack = existingAudioTrack?.mediaStreamTrack; if (audioTrack) { prepareHighFidelityScreenShareAudioTrack(audioTrack); if (existingAudioTrack) { await existingAudioTrack.replaceTrack(audioTrack, false); if (previousAudioMediaTrack && previousAudioMediaTrack !== existingAudioTrack.mediaStreamTrack) { stopMediaTrack(previousAudioMediaTrack); } await this.unmuteScreenShareAudioPublicationInternal(participant, 'replace screen-share audio track'); return true; } await participant.publishTrack(audioTrack, SCREEN_SHARE_AUDIO_PUBLISH_OPTIONS); await this.unmuteScreenShareAudioPublicationInternal(participant, 'publish screen-share audio track'); return true; } this.cleanupScreenShareAudioCaptureRouting(); if (existingAudioTrack) { await participant.unpublishTrack(existingAudioTrack); stopMediaTrack(previousAudioMediaTrack); } return false; } async ensureLinuxScreenShareAudioPublication( room: Room | null, linuxRule?: NonNullable, options: {includeSelfWindowAudio?: boolean; replaceExisting?: boolean} = {}, ): Promise { const participant = room?.localParticipant; if (!participant || !participant.isScreenShareEnabled) return false; if (!linuxRule) return false; const capturedTrack = await captureNativeAudioTrackForLinuxRouting(linuxRule, options); if (!capturedTrack) return false; let adopted = false; try { adopted = await this.replaceActiveScreenShareAudioTrackInternal(participant, capturedTrack); if (adopted) { commitNativeAudioBridgeReplacement(); } } catch (error) { logger.warn('Failed to publish mid-stream Linux native screen-share audio track', {error}); if (!adopted) { try { capturedTrack.stop(); } catch (stopError) { logger.warn('Failed to stop rejected Linux native screen-share audio track', {error: stopError}); } } return false; } this.syncLocalScreenShareAudioStateInternal(participant, true); this.syncPersistedScreenShareAudioPreferenceInternal(participant); return true; } async setScreenShareEnabled( room: Room | null, enabled: boolean, options?: ScreenShareCaptureOptions & { sendUpdate?: boolean; playSound?: boolean; restartIfEnabled?: boolean; reason?: string; preserveStreamAudioPreferences?: boolean; }, publishOptions?: TrackPublishOptions, ): Promise { assert.equal(typeof enabled, 'boolean'); await this.controllerRouting.setEnabled(room, enabled, options, publishOptions); if (!enabled && options?.preserveStreamAudioPreferences !== true) { VoiceSettingsCommands.update({ shareAppAudio: true, shareDesktopAudio: true, shareDeviceAudio: true, muteStreamAudio: false, }); } } async executeScreenShareSetEnabledDirect( room: Room | null, enabled: boolean, options?: ScreenShareCaptureOptions & { sendUpdate?: boolean; playSound?: boolean; restartIfEnabled?: boolean; reason?: string; }, publishOptions?: TrackPublishOptions, ): Promise { assert.equal(typeof enabled, 'boolean'); const selection = selectScreenShareSetEnabledOptions(options); await this.liveKitFlows.setEnabled( room, enabled, { ...selection.captureOptions, sendUpdate: selection.sendUpdate, playSound: selection.playSound, restartIfEnabled: selection.restartIfEnabled, }, publishOptions, ); } async startDeviceScreenShare( room: Room | null, options?: DeviceScreenShareCaptureOptions, publishOptions?: TrackPublishOptions, ): Promise { await this.liveKitFlows.startDeviceScreenShare(room, options, publishOptions); } async replaceActiveDisplayScreenShare( room: Room | null, options?: ScreenShareCaptureOptions, publishOptions?: TrackPublishOptions, ): Promise { return this.liveKitFlows.replaceActiveDisplayShare(room, options, publishOptions); } async replaceActiveDeviceScreenShare( room: Room | null, options?: DeviceScreenShareCaptureOptions, publishOptions?: TrackPublishOptions, ): Promise { return this.liveKitFlows.replaceActiveDeviceShare(room, options, publishOptions); } async applyPendingScreenShareRequestsForRoom(room: Room | null): Promise { const participant = room?.localParticipant; if (!participant) return; await this.applyPendingScreenShareRequestsInternal(room, participant); } stopMediaTrackInternal(track: MediaStreamTrack | undefined): void { stopMediaTrack(track); } async publishControllerScreenViaLiveKitFlows(room: Room | null, options: VoiceEngineV2ScreenOptions): Promise { if (typeof options.captureId !== 'string' || options.captureId.length === 0) { throw new Error('Controller screen-share publish requires a captureId'); } await this.controllerRouting.publishViaLiveKitFlows(room, options); } async unpublishControllerScreenViaLiveKitFlows(room: Room | null): Promise { await this.controllerRouting.unpublishViaLiveKitFlows(room); } async updateActiveScreenShareSettings( room: Room | null, options?: ScreenShareCaptureOptions, publishOptions?: TrackPublishOptions, ): Promise { return this.codecMigration.updateActiveSettings(room, options, publishOptions); } async renegotiateActiveScreenShareCodec( room: Room | null, codec: VideoCodec, reason: NegotiationReason, options: {force?: boolean} = {}, ): Promise { return this.codecMigration.renegotiateActiveCodec(room, codec, reason, options); } setScreenShareAudioMuted(room: Room | null, muted: boolean): void { assert.equal(typeof muted, 'boolean'); const participant = room?.localParticipant; if (!participant) return; const publication = participant.getTrackPublication(Track.Source.ScreenShareAudio); if (!publication) return; const operation = muted ? publication.mute() : publication.unmute(); operation.catch((error) => { logger.warn('Failed to apply immediate screen share audio mute', {error, muted}); }); this.syncLocalScreenShareAudioStateInternal(participant, !muted); } async toggleScreenShareFromKeybind(room: Room | null): Promise { const current = LocalVoiceState.getSelfStream(); await this.setScreenShareEnabled(room, !current); } resetStreamTracking(): void { this.clearScreenShareKeepAliveSinkInternal(); this.setStreamingPriorityInternal(false); this.transitionScreenShareLifecycleInternal({type: 'share.reset'}); this.cleanupActiveScreenShareEndListenerInternal(); this.cancelEncoderVerificationInternal(); AdaptiveScreenShareEngine.stop(); this.endedScreenShareStopInFlight = null; this.cleanupScreenShareAudioRoutingState(); } } export {VoiceEngineV2AppScreenShareExecutionAdapter}; export default new VoiceEngineV2AppScreenShareExecutionAdapter();