Skip to content

Commit 75b9402

Browse files
chore: Implement base signaling system for media calls (RocketChat#36452)
1 parent 72d9474 commit 75b9402

104 files changed

Lines changed: 5980 additions & 6 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import { MediaCall } from '@rocket.chat/core-services';
12
import { License } from '@rocket.chat/license';
2-
import { Meteor } from 'meteor/meteor';
33

44
import { addSettings } from '../settings/voip';
55

6-
Meteor.startup(async () => {
7-
License.onValidateLicense(async () => {
8-
await addSettings();
9-
});
6+
License.onValidateLicense(async () => {
7+
await addSettings();
8+
9+
await MediaCall.hangupExpiredCalls();
1010
});

apps/meteor/ee/server/services/Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ COPY ./packages/core-typings packages/core-typings
1717
COPY ./packages/eslint-config packages/eslint-config
1818
COPY ./packages/tsconfig packages/tsconfig
1919
COPY ./packages/rest-typings packages/rest-typings
20+
COPY ./packages/media-signaling packages/media-signaling
2021
COPY ./packages/model-typings packages/model-typings
2122
COPY ./packages/models packages/models
2223

@@ -42,6 +43,9 @@ COPY --from=build /app/packages/core-typings/dist /app/packages/core-typings/dis
4243
COPY --from=build /app/packages/rest-typings/package.json /app/packages/rest-typings/package.json
4344
COPY --from=build /app/packages/rest-typings/dist /app/packages/rest-typings/dist
4445

46+
COPY --from=build /app/packages/media-signaling/package.json /app/packages/media-signaling/package.json
47+
COPY --from=build /app/packages/media-signaling/dist /app/packages/media-signaling/dist
48+
4549
COPY --from=build /app/packages/model-typings/package.json /app/packages/model-typings/package.json
4650
COPY --from=build /app/packages/model-typings/dist /app/packages/model-typings/dist
4751

apps/meteor/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,8 @@
271271
"@rocket.chat/log-format": "workspace:^",
272272
"@rocket.chat/logger": "workspace:^",
273273
"@rocket.chat/logo": "^0.32.2",
274+
"@rocket.chat/media-calls": "workspace:^",
275+
"@rocket.chat/media-signaling": "workspace:^",
274276
"@rocket.chat/memo": "~0.31.25",
275277
"@rocket.chat/message-parser": "workspace:^",
276278
"@rocket.chat/message-types": "workspace:~",

apps/meteor/server/models.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ import {
4343
LoginServiceConfigurationRaw,
4444
MatrixBridgedRoomRaw,
4545
MatrixBridgedUserRaw,
46+
MediaCallsRaw,
47+
MediaCallChannelsRaw,
48+
MediaCallNegotiationsRaw,
4649
MessageReadsRaw,
4750
MessagesRaw,
4851
MigrationsRaw,
@@ -134,6 +137,9 @@ registerModel('ILivechatVisitorsModel', new LivechatVisitorsRaw(db));
134137
registerModel('ILoginServiceConfigurationModel', new LoginServiceConfigurationRaw(db));
135138
registerModel('IMatrixBridgedRoomModel', new MatrixBridgedRoomRaw(db));
136139
registerModel('IMatrixBridgedUserModel', new MatrixBridgedUserRaw(db));
140+
registerModel('IMediaCallsModel', new MediaCallsRaw(db));
141+
registerModel('IMediaCallChannelsModel', new MediaCallChannelsRaw(db));
142+
registerModel('IMediaCallNegotiationsModel', new MediaCallNegotiationsRaw(db));
137143
registerModel('IMessageReadsModel', new MessageReadsRaw(db));
138144
registerModel('IMessagesModel', new MessagesRaw(db, trashCollection));
139145
registerModel('IMigrationsModel', new MigrationsRaw(db));

apps/meteor/server/modules/listeners/listeners.module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { EnterpriseSettings } from '@rocket.chat/core-services';
55
import { isSettingColor, isSettingEnterprise, UserStatus } from '@rocket.chat/core-typings';
66
import type { IUser, IRoom, IRole, VideoConference, ISetting, IOmnichannelRoom, IMessage, IOTRMessage } from '@rocket.chat/core-typings';
77
import { Logger } from '@rocket.chat/logger';
8+
import type { ServerMediaSignal } from '@rocket.chat/media-signaling';
89
import { parse } from '@rocket.chat/message-parser';
910

1011
import { settings } from '../../../app/settings/server/cached';
@@ -144,6 +145,10 @@ export class ListenersModule {
144145
},
145146
);
146147

148+
service.onEvent('user.media-signal', ({ userId, signal }: { userId: string; signal: ServerMediaSignal }) => {
149+
notifications.notifyUserInThisInstance(userId, 'media-signal', signal);
150+
});
151+
147152
service.onEvent('room.video-conference', ({ rid, callId }) => {
148153
/* deprecated */
149154
(notifications.notifyRoom as any)(rid, callId);

apps/meteor/server/modules/notifications/notifications.module.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Authorization, VideoConf } from '@rocket.chat/core-services';
1+
import { Authorization, MediaCall, VideoConf } from '@rocket.chat/core-services';
22
import type { ISubscription, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings';
33
import type { StreamerCallbackArgs, StreamKeys, StreamNames } from '@rocket.chat/ddp-client';
44
import { Rooms, Subscriptions, Users, Settings } from '@rocket.chat/models';
@@ -308,6 +308,17 @@ export class NotificationsModule {
308308
});
309309
}
310310

311+
if (e === 'media-calls') {
312+
if (!this.userId || !data || typeof data !== 'string') {
313+
return false;
314+
}
315+
316+
void MediaCall.processSerializedSignal(this.userId, data).catch(() => null);
317+
318+
// media call signals don't ever need to be broadcasted
319+
return false;
320+
}
321+
311322
return Boolean(this.userId);
312323
});
313324
this.streamUser.allowRead(async function (eventName) {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { api, ServiceClassInternal, type IMediaCallService } from '@rocket.chat/core-services';
2+
import type { IUser } from '@rocket.chat/core-typings';
3+
import { Logger } from '@rocket.chat/logger';
4+
import { callServer, type IMediaCallServerSettings } from '@rocket.chat/media-calls';
5+
import { isClientMediaSignal, type ClientMediaSignal, type ServerMediaSignal } from '@rocket.chat/media-signaling';
6+
import { MediaCalls } from '@rocket.chat/models';
7+
8+
import { settings } from '../../../app/settings/server';
9+
10+
const logger = new Logger('media-call service');
11+
12+
export class MediaCallService extends ServiceClassInternal implements IMediaCallService {
13+
protected name = 'media-call';
14+
15+
constructor() {
16+
super();
17+
callServer.emitter.on('signalRequest', ({ toUid, signal }) => this.sendSignal(toUid, signal));
18+
19+
this.onEvent('watch.settings', async ({ setting }): Promise<void> => {
20+
if (setting._id.startsWith('VoIP_TeamCollab_')) {
21+
setImmediate(() => this.configureMediaCallServer());
22+
}
23+
});
24+
25+
this.configureMediaCallServer();
26+
}
27+
28+
public async processSignal(uid: IUser['_id'], signal: ClientMediaSignal): Promise<void> {
29+
try {
30+
logger.debug({ msg: 'new client signal', type: signal.type, uid });
31+
callServer.receiveSignal(uid, signal);
32+
} catch (error) {
33+
logger.error({ msg: 'failed to process client signal', error, signal, uid });
34+
}
35+
}
36+
37+
public async processSerializedSignal(uid: IUser['_id'], signal: string): Promise<void> {
38+
try {
39+
logger.debug({ msg: 'new client signal', uid });
40+
41+
const deserialized = await this.deserializeClientSignal(signal);
42+
43+
callServer.receiveSignal(uid, deserialized);
44+
} catch (error) {
45+
logger.error({ msg: 'failed to process client signal', error, uid });
46+
}
47+
}
48+
49+
public async hangupExpiredCalls(): Promise<void> {
50+
await callServer.hangupExpiredCalls().catch((error) => {
51+
logger.error({ msg: 'Media Call Server failed to hangup expired calls', error });
52+
});
53+
54+
try {
55+
if (await MediaCalls.hasUnfinishedCalls()) {
56+
callServer.scheduleExpirationCheck();
57+
}
58+
} catch (error) {
59+
logger.error({ msg: 'Media Call Server failed to check if there are expired calls', error });
60+
}
61+
}
62+
63+
private async sendSignal(toUid: IUser['_id'], signal: ServerMediaSignal): Promise<void> {
64+
void api.broadcast('user.media-signal', { userId: toUid, signal });
65+
}
66+
67+
private configureMediaCallServer(): void {
68+
callServer.configure(this.getMediaServerSettings());
69+
}
70+
71+
private getMediaServerSettings(): IMediaCallServerSettings {
72+
const enabled = settings.get<boolean>('VoIP_TeamCollab_Enabled') ?? false;
73+
const sipEnabled = false;
74+
const forceSip = false;
75+
76+
return {
77+
enabled,
78+
internalCalls: {
79+
requireExtensions: forceSip,
80+
routeExternally: forceSip ? 'always' : 'never',
81+
},
82+
sip: {
83+
enabled: sipEnabled,
84+
},
85+
};
86+
}
87+
88+
private async deserializeClientSignal(serialized: string): Promise<ClientMediaSignal> {
89+
try {
90+
const signal = JSON.parse(serialized);
91+
if (!isClientMediaSignal(signal)) {
92+
throw new Error('signal-format-invalid');
93+
}
94+
return signal;
95+
} catch (error) {
96+
logger.error({ msg: 'Failed to parse client signal' }, error);
97+
throw error;
98+
}
99+
}
100+
}

apps/meteor/server/services/startup.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { DeviceManagementService } from './device-management/service';
1313
import { MediaService } from './image/service';
1414
import { ImportService } from './import/service';
1515
import { LDAPService } from './ldap/service';
16+
import { MediaCallService } from './media-call/service';
1617
import { MessageService } from './messages/service';
1718
import { MeteorService } from './meteor/service';
1819
import { NPSService } from './nps/service';
@@ -61,6 +62,7 @@ export const registerServices = async (): Promise<void> => {
6162
api.registerService(new ImportService());
6263
api.registerService(new OmnichannelAnalyticsService());
6364
api.registerService(new UserService());
65+
api.registerService(new MediaCallService());
6466

6567
// if the process is running in micro services mode we don't need to register services that will run separately
6668
if (!isRunningMs()) {

ee/apps/account-service/Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ COPY ./packages/core-typings/dist packages/core-typings/dist
2020
COPY ./packages/rest-typings/package.json packages/rest-typings/package.json
2121
COPY ./packages/rest-typings/dist packages/rest-typings/dist
2222

23+
COPY ./packages/media-signaling/package.json packages/media-signaling/package.json
24+
COPY ./packages/media-signaling/dist packages/media-signaling/dist
25+
2326
COPY ./packages/message-parser/package.json packages/message-parser/package.json
2427
COPY ./packages/message-parser/dist packages/message-parser/dist
2528
COPY ./packages/message-parser/messageParser.js packages/message-parser/messageParser.js

ee/apps/authorization-service/Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ COPY ./packages/core-typings/dist packages/core-typings/dist
2020
COPY ./packages/rest-typings/package.json packages/rest-typings/package.json
2121
COPY ./packages/rest-typings/dist packages/rest-typings/dist
2222

23+
COPY ./packages/media-signaling/package.json packages/media-signaling/package.json
24+
COPY ./packages/media-signaling/dist packages/media-signaling/dist
25+
2326
COPY ./packages/message-parser/package.json packages/message-parser/package.json
2427
COPY ./packages/message-parser/dist packages/message-parser/dist
2528
COPY ./packages/message-parser/messageParser.js packages/message-parser/messageParser.js

0 commit comments

Comments
 (0)