diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..ef17c7d5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "search.exclude": { + "**/node_modules": false + } +} \ No newline at end of file diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 77278291..1da6f1e8 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -30,6 +30,7 @@ import Grid from '@material-ui/core/Grid'; import makeStyles from '@material-ui/core/styles/makeStyles'; import SupportLink from './SupportLink'; import Divider from '@material-ui/core/Divider'; +import { validateClientPeerConfig } from './validateClientPeerConfig'; // @ts-ignore import reverbOgx from 'arraybuffer-loader!../../static/reverb.ogx'; @@ -79,6 +80,19 @@ interface SocketError { message?: string; } +interface ClientPeerConfig { + forceRelayOnly: boolean; + iceServers: RTCIceServer[]; +} + +const DEFAULT_ICE_CONFIG: RTCConfiguration = { + iceServers: [ + { + urls: 'stun:stun.l.google.com:19302', + }, + ], +}; + function calculateVoiceAudio( state: AmongUsState, settings: ISettings, @@ -408,10 +422,44 @@ const Voice: React.FC = function ({ socket.on('connect', () => { setConnected(true); }); + socket.on('disconnect', () => { setConnected(false); }); + let iceConfig: RTCConfiguration = DEFAULT_ICE_CONFIG; + socket.on('clientPeerConfig', (clientPeerConfig: ClientPeerConfig) => { + if (!validateClientPeerConfig(clientPeerConfig)) { + let errorsFormatted = ''; + if (validateClientPeerConfig.errors) { + errorsFormatted = validateClientPeerConfig.errors + .map((error) => error.dataPath + ' ' + error.message) + .join('\n'); + } + alert( + `Server sent a malformed peer config. Default config will be used. See errors below:\n${errorsFormatted}` + ); + return; + } + + if ( + clientPeerConfig.forceRelayOnly && + !clientPeerConfig.iceServers.some((server) => + server.urls.toString().includes('turn:') + ) + ) { + alert( + 'Server has forced relay mode enabled but provides no relay servers. Default config will be used.' + ); + return; + } + + iceConfig = { + iceTransportPolicy: clientPeerConfig.forceRelayOnly ? 'relay' : 'all', + iceServers: clientPeerConfig.iceServers, + }; + }); + // Initialize variables let audioListener: { connect: () => void; @@ -494,13 +542,7 @@ const Voice: React.FC = function ({ const connection = new Peer({ stream, initiator, - config: { - iceServers: [ - { - urls: 'stun:stun.l.google.com:19302', - }, - ], - }, + config: iceConfig, }); setPeerConnections((connections) => { connections[peer] = connection; diff --git a/src/renderer/validateClientPeerConfig.ts b/src/renderer/validateClientPeerConfig.ts new file mode 100644 index 00000000..867d76c6 --- /dev/null +++ b/src/renderer/validateClientPeerConfig.ts @@ -0,0 +1,34 @@ +import Ajv from 'ajv'; + +export const validateClientPeerConfig = new Ajv({ format: 'full', allErrors: true }).compile({ + type: 'object', + properties: { + forceRelayOnly: { + type: 'boolean' + }, + iceServers: { + type: 'array', + items: { + type: 'object', + properties: { + urls: { + type: ['string', 'array'], + format: 'uri', + items: { + type: 'string', + format: 'uri' + } + }, + username: { + type: 'string', + }, + credential: { + type: 'string', + } + }, + required: ['urls'] + } + } + }, + required: ['forceRelayOnly', 'iceServers'] +});