From 23223947d087d0702510701f016618c99361d0af Mon Sep 17 00:00:00 2001 From: mekya Date: Thu, 21 Nov 2024 17:13:02 +0300 Subject: [PATCH 1/3] Reconnect faster in a more robust way when connection is failed/closed --- src/main/js/webrtc_adaptor.js | 98 ++++++++++++++++++--------- src/test/js/webrtc_adaptor.test.js | 103 +++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 33 deletions(-) diff --git a/src/main/js/webrtc_adaptor.js b/src/main/js/webrtc_adaptor.js index 0baf34d5..70ae7f87 100644 --- a/src/main/js/webrtc_adaptor.js +++ b/src/main/js/webrtc_adaptor.js @@ -358,6 +358,11 @@ export class WebRTCAdaptor { * This is the time info for the last reconnection attempt */ this.lastReconnectiontionTrialTime = 0; + + /** + * TimerId for the pending try again call + */ + this.pendingTryAgainTimerId = -1; /** * All media management works for teh local stream are made by @MediaManager class. @@ -613,7 +618,7 @@ export class WebRTCAdaptor { this.iceConnectionState(streamId) != "connected" && this.iceConnectionState(streamId) != "completed") { //if it is not connected, try to reconnect - this.reconnectIfRequired(0); + this.reconnectIfRequired(0, false); } }, 3000); } @@ -623,25 +628,36 @@ export class WebRTCAdaptor { * @param {number} [delayMs] * @returns */ - reconnectIfRequired(delayMs = 3000) { + reconnectIfRequired(delayMs = 3000, forceReconnect = false) { if (this.reconnectIfRequiredFlag) { //It's important to run the following methods after 3000 ms because the stream may be stopped by the user in the meantime if (delayMs > 0) { - setTimeout(() => { - this.tryAgain(); - }, delayMs); + + //clear the previous timer if exists + clearTimeout(this.pendingTryAgainTimerId); + + //set a timer to prevent too many trial from different paths + this.pendingTryAgainTimerId = setTimeout(() => + { + this.tryAgain(forceReconnect); + }, + delayMs); + } else { - this.tryAgain() + this.tryAgain(forceReconnect) } } } - tryAgain() { + tryAgain(forceReconnect) { const now = Date.now(); //to prevent too many trial from different paths if (now - this.lastReconnectiontionTrialTime < 3000) { + //check again 3 seconds later if it is not stopped on purpose + Logger.debug("Reconnection is tried before 3 seconds. It will check/try again after 3 seconds"); + this.reconnectIfRequired(3000, forceReconnect); return; } this.lastReconnectiontionTrialTime = now; @@ -650,10 +666,10 @@ export class WebRTCAdaptor { //if remotePeerConnection has a peer connection for the stream id, it means that it is not stopped on purpose if (this.remotePeerConnection[this.publishStreamId] != null && + (forceReconnect || //check connection status to not stop streaming an active stream - this.iceConnectionState(this.publishStreamId) != "checking" && - this.iceConnectionState(this.publishStreamId) != "connected" && - this.iceConnectionState(this.publishStreamId) != "completed") { + ["checking", "connected", "completed"].indexOf(this.iceConnectionState(this.publishStreamId)) === -1) + ) { // notify that reconnection process started for publish this.notifyEventListeners("reconnection_attempt_for_publisher", this.publishStreamId); @@ -669,11 +685,12 @@ export class WebRTCAdaptor { //reconnect play for (var index in this.playStreamId) { let streamId = this.playStreamId[index]; - if (this.remotePeerConnection[streamId] != "null" && - //check connection status to not stop streaming an active stream - this.iceConnectionState(streamId) != "checking" && - this.iceConnectionState(streamId) != "connected" && - this.iceConnectionState(streamId) != "completed") { + if (this.remotePeerConnection[streamId] != null && + (forceReconnect || + //check connection status to not stop streaming an active stream + ["checking", "connected", "completed"].indexOf(this.iceConnectionState(streamId)) === -1 + ) + ) { // notify that reconnection process started for play this.notifyEventListeners("reconnection_attempt_for_player", streamId); @@ -1136,27 +1153,36 @@ export class WebRTCAdaptor { this.remotePeerConnection[streamId].oniceconnectionstatechange = event => { var obj = { state: this.remotePeerConnection[streamId].iceConnectionState, streamId: streamId }; - if (obj.state == "failed" || obj.state == "disconnected" || obj.state == "closed") { - this.reconnectIfRequired(3000); - } - this.notifyEventListeners("ice_connection_state_changed", obj); - - // - if (!this.isPlayMode && !this.playStreamId.includes(streamId)) { - if (this.remotePeerConnection[streamId].iceConnectionState == "connected") { - this.mediaManager.changeBandwidth(this.mediaManager.bandwidth, streamId).then(() => { - Logger.debug("Bandwidth is changed to " + this.mediaManager.bandwidth); - }) - .catch(e => Logger.warn(e)); - } - } + this.oniceconnectionstatechangeCallback(obj); } } return this.remotePeerConnection[streamId]; } + + oniceconnectionstatechangeCallback(obj) + { + if (obj.state == "failed" || obj.state == "disconnected" || obj.state == "closed") { + //try immediately + this.reconnectIfRequired(0, false); + } + this.notifyEventListeners("ice_connection_state_changed", obj); + + // + if (!this.isPlayMode && !this.playStreamId.includes(obj.streamId)) { + if (this.remotePeerConnection[streamId].iceConnectionState == "connected") { + + this.mediaManager.changeBandwidth(this.mediaManager.bandwidth, obj.streamId).then(() => { + Logger.debug("Bandwidth is changed to " + this.mediaManager.bandwidth); + }) + .catch(e => Logger.warn(e)); + } + } + } + + /** * Called internally to close PeerConnection. @@ -1745,10 +1771,7 @@ export class WebRTCAdaptor { websocket_url: this.websocketURL, webrtcadaptor: this, callback: (info, obj) => { - if (info == "closed") { - this.reconnectIfRequired(); - } - this.notifyEventListeners(info, obj); + this.websocketCallback(info, obj) }, callbackError: (error, message) => { this.notifyErrorEventListeners(error, message) @@ -1757,6 +1780,15 @@ export class WebRTCAdaptor { }); } } + + websocketCallback(info, obj) { + if (info == "closed") { + Logger.info("Websocket is closed. It will reconnect if required.") + //try with forcing reconnect because webrtc will be closed as well + this.reconnectIfRequired(0, true); + } + this.notifyEventListeners(info, obj); + } /** * Called to stop Web Socket connection diff --git a/src/test/js/webrtc_adaptor.test.js b/src/test/js/webrtc_adaptor.test.js index be34a32d..46a44dd5 100644 --- a/src/test/js/webrtc_adaptor.test.js +++ b/src/test/js/webrtc_adaptor.test.js @@ -262,6 +262,107 @@ describe("WebRTCAdaptor", function() { expect(webSocketAdaptor.connecting).to.be.false; }); + + + it("reconnectIfRequired", async function() { + var adaptor = new WebRTCAdaptor({ + websocketURL: "ws://example.com", + isPlayMode: true + }); + + let tryAgain = sinon.replace(adaptor, "tryAgain", sinon.fake()); + + + adaptor.reconnectIfRequired(100); + adaptor.reconnectIfRequired(200); + clock.tick(300); + + expect(tryAgain.calledOnce).to.be.true; + + + + }); + + it("oniceconnectionstatechangeCallback", async function() { + var adaptor = new WebRTCAdaptor({ + websocketURL: "ws://example.com", + isPlayMode: true + }); + + let reconnectIfRequired = sinon.replace(adaptor, "reconnectIfRequired", sinon.fake()); + var obj = { state: "failed", streamId: "streamId" }; + + var stopFake = sinon.replace(adaptor, "stop", sinon.fake()); + adaptor.oniceconnectionstatechangeCallback(obj); + expect(reconnectIfRequired.calledOnce).to.be.true; + expect(reconnectIfRequired.calledWithExactly(0, false)).to.be.true; + + obj = { state: "closed", streamId: "streamId" }; + + adaptor.oniceconnectionstatechangeCallback(obj); + expect(reconnectIfRequired.calledTwice).to.be.true; + expect(reconnectIfRequired.calledWithExactly(0, false)).to.be.true; + + obj = { state: "disconnected", streamId: "streamId" }; + adaptor.oniceconnectionstatechangeCallback(obj); + expect(reconnectIfRequired.callCount).to.be.equal(3); + + + obj = { state: "connected", streamId: "streamId" }; + adaptor.oniceconnectionstatechangeCallback(obj); + expect(reconnectIfRequired.callCount).to.be.equal(3); + + + }); + + it("websocketCallback", async function() { + var adaptor = new WebRTCAdaptor({ + websocketURL: "ws://example.com", + isPlayMode: true + }); + + let reconnectIfRequired = sinon.replace(adaptor, "reconnectIfRequired", sinon.fake()); + + var stopFake = sinon.replace(adaptor, "stop", sinon.fake()); + adaptor.websocketCallback("closed"); + + expect(reconnectIfRequired.calledOnce).to.be.true; + expect(reconnectIfRequired.calledWithExactly(0, true)).to.be.true; + + + adaptor.websocketCallback("anyOtherThing"); + + //it should be still once + expect(reconnectIfRequired.calledOnce).to.be.true; + + + }); + + it("tryAgainForceReconnect", async function() { + + var adaptor = new WebRTCAdaptor({ + websocketURL: "ws://example.com", + isPlayMode: true + }); + var streamId = "streamId"; + adaptor.publishStreamId = streamId; + + let stop = sinon.replace(adaptor, "stop", sinon.fake()); + + var mockPC = sinon.mock(RTCPeerConnection); + adaptor.remotePeerConnection[streamId] = mockPC + mockPC.iceConnectionState = "connected"; + + adaptor.tryAgain(false); + + expect(stop.calledOnce).to.be.false; + + adaptor.tryAgain(true); + expect(stop.calledOnce).to.be.false; + + + }); + it("Frequent try again call", async function() { @@ -269,6 +370,8 @@ describe("WebRTCAdaptor", function() { websocketURL: "ws://example.com", isPlayMode: true }); + + expect(adaptor.pendingTryAgainTimerId).to.be.equal(-1); let webSocketAdaptor = sinon.mock(adaptor.webSocketAdaptor); let closeExpectation = webSocketAdaptor.expects("close"); From 8c0ca510bae3964afb31d294486536cbc7c770df Mon Sep 17 00:00:00 2001 From: mekya Date: Sun, 24 Nov 2024 12:35:20 +0300 Subject: [PATCH 2/3] Improve SDK to reconnect to backend if server stops --- src/main/js/webrtc_adaptor.js | 66 +++++++++------------ src/main/webapp/conference.html | 13 ++-- src/main/webapp/samples/publish_webrtc.html | 2 +- 3 files changed, 37 insertions(+), 44 deletions(-) diff --git a/src/main/js/webrtc_adaptor.js b/src/main/js/webrtc_adaptor.js index 70ae7f87..dcd97ef2 100644 --- a/src/main/js/webrtc_adaptor.js +++ b/src/main/js/webrtc_adaptor.js @@ -521,15 +521,8 @@ export class WebRTCAdaptor { } //init peer connection for reconnectIfRequired this.initPeerConnection(streamId, "publish"); - setTimeout(() => { - //check if it is connected or not - //this resolves if the server responds with some error message - if (this.iceConnectionState(this.publishStreamId) != "checking" && this.iceConnectionState(this.publishStreamId) != "connected" && this.iceConnectionState(this.publishStreamId) != "completed") { - //if it is not connected, try to reconnect - this.reconnectIfRequired(0); - } - }, 3000); - + + this.reconnectIfRequired(3000, false); } sendPublishCommand(streamId, token, subscriberId, subscriberCode, streamName, mainTrack, metaData, role, videoEnabled, audioEnabled) { @@ -610,17 +603,7 @@ export class WebRTCAdaptor { //init peer connection for reconnectIfRequired this.initPeerConnection(streamId, "play"); - - setTimeout(() => { - //check if it is connected or not - //this resolves if the server responds with some error message - if (this.iceConnectionState(streamId) != "checking" && - this.iceConnectionState(streamId) != "connected" && - this.iceConnectionState(streamId) != "completed") { - //if it is not connected, try to reconnect - this.reconnectIfRequired(0, false); - } - }, 3000); + this.reconnectIfRequired(3000, false); } /** @@ -630,22 +613,21 @@ export class WebRTCAdaptor { */ reconnectIfRequired(delayMs = 3000, forceReconnect = false) { if (this.reconnectIfRequiredFlag) { - //It's important to run the following methods after 3000 ms because the stream may be stopped by the user in the meantime - if (delayMs > 0) { - - //clear the previous timer if exists + if (delayMs <= 0) { + delayMs = 500; + //clear the timer because there is a demand to reconnect without delay clearTimeout(this.pendingTryAgainTimerId); - - //set a timer to prevent too many trial from different paths + this.pendingTryAgainTimerId = -1; + } + + if (this.pendingTryAgainTimerId == -1) + { this.pendingTryAgainTimerId = setTimeout(() => { + this.pendingTryAgainTimerId = -1; this.tryAgain(forceReconnect); }, delayMs); - - } - else { - this.tryAgain(forceReconnect) } } } @@ -654,10 +636,11 @@ export class WebRTCAdaptor { const now = Date.now(); //to prevent too many trial from different paths - if (now - this.lastReconnectiontionTrialTime < 3000) { - //check again 3 seconds later if it is not stopped on purpose - Logger.debug("Reconnection is tried before 3 seconds. It will check/try again after 3 seconds"); - this.reconnectIfRequired(3000, forceReconnect); + const timeDiff = now - this.lastReconnectiontionTrialTime;; + if (timeDiff < 3000 && forceReconnect == false) { + //check again 1 seconds later if it is not stopped on purpose + Logger.debug("Reconnection request received after "+ timeDiff+" ms. It should be at least 3000ms. It will try again after 1000ms"); + this.reconnectIfRequired(1000, forceReconnect); return; } this.lastReconnectiontionTrialTime = now; @@ -1164,15 +1147,17 @@ export class WebRTCAdaptor { oniceconnectionstatechangeCallback(obj) { + Logger.debug("ice connection state is " +obj.state + " for streamId: " + obj.streamId); if (obj.state == "failed" || obj.state == "disconnected" || obj.state == "closed") { //try immediately + Logger.debug("ice connection state is failed, disconnected or closed for streamId: " + obj.streamId + " it will try to reconnect immediately"); this.reconnectIfRequired(0, false); } this.notifyEventListeners("ice_connection_state_changed", obj); // if (!this.isPlayMode && !this.playStreamId.includes(obj.streamId)) { - if (this.remotePeerConnection[streamId].iceConnectionState == "connected") { + if (this.remotePeerConnection[obj.streamId] != null && this.remotePeerConnection[obj.streamId].iceConnectionState == "connected") { this.mediaManager.changeBandwidth(this.mediaManager.bandwidth, obj.streamId).then(() => { Logger.debug("Bandwidth is changed to " + this.mediaManager.bandwidth); @@ -1782,11 +1767,18 @@ export class WebRTCAdaptor { } websocketCallback(info, obj) { - if (info == "closed") { - Logger.info("Websocket is closed. It will reconnect if required.") + + if (info == "closed" || info == "server_will_stop") { + Logger.info("Critical response from server:"+ info +". It will reconnect immediately if there is an active connection"); + + //close websocket reconnect again + if (info == "server_will_stop") { + this.webSocketAdaptor.close(); + } //try with forcing reconnect because webrtc will be closed as well this.reconnectIfRequired(0, true); } + this.notifyEventListeners(info, obj); } diff --git a/src/main/webapp/conference.html b/src/main/webapp/conference.html index 27dd9122..a75b9c5f 100644 --- a/src/main/webapp/conference.html +++ b/src/main/webapp/conference.html @@ -687,12 +687,12 @@

WebRTC Multitrack Conference

var state = webRTCAdaptor .signallingState(publishStreamId); if (state != null - && state != "closed") { + && state != "closed") + { var iceState = webRTCAdaptor .iceConnectionState(publishStreamId); - if (iceState != null - && iceState != "failed" - && iceState != "disconnected") { + if (iceState != null && iceState != "new" && iceState != "closed" && iceState != "failed" && iceState != "disconnected") + { startAnimation(); } } @@ -864,10 +864,11 @@

WebRTC Multitrack Conference

} else { //errorHandler(error, message); - $('video').notify("Warning: " + errorHandler(error, message), { + + $('#roomName').notify("Warning: " + errorHandler(error, message), { autoHideDelay: 5000, className: 'error', - position: 'top right' + position: 'top center' }); } diff --git a/src/main/webapp/samples/publish_webrtc.html b/src/main/webapp/samples/publish_webrtc.html index dad95560..ec6f7586 100644 --- a/src/main/webapp/samples/publish_webrtc.html +++ b/src/main/webapp/samples/publish_webrtc.html @@ -490,7 +490,7 @@ var state = webRTCAdaptor.signallingState(streamId); if (state != null && state != "closed") { var iceState = webRTCAdaptor.iceConnectionState(streamId); - if (iceState != null && iceState != "failed" && iceState != "disconnected") { + if (iceState != null && iceState != "new" && iceState != "closed" && iceState != "failed" && iceState != "disconnected") { startAnimation(); } else { From e353d95e8bdea133eaf496db01f5de9bf77a8d38 Mon Sep 17 00:00:00 2001 From: mekya Date: Sun, 24 Nov 2024 17:57:21 +0300 Subject: [PATCH 3/3] Fix test case --- src/test/js/webrtc_adaptor.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/js/webrtc_adaptor.test.js b/src/test/js/webrtc_adaptor.test.js index 46a44dd5..f365001e 100644 --- a/src/test/js/webrtc_adaptor.test.js +++ b/src/test/js/webrtc_adaptor.test.js @@ -358,7 +358,7 @@ describe("WebRTCAdaptor", function() { expect(stop.calledOnce).to.be.false; adaptor.tryAgain(true); - expect(stop.calledOnce).to.be.false; + expect(stop.calledOnce).to.be.true; });