From d380e5a9ae658b2dec01c7e53f7682f5307f2986 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Fri, 29 May 2026 07:37:12 +0530 Subject: [PATCH 1/4] fix: allow screen sharing without media devices --- packages/i18n/src/locales/en.i18n.json | 2 +- .../src/definition/call/IClientMediaCall.ts | 2 + packages/media-signaling/src/lib/Session.ts | 22 +-- .../src/lib/services/webrtc/Processor.ts | 30 ++-- .../src/context/MediaCallViewContext.ts | 4 + packages/ui-voip/src/context/definitions.d.ts | 2 + .../src/providers/MediaCallViewProvider.tsx | 9 +- .../ui-voip/src/providers/useMediaSession.ts | 11 ++ .../MediaCallWidget.spec.tsx.snap | 153 +++++++++--------- 9 files changed, 127 insertions(+), 108 deletions(-) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 3d361dc9ac675..c00b0bbb637db 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -7250,4 +7250,4 @@ "Avatar_preview_updated": "Avatar preview updated", "Select_message_from_user": "Select message from {{username}}", "Select_message_from_user_with_preview": "Select message from {{username}}: {{message}}" -} \ No newline at end of file +} diff --git a/packages/media-signaling/src/definition/call/IClientMediaCall.ts b/packages/media-signaling/src/definition/call/IClientMediaCall.ts index 32fb06a6a7882..7a27a7b9c613f 100644 --- a/packages/media-signaling/src/definition/call/IClientMediaCall.ts +++ b/packages/media-signaling/src/definition/call/IClientMediaCall.ts @@ -93,6 +93,8 @@ export interface IClientMediaCall { accept(): void; reject(): void; hangup(): void; + setMuted(muted: boolean): void; + setHeld(onHold: boolean): void; requestScreenShare(requested: boolean): void; setScreenVideoTrack(videoTrack: MediaStreamTrack | null): Promise; hasScreenVideoTrack(): boolean; diff --git a/packages/media-signaling/src/lib/Session.ts b/packages/media-signaling/src/lib/Session.ts index 64dd3d4592786..7b2921ffac813 100644 --- a/packages/media-signaling/src/lib/Session.ts +++ b/packages/media-signaling/src/lib/Session.ts @@ -479,7 +479,7 @@ export class MediaSignalingSession extends Emitter { this.currentDeviceId = this.deviceId; - let userMedia: MediaStream | null = null; + let userMedia: MediaStream | null; this.callsToGetUserMedia++; try { userMedia = await this.config.mediaStreamFactory({ audio: this.getAudioConstraints() }).catch(() => null); @@ -495,12 +495,12 @@ export class MediaSignalingSession extends Emitter { } if (!userMedia) { - return this.hangupCallsThatNeedInput(); + return; } const tracks = userMedia.getAudioTracks(); if (!tracks.length) { - return this.hangupCallsThatNeedInput(); + return; } const inputTrack = tracks[0]; @@ -519,22 +519,6 @@ export class MediaSignalingSession extends Emitter { return this.setInputTrack(inputTrack); } - private hangupCallsThatNeedInput(): void { - this.config.logger?.debug('MediaSignalingSession.hangupCallsThatNeedInput'); - - for (const call of this.knownCalls.values()) { - if (!call.needsInputTrack()) { - continue; - } - - try { - call.hangup('input-error'); - } catch { - // - } - } - } - private mayNeedInputTrack(): boolean { for (const call of this.knownCalls.values()) { if (call.mayNeedInputTrack()) { diff --git a/packages/media-signaling/src/lib/services/webrtc/Processor.ts b/packages/media-signaling/src/lib/services/webrtc/Processor.ts index fb47c076d8aec..db7fa22bab909 100644 --- a/packages/media-signaling/src/lib/services/webrtc/Processor.ts +++ b/packages/media-signaling/src/lib/services/webrtc/Processor.ts @@ -157,9 +157,6 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor { if (this.stopped) { throw new Error('WebRTC Processor has already been stopped.'); } - if (!this.inputTrack) { - throw new Error('no-input-track'); - } await this.initialization; @@ -329,8 +326,11 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor { private updateAudioDirectionBeforeNegotiation(): void { // Before the negotiation, we set the direction based on our own state only // We'll tell the SDK that we want to send audio and, depending on the "on hold" state, also receive it - const desiredDirection = this.held ? 'sendonly' : 'sendrecv'; + const desiredDirection = this.getDesiredAudioDirection(); + if (!this.getTransceivers('audio').length) { + this.peer.addTransceiver('audio', { direction: desiredDirection }); + } this.updateDirectionBeforeNegotiation('audio', desiredDirection); } @@ -341,8 +341,8 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor { // If we didn't do this, everything would still work, but the browser would trigger redundant renegotiations whenever the directions mismatch - const desiredDirection = this.held ? 'sendonly' : 'sendrecv'; - const acceptableDirection = this.held ? 'inactive' : 'recvonly'; + const desiredDirection = this.getDesiredAudioDirection(); + const acceptableDirection = this.held || !this.inputTrack ? 'inactive' : 'recvonly'; this.updateDirectionAfterNegotiation('audio', desiredDirection, acceptableDirection); } @@ -438,8 +438,8 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor { return; } - const desiredDirection = this.held ? 'sendonly' : 'sendrecv'; - const acceptableDirection = this.held ? 'inactive' : 'recvonly'; + const desiredDirection = this.getDesiredAudioDirection(); + const acceptableDirection = this.held || !this.inputTrack ? 'inactive' : 'recvonly'; const transceivers = this.getTransceivers('audio'); for (const transceiver of transceivers) { @@ -454,6 +454,14 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor { } } + private getDesiredAudioDirection(): RTCRtpTransceiverDirection { + if (!this.inputTrack) { + return this.held ? 'inactive' : 'recvonly'; + } + + return this.held ? 'sendonly' : 'sendrecv'; + } + private createDataChannel(): void { if (this._dataChannel || this._dataChannelEnded || !this.config.call.hasFlag('create-data-channel')) { return; @@ -477,7 +485,7 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor { channel.onopen = (_event) => { this.config.logger?.debug('Data Channel Open', channel.label); - if (!this._dataChannel || this._dataChannel.readyState !== 'open') { + if (this._dataChannel?.readyState !== 'open') { this._dataChannel = channel; } @@ -533,8 +541,8 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor { private getCommandFromDataChannelMessage(message: string): P2PCommand | null { try { - const obj = JSON.parse(message); - if (obj.command && this.isValidCommand(obj.command)) { + const obj = JSON.parse(message) as { command?: unknown }; + if (typeof obj.command === 'string' && this.isValidCommand(obj.command)) { return obj.command; } } catch { diff --git a/packages/ui-voip/src/context/MediaCallViewContext.ts b/packages/ui-voip/src/context/MediaCallViewContext.ts index c10f93fbfda7f..11e38b0a10d1e 100644 --- a/packages/ui-voip/src/context/MediaCallViewContext.ts +++ b/packages/ui-voip/src/context/MediaCallViewContext.ts @@ -19,6 +19,7 @@ type MediaCallViewContextValue = { onForward: () => void; onTone: (tone: string) => void; onEndCall: () => void; + onToggleScreenShare: () => Promise; onCall: () => Promise; onAccept: () => Promise; onSelectPeer: (peerInfo: PeerInfo) => void; @@ -40,6 +41,8 @@ const defaultSessionState: SessionState = { held: false, remoteMuted: false, remoteHeld: false, + screenSharing: false, + remoteScreenSharing: false, callId: undefined, supportedFeatures: ['audio', 'transfer', 'hold'], }; @@ -52,6 +55,7 @@ export const defaultMediaCallContextValue: MediaCallViewContextValue = { onForward: () => undefined, onTone: () => undefined, onEndCall: () => undefined, + onToggleScreenShare: () => Promise.resolve(undefined), onCall: () => Promise.resolve(undefined), onAccept: () => Promise.resolve(undefined), onSelectPeer: () => undefined, diff --git a/packages/ui-voip/src/context/definitions.d.ts b/packages/ui-voip/src/context/definitions.d.ts index 495bf5cf253fb..f49351cba00d0 100644 --- a/packages/ui-voip/src/context/definitions.d.ts +++ b/packages/ui-voip/src/context/definitions.d.ts @@ -29,6 +29,8 @@ interface IBaseSession { held: boolean; remoteMuted: boolean; remoteHeld: boolean; + screenSharing: boolean; + remoteScreenSharing: boolean; startedAt?: Date; hidden: boolean; supportedFeatures: readonly CallFeature[]; diff --git a/packages/ui-voip/src/providers/MediaCallViewProvider.tsx b/packages/ui-voip/src/providers/MediaCallViewProvider.tsx index bbf30e8a9fdea..3300189dc8955 100644 --- a/packages/ui-voip/src/providers/MediaCallViewProvider.tsx +++ b/packages/ui-voip/src/providers/MediaCallViewProvider.tsx @@ -99,7 +99,6 @@ const MediaCallViewProvider = ({ children }: MediaCallViewProviderProps) => { stopTracks(stream); } catch (error) { console.error('Media Call - Error requesting device', error); - return; } if ('userId' in peerInfo) { @@ -125,10 +124,11 @@ const MediaCallViewProvider = ({ children }: MediaCallViewProviderProps) => { const stream = await requestDevice({ actionType: 'incoming' }); stopTracks(stream); } catch (error) { + console.error('Media Call - Error requesting device', error); if (error instanceof PermissionRequestCancelledCallRejectedError) { controls.endCall(); + return; } - return; } controls.acceptCall(); @@ -201,6 +201,10 @@ const MediaCallViewProvider = ({ children }: MediaCallViewProviderProps) => { controls.endCall(); }; + const onToggleScreenShare = useCallback(async () => { + controls.toggleScreenSharing(); + }, [controls]); + const onSelectPeer = (peerInfo: PeerInfo) => { selectPeer(peerInfo); }; @@ -238,6 +242,7 @@ const MediaCallViewProvider = ({ children }: MediaCallViewProviderProps) => { onForward, onTone, onEndCall, + onToggleScreenShare, onCall, onAccept, onSelectPeer, diff --git a/packages/ui-voip/src/providers/useMediaSession.ts b/packages/ui-voip/src/providers/useMediaSession.ts index 893bbd06ca5d0..6584ef6102fc8 100644 --- a/packages/ui-voip/src/providers/useMediaSession.ts +++ b/packages/ui-voip/src/providers/useMediaSession.ts @@ -17,6 +17,8 @@ const defaultSessionInfo: SessionState = { held: false, remoteMuted: false, remoteHeld: false, + screenSharing: false, + remoteScreenSharing: false, startedAt: undefined, hidden: false, supportedFeatures: ['audio', 'transfer', 'hold'], @@ -155,6 +157,8 @@ export const useMediaSession = (instance?: MediaSignalingSession): MediaSessionS hidden: false, remoteHeld: false, remoteMuted: false, + screenSharing: false, + remoteScreenSharing: false, callId: instanceState.tempCallId, startedAt: undefined, supportedFeatures: [], @@ -169,8 +173,11 @@ export const useMediaSession = (instance?: MediaSignalingSession): MediaSessionS activeTimestamp: startedAt, features: supportedFeatures, transferredBy: callTransferredBy, + localParticipant, remoteParticipant: { muted: remoteMuted, held: remoteHeld, contact }, } = instanceState; + const localMediaStream = localParticipant.getMediaStream('screen-share'); + const remoteMediaStream = instanceState.remoteParticipant.getMediaStream('screen-share'); const transferredBy = callTransferredBy?.displayName || callTransferredBy?.username || undefined; @@ -187,6 +194,8 @@ export const useMediaSession = (instance?: MediaSignalingSession): MediaSessionS hidden, remoteHeld, remoteMuted, + screenSharing: Boolean(localMediaStream?.hasVideo()), + remoteScreenSharing: Boolean(remoteMediaStream?.hasVideo()), callId, startedAt, supportedFeatures, @@ -221,6 +230,8 @@ export const useMediaSession = (instance?: MediaSignalingSession): MediaSessionS hidden, remoteHeld, remoteMuted, + screenSharing: Boolean(localMediaStream?.hasVideo()), + remoteScreenSharing: Boolean(remoteMediaStream?.hasVideo()), callId, startedAt, supportedFeatures, diff --git a/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap b/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap index d5423e552f3e5..4e5033a3ebe75 100644 --- a/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap +++ b/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap @@ -13,7 +13,7 @@ exports[`renders IncomingCall without crashing 1`] = ` >