diff --git a/openvidu-browser/src/OpenVidu/OpenVidu.ts b/openvidu-browser/src/OpenVidu/OpenVidu.ts index 7977f336..cc0c5777 100644 --- a/openvidu-browser/src/OpenVidu/OpenVidu.ts +++ b/openvidu-browser/src/OpenVidu/OpenVidu.ts @@ -919,6 +919,8 @@ export class OpenVidu { console.warn('Websocket connection lost (reconnecting)'); if (!this.isRoomAvailable()) { alert('Connection error. Please reload page.'); + } else { + this.session.emitEvent('reconnecting', []); } } @@ -928,6 +930,7 @@ export class OpenVidu { this.sendRequest('connect', { sessionId: this.session.connection.rpcSessionId }, (error, response) => { if (!!error) { console.error(error); + console.warn('Websocket was able to reconnect to OpenVidu Server, but your Connection was already destroyed due to timeout. You are no longer a participant of the Session and your media streams have been destroyed'); this.session.onLostConnection("networkDisconnect"); this.jsonRpcClient.close(4101, "Reconnection fault"); } else { diff --git a/openvidu-browser/src/OpenVidu/Session.ts b/openvidu-browser/src/OpenVidu/Session.ts index ff1d112e..bcb534d9 100644 --- a/openvidu-browser/src/OpenVidu/Session.ts +++ b/openvidu-browser/src/OpenVidu/Session.ts @@ -971,7 +971,7 @@ export class Session implements EventDispatcher { * @hidden */ onLostConnection(reason: string): void { - console.warn('Lost connection in session ' + this.sessionId + ' waiting for reconnect'); + console.warn('Lost connection in Session ' + this.sessionId); if (!!this.sessionId && !this.connection.disposed) { this.leave(true, reason); } @@ -981,15 +981,15 @@ export class Session implements EventDispatcher { * @hidden */ onRecoveredConnection(): void { - console.warn('Recovered connection in Session ' + this.sessionId); - // this.ee.emitEvent('connectionRecovered', []); + console.info('Recovered connection in Session ' + this.sessionId); + this.reconnectBrokenStreams(); + this.ee.emitEvent('reconnected', []); } /** * @hidden */ onMediaError(params): void { - console.error('Media error: ' + JSON.stringify(params)); const err = params.error; if (err) { @@ -1030,6 +1030,25 @@ export class Session implements EventDispatcher { }); } + /** + * @hidden + */ + reconnectBrokenStreams(): void { + console.info('Re-establishing media connections'); + // Re-establish Publisher stream + if (!!this.connection.stream && this.connection.stream.streamIceConnectionStateBroken()) { + console.warn('Re-establishing Publisher ' + this.connection.stream.streamId); + this.connection.stream.initWebRtcPeerSend(true); + } + // Re-establish Subscriber streams + for (let remoteConnection of Object.values(this.remoteConnections)) { + if (!!remoteConnection.stream && remoteConnection.stream.streamIceConnectionStateBroken()) { + console.warn('Re-establishing Subscriber ' + remoteConnection.stream.streamId); + remoteConnection.stream.initWebRtcPeerReceive(true); + } + } + } + /** * @hidden */ diff --git a/openvidu-browser/src/OpenVidu/Stream.ts b/openvidu-browser/src/OpenVidu/Stream.ts index ad796f60..6d13f189 100644 --- a/openvidu-browser/src/OpenVidu/Stream.ts +++ b/openvidu-browser/src/OpenVidu/Stream.ts @@ -431,7 +431,7 @@ export class Stream implements EventDispatcher { */ subscribe(): Promise { return new Promise((resolve, reject) => { - this.initWebRtcPeerReceive() + this.initWebRtcPeerReceive(false) .then(() => { resolve(); }) @@ -447,7 +447,7 @@ export class Stream implements EventDispatcher { publish(): Promise { return new Promise((resolve, reject) => { if (this.isLocalStreamReadyToPublish) { - this.initWebRtcPeerSend() + this.initWebRtcPeerSend(false) .then(() => { resolve(); }) @@ -473,19 +473,9 @@ export class Stream implements EventDispatcher { */ disposeWebRtcPeer(): void { if (!!this.webRtcPeer) { - const isSenderAndCustomTrack: boolean = !!this.outboundStreamOpts && - typeof MediaStreamTrack !== 'undefined' && this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack; - this.webRtcPeer.dispose(isSenderAndCustomTrack); + this.webRtcPeer.dispose(); + this.stopWebRtcStats(); } - if (!!this.speechEvent) { - if (!!this.speechEvent.stop) { - this.speechEvent.stop(); - } - delete this.speechEvent; - } - - this.stopWebRtcStats(); - console.info((!!this.outboundStreamOpts ? 'Outbound ' : 'Inbound ') + "WebRTCPeer from 'Stream' with id [" + this.streamId + '] is now closed'); } @@ -502,6 +492,12 @@ export class Stream implements EventDispatcher { }); delete this.mediaStream; } + if (!!this.speechEvent) { + if (!!this.speechEvent.stop) { + this.speechEvent.stop(); + } + delete this.speechEvent; + } console.info((!!this.outboundStreamOpts ? 'Local ' : 'Remote ') + "MediaStream from 'Stream' with id [" + this.streamId + '] is now disposed'); } @@ -767,6 +763,17 @@ export class Stream implements EventDispatcher { return this.webRtcPeer.localCandidatesQueue; } + /** + * @hidden + */ + streamIceConnectionStateBroken() { + if (!this.getWebRtcPeer() || !this.getRTCPeerConnection()) { + return false; + } + const iceConnectionState: RTCIceConnectionState = this.getRTCPeerConnection().iceConnectionState; + return iceConnectionState === 'disconnected' || iceConnectionState === 'failed'; + } + /* Private methods */ private setSpeechEventIfNotExists(): boolean { @@ -782,10 +789,15 @@ export class Stream implements EventDispatcher { return false; } - private initWebRtcPeerSend(): Promise { + /** + * @hidden + */ + initWebRtcPeerSend(reconnect: boolean): Promise { return new Promise((resolve, reject) => { - this.initHarkEvents(); // Init hark events for the local stream + if (!reconnect) { + this.initHarkEvents(); // Init hark events for the local stream + } const userMediaConstraints = { audio: this.isSendAudio(), @@ -804,23 +816,32 @@ export class Stream implements EventDispatcher { console.debug('Sending SDP offer to publish as ' + this.streamId, sdpOfferParam); - let typeOfVideo = ''; - if (this.isSendVideo()) { - typeOfVideo = (typeof MediaStreamTrack !== 'undefined' && this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack) ? 'CUSTOM' : (this.isSendScreen() ? 'SCREEN' : 'CAMERA'); + const method = reconnect ? 'reconnectStream' : 'publishVideo'; + let params; + if (reconnect) { + params = { + stream: this.streamId + } + } else { + let typeOfVideo = ''; + if (this.isSendVideo()) { + typeOfVideo = (typeof MediaStreamTrack !== 'undefined' && this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack) ? 'CUSTOM' : (this.isSendScreen() ? 'SCREEN' : 'CAMERA'); + } + params = { + doLoopback: this.displayMyRemote() || false, + hasAudio: this.isSendAudio(), + hasVideo: this.isSendVideo(), + audioActive: this.audioActive, + videoActive: this.videoActive, + typeOfVideo, + frameRate: !!this.frameRate ? this.frameRate : -1, + videoDimensions: JSON.stringify(this.videoDimensions), + filter: this.outboundStreamOpts.publisherProperties.filter + } } + params['sdpOffer'] = sdpOfferParam; - this.session.openvidu.sendRequest('publishVideo', { - sdpOffer: sdpOfferParam, - doLoopback: this.displayMyRemote() || false, - hasAudio: this.isSendAudio(), - hasVideo: this.isSendVideo(), - audioActive: this.audioActive, - videoActive: this.videoActive, - typeOfVideo, - frameRate: !!this.frameRate ? this.frameRate : -1, - videoDimensions: JSON.stringify(this.videoDimensions), - filter: this.outboundStreamOpts.publisherProperties.filter - }, (error, response) => { + this.session.openvidu.sendRequest(method, params, (error, response) => { if (error) { if (error.code === 401) { reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to publish")); @@ -837,32 +858,43 @@ export class Stream implements EventDispatcher { if (this.displayMyRemote()) { this.remotePeerSuccessfullyEstablished(); } - this.ee.emitEvent('stream-created-by-publisher', []); + if (reconnect) { + this.ee.emitEvent('stream-reconnected-by-publisher', []); + } else { + this.ee.emitEvent('stream-created-by-publisher', []); + } this.initWebRtcStats(); + console.info("'Publisher' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "published") + " to session"); resolve(); }) .catch(error => { reject(error); }); - console.info("'Publisher' successfully published to session"); } }); }; + if (reconnect) { + this.disposeWebRtcPeer(); + } if (this.displayMyRemote()) { this.webRtcPeer = new WebRtcPeerSendrecv(options); } else { this.webRtcPeer = new WebRtcPeerSendonly(options); } - this.webRtcPeer.generateOffer().then(offer => { - successCallback(offer); + this.webRtcPeer.addIceConnectionStateChangeListener('publisher of ' + this.connection.connectionId); + this.webRtcPeer.generateOffer().then(sdpOffer => { + successCallback(sdpOffer); }).catch(error => { reject(new Error('(publish) SDP offer error: ' + JSON.stringify(error))); }); }); } - private initWebRtcPeerReceive(): Promise { + /** + * @hidden + */ + initWebRtcPeerReceive(reconnect: boolean): Promise { return new Promise((resolve, reject) => { const offerConstraints = { @@ -881,10 +913,12 @@ export class Stream implements EventDispatcher { const successCallback = (sdpOfferParam) => { console.debug('Sending SDP offer to subscribe to ' + this.streamId, sdpOfferParam); - this.session.openvidu.sendRequest('receiveVideoFrom', { - sender: this.streamId, - sdpOffer: sdpOfferParam - }, (error, response) => { + + const method = reconnect ? 'reconnectStream' : 'receiveVideoFrom'; + const params = { sdpOffer: sdpOfferParam }; + params[reconnect ? 'stream' : 'sender'] = this.streamId; + + this.session.openvidu.sendRequest(method, params, (error, response) => { if (error) { reject(new Error('Error on recvVideoFrom: ' + JSON.stringify(error))); } else { @@ -901,6 +935,7 @@ export class Stream implements EventDispatcher { } const needsTimeoutOnProcessAnswer = this.session.countDownForIonicIosSubscribersActive; this.webRtcPeer.processAnswer(response.sdpAnswer, needsTimeoutOnProcessAnswer).then(() => { + console.info("'Subscriber' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "subscribed")); this.remotePeerSuccessfullyEstablished(); this.initWebRtcStats(); resolve(); @@ -912,9 +947,10 @@ export class Stream implements EventDispatcher { }; this.webRtcPeer = new WebRtcPeerRecvonly(options); + this.webRtcPeer.addIceConnectionStateChangeListener(this.streamId); this.webRtcPeer.generateOffer() - .then(offer => { - successCallback(offer); + .then(sdpOffer => { + successCallback(sdpOffer); }) .catch(error => { reject(new Error('(subscribe) SDP offer error: ' + JSON.stringify(error))); diff --git a/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts index 875fce04..903cd63a 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts @@ -29,7 +29,10 @@ export class SessionDisconnectedEvent extends Event { * - "forceDisconnectByUser": you have been evicted from the Session by other user calling `Session.forceDisconnect()` * - "forceDisconnectByServer": you have been evicted from the Session by the application * - "sessionClosedByServer": the Session has been closed by the application - * - "networkDisconnect": your network connection has dropped + * - "networkDisconnect": your network connection has dropped. Before a SessionDisconnectedEvent with this reason is triggered, + * Session object will always have previously dispatched a `reconnecting` event. If the reconnection process succeeds, + * Session object will dispatch a `reconnected` event. If it fails, Session object will dispatch a SessionDisconnectedEvent + * with reason "networkDisconnect" */ reason: string; diff --git a/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts b/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts index 30604122..6fc8f458 100644 --- a/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts +++ b/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts @@ -97,34 +97,15 @@ export class WebRtcPeer { /** * This method frees the resources used by WebRtcPeer */ - dispose(videoSourceIsMediaStreamTrack: boolean) { + dispose() { console.debug('Disposing WebRtcPeer'); - try { - if (this.pc) { - if (this.pc.signalingState === 'closed') { - return; - } - this.remoteCandidatesQueue = []; - this.localCandidatesQueue = []; - // Stop senders - for (const sender of this.pc.getSenders()) { - if (!videoSourceIsMediaStreamTrack) { - if (!!sender.track) { - sender.track.stop(); - } - } - this.pc.removeTrack(sender); - } - // Stop receivers - for (const receiver of this.pc.getReceivers()) { - if (!!receiver.track) { - receiver.track.stop(); - } - } - this.pc.close(); + if (this.pc) { + if (this.pc.signalingState === 'closed') { + return; } - } catch (err) { - console.warn('Exception disposing webrtc peer ' + err); + this.pc.close(); + this.remoteCandidatesQueue = []; + this.localCandidatesQueue = []; } } @@ -264,6 +245,36 @@ export class WebRtcPeer { }); } + addIceConnectionStateChangeListener(otherId: string) { + this.pc.oniceconnectionstatechange = () => { + const iceConnectionState: RTCIceConnectionState = this.pc.iceConnectionState; + switch (iceConnectionState) { + case 'disconnected': + // Possible network disconnection + console.warn('IceConnectionState of RTCPeerConnection ' + this.id + ' (' + otherId + ') change to "disconnected". Possible network disconnection'); + break; + case 'failed': + console.error('IceConnectionState of RTCPeerConnection ' + this.id + ' (' + otherId + ') to "failed"'); + break; + case 'closed': + console.log('IceConnectionState of RTCPeerConnection ' + this.id + ' (' + otherId + ') change to "closed"'); + break; + case 'new': + console.log('IceConnectionState of RTCPeerConnection ' + this.id + ' (' + otherId + ') change to "new"'); + break; + case 'checking': + console.log('IceConnectionState of RTCPeerConnection ' + this.id + ' (' + otherId + ') change to "checking"'); + break; + case 'connected': + console.log('IceConnectionState of RTCPeerConnection ' + this.id + ' (' + otherId + ') change to "connected"'); + break; + case 'completed': + console.log('IceConnectionState of RTCPeerConnection ' + this.id + ' (' + otherId + ') change to "completed"'); + break; + } + } + } + }