You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-09 10:22:46 +03:00
Do an ICE Restart if WebRTC become disconnected (#3341)
* Do an ice restart if ICE disconnected - Waite two seconds after disconnected - Remove check for finish ICE gathering and try to add each local candidate. Avoid race in multible ICE gathering * Add tests for failed iceConnectionState * suppress type check in unit test * fix pr issues
This commit is contained in:
@@ -239,6 +239,8 @@ export class MockRTCPeerConnection {
|
|||||||
public triggerIncomingDataChannel(): void {
|
public triggerIncomingDataChannel(): void {
|
||||||
this.onDataChannelListener?.({ channel: {} } as RTCDataChannelEvent);
|
this.onDataChannelListener?.({ channel: {} } as RTCDataChannelEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public restartIce(): void {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MockRTCRtpSender {
|
export class MockRTCRtpSender {
|
||||||
|
@@ -1652,12 +1652,18 @@ describe("Call", function () {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
jest.spyOn(call, "hangup");
|
jest.spyOn(call, "hangup");
|
||||||
|
|
||||||
await fakeIncomingCall(client, call, "1");
|
await fakeIncomingCall(client, call, "1");
|
||||||
|
|
||||||
mockPeerConn = call.peerConn as unknown as MockRTCPeerConnection;
|
mockPeerConn = call.peerConn as unknown as MockRTCPeerConnection;
|
||||||
|
|
||||||
mockPeerConn.iceConnectionState = "disconnected";
|
mockPeerConn.iceConnectionState = "disconnected";
|
||||||
mockPeerConn.iceConnectionStateChangeListener!();
|
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", () => {
|
it("should hang up after being disconnected for 30 seconds", () => {
|
||||||
@@ -1665,6 +1671,20 @@ describe("Call", function () {
|
|||||||
expect(call.hangup).toHaveBeenCalledWith(CallErrorCode.IceFailed, false);
|
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", () => {
|
it("should not hangup if we've managed to re-connect", () => {
|
||||||
mockPeerConn.iceConnectionState = "connected";
|
mockPeerConn.iceConnectionState = "connected";
|
||||||
mockPeerConn.iceConnectionStateChangeListener!();
|
mockPeerConn.iceConnectionStateChangeListener!();
|
||||||
|
@@ -263,7 +263,8 @@ const CALL_TIMEOUT_MS = 60 * 1000; // ms
|
|||||||
const CALL_LENGTH_INTERVAL = 1000; // ms
|
const CALL_LENGTH_INTERVAL = 1000; // ms
|
||||||
/** The time after which we end the call, if ICE got disconnected */
|
/** The time after which we end the call, if ICE got disconnected */
|
||||||
const ICE_DISCONNECTED_TIMEOUT = 30 * 1000; // ms
|
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 {
|
export class CallError extends Error {
|
||||||
public readonly code: string;
|
public readonly code: string;
|
||||||
|
|
||||||
@@ -382,6 +383,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
private opponentPartyId: string | null | undefined;
|
private opponentPartyId: string | null | undefined;
|
||||||
private opponentCaps?: CallCapabilities;
|
private opponentCaps?: CallCapabilities;
|
||||||
private iceDisconnectedTimeout?: ReturnType<typeof setTimeout>;
|
private iceDisconnectedTimeout?: ReturnType<typeof setTimeout>;
|
||||||
|
private iceReconnectionTimeOut?: ReturnType<typeof setTimeout> | undefined;
|
||||||
private inviteTimeout?: ReturnType<typeof setTimeout>;
|
private inviteTimeout?: ReturnType<typeof setTimeout>;
|
||||||
private readonly removeTrackListeners = new Map<MediaStream, () => void>();
|
private readonly removeTrackListeners = new Map<MediaStream, () => void>();
|
||||||
|
|
||||||
@@ -965,6 +967,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
await this.initOpponentCrypto();
|
await this.initOpponentCrypto();
|
||||||
try {
|
try {
|
||||||
await this.peerConn.setRemoteDescription(invite.offer);
|
await this.peerConn.setRemoteDescription(invite.offer);
|
||||||
|
logger.debug(`Call ${this.callId} initWithInvite() set remote description: ${invite.offer.type}`);
|
||||||
await this.addBufferedIceCandidates();
|
await this.addBufferedIceCandidates();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug(`Call ${this.callId} initWithInvite() failed to set remote description`, e);
|
logger.debug(`Call ${this.callId} initWithInvite() failed to set remote description`, e);
|
||||||
@@ -1790,10 +1793,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent): void => {
|
private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent): void => {
|
||||||
if (event.candidate) {
|
if (event.candidate) {
|
||||||
if (this.candidatesEnded) {
|
if (this.candidatesEnded) {
|
||||||
logger.warn(
|
logger.warn(`Call ${this.callId} gotLocalIceCandidate() got candidate after candidates have ended!`);
|
||||||
`Call ${this.callId} gotLocalIceCandidate() got candidate after candidates have ended - ignoring!`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Call ${this.callId} got local ICE ${event.candidate.sdpMid} ${event.candidate.candidate}`);
|
logger.debug(`Call ${this.callId} got local ICE ${event.candidate.sdpMid} ${event.candidate.candidate}`);
|
||||||
@@ -1817,7 +1817,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
if (this.peerConn?.iceGatheringState === "complete") {
|
if (this.peerConn?.iceGatheringState === "complete") {
|
||||||
this.queueCandidate(null);
|
this.queueCandidate(null); // We should leave it to WebRTC to announce the end
|
||||||
|
logger.debug(
|
||||||
|
`Call ${this.callId} onIceGatheringStateChange() ice gathering state complete, set candidates have ended`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1899,6 +1902,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
this.isSettingRemoteAnswerPending = true;
|
this.isSettingRemoteAnswerPending = true;
|
||||||
await this.peerConn!.setRemoteDescription(content.answer);
|
await this.peerConn!.setRemoteDescription(content.answer);
|
||||||
this.isSettingRemoteAnswerPending = false;
|
this.isSettingRemoteAnswerPending = false;
|
||||||
|
logger.debug(`Call ${this.callId} onAnswerReceived() set remote description: ${content.answer.type}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.isSettingRemoteAnswerPending = false;
|
this.isSettingRemoteAnswerPending = false;
|
||||||
logger.debug(`Call ${this.callId} onAnswerReceived() failed to set remote description`, e);
|
logger.debug(`Call ${this.callId} onAnswerReceived() failed to set remote description`, e);
|
||||||
@@ -1991,6 +1995,8 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
await this.peerConn!.setRemoteDescription(description); // SRD rolls back as needed
|
await this.peerConn!.setRemoteDescription(description); // SRD rolls back as needed
|
||||||
this.isSettingRemoteAnswerPending = false;
|
this.isSettingRemoteAnswerPending = false;
|
||||||
|
|
||||||
|
logger.debug(`Call ${this.callId} onNegotiateReceived() set remote description: ${description.type}`);
|
||||||
|
|
||||||
if (description.type === "offer") {
|
if (description.type === "offer") {
|
||||||
let answer: RTCSessionDescriptionInit;
|
let answer: RTCSessionDescriptionInit;
|
||||||
try {
|
try {
|
||||||
@@ -2003,6 +2009,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.peerConn!.setLocalDescription(answer);
|
await this.peerConn!.setLocalDescription(answer);
|
||||||
|
logger.debug(`Call ${this.callId} onNegotiateReceived() create an answer`);
|
||||||
|
|
||||||
this.sendVoipEvent(EventType.CallNegotiate, {
|
this.sendVoipEvent(EventType.CallNegotiate, {
|
||||||
description: this.peerConn!.localDescription?.toJSON(),
|
description: this.peerConn!.localDescription?.toJSON(),
|
||||||
@@ -2226,7 +2233,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
return; // because ICE can still complete as we're ending the call
|
return; // because ICE can still complete as we're ending the call
|
||||||
}
|
}
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Call ${this.callId} onIceConnectionStateChanged() running (state=${this.peerConn?.iceConnectionState})`,
|
`Call ${this.callId} onIceConnectionStateChanged() running (state=${this.peerConn?.iceConnectionState}, conn=${this.peerConn?.connectionState})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ideally we'd consider the call to be connected when we get media but
|
// ideally we'd consider the call to be connected when we get media but
|
||||||
@@ -2234,6 +2241,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
if (["connected", "completed"].includes(this.peerConn?.iceConnectionState ?? "")) {
|
if (["connected", "completed"].includes(this.peerConn?.iceConnectionState ?? "")) {
|
||||||
clearTimeout(this.iceDisconnectedTimeout);
|
clearTimeout(this.iceDisconnectedTimeout);
|
||||||
this.iceDisconnectedTimeout = undefined;
|
this.iceDisconnectedTimeout = undefined;
|
||||||
|
if (this.iceReconnectionTimeOut) {
|
||||||
|
clearTimeout(this.iceReconnectionTimeOut);
|
||||||
|
}
|
||||||
this.state = CallState.Connected;
|
this.state = CallState.Connected;
|
||||||
|
|
||||||
if (!this.callLengthInterval && !this.callStartTime) {
|
if (!this.callLengthInterval && !this.callStartTime) {
|
||||||
@@ -2244,11 +2254,15 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
}, CALL_LENGTH_INTERVAL);
|
}, CALL_LENGTH_INTERVAL);
|
||||||
}
|
}
|
||||||
} else if (this.peerConn?.iceConnectionState == "failed") {
|
} else if (this.peerConn?.iceConnectionState == "failed") {
|
||||||
|
this.candidatesEnded = false;
|
||||||
// Firefox for Android does not yet have support for restartIce()
|
// Firefox for Android does not yet have support for restartIce()
|
||||||
// (the types say it's always defined though, so we have to cast
|
// (the types say it's always defined though, so we have to cast
|
||||||
// to prevent typescript from warning).
|
// to prevent typescript from warning).
|
||||||
if (this.peerConn?.restartIce as (() => void) | null) {
|
if (this.peerConn?.restartIce as (() => void) | null) {
|
||||||
this.candidatesEnded = false;
|
this.candidatesEnded = false;
|
||||||
|
logger.debug(
|
||||||
|
`Call ${this.callId} onIceConnectionStateChanged() ice restart (state=${this.peerConn?.iceConnectionState})`,
|
||||||
|
);
|
||||||
this.peerConn!.restartIce();
|
this.peerConn!.restartIce();
|
||||||
} else {
|
} else {
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -2257,7 +2271,19 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
this.hangup(CallErrorCode.IceFailed, false);
|
this.hangup(CallErrorCode.IceFailed, false);
|
||||||
}
|
}
|
||||||
} else if (this.peerConn?.iceConnectionState == "disconnected") {
|
} else if (this.peerConn?.iceConnectionState == "disconnected") {
|
||||||
this.iceDisconnectedTimeout = setTimeout(() => {
|
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(
|
logger.info(
|
||||||
`Call ${this.callId} onIceConnectionStateChanged() hanging up call (ICE disconnected for too long)`,
|
`Call ${this.callId} onIceConnectionStateChanged() hanging up call (ICE disconnected for too long)`,
|
||||||
);
|
);
|
||||||
@@ -2887,6 +2913,11 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!this.ignoreOffer) {
|
if (!this.ignoreOffer) {
|
||||||
logger.info(`Call ${this.callId} addIceCandidates() failed to add remote ICE candidate`, err);
|
logger.info(`Call ${this.callId} addIceCandidates() failed to add remote ICE candidate`, err);
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
`Call ${this.callId} addIceCandidates() failed to add remote ICE candidate because ignoring offer`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user