diff --git a/openvidu-browser/src/OpenVidu/OpenVidu.ts b/openvidu-browser/src/OpenVidu/OpenVidu.ts index 181a6ce3..e776b023 100644 --- a/openvidu-browser/src/OpenVidu/OpenVidu.ts +++ b/openvidu-browser/src/OpenVidu/OpenVidu.ts @@ -117,12 +117,14 @@ export class OpenVidu { * @hidden */ webrtcStatsInterval: number = -1; - /** * @hidden */ sendBrowserLogs: OpenViduLoggerConfiguration = OpenViduLoggerConfiguration.disabled; - + /** + * @hidden + */ + isPro: boolean = false; /** * @hidden */ @@ -768,7 +770,8 @@ export class OpenVidu { filterEventDispatched: this.session.onFilterEventDispatched.bind(this.session), iceCandidate: this.session.recvIceCandidate.bind(this.session), mediaError: this.session.onMediaError.bind(this.session), - masterNodeCrashedNotification: this.onMasterNodeCrashedNotification.bind(this) + masterNodeCrashedNotification: this.onMasterNodeCrashedNotification.bind(this), + forciblyReconnectSubscriber: this.session.onForciblyReconnectSubscriber.bind(this.session) } }; this.jsonRpcClient = new RpcBuilder.clients.JsonRpcClient(config); diff --git a/openvidu-browser/src/OpenVidu/Session.ts b/openvidu-browser/src/OpenVidu/Session.ts index 7c1fcafe..e984c527 100644 --- a/openvidu-browser/src/OpenVidu/Session.ts +++ b/openvidu-browser/src/OpenVidu/Session.ts @@ -778,25 +778,22 @@ export class Session extends EventDispatcher { * @hidden */ onParticipantLeft(event: { connectionId: string, reason: string }): void { + this.getRemoteConnection(event.connectionId, 'onParticipantLeft').then(connection => { + if (!!connection.stream) { + const stream = connection.stream; - if (this.remoteConnections.size > 0) { - this.getRemoteConnection(event.connectionId, 'onParticipantLeft').then(connection => { - if (!!connection.stream) { - const stream = connection.stream; + const streamEvent = new StreamEvent(true, this, 'streamDestroyed', stream, event.reason); + this.ee.emitEvent('streamDestroyed', [streamEvent]); + streamEvent.callDefaultBehavior(); - const streamEvent = new StreamEvent(true, this, 'streamDestroyed', stream, event.reason); - this.ee.emitEvent('streamDestroyed', [streamEvent]); - streamEvent.callDefaultBehavior(); - - this.remoteStreamsCreated.delete(stream.streamId); - } - this.remoteConnections.delete(connection.connectionId); - this.ee.emitEvent('connectionDestroyed', [new ConnectionEvent(false, this, 'connectionDestroyed', connection, event.reason)]); - }) - .catch(openViduError => { - logger.error(openViduError); - }); - } + this.remoteStreamsCreated.delete(stream.streamId); + } + this.remoteConnections.delete(connection.connectionId); + this.ee.emitEvent('connectionDestroyed', [new ConnectionEvent(false, this, 'connectionDestroyed', connection, event.reason)]); + }) + .catch(openViduError => { + logger.error(openViduError); + }); } /** @@ -1123,6 +1120,61 @@ export class Session extends EventDispatcher { }); } + /** + * @hidden + */ + onForciblyReconnectSubscriber(event: { connectionId: string, streamId: string, sdpOffer: string }): Promise { + return new Promise((resolve, reject) => { + this.getRemoteConnection(event.connectionId, 'onForciblyReconnectSubscriber') + .then(connection => { + if (!!connection.stream && connection.stream.streamId === event.streamId) { + const stream = connection.stream; + + if (stream.setupReconnectionEventEmitter(resolve, reject)) { + // Ongoing reconnection + // Wait for the event emitter to be free (with success or error) and call the method again + if (stream.reconnectionEventEmitter!['onForciblyReconnectSubscriberLastEvent'] != null) { + // Two or more onForciblyReconnectSubscriber events were received while a reconnection process + // of the subscriber was already taking place. Always use the last one to retry the re-subscription + // process, as that SDP offer will be the only one available at the server side. Ignore previous ones + stream.reconnectionEventEmitter!['onForciblyReconnectSubscriberLastEvent'] = event; + reject('Ongoing forced subscriber reconnection'); + } else { + // One onForciblyReconnectSubscriber even has been received while a reconnection process + // of the subscriber was already taking place. Set up a listener to wait for it to retry the + // forced reconnection process + stream.reconnectionEventEmitter!['onForciblyReconnectSubscriberLastEvent'] = event; + const callback = () => { + const eventAux = stream.reconnectionEventEmitter!['onForciblyReconnectSubscriberLastEvent']; + delete stream.reconnectionEventEmitter!['onForciblyReconnectSubscriberLastEvent']; + this.onForciblyReconnectSubscriber(eventAux); + } + stream.reconnectionEventEmitter!.once('success', () => { + callback(); + }); + stream.reconnectionEventEmitter!.once('error', () => { + callback(); + }); + } + return; + } + + stream.completeWebRtcPeerReceive(true, event.sdpOffer) + .then(() => stream.finalResolveForSubscription(true, resolve)) + .catch(error => stream.finalRejectForSubscription(true, `Error while forcibly reconnecting remote stream ${event.streamId}: ${error.toString()}`, reject)); + } else { + const errMsg = "No stream with streamId '" + event.streamId + "' found for connection '" + event.connectionId + "' on 'streamPropertyChanged' event"; + logger.error(errMsg); + reject(errMsg); + } + }) + .catch(openViduError => { + logger.error(openViduError); + reject(openViduError); + }); + }); + } + /** * @hidden */ @@ -1463,6 +1515,7 @@ export class Session extends EventDispatcher { if (!!sendBrowserLogs) { this.openvidu.sendBrowserLogs = sendBrowserLogs; } + this.openvidu.isPro = !!webrtcStatsInterval && !!sendBrowserLogs; this.openvidu.wsUri = 'wss://' + url.host + '/openvidu'; this.openvidu.httpUri = 'https://' + url.host; diff --git a/openvidu-browser/src/OpenVidu/Stream.ts b/openvidu-browser/src/OpenVidu/Stream.ts index 142473c8..fe13ffd7 100644 --- a/openvidu-browser/src/OpenVidu/Stream.ts +++ b/openvidu-browser/src/OpenVidu/Stream.ts @@ -815,6 +815,27 @@ export class Stream { return false; } + /** + * @hidden + */ + setupReconnectionEventEmitter(resolve: (value: void | PromiseLike) => void, reject: (reason?: any) => void): boolean { + if (this.reconnectionEventEmitter == undefined) { + // There is no ongoing reconnection + this.reconnectionEventEmitter = new EventEmitter(); + return false; + } else { + // Ongoing reconnection + console.warn(`Trying to reconnect stream ${this.streamId} (${this.isLocal() ? 'Publisher' : 'Subscriber'}) but an ongoing reconnection process is active. Waiting for response...`); + this.reconnectionEventEmitter.once('success', () => { + resolve(); + }); + this.reconnectionEventEmitter.once('error', error => { + reject(error); + }); + return true; + } + } + /** * @hidden */ @@ -822,18 +843,8 @@ export class Stream { return new Promise((resolve, reject) => { if (reconnect) { - if (this.reconnectionEventEmitter == undefined) { - // There is no ongoing reconnection - this.reconnectionEventEmitter = new EventEmitter(); - } else { + if (this.setupReconnectionEventEmitter(resolve, reject)) { // Ongoing reconnection - console.warn(`Trying to reconnect stream ${this.streamId} (Publisher) but an ongoing reconnection process is active. Waiting for response...`); - this.reconnectionEventEmitter.once('success', () => { - resolve(); - }); - this.reconnectionEventEmitter.once('error', error => { - reject(error); - }); return; } } else { @@ -956,6 +967,32 @@ export class Stream { }); } + /** + * @hidden + */ + finalResolveForSubscription(reconnect: boolean, resolve: (value: void | PromiseLike) => void) { + logger.info("'Subscriber' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "subscribed")); + this.remotePeerSuccessfullyEstablished(reconnect); + this.initWebRtcStats(); + if (reconnect) { + this.reconnectionEventEmitter?.emitEvent('success'); + delete this.reconnectionEventEmitter; + } + resolve(); + } + + /** + * @hidden + */ + finalRejectForSubscription(reconnect: boolean, error: any, reject: (reason?: any) => void) { + logger.error("Error for 'Subscriber' (" + this.streamId + ") while trying to " + (reconnect ? "reconnect" : "subscribe") + ": " + error.toString()); + if (reconnect) { + this.reconnectionEventEmitter?.emitEvent('error', [error]); + delete this.reconnectionEventEmitter; + } + reject(error); + } + /** * @hidden */ @@ -963,56 +1000,27 @@ export class Stream { return new Promise((resolve, reject) => { if (reconnect) { - if (this.reconnectionEventEmitter == undefined) { - // There is no ongoing reconnection - this.reconnectionEventEmitter = new EventEmitter(); - } else { + if (this.setupReconnectionEventEmitter(resolve, reject)) { // Ongoing reconnection - console.warn(`Trying to reconnect stream ${this.streamId} (Subscriber) but an ongoing reconnection process is active. Waiting for response...`); - this.reconnectionEventEmitter.once('success', () => { - resolve(); - }); - this.reconnectionEventEmitter.once('error', error => { - reject(error); - }); return; } } - const finalResolve = () => { - logger.info("'Subscriber' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "subscribed")); - this.remotePeerSuccessfullyEstablished(reconnect); - this.initWebRtcStats(); - if (reconnect) { - this.reconnectionEventEmitter?.emitEvent('success'); - delete this.reconnectionEventEmitter; - } - resolve(); - } - - const finalReject = error => { - if (reconnect) { - this.reconnectionEventEmitter?.emitEvent('error', [error]); - delete this.reconnectionEventEmitter; - } - reject(error); - } - if (this.session.openvidu.mediaServer === 'mediasoup') { // Server initiates negotiation this.initWebRtcPeerReceiveFromServer(reconnect) - .then(() => finalResolve()) - .catch(error => finalReject(error)); + .then(() => this.finalResolveForSubscription(reconnect, resolve)) + .catch(error => this.finalRejectForSubscription(reconnect, error, reject)); } else { // Client initiates negotiation this.initWebRtcPeerReceiveFromClient(reconnect) - .then(() => finalResolve()) - .catch(error => finalReject(error)); + .then(() => this.finalResolveForSubscription(reconnect, resolve)) + .catch(error => this.finalRejectForSubscription(reconnect, error, reject)); } }); @@ -1320,7 +1328,10 @@ export class Stream { return state; } - private initWebRtcStats(): void { + /** + * @hidden + */ + initWebRtcStats(): void { this.webRtcStats = new WebRtcStats(this); this.webRtcStats.initWebRtcStats(); diff --git a/openvidu-browser/src/OpenViduInternal/Logger/OpenViduLogger.ts b/openvidu-browser/src/OpenViduInternal/Logger/OpenViduLogger.ts index 4f55376b..b6e7747a 100644 --- a/openvidu-browser/src/OpenViduInternal/Logger/OpenViduLogger.ts +++ b/openvidu-browser/src/OpenViduInternal/Logger/OpenViduLogger.ts @@ -24,14 +24,14 @@ export class OpenViduLogger { private loggingSessionId: string | undefined; /** - * @hidden - */ + * @hidden + */ static configureJSNLog(openVidu: OpenVidu, token: string) { try { - // If dev mode + // If dev mode or... if ((window['LOG_JSNLOG_RESULTS']) || // If instance is created and it is OpenVidu Pro - (this.instance && openVidu.webrtcStatsInterval > -1 + (this.instance && openVidu.isPro // If logs are enabled && this.instance.isOpenViduBrowserLogsDebugActive(openVidu) // Only reconfigure it if session or finalUserId has changed @@ -135,8 +135,8 @@ export class OpenViduLogger { } /** - * @hidden - */ + * @hidden + */ static getInstance(): OpenViduLogger { if (!OpenViduLogger.instance) { OpenViduLogger.instance = new OpenViduLogger(); @@ -159,9 +159,9 @@ export class OpenViduLogger { // Return console functions with jsnlog integration private getConsoleWithJSNLog() { - return function(openViduLogger: OpenViduLogger){ + return function (openViduLogger: OpenViduLogger) { return { - log: function(...args){ + log: function (...args) { openViduLogger.defaultConsoleLogger.log.apply(openViduLogger.defaultConsoleLogger.logger, arguments); if (openViduLogger.isJSNLogSetup) { JL().info(arguments); @@ -173,7 +173,7 @@ export class OpenViduLogger { JL().info(arguments); } }, - debug: function(...args) { + debug: function (...args) { openViduLogger.defaultConsoleLogger.debug.apply(openViduLogger.defaultConsoleLogger.logger, arguments); }, warn: function (...args) { @@ -202,7 +202,7 @@ export class OpenViduLogger { } private disableLogger() { - JL.setOptions({enabled: false}); + JL.setOptions({ enabled: false }); this.isJSNLogSetup = false; this.loggingSessionId = undefined; this.currentAppender = undefined; @@ -215,8 +215,8 @@ export class OpenViduLogger { } /** - * @hidden - */ + * @hidden + */ log(...args: any[]) { if (!this.isProdMode) { this.defaultConsoleLogger.log.apply(this.defaultConsoleLogger.logger, arguments); @@ -227,8 +227,8 @@ export class OpenViduLogger { } /** - * @hidden - */ + * @hidden + */ debug(...args: any[]) { if (!this.isProdMode) { this.defaultConsoleLogger.debug.apply(this.defaultConsoleLogger.logger, arguments); @@ -236,8 +236,8 @@ export class OpenViduLogger { } /** - * @hidden - */ + * @hidden + */ info(...args: any[]) { if (!this.isProdMode) { this.defaultConsoleLogger.info.apply(this.defaultConsoleLogger.logger, arguments); @@ -248,8 +248,8 @@ export class OpenViduLogger { } /** - * @hidden - */ + * @hidden + */ warn(...args: any[]) { if (!this.isProdMode) { this.defaultConsoleLogger.warn.apply(this.defaultConsoleLogger.logger, arguments); @@ -260,8 +260,8 @@ export class OpenViduLogger { } /** - * @hidden - */ + * @hidden + */ error(...args: any[]) { this.defaultConsoleLogger.error.apply(this.defaultConsoleLogger.logger, arguments); if (this.isJSNLogSetup) { @@ -270,8 +270,8 @@ export class OpenViduLogger { } /** - * @hidden - */ + * @hidden + */ flush() { if (this.isJSNLogSetup && this.currentAppender != null) { this.currentAppender.sendBatch();