From ee38dd39ead66a9a8b6082bf9927c488f3b2e65a Mon Sep 17 00:00:00 2001 From: Claudio Costa Date: Thu, 16 Jun 2022 14:15:17 +0200 Subject: [PATCH] Add support for ICEServersConfigs (#6371) --- app/products/calls/connection.ts | 3 +- app/products/calls/simple-peer.ts | 17 ++------ .../calls/store/actions/calls.test.js | 5 ++- app/products/calls/store/actions/calls.ts | 3 +- .../calls/store/reducers/calls.test.js | 2 +- .../calls/store/selectors/calls.test.js | 43 +++++++++++++++++++ app/products/calls/store/selectors/calls.ts | 24 ++++++++++- app/products/calls/store/types/calls.ts | 6 +++ 8 files changed, 85 insertions(+), 18 deletions(-) diff --git a/app/products/calls/connection.ts b/app/products/calls/connection.ts index c982dad8a..4ec440a54 100644 --- a/app/products/calls/connection.ts +++ b/app/products/calls/connection.ts @@ -14,6 +14,7 @@ import { import {Client4} from '@client/rest'; import {WebsocketEvents} from '@constants'; +import {ICEServersConfigs} from '@mmproducts/calls/store/types/calls'; import Peer from './simple-peer'; import WebSocketClient from './websocket'; @@ -22,7 +23,7 @@ export let client: any = null; const websocketConnectTimeout = 3000; -export async function newClient(channelID: string, iceServers: string[], closeCb: () => void, setScreenShareURL: (url: string) => void) { +export async function newClient(channelID: string, iceServers: ICEServersConfigs, closeCb: () => void, setScreenShareURL: (url: string) => void) { let peer: Peer | null = null; let stream: MediaStream; let voiceTrackAdded = false; diff --git a/app/products/calls/simple-peer.ts b/app/products/calls/simple-peer.ts index b1e5a07ce..6af974469 100644 --- a/app/products/calls/simple-peer.ts +++ b/app/products/calls/simple-peer.ts @@ -21,6 +21,8 @@ import { } from 'react-native-webrtc'; import stream from 'readable-stream'; +import {ICEServersConfigs} from '@mmproducts/calls/store/types/calls'; + const queueMicrotask = (callback: any) => { Promise.resolve().then(callback).catch((e) => setTimeout(() => { throw e; @@ -94,7 +96,7 @@ export default class Peer extends stream.Duplex { private pc: RTCPeerConnection|null = null; private onFinishBound?: () => void; - constructor(localStream: MediaStream | null, iceServers?: string[]) { + constructor(localStream: MediaStream | null, iceServers: ICEServersConfigs) { super({allowHalfOpen: false}); this.streams = localStream ? [localStream] : []; @@ -104,21 +106,10 @@ export default class Peer extends stream.Duplex { }; const connConfig = { - iceServers: [ - { - urls: [ - 'stun:stun.l.google.com:19302', - 'stun:global.stun.twilio.com:3478', - ], - }, - ], + iceServers, sdpSemantics: 'unified-plan', }; - if (iceServers && iceServers.length > 0) { - connConfig.iceServers[0].urls = iceServers; - } - try { this.pc = new RTCPeerConnection(connConfig); } catch (err) { diff --git a/app/products/calls/store/actions/calls.test.js b/app/products/calls/store/actions/calls.test.js index d09dfdc99..9a97a8135 100644 --- a/app/products/calls/store/actions/calls.test.js +++ b/app/products/calls/store/actions/calls.test.js @@ -34,7 +34,10 @@ jest.mock('@client/rest', () => ({ }, ]), getCallsConfig: jest.fn(() => ({ - ICEServers: ['mattermost.com'], + ICEServersConfigs: [{ + urls: 'stun:stun1.example.com', + }, + ], AllowEnableCalls: true, DefaultEnabled: true, last_retrieved_at: 1234, diff --git a/app/products/calls/store/actions/calls.ts b/app/products/calls/store/actions/calls.ts index 931e19d15..076bff2a2 100644 --- a/app/products/calls/store/actions/calls.ts +++ b/app/products/calls/store/actions/calls.ts @@ -28,6 +28,7 @@ import CallsTypes from '@mmproducts/calls/store/action_types/calls'; import { getCallInCurrentChannel, getConfig, + getICEServersConfigs, getNumCurrentConnectedParticipants, } from '@mmproducts/calls/store/selectors/calls'; import {Call, CallParticipant, DefaultServerConfig} from '@mmproducts/calls/store/types/calls'; @@ -201,7 +202,7 @@ export function joinCall(channelId: string, intl: typeof intlShape): ActionFunc dispatch(setSpeakerphoneOn(false)); try { - ws = await newClient(channelId, getConfig(getState()).ICEServers, () => { + ws = await newClient(channelId, getICEServersConfigs(getState()), () => { dispatch(setSpeakerphoneOn(false)); dispatch({type: CallsTypes.RECEIVED_MYSELF_LEFT_CALL}); }, setScreenShareURL); diff --git a/app/products/calls/store/reducers/calls.test.js b/app/products/calls/store/reducers/calls.test.js index 170152b33..a723cccea 100644 --- a/app/products/calls/store/reducers/calls.test.js +++ b/app/products/calls/store/reducers/calls.test.js @@ -408,7 +408,7 @@ describe('Reducers.calls.config', () => { const testAction = { type: CallsTypes.RECEIVED_CONFIG, data: { - ICEServers: ['google.com'], + ICEServers: ['stun:stun.example.com'], AllowEnableCalls: true, DefaultEnabled: true, last_retrieved_at: 123, diff --git a/app/products/calls/store/selectors/calls.test.js b/app/products/calls/store/selectors/calls.test.js index 9b98d68e8..c9040f713 100644 --- a/app/products/calls/store/selectors/calls.test.js +++ b/app/products/calls/store/selectors/calls.test.js @@ -197,4 +197,47 @@ describe('Selectors.Calls', () => { // On cloud, MaxCallParticipants missing, default should be used. assert.equal(Selectors.isLimitRestricted(newState, 'call1'), true); }); + + it('getICEServersConfigs', () => { + assert.deepEqual(Selectors.getICEServersConfigs(testState), []); + + // backwards compatible case, no ICEServersConfigs present. + let newState = { + ...testState, + entities: { + ...testState.entities, + calls: { + ...testState.entities.calls, + config: { + ...testState.entities.calls.config, + ICEServers: ['stun:stun1.example.com'], + }, + }, + }, + }; + assert.deepEqual(Selectors.getICEServersConfigs(newState), [{urls: ['stun:stun1.example.com']}]); + + // ICEServersConfigs defined case + newState = { + ...testState, + entities: { + ...testState.entities, + calls: { + ...testState.entities.calls, + config: { + ...testState.entities.calls.config, + ICEServers: ['stun:stun1.example.com'], + ICEServersConfigs: [ + {urls: 'stun:stun1.example.com'}, + {urls: 'turn:turn.example.com', username: 'username', credentail: 'password'}, + ], + }, + }, + }, + }; + assert.deepEqual(Selectors.getICEServersConfigs(newState), [ + {urls: 'stun:stun1.example.com'}, + {urls: 'turn:turn.example.com', username: 'username', credentail: 'password'}, + ]); + }); }); diff --git a/app/products/calls/store/selectors/calls.ts b/app/products/calls/store/selectors/calls.ts index d456ee34c..08d73f948 100644 --- a/app/products/calls/store/selectors/calls.ts +++ b/app/products/calls/store/selectors/calls.ts @@ -9,7 +9,7 @@ import {getCurrentChannelId} from '@mm-redux/selectors/entities/common'; import {getLicense, getServerVersion} from '@mm-redux/selectors/entities/general'; import {GlobalState} from '@mm-redux/types/store'; import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; -import {Call} from '@mmproducts/calls/store/types/calls'; +import {Call, ICEServersConfigs} from '@mmproducts/calls/store/types/calls'; export function getConfig(state: GlobalState) { return state.entities.calls.config; @@ -114,3 +114,25 @@ export const isLimitRestricted: (state: GlobalState, channelId?: string) => bool return max !== 0 && numParticipants >= max; }, ); + +export const getICEServersConfigs: (state: GlobalState) => ICEServersConfigs = createSelector( + getConfig, + (config) => { + // if ICEServersConfigs is set, we can trust this to be complete and + // coming from an updated API. + if (config.ICEServersConfigs?.length > 0) { + return config.ICEServersConfigs; + } + + // otherwise we revert to using the now deprecated field. + if (config.ICEServers?.length > 0) { + return [ + { + urls: config.ICEServers, + }, + ]; + } + + return []; + }, +); diff --git a/app/products/calls/store/types/calls.ts b/app/products/calls/store/types/calls.ts index 83dcdfdcb..e56f9bc93 100644 --- a/app/products/calls/store/types/calls.ts +++ b/app/products/calls/store/types/calls.ts @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {ConfigurationParamWithUrls, ConfigurationParamWithUrl} from 'react-native-webrtc'; + import {UserProfile} from '@mm-redux/types/users'; import {Dictionary} from '@mm-redux/types/utilities'; @@ -60,6 +62,7 @@ export type VoiceEventData = { export type ServerConfig = { ICEServers: string[]; + ICEServersConfigs: ICEServersConfigs; AllowEnableCalls: boolean; DefaultEnabled: boolean; MaxCallParticipants: number; @@ -69,9 +72,12 @@ export type ServerConfig = { export const DefaultServerConfig = { ICEServers: [], + ICEServersConfigs: [], AllowEnableCalls: false, DefaultEnabled: false, MaxCallParticipants: 0, sku_short_name: '', last_retrieved_at: 0, } as ServerConfig; + +export type ICEServersConfigs = ConfigurationParamWithUrls[] | ConfigurationParamWithUrl[];