Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check TURN servers periodically, and at start of calls #1634

Merged
merged 4 commits into from
Mar 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions spec/unit/webrtc/call.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class MockRTCPeerConnection {
return Promise.resolve();
}
close() {}
getStats() { return []; }
}

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

it('should ignore candidate events from non-matching party ID', async function() {
await call.placeVoiceCall();
const callPromise = call.placeVoiceCall();
await client.httpBackend.flush();
await callPromise;
await call.onAnswerReceived({
getContent: () => {
return {
Expand Down Expand Up @@ -192,7 +196,9 @@ describe('Call', function() {
});

it('should add candidates received before answer if party ID is correct', async function() {
await call.placeVoiceCall();
const callPromise = call.placeVoiceCall();
await client.httpBackend.flush();
await callPromise;
call.peerConn.addIceCandidate = jest.fn();

call.onRemoteIceCandidatesReceived({
Expand Down
91 changes: 52 additions & 39 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {DEHYDRATION_ALGORITHM} from "./crypto/dehydration";
const SCROLLBACK_DELAY_MS = 3000;
export const CRYPTO_ENABLED = isCryptoAvailable();
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes

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

this._turnServers = [];
this._turnServersExpiry = null;
this._turnServersExpiry = 0;
this._checkTurnServersIntervalID = null;

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

MatrixClient.prototype._checkTurnServers = async function() {
if (!this._supportsVoip) {
return;
}

let credentialsGood = false;
const remainingTime = this._turnServersExpiry - Date.now();
if (remainingTime > TURN_CHECK_INTERVAL) {
logger.debug("TURN creds are valid for another " + remainingTime + " ms: not fetching new ones.");
credentialsGood = true;
} else {
logger.debug("Fetching new TURN credentials");
try {
const res = await this.turnServer();
if (res.uris) {
logger.log("Got TURN URIs: " + res.uris + " refresh in " + res.ttl + " secs");
// map the response to a format that can be fed to RTCPeerConnection
const servers = {
urls: res.uris,
username: res.username,
credential: res.password,
};
this._turnServers = [servers];
// The TTL is in seconds but we work in ms
this._turnServersExpiry = Date.now() + (res.ttl * 1000);
credentialsGood = true;
}
} catch (err) {
logger.error("Failed to get TURN URIs", err);
// If we get a 403, there's no point in looping forever.
if (err.httpStatus === 403) {
logger.info("TURN access unavailable for this account: stopping credentials checks");
if (this._checkTurnServersIntervalID !== null) global.clearInterval(this._checkTurnServersIntervalID);
this._checkTurnServersIntervalID = null;
}
}
// otherwise, if we failed for whatever reason, try again the next time we're called.
}

return credentialsGood;
};

/**
* Set whether to allow a fallback ICE server should be used for negotiating a
* WebRTC connection if the homeserver doesn't provide any servers. Defaults to
Expand Down Expand Up @@ -5106,7 +5150,12 @@ MatrixClient.prototype.startClient = async function(opts) {
}

// periodically poll for turn servers if we support voip
checkTurnServers(this);
if (this._supportsVoip) {
this._checkTurnServersIntervalID = setInterval(() => {
this._checkTurnServers();
}, TURN_CHECK_INTERVAL);
this._checkTurnServers();
}

if (this._syncApi) {
// This shouldn't happen since we thought the client was not running
Expand Down Expand Up @@ -5218,7 +5267,7 @@ MatrixClient.prototype.stopClient = function() {
this._callEventHandler = null;
}

global.clearTimeout(this._checkTurnServersTimeoutID);
global.clearInterval(this._checkTurnServersIntervalID);
if (this._clientWellKnownIntervalID !== undefined) {
global.clearInterval(this._clientWellKnownIntervalID);
}
Expand Down Expand Up @@ -5435,42 +5484,6 @@ async function(roomId, eventId, relationType, eventType, opts = {}) {
};
};

function checkTurnServers(client) {
if (!client._supportsVoip) {
return;
}

client.turnServer().then(function(res) {
if (res.uris) {
logger.log("Got TURN URIs: " + res.uris + " refresh in " +
res.ttl + " secs");
// map the response to a format that can be fed to
// RTCPeerConnection
const servers = {
urls: res.uris,
username: res.username,
credential: res.password,
};
client._turnServers = [servers];
client._turnServersExpiry = Date.now() + res.ttl;
// re-fetch when we're about to reach the TTL
client._checkTurnServersTimeoutID = setTimeout(() => {
checkTurnServers(client);
}, (res.ttl || (60 * 60)) * 1000 * 0.9);
}
}, function(err) {
logger.error("Failed to get TURN URIs");
// If we get a 403, there's no point in looping forever.
if (err.httpStatus === 403) {
logger.info("TURN access unavailable for this account");
return;
}
client._checkTurnServersTimeoutID = setTimeout(function() {
checkTurnServers(client);
}, 60000);
});
}

function _reject(callback, reject, err) {
if (callback) {
callback(err);
Expand Down
33 changes: 23 additions & 10 deletions src/webrtc/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,11 +333,11 @@ export class MatrixCall extends EventEmitter {
* Place a voice call to this room.
* @throws If you have not specified a listener for 'error' events.
*/
placeVoiceCall() {
async placeVoiceCall() {
logger.debug("placeVoiceCall");
this.checkForErrorListener();
const constraints = getUserMediaContraints(ConstraintsType.Audio);
this.placeCallWithConstraints(constraints);
await this.placeCallWithConstraints(constraints);
this.type = CallType.Voice;
}

Expand All @@ -349,13 +349,13 @@ export class MatrixCall extends EventEmitter {
* to render the local camera preview.
* @throws If you have not specified a listener for 'error' events.
*/
placeVideoCall(remoteVideoElement: HTMLVideoElement, localVideoElement: HTMLVideoElement) {
async placeVideoCall(remoteVideoElement: HTMLVideoElement, localVideoElement: HTMLVideoElement) {
logger.debug("placeVideoCall");
this.checkForErrorListener();
this.localVideoElement = localVideoElement;
this.remoteVideoElement = remoteVideoElement;
const constraints = getUserMediaContraints(ConstraintsType.Video);
this.placeCallWithConstraints(constraints);
await this.placeCallWithConstraints(constraints);
this.type = CallType.Video;
}

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

// make sure we have valid turn creds. Unless something's gone wrong, it should
// poll and keep the credentials valid so this should be instant.
const haveTurnCreds = await this.client._checkTurnServers();
if (!haveTurnCreds) {
logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
}

this.peerConn = this.createPeerConnection();
// we must set the party ID before await-ing on anything: the call event
// handler will start giving us more call events (eg. candidates) so if
Expand Down Expand Up @@ -857,7 +864,6 @@ export class MatrixCall extends EventEmitter {

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

for (const audioTrack of stream.getAudioTracks()) {
logger.info("Adding audio track with id " + audioTrack.id);
Expand Down Expand Up @@ -1662,11 +1668,18 @@ export class MatrixCall extends EventEmitter {
this.setState(CallState.WaitLocalMedia);
this.direction = CallDirection.Outbound;
this.config = constraints;
// It would be really nice if we could start gathering candidates at this point
// so the ICE agent could be gathering while we open our media devices: we already
// know the type of the call and therefore what tracks we want to send.
// Perhaps we could do this by making fake tracks now and then using replaceTrack()
// once we have the actual tracks? (Can we make fake tracks?)

// make sure we have valid turn creds. Unless something's gone wrong, it should
// poll and keep the credentials valid so this should be instant.
const haveTurnCreds = await this.client._checkTurnServers();
if (!haveTurnCreds) {
logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
}

// create the peer connection now so it can be gathering candidates while we get user
// media (assuming a candidate pool size is configured)
this.peerConn = this.createPeerConnection();

try {
const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
this.gotUserMediaForInvite(mediaStream);
Expand Down
2 changes: 1 addition & 1 deletion src/webrtc/callEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export class CallEventHandler {
}

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