/* eslint-disable, @typescript-eslint/no-non-null-assertion, @typescript-eslint/member-ordering, id-denylist */ /** * Manages screen sharing: getDisplayMedia / Electron desktop capturer, * system-audio capture, and attaching screen tracks to peers. */ import { WebRTCLogger } from './webrtc-logger'; import { PeerData } from './webrtc.types'; import { TRACK_KIND_AUDIO, TRACK_KIND_VIDEO, TRANSCEIVER_SEND_RECV, TRANSCEIVER_RECV_ONLY, ELECTRON_ENTIRE_SCREEN_SOURCE_NAME } from './webrtc.constants'; import { DEFAULT_SCREEN_SHARE_START_OPTIONS, SCREEN_SHARE_QUALITY_PRESETS, ScreenShareQualityPreset, ScreenShareStartOptions } from './screen-share.config'; /** * Callbacks the ScreenShareManager needs from the owning service. */ export interface ScreenShareCallbacks { getActivePeers(): Map; getLocalMediaStream(): MediaStream | null; renegotiate(peerId: string): Promise; broadcastCurrentStates(): void; } interface LinuxScreenShareAudioRoutingInfo { available: boolean; active: boolean; monitorCaptureSupported: boolean; screenShareSinkName: string; screenShareMonitorSourceName: string; voiceSinkName: string; reason?: string; } interface LinuxScreenShareMonitorCaptureInfo { bitsPerSample: number; captureId: string; channelCount: number; sampleRate: number; sourceName: string; } interface LinuxScreenShareMonitorAudioChunkPayload { captureId: string; chunk: Uint8Array; } interface LinuxScreenShareMonitorAudioEndedPayload { captureId: string; reason?: string; } interface LinuxScreenShareMonitorAudioPipeline { audioContext: AudioContext; audioTrack: MediaStreamTrack; bitsPerSample: number; captureId: string; channelCount: number; mediaDestination: MediaStreamAudioDestinationNode; nextStartTime: number; pendingBytes: Uint8Array; sampleRate: number; unsubscribeChunk: () => void; unsubscribeEnded: () => void; } interface DesktopSource { id: string; name: string; thumbnail: string; } interface ScreenShareElectronApi { getSources?: () => Promise; prepareLinuxScreenShareAudioRouting?: () => Promise; activateLinuxScreenShareAudioRouting?: () => Promise; deactivateLinuxScreenShareAudioRouting?: () => Promise; startLinuxScreenShareMonitorCapture?: () => Promise; stopLinuxScreenShareMonitorCapture?: (captureId?: string) => Promise; onLinuxScreenShareMonitorAudioChunk?: (listener: (payload: LinuxScreenShareMonitorAudioChunkPayload) => void) => () => void; onLinuxScreenShareMonitorAudioEnded?: (listener: (payload: LinuxScreenShareMonitorAudioEndedPayload) => void) => () => void; } type ElectronDesktopVideoConstraint = MediaTrackConstraints & { mandatory: { chromeMediaSource: 'desktop'; chromeMediaSourceId: string; maxWidth: number; maxHeight: number; maxFrameRate: number; }; }; type ElectronDesktopAudioConstraint = MediaTrackConstraints & { mandatory: { chromeMediaSource: 'desktop'; chromeMediaSourceId: string; }; }; interface ElectronDesktopMediaStreamConstraints extends MediaStreamConstraints { video: ElectronDesktopVideoConstraint; audio?: false | ElectronDesktopAudioConstraint; } type ScreenShareWindow = Window & { electronAPI?: ScreenShareElectronApi; }; export class ScreenShareManager { /** The active screen-capture stream. */ private activeScreenStream: MediaStream | null = null; /** Optional system-audio stream captured alongside the screen. */ private screenAudioStream: MediaStream | null = null; /** The quality preset currently applied to the active share. */ private activeScreenPreset: ScreenShareQualityPreset | null = null; /** Remote peers that explicitly requested screen-share video. */ private readonly requestedViewerPeerIds = new Set(); /** Whether screen sharing is currently active. */ private isScreenActive = false; /** Whether Linux-specific Electron audio routing is currently active. */ private linuxElectronAudioRoutingActive = false; /** Pending teardown of Linux-specific Electron audio routing. */ private linuxAudioRoutingResetPromise: Promise | null = null; /** Renderer-side audio pipeline for Linux monitor-source capture. */ private linuxMonitorAudioPipeline: LinuxScreenShareMonitorAudioPipeline | null = null; constructor( private readonly logger: WebRTCLogger, private callbacks: ScreenShareCallbacks ) {} /** * Replace the callback set at runtime. * Needed because of circular initialisation between managers. * * @param nextCallbacks - The new callback interface to wire into this manager. */ setCallbacks(nextCallbacks: ScreenShareCallbacks): void { this.callbacks = nextCallbacks; } /** Returns the current screen-capture stream, or `null` if inactive. */ getScreenStream(): MediaStream | null { return this.activeScreenStream; } /** Whether screen sharing is currently active. */ getIsScreenActive(): boolean { return this.isScreenActive; } /** * Begin screen sharing. * * On Linux Electron builds, prefers a dedicated PulseAudio/PipeWire routing * path so remote voice playback is kept out of captured system audio. * Otherwise prefers `getDisplayMedia` when system audio is requested so the * browser can filter MeToYou's own playback via `restrictOwnAudio`, then * falls back to Electron desktop capture when needed. * * @param options - Screen-share capture options. * @returns The captured screen {@link MediaStream}. * @throws If both Electron and browser screen capture fail. */ async startScreenShare(options: ScreenShareStartOptions = DEFAULT_SCREEN_SHARE_START_OPTIONS): Promise { const shareOptions = { ...DEFAULT_SCREEN_SHARE_START_OPTIONS, ...options }; const preset = SCREEN_SHARE_QUALITY_PRESETS[shareOptions.quality]; try { this.logger.info('startScreenShare invoked', shareOptions); if (this.activeScreenStream) { this.stopScreenShare(); } await this.awaitPendingLinuxAudioRoutingReset(); this.activeScreenStream = null; if (shareOptions.includeSystemAudio && this.isLinuxElectronAudioRoutingSupported()) { try { this.activeScreenStream = await this.startWithLinuxElectronAudioRouting(shareOptions, preset); } catch (error) { this.logger.warn('Linux Electron audio routing failed; falling back to standard capture', error); } } if (!this.activeScreenStream && shareOptions.includeSystemAudio) { try { this.activeScreenStream = await this.startWithDisplayMedia(shareOptions, preset); if (this.activeScreenStream.getAudioTracks().length === 0) { this.logger.warn('getDisplayMedia did not provide system audio; trying Electron desktop capture'); this.activeScreenStream.getTracks().forEach((track) => track.stop()); this.activeScreenStream = null; } } catch (error) { this.logger.warn('getDisplayMedia with system audio failed; falling back to Electron desktop capture', error); } } if (!this.activeScreenStream && this.getElectronApi()?.getSources) { try { this.activeScreenStream = await this.startWithElectronDesktopCapturer(shareOptions, preset); } catch (error) { this.logger.warn('Electron desktop capture failed; falling back to getDisplayMedia', error); } } if (!this.activeScreenStream) { this.activeScreenStream = await this.startWithDisplayMedia(shareOptions, preset); } this.configureScreenStream(preset); this.prepareScreenAudio(shareOptions.includeSystemAudio); this.activeScreenPreset = preset; this.attachScreenTracksToPeers(preset); this.isScreenActive = true; this.callbacks.broadcastCurrentStates(); const activeScreenStream = this.activeScreenStream; if (!activeScreenStream) { throw new Error('Screen sharing did not produce an active stream.'); } const screenVideoTrack = activeScreenStream.getVideoTracks()[0]; if (screenVideoTrack) { screenVideoTrack.onended = () => { this.logger.warn('Screen video track ended'); this.stopScreenShare(); }; } return activeScreenStream; } catch (error) { this.logger.error('Failed to start screen share', error); throw error; } } /** * Stop screen sharing and remove screen-share tracks on all peers. * * Stops all screen-capture tracks, resets screen transceivers to receive-only, * and triggers renegotiation. */ stopScreenShare(): void { if (this.activeScreenStream) { this.activeScreenStream.getTracks().forEach((track) => track.stop()); this.activeScreenStream = null; } this.scheduleLinuxAudioRoutingReset(); this.screenAudioStream = null; this.activeScreenPreset = null; this.isScreenActive = false; this.callbacks.broadcastCurrentStates(); this.callbacks.getActivePeers().forEach((peerData, peerId) => { this.detachScreenTracksFromPeer(peerData, peerId); }); } requestScreenShareForPeer(peerId: string): void { this.requestedViewerPeerIds.add(peerId); if (!this.isScreenActive || !this.activeScreenPreset) { return; } const peerData = this.callbacks.getActivePeers().get(peerId); if (!peerData) { return; } this.attachScreenTracksToPeer(peerData, peerId, this.activeScreenPreset); } stopScreenShareForPeer(peerId: string): void { this.requestedViewerPeerIds.delete(peerId); const peerData = this.callbacks.getActivePeers().get(peerId); if (!peerData) { return; } this.detachScreenTracksFromPeer(peerData, peerId); } clearScreenShareRequest(peerId: string): void { this.requestedViewerPeerIds.delete(peerId); } /** * Attach the current screen-share tracks to a newly-connected peer. * * This is needed when a peer connects after screen sharing already started, * because `startScreenShare()` only pushes tracks to peers that existed at * the time sharing began. */ syncScreenShareToPeer(peerId: string): void { if ( !this.requestedViewerPeerIds.has(peerId) || !this.isScreenActive || !this.activeScreenStream || !this.activeScreenPreset ) { return; } const peerData = this.callbacks.getActivePeers().get(peerId); if (!peerData) { return; } this.attachScreenTracksToPeer(peerData, peerId, this.activeScreenPreset); } /** Clean up all resources. */ destroy(): void { this.stopScreenShare(); } private getElectronApi(): ScreenShareElectronApi | null { return typeof window !== 'undefined' ? (window as ScreenShareWindow).electronAPI ?? null : null; } private getRequiredLinuxElectronApi(): Required> { const electronApi = this.getElectronApi(); if (!electronApi?.prepareLinuxScreenShareAudioRouting || !electronApi.activateLinuxScreenShareAudioRouting || !electronApi.deactivateLinuxScreenShareAudioRouting || !electronApi.startLinuxScreenShareMonitorCapture || !electronApi.stopLinuxScreenShareMonitorCapture || !electronApi.onLinuxScreenShareMonitorAudioChunk || !electronApi.onLinuxScreenShareMonitorAudioEnded) { throw new Error('Linux Electron audio routing is unavailable.'); } return { prepareLinuxScreenShareAudioRouting: electronApi.prepareLinuxScreenShareAudioRouting, activateLinuxScreenShareAudioRouting: electronApi.activateLinuxScreenShareAudioRouting, deactivateLinuxScreenShareAudioRouting: electronApi.deactivateLinuxScreenShareAudioRouting, startLinuxScreenShareMonitorCapture: electronApi.startLinuxScreenShareMonitorCapture, stopLinuxScreenShareMonitorCapture: electronApi.stopLinuxScreenShareMonitorCapture, onLinuxScreenShareMonitorAudioChunk: electronApi.onLinuxScreenShareMonitorAudioChunk, onLinuxScreenShareMonitorAudioEnded: electronApi.onLinuxScreenShareMonitorAudioEnded }; } private assertLinuxAudioRoutingReady( routingInfo: LinuxScreenShareAudioRoutingInfo, unavailableReason: string ): void { if (!routingInfo.available) { throw new Error(routingInfo.reason || unavailableReason); } if (!routingInfo.monitorCaptureSupported) { throw new Error('Linux screen-share monitor capture requires restarting the desktop app so the new Electron main process can load.'); } } /** * Create a dedicated stream for system audio captured alongside the screen. * * @param includeSystemAudio - Whether system audio should be sent. */ private prepareScreenAudio(includeSystemAudio: boolean): void { const screenAudioTrack = includeSystemAudio ? (this.activeScreenStream?.getAudioTracks()[0] || null) : null; if (!screenAudioTrack) { if (includeSystemAudio) { this.logger.warn('System audio was requested, but no screen audio track was captured'); } this.screenAudioStream = null; return; } this.screenAudioStream = new MediaStream([screenAudioTrack]); this.logger.attachTrackDiagnostics(screenAudioTrack, 'screenAudio'); this.logger.logStream('screenAudio', this.screenAudioStream); } /** * Attach screen video and optional system-audio tracks to all * active peer connections, then trigger SDP renegotiation. * * @param options - Screen-share capture options. * @param preset - Selected quality preset for sender tuning. */ private attachScreenTracksToPeers( preset: ScreenShareQualityPreset ): void { this.callbacks.getActivePeers().forEach((peerData, peerId) => { if (!this.requestedViewerPeerIds.has(peerId)) { return; } this.attachScreenTracksToPeer(peerData, peerId, preset); }); } private attachScreenTracksToPeer( peerData: PeerData, peerId: string, preset: ScreenShareQualityPreset ): void { if (!this.activeScreenStream) { return; } const screenVideoTrack = this.activeScreenStream.getVideoTracks()[0]; if (!screenVideoTrack) { return; } this.logger.attachTrackDiagnostics(screenVideoTrack, `screenVideo:${peerId}`); let videoSender = peerData.videoSender || peerData.connection.getSenders().find((sender) => sender.track?.kind === TRACK_KIND_VIDEO); if (!videoSender) { const videoTransceiver = peerData.connection.addTransceiver(TRACK_KIND_VIDEO, { direction: TRANSCEIVER_SEND_RECV }); videoSender = videoTransceiver.sender; peerData.videoSender = videoSender; } else { const videoTransceiver = peerData.connection.getTransceivers().find( (transceiver) => transceiver.sender === videoSender ); if (videoTransceiver?.direction === TRANSCEIVER_RECV_ONLY) { videoTransceiver.direction = TRANSCEIVER_SEND_RECV; } } peerData.screenVideoSender = videoSender; videoSender.replaceTrack(screenVideoTrack) .then(() => { this.logger.info('screen video replaceTrack ok', { peerId }); void this.applyScreenShareVideoParameters(videoSender, preset, peerId); }) .catch((error) => this.logger.error('screen video replaceTrack failed', error)); const screenAudioTrack = this.screenAudioStream?.getAudioTracks()[0] || null; if (screenAudioTrack) { this.logger.attachTrackDiagnostics(screenAudioTrack, `screenAudio:${peerId}`); let screenAudioSender = peerData.screenAudioSender; if (!screenAudioSender) { const screenAudioTransceiver = peerData.connection.addTransceiver(TRACK_KIND_AUDIO, { direction: TRANSCEIVER_SEND_RECV }); screenAudioSender = screenAudioTransceiver.sender; } else { const screenAudioTransceiver = peerData.connection.getTransceivers().find( (transceiver) => transceiver.sender === screenAudioSender ); if (screenAudioTransceiver?.direction === TRANSCEIVER_RECV_ONLY) { screenAudioTransceiver.direction = TRANSCEIVER_SEND_RECV; } } peerData.screenAudioSender = screenAudioSender; screenAudioSender.replaceTrack(screenAudioTrack) .then(() => this.logger.info('screen audio replaceTrack ok', { peerId })) .catch((error) => this.logger.error('screen audio replaceTrack failed', error)); } this.callbacks.renegotiate(peerId); } private detachScreenTracksFromPeer(peerData: PeerData, peerId: string): void { const transceivers = peerData.connection.getTransceivers(); const videoTransceiver = transceivers.find( (transceiver) => transceiver.sender === peerData.videoSender || transceiver.sender === peerData.screenVideoSender ); const screenAudioTransceiver = transceivers.find( (transceiver) => transceiver.sender === peerData.screenAudioSender ); if (videoTransceiver) { videoTransceiver.sender.replaceTrack(null).catch((error) => { this.logger.error('Failed to clear screen video sender track', error, { peerId }); }); if (videoTransceiver.direction === TRANSCEIVER_SEND_RECV) { videoTransceiver.direction = TRANSCEIVER_RECV_ONLY; } } if (screenAudioTransceiver) { screenAudioTransceiver.sender.replaceTrack(null).catch((error) => { this.logger.error('Failed to clear screen audio sender track', error, { peerId }); }); if (screenAudioTransceiver.direction === TRANSCEIVER_SEND_RECV) { screenAudioTransceiver.direction = TRANSCEIVER_RECV_ONLY; } } peerData.screenVideoSender = undefined; peerData.screenAudioSender = undefined; this.callbacks.renegotiate(peerId); } private async startWithDisplayMedia( options: ScreenShareStartOptions, preset: ScreenShareQualityPreset ): Promise { const displayConstraints = this.buildDisplayMediaConstraints(options, preset); this.logger.info('getDisplayMedia constraints', displayConstraints); if (!navigator.mediaDevices?.getDisplayMedia) { throw new Error('navigator.mediaDevices.getDisplayMedia is not available.'); } return await navigator.mediaDevices.getDisplayMedia(displayConstraints); } private async startWithElectronDesktopCapturer( options: ScreenShareStartOptions, preset: ScreenShareQualityPreset ): Promise { const electronApi = this.getElectronApi(); if (!electronApi?.getSources) { throw new Error('Electron desktop capture is unavailable.'); } const sources = await electronApi.getSources(); const screenSource = sources.find((source) => source.name === ELECTRON_ENTIRE_SCREEN_SOURCE_NAME) ?? sources[0]; if (!screenSource) { throw new Error('No desktop capture sources were available.'); } const electronConstraints = this.buildElectronDesktopConstraints(screenSource.id, options, preset); this.logger.info('desktopCapturer constraints', electronConstraints); if (!navigator.mediaDevices?.getUserMedia) { throw new Error('navigator.mediaDevices.getUserMedia is not available (requires HTTPS or localhost).'); } return await navigator.mediaDevices.getUserMedia(electronConstraints); } private isLinuxElectronAudioRoutingSupported(): boolean { if (typeof window === 'undefined' || typeof navigator === 'undefined') { return false; } const electronApi = this.getElectronApi(); const platformHint = `${navigator.userAgent} ${navigator.platform}`; return !!electronApi?.prepareLinuxScreenShareAudioRouting && !!electronApi?.activateLinuxScreenShareAudioRouting && !!electronApi?.deactivateLinuxScreenShareAudioRouting && !!electronApi?.startLinuxScreenShareMonitorCapture && !!electronApi?.stopLinuxScreenShareMonitorCapture && !!electronApi?.onLinuxScreenShareMonitorAudioChunk && !!electronApi?.onLinuxScreenShareMonitorAudioEnded && /linux/i.test(platformHint); } private async startWithLinuxElectronAudioRouting( options: ScreenShareStartOptions, preset: ScreenShareQualityPreset ): Promise { const electronApi = this.getRequiredLinuxElectronApi(); const routingInfo = await electronApi.prepareLinuxScreenShareAudioRouting(); this.assertLinuxAudioRoutingReady(routingInfo, 'Linux Electron audio routing is unavailable.'); let desktopStream: MediaStream | null = null; try { const activation = await electronApi.activateLinuxScreenShareAudioRouting(); this.assertLinuxAudioRoutingReady(activation, 'Failed to activate Linux Electron audio routing.'); if (!activation.active) { throw new Error(activation.reason || 'Failed to activate Linux Electron audio routing.'); } desktopStream = await this.startWithElectronDesktopCapturer({ ...options, includeSystemAudio: false }, preset); const { audioTrack, captureInfo } = await this.startLinuxScreenShareMonitorTrack(); const stream = new MediaStream([...desktopStream.getVideoTracks(), audioTrack]); desktopStream.getAudioTracks().forEach((track) => track.stop()); this.linuxElectronAudioRoutingActive = true; this.logger.info('Linux Electron screen-share audio routing enabled', { screenShareMonitorSourceName: captureInfo.sourceName, voiceSinkName: activation.voiceSinkName }); return stream; } catch (error) { desktopStream?.getTracks().forEach((track) => track.stop()); await this.resetLinuxElectronAudioRouting(); throw error; } } private scheduleLinuxAudioRoutingReset(): void { if (!this.linuxElectronAudioRoutingActive || this.linuxAudioRoutingResetPromise) { return; } this.linuxAudioRoutingResetPromise = this.resetLinuxElectronAudioRouting() .catch((error) => { this.logger.warn('Failed to reset Linux Electron audio routing', error); }) .finally(() => { this.linuxAudioRoutingResetPromise = null; }); } private async awaitPendingLinuxAudioRoutingReset(): Promise { if (!this.linuxAudioRoutingResetPromise) { return; } await this.linuxAudioRoutingResetPromise; } private async resetLinuxElectronAudioRouting(): Promise { const electronApi = this.getElectronApi(); const captureId = this.linuxMonitorAudioPipeline?.captureId; this.linuxElectronAudioRoutingActive = false; this.disposeLinuxScreenShareMonitorAudioPipeline(); try { if (captureId && electronApi?.stopLinuxScreenShareMonitorCapture) { await electronApi.stopLinuxScreenShareMonitorCapture(captureId); } } catch (error) { this.logger.warn('Failed to stop Linux screen-share monitor capture', error); } try { if (electronApi?.deactivateLinuxScreenShareAudioRouting) { await electronApi.deactivateLinuxScreenShareAudioRouting(); } } catch (error) { this.logger.warn('Failed to deactivate Linux Electron audio routing', error); } } private async startLinuxScreenShareMonitorTrack(): Promise<{ audioTrack: MediaStreamTrack; captureInfo: LinuxScreenShareMonitorCaptureInfo; }> { const electronApi = this.getElectronApi(); if (!electronApi?.startLinuxScreenShareMonitorCapture || !electronApi?.stopLinuxScreenShareMonitorCapture || !electronApi?.onLinuxScreenShareMonitorAudioChunk || !electronApi?.onLinuxScreenShareMonitorAudioEnded) { throw new Error('Linux screen-share monitor capture is unavailable.'); } const queuedChunksByCaptureId = new Map(); const queuedEndedReasons = new Map(); let pipeline: LinuxScreenShareMonitorAudioPipeline | null = null; let captureInfo: LinuxScreenShareMonitorCaptureInfo | null = null; const queueChunk = (captureId: string, chunk: Uint8Array): void => { const queuedChunks = queuedChunksByCaptureId.get(captureId) || []; queuedChunks.push(this.copyLinuxMonitorAudioBytes(chunk)); queuedChunksByCaptureId.set(captureId, queuedChunks); }; const onChunk = (payload: LinuxScreenShareMonitorAudioChunkPayload): void => { if (!pipeline || payload.captureId !== pipeline.captureId) { queueChunk(payload.captureId, payload.chunk); return; } this.handleLinuxScreenShareMonitorAudioChunk(pipeline, payload.chunk); }; const onEnded = (payload: LinuxScreenShareMonitorAudioEndedPayload): void => { if (!pipeline || payload.captureId !== pipeline.captureId) { queuedEndedReasons.set(payload.captureId, payload.reason); return; } this.logger.warn('Linux screen-share monitor capture ended', payload); if (this.isScreenActive && this.linuxMonitorAudioPipeline?.captureId === payload.captureId) { this.stopScreenShare(); } }; const unsubscribeChunk = electronApi.onLinuxScreenShareMonitorAudioChunk(onChunk) as () => void; const unsubscribeEnded = electronApi.onLinuxScreenShareMonitorAudioEnded(onEnded) as () => void; try { captureInfo = await electronApi.startLinuxScreenShareMonitorCapture() as LinuxScreenShareMonitorCaptureInfo; const audioContext = new AudioContext({ sampleRate: captureInfo.sampleRate }); const mediaDestination = audioContext.createMediaStreamDestination(); await audioContext.resume(); const audioTrack = mediaDestination.stream.getAudioTracks()[0]; if (!audioTrack) { throw new Error('Renderer audio pipeline did not produce a screen-share monitor track.'); } pipeline = { audioContext, audioTrack, bitsPerSample: captureInfo.bitsPerSample, captureId: captureInfo.captureId, channelCount: captureInfo.channelCount, mediaDestination, nextStartTime: audioContext.currentTime + 0.05, pendingBytes: new Uint8Array(0), sampleRate: captureInfo.sampleRate, unsubscribeChunk, unsubscribeEnded }; this.linuxMonitorAudioPipeline = pipeline; const activeCaptureId = captureInfo.captureId; audioTrack.addEventListener('ended', () => { if (this.isScreenActive && this.linuxMonitorAudioPipeline?.captureId === activeCaptureId) { this.stopScreenShare(); } }, { once: true }); const queuedChunks = queuedChunksByCaptureId.get(captureInfo.captureId) || []; const activePipeline = pipeline; queuedChunks.forEach((chunk) => { this.handleLinuxScreenShareMonitorAudioChunk(activePipeline, chunk); }); queuedChunksByCaptureId.delete(captureInfo.captureId); if (queuedEndedReasons.has(captureInfo.captureId)) { throw new Error(queuedEndedReasons.get(captureInfo.captureId) || 'Linux screen-share monitor capture ended before audio initialisation completed.'); } return { audioTrack, captureInfo }; } catch (error) { if (pipeline) { this.disposeLinuxScreenShareMonitorAudioPipeline(pipeline.captureId); } else { unsubscribeChunk(); unsubscribeEnded(); } try { await electronApi.stopLinuxScreenShareMonitorCapture(captureInfo?.captureId); } catch (stopError) { this.logger.warn('Failed to stop Linux screen-share monitor capture after startup failure', stopError); } throw error; } } private disposeLinuxScreenShareMonitorAudioPipeline(captureId?: string): void { if (!this.linuxMonitorAudioPipeline) { return; } if (captureId && captureId !== this.linuxMonitorAudioPipeline.captureId) { return; } const pipeline = this.linuxMonitorAudioPipeline; this.linuxMonitorAudioPipeline = null; pipeline.unsubscribeChunk(); pipeline.unsubscribeEnded(); pipeline.audioTrack.stop(); pipeline.pendingBytes = new Uint8Array(0); void pipeline.audioContext.close().catch((error) => { this.logger.warn('Failed to close Linux screen-share monitor audio context', error); }); } private handleLinuxScreenShareMonitorAudioChunk( pipeline: LinuxScreenShareMonitorAudioPipeline, chunk: Uint8Array ): void { if (pipeline.bitsPerSample !== 16) { this.logger.warn('Unsupported Linux screen-share monitor capture sample size', { bitsPerSample: pipeline.bitsPerSample, captureId: pipeline.captureId }); return; } const bytesPerSample = pipeline.bitsPerSample / 8; const bytesPerFrame = bytesPerSample * pipeline.channelCount; if (!Number.isFinite(bytesPerFrame) || bytesPerFrame <= 0) { return; } const combinedBytes = this.concatLinuxMonitorAudioBytes(pipeline.pendingBytes, chunk); const completeByteLength = combinedBytes.byteLength - (combinedBytes.byteLength % bytesPerFrame); if (completeByteLength <= 0) { pipeline.pendingBytes = combinedBytes; return; } const completeBytes = combinedBytes.subarray(0, completeByteLength); pipeline.pendingBytes = this.copyLinuxMonitorAudioBytes(combinedBytes.subarray(completeByteLength)); if (pipeline.audioContext.state !== 'running') { void pipeline.audioContext.resume().catch((error) => { this.logger.warn('Failed to resume Linux screen-share monitor audio context', error); }); } const frameCount = completeByteLength / bytesPerFrame; const audioBuffer = this.createLinuxScreenShareAudioBuffer(pipeline, completeBytes, frameCount); const source = pipeline.audioContext.createBufferSource(); source.buffer = audioBuffer; source.connect(pipeline.mediaDestination); source.onended = () => { source.disconnect(); }; const now = pipeline.audioContext.currentTime; const startTime = Math.max(pipeline.nextStartTime, now + 0.02); source.start(startTime); pipeline.nextStartTime = startTime + audioBuffer.duration; } private createLinuxScreenShareAudioBuffer( pipeline: LinuxScreenShareMonitorAudioPipeline, bytes: Uint8Array, frameCount: number ): AudioBuffer { const audioBuffer = pipeline.audioContext.createBuffer(pipeline.channelCount, frameCount, pipeline.sampleRate); const sampleData = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const channelData = Array.from({ length: pipeline.channelCount }, (_, channelIndex) => audioBuffer.getChannelData(channelIndex)); const bytesPerSample = pipeline.bitsPerSample / 8; const bytesPerFrame = bytesPerSample * pipeline.channelCount; for (let frameIndex = 0; frameIndex < frameCount; frameIndex += 1) { const frameOffset = frameIndex * bytesPerFrame; for (let channelIndex = 0; channelIndex < pipeline.channelCount; channelIndex += 1) { const sampleOffset = frameOffset + (channelIndex * bytesPerSample); channelData[channelIndex][frameIndex] = sampleData.getInt16(sampleOffset, true) / 32768; } } return audioBuffer; } private concatLinuxMonitorAudioBytes(first: Uint8Array, second: Uint8Array): Uint8Array { if (first.byteLength === 0) { return this.copyLinuxMonitorAudioBytes(second); } if (second.byteLength === 0) { return this.copyLinuxMonitorAudioBytes(first); } const combined = new Uint8Array(first.byteLength + second.byteLength); combined.set(first, 0); combined.set(second, first.byteLength); return combined; } private copyLinuxMonitorAudioBytes(bytes: Uint8Array): Uint8Array { return bytes.byteLength > 0 ? new Uint8Array(bytes) : new Uint8Array(0); } private buildDisplayMediaConstraints( options: ScreenShareStartOptions, preset: ScreenShareQualityPreset ): DisplayMediaStreamOptions { const supportedConstraints = navigator.mediaDevices?.getSupportedConstraints?.() as Record | undefined; const audioConstraints: Record | false = options.includeSystemAudio ? { echoCancellation: false, noiseSuppression: false, autoGainControl: false } : false; if (audioConstraints && supportedConstraints?.['restrictOwnAudio']) { audioConstraints['restrictOwnAudio'] = true; } if (audioConstraints && supportedConstraints?.['suppressLocalAudioPlayback']) { audioConstraints['suppressLocalAudioPlayback'] = true; } return { video: { width: { ideal: preset.width, max: preset.width }, height: { ideal: preset.height, max: preset.height }, frameRate: { ideal: preset.frameRate, max: preset.frameRate } }, audio: audioConstraints, monitorTypeSurfaces: 'include', selfBrowserSurface: 'exclude', surfaceSwitching: 'include', systemAudio: options.includeSystemAudio ? 'include' : 'exclude' } as DisplayMediaStreamOptions; } private buildElectronDesktopConstraints( sourceId: string, options: ScreenShareStartOptions, preset: ScreenShareQualityPreset ): ElectronDesktopMediaStreamConstraints { const electronConstraints: ElectronDesktopMediaStreamConstraints = { video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: sourceId, maxWidth: preset.width, maxHeight: preset.height, maxFrameRate: preset.frameRate } } }; if (options.includeSystemAudio) { electronConstraints.audio = { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: sourceId } }; } else { electronConstraints.audio = false; } return electronConstraints; } private configureScreenStream(preset: ScreenShareQualityPreset): void { const screenVideoTrack = this.activeScreenStream?.getVideoTracks()[0]; if (!screenVideoTrack) { throw new Error('Screen capture returned no video track.'); } if ('contentHint' in screenVideoTrack) { screenVideoTrack.contentHint = preset.contentHint; } this.logger.attachTrackDiagnostics(screenVideoTrack, 'screenVideo'); this.logger.logStream('screen', this.activeScreenStream); if (typeof screenVideoTrack.applyConstraints === 'function') { screenVideoTrack.applyConstraints({ width: { ideal: preset.width, max: preset.width }, height: { ideal: preset.height, max: preset.height }, frameRate: { ideal: preset.frameRate, max: preset.frameRate } }).catch((error) => { this.logger.warn('Failed to re-apply screen video constraints', error); }); } } private async applyScreenShareVideoParameters( sender: RTCRtpSender, preset: ScreenShareQualityPreset, peerId: string ): Promise { try { const params = sender.getParameters(); const encodings = params.encodings?.length ? params.encodings : [{} as RTCRtpEncodingParameters]; params.encodings = encodings.map((encoding, index) => index === 0 ? { ...encoding, maxBitrate: preset.maxBitrateBps, maxFramerate: preset.frameRate, scaleResolutionDownBy: preset.scaleResolutionDownBy ?? encoding.scaleResolutionDownBy ?? 1 } : encoding); (params as RTCRtpSendParameters & { degradationPreference?: string }).degradationPreference = preset.degradationPreference; await sender.setParameters(params); this.logger.info('Applied screen-share sender parameters', { peerId, maxBitrate: preset.maxBitrateBps, maxFramerate: preset.frameRate }); } catch (error) { this.logger.warn('Failed to apply screen-share sender parameters', error, { peerId }); } } }