Skip to content

Commit 6c5fc15

Browse files
authored
Merge pull request #1634 from matrix-org/dbkr/check_turn_interval
Check TURN servers periodically, and at start of calls
2 parents bed7543 + 07f15b4 commit 6c5fc15

File tree

4 files changed

+84
-52
lines changed

4 files changed

+84
-52
lines changed

spec/unit/webrtc/call.spec.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class MockRTCPeerConnection {
7979
return Promise.resolve();
8080
}
8181
close() {}
82+
getStats() { return []; }
8283
}
8384

8485
describe('Call', function() {
@@ -122,6 +123,7 @@ describe('Call', function() {
122123
// We just stub out sendEvent: we're not interested in testing the client's
123124
// event sending code here
124125
client.client.sendEvent = () => {};
126+
client.httpBackend.when("GET", "/voip/turnServer").respond(200, {});
125127
call = new MatrixCall({
126128
client: client.client,
127129
roomId: '!foo:bar',
@@ -138,7 +140,9 @@ describe('Call', function() {
138140
});
139141

140142
it('should ignore candidate events from non-matching party ID', async function() {
141-
await call.placeVoiceCall();
143+
const callPromise = call.placeVoiceCall();
144+
await client.httpBackend.flush();
145+
await callPromise;
142146
await call.onAnswerReceived({
143147
getContent: () => {
144148
return {
@@ -192,7 +196,9 @@ describe('Call', function() {
192196
});
193197

194198
it('should add candidates received before answer if party ID is correct', async function() {
195-
await call.placeVoiceCall();
199+
const callPromise = call.placeVoiceCall();
200+
await client.httpBackend.flush();
201+
await callPromise;
196202
call.peerConn.addIceCandidate = jest.fn();
197203

198204
call.onRemoteIceCandidatesReceived({

src/client.js

Lines changed: 52 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {DEHYDRATION_ALGORITHM} from "./crypto/dehydration";
6161
const SCROLLBACK_DELAY_MS = 3000;
6262
export const CRYPTO_ENABLED = isCryptoAvailable();
6363
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
64+
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
6465

6566
function keysFromRecoverySession(sessions, decryptionKey, roomId) {
6667
const keys = [];
@@ -394,7 +395,8 @@ export function MatrixClient(opts) {
394395
this._clientWellKnownPromise = undefined;
395396

396397
this._turnServers = [];
397-
this._turnServersExpiry = null;
398+
this._turnServersExpiry = 0;
399+
this._checkTurnServersIntervalID = null;
398400

399401
// The SDK doesn't really provide a clean way for events to recalculate the push
400402
// actions for themselves, so we have to kinda help them out when they are encrypted.
@@ -4955,6 +4957,48 @@ MatrixClient.prototype.getTurnServersExpiry = function() {
49554957
return this._turnServersExpiry;
49564958
};
49574959

4960+
MatrixClient.prototype._checkTurnServers = async function() {
4961+
if (!this._supportsVoip) {
4962+
return;
4963+
}
4964+
4965+
let credentialsGood = false;
4966+
const remainingTime = this._turnServersExpiry - Date.now();
4967+
if (remainingTime > TURN_CHECK_INTERVAL) {
4968+
logger.debug("TURN creds are valid for another " + remainingTime + " ms: not fetching new ones.");
4969+
credentialsGood = true;
4970+
} else {
4971+
logger.debug("Fetching new TURN credentials");
4972+
try {
4973+
const res = await this.turnServer();
4974+
if (res.uris) {
4975+
logger.log("Got TURN URIs: " + res.uris + " refresh in " + res.ttl + " secs");
4976+
// map the response to a format that can be fed to RTCPeerConnection
4977+
const servers = {
4978+
urls: res.uris,
4979+
username: res.username,
4980+
credential: res.password,
4981+
};
4982+
this._turnServers = [servers];
4983+
// The TTL is in seconds but we work in ms
4984+
this._turnServersExpiry = Date.now() + (res.ttl * 1000);
4985+
credentialsGood = true;
4986+
}
4987+
} catch (err) {
4988+
logger.error("Failed to get TURN URIs", err);
4989+
// If we get a 403, there's no point in looping forever.
4990+
if (err.httpStatus === 403) {
4991+
logger.info("TURN access unavailable for this account: stopping credentials checks");
4992+
if (this._checkTurnServersIntervalID !== null) global.clearInterval(this._checkTurnServersIntervalID);
4993+
this._checkTurnServersIntervalID = null;
4994+
}
4995+
}
4996+
// otherwise, if we failed for whatever reason, try again the next time we're called.
4997+
}
4998+
4999+
return credentialsGood;
5000+
};
5001+
49585002
/**
49595003
* Set whether to allow a fallback ICE server should be used for negotiating a
49605004
* WebRTC connection if the homeserver doesn't provide any servers. Defaults to
@@ -5107,7 +5151,12 @@ MatrixClient.prototype.startClient = async function(opts) {
51075151
}
51085152

51095153
// periodically poll for turn servers if we support voip
5110-
checkTurnServers(this);
5154+
if (this._supportsVoip) {
5155+
this._checkTurnServersIntervalID = setInterval(() => {
5156+
this._checkTurnServers();
5157+
}, TURN_CHECK_INTERVAL);
5158+
this._checkTurnServers();
5159+
}
51115160

51125161
if (this._syncApi) {
51135162
// This shouldn't happen since we thought the client was not running
@@ -5219,7 +5268,7 @@ MatrixClient.prototype.stopClient = function() {
52195268
this._callEventHandler = null;
52205269
}
52215270

5222-
global.clearTimeout(this._checkTurnServersTimeoutID);
5271+
global.clearInterval(this._checkTurnServersIntervalID);
52235272
if (this._clientWellKnownIntervalID !== undefined) {
52245273
global.clearInterval(this._clientWellKnownIntervalID);
52255274
}
@@ -5436,42 +5485,6 @@ async function(roomId, eventId, relationType, eventType, opts = {}) {
54365485
};
54375486
};
54385487

5439-
function checkTurnServers(client) {
5440-
if (!client._supportsVoip) {
5441-
return;
5442-
}
5443-
5444-
client.turnServer().then(function(res) {
5445-
if (res.uris) {
5446-
logger.log("Got TURN URIs: " + res.uris + " refresh in " +
5447-
res.ttl + " secs");
5448-
// map the response to a format that can be fed to
5449-
// RTCPeerConnection
5450-
const servers = {
5451-
urls: res.uris,
5452-
username: res.username,
5453-
credential: res.password,
5454-
};
5455-
client._turnServers = [servers];
5456-
client._turnServersExpiry = Date.now() + res.ttl;
5457-
// re-fetch when we're about to reach the TTL
5458-
client._checkTurnServersTimeoutID = setTimeout(() => {
5459-
checkTurnServers(client);
5460-
}, (res.ttl || (60 * 60)) * 1000 * 0.9);
5461-
}
5462-
}, function(err) {
5463-
logger.error("Failed to get TURN URIs");
5464-
// If we get a 403, there's no point in looping forever.
5465-
if (err.httpStatus === 403) {
5466-
logger.info("TURN access unavailable for this account");
5467-
return;
5468-
}
5469-
client._checkTurnServersTimeoutID = setTimeout(function() {
5470-
checkTurnServers(client);
5471-
}, 60000);
5472-
});
5473-
}
5474-
54755488
function _reject(callback, reject, err) {
54765489
if (callback) {
54775490
callback(err);

src/webrtc/call.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -333,11 +333,11 @@ export class MatrixCall extends EventEmitter {
333333
* Place a voice call to this room.
334334
* @throws If you have not specified a listener for 'error' events.
335335
*/
336-
placeVoiceCall() {
336+
async placeVoiceCall() {
337337
logger.debug("placeVoiceCall");
338338
this.checkForErrorListener();
339339
const constraints = getUserMediaContraints(ConstraintsType.Audio);
340-
this.placeCallWithConstraints(constraints);
340+
await this.placeCallWithConstraints(constraints);
341341
this.type = CallType.Voice;
342342
}
343343

@@ -349,13 +349,13 @@ export class MatrixCall extends EventEmitter {
349349
* to render the local camera preview.
350350
* @throws If you have not specified a listener for 'error' events.
351351
*/
352-
placeVideoCall(remoteVideoElement: HTMLVideoElement, localVideoElement: HTMLVideoElement) {
352+
async placeVideoCall(remoteVideoElement: HTMLVideoElement, localVideoElement: HTMLVideoElement) {
353353
logger.debug("placeVideoCall");
354354
this.checkForErrorListener();
355355
this.localVideoElement = localVideoElement;
356356
this.remoteVideoElement = remoteVideoElement;
357357
const constraints = getUserMediaContraints(ConstraintsType.Video);
358-
this.placeCallWithConstraints(constraints);
358+
await this.placeCallWithConstraints(constraints);
359359
this.type = CallType.Video;
360360
}
361361

@@ -527,6 +527,13 @@ export class MatrixCall extends EventEmitter {
527527
const invite = event.getContent();
528528
this.direction = CallDirection.Inbound;
529529

530+
// make sure we have valid turn creds. Unless something's gone wrong, it should
531+
// poll and keep the credentials valid so this should be instant.
532+
const haveTurnCreds = await this.client._checkTurnServers();
533+
if (!haveTurnCreds) {
534+
logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
535+
}
536+
530537
this.peerConn = this.createPeerConnection();
531538
// we must set the party ID before await-ing on anything: the call event
532539
// handler will start giving us more call events (eg. candidates) so if
@@ -857,7 +864,6 @@ export class MatrixCall extends EventEmitter {
857864

858865
// why do we enable audio (and only audio) tracks here? -- matthew
859866
setTracksEnabled(stream.getAudioTracks(), true);
860-
this.peerConn = this.createPeerConnection();
861867

862868
for (const audioTrack of stream.getAudioTracks()) {
863869
logger.info("Adding audio track with id " + audioTrack.id);
@@ -1662,11 +1668,18 @@ export class MatrixCall extends EventEmitter {
16621668
this.setState(CallState.WaitLocalMedia);
16631669
this.direction = CallDirection.Outbound;
16641670
this.config = constraints;
1665-
// It would be really nice if we could start gathering candidates at this point
1666-
// so the ICE agent could be gathering while we open our media devices: we already
1667-
// know the type of the call and therefore what tracks we want to send.
1668-
// Perhaps we could do this by making fake tracks now and then using replaceTrack()
1669-
// once we have the actual tracks? (Can we make fake tracks?)
1671+
1672+
// make sure we have valid turn creds. Unless something's gone wrong, it should
1673+
// poll and keep the credentials valid so this should be instant.
1674+
const haveTurnCreds = await this.client._checkTurnServers();
1675+
if (!haveTurnCreds) {
1676+
logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
1677+
}
1678+
1679+
// create the peer connection now so it can be gathering candidates while we get user
1680+
// media (assuming a candidate pool size is configured)
1681+
this.peerConn = this.createPeerConnection();
1682+
16701683
try {
16711684
const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
16721685
this.gotUserMediaForInvite(mediaStream);

src/webrtc/callEventHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export class CallEventHandler {
139139
}
140140

141141
const timeUntilTurnCresExpire = this.client.getTurnServersExpiry() - Date.now();
142-
logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " seconds");
142+
logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
143143
call = createNewMatrixCall(this.client, event.getRoomId(), {
144144
forceTURN: this.client._forceTURN,
145145
});

0 commit comments

Comments
 (0)