[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:
parent
a2a3f4940d
commit
c71b28a6a8
8 changed files with 153 additions and 32 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
80
app/products/calls/utils.test.ts
Normal file
80
app/products/calls/utils.test.ts
Normal 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'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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 [];
|
||||
}
|
||||
|
|
|
|||
2
types/modules/react-native-webrtc.d.ts
vendored
2
types/modules/react-native-webrtc.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue