Skip to content

Commit 1741a20

Browse files
feat: Voice Call - Native Screen Sharing (#37922)
Co-authored-by: Pierre Lehnen <[email protected]>
1 parent 9db6725 commit 1741a20

File tree

89 files changed

+1977
-211
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+1977
-211
lines changed

.changeset/many-glasses-care.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@rocket.chat/meteor": minor
3+
"@rocket.chat/i18n": minor
4+
"@rocket.chat/media-signaling": minor
5+
"@rocket.chat/ui-voip": minor
6+
"@rocket.chat/media-calls": minor
7+
---
8+
9+
Introduces native screen sharing for internal voice calls. This feature is currently in beta and can be disabled through admin settings.

apps/meteor/client/providers/MediaCallProvider.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const MediaCallProvider = ({ children }: { children: ReactNode }) => {
1414

1515
const unauthorizedContextValue = useMemo(
1616
() => ({
17+
inRoomView: false,
18+
setInRoomView: () => undefined,
1719
instance: undefined,
1820
signalEmitter: new Emitter<any>(),
1921
audioElement: undefined,

apps/meteor/client/views/room/Room.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import RoomE2EESetup from './E2EESetup/RoomE2EESetup';
1212
import Header from './Header';
1313
import MessageHighlightProvider from './MessageList/providers/MessageHighlightProvider';
1414
import RoomInvite from './RoomInvite';
15+
import MediaCallRoom from './body/MediaCallRoom';
1516
import RoomBody from './body/RoomBody';
1617
import { useRoom, useRoomSubscription } from './contexts/RoomContext';
1718
import { useAppsContextualBar } from './hooks/useAppsContextualBar';
@@ -54,7 +55,15 @@ const Room = (): ReactElement => {
5455
data-qa-rc-room={room._id}
5556
aria-label={roomLabel}
5657
header={<Header room={room} />}
57-
body={shouldDisplayE2EESetup ? <RoomE2EESetup /> : <RoomBody />}
58+
body={
59+
shouldDisplayE2EESetup ? (
60+
<RoomE2EESetup />
61+
) : (
62+
<MediaCallRoom>
63+
<RoomBody />
64+
</MediaCallRoom>
65+
)
66+
}
5867
aside={
5968
(toolbox.tab?.tabComponent && (
6069
<ErrorBoundary fallback={null}>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { IRoom } from '@rocket.chat/core-typings';
2+
import { isDirectMessageRoom } from '@rocket.chat/core-typings';
3+
import type { PeerInfo } from '@rocket.chat/ui-voip';
4+
import {
5+
MediaCallRoomActivity,
6+
usePeekMediaSessionState,
7+
usePeekMediaSessionPeerInfo,
8+
usePeekMediaSessionFeatures,
9+
} from '@rocket.chat/ui-voip';
10+
import type { ReactNode } from 'react';
11+
import { memo } from 'react';
12+
13+
import { useRoom } from '../contexts/RoomContext';
14+
15+
const isMediaCallRoom = (room: IRoom, peerInfo?: PeerInfo) => {
16+
if (!peerInfo || 'number' in peerInfo) {
17+
return false;
18+
}
19+
if (!isDirectMessageRoom(room)) {
20+
return false;
21+
}
22+
if (room.uids?.length !== 2) {
23+
return false;
24+
}
25+
26+
return room.uids.includes(peerInfo.userId);
27+
};
28+
29+
type MediaCallRoomProps = {
30+
children: ReactNode;
31+
};
32+
33+
const MediaCallRoom = ({ children }: MediaCallRoomProps) => {
34+
const state = usePeekMediaSessionState();
35+
const peerInfo = usePeekMediaSessionPeerInfo();
36+
const features = usePeekMediaSessionFeatures();
37+
const room = useRoom();
38+
39+
const screenShareEnabled = features.includes('screen-share');
40+
41+
if (!screenShareEnabled) {
42+
return children;
43+
}
44+
45+
if (state !== 'ongoing' || !isMediaCallRoom(room, peerInfo)) {
46+
return children;
47+
}
48+
49+
return <MediaCallRoomActivity>{children}</MediaCallRoomActivity>;
50+
};
51+
52+
export default memo(MediaCallRoom);

apps/meteor/client/views/room/layout/RoomLayout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const RoomLayout = ({ header, body, footer, aside, ...props }: RoomLayoutProps):
6060
<Suspense fallback={<HeaderSkeleton />}>{header}</Suspense>
6161
<Box display='flex' flexGrow={1} overflow='hidden' height='full' position='relative'>
6262
<Box display='flex' flexDirection='column' flexGrow={1} minWidth={0}>
63-
<Box is='div' display='flex' flexDirection='column' flexGrow={1}>
63+
<Box is='div' display='flex' flexDirection='column' flexGrow={1} maxHeight='100%'>
6464
<Suspense fallback={null}>{body}</Suspense>
6565
</Box>
6666
{footer && <Suspense fallback={null}>{footer}</Suspense>}

apps/meteor/ee/server/settings/voip.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ export function addSettings(): Promise<void> {
99
},
1010
async function () {
1111
await this.section('VoIP_TeamCollab_WebRTC', async function () {
12+
await this.add('VoIP_TeamCollab_Screen_Sharing_Enabled', true, {
13+
type: 'boolean',
14+
public: true,
15+
invalidValue: false,
16+
alert: 'VoIP_TeamCollab_Screen_Sharing_Enabled_Alert',
17+
i18nDescription: 'VoIP_TeamCollab_Screen_Sharing_Enabled_Description',
18+
});
19+
1220
await this.add('VoIP_TeamCollab_Ice_Servers', 'stun:stun.l.google.com:19302', {
1321
type: 'string',
1422
public: true,

apps/meteor/server/services/media-call/service.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
} from '@rocket.chat/core-typings';
1010
import { Logger } from '@rocket.chat/logger';
1111
import { callServer, type IMediaCallServerSettings } from '@rocket.chat/media-calls';
12-
import { isClientMediaSignal, type ClientMediaSignal, type ServerMediaSignal } from '@rocket.chat/media-signaling';
12+
import { type CallFeature, isClientMediaSignal, type ClientMediaSignal, type ServerMediaSignal } from '@rocket.chat/media-signaling';
1313
import type { InsertionModel } from '@rocket.chat/model-typings';
1414
import { CallHistory, MediaCalls, Rooms, Users } from '@rocket.chat/models';
1515
import { getHistoryMessagePayload } from '@rocket.chat/ui-voip/dist/ui-kit/getHistoryMessagePayload';
@@ -315,9 +315,22 @@ export class MediaCallService extends ServiceClassInternal implements IMediaCall
315315
},
316316
},
317317
permissionCheck: (uid, callType) => this.userHasMediaCallPermission(uid, callType),
318+
isFeatureAvailableForUser: (uid, feature) => this.userHasFeaturePermission(uid, feature),
318319
};
319320
}
320321

322+
private userHasFeaturePermission(_uid: IUser['_id'], feature: CallFeature): boolean {
323+
if (feature === 'audio') {
324+
return true;
325+
}
326+
327+
if (feature === 'screen-share') {
328+
return settings.get<boolean>('VoIP_TeamCollab_Screen_Sharing_Enabled') ?? false;
329+
}
330+
331+
return true;
332+
}
333+
321334
private async userHasMediaCallPermission(uid: IUser['_id'], callType: 'internal' | 'external' | 'any'): Promise<boolean> {
322335
if (callType === 'any') {
323336
return Authorization.hasAtLeastOnePermission(uid, ['allow-internal-voice-calls', 'allow-external-voice-calls']);

apps/meteor/tests/e2e/voice-calls-ee.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import { IS_EE } from './config/constants';
44
import { createAuxContext } from './fixtures/createAuxContext';
55
import { Users } from './fixtures/userStates';
66
import { HomeChannel } from './page-objects';
7+
import { setSettingValueById } from './utils';
78
import { expect, test } from './utils/test';
89

910
test.describe('Internal Voice Calls - Enterprise Edition', () => {
1011
test.skip(!IS_EE, 'Enterprise Edition Only');
1112
let sessions: { page: Page; poHomeChannel: HomeChannel }[];
1213

1314
test.beforeAll(async ({ api }) => {
15+
await setSettingValueById(api, 'VoIP_TeamCollab_Screen_Sharing_Enabled', false);
1416
await Promise.all([
1517
api.post('/users.setStatus', { status: 'online', username: 'user1' }),
1618
api.post('/users.setStatus', { status: 'online', username: 'user2' }),
@@ -24,6 +26,10 @@ test.describe('Internal Voice Calls - Enterprise Edition', () => {
2426
]);
2527
});
2628

29+
test.afterAll(async ({ api }) => {
30+
await setSettingValueById(api, 'VoIP_TeamCollab_Screen_Sharing_Enabled', true);
31+
});
32+
2733
test('should initiate voice call from direct message', async () => {
2834
const [user1, user2] = sessions;
2935

ee/packages/media-calls/src/definition/IMediaCallServer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { IUser } from '@rocket.chat/core-typings';
22
import type { Emitter } from '@rocket.chat/emitter';
3-
import type { ClientMediaSignal, ClientMediaSignalBody, ServerMediaSignal } from '@rocket.chat/media-signaling';
3+
import type { CallFeature, ClientMediaSignal, ClientMediaSignalBody, ServerMediaSignal } from '@rocket.chat/media-signaling';
44

55
import type { InternalCallParams } from './common';
66

@@ -30,6 +30,7 @@ export interface IMediaCallServerSettings {
3030
};
3131

3232
permissionCheck: (uid: IUser['_id'], callType: 'internal' | 'external' | 'any') => Promise<boolean>;
33+
isFeatureAvailableForUser: (uid: IUser['_id'], feature: CallFeature) => boolean;
3334
}
3435

3536
export interface IMediaCallServer {
@@ -52,4 +53,5 @@ export interface IMediaCallServer {
5253
requestCall(params: InternalCallParams): Promise<void>;
5354

5455
permissionCheck(uid: IUser['_id'], callType: 'internal' | 'external' | 'any'): Promise<boolean>;
56+
isFeatureAvailableForUser(uid: IUser['_id'], feature: CallFeature): boolean;
5557
}

ee/packages/media-calls/src/server/CallDirector.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ class MediaCallDirector {
203203
callerAgent.oppositeAgent = calleeAgent;
204204
calleeAgent.oppositeAgent = callerAgent;
205205

206+
const allowedFeatures = features.filter((feature) => getMediaCallServer().isFeatureAvailableForUser(caller.id, feature));
207+
206208
const call: Omit<IMediaCall, '_id' | '_updatedAt'> = {
207209
service,
208210
kind: 'direct',
@@ -225,7 +227,7 @@ class MediaCallDirector {
225227
...(requestedCallId && { callerRequestedId: requestedCallId }),
226228
...(parentCallId && { parentCallId }),
227229

228-
features,
230+
features: allowedFeatures,
229231
};
230232

231233
logger.debug({ msg: 'creating call', call });

0 commit comments

Comments
 (0)