[MM-45749] Support for advanced ICE server configs and TURN credentials (#6544)

* Support for advanced ICE server configs and TURN credentials

* PR comments

* merge conflicts

Co-authored-by: Christopher Poile <cpoile@gmail.com>
This commit is contained in:
Claudio Costa 2022-08-10 18:33:02 +02:00 committed by GitHub
parent a2a3f4940d
commit c71b28a6a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 153 additions and 32 deletions

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ServerChannelState, ServerCallsConfig, ApiResp} from '@calls/types/calls';
import type {ServerChannelState, ServerCallsConfig, ApiResp, ICEServersConfigs} from '@calls/types/calls';
export interface ClientCallsMix {
getEnabled: () => Promise<Boolean>;
@ -9,6 +9,7 @@ export interface ClientCallsMix {
getCallsConfig: () => Promise<ServerCallsConfig>;
enableChannelCalls: (channelId: string, enable: boolean) => Promise<ServerChannelState>;
endCall: (channelId: string) => Promise<ApiResp>;
genTURNCredentials: () => Promise<ICEServersConfigs>;
}
const ClientCalls = (superclass: any) => class extends superclass {
@ -51,6 +52,13 @@ const ClientCalls = (superclass: any) => class extends superclass {
{method: 'post'},
);
};
genTURNCredentials = async () => {
return this.doFetch(
`${this.getCallsRoute()}/turn-credentials`,
{method: 'get'},
);
};
};
export default ClientCalls;

View file

