Skip to content
Open
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
4 changes: 4 additions & 0 deletions apps/meteor/server/startup/callHistoryTestData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export async function addCallHistoryTestData(uid: string, extraUid: string): Pro
acceptedAt: new Date(),
activatedAt: new Date(),
uids: [uid, extraUid],
features: ['audio'],
},
{
_id: callId2,
Expand Down Expand Up @@ -165,6 +166,7 @@ export async function addCallHistoryTestData(uid: string, extraUid: string): Pro
acceptedAt: new Date(),
activatedAt: new Date(),
uids: [uid, extraUid],
features: ['audio'],
},
{
_id: callId3,
Expand Down Expand Up @@ -197,6 +199,7 @@ export async function addCallHistoryTestData(uid: string, extraUid: string): Pro
acceptedAt: new Date(),
activatedAt: new Date(),
uids: [uid],
features: ['audio'],
},
{
_id: callId4,
Expand Down Expand Up @@ -229,6 +232,7 @@ export async function addCallHistoryTestData(uid: string, extraUid: string): Pro
acceptedAt: new Date(),
activatedAt: new Date(),
uids: [uid],
features: ['audio'],
},
]);
}
4 changes: 2 additions & 2 deletions ee/packages/media-calls/src/base/BaseAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
MediaCallContact,
MediaCallSignedActor,
} from '@rocket.chat/core-typings';
import type { CallRole } from '@rocket.chat/media-signaling';
import type { CallFeature, CallRole } from '@rocket.chat/media-signaling';
import type { InsertionModel } from '@rocket.chat/model-typings';
import { MediaCallChannels } from '@rocket.chat/models';

Expand Down Expand Up @@ -61,7 +61,7 @@ export abstract class BaseMediaCallAgent implements IMediaCallAgent {
};
}

public abstract onCallAccepted(callId: string, signedContractId: string): Promise<void>;
public abstract onCallAccepted(callId: string, data: { signedContractId: string; features: CallFeature[] }): Promise<void>;

public abstract onCallActive(callId: string): Promise<void>;

Expand Down
4 changes: 2 additions & 2 deletions ee/packages/media-calls/src/definition/IMediaCallAgent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IMediaCall, MediaCallActor, MediaCallActorType, MediaCallContact } from '@rocket.chat/core-typings';
import type { CallRole } from '@rocket.chat/media-signaling';
import type { CallFeature, CallRole } from '@rocket.chat/media-signaling';

