From af2098327bc023062da0b6ac6607f1051eea9549 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 26 Sep 2022 00:47:12 +0100 Subject: [PATCH 01/16] first cut at implementing TURN --- src/matrix/Session.js | 13 +++++++++++++ src/matrix/calls/CallHandler.ts | 6 ++++++ src/matrix/net/HomeServerApi.ts | 6 +++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index f82ad555..de05f040 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -513,6 +513,7 @@ export class Session { // TODO: what can we do if this throws? await txn.complete(); } + await this._updateTurnServers(); // enable session backup, this requests the latest backup version if (!this._keyBackup.get()) { if (dehydratedDevice) { @@ -559,6 +560,18 @@ export class Session { } } + async _updateTurnServers() { + const turnServersData = await this._hsApi.getTurnServers().response(); + this._callHandler.setTurnServers({ + urls: turnServerData.uris, + username: turnServerData.username, + credential: turnServerData.password, + }); + if (turnServersData.ttl > 0) { + setTimeout(this._updateTurnServers, turnServersData.ttl * 1000); + } + } + async _getPendingEventsByRoom(txn) { const pendingEvents = await txn.pendingEvents.getAll(); return pendingEvents.reduce((groups, pe) => { diff --git a/src/matrix/calls/CallHandler.ts b/src/matrix/calls/CallHandler.ts index ca58c4a8..93491e07 100644 --- a/src/matrix/calls/CallHandler.ts +++ b/src/matrix/calls/CallHandler.ts @@ -75,6 +75,12 @@ export class CallHandler implements RoomStateHandler { this._loadCallEntries(callEntries, txn); } + async setTurnServers(turnServers: RTCIceServer) { + this.options.turnServers = turnServers; + this.groupCallOptions.turnServers = turnServers; + // TODO: we should update any ongoing peerconnections if the TURN server details have changed + } + private async _getLoadTxn(): Promise { const names = this.options.storage.storeNames; const txn = await this.options.storage.readTxn([ diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index 30406c34..828a1c82 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -305,10 +305,14 @@ export class HomeServerApi { createRoom(payload: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/createRoom`, {}, payload, options); } - + setAccountData(ownUserId: string, type: string, content: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._put(`/user/${encodeURIComponent(ownUserId)}/account_data/${encodeURIComponent(type)}`, {}, content, options); } + + getTurnServer(options?: BaseRequestOptions): IHomeServerRequest { + return this._get(`/voip/turnServer`, undefined, undefined, options); + } } import {Request as MockRequest} from "../../mocks/Request.js"; From eab87e5157c795d255ae3760515b29ddd477aec8 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 26 Sep 2022 00:52:41 +0100 Subject: [PATCH 02/16] fix plurals --- src/matrix/Session.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index de05f040..527c0e00 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -561,14 +561,14 @@ export class Session { } async _updateTurnServers() { - const turnServersData = await this._hsApi.getTurnServers().response(); + const turnServerData = await this._hsApi.getTurnServer().response(); this._callHandler.setTurnServers({ urls: turnServerData.uris, username: turnServerData.username, credential: turnServerData.password, }); - if (turnServersData.ttl > 0) { - setTimeout(this._updateTurnServers, turnServersData.ttl * 1000); + if (turnServerData.ttl > 0) { + setTimeout(this._updateTurnServers, turnServerData.ttl * 1000); } } From 3d7852a469bd4584798dad39e834cbe38451f763 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 26 Sep 2022 01:07:24 +0100 Subject: [PATCH 03/16] set array of turnservers --- src/matrix/Session.js | 4 ++-- src/matrix/calls/CallHandler.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 527c0e00..3c49182d 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -562,11 +562,11 @@ export class Session { async _updateTurnServers() { const turnServerData = await this._hsApi.getTurnServer().response(); - this._callHandler.setTurnServers({ + this._callHandler.setTurnServers([{ urls: turnServerData.uris, username: turnServerData.username, credential: turnServerData.password, - }); + }]); if (turnServerData.ttl > 0) { setTimeout(this._updateTurnServers, turnServerData.ttl * 1000); } diff --git a/src/matrix/calls/CallHandler.ts b/src/matrix/calls/CallHandler.ts index 93491e07..183ec5a1 100644 --- a/src/matrix/calls/CallHandler.ts +++ b/src/matrix/calls/CallHandler.ts @@ -75,7 +75,7 @@ export class CallHandler implements RoomStateHandler { this._loadCallEntries(callEntries, txn); } - async setTurnServers(turnServers: RTCIceServer) { + async setTurnServers(turnServers: RTCIceServer[]) { this.options.turnServers = turnServers; this.groupCallOptions.turnServers = turnServers; // TODO: we should update any ongoing peerconnections if the TURN server details have changed From 6570ec46f4a9ba500215b7b15f65e858f74cb0a2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 26 Sep 2022 09:02:22 +0100 Subject: [PATCH 04/16] setTurnServers doesn't need to be async --- src/matrix/calls/CallHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/calls/CallHandler.ts b/src/matrix/calls/CallHandler.ts index 183ec5a1..17aa0e55 100644 --- a/src/matrix/calls/CallHandler.ts +++ b/src/matrix/calls/CallHandler.ts @@ -75,7 +75,7 @@ export class CallHandler implements RoomStateHandler { this._loadCallEntries(callEntries, txn); } - async setTurnServers(turnServers: RTCIceServer[]) { + setTurnServers(turnServers: RTCIceServer[]) { this.options.turnServers = turnServers; this.groupCallOptions.turnServers = turnServers; // TODO: we should update any ongoing peerconnections if the TURN server details have changed From f4e633beb4faebe1eaced7ea64f3903ecc57f08b Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:24:14 +0200 Subject: [PATCH 05/16] support onfirstsubscribe callback in ROV will use it to start polling for turnServer updates --- src/observable/value/RetainedObservableValue.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/observable/value/RetainedObservableValue.ts b/src/observable/value/RetainedObservableValue.ts index edfb6c15..16058f8e 100644 --- a/src/observable/value/RetainedObservableValue.ts +++ b/src/observable/value/RetainedObservableValue.ts @@ -17,15 +17,17 @@ limitations under the License. import {ObservableValue} from "./ObservableValue"; export class RetainedObservableValue extends ObservableValue { - private _freeCallback: () => void; - constructor(initialValue: T, freeCallback: () => void) { + constructor(initialValue: T, private freeCallback: () => void, private startCallback: () => void = () => {}) { super(initialValue); - this._freeCallback = freeCallback; + } + + onSubscribeFirst() { + this.startCallback(); } onUnsubscribeLast() { super.onUnsubscribeLast(); - this._freeCallback(); + this.freeCallback(); } } From 035ead0d5b47c7ebb742f74aae0e4949f556fcb9 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:24:41 +0200 Subject: [PATCH 06/16] implement polling of voip turnServer settings from HS in separate class --- src/matrix/calls/TurnServerSource.ts | 222 +++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 src/matrix/calls/TurnServerSource.ts diff --git a/src/matrix/calls/TurnServerSource.ts b/src/matrix/calls/TurnServerSource.ts new file mode 100644 index 00000000..cc6923af --- /dev/null +++ b/src/matrix/calls/TurnServerSource.ts @@ -0,0 +1,222 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {RetainedObservableValue} from "../../observable/value/RetainedObservableValue"; + +import type {HomeServerApi} from "../net/HomeServerApi"; +import type {IHomeServerRequest} from "../net/HomeServerRequest"; +import type {BaseObservableValue} from "../../observable/value/BaseObservableValue"; +import type {ObservableValue} from "../../observable/value/ObservableValue"; +import type {Clock, Timeout} from "../../platform/web/dom/Clock"; +import type {ILogItem} from "../../logging/types"; + +type TurnServerSettings = { + urls: string[], + username: string, + password: string, + ttl: number +}; + +const DEFAULT_TTL = 5 * 60; // 5min +const DEFAULT_SETTINGS: RTCIceServer = { + urls: ["stun:turn.matrix.org"], + username: "", + credential: "", +}; + +export class TurnServerSource { + private currentObservable?: ObservableValue; + private pollTimeout?: Timeout; + private pollRequest?: IHomeServerRequest; + private isPolling = false; + + constructor( + private hsApi: HomeServerApi, + private clock: Clock, + private defaultSettings: RTCIceServer = DEFAULT_SETTINGS + ) {} + + getSettings(log: ILogItem): Promise> { + return log.wrap("get turn server", async log => { + if (!this.isPolling) { + const settings = await this.doRequest(log); + const iceServer = settings ? toIceServer(settings) : this.defaultSettings; + if (this.currentObservable) { + this.currentObservable.set(iceServer); + } else { + this.currentObservable = new RetainedObservableValue(iceServer, + () => { + this.stopPollLoop(); + }, + () => { + // start loop on first subscribe + this.runLoop(this.currentObservable!, settings?.ttl ?? DEFAULT_TTL); + }); + } + } + return this.currentObservable!; + }); + } + + private async runLoop(observable: ObservableValue, initialTtl: number): Promise { + let ttl = initialTtl; + this.isPolling = true; + while(this.isPolling) { + try { + this.pollTimeout = this.clock.createTimeout(ttl * 1000); + await this.pollTimeout.elapsed(); + this.pollTimeout = undefined; + const settings = await this.doRequest(undefined); + if (settings) { + const iceServer = toIceServer(settings); + if (shouldUpdate(observable, iceServer)) { + observable.set(iceServer); + } + if (settings.ttl > 0) { + ttl = settings.ttl; + } else { + // stop polling is settings are good indefinitely + this.stopPollLoop(); + } + } else { + ttl = DEFAULT_TTL; + } + } catch (err) { + if (err.name === "AbortError") { + /* ignore, the loop will exit because isPolling is false */ + } else { + // TODO: log error + } + } + } + } + + private async doRequest(log: ILogItem | undefined): Promise { + try { + this.pollRequest = this.hsApi.getTurnServer({log}); + const settings = await this.pollRequest.response(); + return settings; + } catch (err) { + if (err.name === "HomeServerError") { + return undefined; + } + throw err; + } finally { + this.pollRequest = undefined; + } + } + + stopPollLoop() { + this.isPolling = false; + this.currentObservable = undefined; + this.pollTimeout?.dispose(); + this.pollTimeout = undefined; + this.pollRequest?.abort(); + this.pollRequest = undefined; + } + + dispose() { + this.stopPollLoop(); + } +} + +function shouldUpdate(observable: BaseObservableValue, settings: RTCIceServer): boolean { + const currentSettings = observable.get(); + if (!currentSettings) { + return true; + } + // same length and new settings doesn't contain any uri the old settings don't contain + const currentUrls = Array.isArray(currentSettings.urls) ? currentSettings.urls : [currentSettings.urls]; + const newUrls = Array.isArray(settings.urls) ? settings.urls : [settings.urls]; + const arraysEqual = currentUrls.length === newUrls.length && + !newUrls.some(uri => !currentUrls.includes(uri)); + return !arraysEqual || settings.username !== currentSettings.username || + settings.credential !== currentSettings.credential; +} + +function toIceServer(settings: TurnServerSettings): RTCIceServer { + return { + urls: settings.urls, + username: settings.username, + credential: settings.password, + credentialType: "password" + } +} + +export function tests() { + return { + "shouldUpdate returns false for same object": assert => { + const observable = {get() { + return { + urls: ["a", "b"], + username: "alice", + credential: "f00", + }; + }}; + const same = { + urls: ["a", "b"], + username: "alice", + credential: "f00", + }; + assert.equal(false, shouldUpdate(observable as any as BaseObservableValue, same)); + }, + "shouldUpdate returns true for 1 different uri": assert => { + const observable = {get() { + return { + urls: ["a", "c"], + username: "alice", + credential: "f00", + }; + }}; + const same = { + urls: ["a", "b"], + username: "alice", + credential: "f00", + }; + assert.equal(true, shouldUpdate(observable as any as BaseObservableValue, same)); + }, + "shouldUpdate returns true for different user": assert => { + const observable = {get() { + return { + urls: ["a", "b"], + username: "alice", + credential: "f00", + }; + }}; + const same = { + urls: ["a", "b"], + username: "bob", + credential: "f00", + }; + assert.equal(true, shouldUpdate(observable as any as BaseObservableValue, same)); + }, + "shouldUpdate returns true for different password": assert => { + const observable = {get() { + return { + urls: ["a", "b"], + username: "alice", + credential: "f00", + }; + }}; + const same = { + urls: ["a", "b"], + username: "alice", + credential: "b4r", + }; + assert.equal(true, shouldUpdate(observable as any as BaseObservableValue, same)); + } + } +} From 3a4c38086c066c31534263d98c19e875b3627a0f Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:25:24 +0200 Subject: [PATCH 07/16] fetch turn server settings when joining a call, and pass down --- src/matrix/calls/CallHandler.ts | 8 ++------ src/matrix/calls/PeerCall.ts | 3 ++- src/matrix/calls/group/GroupCall.ts | 19 +++++++++++++++---- src/matrix/calls/group/Member.ts | 22 ++++++++++++++++------ 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/matrix/calls/CallHandler.ts b/src/matrix/calls/CallHandler.ts index 17aa0e55..354c9e6a 100644 --- a/src/matrix/calls/CallHandler.ts +++ b/src/matrix/calls/CallHandler.ts @@ -23,6 +23,7 @@ import {GroupCall} from "./group/GroupCall"; import {makeId} from "../common"; import {CALL_LOG_TYPE} from "./common"; import {EVENT_TYPE as MEMBER_EVENT_TYPE, RoomMember} from "../room/members/RoomMember"; +import {TurnServerSource} from "./TurnServerSource"; import type {LocalMedia} from "./LocalMedia"; import type {Room} from "../room/Room"; @@ -57,6 +58,7 @@ export class CallHandler implements RoomStateHandler { constructor(private readonly options: Options) { this.groupCallOptions = Object.assign({}, this.options, { + turnServerSource: new TurnServerSource(this.options.hsApi, this.options.clock), emitUpdate: (groupCall, params) => this._calls.update(groupCall.id, params), createTimeout: this.options.clock.createTimeout, sessionId: this.sessionId @@ -75,12 +77,6 @@ export class CallHandler implements RoomStateHandler { this._loadCallEntries(callEntries, txn); } - setTurnServers(turnServers: RTCIceServer[]) { - this.options.turnServers = turnServers; - this.groupCallOptions.turnServers = turnServers; - // TODO: we should update any ongoing peerconnections if the TURN server details have changed - } - private async _getLoadTxn(): Promise { const names = this.options.storage.storeNames; const txn = await this.options.storage.readTxn([ diff --git a/src/matrix/calls/PeerCall.ts b/src/matrix/calls/PeerCall.ts index d004dedc..ef906d97 100644 --- a/src/matrix/calls/PeerCall.ts +++ b/src/matrix/calls/PeerCall.ts @@ -15,6 +15,7 @@ limitations under the License. */ import {ObservableMap} from "../../observable/map/ObservableMap"; +import {BaseObservableValue} from "../../observable/value/BaseObservableValue"; import {recursivelyAssign} from "../../utils/recursivelyAssign"; import {Disposables, Disposable, IDisposable} from "../../utils/Disposables"; import {WebRTC, PeerConnection, Transceiver, TransceiverDirection, Sender, Receiver, PeerConnectionEventMap} from "../../platform/types/WebRTC"; @@ -47,7 +48,7 @@ import type { export type Options = { webRTC: WebRTC, forceTURN: boolean, - turnServers: RTCIceServer[], + turnServer: BaseObservableValue, createTimeout: TimeoutCreator, emitUpdate: (peerCall: PeerCall, params: any, log: ILogItem) => void; sendSignallingMessage: (message: SignallingMessage, log: ILogItem) => Promise; diff --git a/src/matrix/calls/group/GroupCall.ts b/src/matrix/calls/group/GroupCall.ts index 07aa2a08..4c038be9 100644 --- a/src/matrix/calls/group/GroupCall.ts +++ b/src/matrix/calls/group/GroupCall.ts @@ -23,6 +23,7 @@ import {EventEmitter} from "../../../utils/EventEmitter"; import {EventType, CallIntent} from "../callEventTypes"; import type {Options as MemberOptions} from "./Member"; +import type {TurnServerSource} from "../TurnServerSource"; import type {BaseObservableMap} from "../../../observable/map/BaseObservableMap"; import type {Track} from "../../../platform/types/MediaDevices"; import type {SignallingMessage, MGroupCallBase, CallMembership} from "../callEventTypes"; @@ -32,6 +33,7 @@ import type {Platform} from "../../../platform/web/Platform"; import type {EncryptedMessage} from "../../e2ee/olm/Encryption"; import type {ILogItem, ILogger} from "../../../logging/types"; import type {Storage} from "../../storage/idb/Storage"; +import type {BaseObservableValue} from "../../../observable/value/BaseObservableValue"; export enum GroupCallState { Fledgling = "fledgling", @@ -53,11 +55,12 @@ function getDeviceFromMemberKey(key: string): string { return JSON.parse(`[${key}]`)[1]; } -export type Options = Omit & { +export type Options = Omit & { emitUpdate: (call: GroupCall, params?: any) => void; encryptDeviceMessage: (roomId: string, userId: string, deviceId: string, message: SignallingMessage, log: ILogItem) => Promise, storage: Storage, logger: ILogger, + turnServerSource: TurnServerSource }; class JoinedData { @@ -65,7 +68,8 @@ class JoinedData { public readonly logItem: ILogItem, public readonly membersLogItem: ILogItem, public localMedia: LocalMedia, - public localMuteSettings: MuteSettings + public localMuteSettings: MuteSettings, + public turnServer: BaseObservableValue ) {} dispose() { @@ -136,6 +140,7 @@ export class GroupCall extends EventEmitter<{change: never}> { id: this.id, ownSessionId: this.options.sessionId }); + const turnServer = await this.options.turnServerSource.getSettings(logItem); const membersLogItem = logItem.child("member connections"); const localMuteSettings = new MuteSettings(); localMuteSettings.updateTrackInfo(localMedia.userMedia); @@ -143,7 +148,8 @@ export class GroupCall extends EventEmitter<{change: never}> { logItem, membersLogItem, localMedia, - localMuteSettings + localMuteSettings, + turnServer ); this.joinedData = joinedData; await joinedData.logItem.wrap("join", async log => { @@ -509,7 +515,12 @@ export class GroupCall extends EventEmitter<{change: never}> { const logItem = joinedData.membersLogItem.child({l: "member", id: memberKey}); logItem.set("sessionId", member.sessionId); log.wrap({l: "connect", id: memberKey}, log => { - const connectItem = member.connect(joinedData.localMedia, joinedData.localMuteSettings, logItem); + const connectItem = member.connect( + joinedData.localMedia, + joinedData.localMuteSettings, + joinedData.turnServer, + logItem + ); if (connectItem) { log.refDetached(connectItem); } diff --git a/src/matrix/calls/group/Member.ts b/src/matrix/calls/group/Member.ts index 98e2aa4d..7cd900f2 100644 --- a/src/matrix/calls/group/Member.ts +++ b/src/matrix/calls/group/Member.ts @@ -19,6 +19,7 @@ import {makeTxnId, makeId} from "../../common"; import {EventType, CallErrorCode} from "../callEventTypes"; import {formatToDeviceMessagesPayload} from "../../common"; import {sortedIndex} from "../../../utils/sortedIndex"; + import type {MuteSettings} from "../common"; import type {Options as PeerCallOptions, RemoteMedia} from "../PeerCall"; import type {LocalMedia} from "../LocalMedia"; @@ -28,8 +29,9 @@ import type {GroupCall} from "./GroupCall"; import type {RoomMember} from "../../room/members/RoomMember"; import type {EncryptedMessage} from "../../e2ee/olm/Encryption"; import type {ILogItem} from "../../../logging/types"; +import type {BaseObservableValue} from "../../../observable/value/BaseObservableValue"; -export type Options = Omit & { +export type Options = Omit & { confId: string, ownUserId: string, ownDeviceId: string, @@ -60,6 +62,7 @@ class MemberConnection { constructor( public localMedia: LocalMedia, public localMuteSettings: MuteSettings, + public turnServer: BaseObservableValue, public readonly logItem: ILogItem ) {} } @@ -112,12 +115,17 @@ export class Member { } /** @internal */ - connect(localMedia: LocalMedia, localMuteSettings: MuteSettings, memberLogItem: ILogItem): ILogItem | undefined { + connect(localMedia: LocalMedia, localMuteSettings: MuteSettings, turnServer: BaseObservableValue, memberLogItem: ILogItem): ILogItem | undefined { if (this.connection) { return; } // Safari can't send a MediaStream to multiple sources, so clone it - const connection = new MemberConnection(localMedia.clone(), localMuteSettings, memberLogItem); + const connection = new MemberConnection( + localMedia.clone(), + localMuteSettings, + turnServer, + memberLogItem + ); this.connection = connection; let connectLogItem; connection.logItem.wrap("connect", async log => { @@ -204,7 +212,7 @@ export class Member { if (hangupReason && !errorCodesWithoutRetry.includes(hangupReason)) { connection.retryCount += 1; const {retryCount} = connection; - connection.logItem.wrap({l: "retry connection", retryCount}, async retryLog => { + connection.logItem.wrap({l: "retry connection", retryCount}, async retryLog => { log.refDetached(retryLog); if (retryCount <= 3) { await this.callIfNeeded(retryLog); @@ -337,9 +345,11 @@ export class Member { } private _createPeerCall(callId: string): PeerCall { + const connection = this.connection!; return new PeerCall(callId, Object.assign({}, this.options, { emitUpdate: this.emitUpdateFromPeerCall, - sendSignallingMessage: this.sendSignallingMessage - }), this.connection!.logItem); + sendSignallingMessage: this.sendSignallingMessage, + turnServer: connection.turnServer + }), connection.logItem); } } From 917ad52302a78d7367c19a56287bc4284e411f5a Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:25:53 +0200 Subject: [PATCH 08/16] use observable for turnserver in peercall and subscribe if changing config is supported --- src/matrix/calls/PeerCall.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/matrix/calls/PeerCall.ts b/src/matrix/calls/PeerCall.ts index ef906d97..ccff5868 100644 --- a/src/matrix/calls/PeerCall.ts +++ b/src/matrix/calls/PeerCall.ts @@ -115,8 +115,18 @@ export class PeerCall implements IDisposable { ) { logItem.log({l: "create PeerCall", id: callId}); this._remoteMedia = new RemoteMedia(); - this.peerConnection = options.webRTC.createPeerConnection(this.options.forceTURN, this.options.turnServers, 0); - + this.peerConnection = options.webRTC.createPeerConnection( + this.options.forceTURN, + [this.options.turnServer.get()], + 0 + ); + // update turn servers when they change (see TurnServerSource) if possible + if (typeof this.peerConnection["setConfiguration"] === "function") { + this.disposables.track(this.options.turnServer.subscribe(turnServer => { + this.logItem.log({l: "updating turn server", turnServer}) + this.peerConnection["setConfiguration"]({iceServers: [turnServer]}); + })); + } const listen = (type: K, listener: (this: PeerConnection, ev: PeerConnectionEventMap[K]) => any, options?: boolean | EventListenerOptions): void => { this.peerConnection.addEventListener(type, listener); const dispose = () => { From f74c4e642590395e18f135d6266578123c7a7350 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:26:16 +0200 Subject: [PATCH 09/16] remove previous approach --- src/matrix/Session.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 3c49182d..f82ad555 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -513,7 +513,6 @@ export class Session { // TODO: what can we do if this throws? await txn.complete(); } - await this._updateTurnServers(); // enable session backup, this requests the latest backup version if (!this._keyBackup.get()) { if (dehydratedDevice) { @@ -560,18 +559,6 @@ export class Session { } } - async _updateTurnServers() { - const turnServerData = await this._hsApi.getTurnServer().response(); - this._callHandler.setTurnServers([{ - urls: turnServerData.uris, - username: turnServerData.username, - credential: turnServerData.password, - }]); - if (turnServerData.ttl > 0) { - setTimeout(this._updateTurnServers, turnServerData.ttl * 1000); - } - } - async _getPendingEventsByRoom(txn) { const pendingEvents = await txn.pendingEvents.getAll(); return pendingEvents.reduce((groups, pe) => { From e6bf49a6ccbee379613848d6f25fd7fa9981f817 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:27:41 +0200 Subject: [PATCH 10/16] remove previous hardcoded turnServers setting --- src/matrix/Session.js | 3 --- src/matrix/calls/CallHandler.ts | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index f82ad555..7d187b30 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -101,9 +101,6 @@ export class Session { ownDeviceId: sessionInfo.deviceId, ownUserId: sessionInfo.userId, logger: this._platform.logger, - turnServers: [{ - urls: ["stun:turn.matrix.org"], - }], forceTURN: false, }); this._roomStateHandler = new RoomStateHandlerSet(); diff --git a/src/matrix/calls/CallHandler.ts b/src/matrix/calls/CallHandler.ts index 354c9e6a..7b8265c5 100644 --- a/src/matrix/calls/CallHandler.ts +++ b/src/matrix/calls/CallHandler.ts @@ -40,7 +40,7 @@ import type {Clock} from "../../platform/web/dom/Clock"; import type {RoomStateHandler} from "../room/state/types"; import type {MemberSync} from "../room/timeline/persistence/MemberWriter"; -export type Options = Omit & { +export type Options = Omit & { clock: Clock }; From 05c2da95c4ffa99b2581e7b1a3077a5fcb41901d Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:38:04 +0200 Subject: [PATCH 11/16] fix typo --- src/matrix/calls/TurnServerSource.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/matrix/calls/TurnServerSource.ts b/src/matrix/calls/TurnServerSource.ts index cc6923af..cac08bdf 100644 --- a/src/matrix/calls/TurnServerSource.ts +++ b/src/matrix/calls/TurnServerSource.ts @@ -24,7 +24,7 @@ import type {Clock, Timeout} from "../../platform/web/dom/Clock"; import type {ILogItem} from "../../logging/types"; type TurnServerSettings = { - urls: string[], + uris: string[], username: string, password: string, ttl: number @@ -54,6 +54,7 @@ export class TurnServerSource { if (!this.isPolling) { const settings = await this.doRequest(log); const iceServer = settings ? toIceServer(settings) : this.defaultSettings; + log.set("iceServer", iceServer); if (this.currentObservable) { this.currentObservable.set(iceServer); } else { @@ -149,7 +150,7 @@ function shouldUpdate(observable: BaseObservableValue, function toIceServer(settings: TurnServerSettings): RTCIceServer { return { - urls: settings.urls, + urls: settings.uris, username: settings.username, credential: settings.password, credentialType: "password" From d36b9be24f1d041c2c668f213041165a23921fd0 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:41:30 +0200 Subject: [PATCH 12/16] fix screwing up whitespace --- src/matrix/calls/group/Member.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/calls/group/Member.ts b/src/matrix/calls/group/Member.ts index 7cd900f2..b1c7b430 100644 --- a/src/matrix/calls/group/Member.ts +++ b/src/matrix/calls/group/Member.ts @@ -212,7 +212,7 @@ export class Member { if (hangupReason && !errorCodesWithoutRetry.includes(hangupReason)) { connection.retryCount += 1; const {retryCount} = connection; - connection.logItem.wrap({l: "retry connection", retryCount}, async retryLog => { + connection.logItem.wrap({l: "retry connection", retryCount}, async retryLog => { log.refDetached(retryLog); if (retryCount <= 3) { await this.callIfNeeded(retryLog); From ac319bdafd747b1dbf3575dc2aeba59145db50cb Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:45:41 +0200 Subject: [PATCH 13/16] we can assume setConfiguration is available --- src/matrix/calls/PeerCall.ts | 12 +++++------- src/platform/types/WebRTC.ts | 1 + 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/matrix/calls/PeerCall.ts b/src/matrix/calls/PeerCall.ts index ccff5868..a7ae7b04 100644 --- a/src/matrix/calls/PeerCall.ts +++ b/src/matrix/calls/PeerCall.ts @@ -120,13 +120,11 @@ export class PeerCall implements IDisposable { [this.options.turnServer.get()], 0 ); - // update turn servers when they change (see TurnServerSource) if possible - if (typeof this.peerConnection["setConfiguration"] === "function") { - this.disposables.track(this.options.turnServer.subscribe(turnServer => { - this.logItem.log({l: "updating turn server", turnServer}) - this.peerConnection["setConfiguration"]({iceServers: [turnServer]}); - })); - } + // update turn servers when they change (see TurnServerSource) + this.disposables.track(this.options.turnServer.subscribe(turnServer => { + this.logItem.log({l: "updating turn server", turnServer}) + this.peerConnection.setConfiguration({iceServers: [turnServer]}); + })); const listen = (type: K, listener: (this: PeerConnection, ev: PeerConnectionEventMap[K]) => any, options?: boolean | EventListenerOptions): void => { this.peerConnection.addEventListener(type, listener); const dispose = () => { diff --git a/src/platform/types/WebRTC.ts b/src/platform/types/WebRTC.ts index 39ad49c5..236e8354 100644 --- a/src/platform/types/WebRTC.ts +++ b/src/platform/types/WebRTC.ts @@ -148,6 +148,7 @@ export interface PeerConnection { addEventListener(type: K, listener: (this: PeerConnection, ev: PeerConnectionEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; removeEventListener(type: K, listener: (this: PeerConnection, ev: PeerConnectionEventMap[K]) => any, options?: boolean | EventListenerOptions): void; getStats(selector?: Track | null): Promise; + setConfiguration(configuration?: RTCConfiguration): void; } From 24ebf6c559154aa6dcbc4685613b1b57e561f7de Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:47:48 +0200 Subject: [PATCH 14/16] cleanup --- src/matrix/calls/TurnServerSource.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/matrix/calls/TurnServerSource.ts b/src/matrix/calls/TurnServerSource.ts index cac08bdf..ca0f6848 100644 --- a/src/matrix/calls/TurnServerSource.ts +++ b/src/matrix/calls/TurnServerSource.ts @@ -64,7 +64,7 @@ export class TurnServerSource { }, () => { // start loop on first subscribe - this.runLoop(this.currentObservable!, settings?.ttl ?? DEFAULT_TTL); + this.runLoop(settings?.ttl ?? DEFAULT_TTL); }); } } @@ -72,7 +72,7 @@ export class TurnServerSource { }); } - private async runLoop(observable: ObservableValue, initialTtl: number): Promise { + private async runLoop(initialTtl: number): Promise { let ttl = initialTtl; this.isPolling = true; while(this.isPolling) { @@ -83,8 +83,8 @@ export class TurnServerSource { const settings = await this.doRequest(undefined); if (settings) { const iceServer = toIceServer(settings); - if (shouldUpdate(observable, iceServer)) { - observable.set(iceServer); + if (shouldUpdate(this.currentObservable!, iceServer)) { + this.currentObservable!.set(iceServer); } if (settings.ttl > 0) { ttl = settings.ttl; From eccbab1491d6245ae95084ff120907be66cfd7f2 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:57:20 +0200 Subject: [PATCH 15/16] cleanup polling and also hangup on all ongoing calls on dispose session --- src/matrix/Session.js | 2 ++ src/matrix/calls/CallHandler.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 7d187b30..31b57b57 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -488,6 +488,8 @@ export class Session { this._megolmDecryption = undefined; this._e2eeAccount?.dispose(); this._e2eeAccount = undefined; + this._callHandler?.dispose(); + this._callHandler = undefined; for (const room of this._rooms.values()) { room.dispose(); } diff --git a/src/matrix/calls/CallHandler.ts b/src/matrix/calls/CallHandler.ts index 7b8265c5..67cf3dc7 100644 --- a/src/matrix/calls/CallHandler.ts +++ b/src/matrix/calls/CallHandler.ts @@ -55,10 +55,12 @@ export class CallHandler implements RoomStateHandler { private roomMemberToCallIds: Map> = new Map(); private groupCallOptions: GroupCallOptions; private sessionId = makeId("s"); + private turnServerSource: TurnServerSource; constructor(private readonly options: Options) { + this.turnServerSource = new TurnServerSource(this.options.hsApi, this.options.clock); this.groupCallOptions = Object.assign({}, this.options, { - turnServerSource: new TurnServerSource(this.options.hsApi, this.options.clock), + turnServerSource: this.turnServerSource, emitUpdate: (groupCall, params) => this._calls.update(groupCall.id, params), createTimeout: this.options.clock.createTimeout, sessionId: this.sessionId @@ -243,5 +245,11 @@ export class CallHandler implements RoomStateHandler { this.roomMemberToCallIds.set(roomMemberKey, newCallIdsMemberOf); } } + + dispose() { + this.turnServerSource.dispose(); + const joinedCalls = Array.from(this._calls.values()).filter(c => c.hasJoined); + Promise.all(joinedCalls.map(c => c.leave())).then(() => {}, () => {}); + } } From c660d82d6a47620a78ac18b9eed443b103cec3c6 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 26 Sep 2022 16:00:25 +0200 Subject: [PATCH 16/16] make internal method private --- src/matrix/calls/TurnServerSource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/calls/TurnServerSource.ts b/src/matrix/calls/TurnServerSource.ts index ca0f6848..1066f7a6 100644 --- a/src/matrix/calls/TurnServerSource.ts +++ b/src/matrix/calls/TurnServerSource.ts @@ -120,7 +120,7 @@ export class TurnServerSource { } } - stopPollLoop() { + private stopPollLoop() { this.isPolling = false; this.currentObservable = undefined; this.pollTimeout?.dispose();