Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"scripts": {
"build": "turbo run build",
"build:services": "turbo run build --filter=rocketchat-services...",
"dev": "turbo run dev --env-mode=loose --parallel --filter=@rocket.chat/meteor...",
"dev": "yarn workspace @rocket.chat/core-typings run build && yarn workspace @rocket.chat/livechat run build && turbo run dev --env-mode=loose --parallel --filter=@rocket.chat/meteor",
"dsv": "turbo run dsv --env-mode=loose --filter=@rocket.chat/meteor...",
"ms": "turbo run ms --env-mode=loose",
"fossify": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node scripts/fossify.ts",
Expand Down
3 changes: 2 additions & 1 deletion packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -7448,4 +7448,5 @@
"Select_message_from_user": "Select message from {{username}}",
"Select_message_from_user_with_preview": "Select message from {{username}}: {{message}}",
"current": "current"
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,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<void>;
hasScreenVideoTrack(): boolean;
Expand Down
20 changes: 2 additions & 18 deletions packages/media-signaling/src/lib/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,12 +561,12 @@ export class MediaSignalingSession extends Emitter<MediaSignalingEvents> {
this.config.logger?.debug('MediaSignalingSession.startInputTrack.done', this.callsToGetUserMedia);

if (!userMedia) {
return this.hangupCallsThatNeedInput();
return;
}

const tracks = userMedia.getAudioTracks();
if (!tracks.length) {
return this.hangupCallsThatNeedInput();
return;
}

const inputTrack = tracks[0];
Expand All @@ -585,22 +585,6 @@ export class MediaSignalingSession extends Emitter<MediaSignalingEvents> {
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()) {
Expand Down
32 changes: 22 additions & 10 deletions packages/media-signaling/src/lib/services/webrtc/Processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,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;

Expand Down Expand Up @@ -339,8 +336,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);
}

Expand All @@ -351,8 +351,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);
}
Expand Down Expand Up @@ -470,8 +470,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) {
Expand All @@ -486,6 +486,14 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor {
}
}

private getDesiredAudioDirection(): RTCRtpTransceiverDirection {
if (!this.inputTrack) {
return this.held ? 'inactive' : 'recvonly';
}

return this.held ? 'sendonly' : 'sendrecv';
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private createDataChannel(): void {
if (this._dataChannel || this._dataChannelEnded || !this.config.call.hasFlag('create-data-channel')) {
return;
Expand Down Expand Up @@ -565,8 +573,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 {
Expand Down Expand Up @@ -632,6 +640,10 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor {
return;
}

if (!this.inputTrack) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This guard correctly prevents micless calls from being falsely inferred as remote-held, but it also disables remote-held detection entirely once the local input track is absent. Because inputTrack can be cleared mid-call via setInputTrack(null), a call that was correctly marked as remote-held while the mic was present will keep _remoteHeld (exposed as remoteHeld in Call.ts) frozen at true even after the remote resumes, since updateRemoteHeld now returns before re-evaluating the audio transceivers. If stale held state is a concern, consider only skipping when there is genuinely nothing to infer, or explicitly resetting held state when the track is removed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/media-signaling/src/lib/services/webrtc/Processor.ts, line 643:

<comment>This guard correctly prevents micless calls from being falsely inferred as remote-held, but it also disables remote-held detection entirely once the local input track is absent. Because inputTrack can be cleared mid-call via setInputTrack(null), a call that was correctly marked as remote-held while the mic was present will keep _remoteHeld (exposed as remoteHeld in Call.ts) frozen at true even after the remote resumes, since updateRemoteHeld now returns before re-evaluating the audio transceivers. If stale held state is a concern, consider only skipping when there is genuinely nothing to infer, or explicitly resetting held state when the track is removed.</comment>

<file context>
@@ -640,6 +640,10 @@ export class MediaCallWebRTCProcessor implements IWebRTCProcessor {
 			return;
 		}
 
+		if (!this.inputTrack) {
+			return;
+		}
</file context>

return;
}

let anyTransceiverNotSending = false;
const transceivers = this.getTransceivers('audio');

Expand Down
4 changes: 4 additions & 0 deletions packages/ui-voip/src/context/MediaCallViewContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type MediaCallViewContextValue = {
onForward: () => void;
onTone: (tone: string) => void;
onEndCall: () => void;
onToggleScreenShare: () => Promise<void>;
onCall: () => Promise<void>;
onAccept: () => Promise<void>;
onSelectPeer: (peerInfo: PeerInfo) => void;
Expand All @@ -42,6 +43,8 @@ const defaultSessionState: SessionState = {
held: false,
remoteMuted: false,
remoteHeld: false,
screenSharing: false,
remoteScreenSharing: false,
callId: undefined,
supportedFeatures: ['audio', 'transfer', 'hold'],
};
Expand All @@ -54,6 +57,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,
Expand Down
2 changes: 2 additions & 0 deletions packages/ui-voip/src/context/definitions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ interface IBaseSession {
held: boolean;
remoteMuted: boolean;
remoteHeld: boolean;
screenSharing: boolean;
remoteScreenSharing: boolean;
startedAt?: Date;
hidden: boolean;
supportedFeatures: readonly CallFeature[];
Expand Down
9 changes: 8 additions & 1 deletion packages/ui-voip/src/providers/MediaCallViewProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const MediaCallViewProvider = ({ children }: MediaCallViewProviderProps) => {
startCall(false);
} catch (error) {
console.error('Media Call - Error requesting device', error);

startCall(true);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};
Expand All @@ -128,7 +129,8 @@ const MediaCallViewProvider = ({ children }: MediaCallViewProviderProps) => {
stopTracks(stream);
controls.acceptCall(false);
} catch (error) {
console.error('MediaCall - onAccept - Failed to get device, procceeding without microphone', error);
console.error('Media Call - Error requesting device', error);

controls.acceptCall(true);
}
};
Expand Down Expand Up @@ -200,6 +202,10 @@ const MediaCallViewProvider = ({ children }: MediaCallViewProviderProps) => {
controls.endCall();
};

const onToggleScreenShare = useCallback(async () => {
controls.toggleScreenSharing();
}, [controls]);

const onSelectPeer = (peerInfo: PeerInfo) => {
selectPeer(peerInfo);
};
Expand Down Expand Up @@ -245,6 +251,7 @@ const MediaCallViewProvider = ({ children }: MediaCallViewProviderProps) => {
onForward,
onTone,
onEndCall,
onToggleScreenShare,
onCall,
onAccept,
onSelectPeer,
Expand Down
11 changes: 11 additions & 0 deletions packages/ui-voip/src/providers/useMediaSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -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: [],
Expand All @@ -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;

Expand All @@ -187,6 +194,8 @@ export const useMediaSession = (instance?: MediaSignalingSession): MediaSessionS
hidden,
remoteHeld,
remoteMuted,
screenSharing: Boolean(localMediaStream?.hasVideo()),
remoteScreenSharing: Boolean(remoteMediaStream?.hasVideo()),
callId,
startedAt,
supportedFeatures,
Expand Down Expand Up @@ -221,6 +230,8 @@ export const useMediaSession = (instance?: MediaSignalingSession): MediaSessionS
hidden,
remoteHeld,
remoteMuted,
screenSharing: Boolean(localMediaStream?.hasVideo()),
remoteScreenSharing: Boolean(remoteMediaStream?.hasVideo()),
callId,
startedAt,
supportedFeatures,
Expand Down
Loading