@ -12,15 +12,17 @@ import {
mediaDevices,
} from 'react-native-webrtc';
import {CallsConnection} from '@calls/types/calls';
import {getICEServersConfigs} from '@calls/utils';
import {WebsocketEvents} from '@constants';
import {getServerCredentials} from '@init/credentials';
import NetworkManager from '@managers/network_manager';
import {logError, logDebug} from '@utils/log';
import {logError, logDebug, logWarning} from '@utils/log';
import Peer from './simple-peer';
import {WebSocketClient, wsReconnectionTimeoutErr} from './websocket_client';
import type {CallsConnection} from '@calls/types/calls';
const websocketConnectTimeout = 3000;
export async function newConnection(serverUrl: string, channelID: string, closeCb: () => void, setScreenShareURL: (url: string) => void) {
@ -169,9 +171,18 @@ export async function newConnection(serverUrl: string, channelID: string, closeC
return;
}
const iceConfigs = getICEServersConfigs(config);
if (config.NeedsTURNCredentials) {
try {
iceConfigs.push(...await client.genTURNCredentials());
} catch (err) {
logWarning('failed to fetch TURN credentials:', err);
}
}
InCallManager.start({media: 'audio'});
InCallManager.stopProximitySensor();
peer = new Peer(null, config.ICEServers);
peer = new Peer(null, iceConfigs);
peer.on('signal', (data: any) => {
if (data.type === 'offer' || data.type === 'answer') {
ws.send('sdp', {

View file

@ -21,6 +21,8 @@ import {
} from 'react-native-webrtc';
import stream from 'readable-stream';
import type {ICEServersConfigs} from '@calls/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) {
@ -539,11 +530,9 @@ export default class Peer extends stream.Duplex {
};
this.channel.onerror = (e: any) => {
const err =
e.error instanceof Error ?
e.error :
new Error(
`Datachannel error: ${e.message} ${e.filename}:${e.lineno}:${e.colno}`,
);
e.error instanceof Error ? e.error : new Error(
`Datachannel error: ${e.message} ${e.filename}:${e.lineno}:${e.colno}`,
);
this.destroy(errCode(err, 'ERR_DATA_CHANNEL'));
};

View file

@ -589,9 +589,18 @@ describe('useCallsState', () => {
it('config', () => {
const newConfig = {
ICEServers: ['google.com'],
ICEServers: [],
ICEServersConfigs: [
{
urls: ['stun:stun.example.com:3478'],
},
{
urls: ['turn:turn.example.com:3478'],
},
],
AllowEnableCalls: true,
DefaultEnabled: true,
NeedsTURNCredentials: false,
last_retrieved_at: 123,
};

View file

@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import type UserModel from '@typings/database/models/servers/user';
import type {ConfigurationParamWithUrls, ConfigurationParamWithUrl} from 'react-native-webrtc';
export type CallsState = {
serverUrl: string;
@ -91,27 +92,30 @@ export type CallsConnection = {
}
export type ServerCallsConfig = {
ICEServers: string[];
ICEServers?: string[]; // deprecated
ICEServersConfigs?: ICEServersConfigs;
AllowEnableCalls: boolean;
DefaultEnabled: boolean;
NeedsTURNCredentials: boolean;
}
export type CallsConfig = {
export type CallsConfig = ServerCallsConfig & {
pluginEnabled: boolean;
ICEServers: string[];
AllowEnableCalls: boolean;
DefaultEnabled: boolean;
last_retrieved_at: number;
}
export const DefaultCallsConfig = {
pluginEnabled: false,
ICEServers: [],
ICEServers: [], // deprecated
ICEServersConfigs: [],
AllowEnableCalls: false,
DefaultEnabled: false,
NeedsTURNCredentials: false,
last_retrieved_at: 0,
} as CallsConfig;
export type ICEServersConfigs = Array<ConfigurationParamWithUrls | ConfigurationParamWithUrl>;
export type ApiResp = {
message?: string;
detailed_error?: string;

View file

@ -0,0 +1,80 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import assert from 'assert';
import {getICEServersConfigs} from './utils';
describe('getICEServersConfigs', () => {
it('backwards compatible case, no ICEServersConfigs present', () => {
const config = {
ICEServers: ['stun:stun.example.com:3478'],
AllowEnableCalls: true,
DefaultEnabled: true,
NeedsTURNCredentials: false,
last_retrieved_at: 0,
};
const iceConfigs = getICEServersConfigs(config);
assert.deepEqual(iceConfigs, [
{
urls: ['stun:stun.example.com:3478'],
},
]);
});
it('ICEServersConfigs set', () => {
const config = {
ICEServersConfigs: [
{
urls: ['stun:stun.example.com:3478'],
},
{
urls: ['turn:turn.example.com:3478'],
},
],
AllowEnableCalls: true,
DefaultEnabled: true,
NeedsTURNCredentials: false,
last_retrieved_at: 0,
};
const iceConfigs = getICEServersConfigs(config);
assert.deepEqual(iceConfigs, [
{
urls: ['stun:stun.example.com:3478'],
},
{
urls: ['turn:turn.example.com:3478'],
},
]);
});
it('Both ICEServers and ICEServersConfigs set', () => {
const config = {
ICEServers: ['stun:stuna.example.com:3478'],
ICEServersConfigs: [
{
urls: ['stun:stunb.example.com:3478'],
},
{
urls: ['turn:turn.example.com:3478'],
},
],
AllowEnableCalls: true,
DefaultEnabled: true,
NeedsTURNCredentials: false,
last_retrieved_at: 0,
};
const iceConfigs = getICEServersConfigs(config);
assert.deepEqual(iceConfigs, [
{
urls: ['stun:stunb.example.com:3478'],
},
{
urls: ['turn:turn.example.com:3478'],
},
]);
});
});

View file

@ -3,11 +3,12 @@
import {Alert} from 'react-native';
import {CallParticipant} from '@calls/types/calls';
import {Post, Calls} from '@constants';
import {Post} from '@constants';
import Calls from '@constants/calls';
import {isMinimumServerVersion} from '@utils/helpers';
import {displayUsername} from '@utils/user';
import type {CallParticipant, ServerCallsConfig} from '@calls/types/calls';
import type PostModel from '@typings/database/models/servers/post';
import type {IntlShape} from 'react-intl';
@ -104,3 +105,22 @@ export function errorAlert(error: string, intl: IntlShape) {
}, {error}),
);
}
export function getICEServersConfigs(config: ServerCallsConfig) {
// if ICEServersConfigs is set, we can trust this to be complete and
// coming from an updated API.
if (config.ICEServersConfigs && config.ICEServersConfigs.length > 0) {
return config.ICEServersConfigs;
}
// otherwise we revert to using the now deprecated field.
if (config.ICEServers && config.ICEServers.length > 0) {
return [
{
urls: config.ICEServers,
},
];
}
return [];
}

View file

@ -168,7 +168,7 @@ declare module 'react-native-webrtc' {
}
export interface RTCPeerConnectionConfiguration {
iceServers: ConfigurationParamWithUrls[] | ConfigurationParamWithUrl[];
iceServers: Array<ConfigurationParamWithUrls | ConfigurationParamWithUrl>;
iceTransportPolicy?: 'all' | 'relay' | 'nohost' | 'none' | undefined;
bundlePolicy?: 'balanced' | 'max-compat' | 'max-bundle' | undefined;
rtcpMuxPolicy?: 'negotiate' | 'require' | undefined;