export interface IMediaCallAgent {
readonly actorType: MediaCallActorType;
Expand All @@ -12,7 +12,7 @@ export interface IMediaCallAgent {

onCallEnded(callId: string): Promise<void>;
/* Called when the call was accepted, even if the webrtc negotiation is pending */
onCallAccepted(callId: string, signedContractId: string): Promise<void>;
onCallAccepted(callId: string, data: { signedContractId: string; features: CallFeature[] }): Promise<void>;
onCallActive(callId: string): Promise<void>;
onCallCreated(call: IMediaCall): Promise<void>;
/* Called when the sdp of the other actor is available, regardless of call state, or when this actor must provide an offer */
Expand Down
3 changes: 2 additions & 1 deletion ee/packages/media-calls/src/definition/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { AtLeast, IMediaCall, IUser, MediaCallActorType, MediaCallContact, MediaCallSignedContact } from '@rocket.chat/core-typings';
import type { CallRejectedReason, CallService } from '@rocket.chat/media-signaling';
import type { CallFeature, CallRejectedReason, CallService } from '@rocket.chat/media-signaling';

export type MinimalUserData = Pick<IUser, '_id' | 'username' | 'name' | 'freeSwitchExtension'>;

Expand All @@ -15,6 +15,7 @@ export type InternalCallParams = {
requestedService?: CallService;
parentCallId?: string;
requestedBy?: MediaCallSignedContact;
features?: CallFeature[];
};

export type MediaCallHeader = AtLeast<IMediaCall, '_id' | 'caller' | 'callee'>;
Expand Down
2 changes: 2 additions & 0 deletions ee/packages/media-calls/src/internal/SignalProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export class GlobalSignalProcessor {

const services = signal.supportedServices ?? [];
const requestedService = services.includes('webrtc') ? 'webrtc' : services[0];
const features = signal.supportedFeatures ?? ['audio'];

const params: InternalCallParams = {
caller: {
Expand All @@ -196,6 +197,7 @@ export class GlobalSignalProcessor {
},
requestedCallId: signal.callId,
...(requestedService && { requestedService }),
features,
};

this.createCall(params);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import type {
import { isPendingState, isBusyState } from '@rocket.chat/media-signaling';
import type {
ClientMediaSignalTransfer,
CallAnswer,
CallHangupReason,
CallRole,
ClientMediaSignal,
ClientMediaSignalError,
ClientMediaSignalLocalState,
ServerMediaSignal,
ClientMediaSignalAnswer,
CallFeature,
} from '@rocket.chat/media-signaling';
import { MediaCallChannels, MediaCallNegotiations, MediaCalls } from '@rocket.chat/models';

Expand Down Expand Up @@ -98,7 +99,7 @@ export class UserActorSignalProcessor {
case 'local-sdp':
return this.saveLocalDescription(signal.sdp, signal.negotiationId);
case 'answer':
return this.processAnswer(signal.answer);
return this.processAnswer(signal);
case 'hangup':
return this.hangup(signal.reason);
case 'local-state':
Expand Down Expand Up @@ -126,12 +127,12 @@ export class UserActorSignalProcessor {
await mediaCallDirector.saveWebrtcSession(this.call, this.agent, { sdp, negotiationId }, this.contractId);
}

private async processAnswer(answer: CallAnswer): Promise<void> {
switch (answer) {
private async processAnswer(signal: ClientMediaSignalAnswer): Promise<void> {
switch (signal.answer) {
case 'ack':
return this.clientIsReachable();
case 'accept':
return this.clientHasAccepted();
return this.clientHasAccepted(signal.supportedFeatures || ['audio']);
case 'unavailable':
return this.clientIsUnavailable();
case 'reject':
Expand Down Expand Up @@ -307,13 +308,13 @@ export class UserActorSignalProcessor {
await mediaCallDirector.hangup(this.call, this.agent, 'unavailable');
}

protected async clientHasAccepted(): Promise<void> {
protected async clientHasAccepted(supportedFeatures: CallFeature[]): Promise<void> {
if (!this.isCallPending()) {
return;
}

if (this.role === 'callee') {
await mediaCallDirector.acceptCall(this.call, this.agent, { calleeContractId: this.contractId });
await mediaCallDirector.acceptCall(this.call, this.agent, { calleeContractId: this.contractId, supportedFeatures });
}
}

Expand Down
9 changes: 5 additions & 4 deletions ee/packages/media-calls/src/internal/agents/UserActorAgent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { IMediaCall, MediaCallSignedContact } from '@rocket.chat/core-typings';
import { isBusyState, type ClientMediaSignal, type ServerMediaSignal } from '@rocket.chat/media-signaling';
import { isBusyState } from '@rocket.chat/media-signaling';
import type { ClientMediaSignal, ServerMediaSignal, CallFeature } from '@rocket.chat/media-signaling';
import { MediaCallNegotiations, MediaCalls } from '@rocket.chat/models';

import { UserActorSignalProcessor } from './CallSignalProcessor';
Expand All @@ -20,12 +21,12 @@ export class UserActorAgent extends BaseMediaCallAgent {
getMediaCallServer().sendSignal(this.actorId, signal);
}

public async onCallAccepted(callId: string, signedContractId: string): Promise<void> {
public async onCallAccepted(callId: string, data: { signedContractId: string; features: CallFeature[] }): Promise<void> {
await this.sendSignal({
callId,
type: 'notification',
notification: 'accepted',
signedContractId,
...data,
});

if (this.role !== 'callee') {
Expand All @@ -40,7 +41,7 @@ export class UserActorAgent extends BaseMediaCallAgent {

await this.sendSignal({
callId,
toContractId: signedContractId,
toContractId: data.signedContractId,
type: 'remote-sdp',
sdp: negotiation.offer,
negotiationId: negotiation._id,
Expand Down
4 changes: 2 additions & 2 deletions ee/packages/media-calls/src/server/BroadcastAgent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IMediaCall } from '@rocket.chat/core-typings';
import type { ClientMediaSignalBody } from '@rocket.chat/media-signaling';
import type { CallFeature, ClientMediaSignalBody } from '@rocket.chat/media-signaling';

import { BaseMediaCallAgent } from '../base/BaseAgent';
import { logger } from '../logger';
Expand All @@ -14,7 +14,7 @@ import type { BaseCallProvider } from '../base/BaseCallProvider';
export class BroadcastActorAgent extends BaseMediaCallAgent {
public provider: BaseCallProvider | null = null;

public async onCallAccepted(callId: string, _signedContractId: string): Promise<void> {
public async onCallAccepted(callId: string, _data: { signedContractId: string; features: CallFeature[] }): Promise<void> {
this.reportCallUpdated({ callId });
}

Expand Down
25 changes: 20 additions & 5 deletions ee/packages/media-calls/src/server/CallDirector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IMediaCall, IMediaCallNegotiation, MediaCallContact, MediaCallSignedContact, ServerActor } from '@rocket.chat/core-typings';
import type { CallHangupReason, CallRole } from '@rocket.chat/media-signaling';
import type { CallFeature, CallHangupReason, CallRole } from '@rocket.chat/media-signaling';
import type { InsertionModel } from '@rocket.chat/model-typings';
import { MediaCallNegotiations, MediaCalls } from '@rocket.chat/models';

Expand Down Expand Up @@ -52,7 +52,7 @@ class MediaCallDirector {
public async acceptCall(
call: MediaCallHeader,
calleeAgent: IMediaCallAgent,
data: { calleeContractId: string; webrtcAnswer?: RTCSessionDescriptionInit },
data: { calleeContractId: string; webrtcAnswer?: RTCSessionDescriptionInit; supportedFeatures: CallFeature[] },
): Promise<boolean> {
logger.debug({ msg: 'MediaCallDirector.acceptCall' });

Expand All @@ -71,8 +71,11 @@ class MediaCallDirector {
logger.info({ msg: 'Call was flagged as accepted', callId: call._id });
this.scheduleExpirationCheckByCallId(call._id);

await calleeAgent.onCallAccepted(call._id, data.calleeContractId);
await calleeAgent.oppositeAgent?.onCallAccepted(call._id, call.caller.contractId);
const updatedCall = await MediaCalls.findOneById(call._id, { projection: { features: 1 } });
const features = (updatedCall?.features || ['audio']) as CallFeature[];

await calleeAgent.onCallAccepted(call._id, { signedContractId: data.calleeContractId, features });
await calleeAgent.oppositeAgent?.onCallAccepted(call._id, { signedContractId: call.caller.contractId, features });

if (data.webrtcAnswer && negotiation) {
const negotiationResult = await MediaCallNegotiations.setAnswerById(negotiation._id, data.webrtcAnswer);
Expand Down Expand Up @@ -167,7 +170,17 @@ class MediaCallDirector {
}

public async createCall(params: CreateCallParams): Promise<IMediaCall> {
const { caller, callee, requestedCallId, requestedService, callerAgent, calleeAgent, parentCallId, requestedBy } = params;
const {
caller,
callee,
requestedCallId,
requestedService,
callerAgent,
calleeAgent,
parentCallId,
requestedBy,
features = ['audio'],
} = params;

// The caller must always have a contract to create the call
if (!caller.contractId) {
Expand Down Expand Up @@ -213,6 +226,8 @@ class MediaCallDirector {

...(requestedCallId && { callerRequestedId: requestedCallId }),
...(parentCallId && { parentCallId }),

features,
};

logger.debug({ msg: 'creating call', call });
Expand Down
2 changes: 2 additions & 0 deletions ee/packages/media-calls/src/sip/providers/OutgoingSipCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export class OutgoingSipCall extends BaseSipCall {
callee: signedCallee,
calleeAgent,
callerAgent,
features: ['audio'],
});

const channel = await calleeAgent.getOrCreateChannel(call, session.sessionId);
Expand Down Expand Up @@ -266,6 +267,7 @@ export class OutgoingSipCall extends BaseSipCall {
await mediaCallDirector.acceptCall(call, this.agent, {
calleeContractId: this.session.sessionId,
webrtcAnswer: { type: 'answer', sdp: this.sipDialog.remote.sdp },
supportedFeatures: ['audio'],
});
}

Expand Down
3 changes: 3 additions & 0 deletions packages/core-typings/src/mediaCalls/IMediaCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,7 @@ export interface IMediaCall extends IRocketChatRecord {
transferredAt?: Date;

uids: IUser['_id'][];

/** The list of features that may be used in this call. Values are final once the call is accepted. */
features: string[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export type CallRole = 'caller' | 'callee';

export type CallService = 'webrtc';

export const callFeatureList = ['audio'] as const;

export type CallFeature = (typeof callFeatureList)[number];

export type CallState =
| 'none' // trying to call with no idea if it'll reach anyone
| 'ringing' // call has been acknoledged by the callee's agent, but no response about them accepting it or not
Expand Down Expand Up @@ -113,4 +117,5 @@ export interface IClientMediaCall {
sendDTMF(dtmf: string, duration?: number): void;

getStats(selector?: MediaStreamTrack | null): Promise<RTCStatsReport | null>;
isFeatureAvailable(feature: CallFeature): boolean;
}
14 changes: 13 additions & 1 deletion packages/media-signaling/src/definition/signals/client/answer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { JSONSchemaType } from 'ajv';

import type { CallAnswer } from '../../call';
import type { CallAnswer, CallFeature } from '../../call';
import { callFeatureList } from '../../call/IClientMediaCall';

/** Client is saying that the user accepted or rejected a call, or simply reporting that the user can or can't be reached */
export type ClientMediaSignalAnswer = {
Expand All @@ -9,6 +10,8 @@ export type ClientMediaSignalAnswer = {
contractId: string;

answer: CallAnswer;

supportedFeatures?: CallFeature[];
};

export const clientMediaSignalAnswerSchema: JSONSchemaType<ClientMediaSignalAnswer> = {
Expand All @@ -33,6 +36,15 @@ export const clientMediaSignalAnswerSchema: JSONSchemaType<ClientMediaSignalAnsw
enum: ['accept', 'reject', 'ack', 'unavailable'],
nullable: false,
},
supportedFeatures: {
type: 'array',
items: {
type: 'string',
enum: callFeatureList,
nullable: false,
},
nullable: true,
},
},
additionalProperties: false,
required: ['callId', 'contractId', 'type', 'answer'],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { JSONSchemaType } from 'ajv';

import type { CallService } from '../../call';
import type { CallFeature, CallService } from '../../call';
import { callFeatureList } from '../../call/IClientMediaCall';

export type ClientMediaSignalRequestCall = {
/** the callId on this signal is temporary and is never propagated to other agents */
Expand All @@ -12,6 +13,7 @@ export type ClientMediaSignalRequestCall = {
id: string;
};
supportedServices: CallService[];
supportedFeatures?: CallFeature[];
};

export const clientMediaSignalRequestCallSchema: JSONSchemaType<ClientMediaSignalRequestCall> = {
Expand Down Expand Up @@ -57,6 +59,15 @@ export const clientMediaSignalRequestCallSchema: JSONSchemaType<ClientMediaSigna
},
nullable: false,
},
supportedFeatures: {
type: 'array',
items: {
type: 'string',
enum: callFeatureList,
nullable: false,
},
nullable: true,
},
},
additionalProperties: false,
required: ['callId', 'contractId', 'type', 'callee', 'supportedServices'],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CallNotification } from '../../call';
import type { CallFeature, CallNotification } from '../../call';

/** Server is sending a notification about the call state */
export type ServerMediaSignalNotification = {
Expand All @@ -11,4 +11,6 @@ export type ServerMediaSignalNotification = {
* Optional in general, but at least one notification must be sent with it before the callee can join the call
*/
signedContractId?: string;

features?: CallFeature[];
};
Loading
Loading