diff --git a/spec/test-utils/webrtc.ts b/spec/test-utils/webrtc.ts index e50a8df45..2037767cc 100644 --- a/spec/test-utils/webrtc.ts +++ b/spec/test-utils/webrtc.ts @@ -239,6 +239,8 @@ export class MockRTCPeerConnection { public triggerIncomingDataChannel(): void { this.onDataChannelListener?.({ channel: {} } as RTCDataChannelEvent); } + + public restartIce(): void {} } export class MockRTCRtpSender { diff --git a/spec/unit/webrtc/call.spec.ts b/spec/unit/webrtc/call.spec.ts index db84ee540..8fb2f3be4 100644 --- a/spec/unit/webrtc/call.spec.ts +++ b/spec/unit/webrtc/call.spec.ts @@ -1652,12 +1652,18 @@ describe("Call", function () { beforeEach(async () => { jest.useFakeTimers(); jest.spyOn(call, "hangup"); - await fakeIncomingCall(client, call, "1"); mockPeerConn = call.peerConn as unknown as MockRTCPeerConnection; + mockPeerConn.iceConnectionState = "disconnected"; mockPeerConn.iceConnectionStateChangeListener!(); + jest.spyOn(mockPeerConn, "restartIce"); + }); + + it("should restart ICE gathering after being disconnected for 2 seconds", () => { + jest.advanceTimersByTime(3 * 1000); + expect(mockPeerConn.restartIce).toHaveBeenCalled(); }); it("should hang up after being disconnected for 30 seconds", () => { @@ -1665,6 +1671,20 @@ describe("Call", function () { expect(call.hangup).toHaveBeenCalledWith(CallErrorCode.IceFailed, false); }); + it("should restart ICE gathering once again after ICE being failed", () => { + mockPeerConn.iceConnectionState = "failed"; + mockPeerConn.iceConnectionStateChangeListener!(); + expect(mockPeerConn.restartIce).toHaveBeenCalled(); + }); + + it("should call hangup after ICE being failed and if there not exists a restartIce method", () => { + // @ts-ignore + mockPeerConn.restartIce = null; + mockPeerConn.iceConnectionState = "failed"; + mockPeerConn.iceConnectionStateChangeListener!(); + expect(call.hangup).toHaveBeenCalledWith(CallErrorCode.IceFailed, false); + }); + it("should not hangup if we've managed to re-connect", () => { mockPeerConn.iceConnectionState = "connected"; mockPeerConn.iceConnectionStateChangeListener!(); diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 641d4cb98..df2a22587 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -263,7 +263,8 @@ const CALL_TIMEOUT_MS = 60 * 1000; // ms const CALL_LENGTH_INTERVAL = 1000; // ms /** The time after which we end the call, if ICE got disconnected */ const ICE_DISCONNECTED_TIMEOUT = 30 * 1000; // ms - +/** The time after which we try a ICE restart, if ICE got disconnected */ +const ICE_RECONNECTING_TIMEOUT = 2 * 1000; // ms export class CallError extends Error { public readonly code: string; @@ -382,6 +383,7 @@ export class MatrixCall extends TypedEventEmitter; + private iceReconnectionTimeOut?: ReturnType | undefined; private inviteTimeout?: ReturnType; private readonly removeTrackListeners = new Map void>(); @@ -965,6 +967,7 @@ export class MatrixCall extends TypedEventEmitter { if (event.candidate) { if (this.candidatesEnded) { - logger.warn( - `Call ${this.callId} gotLocalIceCandidate() got candidate after candidates have ended - ignoring!`, - ); - return; + logger.warn(`Call ${this.callId} gotLocalIceCandidate() got candidate after candidates have ended!`); } logger.debug(`Call ${this.callId} got local ICE ${event.candidate.sdpMid} ${event.candidate.candidate}`); @@ -1817,7 +1817,10 @@ export class MatrixCall extends TypedEventEmitter void) | null) { this.candidatesEnded = false; + logger.debug( + `Call ${this.callId} onIceConnectionStateChanged() ice restart (state=${this.peerConn?.iceConnectionState})`, + ); this.peerConn!.restartIce(); } else { logger.info( @@ -2257,7 +2271,19 @@ export class MatrixCall extends TypedEventEmitter { + this.candidatesEnded = false; + this.iceReconnectionTimeOut = setTimeout((): void => { + logger.info( + `Call ${this.callId} onIceConnectionStateChanged() ICE restarting because of ICE disconnected, (state=${this.peerConn?.iceConnectionState}, conn=${this.peerConn?.connectionState})`, + ); + if (this.peerConn?.restartIce as (() => void) | null) { + this.candidatesEnded = false; + this.peerConn!.restartIce(); + } + this.iceReconnectionTimeOut = undefined; + }, ICE_RECONNECTING_TIMEOUT); + + this.iceDisconnectedTimeout = setTimeout((): void => { logger.info( `Call ${this.callId} onIceConnectionStateChanged() hanging up call (ICE disconnected for too long)`, ); @@ -2887,6 +2913,11 @@ export class MatrixCall extends TypedEventEmitter