From 62ac6772a45c88e5618070f15b48e3bc0f78be7a Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Mon, 9 Feb 2026 18:01:36 -0300 Subject: [PATCH 1/3] chore: allow media call clients to specify which call features they support --- ee/packages/media-calls/src/base/BaseAgent.ts | 4 +-- .../src/definition/IMediaCallAgent.ts | 4 +-- .../media-calls/src/definition/common.ts | 3 +- .../src/internal/SignalProcessor.ts | 2 ++ .../internal/agents/CallSignalProcessor.ts | 15 +++++----- .../src/internal/agents/UserActorAgent.ts | 9 +++--- .../media-calls/src/server/BroadcastAgent.ts | 4 +-- .../media-calls/src/server/CallDirector.ts | 25 ++++++++++++---- .../src/sip/providers/OutgoingSipCall.ts | 2 ++ .../core-typings/src/mediaCalls/IMediaCall.ts | 3 ++ .../src/definition/call/IClientMediaCall.ts | 5 ++++ .../src/definition/signals/client/answer.ts | 14 ++++++++- .../definition/signals/client/request-call.ts | 13 +++++++- .../definition/signals/server/notification.ts | 4 ++- packages/media-signaling/src/lib/Call.ts | 30 ++++++++++++++++--- packages/media-signaling/src/lib/Session.ts | 6 ++-- .../src/lib/TransportWrapper.ts | 6 ++-- .../src/models/IMediaCallsModel.ts | 2 +- packages/models/src/models/MediaCalls.ts | 11 ++++++- .../src/context/useMediaSessionInstance.ts | 1 + 20 files changed, 126 insertions(+), 37 deletions(-) diff --git a/ee/packages/media-calls/src/base/BaseAgent.ts b/ee/packages/media-calls/src/base/BaseAgent.ts index 6b4d95b04f232..05f3fa76dba54 100644 --- a/ee/packages/media-calls/src/base/BaseAgent.ts +++ b/ee/packages/media-calls/src/base/BaseAgent.ts @@ -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'; @@ -61,7 +61,7 @@ export abstract class BaseMediaCallAgent implements IMediaCallAgent { }; } - public abstract onCallAccepted(callId: string, signedContractId: string): Promise; + public abstract onCallAccepted(callId: string, data: { signedContractId: string; features: CallFeature[] }): Promise; public abstract onCallActive(callId: string): Promise; diff --git a/ee/packages/media-calls/src/definition/IMediaCallAgent.ts b/ee/packages/media-calls/src/definition/IMediaCallAgent.ts index 7481bad53b739..bc320aa2fb73e 100644 --- a/ee/packages/media-calls/src/definition/IMediaCallAgent.ts +++ b/ee/packages/media-calls/src/definition/IMediaCallAgent.ts @@ -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; @@ -12,7 +12,7 @@ export interface IMediaCallAgent { onCallEnded(callId: string): Promise; /* Called when the call was accepted, even if the webrtc negotiation is pending */ - onCallAccepted(callId: string, signedContractId: string): Promise; + onCallAccepted(callId: string, data: { signedContractId: string; features: CallFeature[] }): Promise; onCallActive(callId: string): Promise; onCallCreated(call: IMediaCall): Promise; /* Called when the sdp of the other actor is available, regardless of call state, or when this actor must provide an offer */ diff --git a/ee/packages/media-calls/src/definition/common.ts b/ee/packages/media-calls/src/definition/common.ts index e02473dcf1b8f..cb0af26822ffa 100644 --- a/ee/packages/media-calls/src/definition/common.ts +++ b/ee/packages/media-calls/src/definition/common.ts @@ -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; @@ -15,6 +15,7 @@ export type InternalCallParams = { requestedService?: CallService; parentCallId?: string; requestedBy?: MediaCallSignedContact; + features?: CallFeature[]; }; export type MediaCallHeader = AtLeast; diff --git a/ee/packages/media-calls/src/internal/SignalProcessor.ts b/ee/packages/media-calls/src/internal/SignalProcessor.ts index 9b292f94d2299..19a0e9f619330 100644 --- a/ee/packages/media-calls/src/internal/SignalProcessor.ts +++ b/ee/packages/media-calls/src/internal/SignalProcessor.ts @@ -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: { @@ -196,6 +197,7 @@ export class GlobalSignalProcessor { }, requestedCallId: signal.callId, ...(requestedService && { requestedService }), + features, }; this.createCall(params); diff --git a/ee/packages/media-calls/src/internal/agents/CallSignalProcessor.ts b/ee/packages/media-calls/src/internal/agents/CallSignalProcessor.ts index fa30eecf312c6..ce5cc8ce13a8d 100644 --- a/ee/packages/media-calls/src/internal/agents/CallSignalProcessor.ts +++ b/ee/packages/media-calls/src/internal/agents/CallSignalProcessor.ts @@ -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'; @@ -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': @@ -126,12 +127,12 @@ export class UserActorSignalProcessor { await mediaCallDirector.saveWebrtcSession(this.call, this.agent, { sdp, negotiationId }, this.contractId); } - private async processAnswer(answer: CallAnswer): Promise { - switch (answer) { + private async processAnswer(signal: ClientMediaSignalAnswer): Promise { + 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': @@ -307,13 +308,13 @@ export class UserActorSignalProcessor { await mediaCallDirector.hangup(this.call, this.agent, 'unavailable'); } - protected async clientHasAccepted(): Promise { + protected async clientHasAccepted(supportedFeatures: CallFeature[]): Promise { 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 }); } } diff --git a/ee/packages/media-calls/src/internal/agents/UserActorAgent.ts b/ee/packages/media-calls/src/internal/agents/UserActorAgent.ts index 0190346a5d134..fb39234cd2923 100644 --- a/ee/packages/media-calls/src/internal/agents/UserActorAgent.ts +++ b/ee/packages/media-calls/src/internal/agents/UserActorAgent.ts @@ -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'; @@ -20,12 +21,12 @@ export class UserActorAgent extends BaseMediaCallAgent { getMediaCallServer().sendSignal(this.actorId, signal); } - public async onCallAccepted(callId: string, signedContractId: string): Promise { + public async onCallAccepted(callId: string, data: { signedContractId: string; features: CallFeature[] }): Promise { await this.sendSignal({ callId, type: 'notification', notification: 'accepted', - signedContractId, + ...data, }); if (this.role !== 'callee') { @@ -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, diff --git a/ee/packages/media-calls/src/server/BroadcastAgent.ts b/ee/packages/media-calls/src/server/BroadcastAgent.ts index 2a799087b9ca3..5b76704dd799c 100644 --- a/ee/packages/media-calls/src/server/BroadcastAgent.ts +++ b/ee/packages/media-calls/src/server/BroadcastAgent.ts @@ -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'; @@ -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 { + public async onCallAccepted(callId: string, _data: { signedContractId: string; features: CallFeature[] }): Promise { this.reportCallUpdated({ callId }); } diff --git a/ee/packages/media-calls/src/server/CallDirector.ts b/ee/packages/media-calls/src/server/CallDirector.ts index 705a5df1a528d..6463cd9d0359b 100644 --- a/ee/packages/media-calls/src/server/CallDirector.ts +++ b/ee/packages/media-calls/src/server/CallDirector.ts @@ -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'; @@ -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 { logger.debug({ msg: 'MediaCallDirector.acceptCall' }); @@ -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); @@ -167,7 +170,17 @@ class MediaCallDirector { } public async createCall(params: CreateCallParams): Promise { - 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) { @@ -213,6 +226,8 @@ class MediaCallDirector { ...(requestedCallId && { callerRequestedId: requestedCallId }), ...(parentCallId && { parentCallId }), + + features, }; logger.debug({ msg: 'creating call', call }); diff --git a/ee/packages/media-calls/src/sip/providers/OutgoingSipCall.ts b/ee/packages/media-calls/src/sip/providers/OutgoingSipCall.ts index eeb87ea5e7481..348eda1429e3a 100644 --- a/ee/packages/media-calls/src/sip/providers/OutgoingSipCall.ts +++ b/ee/packages/media-calls/src/sip/providers/OutgoingSipCall.ts @@ -72,6 +72,7 @@ export class OutgoingSipCall extends BaseSipCall { callee: signedCallee, calleeAgent, callerAgent, + features: ['audio'], }); const channel = await calleeAgent.getOrCreateChannel(call, session.sessionId); @@ -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'], }); } diff --git a/packages/core-typings/src/mediaCalls/IMediaCall.ts b/packages/core-typings/src/mediaCalls/IMediaCall.ts index 40f130e01a331..b726495500543 100644 --- a/packages/core-typings/src/mediaCalls/IMediaCall.ts +++ b/packages/core-typings/src/mediaCalls/IMediaCall.ts @@ -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[]; } diff --git a/packages/media-signaling/src/definition/call/IClientMediaCall.ts b/packages/media-signaling/src/definition/call/IClientMediaCall.ts index 475d8b8600aca..900915b0fcc5a 100644 --- a/packages/media-signaling/src/definition/call/IClientMediaCall.ts +++ b/packages/media-signaling/src/definition/call/IClientMediaCall.ts @@ -18,6 +18,10 @@ export type CallRole = 'caller' | 'callee'; export type CallService = 'webrtc'; +export type CallFeature = 'audio'; + +export const callFeatureList: readonly CallFeature[] = ['audio']; + 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 @@ -113,4 +117,5 @@ export interface IClientMediaCall { sendDTMF(dtmf: string, duration?: number): void; getStats(selector?: MediaStreamTrack | null): Promise; + isFeatureAvailable(feature: CallFeature): boolean; } diff --git a/packages/media-signaling/src/definition/signals/client/answer.ts b/packages/media-signaling/src/definition/signals/client/answer.ts index 61e9ad8e89765..9b14771a1cda3 100644 --- a/packages/media-signaling/src/definition/signals/client/answer.ts +++ b/packages/media-signaling/src/definition/signals/client/answer.ts @@ -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 = { @@ -9,6 +10,8 @@ export type ClientMediaSignalAnswer = { contractId: string; answer: CallAnswer; + + supportedFeatures?: CallFeature[]; }; export const clientMediaSignalAnswerSchema: JSONSchemaType = { @@ -33,6 +36,15 @@ export const clientMediaSignalAnswerSchema: JSONSchemaType = { @@ -57,6 +59,15 @@ export const clientMediaSignalRequestCallSchema: JSONSchemaType { + public async requestCall( + callee: { type: CallActorType; id: string }, + supportedFeatures: CallFeature[], + contactInfo?: CallContact, + ): Promise { if (this.initialized) { return; } @@ -288,6 +297,7 @@ export class ClientMediaCall implements IClientMediaCall { this.config.transporter.sendToServer(this.callId, 'request-call', { callee, supportedServices: Object.keys(this.config.processorFactories) as CallService[], + supportedFeatures, }); return this.initializeOutboundCall({ ...contactInfo, ...callee }); @@ -530,7 +540,7 @@ export class ClientMediaCall implements IClientMediaCall { } this.acceptedLocally = true; - this.config.transporter.answer(this.callId, 'accept'); + this.config.transporter.answer(this.callId, 'accept', { supportedFeatures: this.config.supportedFeatures }); if (this.getClientState() === 'accepting') { this.updateStateTimeouts(); @@ -735,6 +745,14 @@ export class ClientMediaCall implements IClientMediaCall { return this.webrtcProcessor?.getStats(selector) ?? null; } + public isFeatureAvailable(feature: CallFeature): boolean { + if (!this.enabledFeatures) { + return false; + } + + return this.enabledFeatures.includes(feature); + } + private changeState(newState: CallState): void { if (newState === this._state) { return; @@ -960,7 +978,7 @@ export class ClientMediaCall implements IClientMediaCall { switch (signal.notification) { case 'accepted': - return this.flagAsAccepted(); + return this.flagAsAccepted(signal.features); case 'active': if (this.state === 'accepted' || this.hidden) { this.changeState('active'); @@ -972,9 +990,13 @@ export class ClientMediaCall implements IClientMediaCall { } } - private async flagAsAccepted(): Promise { + private async flagAsAccepted(enabledFeatures?: CallFeature[]): Promise { this.config.logger?.debug('ClientMediaCall.flagAsAccepted'); + if (enabledFeatures && this._state !== 'accepted') { + this.enabledFeatures = enabledFeatures; + } + // If hidden, just move the state without doing anything if (this.hidden) { this.changeState('accepted'); diff --git a/packages/media-signaling/src/lib/Session.ts b/packages/media-signaling/src/lib/Session.ts index 3e52ba182c0b1..3954c07462269 100644 --- a/packages/media-signaling/src/lib/Session.ts +++ b/packages/media-signaling/src/lib/Session.ts @@ -10,7 +10,7 @@ import type { RandomStringFactory, ServerMediaSignal, } from '../definition'; -import type { IClientMediaCall, CallActorType, CallContact } from '../definition/call'; +import type { IClientMediaCall, CallActorType, CallContact, CallFeature } from '../definition/call'; import type { IMediaSignalLogger } from '../definition/logger'; export type MediaSignalingEvents = { @@ -31,6 +31,7 @@ export type MediaSignalingSessionConfig = { transport: MediaSignalTransport; iceGatheringTimeout?: number; iceServers?: RTCIceServer[]; + features: CallFeature[]; }; const STATE_REPORT_INTERVAL = 60000; @@ -206,7 +207,7 @@ export class MediaSignalingSession extends Emitter { const callId = this.createTemporaryCallId(); const call = this.createCall(callId); - await call.requestCall({ type: calleeType, id: calleeId }, contactInfo); + await call.requestCall({ type: calleeType, id: calleeId }, this.config.features, contactInfo); } public register(): void { @@ -486,6 +487,7 @@ export class MediaSignalingSession extends Emitter { iceGatheringTimeout: this.config.iceGatheringTimeout || 5000, iceServers: this.config.iceServers || [], sessionId: this._sessionId, + supportedFeatures: this.config.features, }; const call = new ClientMediaCall(config, callId, { inputTrack: this.inputTrack }); diff --git a/packages/media-signaling/src/lib/TransportWrapper.ts b/packages/media-signaling/src/lib/TransportWrapper.ts index 8e2d5dda945ab..ae8234d9c1426 100644 --- a/packages/media-signaling/src/lib/TransportWrapper.ts +++ b/packages/media-signaling/src/lib/TransportWrapper.ts @@ -1,4 +1,4 @@ -import type { CallAnswer, CallHangupReason } from '../definition'; +import type { CallAnswer, CallFeature, CallHangupReason } from '../definition'; import type { IMediaSignalLogger } from '../definition/logger'; import type { MediaSignalTransport, @@ -36,8 +36,8 @@ export class MediaSignalTransportWrapper { }); } - public answer(callId: string, answer: CallAnswer) { - return this.sendToServer(callId, 'answer', { answer }); + public answer(callId: string, answer: CallAnswer, extraData: { supportedFeatures?: CallFeature[] } = {}) { + return this.sendToServer(callId, 'answer', { answer, ...extraData }); } public hangup(callId: string, reason: CallHangupReason) { diff --git a/packages/model-typings/src/models/IMediaCallsModel.ts b/packages/model-typings/src/models/IMediaCallsModel.ts index 4537e37a0aa7c..a290fc73c3f33 100644 --- a/packages/model-typings/src/models/IMediaCallsModel.ts +++ b/packages/model-typings/src/models/IMediaCallsModel.ts @@ -10,7 +10,7 @@ export interface IMediaCallsModel extends IBaseModel { options?: FindOptions, ): Promise; startRingingById(callId: string, expiresAt: Date): Promise; - acceptCallById(callId: string, data: { calleeContractId: string }, expiresAt: Date): Promise; + acceptCallById(callId: string, data: { calleeContractId: string; supportedFeatures: string[] }, expiresAt: Date): Promise; activateCallById(callId: string, expiresAt: Date): Promise; setExpiresAtById(callId: string, expiresAt: Date): Promise; hangupCallById(callId: string, params: { endedBy?: IMediaCall['endedBy']; reason?: string } | undefined): Promise; diff --git a/packages/models/src/models/MediaCalls.ts b/packages/models/src/models/MediaCalls.ts index 8f52333aa36db..ab02a344fb91f 100644 --- a/packages/models/src/models/MediaCalls.ts +++ b/packages/models/src/models/MediaCalls.ts @@ -70,7 +70,11 @@ export class MediaCallsRaw extends BaseRaw implements IMediaCallsMod ); } - public async acceptCallById(callId: string, data: { calleeContractId: string }, expiresAt: Date): Promise { + public async acceptCallById( + callId: string, + data: { calleeContractId: string; supportedFeatures: string[] }, + expiresAt: Date, + ): Promise { const { calleeContractId } = data; return this.updateOne( @@ -85,6 +89,11 @@ export class MediaCallsRaw extends BaseRaw implements IMediaCallsMod 'acceptedAt': new Date(), expiresAt, }, + $pull: { + features: { + $nin: data.supportedFeatures, + }, + }, }, ); } diff --git a/packages/ui-voip/src/context/useMediaSessionInstance.ts b/packages/ui-voip/src/context/useMediaSessionInstance.ts index c70d550e8af35..39b19593017a2 100644 --- a/packages/ui-voip/src/context/useMediaSessionInstance.ts +++ b/packages/ui-voip/src/context/useMediaSessionInstance.ts @@ -120,6 +120,7 @@ class MediaSessionStore extends Emitter<{ change: void }> { randomStringFactory, oldSessionId: this.getOldSessionId(userId), logger: new MediaCallLogger(), + features: ['audio'], }); if (window.sessionStorage) { From 9fda6dc5b67f21ccd3d234e57f132bc46ab34689 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Tue, 10 Feb 2026 09:57:22 -0300 Subject: [PATCH 2/3] test data --- apps/meteor/server/startup/callHistoryTestData.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/meteor/server/startup/callHistoryTestData.ts b/apps/meteor/server/startup/callHistoryTestData.ts index 2e18f74d959b7..eb2099cc18056 100644 --- a/apps/meteor/server/startup/callHistoryTestData.ts +++ b/apps/meteor/server/startup/callHistoryTestData.ts @@ -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, @@ -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, @@ -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, @@ -229,6 +232,7 @@ export async function addCallHistoryTestData(uid: string, extraUid: string): Pro acceptedAt: new Date(), activatedAt: new Date(), uids: [uid], + features: ['audio'], }, ]); } From 087dd0a412c692203103b4b130ba9c6fc9dd9ca8 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 11 Feb 2026 10:48:55 -0300 Subject: [PATCH 3/3] improve type --- .../media-signaling/src/definition/call/IClientMediaCall.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/media-signaling/src/definition/call/IClientMediaCall.ts b/packages/media-signaling/src/definition/call/IClientMediaCall.ts index 900915b0fcc5a..e96be078b6859 100644 --- a/packages/media-signaling/src/definition/call/IClientMediaCall.ts +++ b/packages/media-signaling/src/definition/call/IClientMediaCall.ts @@ -18,9 +18,9 @@ export type CallRole = 'caller' | 'callee'; export type CallService = 'webrtc'; -export type CallFeature = 'audio'; +export const callFeatureList = ['audio'] as const; -export const callFeatureList: readonly CallFeature[] = ['audio']; +export type CallFeature = (typeof callFeatureList)[number]; export type CallState = | 'none' // trying to call with no idea if it'll reach anyone