diff --git a/openvidu-browser/.prettierrc b/openvidu-browser/.prettierrc new file mode 100644 index 00000000..bc72fe80 --- /dev/null +++ b/openvidu-browser/.prettierrc @@ -0,0 +1,10 @@ +{ + "singleQuote": true, + "printWidth": 140, + "trailingComma": "none", + "semi": true, + "bracketSpacing": true, + "useTabs": false, + "jsxSingleQuote": true, + "tabWidth": 4 +} diff --git a/openvidu-browser/src/Main.ts b/openvidu-browser/src/Main.ts index bd14841e..dfc88ff4 100644 --- a/openvidu-browser/src/Main.ts +++ b/openvidu-browser/src/Main.ts @@ -6,4 +6,4 @@ if (typeof globalThis !== 'undefined') { } // Disable jsnlog when library is loaded -JL.setOptions({ enabled: false }) +JL.setOptions({ enabled: false }); diff --git a/openvidu-browser/src/OpenVidu/Connection.ts b/openvidu-browser/src/OpenVidu/Connection.ts index 2fc6383a..c3424572 100644 --- a/openvidu-browser/src/OpenVidu/Connection.ts +++ b/openvidu-browser/src/OpenVidu/Connection.ts @@ -29,13 +29,11 @@ import { ExceptionEvent, ExceptionEventName } from '../OpenViduInternal/Events/E */ const logger: OpenViduLogger = OpenViduLogger.getInstance(); - /** * Represents each one of the user's connection to the session (the local one and other user's connections). * Therefore each [[Session]] and [[Stream]] object has an attribute of type Connection */ export class Connection { - /** * Unique identifier of the connection */ @@ -125,38 +123,46 @@ export class Connection { logger.info(msg); } - /* Hidden methods */ /** * @hidden */ sendIceCandidate(candidate: RTCIceCandidate): void { + logger.debug((!!this.stream!.outboundStreamOpts ? 'Local' : 'Remote') + 'candidate for' + this.connectionId, candidate); - logger.debug((!!this.stream!.outboundStreamOpts ? 'Local' : 'Remote') + 'candidate for' + - this.connectionId, candidate); - - this.session.openvidu.sendRequest('onIceCandidate', { - endpointName: this.connectionId, - candidate: candidate.candidate, - sdpMid: candidate.sdpMid, - sdpMLineIndex: candidate.sdpMLineIndex - }, (error, response) => { - if (error) { - logger.error('Error sending ICE candidate: ' + JSON.stringify(error)); - this.session.emitEvent('exception', [new ExceptionEvent(this.session, ExceptionEventName.ICE_CANDIDATE_ERROR, this.session, "There was an unexpected error on the server-side processing an ICE candidate generated and sent by the client-side", error)]); + this.session.openvidu.sendRequest( + 'onIceCandidate', + { + endpointName: this.connectionId, + candidate: candidate.candidate, + sdpMid: candidate.sdpMid, + sdpMLineIndex: candidate.sdpMLineIndex + }, + (error, response) => { + if (error) { + logger.error('Error sending ICE candidate: ' + JSON.stringify(error)); + this.session.emitEvent('exception', [ + new ExceptionEvent( + this.session, + ExceptionEventName.ICE_CANDIDATE_ERROR, + this.session, + 'There was an unexpected error on the server-side processing an ICE candidate generated and sent by the client-side', + error + ) + ]); + } } - }); + ); } /** * @hidden */ initRemoteStreams(options: StreamOptionsServer[]): void { - // This is ready for supporting multiple streams per Connection object. Right now the loop will always run just once // this.stream should also be replaced by a collection of streams to support multiple streams per Connection - options.forEach(opts => { + options.forEach((opts) => { const streamOptions: InboundStreamOptions = { id: opts.id, createdAt: opts.createdAt, @@ -175,7 +181,10 @@ export class Connection { this.addStream(stream); }); - logger.info("Remote 'Connection' with 'connectionId' [" + this.connectionId + '] is now configured for receiving Streams with options: ', this.stream!.inboundStreamOpts); + logger.info( + "Remote 'Connection' with 'connectionId' [" + this.connectionId + '] is now configured for receiving Streams with options: ', + this.stream!.inboundStreamOpts + ); } /** @@ -202,5 +211,4 @@ export class Connection { } this.disposed = true; } - } diff --git a/openvidu-browser/src/OpenVidu/EventDispatcher.ts b/openvidu-browser/src/OpenVidu/EventDispatcher.ts index 49c467e7..1f894a0e 100644 --- a/openvidu-browser/src/OpenVidu/EventDispatcher.ts +++ b/openvidu-browser/src/OpenVidu/EventDispatcher.ts @@ -27,7 +27,6 @@ import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger'; const logger: OpenViduLogger = OpenViduLogger.getInstance(); export abstract class EventDispatcher { - /** * @hidden */ @@ -42,27 +41,27 @@ export abstract class EventDispatcher { * * @returns The EventDispatcher object */ - abstract on(type: K, handler: (event: (EventMap)[K]) => void): this; + abstract on(type: K, handler: (event: EventMap[K]) => void): this; /** * Adds function `handler` to handle event `type` just once. The handler will be automatically removed after first execution * * @returns The object that dispatched the event */ - abstract once(type: K, handler: (event: (EventMap)[K]) => void): this; + abstract once(type: K, handler: (event: EventMap[K]) => void): this; /** * Removes a `handler` from event `type`. If no handler is provided, all handlers will be removed from the event * * @returns The object that dispatched the event */ - abstract off(type: K, handler?: (event: (EventMap)[K]) => void): this; + abstract off(type: K, handler?: (event: EventMap[K]) => void): this; /** * @hidden */ onAux(type: string, message: string, handler: (event: Event) => void): EventDispatcher { - const arrowHandler = event => { + const arrowHandler = (event) => { if (event) { logger.info(message, event); } else { @@ -79,7 +78,7 @@ export abstract class EventDispatcher { * @hidden */ onceAux(type: string, message: string, handler: (event: Event) => void): EventDispatcher { - const arrowHandler = event => { + const arrowHandler = (event) => { if (event) { logger.info(message, event); } else { @@ -110,5 +109,4 @@ export abstract class EventDispatcher { } return this; } - -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenVidu/Filter.ts b/openvidu-browser/src/OpenVidu/Filter.ts index 05e25430..d85557f0 100644 --- a/openvidu-browser/src/OpenVidu/Filter.ts +++ b/openvidu-browser/src/OpenVidu/Filter.ts @@ -32,7 +32,6 @@ const logger: OpenViduLogger = OpenViduLogger.getInstance(); * Video/audio filter applied to a Stream. See [[Stream.applyFilter]] */ export class Filter { - /** * Type of filter applied. This is the name of the remote class identifying the filter to apply in Kurento Media Server. * For example: `"FaceOverlayFilter"`, `"GStreamerFilter"`. @@ -59,7 +58,8 @@ export class Filter { * You can use this value to know the current status of any applied filter */ lastExecMethod?: { - method: string, params: Object + method: string; + params: Object; }; /** @@ -73,7 +73,6 @@ export class Filter { stream: Stream; private logger: OpenViduLogger; - /** * @hidden */ @@ -82,7 +81,6 @@ export class Filter { this.options = options; } - /** * Executes a filter method. Available methods are specific for each filter * @@ -91,24 +89,40 @@ export class Filter { */ execMethod(method: string, params: Object): Promise { return new Promise((resolve, reject) => { - logger.info('Executing filter method to stream ' + this.stream.streamId); let finalParams; - const successExecMethod = triggerEvent => { + const successExecMethod = (triggerEvent) => { logger.info('Filter method successfully executed on Stream ' + this.stream.streamId); const oldValue = (Object).assign({}, this.stream.filter); this.stream.filter!.lastExecMethod = { method, params: finalParams }; if (triggerEvent) { - this.stream.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.stream.session, this.stream, 'filter', this.stream.filter!, oldValue, 'execFilterMethod')]); - this.stream.streamManager.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.stream.streamManager, this.stream, 'filter', this.stream.filter!, oldValue, 'execFilterMethod')]); + this.stream.session.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent( + this.stream.session, + this.stream, + 'filter', + this.stream.filter!, + oldValue, + 'execFilterMethod' + ) + ]); + this.stream.streamManager.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent( + this.stream.streamManager, + this.stream, + 'filter', + this.stream.filter!, + oldValue, + 'execFilterMethod' + ) + ]); } return resolve(); - } + }; if (this.type.startsWith('VB:')) { - if (typeof params === 'string') { try { params = JSON.parse(params); @@ -121,23 +135,31 @@ export class Filter { if (method === 'update') { if (!this.stream.virtualBackgroundSinkElements?.VB) { - return reject(new OpenViduError(OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR, 'There is no Virtual Background filter applied')); + return reject( + new OpenViduError(OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR, 'There is no Virtual Background filter applied') + ); } else { this.stream.virtualBackgroundSinkElements.VB.updateValues(params) .then(() => successExecMethod(false)) - .catch(error => { + .catch((error) => { if (error.name === OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR) { return reject(new OpenViduError(error.name, error.message)); } else { - return reject(new OpenViduError(OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR, 'Error updating values on Virtual Background filter: ' + error)); + return reject( + new OpenViduError( + OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR, + 'Error updating values on Virtual Background filter: ' + error + ) + ); } }); } } else { - return reject(new OpenViduError(OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR, `Unknown Virtual Background method "${method}"`)); + return reject( + new OpenViduError(OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR, `Unknown Virtual Background method "${method}"`) + ); } } else { - let stringParams; if (typeof params !== 'string') { try { @@ -160,7 +182,12 @@ export class Filter { if (error) { logger.error('Error executing filter method for Stream ' + this.stream.streamId, error); if (error.code === 401) { - return reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to execute a filter method")); + return reject( + new OpenViduError( + OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, + "You don't have permissions to execute a filter method" + ) + ); } else { return reject(error); } @@ -173,7 +200,6 @@ export class Filter { }); } - /** * Subscribe to certain filter event. Available events are specific for each filter * @@ -190,15 +216,25 @@ export class Filter { { streamId: this.stream.streamId, eventType }, (error, response) => { if (error) { - logger.error('Error adding filter event listener to event ' + eventType + 'for Stream ' + this.stream.streamId, error); + logger.error( + 'Error adding filter event listener to event ' + eventType + 'for Stream ' + this.stream.streamId, + error + ); if (error.code === 401) { - return reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to add a filter event listener")); + return reject( + new OpenViduError( + OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, + "You don't have permissions to add a filter event listener" + ) + ); } else { return reject(error); } } else { this.handlers.set(eventType, handler); - logger.info('Filter event listener to event ' + eventType + ' successfully applied on Stream ' + this.stream.streamId); + logger.info( + 'Filter event listener to event ' + eventType + ' successfully applied on Stream ' + this.stream.streamId + ); return resolve(); } } @@ -206,7 +242,6 @@ export class Filter { }); } - /** * Removes certain filter event listener previously set. * @@ -222,20 +257,29 @@ export class Filter { { streamId: this.stream.streamId, eventType }, (error, response) => { if (error) { - logger.error('Error removing filter event listener to event ' + eventType + 'for Stream ' + this.stream.streamId, error); + logger.error( + 'Error removing filter event listener to event ' + eventType + 'for Stream ' + this.stream.streamId, + error + ); if (error.code === 401) { - return reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to add a filter event listener")); + return reject( + new OpenViduError( + OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, + "You don't have permissions to add a filter event listener" + ) + ); } else { return reject(error); } } else { this.handlers.delete(eventType); - logger.info('Filter event listener to event ' + eventType + ' successfully removed on Stream ' + this.stream.streamId); + logger.info( + 'Filter event listener to event ' + eventType + ' successfully removed on Stream ' + this.stream.streamId + ); return resolve(); } } ); }); } - -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenVidu/LocalRecorder.ts b/openvidu-browser/src/OpenVidu/LocalRecorder.ts index 655307ff..d6332ea1 100644 --- a/openvidu-browser/src/OpenVidu/LocalRecorder.ts +++ b/openvidu-browser/src/OpenVidu/LocalRecorder.ts @@ -31,12 +31,10 @@ const logger: OpenViduLogger = OpenViduLogger.getInstance(); */ let platform: PlatformUtils; - /** * Easy recording of [[Stream]] objects straightaway from the browser. Initialized with [[OpenVidu.initLocalRecorder]] method */ export class LocalRecorder { - state: LocalRecorderState; private connectionId: string; @@ -52,18 +50,17 @@ export class LocalRecorder { */ constructor(private stream: Stream) { platform = PlatformUtils.getInstance(); - this.connectionId = (!!this.stream.connection) ? this.stream.connection.connectionId : 'default-connection'; + this.connectionId = !!this.stream.connection ? this.stream.connection.connectionId : 'default-connection'; this.id = this.stream.streamId + '_' + this.connectionId + '_localrecord'; this.state = LocalRecorderState.READY; } - /** * Starts the recording of the Stream. [[state]] property must be `READY`. After method succeeds is set to `RECORDING` * * @param options The [MediaRecorder.options](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/MediaRecorder#parameters) to be used to record this Stream. - * For example: - * + * For example: + * * ```javascript * var OV = new OpenVidu(); * var publisher = await OV.initPublisherAsync(); @@ -75,7 +72,7 @@ export class LocalRecorder { * }; * localRecorder.record(options); * ``` - * + * * If not specified, the default options preferred by the platform will be used. * * @returns A Promise (to which you can optionally subscribe to) that is resolved if the recording successfully started and rejected with an Error object if not @@ -84,14 +81,24 @@ export class LocalRecorder { return new Promise((resolve, reject) => { try { if (typeof options === 'string' || options instanceof String) { - return reject(`When calling LocalRecorder.record(options) parameter 'options' cannot be a string. Must be an object like { mimeType: "${options}" }`); + return reject( + `When calling LocalRecorder.record(options) parameter 'options' cannot be a string. Must be an object like { mimeType: "${options}" }` + ); } if (typeof MediaRecorder === 'undefined') { - logger.error('MediaRecorder not supported on your device. See compatibility in https://caniuse.com/#search=MediaRecorder'); - throw (Error('MediaRecorder not supported on your device. See compatibility in https://caniuse.com/#search=MediaRecorder')); + logger.error( + 'MediaRecorder not supported on your device. See compatibility in https://caniuse.com/#search=MediaRecorder' + ); + throw Error( + 'MediaRecorder not supported on your device. See compatibility in https://caniuse.com/#search=MediaRecorder' + ); } if (this.state !== LocalRecorderState.READY) { - throw (Error('\'LocalRecord.record()\' needs \'LocalRecord.state\' to be \'READY\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.clean()\' or init a new LocalRecorder before')); + throw Error( + "'LocalRecord.record()' needs 'LocalRecord.state' to be 'READY' (current value: '" + + this.state + + "'). Call 'LocalRecorder.clean()' or init a new LocalRecorder before" + ); } logger.log("Starting local recording of stream '" + this.stream.streamId + "' of connection '" + this.connectionId + "'"); @@ -103,7 +110,6 @@ export class LocalRecorder { this.mediaRecorder = new MediaRecorder(this.stream.getMediaStream(), options); this.mediaRecorder.start(); - } catch (err) { return reject(err); } @@ -136,11 +142,9 @@ export class LocalRecorder { this.state = LocalRecorderState.RECORDING; return resolve(); - }); } - /** * Ends the recording of the Stream. [[state]] property must be `RECORDING` or `PAUSED`. After method succeeds is set to `FINISHED` * @returns A Promise (to which you can optionally subscribe to) that is resolved if the recording successfully stopped and rejected with an Error object if not @@ -149,7 +153,11 @@ export class LocalRecorder { return new Promise((resolve, reject) => { try { if (this.state === LocalRecorderState.READY || this.state === LocalRecorderState.FINISHED) { - throw (Error('\'LocalRecord.stop()\' needs \'LocalRecord.state\' to be \'RECORDING\' or \'PAUSED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.start()\' before')); + throw Error( + "'LocalRecord.stop()' needs 'LocalRecord.state' to be 'RECORDING' or 'PAUSED' (current value: '" + + this.state + + "'). Call 'LocalRecorder.start()' before" + ); } this.mediaRecorder.onstop = () => { this.onStopDefault(); @@ -162,7 +170,6 @@ export class LocalRecorder { }); } - /** * Pauses the recording of the Stream. [[state]] property must be `RECORDING`. After method succeeds is set to `PAUSED` * @returns A Promise (to which you can optionally subscribe to) that is resolved if the recording was successfully paused and rejected with an Error object if not @@ -171,7 +178,13 @@ export class LocalRecorder { return new Promise((resolve, reject) => { try { if (this.state !== LocalRecorderState.RECORDING) { - return reject(Error('\'LocalRecord.pause()\' needs \'LocalRecord.state\' to be \'RECORDING\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.start()\' or \'LocalRecorder.resume()\' before')); + return reject( + Error( + "'LocalRecord.pause()' needs 'LocalRecord.state' to be 'RECORDING' (current value: '" + + this.state + + "'). Call 'LocalRecorder.start()' or 'LocalRecorder.resume()' before" + ) + ); } this.mediaRecorder.pause(); this.state = LocalRecorderState.PAUSED; @@ -190,7 +203,11 @@ export class LocalRecorder { return new Promise((resolve, reject) => { try { if (this.state !== LocalRecorderState.PAUSED) { - throw (Error('\'LocalRecord.resume()\' needs \'LocalRecord.state\' to be \'PAUSED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.pause()\' before')); + throw Error( + "'LocalRecord.resume()' needs 'LocalRecord.state' to be 'PAUSED' (current value: '" + + this.state + + "'). Call 'LocalRecorder.pause()' before" + ); } this.mediaRecorder.resume(); this.state = LocalRecorderState.RECORDING; @@ -201,14 +218,16 @@ export class LocalRecorder { }); } - /** * Previews the recording, appending a new HTMLVideoElement to element with id `parentId`. [[state]] property must be `FINISHED` */ preview(parentElement): HTMLVideoElement { - if (this.state !== LocalRecorderState.FINISHED) { - throw (Error('\'LocalRecord.preview()\' needs \'LocalRecord.state\' to be \'FINISHED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.stop()\' before')); + throw Error( + "'LocalRecord.preview()' needs 'LocalRecord.state' to be 'FINISHED' (current value: '" + + this.state + + "'). Call 'LocalRecorder.stop()' before" + ); } this.videoPreview = document.createElement('video'); @@ -234,7 +253,6 @@ export class LocalRecorder { return this.videoPreview; } - /** * Gracefully stops and cleans the current recording (WARNING: it is completely dismissed). Sets [[state]] to `READY` so the recording can start again */ @@ -245,19 +263,24 @@ export class LocalRecorder { this.state = LocalRecorderState.READY; }; if (this.state === LocalRecorderState.RECORDING || this.state === LocalRecorderState.PAUSED) { - this.stop().then(() => f()).catch(() => f()); + this.stop() + .then(() => f()) + .catch(() => f()); } else { f(); } } - /** * Downloads the recorded video through the browser. [[state]] property must be `FINISHED` */ download(): void { if (this.state !== LocalRecorderState.FINISHED) { - throw (Error('\'LocalRecord.download()\' needs \'LocalRecord.state\' to be \'FINISHED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.stop()\' before')); + throw Error( + "'LocalRecord.download()' needs 'LocalRecord.state' to be 'FINISHED' (current value: '" + + this.state + + "'). Call 'LocalRecorder.stop()' before" + ); } else { const a: HTMLAnchorElement = document.createElement('a'); a.style.display = 'none'; @@ -278,13 +301,12 @@ export class LocalRecorder { */ getBlob(): Blob { if (this.state !== LocalRecorderState.FINISHED) { - throw (Error('Call \'LocalRecord.stop()\' before getting Blob file')); + throw Error("Call 'LocalRecord.stop()' before getting Blob file"); } else { return this.blob!; } } - /** * Uploads the recorded video as a binary file performing an HTTP/POST operation to URL `endpoint`. [[state]] property must be `FINISHED`. Optional HTTP headers can be passed as second parameter. For example: * ``` @@ -298,7 +320,13 @@ export class LocalRecorder { uploadAsBinary(endpoint: string, headers?: any): Promise { return new Promise((resolve, reject) => { if (this.state !== LocalRecorderState.FINISHED) { - return reject(Error('\'LocalRecord.uploadAsBinary()\' needs \'LocalRecord.state\' to be \'FINISHED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.stop()\' before')); + return reject( + Error( + "'LocalRecord.uploadAsBinary()' needs 'LocalRecord.state' to be 'FINISHED' (current value: '" + + this.state + + "'). Call 'LocalRecorder.stop()' before" + ) + ); } else { const http = new XMLHttpRequest(); http.open('POST', endpoint, true); @@ -324,7 +352,6 @@ export class LocalRecorder { }); } - /** * Uploads the recorded video as a multipart file performing an HTTP/POST operation to URL `endpoint`. [[state]] property must be `FINISHED`. Optional HTTP headers can be passed as second parameter. For example: * ``` @@ -338,7 +365,13 @@ export class LocalRecorder { uploadAsMultipartfile(endpoint: string, headers?: any): Promise { return new Promise((resolve, reject) => { if (this.state !== LocalRecorderState.FINISHED) { - return reject(Error('\'LocalRecord.uploadAsMultipartfile()\' needs \'LocalRecord.state\' to be \'FINISHED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.stop()\' before')); + return reject( + Error( + "'LocalRecord.uploadAsMultipartfile()' needs 'LocalRecord.state' to be 'FINISHED' (current value: '" + + this.state + + "'). Call 'LocalRecorder.stop()' before" + ) + ); } else { const http = new XMLHttpRequest(); http.open('POST', endpoint, true); @@ -368,7 +401,6 @@ export class LocalRecorder { }); } - /* Private methods */ private onStopDefault(): void { @@ -381,5 +413,4 @@ export class LocalRecorder { this.state = LocalRecorderState.FINISHED; } - } diff --git a/openvidu-browser/src/OpenVidu/OpenVidu.ts b/openvidu-browser/src/OpenVidu/OpenVidu.ts index bf28157d..27c4d6b2 100644 --- a/openvidu-browser/src/OpenVidu/OpenVidu.ts +++ b/openvidu-browser/src/OpenVidu/OpenVidu.ts @@ -32,7 +32,7 @@ import { PlatformUtils } from '../OpenViduInternal/Utils/Platform'; import * as screenSharingAuto from '../OpenViduInternal/ScreenSharing/Screen-Capturing-Auto'; import * as screenSharing from '../OpenViduInternal/ScreenSharing/Screen-Capturing'; -import { OpenViduLoggerConfiguration } from "../OpenViduInternal/Logger/OpenViduLoggerConfiguration"; +import { OpenViduLoggerConfiguration } from '../OpenViduInternal/Logger/OpenViduLoggerConfiguration'; /** * @hidden */ @@ -65,1132 +65,1216 @@ let platform: PlatformUtils; * Use it to initialize objects of type [[Session]], [[Publisher]] and [[LocalRecorder]] */ export class OpenVidu { + private jsonRpcClient: any; + private masterNodeHasCrashed = false; - private jsonRpcClient: any; - private masterNodeHasCrashed = false; + /** + * @hidden + */ + session: Session; + /** + * @hidden + */ + publishers: Publisher[] = []; + /** + * @hidden + */ + wsUri: string; + /** + * @hidden + */ + httpUri: string; + /** + * @hidden + */ + secret = ''; + /** + * @hidden + */ + recorder = false; + /** + * @hidden + */ + iceServers: RTCIceServer[]; + /** + * @hidden + */ + role: string; + /** + * @hidden + */ + finalUserId: string; + /** + * @hidden + */ + mediaServer: string; + /** + * @hidden + */ + videoSimulcast: boolean; + /** + * @hidden + */ + life: number = -1; + /** + * @hidden + */ + advancedConfiguration: OpenViduAdvancedConfiguration = {}; + /** + * @hidden + */ + webrtcStatsInterval: number = -1; + /** + * @hidden + */ + sendBrowserLogs: OpenViduLoggerConfiguration = OpenViduLoggerConfiguration.disabled; + /** + * @hidden + */ + isAtLeastPro: boolean = false; + /** + * @hidden + */ + isEnterprise: boolean = false; + /** + * @hidden + */ + libraryVersion: string; + /** + * @hidden + */ + ee = new EventEmitter(); - /** - * @hidden - */ - session: Session; - /** - * @hidden - */ - publishers: Publisher[] = []; - /** - * @hidden - */ - wsUri: string; - /** - * @hidden - */ - httpUri: string; - /** - * @hidden - */ - secret = ''; - /** - * @hidden - */ - recorder = false; - /** - * @hidden - */ - iceServers: RTCIceServer[]; - /** - * @hidden - */ - role: string; - /** - * @hidden - */ - finalUserId: string; - /** - * @hidden - */ - mediaServer: string; - /** - * @hidden - */ - videoSimulcast: boolean; - /** - * @hidden - */ - life: number = -1; - /** - * @hidden - */ - advancedConfiguration: OpenViduAdvancedConfiguration = {}; - /** - * @hidden - */ - webrtcStatsInterval: number = -1; - /** - * @hidden - */ - sendBrowserLogs: OpenViduLoggerConfiguration = OpenViduLoggerConfiguration.disabled; - /** - * @hidden - */ - isAtLeastPro: boolean = false; - /** - * @hidden - */ - isEnterprise: boolean = false; - /** - * @hidden - */ - libraryVersion: string; - /** - * @hidden - */ - ee = new EventEmitter() + constructor() { + platform = PlatformUtils.getInstance(); + this.libraryVersion = packageJson.version; + logger.info('OpenVidu initialized'); + logger.info('Platform detected: ' + platform.getDescription()); + logger.info('openvidu-browser version: ' + this.libraryVersion); - constructor() { - platform = PlatformUtils.getInstance(); - this.libraryVersion = packageJson.version; - logger.info("OpenVidu initialized"); - logger.info('Platform detected: ' + platform.getDescription()); - logger.info('openvidu-browser version: ' + this.libraryVersion); - - if (platform.isMobileDevice() || platform.isReactNative()) { - // Listen to orientationchange only on mobile devices - this.onOrientationChanged(() => { - this.publishers.forEach(publisher => { - if (publisher.stream.isLocalStreamPublished && !!publisher.stream && !!publisher.stream.hasVideo) { - this.sendNewVideoDimensionsIfRequired(publisher, 'deviceRotated', 75, 10); - } - }); - }); - } - } - - /** - * Returns new session - */ - initSession(): Session { - this.session = new Session(this); - return this.session; - } - - - initPublisher(targetElement: string | HTMLElement | undefined): Publisher; - initPublisher(targetElement: string | HTMLElement | undefined, properties: PublisherProperties): Publisher; - initPublisher(targetElement: string | HTMLElement | undefined, completionHandler: (error: Error | undefined) => void): Publisher; - initPublisher(targetElement: string | HTMLElement | undefined, properties: PublisherProperties, completionHandler: (error: Error | undefined) => void): Publisher; - - /** - * Returns a new publisher - * - * #### Events dispatched - * - * The [[Publisher]] object will dispatch an `accessDialogOpened` event, only if the pop-up shown by the browser to request permissions for the camera is opened. You can use this event to alert the user about granting permissions - * for your website. An `accessDialogClosed` event will also be dispatched after user clicks on "Allow" or "Block" in the pop-up. - * - * The [[Publisher]] object will dispatch an `accessAllowed` or `accessDenied` event once it has been granted access to the requested input devices or not. - * - * The [[Publisher]] object will dispatch a `videoElementCreated` event once a HTML video element has been added to DOM (only if you - * [let OpenVidu take care of the video players](/en/stable/cheatsheet/manage-videos/#let-openvidu-take-care-of-the-video-players)). See [[VideoElementEvent]] to learn more. - * - * The [[Publisher]] object will dispatch a `streamPlaying` event once the local streams starts playing. See [[StreamManagerEvent]] to learn more. - * - * @param targetElement HTML DOM element (or its `id` attribute) in which the video element of the Publisher will be inserted (see [[PublisherProperties.insertMode]]). If *null* or *undefined* no default video will be created for this Publisher. - * You can always call method [[Publisher.addVideoElement]] or [[Publisher.createVideoElement]] to manage the video elements on your own (see [Manage video players](/en/stable/cheatsheet/manage-videos) section) - * @param completionHandler `error` parameter is null if `initPublisher` succeeds, and is defined if it fails. - * `completionHandler` function is called before the Publisher dispatches an `accessAllowed` or an `accessDenied` event - */ - initPublisher(targetElement: string | HTMLElement | undefined, param2?, param3?): Publisher { - - let properties: PublisherProperties; - - if (!!param2 && (typeof param2 !== 'function')) { - - // Matches 'initPublisher(targetElement, properties)' or 'initPublisher(targetElement, properties, completionHandler)' - - properties = (param2); - - properties = { - audioSource: (typeof properties.audioSource !== 'undefined') ? properties.audioSource : undefined, - frameRate: (typeof MediaStreamTrack !== 'undefined' && properties.videoSource instanceof MediaStreamTrack) ? undefined : ((typeof properties.frameRate !== 'undefined') ? properties.frameRate : undefined), - insertMode: (typeof properties.insertMode !== 'undefined') ? ((typeof properties.insertMode === 'string') ? VideoInsertMode[properties.insertMode] : properties.insertMode) : VideoInsertMode.APPEND, - mirror: (typeof properties.mirror !== 'undefined') ? properties.mirror : true, - publishAudio: (typeof properties.publishAudio !== 'undefined') ? properties.publishAudio : true, - publishVideo: (typeof properties.publishVideo !== 'undefined') ? properties.publishVideo : true, - resolution: (typeof MediaStreamTrack !== 'undefined' && properties.videoSource instanceof MediaStreamTrack) ? undefined : ((typeof properties.resolution !== 'undefined') ? properties.resolution : '640x480'), - videoSource: (typeof properties.videoSource !== 'undefined') ? properties.videoSource : undefined, - videoSimulcast: properties.videoSimulcast, - filter: properties.filter - }; - } else { - - // Matches 'initPublisher(targetElement)' or 'initPublisher(targetElement, completionHandler)' - - properties = { - insertMode: VideoInsertMode.APPEND, - mirror: true, - publishAudio: true, - publishVideo: true, - resolution: '640x480' - }; - } - - const publisher: Publisher = new Publisher(targetElement, properties, this); - - let completionHandler: (error: Error | undefined) => void; - if (!!param2 && (typeof param2 === 'function')) { - completionHandler = param2; - } else if (!!param3) { - completionHandler = param3; - } - - publisher.initialize() - .then(() => { - if (completionHandler !== undefined) { - completionHandler(undefined); - } - publisher.emitEvent('accessAllowed', []); - }).catch((error) => { - if (completionHandler !== undefined) { - completionHandler(error); - } - publisher.emitEvent('accessDenied', [error]); - }); - - this.publishers.push(publisher); - return publisher; - } - - - /** - * Promisified version of [[OpenVidu.initPublisher]] - * - * > WARNING: events `accessDialogOpened` and `accessDialogClosed` will not be dispatched if using this method instead of [[OpenVidu.initPublisher]] - */ - initPublisherAsync(targetElement: string | HTMLElement | undefined): Promise; - initPublisherAsync(targetElement: string | HTMLElement | undefined, properties: PublisherProperties): Promise; - - initPublisherAsync(targetElement: string | HTMLElement | undefined, properties?: PublisherProperties): Promise { - return new Promise((resolve, reject) => { - - let publisher: Publisher; - - const callback = (error: Error) => { - if (!!error) { - return reject(error); - } else { - return resolve(publisher); - } - }; - - if (!!properties) { - publisher = this.initPublisher(targetElement, properties, callback); - } else { - publisher = this.initPublisher(targetElement, callback); - } - }); - } - - - /** - * Returns a new local recorder for recording streams straight away from the browser - * @param stream Stream to record - */ - initLocalRecorder(stream: Stream): LocalRecorder { - return new LocalRecorder(stream); - } - - - /** - * Checks if the browser supports OpenVidu - * @returns 1 if the browser supports OpenVidu, 0 otherwise - */ - checkSystemRequirements(): boolean { - // Specific iOS platform support (iPhone, iPad) - if (platform.isIPhoneOrIPad()) { - return ( - platform.isIOSWithSafari() || - platform.isChromeMobileBrowser() || - platform.isFirefoxMobileBrowser() || - platform.isOperaMobileBrowser() || - platform.isEdgeMobileBrowser() || - platform.isIonicIos() // Ionic apps for iOS - ); - } - - // General platform support for web clients (Desktop, Mobile) - return ( - platform.isChromeBrowser() || - platform.isChromeMobileBrowser() || - platform.isFirefoxBrowser() || - platform.isFirefoxMobileBrowser() || - platform.isOperaBrowser() || - platform.isOperaMobileBrowser() || - platform.isEdgeBrowser() || - platform.isEdgeMobileBrowser() || - platform.isSamsungBrowser() || - platform.isSafariBrowser() || - platform.isAndroidBrowser() || // Android WebView & Ionic apps for Android - platform.isElectron() || - platform.isNodeJs() - ); - } - - /** - * Checks if the browser supports screen-sharing. Desktop Chrome, Firefox and Opera support screen-sharing - * @returns 1 if the browser supports screen-sharing, 0 otherwise - */ - checkScreenSharingCapabilities(): boolean { - return platform.canScreenShare(); - } - - - /** - * Collects information about the media input devices available on the system. You can pass property `deviceId` of a [[Device]] object as value of `audioSource` or `videoSource` properties in [[initPublisher]] method - */ - getDevices(): Promise { - return new Promise((resolve, reject) => { - navigator.mediaDevices.enumerateDevices().then((deviceInfos) => { - const devices: Device[] = []; - - // Ionic Android devices - if (platform.isIonicAndroid() && typeof cordova != "undefined" && cordova?.plugins?.EnumerateDevicesPlugin) { - cordova.plugins.EnumerateDevicesPlugin.getEnumerateDevices().then((pluginDevices: Device[]) => { - let pluginAudioDevices: Device[] = []; - let videoDevices: Device[] = []; - let audioDevices: Device[] = []; - pluginAudioDevices = pluginDevices.filter((device: Device) => device.kind === 'audioinput'); - videoDevices = deviceInfos.filter((device: Device) => device.kind === 'videoinput'); - audioDevices = deviceInfos.filter((device: Device) => device.kind === 'audioinput'); - videoDevices.forEach((deviceInfo, index) => { - if (!deviceInfo.label) { - let label = ""; - if (index === 0) { - label = "Front Camera"; - } else if (index === 1) { - label = "Back Camera"; - } else { - label = "Unknown Camera"; - } - devices.push({ - kind: deviceInfo.kind, - deviceId: deviceInfo.deviceId, - label: label - }); - - } else { - devices.push({ - kind: deviceInfo.kind, - deviceId: deviceInfo.deviceId, - label: deviceInfo.label - }); - } - }); - audioDevices.forEach((deviceInfo, index) => { - if (!deviceInfo.label) { - let label = ""; - switch (index) { - case 0: // Default Microphone - label = 'Default'; - break; - case 1: // Microphone + Speakerphone - const defaultMatch = pluginAudioDevices.filter((d) => d.label.includes('Built'))[0]; - label = defaultMatch ? defaultMatch.label : 'Built-in Microphone'; - break; - case 2: // Headset Microphone - const wiredMatch = pluginAudioDevices.filter((d) => d.label.includes('Wired'))[0]; - if (wiredMatch) { - label = wiredMatch.label; - } else { - label = 'Headset earpiece'; + if (platform.isMobileDevice() || platform.isReactNative()) { + // Listen to orientationchange only on mobile devices + this.onOrientationChanged(() => { + this.publishers.forEach((publisher) => { + if (publisher.stream.isLocalStreamPublished && !!publisher.stream && !!publisher.stream.hasVideo) { + this.sendNewVideoDimensionsIfRequired(publisher, 'deviceRotated', 75, 10); } - break; - case 3: - const wirelessMatch = pluginAudioDevices.filter((d) => d.label.includes('Bluetooth'))[0]; - label = wirelessMatch ? wirelessMatch.label : 'Wireless'; - break; - default: - label = "Unknown Microphone"; - break; - } - devices.push({ - kind: deviceInfo.kind, - deviceId: deviceInfo.deviceId, - label: label }); - - } else { - devices.push({ - kind: deviceInfo.kind, - deviceId: deviceInfo.deviceId, - label: deviceInfo.label - }); - } }); - return resolve(devices); - }); - } else { - - // Rest of platforms - deviceInfos.forEach(deviceInfo => { - if (deviceInfo.kind === 'audioinput' || deviceInfo.kind === 'videoinput') { - devices.push({ - kind: deviceInfo.kind, - deviceId: deviceInfo.deviceId, - label: deviceInfo.label - }); - } - }); - return resolve(devices); } - }).catch((error) => { - logger.error('Error getting devices', error); - return reject(error); - }); - }); - } - - - - /** - * Get a MediaStream object that you can customize before calling [[initPublisher]] (pass _MediaStreamTrack_ property of the _MediaStream_ value resolved by the Promise as `audioSource` or `videoSource` properties in [[initPublisher]]) - * - * Parameter `options` is the same as in [[initPublisher]] second parameter (of type [[PublisherProperties]]), but only the following properties will be applied: `audioSource`, `videoSource`, `frameRate`, `resolution` - * - * To customize the Publisher's video, the API for HTMLCanvasElement is very useful. For example, to get a black-and-white video at 10 fps and HD resolution with no sound: - * ``` - * var OV = new OpenVidu(); - * var FRAME_RATE = 10; - * - * OV.getUserMedia({ - * audioSource: false, - * videoSource: undefined, - * resolution: '1280x720', - * frameRate: FRAME_RATE - * }) - * .then(mediaStream => { - * - * var videoTrack = mediaStream.getVideoTracks()[0]; - * var video = document.createElement('video'); - * video.srcObject = new MediaStream([videoTrack]); - * - * var canvas = document.createElement('canvas'); - * var ctx = canvas.getContext('2d'); - * ctx.filter = 'grayscale(100%)'; - * - * video.addEventListener('play', () => { - * var loop = () => { - * if (!video.paused && !video.ended) { - * ctx.drawImage(video, 0, 0, 300, 170); - * setTimeout(loop, 1000/ FRAME_RATE); // Drawing at 10 fps - * } - * }; - * loop(); - * }); - * video.play(); - * - * var grayVideoTrack = canvas.captureStream(FRAME_RATE).getVideoTracks()[0]; - * var publisher = this.OV.initPublisher( - * myHtmlTarget, - * { - * audioSource: false, - * videoSource: grayVideoTrack - * }); - * }); - * ``` - */ - getUserMedia(options: PublisherProperties): Promise { - return new Promise(async (resolve, reject) => { - - const askForAudioStreamOnly = async (previousMediaStream: MediaStream, constraints: MediaStreamConstraints) => { - const definedAudioConstraint = ((constraints.audio === undefined) ? true : constraints.audio); - const constraintsAux: MediaStreamConstraints = { audio: definedAudioConstraint, video: false }; - try { - const audioOnlyStream = await navigator.mediaDevices.getUserMedia(constraintsAux); - previousMediaStream.addTrack(audioOnlyStream.getAudioTracks()[0]); - return resolve(previousMediaStream); - } catch (error) { - previousMediaStream.getAudioTracks().forEach((track) => { - track.stop(); - }); - previousMediaStream.getVideoTracks().forEach((track) => { - track.stop(); - }); - return reject(this.generateAudioDeviceError(error, constraintsAux)); - } - } - - try { - const myConstraints = await this.generateMediaConstraints(options); - if (!!myConstraints.videoTrack && !!myConstraints.audioTrack || - !!myConstraints.audioTrack && myConstraints.constraints?.video === false || - !!myConstraints.videoTrack && myConstraints.constraints?.audio === false) { - - // No need to call getUserMedia at all. Both tracks provided, or only AUDIO track provided or only VIDEO track provided - return resolve(this.addAlreadyProvidedTracks(myConstraints, new MediaStream())); - - } else { - // getUserMedia must be called. AUDIO or VIDEO are requesting a new track - - // Delete already provided constraints for audio or video - if (!!myConstraints.videoTrack) { - delete myConstraints.constraints!.video; - } - if (!!myConstraints.audioTrack) { - delete myConstraints.constraints!.audio; - } - - let mustAskForAudioTrackLater = false; - if (typeof options.videoSource === 'string') { - // Video is deviceId or screen sharing - if (options.videoSource === 'screen' || - options.videoSource === 'window' || - (platform.isElectron() && options.videoSource.startsWith('screen:'))) { - // Video is screen sharing - mustAskForAudioTrackLater = !myConstraints.audioTrack && (options.audioSource !== null && options.audioSource !== false); - if (navigator.mediaDevices['getDisplayMedia'] && !platform.isElectron()) { - // getDisplayMedia supported - try { - const mediaStream = await navigator.mediaDevices['getDisplayMedia']({ video: true }); - this.addAlreadyProvidedTracks(myConstraints, mediaStream); - if (mustAskForAudioTrackLater) { - await askForAudioStreamOnly(mediaStream, myConstraints.constraints); - } else { - return resolve(mediaStream); - } - - } catch (error) { - let errorName: OpenViduErrorName = OpenViduErrorName.SCREEN_CAPTURE_DENIED; - const errorMessage = error.toString(); - return reject(new OpenViduError(errorName, errorMessage)); - } - - } else { - // getDisplayMedia NOT supported. Can perform getUserMedia below with already calculated constraints - } - } else { - // Video is deviceId. Can perform getUserMedia below with already calculated constraints - } - } - // Use already calculated constraints - const constraintsAux = mustAskForAudioTrackLater ? { video: myConstraints.constraints!.video } : myConstraints.constraints; - try { - const mediaStream = await navigator.mediaDevices.getUserMedia(constraintsAux); - this.addAlreadyProvidedTracks(myConstraints, mediaStream); - if (mustAskForAudioTrackLater) { - await askForAudioStreamOnly(mediaStream, myConstraints.constraints); - } else { - return resolve(mediaStream); - } - } catch (error) { - let errorName: OpenViduErrorName; - const errorMessage = error.toString(); - if (!(options.videoSource === 'screen')) { - errorName = OpenViduErrorName.DEVICE_ACCESS_DENIED; - } else { - errorName = OpenViduErrorName.SCREEN_CAPTURE_DENIED; - } - return reject(new OpenViduError(errorName, errorMessage)); - } - } - } catch (error) { - reject(error); - } - }); - } - - - /* tslint:disable:no-empty */ - /** - * Disable all logging except error level - */ - enableProdMode(): void { - logger.enableProdMode(); - } - /* tslint:enable:no-empty */ - - - /** - * Set OpenVidu advanced configuration options. `configuration` is an object of type [[OpenViduAdvancedConfiguration]]. Call this method to override previous values at any moment. - */ - setAdvancedConfiguration(configuration: OpenViduAdvancedConfiguration): void { - this.advancedConfiguration = configuration; - } - - - /* Hidden methods */ - - /** - * @hidden - */ - onOrientationChanged(handler): void { - (globalThis as any).addEventListener('orientationchange', handler); - } - - /** - * @hidden - */ - sendNewVideoDimensionsIfRequired(publisher: Publisher, reason: string, WAIT_INTERVAL: number, MAX_ATTEMPTS: number) { - let attempts = 0; - const oldWidth = publisher?.stream?.videoDimensions?.width || 0; - const oldHeight = publisher?.stream?.videoDimensions?.height || 0; - - const repeatUntilChangeOrMaxAttempts: NodeJS.Timeout = setInterval(() => { - attempts++; - if (attempts > MAX_ATTEMPTS) { - clearTimeout(repeatUntilChangeOrMaxAttempts); - } - publisher.getVideoDimensions().then(newDimensions => { - if (newDimensions.width !== oldWidth || newDimensions.height !== oldHeight) { - clearTimeout(repeatUntilChangeOrMaxAttempts); - this.sendVideoDimensionsChangedEvent(publisher, reason, oldWidth, oldHeight, newDimensions.width, newDimensions.height); - } - }); - }, WAIT_INTERVAL); - } - - /** - * @hidden - */ - sendVideoDimensionsChangedEvent(publisher: Publisher, reason: string, oldWidth: number, oldHeight: number, newWidth: number, newHeight: number) { - publisher.stream.videoDimensions = { - width: newWidth || 0, - height: newHeight || 0 - }; - this.sendRequest( - 'streamPropertyChanged', - { - streamId: publisher.stream.streamId, - property: 'videoDimensions', - newValue: JSON.stringify(publisher.stream.videoDimensions), - reason - }, - (error, response) => { - if (error) { - logger.error("Error sending 'streamPropertyChanged' event", error); - } else { - this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, publisher.stream, 'videoDimensions', publisher.stream.videoDimensions, { width: oldWidth, height: oldHeight }, reason)]); - publisher.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(publisher, publisher.stream, 'videoDimensions', publisher.stream.videoDimensions, { width: oldWidth, height: oldHeight }, reason)]); - this.session.sendVideoData(publisher); - } - }); - }; - - /** - * @hidden - */ - sendTrackChangedEvent(publisher: Publisher, reason: string, oldLabel: string, newLabel: string, propertyType: string) { - const oldValue = {label: oldLabel}; - const newValue = {label: newLabel}; - - if(publisher.stream.isLocalStreamPublished){ - this.sendRequest( - 'streamPropertyChanged', - { - streamId: publisher.stream.streamId, - property: propertyType, - newValue: JSON.stringify({newLabel}), - reason - }, - (error, response) => { - if (error) { - logger.error("Error sending 'streamPropertyChanged' event", error); - } else { - this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, publisher.stream, propertyType, newValue, oldValue, reason)]); - publisher.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(publisher, publisher.stream, propertyType, newValue, oldValue, reason)]); - } - }); - } else { - this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, publisher.stream, propertyType, newValue, oldValue, reason)]); - publisher.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(publisher, publisher.stream, propertyType, newValue, oldValue, reason)]); - } - } - - /** - * @hidden - */ - generateMediaConstraints(publisherProperties: PublisherProperties): Promise { - return new Promise((resolve, reject) => { - - const myConstraints: CustomMediaStreamConstraints = { - audioTrack: undefined, - videoTrack: undefined, - constraints: { - audio: undefined, - video: undefined - } - } - const audioSource = publisherProperties.audioSource; - const videoSource = publisherProperties.videoSource; - - // CASE 1: null/false - if (audioSource === null || audioSource === false) { - // No audio track - myConstraints.constraints!.audio = false; - } - if (videoSource === null || videoSource === false) { - // No video track - myConstraints.constraints!.video = false; - } - if (myConstraints.constraints!.audio === false && myConstraints.constraints!.video === false) { - // ERROR! audioSource and videoSource cannot be both false at the same time - return reject(new OpenViduError(OpenViduErrorName.NO_INPUT_SOURCE_SET, - "Properties 'audioSource' and 'videoSource' cannot be set to false or null at the same time")); - } - - // CASE 2: MediaStreamTracks - if (typeof MediaStreamTrack !== 'undefined' && audioSource instanceof MediaStreamTrack) { - // Already provided audio track - myConstraints.audioTrack = audioSource; - } - if (typeof MediaStreamTrack !== 'undefined' && videoSource instanceof MediaStreamTrack) { - // Already provided video track - myConstraints.videoTrack = videoSource; - } - - // CASE 3: Default tracks - if (audioSource === undefined) { - myConstraints.constraints!.audio = true; - } - if (videoSource === undefined) { - myConstraints.constraints!.video = { - width: { - ideal: 640 - }, - height: { - ideal: 480 - } - }; - } - - // CASE 3.5: give values to resolution and frameRate if video not null/false - if (videoSource !== null && videoSource !== false) { - if (!!publisherProperties.resolution) { - const widthAndHeight = publisherProperties.resolution.toLowerCase().split('x'); - const idealWidth = Number(widthAndHeight[0]); - const idealHeight = Number(widthAndHeight[1]); - myConstraints.constraints!.video = { - width: { - ideal: idealWidth - }, - height: { - ideal: idealHeight - } - } - } - if (!!publisherProperties.frameRate) { - (myConstraints.constraints!.video).frameRate = { ideal: publisherProperties.frameRate }; - } - } - - // CASE 4: deviceId or screen sharing - this.configureDeviceIdOrScreensharing(myConstraints, publisherProperties, resolve, reject); - - return resolve(myConstraints); - }); - } - - /** - * @hidden - */ - startWs(onConnectSucces: (error: Error) => void): void { - const config = { - heartbeat: 5000, - ws: { - uri: this.wsUri + '?sessionId=' + this.session.sessionId, - onconnected: onConnectSucces, - ondisconnect: this.disconnectCallback.bind(this), - onreconnecting: this.reconnectingCallback.bind(this), - onreconnected: this.reconnectedCallback.bind(this), - ismasternodecrashed: this.isMasterNodeCrashed.bind(this) - }, - rpc: { - requestTimeout: 10000, - heartbeatRequestTimeout: 5000, - participantJoined: this.session.onParticipantJoined.bind(this.session), - participantPublished: this.session.onParticipantPublished.bind(this.session), - participantUnpublished: this.session.onParticipantUnpublished.bind(this.session), - participantLeft: this.session.onParticipantLeft.bind(this.session), - participantEvicted: this.session.onParticipantEvicted.bind(this.session), - recordingStarted: this.session.onRecordingStarted.bind(this.session), - recordingStopped: this.session.onRecordingStopped.bind(this.session), - sendMessage: this.session.onNewMessage.bind(this.session), - streamPropertyChanged: this.session.onStreamPropertyChanged.bind(this.session), - connectionPropertyChanged: this.session.onConnectionPropertyChanged.bind(this.session), - networkQualityLevelChanged: this.session.onNetworkQualityLevelChangedChanged.bind(this.session), - 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), - forciblyReconnectSubscriber: this.session.onForciblyReconnectSubscriber.bind(this.session) - } - }; - this.jsonRpcClient = new RpcBuilder.clients.JsonRpcClient(config); - } - - /** - * @hidden - */ - onMasterNodeCrashedNotification(response): void { - console.error('Master Node has crashed'); - this.masterNodeHasCrashed = true; - this.session.onLostConnection("nodeCrashed"); - this.jsonRpcClient.close(4103, "Master Node has crashed"); - } - - /** - * @hidden - */ - getWsReadyState(): number { - return this.jsonRpcClient.getReadyState(); - } - - /** - * @hidden - */ - closeWs(): void { - this.jsonRpcClient.close(4102, "Connection closed by client"); - } - - /** - * @hidden - */ - sendRequest(method: string, params: any, callback?): void { - if (params && params instanceof Function) { - callback = params; - params = {}; - } - logger.debug('Sending request: {method:"' + method + '", params: ' + JSON.stringify(params) + '}'); - this.jsonRpcClient?.send(method, params, callback); - } - - /** - * @hidden - */ - getWsUri(): string { - return this.wsUri; - } - - /** - * @hidden - */ - getSecret(): string { - return this.secret; - } - - /** - * @hidden - */ - getRecorder(): boolean { - return this.recorder; - } - - /** - * @hidden - */ - generateAudioDeviceError(error, constraints: MediaStreamConstraints): OpenViduError { - if (error.name === 'Error') { - // Safari OverConstrainedError has as name property 'Error' instead of 'OverConstrainedError' - error.name = error.constructor.name; - } - let errorName, errorMessage: string; - switch (error.name.toLowerCase()) { - case 'notfounderror': - errorName = OpenViduErrorName.INPUT_AUDIO_DEVICE_NOT_FOUND; - errorMessage = error.toString(); - return new OpenViduError(errorName, errorMessage); - case 'notallowederror': - errorName = OpenViduErrorName.DEVICE_ACCESS_DENIED; - errorMessage = error.toString(); - return new OpenViduError(errorName, errorMessage); - case 'overconstrainederror': - if (error.constraint.toLowerCase() === 'deviceid') { - errorName = OpenViduErrorName.INPUT_AUDIO_DEVICE_NOT_FOUND; - errorMessage = "Audio input device with deviceId '" + ((constraints.audio).deviceId!!).exact + "' not found"; - } else { - errorName = OpenViduErrorName.PUBLISHER_PROPERTIES_ERROR; - errorMessage = "Audio input device doesn't support the value passed for constraint '" + error.constraint + "'"; - } - return new OpenViduError(errorName, errorMessage); - case 'notreadableerror': - errorName = OpenViduErrorName.DEVICE_ALREADY_IN_USE; - errorMessage = error.toString(); - return (new OpenViduError(errorName, errorMessage)); - default: - return new OpenViduError(OpenViduErrorName.INPUT_AUDIO_DEVICE_GENERIC_ERROR, error.toString()); - } - } - - /** - * @hidden - */ - addAlreadyProvidedTracks(myConstraints: CustomMediaStreamConstraints, mediaStream: MediaStream, stream?: Stream) { - if (!!myConstraints.videoTrack) { - mediaStream.addTrack(myConstraints.videoTrack); - if (!!stream) { - if (!!myConstraints.constraints.video) { - stream.lastVideoTrackConstraints = myConstraints.constraints.video; - } else { - stream.lastVideoTrackConstraints = myConstraints.videoTrack.getConstraints(); - } - } - } - if (!!myConstraints.audioTrack) { - mediaStream.addTrack(myConstraints.audioTrack); - } - return mediaStream; - } - - /** - * @hidden - */ - protected configureDeviceIdOrScreensharing(myConstraints: CustomMediaStreamConstraints, publisherProperties: PublisherProperties, resolve, reject) { - const audioSource = publisherProperties.audioSource; - const videoSource = publisherProperties.videoSource; - if (typeof audioSource === 'string') { - myConstraints.constraints!.audio = { deviceId: { exact: audioSource } }; } - if (typeof videoSource === 'string') { + /** + * Returns new session + */ + initSession(): Session { + this.session = new Session(this); + return this.session; + } - if (!this.isScreenShare(videoSource)) { - this.setVideoSource(myConstraints, videoSource); + initPublisher(targetElement: string | HTMLElement | undefined): Publisher; + initPublisher(targetElement: string | HTMLElement | undefined, properties: PublisherProperties): Publisher; + initPublisher(targetElement: string | HTMLElement | undefined, completionHandler: (error: Error | undefined) => void): Publisher; + initPublisher( + targetElement: string | HTMLElement | undefined, + properties: PublisherProperties, + completionHandler: (error: Error | undefined) => void + ): Publisher; - } else { + /** + * Returns a new publisher + * + * #### Events dispatched + * + * The [[Publisher]] object will dispatch an `accessDialogOpened` event, only if the pop-up shown by the browser to request permissions for the camera is opened. You can use this event to alert the user about granting permissions + * for your website. An `accessDialogClosed` event will also be dispatched after user clicks on "Allow" or "Block" in the pop-up. + * + * The [[Publisher]] object will dispatch an `accessAllowed` or `accessDenied` event once it has been granted access to the requested input devices or not. + * + * The [[Publisher]] object will dispatch a `videoElementCreated` event once a HTML video element has been added to DOM (only if you + * [let OpenVidu take care of the video players](/en/stable/cheatsheet/manage-videos/#let-openvidu-take-care-of-the-video-players)). See [[VideoElementEvent]] to learn more. + * + * The [[Publisher]] object will dispatch a `streamPlaying` event once the local streams starts playing. See [[StreamManagerEvent]] to learn more. + * + * @param targetElement HTML DOM element (or its `id` attribute) in which the video element of the Publisher will be inserted (see [[PublisherProperties.insertMode]]). If *null* or *undefined* no default video will be created for this Publisher. + * You can always call method [[Publisher.addVideoElement]] or [[Publisher.createVideoElement]] to manage the video elements on your own (see [Manage video players](/en/stable/cheatsheet/manage-videos) section) + * @param completionHandler `error` parameter is null if `initPublisher` succeeds, and is defined if it fails. + * `completionHandler` function is called before the Publisher dispatches an `accessAllowed` or an `accessDenied` event + */ + initPublisher(targetElement: string | HTMLElement | undefined, param2?, param3?): Publisher { + let properties: PublisherProperties; - // Screen sharing + if (!!param2 && typeof param2 !== 'function') { + // Matches 'initPublisher(targetElement, properties)' or 'initPublisher(targetElement, properties, completionHandler)' - if (!this.checkScreenSharingCapabilities()) { - const error = new OpenViduError(OpenViduErrorName.SCREEN_SHARING_NOT_SUPPORTED, 'You can only screen share in desktop Chrome, Firefox, Opera, Safari (>=13.0), Edge (>= 80) or Electron. Detected client: ' + platform.getName() + ' ' + platform.getVersion()); - logger.error(error); - return reject(error); - } else { + properties = param2; - if (platform.isElectron()) { - const prefix = "screen:"; - const videoSourceString: string = videoSource; - const electronScreenId = videoSourceString.substr(videoSourceString.indexOf(prefix) + prefix.length); - (myConstraints.constraints!.video) = { - mandatory: { - chromeMediaSource: 'desktop', - chromeMediaSourceId: electronScreenId - } + properties = { + audioSource: typeof properties.audioSource !== 'undefined' ? properties.audioSource : undefined, + frameRate: + typeof MediaStreamTrack !== 'undefined' && properties.videoSource instanceof MediaStreamTrack + ? undefined + : typeof properties.frameRate !== 'undefined' + ? properties.frameRate + : undefined, + insertMode: + typeof properties.insertMode !== 'undefined' + ? typeof properties.insertMode === 'string' + ? VideoInsertMode[properties.insertMode] + : properties.insertMode + : VideoInsertMode.APPEND, + mirror: typeof properties.mirror !== 'undefined' ? properties.mirror : true, + publishAudio: typeof properties.publishAudio !== 'undefined' ? properties.publishAudio : true, + publishVideo: typeof properties.publishVideo !== 'undefined' ? properties.publishVideo : true, + resolution: + typeof MediaStreamTrack !== 'undefined' && properties.videoSource instanceof MediaStreamTrack + ? undefined + : typeof properties.resolution !== 'undefined' + ? properties.resolution + : '640x480', + videoSource: typeof properties.videoSource !== 'undefined' ? properties.videoSource : undefined, + videoSimulcast: properties.videoSimulcast, + filter: properties.filter }; + } else { + // Matches 'initPublisher(targetElement)' or 'initPublisher(targetElement, completionHandler)' + + properties = { + insertMode: VideoInsertMode.APPEND, + mirror: true, + publishAudio: true, + publishVideo: true, + resolution: '640x480' + }; + } + + const publisher: Publisher = new Publisher(targetElement, properties, this); + + let completionHandler: (error: Error | undefined) => void; + if (!!param2 && typeof param2 === 'function') { + completionHandler = param2; + } else if (!!param3) { + completionHandler = param3; + } + + publisher + .initialize() + .then(() => { + if (completionHandler !== undefined) { + completionHandler(undefined); + } + publisher.emitEvent('accessAllowed', []); + }) + .catch((error) => { + if (completionHandler !== undefined) { + completionHandler(error); + } + publisher.emitEvent('accessDenied', [error]); + }); + + this.publishers.push(publisher); + return publisher; + } + + /** + * Promisified version of [[OpenVidu.initPublisher]] + * + * > WARNING: events `accessDialogOpened` and `accessDialogClosed` will not be dispatched if using this method instead of [[OpenVidu.initPublisher]] + */ + initPublisherAsync(targetElement: string | HTMLElement | undefined): Promise; + initPublisherAsync(targetElement: string | HTMLElement | undefined, properties: PublisherProperties): Promise; + + initPublisherAsync(targetElement: string | HTMLElement | undefined, properties?: PublisherProperties): Promise { + return new Promise((resolve, reject) => { + let publisher: Publisher; + + const callback = (error: Error) => { + if (!!error) { + return reject(error); + } else { + return resolve(publisher); + } + }; + + if (!!properties) { + publisher = this.initPublisher(targetElement, properties, callback); + } else { + publisher = this.initPublisher(targetElement, callback); + } + }); + } + + /** + * Returns a new local recorder for recording streams straight away from the browser + * @param stream Stream to record + */ + initLocalRecorder(stream: Stream): LocalRecorder { + return new LocalRecorder(stream); + } + + /** + * Checks if the browser supports OpenVidu + * @returns 1 if the browser supports OpenVidu, 0 otherwise + */ + checkSystemRequirements(): boolean { + // Specific iOS platform support (iPhone, iPad) + if (platform.isIPhoneOrIPad()) { + return ( + platform.isIOSWithSafari() || + platform.isChromeMobileBrowser() || + platform.isFirefoxMobileBrowser() || + platform.isOperaMobileBrowser() || + platform.isEdgeMobileBrowser() || + platform.isIonicIos() // Ionic apps for iOS + ); + } + + // General platform support for web clients (Desktop, Mobile) + return ( + platform.isChromeBrowser() || + platform.isChromeMobileBrowser() || + platform.isFirefoxBrowser() || + platform.isFirefoxMobileBrowser() || + platform.isOperaBrowser() || + platform.isOperaMobileBrowser() || + platform.isEdgeBrowser() || + platform.isEdgeMobileBrowser() || + platform.isSamsungBrowser() || + platform.isSafariBrowser() || + platform.isAndroidBrowser() || // Android WebView & Ionic apps for Android + platform.isElectron() || + platform.isNodeJs() + ); + } + + /** + * Checks if the browser supports screen-sharing. Desktop Chrome, Firefox and Opera support screen-sharing + * @returns 1 if the browser supports screen-sharing, 0 otherwise + */ + checkScreenSharingCapabilities(): boolean { + return platform.canScreenShare(); + } + + /** + * Collects information about the media input devices available on the system. You can pass property `deviceId` of a [[Device]] object as value of `audioSource` or `videoSource` properties in [[initPublisher]] method + */ + getDevices(): Promise { + return new Promise((resolve, reject) => { + navigator.mediaDevices + .enumerateDevices() + .then((deviceInfos) => { + const devices: Device[] = []; + + // Ionic Android devices + if (platform.isIonicAndroid() && typeof cordova != 'undefined' && cordova?.plugins?.EnumerateDevicesPlugin) { + cordova.plugins.EnumerateDevicesPlugin.getEnumerateDevices().then((pluginDevices: Device[]) => { + let pluginAudioDevices: Device[] = []; + let videoDevices: Device[] = []; + let audioDevices: Device[] = []; + pluginAudioDevices = pluginDevices.filter((device: Device) => device.kind === 'audioinput'); + videoDevices = deviceInfos.filter((device: Device) => device.kind === 'videoinput'); + audioDevices = deviceInfos.filter((device: Device) => device.kind === 'audioinput'); + videoDevices.forEach((deviceInfo, index) => { + if (!deviceInfo.label) { + let label = ''; + if (index === 0) { + label = 'Front Camera'; + } else if (index === 1) { + label = 'Back Camera'; + } else { + label = 'Unknown Camera'; + } + devices.push({ + kind: deviceInfo.kind, + deviceId: deviceInfo.deviceId, + label: label + }); + } else { + devices.push({ + kind: deviceInfo.kind, + deviceId: deviceInfo.deviceId, + label: deviceInfo.label + }); + } + }); + audioDevices.forEach((deviceInfo, index) => { + if (!deviceInfo.label) { + let label = ''; + switch (index) { + case 0: // Default Microphone + label = 'Default'; + break; + case 1: // Microphone + Speakerphone + const defaultMatch = pluginAudioDevices.filter((d) => d.label.includes('Built'))[0]; + label = defaultMatch ? defaultMatch.label : 'Built-in Microphone'; + break; + case 2: // Headset Microphone + const wiredMatch = pluginAudioDevices.filter((d) => d.label.includes('Wired'))[0]; + if (wiredMatch) { + label = wiredMatch.label; + } else { + label = 'Headset earpiece'; + } + break; + case 3: + const wirelessMatch = pluginAudioDevices.filter((d) => d.label.includes('Bluetooth'))[0]; + label = wirelessMatch ? wirelessMatch.label : 'Wireless'; + break; + default: + label = 'Unknown Microphone'; + break; + } + devices.push({ + kind: deviceInfo.kind, + deviceId: deviceInfo.deviceId, + label: label + }); + } else { + devices.push({ + kind: deviceInfo.kind, + deviceId: deviceInfo.deviceId, + label: deviceInfo.label + }); + } + }); + return resolve(devices); + }); + } else { + // Rest of platforms + deviceInfos.forEach((deviceInfo) => { + if (deviceInfo.kind === 'audioinput' || deviceInfo.kind === 'videoinput') { + devices.push({ + kind: deviceInfo.kind, + deviceId: deviceInfo.deviceId, + label: deviceInfo.label + }); + } + }); + return resolve(devices); + } + }) + .catch((error) => { + logger.error('Error getting devices', error); + return reject(error); + }); + }); + } + + /** + * Get a MediaStream object that you can customize before calling [[initPublisher]] (pass _MediaStreamTrack_ property of the _MediaStream_ value resolved by the Promise as `audioSource` or `videoSource` properties in [[initPublisher]]) + * + * Parameter `options` is the same as in [[initPublisher]] second parameter (of type [[PublisherProperties]]), but only the following properties will be applied: `audioSource`, `videoSource`, `frameRate`, `resolution` + * + * To customize the Publisher's video, the API for HTMLCanvasElement is very useful. For example, to get a black-and-white video at 10 fps and HD resolution with no sound: + * ``` + * var OV = new OpenVidu(); + * var FRAME_RATE = 10; + * + * OV.getUserMedia({ + * audioSource: false, + * videoSource: undefined, + * resolution: '1280x720', + * frameRate: FRAME_RATE + * }) + * .then(mediaStream => { + * + * var videoTrack = mediaStream.getVideoTracks()[0]; + * var video = document.createElement('video'); + * video.srcObject = new MediaStream([videoTrack]); + * + * var canvas = document.createElement('canvas'); + * var ctx = canvas.getContext('2d'); + * ctx.filter = 'grayscale(100%)'; + * + * video.addEventListener('play', () => { + * var loop = () => { + * if (!video.paused && !video.ended) { + * ctx.drawImage(video, 0, 0, 300, 170); + * setTimeout(loop, 1000/ FRAME_RATE); // Drawing at 10 fps + * } + * }; + * loop(); + * }); + * video.play(); + * + * var grayVideoTrack = canvas.captureStream(FRAME_RATE).getVideoTracks()[0]; + * var publisher = this.OV.initPublisher( + * myHtmlTarget, + * { + * audioSource: false, + * videoSource: grayVideoTrack + * }); + * }); + * ``` + */ + getUserMedia(options: PublisherProperties): Promise { + return new Promise(async (resolve, reject) => { + const askForAudioStreamOnly = async (previousMediaStream: MediaStream, constraints: MediaStreamConstraints) => { + const definedAudioConstraint = constraints.audio === undefined ? true : constraints.audio; + const constraintsAux: MediaStreamConstraints = { audio: definedAudioConstraint, video: false }; + try { + const audioOnlyStream = await navigator.mediaDevices.getUserMedia(constraintsAux); + previousMediaStream.addTrack(audioOnlyStream.getAudioTracks()[0]); + return resolve(previousMediaStream); + } catch (error) { + previousMediaStream.getAudioTracks().forEach((track) => { + track.stop(); + }); + previousMediaStream.getVideoTracks().forEach((track) => { + track.stop(); + }); + return reject(this.generateAudioDeviceError(error, constraintsAux)); + } + }; + + try { + const myConstraints = await this.generateMediaConstraints(options); + if ( + (!!myConstraints.videoTrack && !!myConstraints.audioTrack) || + (!!myConstraints.audioTrack && myConstraints.constraints?.video === false) || + (!!myConstraints.videoTrack && myConstraints.constraints?.audio === false) + ) { + // No need to call getUserMedia at all. Both tracks provided, or only AUDIO track provided or only VIDEO track provided + return resolve(this.addAlreadyProvidedTracks(myConstraints, new MediaStream())); + } else { + // getUserMedia must be called. AUDIO or VIDEO are requesting a new track + + // Delete already provided constraints for audio or video + if (!!myConstraints.videoTrack) { + delete myConstraints.constraints!.video; + } + if (!!myConstraints.audioTrack) { + delete myConstraints.constraints!.audio; + } + + let mustAskForAudioTrackLater = false; + if (typeof options.videoSource === 'string') { + // Video is deviceId or screen sharing + if ( + options.videoSource === 'screen' || + options.videoSource === 'window' || + (platform.isElectron() && options.videoSource.startsWith('screen:')) + ) { + // Video is screen sharing + mustAskForAudioTrackLater = + !myConstraints.audioTrack && options.audioSource !== null && options.audioSource !== false; + if (navigator.mediaDevices['getDisplayMedia'] && !platform.isElectron()) { + // getDisplayMedia supported + try { + const mediaStream = await navigator.mediaDevices['getDisplayMedia']({ video: true }); + this.addAlreadyProvidedTracks(myConstraints, mediaStream); + if (mustAskForAudioTrackLater) { + await askForAudioStreamOnly(mediaStream, myConstraints.constraints); + } else { + return resolve(mediaStream); + } + } catch (error) { + let errorName: OpenViduErrorName = OpenViduErrorName.SCREEN_CAPTURE_DENIED; + const errorMessage = error.toString(); + return reject(new OpenViduError(errorName, errorMessage)); + } + } else { + // getDisplayMedia NOT supported. Can perform getUserMedia below with already calculated constraints + } + } else { + // Video is deviceId. Can perform getUserMedia below with already calculated constraints + } + } + // Use already calculated constraints + const constraintsAux = mustAskForAudioTrackLater + ? { video: myConstraints.constraints!.video } + : myConstraints.constraints; + try { + const mediaStream = await navigator.mediaDevices.getUserMedia(constraintsAux); + this.addAlreadyProvidedTracks(myConstraints, mediaStream); + if (mustAskForAudioTrackLater) { + await askForAudioStreamOnly(mediaStream, myConstraints.constraints); + } else { + return resolve(mediaStream); + } + } catch (error) { + let errorName: OpenViduErrorName; + const errorMessage = error.toString(); + if (!(options.videoSource === 'screen')) { + errorName = OpenViduErrorName.DEVICE_ACCESS_DENIED; + } else { + errorName = OpenViduErrorName.SCREEN_CAPTURE_DENIED; + } + return reject(new OpenViduError(errorName, errorMessage)); + } + } + } catch (error) { + reject(error); + } + }); + } + + /* tslint:disable:no-empty */ + /** + * Disable all logging except error level + */ + enableProdMode(): void { + logger.enableProdMode(); + } + /* tslint:enable:no-empty */ + + /** + * Set OpenVidu advanced configuration options. `configuration` is an object of type [[OpenViduAdvancedConfiguration]]. Call this method to override previous values at any moment. + */ + setAdvancedConfiguration(configuration: OpenViduAdvancedConfiguration): void { + this.advancedConfiguration = configuration; + } + + /* Hidden methods */ + + /** + * @hidden + */ + onOrientationChanged(handler): void { + (globalThis as any).addEventListener('orientationchange', handler); + } + + /** + * @hidden + */ + sendNewVideoDimensionsIfRequired(publisher: Publisher, reason: string, WAIT_INTERVAL: number, MAX_ATTEMPTS: number) { + let attempts = 0; + const oldWidth = publisher?.stream?.videoDimensions?.width || 0; + const oldHeight = publisher?.stream?.videoDimensions?.height || 0; + + const repeatUntilChangeOrMaxAttempts: NodeJS.Timeout = setInterval(() => { + attempts++; + if (attempts > MAX_ATTEMPTS) { + clearTimeout(repeatUntilChangeOrMaxAttempts); + } + publisher.getVideoDimensions().then((newDimensions) => { + if (newDimensions.width !== oldWidth || newDimensions.height !== oldHeight) { + clearTimeout(repeatUntilChangeOrMaxAttempts); + this.sendVideoDimensionsChangedEvent(publisher, reason, oldWidth, oldHeight, newDimensions.width, newDimensions.height); + } + }); + }, WAIT_INTERVAL); + } + + /** + * @hidden + */ + sendVideoDimensionsChangedEvent( + publisher: Publisher, + reason: string, + oldWidth: number, + oldHeight: number, + newWidth: number, + newHeight: number + ) { + publisher.stream.videoDimensions = { + width: newWidth || 0, + height: newHeight || 0 + }; + this.sendRequest( + 'streamPropertyChanged', + { + streamId: publisher.stream.streamId, + property: 'videoDimensions', + newValue: JSON.stringify(publisher.stream.videoDimensions), + reason + }, + (error, response) => { + if (error) { + logger.error("Error sending 'streamPropertyChanged' event", error); + } else { + this.session.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent( + this.session, + publisher.stream, + 'videoDimensions', + publisher.stream.videoDimensions, + { width: oldWidth, height: oldHeight }, + reason + ) + ]); + publisher.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent( + publisher, + publisher.stream, + 'videoDimensions', + publisher.stream.videoDimensions, + { width: oldWidth, height: oldHeight }, + reason + ) + ]); + this.session.sendVideoData(publisher); + } + } + ); + } + + /** + * @hidden + */ + sendTrackChangedEvent(publisher: Publisher, reason: string, oldLabel: string, newLabel: string, propertyType: string) { + const oldValue = { label: oldLabel }; + const newValue = { label: newLabel }; + + if (publisher.stream.isLocalStreamPublished) { + this.sendRequest( + 'streamPropertyChanged', + { + streamId: publisher.stream.streamId, + property: propertyType, + newValue: JSON.stringify({ newLabel }), + reason + }, + (error, response) => { + if (error) { + logger.error("Error sending 'streamPropertyChanged' event", error); + } else { + this.session.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent(this.session, publisher.stream, propertyType, newValue, oldValue, reason) + ]); + publisher.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent(publisher, publisher.stream, propertyType, newValue, oldValue, reason) + ]); + } + } + ); + } else { + this.session.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent(this.session, publisher.stream, propertyType, newValue, oldValue, reason) + ]); + publisher.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent(publisher, publisher.stream, propertyType, newValue, oldValue, reason) + ]); + } + } + + /** + * @hidden + */ + generateMediaConstraints(publisherProperties: PublisherProperties): Promise { + return new Promise((resolve, reject) => { + const myConstraints: CustomMediaStreamConstraints = { + audioTrack: undefined, + videoTrack: undefined, + constraints: { + audio: undefined, + video: undefined + } + }; + const audioSource = publisherProperties.audioSource; + const videoSource = publisherProperties.videoSource; + + // CASE 1: null/false + if (audioSource === null || audioSource === false) { + // No audio track + myConstraints.constraints!.audio = false; + } + if (videoSource === null || videoSource === false) { + // No video track + myConstraints.constraints!.video = false; + } + if (myConstraints.constraints!.audio === false && myConstraints.constraints!.video === false) { + // ERROR! audioSource and videoSource cannot be both false at the same time + return reject( + new OpenViduError( + OpenViduErrorName.NO_INPUT_SOURCE_SET, + "Properties 'audioSource' and 'videoSource' cannot be set to false or null at the same time" + ) + ); + } + + // CASE 2: MediaStreamTracks + if (typeof MediaStreamTrack !== 'undefined' && audioSource instanceof MediaStreamTrack) { + // Already provided audio track + myConstraints.audioTrack = audioSource; + } + if (typeof MediaStreamTrack !== 'undefined' && videoSource instanceof MediaStreamTrack) { + // Already provided video track + myConstraints.videoTrack = videoSource; + } + + // CASE 3: Default tracks + if (audioSource === undefined) { + myConstraints.constraints!.audio = true; + } + if (videoSource === undefined) { + myConstraints.constraints!.video = { + width: { + ideal: 640 + }, + height: { + ideal: 480 + } + }; + } + + // CASE 3.5: give values to resolution and frameRate if video not null/false + if (videoSource !== null && videoSource !== false) { + if (!!publisherProperties.resolution) { + const widthAndHeight = publisherProperties.resolution.toLowerCase().split('x'); + const idealWidth = Number(widthAndHeight[0]); + const idealHeight = Number(widthAndHeight[1]); + myConstraints.constraints!.video = { + width: { + ideal: idealWidth + }, + height: { + ideal: idealHeight + } + }; + } + if (!!publisherProperties.frameRate) { + (myConstraints.constraints!.video).frameRate = { ideal: publisherProperties.frameRate }; + } + } + + // CASE 4: deviceId or screen sharing + this.configureDeviceIdOrScreensharing(myConstraints, publisherProperties, resolve, reject); + return resolve(myConstraints); + }); + } - } else { + /** + * @hidden + */ + startWs(onConnectSucces: (error: Error) => void): void { + const config = { + heartbeat: 5000, + ws: { + uri: this.wsUri + '?sessionId=' + this.session.sessionId, + onconnected: onConnectSucces, + ondisconnect: this.disconnectCallback.bind(this), + onreconnecting: this.reconnectingCallback.bind(this), + onreconnected: this.reconnectedCallback.bind(this), + ismasternodecrashed: this.isMasterNodeCrashed.bind(this) + }, + rpc: { + requestTimeout: 10000, + heartbeatRequestTimeout: 5000, + participantJoined: this.session.onParticipantJoined.bind(this.session), + participantPublished: this.session.onParticipantPublished.bind(this.session), + participantUnpublished: this.session.onParticipantUnpublished.bind(this.session), + participantLeft: this.session.onParticipantLeft.bind(this.session), + participantEvicted: this.session.onParticipantEvicted.bind(this.session), + recordingStarted: this.session.onRecordingStarted.bind(this.session), + recordingStopped: this.session.onRecordingStopped.bind(this.session), + sendMessage: this.session.onNewMessage.bind(this.session), + streamPropertyChanged: this.session.onStreamPropertyChanged.bind(this.session), + connectionPropertyChanged: this.session.onConnectionPropertyChanged.bind(this.session), + networkQualityLevelChanged: this.session.onNetworkQualityLevelChangedChanged.bind(this.session), + 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), + forciblyReconnectSubscriber: this.session.onForciblyReconnectSubscriber.bind(this.session) + } + }; + this.jsonRpcClient = new RpcBuilder.clients.JsonRpcClient(config); + } - if (!!this.advancedConfiguration.screenShareChromeExtension && !(platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) && !navigator.mediaDevices['getDisplayMedia']) { + /** + * @hidden + */ + onMasterNodeCrashedNotification(response): void { + console.error('Master Node has crashed'); + this.masterNodeHasCrashed = true; + this.session.onLostConnection('nodeCrashed'); + this.jsonRpcClient.close(4103, 'Master Node has crashed'); + } - // Custom screen sharing extension for Chrome (and Opera) and no support for MediaDevices.getDisplayMedia() + /** + * @hidden + */ + getWsReadyState(): number { + return this.jsonRpcClient.getReadyState(); + } - screenSharing.getScreenConstraints((error, screenConstraints) => { - if (!!error || !!screenConstraints.mandatory && screenConstraints.mandatory.chromeMediaSource === 'screen') { - if (error === 'permission-denied' || error === 'PermissionDeniedError') { - const error = new OpenViduError(OpenViduErrorName.SCREEN_CAPTURE_DENIED, 'You must allow access to one window of your desktop'); + /** + * @hidden + */ + closeWs(): void { + this.jsonRpcClient.close(4102, 'Connection closed by client'); + } + + /** + * @hidden + */ + sendRequest(method: string, params: any, callback?): void { + if (params && params instanceof Function) { + callback = params; + params = {}; + } + logger.debug('Sending request: {method:"' + method + '", params: ' + JSON.stringify(params) + '}'); + this.jsonRpcClient?.send(method, params, callback); + } + + /** + * @hidden + */ + getWsUri(): string { + return this.wsUri; + } + + /** + * @hidden + */ + getSecret(): string { + return this.secret; + } + + /** + * @hidden + */ + getRecorder(): boolean { + return this.recorder; + } + + /** + * @hidden + */ + generateAudioDeviceError(error, constraints: MediaStreamConstraints): OpenViduError { + if (error.name === 'Error') { + // Safari OverConstrainedError has as name property 'Error' instead of 'OverConstrainedError' + error.name = error.constructor.name; + } + let errorName, errorMessage: string; + switch (error.name.toLowerCase()) { + case 'notfounderror': + errorName = OpenViduErrorName.INPUT_AUDIO_DEVICE_NOT_FOUND; + errorMessage = error.toString(); + return new OpenViduError(errorName, errorMessage); + case 'notallowederror': + errorName = OpenViduErrorName.DEVICE_ACCESS_DENIED; + errorMessage = error.toString(); + return new OpenViduError(errorName, errorMessage); + case 'overconstrainederror': + if (error.constraint.toLowerCase() === 'deviceid') { + errorName = OpenViduErrorName.INPUT_AUDIO_DEVICE_NOT_FOUND; + errorMessage = + "Audio input device with deviceId '" + + ((constraints.audio).deviceId!!).exact + + "' not found"; + } else { + errorName = OpenViduErrorName.PUBLISHER_PROPERTIES_ERROR; + errorMessage = "Audio input device doesn't support the value passed for constraint '" + error.constraint + "'"; + } + return new OpenViduError(errorName, errorMessage); + case 'notreadableerror': + errorName = OpenViduErrorName.DEVICE_ALREADY_IN_USE; + errorMessage = error.toString(); + return new OpenViduError(errorName, errorMessage); + default: + return new OpenViduError(OpenViduErrorName.INPUT_AUDIO_DEVICE_GENERIC_ERROR, error.toString()); + } + } + + /** + * @hidden + */ + addAlreadyProvidedTracks(myConstraints: CustomMediaStreamConstraints, mediaStream: MediaStream, stream?: Stream) { + if (!!myConstraints.videoTrack) { + mediaStream.addTrack(myConstraints.videoTrack); + if (!!stream) { + if (!!myConstraints.constraints.video) { + stream.lastVideoTrackConstraints = myConstraints.constraints.video; + } else { + stream.lastVideoTrackConstraints = myConstraints.videoTrack.getConstraints(); + } + } + } + if (!!myConstraints.audioTrack) { + mediaStream.addTrack(myConstraints.audioTrack); + } + return mediaStream; + } + + /** + * @hidden + */ + protected configureDeviceIdOrScreensharing( + myConstraints: CustomMediaStreamConstraints, + publisherProperties: PublisherProperties, + resolve, + reject + ) { + const audioSource = publisherProperties.audioSource; + const videoSource = publisherProperties.videoSource; + if (typeof audioSource === 'string') { + myConstraints.constraints!.audio = { deviceId: { exact: audioSource } }; + } + + if (typeof videoSource === 'string') { + if (!this.isScreenShare(videoSource)) { + this.setVideoSource(myConstraints, videoSource); + } else { + // Screen sharing + + if (!this.checkScreenSharingCapabilities()) { + const error = new OpenViduError( + OpenViduErrorName.SCREEN_SHARING_NOT_SUPPORTED, + 'You can only screen share in desktop Chrome, Firefox, Opera, Safari (>=13.0), Edge (>= 80) or Electron. Detected client: ' + + platform.getName() + + ' ' + + platform.getVersion() + ); logger.error(error); return reject(error); - } else { - const extensionId = this.advancedConfiguration.screenShareChromeExtension!.split('/').pop()!!.trim(); - screenSharing.getChromeExtensionStatus(extensionId, status => { - if (status === 'installed-disabled') { - const error = new OpenViduError(OpenViduErrorName.SCREEN_EXTENSION_DISABLED, 'You must enable the screen extension'); - logger.error(error); - return reject(error); - } - if (status === 'not-installed') { - const error = new OpenViduError(OpenViduErrorName.SCREEN_EXTENSION_NOT_INSTALLED, (this.advancedConfiguration.screenShareChromeExtension)); - logger.error(error); - return reject(error); - } - }); - return; - } } else { - myConstraints.constraints!.video = screenConstraints; - return resolve(myConstraints); - } - }); - return; - } else { - - if (navigator.mediaDevices['getDisplayMedia']) { - // getDisplayMedia support (Chrome >= 72, Firefox >= 66, Safari >= 13) - return resolve(myConstraints); - } else { - // Default screen sharing extension for Chrome/Opera, or is Firefox < 66 - const firefoxString = (platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) ? publisherProperties.videoSource : undefined; - - screenSharingAuto.getScreenId(firefoxString, (error, sourceId, screenConstraints) => { - if (!!error) { - if (error === 'not-installed') { - const extensionUrl = !!this.advancedConfiguration.screenShareChromeExtension ? this.advancedConfiguration.screenShareChromeExtension : - 'https://chrome.google.com/webstore/detail/openvidu-screensharing/lfcgfepafnobdloecchnfaclibenjold'; - const err = new OpenViduError(OpenViduErrorName.SCREEN_EXTENSION_NOT_INSTALLED, extensionUrl); - logger.error(err); - return reject(err); - } else if (error === 'installed-disabled') { - const err = new OpenViduError(OpenViduErrorName.SCREEN_EXTENSION_DISABLED, 'You must enable the screen extension'); - logger.error(err); - return reject(err); - } else if (error === 'permission-denied') { - const err = new OpenViduError(OpenViduErrorName.SCREEN_CAPTURE_DENIED, 'You must allow access to one window of your desktop'); - logger.error(err); - return reject(err); + if (platform.isElectron()) { + const prefix = 'screen:'; + const videoSourceString: string = videoSource; + const electronScreenId = videoSourceString.substr(videoSourceString.indexOf(prefix) + prefix.length); + (myConstraints.constraints!.video) = { + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: electronScreenId + } + }; + return resolve(myConstraints); } else { - const err = new OpenViduError(OpenViduErrorName.GENERIC_ERROR, 'Unknown error when accessing screen share'); - logger.error(err); - logger.error(error); - return reject(err); + if ( + !!this.advancedConfiguration.screenShareChromeExtension && + !(platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) && + !navigator.mediaDevices['getDisplayMedia'] + ) { + // Custom screen sharing extension for Chrome (and Opera) and no support for MediaDevices.getDisplayMedia() + + screenSharing.getScreenConstraints((error, screenConstraints) => { + if ( + !!error || + (!!screenConstraints.mandatory && screenConstraints.mandatory.chromeMediaSource === 'screen') + ) { + if (error === 'permission-denied' || error === 'PermissionDeniedError') { + const error = new OpenViduError( + OpenViduErrorName.SCREEN_CAPTURE_DENIED, + 'You must allow access to one window of your desktop' + ); + logger.error(error); + return reject(error); + } else { + const extensionId = this.advancedConfiguration + .screenShareChromeExtension!.split('/') + .pop()!! + .trim(); + screenSharing.getChromeExtensionStatus(extensionId, (status) => { + if (status === 'installed-disabled') { + const error = new OpenViduError( + OpenViduErrorName.SCREEN_EXTENSION_DISABLED, + 'You must enable the screen extension' + ); + logger.error(error); + return reject(error); + } + if (status === 'not-installed') { + const error = new OpenViduError( + OpenViduErrorName.SCREEN_EXTENSION_NOT_INSTALLED, + this.advancedConfiguration.screenShareChromeExtension + ); + logger.error(error); + return reject(error); + } + }); + return; + } + } else { + myConstraints.constraints!.video = screenConstraints; + return resolve(myConstraints); + } + }); + return; + } else { + if (navigator.mediaDevices['getDisplayMedia']) { + // getDisplayMedia support (Chrome >= 72, Firefox >= 66, Safari >= 13) + return resolve(myConstraints); + } else { + // Default screen sharing extension for Chrome/Opera, or is Firefox < 66 + const firefoxString = + platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser() + ? publisherProperties.videoSource + : undefined; + + screenSharingAuto.getScreenId(firefoxString, (error, sourceId, screenConstraints) => { + if (!!error) { + if (error === 'not-installed') { + const extensionUrl = !!this.advancedConfiguration.screenShareChromeExtension + ? this.advancedConfiguration.screenShareChromeExtension + : 'https://chrome.google.com/webstore/detail/openvidu-screensharing/lfcgfepafnobdloecchnfaclibenjold'; + const err = new OpenViduError(OpenViduErrorName.SCREEN_EXTENSION_NOT_INSTALLED, extensionUrl); + logger.error(err); + return reject(err); + } else if (error === 'installed-disabled') { + const err = new OpenViduError( + OpenViduErrorName.SCREEN_EXTENSION_DISABLED, + 'You must enable the screen extension' + ); + logger.error(err); + return reject(err); + } else if (error === 'permission-denied') { + const err = new OpenViduError( + OpenViduErrorName.SCREEN_CAPTURE_DENIED, + 'You must allow access to one window of your desktop' + ); + logger.error(err); + return reject(err); + } else { + const err = new OpenViduError( + OpenViduErrorName.GENERIC_ERROR, + 'Unknown error when accessing screen share' + ); + logger.error(err); + logger.error(error); + return reject(err); + } + } else { + myConstraints.constraints!.video = screenConstraints.video; + return resolve(myConstraints); + } + }); + return; + } + } } - } else { - myConstraints.constraints!.video = screenConstraints.video; - return resolve(myConstraints); - } - }); - return; - } - } - } - } - } - } - } - - /** - * @hidden - */ - protected setVideoSource(myConstraints: CustomMediaStreamConstraints, videoSource: string) { - if (!myConstraints.constraints!.video) { - myConstraints.constraints!.video = {}; - } - (myConstraints.constraints!.video)['deviceId'] = { exact: videoSource }; - } - - - /* Private methods */ - - private disconnectCallback(): void { - logger.warn('Websocket connection lost'); - if (this.isRoomAvailable()) { - this.session.onLostConnection('networkDisconnect'); - } else { - alert('Connection error. Please reload page.'); - } - } - - private reconnectingCallback(): void { - logger.warn('Websocket connection lost (reconnecting)'); - if (!this.isRoomAvailable()) { - alert('Connection error. Please reload page.'); - } else { - this.session.emitEvent('reconnecting', []); - } - } - - private reconnectWebsocketThroughRpcConnectMethod(rpcSessionId) { - // This RPC method allows checking: - // Single Master: if success, connection recovered - // if error, no Master Node crashed and life will be -1. onLostConnection with reason networkDisconnect will be triggered - // Multi Master: if success, connection recovered - // if error and Master Node crashed notification was already received, nothing must be done - // if error and Master Node NOT crashed, sessionStatus method must be sent: - // if life is equal, networkDisconnect - // if life is greater, nodeCrashed - this.sendRequest('connect', { sessionId: rpcSessionId, reconnect: true }, (error, response) => { - if (!!error) { - - if (this.isMasterNodeCrashed()) { - - logger.warn('Master Node has crashed!'); - - } else { - - logger.error(error); - - const notifyLostConnection = (reason, errorMsg) => { - logger.warn(errorMsg); - this.session.onLostConnection(reason); - this.jsonRpcClient.close(4101, "Reconnection fault: " + errorMsg); - } - - const rpcSessionStatus = () => { - if (this.life === -1) { - // Single Master - notifyLostConnection('networkDisconnect', 'WS successfully reconnected but the user was already evicted due to timeout'); - } else { - // Multi Master - // This RPC method is only required to find out the reason of the disconnection: - // whether the client lost its network connection or a Master Node crashed - this.sendRequest('sessionStatus', { sessionId: this.session.sessionId }, (error, response) => { - if (error != null) { - console.error('Error checking session status', error); - } else { - if (this.life === response.life) { - // If the life stored in the client matches the life stored in the server, it means that the client lost its network connection - notifyLostConnection('networkDisconnect', 'WS successfully reconnected but the user was already evicted due to timeout'); - } else { - // If the life stored in the client is below the life stored in the server, it means that the Master Node has crashed - notifyLostConnection('nodeCrashed', 'WS successfully reconnected to OpenVidu Server but your Master Node crashed'); - } } - }); } - }; - - if (error.code === 40007 && error.message === 'reconnection error') { - // Kurento error: invalid RPC sessionId. This means that the kurento-jsonrpc-server of openvidu-server where kurento-jsonrpc-client - // is trying to reconnect does not know about this sessionId. This can mean two things: - // 1) openvidu-browser managed to reconnect after a while, but openvidu-server already evicted the user for not receiving ping. - // 2) openvidu-server process is a different one because of a node crash. - // Send a "sessionStatus" method to check the reason - console.error('Invalid RPC sessionId. Client network disconnection or Master Node crash'); - rpcSessionStatus(); - } else { - rpcSessionStatus(); - } - } - } else { - this.jsonRpcClient.resetPing(); - this.session.onRecoveredConnection(); - } - }); - } - - private reconnectedCallback(): void { - logger.warn('Websocket reconnected'); - if (this.isRoomAvailable()) { - if (!!this.session.connection) { - this.reconnectWebsocketThroughRpcConnectMethod(this.session.connection.rpcSessionId); - } else { - logger.warn('There was no previous connection when running reconnection callback'); - // Make Session object dispatch 'sessionDisconnected' event - const sessionDisconnectEvent = new SessionDisconnectedEvent(this.session, 'networkDisconnect'); - this.session.ee.emitEvent('sessionDisconnected', [sessionDisconnectEvent]); - sessionDisconnectEvent.callDefaultBehavior(); - } - } else { - alert('Connection error. Please reload page.'); } - } - private isMasterNodeCrashed() { - return this.masterNodeHasCrashed; - } - - private isRoomAvailable(): boolean { - if (this.session !== undefined && this.session instanceof Session) { - return true; - } else { - logger.warn('Session instance not found'); - return false; + /** + * @hidden + */ + protected setVideoSource(myConstraints: CustomMediaStreamConstraints, videoSource: string) { + if (!myConstraints.constraints!.video) { + myConstraints.constraints!.video = {}; + } + (myConstraints.constraints!.video)['deviceId'] = { exact: videoSource }; } - } - private isScreenShare(videoSource: string) { - return videoSource === 'screen' || - videoSource === 'window' || - (platform.isElectron() && videoSource.startsWith('screen:')) - } + /* Private methods */ + private disconnectCallback(): void { + logger.warn('Websocket connection lost'); + if (this.isRoomAvailable()) { + this.session.onLostConnection('networkDisconnect'); + } else { + alert('Connection error. Please reload page.'); + } + } + + private reconnectingCallback(): void { + logger.warn('Websocket connection lost (reconnecting)'); + if (!this.isRoomAvailable()) { + alert('Connection error. Please reload page.'); + } else { + this.session.emitEvent('reconnecting', []); + } + } + + private reconnectWebsocketThroughRpcConnectMethod(rpcSessionId) { + // This RPC method allows checking: + // Single Master: if success, connection recovered + // if error, no Master Node crashed and life will be -1. onLostConnection with reason networkDisconnect will be triggered + // Multi Master: if success, connection recovered + // if error and Master Node crashed notification was already received, nothing must be done + // if error and Master Node NOT crashed, sessionStatus method must be sent: + // if life is equal, networkDisconnect + // if life is greater, nodeCrashed + this.sendRequest('connect', { sessionId: rpcSessionId, reconnect: true }, (error, response) => { + if (!!error) { + if (this.isMasterNodeCrashed()) { + logger.warn('Master Node has crashed!'); + } else { + logger.error(error); + + const notifyLostConnection = (reason, errorMsg) => { + logger.warn(errorMsg); + this.session.onLostConnection(reason); + this.jsonRpcClient.close(4101, 'Reconnection fault: ' + errorMsg); + }; + + const rpcSessionStatus = () => { + if (this.life === -1) { + // Single Master + notifyLostConnection( + 'networkDisconnect', + 'WS successfully reconnected but the user was already evicted due to timeout' + ); + } else { + // Multi Master + // This RPC method is only required to find out the reason of the disconnection: + // whether the client lost its network connection or a Master Node crashed + this.sendRequest('sessionStatus', { sessionId: this.session.sessionId }, (error, response) => { + if (error != null) { + console.error('Error checking session status', error); + } else { + if (this.life === response.life) { + // If the life stored in the client matches the life stored in the server, it means that the client lost its network connection + notifyLostConnection( + 'networkDisconnect', + 'WS successfully reconnected but the user was already evicted due to timeout' + ); + } else { + // If the life stored in the client is below the life stored in the server, it means that the Master Node has crashed + notifyLostConnection( + 'nodeCrashed', + 'WS successfully reconnected to OpenVidu Server but your Master Node crashed' + ); + } + } + }); + } + }; + + if (error.code === 40007 && error.message === 'reconnection error') { + // Kurento error: invalid RPC sessionId. This means that the kurento-jsonrpc-server of openvidu-server where kurento-jsonrpc-client + // is trying to reconnect does not know about this sessionId. This can mean two things: + // 1) openvidu-browser managed to reconnect after a while, but openvidu-server already evicted the user for not receiving ping. + // 2) openvidu-server process is a different one because of a node crash. + // Send a "sessionStatus" method to check the reason + console.error('Invalid RPC sessionId. Client network disconnection or Master Node crash'); + rpcSessionStatus(); + } else { + rpcSessionStatus(); + } + } + } else { + this.jsonRpcClient.resetPing(); + this.session.onRecoveredConnection(); + } + }); + } + + private reconnectedCallback(): void { + logger.warn('Websocket reconnected'); + if (this.isRoomAvailable()) { + if (!!this.session.connection) { + this.reconnectWebsocketThroughRpcConnectMethod(this.session.connection.rpcSessionId); + } else { + logger.warn('There was no previous connection when running reconnection callback'); + // Make Session object dispatch 'sessionDisconnected' event + const sessionDisconnectEvent = new SessionDisconnectedEvent(this.session, 'networkDisconnect'); + this.session.ee.emitEvent('sessionDisconnected', [sessionDisconnectEvent]); + sessionDisconnectEvent.callDefaultBehavior(); + } + } else { + alert('Connection error. Please reload page.'); + } + } + + private isMasterNodeCrashed() { + return this.masterNodeHasCrashed; + } + + private isRoomAvailable(): boolean { + if (this.session !== undefined && this.session instanceof Session) { + return true; + } else { + logger.warn('Session instance not found'); + return false; + } + } + + private isScreenShare(videoSource: string) { + return videoSource === 'screen' || videoSource === 'window' || (platform.isElectron() && videoSource.startsWith('screen:')); + } } diff --git a/openvidu-browser/src/OpenVidu/Publisher.ts b/openvidu-browser/src/OpenVidu/Publisher.ts index 3f22aad7..5ddc7f24 100644 --- a/openvidu-browser/src/OpenVidu/Publisher.ts +++ b/openvidu-browser/src/OpenVidu/Publisher.ts @@ -45,7 +45,6 @@ let platform: PlatformUtils; * See available event listeners at [[PublisherEventMap]]. */ export class Publisher extends StreamManager { - /** * Whether the Publisher has been granted access to the requested input devices or not */ @@ -82,7 +81,13 @@ export class Publisher extends StreamManager { * @hidden */ constructor(targEl: string | HTMLElement | undefined, properties: PublisherProperties, openvidu: OpenVidu) { - super(new Stream((!!openvidu.session) ? openvidu.session : new Session(openvidu), { publisherProperties: properties, mediaConstraints: {} }), targEl); + super( + new Stream(!!openvidu.session ? openvidu.session : new Session(openvidu), { + publisherProperties: properties, + mediaConstraints: {} + }), + targEl + ); platform = PlatformUtils.getInstance(); this.properties = properties; this.openvidu = openvidu; @@ -95,7 +100,6 @@ export class Publisher extends StreamManager { }); } - /** * Publish or unpublish the audio stream (if available). Calling this method twice in a row passing same `enabled` value will have no effect * @@ -115,7 +119,9 @@ export class Publisher extends StreamManager { */ publishAudio(enabled: boolean): void { if (this.stream.audioActive !== enabled) { - const affectedMediaStream: MediaStream = this.stream.displayMyRemote() ? this.stream.localMediaStreamWhenSubscribedToRemote! : this.stream.getMediaStream(); + const affectedMediaStream: MediaStream = this.stream.displayMyRemote() + ? this.stream.localMediaStreamWhenSubscribedToRemote! + : this.stream.getMediaStream(); affectedMediaStream.getAudioTracks().forEach((track) => { track.enabled = enabled; }); @@ -132,18 +138,22 @@ export class Publisher extends StreamManager { if (error) { logger.error("Error sending 'streamPropertyChanged' event", error); } else { - this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, this.stream, 'audioActive', enabled, !enabled, 'publishAudio')]); - this.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this, this.stream, 'audioActive', enabled, !enabled, 'publishAudio')]); + this.session.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent(this.session, this.stream, 'audioActive', enabled, !enabled, 'publishAudio') + ]); + this.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent(this, this.stream, 'audioActive', enabled, !enabled, 'publishAudio') + ]); this.session.sendVideoData(this.stream.streamManager); } - }); + } + ); } this.stream.audioActive = enabled; logger.info("'Publisher' has " + (enabled ? 'published' : 'unpublished') + ' its audio stream'); } } - /** * Publish or unpublish the video stream (if available). Calling this method twice in a row passing same `enabled` value will have no effect * @@ -169,12 +179,11 @@ export class Publisher extends StreamManager { * will be used instead. */ publishVideo(enabled: T, resource?: T extends false ? boolean : MediaStreamTrack): Promise { - return new Promise(async (resolve, reject) => { - if (this.stream.videoActive !== enabled) { - - const affectedMediaStream: MediaStream = this.stream.displayMyRemote() ? this.stream.localMediaStreamWhenSubscribedToRemote! : this.stream.getMediaStream(); + const affectedMediaStream: MediaStream = this.stream.displayMyRemote() + ? this.stream.localMediaStreamWhenSubscribedToRemote! + : this.stream.getMediaStream(); let mustRestartMediaStream = false; affectedMediaStream.getVideoTracks().forEach((track) => { track.enabled = enabled; @@ -212,13 +221,16 @@ export class Publisher extends StreamManager { delete this.stream.lastVBFilter; }, 1); } - } + }; if (!!resource && resource instanceof MediaStreamTrack) { await replaceVideoTrack(resource); } else { try { - const mediaStream = await navigator.mediaDevices.getUserMedia({ audio: false, video: this.stream.lastVideoTrackConstraints }); + const mediaStream = await navigator.mediaDevices.getUserMedia({ + audio: false, + video: this.stream.lastVideoTrackConstraints + }); await replaceVideoTrack(mediaStream.getVideoTracks()[0]); } catch (error) { return reject(error); @@ -239,11 +251,23 @@ export class Publisher extends StreamManager { if (error) { logger.error("Error sending 'streamPropertyChanged' event", error); } else { - this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, this.stream, 'videoActive', enabled, !enabled, 'publishVideo')]); - this.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this, this.stream, 'videoActive', enabled, !enabled, 'publishVideo')]); + this.session.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent( + this.session, + this.stream, + 'videoActive', + enabled, + !enabled, + 'publishVideo' + ) + ]); + this.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent(this, this.stream, 'videoActive', enabled, !enabled, 'publishVideo') + ]); this.session.sendVideoData(this.stream.streamManager); } - }); + } + ); } this.stream.videoActive = enabled; logger.info("'Publisher' has " + (enabled ? 'published' : 'unpublished') + ' its video stream'); @@ -252,22 +276,19 @@ export class Publisher extends StreamManager { }); } - /** * Call this method before [[Session.publish]] if you prefer to subscribe to your Publisher's remote stream instead of using the local stream, as any other user would do. */ subscribeToRemote(value?: boolean): void { - value = (value !== undefined) ? value : true; + value = value !== undefined ? value : true; this.isSubscribedToRemote = value; this.stream.subscribeToMyRemote(value); } - /** * See [[EventDispatcher.on]] */ on(type: K, handler: (event: PublisherEventMap[K]) => void): this { - super.on(type, handler); if (type === 'streamCreated') { @@ -292,12 +313,10 @@ export class Publisher extends StreamManager { return this; } - /** * See [[EventDispatcher.once]] */ once(type: K, handler: (event: PublisherEventMap[K]) => void): this { - super.once(type, handler); if (type === 'streamCreated') { @@ -322,7 +341,6 @@ export class Publisher extends StreamManager { return this; } - /** * See [[EventDispatcher.off]] */ @@ -331,7 +349,6 @@ export class Publisher extends StreamManager { return this; } - /** * Replaces the current video or audio track with a different one. This allows you to replace an ongoing track with a different one * without having to renegotiate the whole WebRTC connection (that is, initializing a new Publisher, unpublishing the previous one @@ -359,7 +376,6 @@ export class Publisher extends StreamManager { */ initialize(): Promise { return new Promise(async (resolve, reject) => { - let constraints: MediaStreamConstraints = {}; let constraintsAux: MediaStreamConstraints = {}; const timeForDialogEvent = 2000; @@ -368,7 +384,7 @@ export class Publisher extends StreamManager { const errorCallback = (openViduError: OpenViduError) => { this.accessDenied = true; this.accessAllowed = false; - logger.error(`Publisher initialization failed. ${openViduError.name}: ${openViduError.message}`) + logger.error(`Publisher initialization failed. ${openViduError.name}: ${openViduError.message}`); return reject(openViduError); }; @@ -378,21 +394,27 @@ export class Publisher extends StreamManager { if (typeof MediaStreamTrack !== 'undefined' && this.properties.audioSource instanceof MediaStreamTrack) { mediaStream.removeTrack(mediaStream.getAudioTracks()[0]); - mediaStream.addTrack((this.properties.audioSource)); + mediaStream.addTrack(this.properties.audioSource); } if (typeof MediaStreamTrack !== 'undefined' && this.properties.videoSource instanceof MediaStreamTrack) { mediaStream.removeTrack(mediaStream.getVideoTracks()[0]); - mediaStream.addTrack((this.properties.videoSource)); + mediaStream.addTrack(this.properties.videoSource); } // Apply PublisherProperties.publishAudio and PublisherProperties.publishVideo if (!!mediaStream.getAudioTracks()[0]) { - const enabled = (this.stream.audioActive !== undefined && this.stream.audioActive !== null) ? this.stream.audioActive : !!this.stream.outboundStreamOpts.publisherProperties.publishAudio; + const enabled = + this.stream.audioActive !== undefined && this.stream.audioActive !== null + ? this.stream.audioActive + : !!this.stream.outboundStreamOpts.publisherProperties.publishAudio; mediaStream.getAudioTracks()[0].enabled = enabled; } if (!!mediaStream.getVideoTracks()[0]) { - const enabled = (this.stream.videoActive !== undefined && this.stream.videoActive !== null) ? this.stream.videoActive : !!this.stream.outboundStreamOpts.publisherProperties.publishVideo; + const enabled = + this.stream.videoActive !== undefined && this.stream.videoActive !== null + ? this.stream.videoActive + : !!this.stream.outboundStreamOpts.publisherProperties.publishVideo; mediaStream.getVideoTracks()[0].enabled = enabled; } @@ -411,16 +433,16 @@ export class Publisher extends StreamManager { // https://w3c.github.io/mst-content-hint/#video-content-hints switch (this.stream.typeOfVideo) { case TypeOfVideo.SCREEN: - track.contentHint = "detail"; + track.contentHint = 'detail'; break; case TypeOfVideo.CUSTOM: - logger.warn("CUSTOM type video track was provided without Content Hint!"); - track.contentHint = "motion"; + logger.warn('CUSTOM type video track was provided without Content Hint!'); + track.contentHint = 'motion'; break; case TypeOfVideo.CAMERA: case TypeOfVideo.IPCAM: default: - track.contentHint = "motion"; + track.contentHint = 'motion'; break; } logger.info(`Video track Content Hint set: '${track.contentHint}'`); @@ -438,7 +460,7 @@ export class Publisher extends StreamManager { if (this.stream.isSendVideo()) { // Has video track - this.getVideoDimensions().then(dimensions => { + this.getVideoDimensions().then((dimensions) => { this.stream.videoDimensions = { width: dimensions.width, height: dimensions.height @@ -491,7 +513,6 @@ export class Publisher extends StreamManager { this.clearPermissionDialogTimer(startTime, timeForDialogEvent); mediaStream.addTrack(audioOnlyStream.getAudioTracks()[0]); successCallback(mediaStream); - } catch (error) { this.clearPermissionDialogTimer(startTime, timeForDialogEvent); mediaStream.getAudioTracks().forEach((track) => { @@ -529,7 +550,6 @@ export class Publisher extends StreamManager { errorName = OpenViduErrorName.INPUT_AUDIO_DEVICE_NOT_FOUND; errorMessage = error.toString(); errorCallback(new OpenViduError(errorName, errorMessage)); - } catch (error) { errorName = OpenViduErrorName.INPUT_VIDEO_DEVICE_NOT_FOUND; errorMessage = error.toString(); @@ -538,12 +558,13 @@ export class Publisher extends StreamManager { break; case 'notallowederror': - errorName = this.stream.isSendScreen() ? OpenViduErrorName.SCREEN_CAPTURE_DENIED : OpenViduErrorName.DEVICE_ACCESS_DENIED; + errorName = this.stream.isSendScreen() + ? OpenViduErrorName.SCREEN_CAPTURE_DENIED + : OpenViduErrorName.DEVICE_ACCESS_DENIED; errorMessage = error.toString(); errorCallback(new OpenViduError(errorName, errorMessage)); break; case 'overconstrainederror': - try { const mediaStream = await navigator.mediaDevices.getUserMedia({ audio: false, @@ -554,20 +575,27 @@ export class Publisher extends StreamManager { }); if (error.constraint.toLowerCase() === 'deviceid') { errorName = OpenViduErrorName.INPUT_AUDIO_DEVICE_NOT_FOUND; - errorMessage = "Audio input device with deviceId '" + ((constraints.audio).deviceId!!).exact + "' not found"; + errorMessage = + "Audio input device with deviceId '" + + ((constraints.audio).deviceId!!).exact + + "' not found"; } else { errorName = OpenViduErrorName.PUBLISHER_PROPERTIES_ERROR; - errorMessage = "Audio input device doesn't support the value passed for constraint '" + error.constraint + "'"; + errorMessage = + "Audio input device doesn't support the value passed for constraint '" + error.constraint + "'"; } errorCallback(new OpenViduError(errorName, errorMessage)); - } catch (error) { if (error.constraint.toLowerCase() === 'deviceid') { errorName = OpenViduErrorName.INPUT_VIDEO_DEVICE_NOT_FOUND; - errorMessage = "Video input device with deviceId '" + ((constraints.video).deviceId!!).exact + "' not found"; + errorMessage = + "Video input device with deviceId '" + + ((constraints.video).deviceId!!).exact + + "' not found"; } else { errorName = OpenViduErrorName.PUBLISHER_PROPERTIES_ERROR; - errorMessage = "Video input device doesn't support the value passed for constraint '" + error.constraint + "'"; + errorMessage = + "Video input device doesn't support the value passed for constraint '" + error.constraint + "'"; } errorCallback(new OpenViduError(errorName, errorMessage)); } @@ -585,13 +613,15 @@ export class Publisher extends StreamManager { errorCallback(new OpenViduError(errorName, errorMessage)); break; } - } + }; try { - const myConstraints = await this.openvidu.generateMediaConstraints(this.properties); - if (!!myConstraints.videoTrack && !!myConstraints.audioTrack || - !!myConstraints.audioTrack && myConstraints.constraints?.video === false || - !!myConstraints.videoTrack && myConstraints.constraints?.audio === false) { + const myConstraints = await this.openvidu.generateMediaConstraints(this.properties); + if ( + (!!myConstraints.videoTrack && !!myConstraints.audioTrack) || + (!!myConstraints.audioTrack && myConstraints.constraints?.video === false) || + (!!myConstraints.videoTrack && myConstraints.constraints?.audio === false) + ) { // No need to call getUserMedia at all. MediaStreamTracks already provided successCallback(this.openvidu.addAlreadyProvidedTracks(myConstraints, new MediaStream(), this.stream)); } else { @@ -603,7 +633,7 @@ export class Publisher extends StreamManager { }; this.stream.setOutboundStreamOptions(outboundStreamOptions); - const definedAudioConstraint = ((constraints.audio === undefined) ? true : constraints.audio); + const definedAudioConstraint = constraints.audio === undefined ? true : constraints.audio; constraintsAux.audio = this.stream.isSendScreen() ? false : definedAudioConstraint; constraintsAux.video = constraints.video; startTime = Date.now(); @@ -664,9 +694,8 @@ export class Publisher extends StreamManager { * and then try to use MediaStreamTrack.getSettingsMethod(). If not available, then we * use the HTMLVideoElement properties videoWidth and videoHeight */ - getVideoDimensions(): Promise<{ width: number, height: number }> { + getVideoDimensions(): Promise<{ width: number; height: number }> { return new Promise((resolve, reject) => { - // Ionic iOS and Safari iOS supposedly require the video element to actually exist inside the DOM const requiresDomInsertion: boolean = platform.isIonicIos() || platform.isIOSWithSafari(); @@ -692,7 +721,7 @@ export class Publisher extends StreamManager { } return resolve({ width, height }); - } + }; if (this.videoReference.readyState >= 1) { // The video already has metadata available @@ -739,7 +768,14 @@ export class Publisher extends StreamManager { this.videoReference.muted = true; this.videoReference.autoplay = true; this.videoReference.controls = false; - if (platform.isSafariBrowser() || (platform.isIPhoneOrIPad() && (platform.isChromeMobileBrowser() || platform.isEdgeMobileBrowser() || platform.isOperaMobileBrowser() || platform.isFirefoxMobileBrowser()))) { + if ( + platform.isSafariBrowser() || + (platform.isIPhoneOrIPad() && + (platform.isChromeMobileBrowser() || + platform.isEdgeMobileBrowser() || + platform.isOperaMobileBrowser() || + platform.isFirefoxMobileBrowser())) + ) { this.videoReference.playsInline = true; } this.stream.setMediaStream(mediaStream); @@ -753,7 +789,9 @@ export class Publisher extends StreamManager { * @hidden */ replaceTrackInMediaStream(track: MediaStreamTrack, updateLastConstraints: boolean): void { - const mediaStream: MediaStream = this.stream.displayMyRemote() ? this.stream.localMediaStreamWhenSubscribedToRemote! : this.stream.getMediaStream(); + const mediaStream: MediaStream = this.stream.displayMyRemote() + ? this.stream.localMediaStreamWhenSubscribedToRemote! + : this.stream.getMediaStream(); let removedTrack: MediaStreamTrack; if (track.kind === 'video') { removedTrack = mediaStream.getVideoTracks()[0]; @@ -773,12 +811,12 @@ export class Publisher extends StreamManager { }; if (track.kind === 'video' && updateLastConstraints) { this.openvidu.sendNewVideoDimensionsIfRequired(this, 'trackReplaced', 50, 30); - this.openvidu.sendTrackChangedEvent(this,'trackReplaced', trackInfo.oldLabel, trackInfo.newLabel, 'videoActive'); - if(this.stream.isLocalStreamPublished) { + this.openvidu.sendTrackChangedEvent(this, 'trackReplaced', trackInfo.oldLabel, trackInfo.newLabel, 'videoActive'); + if (this.stream.isLocalStreamPublished) { this.session.sendVideoData(this.stream.streamManager, 5, true, 5); } - } else if(track.kind === 'audio' && updateLastConstraints) { - this.openvidu.sendTrackChangedEvent(this,'trackReplaced', trackInfo.oldLabel, trackInfo.newLabel, 'audioActive'); + } else if (track.kind === 'audio' && updateLastConstraints) { + this.openvidu.sendTrackChangedEvent(this, 'trackReplaced', trackInfo.oldLabel, trackInfo.newLabel, 'audioActive'); } if (track.kind === 'audio') { this.stream.disableHarkSpeakingEvent(false); @@ -798,7 +836,7 @@ export class Publisher extends StreamManager { private clearPermissionDialogTimer(startTime: number, waitTime: number): void { clearTimeout(this.permissionDialogTimeout); - if ((Date.now() - startTime) > waitTime) { + if (Date.now() - startTime > waitTime) { // Permission dialog was shown and now is closed this.emitEvent('accessDialogClosed', []); } @@ -808,19 +846,18 @@ export class Publisher extends StreamManager { const senders: RTCRtpSender[] = this.stream.getRTCPeerConnection().getSenders(); let sender: RTCRtpSender | undefined; if (track.kind === 'video') { - sender = senders.find(s => !!s.track && s.track.kind === 'video'); + sender = senders.find((s) => !!s.track && s.track.kind === 'video'); if (!sender) { - throw new Error('There\'s no replaceable track for that kind of MediaStreamTrack in this Publisher object'); + throw new Error("There's no replaceable track for that kind of MediaStreamTrack in this Publisher object"); } } else if (track.kind === 'audio') { - sender = senders.find(s => !!s.track && s.track.kind === 'audio'); + sender = senders.find((s) => !!s.track && s.track.kind === 'audio'); if (!sender) { - throw new Error('There\'s no replaceable track for that kind of MediaStreamTrack in this Publisher object'); + throw new Error("There's no replaceable track for that kind of MediaStreamTrack in this Publisher object"); } } else { throw new Error('Unknown track kind ' + track.kind); } await (sender as RTCRtpSender).replaceTrack(track); } - } diff --git a/openvidu-browser/src/OpenVidu/Session.ts b/openvidu-browser/src/OpenVidu/Session.ts index d82aa6b7..39ac6dbc 100644 --- a/openvidu-browser/src/OpenVidu/Session.ts +++ b/openvidu-browser/src/OpenVidu/Session.ts @@ -70,7 +70,6 @@ let platform: PlatformUtils; * See available event listeners at [[SessionEventMap]]. */ export class Session extends EventDispatcher { - /** * Local connection to the Session. This object is defined only after [[Session.connect]] has been successfully executed, and can be retrieved subscribing to `connectionCreated` event */ @@ -164,7 +163,6 @@ export class Session extends EventDispatcher { */ connect(token: string, metadata?: any): Promise { return new Promise((resolve, reject) => { - this.processToken(token); if (this.openvidu.checkSystemRequirements()) { @@ -176,9 +174,20 @@ export class Session extends EventDispatcher { }; this.connectAux(token) .then(() => resolve()) - .catch(error => reject(error)); + .catch((error) => reject(error)); } else { - return reject(new OpenViduError(OpenViduErrorName.BROWSER_NOT_SUPPORTED, 'Browser ' + platform.getName() + ' (version ' + platform.getVersion() + ') for ' + platform.getFamily() + ' is not supported in OpenVidu')); + return reject( + new OpenViduError( + OpenViduErrorName.BROWSER_NOT_SUPPORTED, + 'Browser ' + + platform.getName() + + ' (version ' + + platform.getVersion() + + ') for ' + + platform.getFamily() + + ' is not supported in OpenVidu' + ) + ); } }); } @@ -216,8 +225,17 @@ export class Session extends EventDispatcher { subscribe(stream: Stream, targetElement: string | HTMLElement | undefined): Subscriber; subscribe(stream: Stream, targetElement: string | HTMLElement | undefined, properties: SubscriberProperties): Subscriber; - subscribe(stream: Stream, targetElement: string | HTMLElement | undefined, completionHandler: (error: Error | undefined) => void): Subscriber; - subscribe(stream: Stream, targetElement: string | HTMLElement | undefined, properties: SubscriberProperties, completionHandler: (error: Error | undefined) => void): Subscriber; + subscribe( + stream: Stream, + targetElement: string | HTMLElement | undefined, + completionHandler: (error: Error | undefined) => void + ): Subscriber; + subscribe( + stream: Stream, + targetElement: string | HTMLElement | undefined, + properties: SubscriberProperties, + completionHandler: (error: Error | undefined) => void + ): Subscriber; /** * Subscribes to a `stream`, adding a new HTML video element to DOM with `subscriberProperties` settings. This method is usually called in the callback of `streamCreated` event. @@ -234,13 +252,23 @@ export class Session extends EventDispatcher { * You can always call method [[Subscriber.addVideoElement]] or [[Subscriber.createVideoElement]] to manage the video elements on your own (see [Manage video players](/en/stable/cheatsheet/manage-videos) section) * @param completionHandler `error` parameter is null if `subscribe` succeeds, and is defined if it fails. */ - subscribe(stream: Stream, targetElement: string | HTMLElement | undefined, param3?: ((error: Error | undefined) => void) | SubscriberProperties, param4?: ((error: Error | undefined) => void)): Subscriber { + subscribe( + stream: Stream, + targetElement: string | HTMLElement | undefined, + param3?: ((error: Error | undefined) => void) | SubscriberProperties, + param4?: (error: Error | undefined) => void + ): Subscriber { let properties: SubscriberProperties = {}; if (!!param3 && typeof param3 !== 'function') { properties = { - insertMode: (typeof param3.insertMode !== 'undefined') ? ((typeof param3.insertMode === 'string') ? VideoInsertMode[param3.insertMode] : properties.insertMode) : VideoInsertMode.APPEND, - subscribeToAudio: (typeof param3.subscribeToAudio !== 'undefined') ? param3.subscribeToAudio : true, - subscribeToVideo: (typeof param3.subscribeToVideo !== 'undefined') ? param3.subscribeToVideo : true + insertMode: + typeof param3.insertMode !== 'undefined' + ? typeof param3.insertMode === 'string' + ? VideoInsertMode[param3.insertMode] + : properties.insertMode + : VideoInsertMode.APPEND, + subscribeToAudio: typeof param3.subscribeToAudio !== 'undefined' ? param3.subscribeToAudio : true, + subscribeToVideo: typeof param3.subscribeToVideo !== 'undefined' ? param3.subscribeToVideo : true }; } else { properties = { @@ -251,7 +279,7 @@ export class Session extends EventDispatcher { } let completionHandler: ((error: Error | undefined) => void) | undefined = undefined; - if (!!param3 && (typeof param3 === 'function')) { + if (!!param3 && typeof param3 === 'function') { completionHandler = param3; } else if (!!param4) { completionHandler = param4; @@ -266,14 +294,15 @@ export class Session extends EventDispatcher { logger.info('Subscribing to ' + stream.connection.connectionId); - stream.subscribe() + stream + .subscribe() .then(() => { logger.info('Subscribed correctly to ' + stream.connection.connectionId); if (completionHandler !== undefined) { completionHandler(undefined); } }) - .catch(error => { + .catch((error) => { if (completionHandler !== undefined) { completionHandler(error); } @@ -285,7 +314,6 @@ export class Session extends EventDispatcher { return subscriber; } - /** * Promisified version of [[Session.subscribe]] */ @@ -294,7 +322,6 @@ export class Session extends EventDispatcher { subscribeAsync(stream: Stream, targetElement: string | HTMLElement, properties?: SubscriberProperties): Promise { return new Promise((resolve, reject) => { - if (!this.sessionConnected()) { return reject(this.notConnectedError()); } @@ -314,11 +341,9 @@ export class Session extends EventDispatcher { } else { subscriber = this.subscribe(stream, targetElement, callback); } - }); } - /** * Unsubscribes from `subscriber`, automatically removing its associated HTML video elements. * @@ -330,9 +355,7 @@ export class Session extends EventDispatcher { * See [[VideoElementEvent]] to learn more */ unsubscribe(subscriber: Subscriber): Promise { - return new Promise((resolve, reject) => { - if (!this.sessionConnected()) { return reject(this.notConnectedError()); } else { @@ -360,7 +383,6 @@ export class Session extends EventDispatcher { }); } - /** * Publishes to the Session the Publisher object * @@ -376,7 +398,6 @@ export class Session extends EventDispatcher { */ publish(publisher: Publisher): Promise { return new Promise((resolve, reject) => { - if (!this.sessionConnected()) { return reject(this.notConnectedError()); } @@ -387,30 +408,33 @@ export class Session extends EventDispatcher { if (!publisher.stream.publishedOnce) { // 'Session.unpublish(Publisher)' has NOT been called this.connection.addStream(publisher.stream); - publisher.stream.publish() + publisher.stream + .publish() .then(() => { this.sendVideoData(publisher, 8, true, 5); return resolve(); }) - .catch(error => reject(error)); + .catch((error) => reject(error)); } else { // 'Session.unpublish(Publisher)' has been called. Must initialize again Publisher - publisher.initialize() + publisher + .initialize() .then(() => { this.connection.addStream(publisher.stream); publisher.reestablishStreamPlayingEvent(); - publisher.stream.publish() + publisher.stream + .publish() .then(() => { this.sendVideoData(publisher, 8, true, 5); return resolve(); }) - .catch(error => reject(error)); - }).catch(error => reject(error)); + .catch((error) => reject(error)); + }) + .catch((error) => reject(error)); } }); } - /** * Unpublishes from the Session the Publisher object. * @@ -431,11 +455,9 @@ export class Session extends EventDispatcher { * See [[StreamEvent]] and [[VideoElementEvent]] to learn more. */ unpublish(publisher: Publisher): Promise { - return new Promise((resolve, reject) => { - if (!this.sessionConnected()) { - throw this.notConnectedError() + throw this.notConnectedError(); } const stream = publisher.stream; @@ -443,10 +465,13 @@ export class Session extends EventDispatcher { if (!stream.connection) { return reject(new Error('The associated Connection object of this Publisher is null')); } else if (stream.connection !== this.connection) { - return reject(new Error('The associated Connection object of this Publisher is not your local Connection. ' + - "Only moderators can force unpublish on remote Streams via 'forceUnpublish' method")); + return reject( + new Error( + 'The associated Connection object of this Publisher is not your local Connection. ' + + "Only moderators can force unpublish on remote Streams via 'forceUnpublish' method" + ) + ); } else { - logger.info('Unpublishing local media (' + stream.connection.connectionId + ')'); this.openvidu.sendRequest('unpublishVideo', (error, response) => { @@ -473,7 +498,6 @@ export class Session extends EventDispatcher { }); } - /** * Forces some user to leave the session * @@ -493,33 +517,32 @@ export class Session extends EventDispatcher { */ forceDisconnect(connection: Connection): Promise { return new Promise((resolve, reject) => { - if (!this.sessionConnected()) { return reject(this.notConnectedError()); } logger.info('Forcing disconnect for connection ' + connection.connectionId); - this.openvidu.sendRequest( - 'forceDisconnect', - { connectionId: connection.connectionId }, - (error, response) => { - if (error) { - logger.error('Error forcing disconnect for Connection ' + connection.connectionId, error); - if (error.code === 401) { - return reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to force a disconnection")); - } else { - return reject(error); - } + this.openvidu.sendRequest('forceDisconnect', { connectionId: connection.connectionId }, (error, response) => { + if (error) { + logger.error('Error forcing disconnect for Connection ' + connection.connectionId, error); + if (error.code === 401) { + return reject( + new OpenViduError( + OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, + "You don't have permissions to force a disconnection" + ) + ); } else { - logger.info('Forcing disconnect correctly for Connection ' + connection.connectionId); - return resolve(); + return reject(error); } + } else { + logger.info('Forcing disconnect correctly for Connection ' + connection.connectionId); + return resolve(); } - ); + }); }); } - /** * Forces some user to unpublish a Stream * @@ -537,33 +560,32 @@ export class Session extends EventDispatcher { */ forceUnpublish(stream: Stream): Promise { return new Promise((resolve, reject) => { - if (!this.sessionConnected()) { return reject(this.notConnectedError()); } logger.info('Forcing unpublish for stream ' + stream.streamId); - this.openvidu.sendRequest( - 'forceUnpublish', - { streamId: stream.streamId }, - (error, response) => { - if (error) { - logger.error('Error forcing unpublish for Stream ' + stream.streamId, error); - if (error.code === 401) { - return reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to force an unpublishing")); - } else { - return reject(error); - } + this.openvidu.sendRequest('forceUnpublish', { streamId: stream.streamId }, (error, response) => { + if (error) { + logger.error('Error forcing unpublish for Stream ' + stream.streamId, error); + if (error.code === 401) { + return reject( + new OpenViduError( + OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, + "You don't have permissions to force an unpublishing" + ) + ); } else { - logger.info('Forcing unpublish correctly for Stream ' + stream.streamId); - return resolve(); + return reject(error); } + } else { + logger.info('Forcing unpublish correctly for Stream ' + stream.streamId); + return resolve(); } - ); + }); }); } - /** * Sends one signal. `signal` object has the following optional properties: * ```json @@ -578,7 +600,6 @@ export class Session extends EventDispatcher { /* tslint:disable:no-string-literal */ signal(signal: SignalOptions): Promise { return new Promise((resolve, reject) => { - if (!this.sessionConnected()) { return reject(this.notConnectedError()); } @@ -587,7 +608,7 @@ export class Session extends EventDispatcher { if (signal.to && signal.to.length > 0) { const connectionIds: string[] = []; - signal.to.forEach(connection => { + signal.to.forEach((connection) => { if (!!connection.connectionId) { connectionIds.push(connection.connectionId); } @@ -607,30 +628,32 @@ export class Session extends EventDispatcher { } signalMessage['type'] = typeAux; - this.openvidu.sendRequest('sendMessage', { - message: JSON.stringify(signalMessage) - }, (error, response) => { - if (!!error) { - return reject(error); - } else { - return resolve(); + this.openvidu.sendRequest( + 'sendMessage', + { + message: JSON.stringify(signalMessage) + }, + (error, response) => { + if (!!error) { + return reject(error); + } else { + return resolve(); + } } - }); + ); }); } /* tslint:enable:no-string-literal */ - /** * See [[EventDispatcher.on]] */ on(type: K, handler: (event: SessionEventMap[K]) => void): this { - super.onAux(type, "Event '" + type + "' triggered by 'Session'", handler); if (type === 'publisherStartSpeaking') { // If there are already available remote streams with audio, enable hark 'speaking' event in all of them - this.remoteConnections.forEach(remoteConnection => { + this.remoteConnections.forEach((remoteConnection) => { if (!!remoteConnection.stream?.hasAudio) { remoteConnection.stream.enableHarkSpeakingEvent(); } @@ -642,7 +665,7 @@ export class Session extends EventDispatcher { } if (type === 'publisherStopSpeaking') { // If there are already available remote streams with audio, enable hark 'stopped_speaking' event in all of them - this.remoteConnections.forEach(remoteConnection => { + this.remoteConnections.forEach((remoteConnection) => { if (!!remoteConnection.stream?.hasAudio) { remoteConnection.stream.enableHarkStoppedSpeakingEvent(); } @@ -656,17 +679,15 @@ export class Session extends EventDispatcher { return this; } - /** * See [[EventDispatcher.once]] */ once(type: K, handler: (event: SessionEventMap[K]) => void): this { - super.onceAux(type, "Event '" + type + "' triggered once by 'Session'", handler); if (type === 'publisherStartSpeaking') { // If there are already available remote streams with audio, enable hark 'speaking' event (once) in all of them once - this.remoteConnections.forEach(remoteConnection => { + this.remoteConnections.forEach((remoteConnection) => { if (!!remoteConnection.stream?.hasAudio) { remoteConnection.stream.enableOnceHarkSpeakingEvent(); } @@ -678,7 +699,7 @@ export class Session extends EventDispatcher { } if (type === 'publisherStopSpeaking') { // If there are already available remote streams with audio, enable hark 'stopped_speaking' event (once) in all of them once - this.remoteConnections.forEach(remoteConnection => { + this.remoteConnections.forEach((remoteConnection) => { if (!!remoteConnection.stream?.hasAudio) { remoteConnection.stream.enableOnceHarkStoppedSpeakingEvent(); } @@ -692,18 +713,16 @@ export class Session extends EventDispatcher { return this; } - /** * See [[EventDispatcher.off]] */ off(type: K, handler?: (event: SessionEventMap[K]) => void): this { - super.offAux(type, handler); if (type === 'publisherStartSpeaking') { // Check if Session object still has some listener for the event if (!this.anySpeechEventListenerEnabled('publisherStartSpeaking', false)) { - this.remoteConnections.forEach(remoteConnection => { + this.remoteConnections.forEach((remoteConnection) => { if (!!remoteConnection.stream?.streamManager) { // Check if Subscriber object still has some listener for the event if (!this.anySpeechEventListenerEnabled('publisherStartSpeaking', false, remoteConnection.stream.streamManager)) { @@ -722,7 +741,7 @@ export class Session extends EventDispatcher { if (type === 'publisherStopSpeaking') { // Check if Session object still has some listener for the event if (!this.anySpeechEventListenerEnabled('publisherStopSpeaking', false)) { - this.remoteConnections.forEach(remoteConnection => { + this.remoteConnections.forEach((remoteConnection) => { if (!!remoteConnection.stream?.streamManager) { // Check if Subscriber object still has some listener for the event if (!this.anySpeechEventListenerEnabled('publisherStopSpeaking', false, remoteConnection.stream.streamManager)) { @@ -741,7 +760,6 @@ export class Session extends EventDispatcher { return this; } - /* Hidden methods */ /** @@ -750,10 +768,10 @@ export class Session extends EventDispatcher { onParticipantJoined(event: RemoteConnectionOptions): void { // Connection shouldn't exist this.getConnection(event.id, '') - .then(connection => { + .then((connection) => { logger.warn('Connection ' + connection.connectionId + ' already exists in connections list'); }) - .catch(openViduError => { + .catch((openViduError) => { const connection = new Connection(this, event); this.remoteConnections.set(event.id, connection); this.ee.emitEvent('connectionCreated', [new ConnectionEvent(false, this, 'connectionCreated', connection, '')]); @@ -763,21 +781,24 @@ 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; + onParticipantLeft(event: { connectionId: string; reason: string }): void { + 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 => { + 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); }); } @@ -786,9 +807,7 @@ export class Session extends EventDispatcher { * @hidden */ onParticipantPublished(event: RemoteConnectionOptions): void { - const afterConnectionFound = (connection) => { - this.remoteConnections.set(connection.connectionId, connection); if (!this.remoteStreamsCreated.get(connection.stream.streamId)) { @@ -807,7 +826,7 @@ export class Session extends EventDispatcher { let connection: Connection; this.getRemoteConnection(event.id, 'onParticipantPublished') - .then(con => { + .then((con) => { // Update existing Connection connection = con; event.metadata = con.data; @@ -815,7 +834,7 @@ export class Session extends EventDispatcher { connection.initRemoteStreams(event.streams); afterConnectionFound(connection); }) - .catch(openViduError => { + .catch((openViduError) => { // Create new Connection connection = new Connection(this, event); afterConnectionFound(connection); @@ -825,15 +844,14 @@ export class Session extends EventDispatcher { /** * @hidden */ - onParticipantUnpublished(event: { connectionId: string, reason: string }): void { + onParticipantUnpublished(event: { connectionId: string; reason: string }): void { if (event.connectionId === this.connection.connectionId) { // Your stream has been forcedly unpublished from the session this.stopPublisherStream(event.reason); } else { this.getRemoteConnection(event.connectionId, 'onParticipantUnpublished') - .then(connection => { - + .then((connection) => { const streamEvent = new StreamEvent(true, this, 'streamDestroyed', connection.stream!, event.reason); this.ee.emitEvent('streamDestroyed', [streamEvent]); streamEvent.callDefaultBehavior(); @@ -844,7 +862,7 @@ export class Session extends EventDispatcher { connection.removeStream(streamId); }) - .catch(openViduError => { + .catch((openViduError) => { logger.error(openViduError); }); } @@ -853,7 +871,7 @@ export class Session extends EventDispatcher { /** * @hidden */ - onParticipantEvicted(event: { connectionId: string, reason: string }): void { + onParticipantEvicted(event: { connectionId: string; reason: string }): void { if (event.connectionId === this.connection.connectionId) { // You have been evicted from the session if (!!this.sessionId && !this.connection.disposed) { @@ -865,24 +883,30 @@ export class Session extends EventDispatcher { /** * @hidden */ - onNewMessage(event: { type?: string, data?: string, from?: string }): void { - + onNewMessage(event: { type?: string; data?: string; from?: string }): void { logger.info('New signal: ' + JSON.stringify(event)); const strippedType = !!event.type ? event.type.replace(/^(signal:)/, '') : undefined; if (!!event.from) { // Signal sent by other client - this.getConnection(event.from, "Connection '" + event.from + "' unknown when 'onNewMessage'. Existing remote connections: " - + JSON.stringify(this.remoteConnections.keys()) + '. Existing local connection: ' + this.connection.connectionId) + this.getConnection( + event.from, + "Connection '" + + event.from + + "' unknown when 'onNewMessage'. Existing remote connections: " + + JSON.stringify(this.remoteConnections.keys()) + + '. Existing local connection: ' + + this.connection.connectionId + ) - .then(connection => { + .then((connection) => { this.ee.emitEvent('signal', [new SignalEvent(this, strippedType, event.data, connection)]); if (!!event.type && event.type !== 'signal') { this.ee.emitEvent(event.type, [new SignalEvent(this, strippedType, event.data, connection)]); } }) - .catch(openViduError => { + .catch((openViduError) => { logger.error(openViduError); }); } else { @@ -897,8 +921,7 @@ export class Session extends EventDispatcher { /** * @hidden */ - onStreamPropertyChanged(event: { connectionId: string, streamId: string, property: string, newValue: any, reason: string }): void { - + onStreamPropertyChanged(event: { connectionId: string; streamId: string; property: string; newValue: any; reason: string }): void { const callback = (connection: Connection) => { if (!!connection.stream && connection.stream.streamId === event.streamId) { const stream = connection.stream; @@ -921,7 +944,7 @@ export class Session extends EventDispatcher { break; case 'filter': oldValue = stream.filter; - event.newValue = (Object.keys(event.newValue).length > 0) ? event.newValue : undefined; + event.newValue = Object.keys(event.newValue).length > 0 ? event.newValue : undefined; if (event.newValue !== undefined) { stream.filter = new Filter(event.newValue.type, event.newValue.options); stream.filter.stream = stream; @@ -934,12 +957,22 @@ export class Session extends EventDispatcher { event.newValue = stream.filter; break; } - this.ee.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this, stream, event.property, event.newValue, oldValue, event.reason)]); + this.ee.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent(this, stream, event.property, event.newValue, oldValue, event.reason) + ]); if (!!stream.streamManager) { - stream.streamManager.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(stream.streamManager, stream, event.property, event.newValue, oldValue, event.reason)]); + stream.streamManager.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent(stream.streamManager, stream, event.property, event.newValue, oldValue, event.reason) + ]); } } else { - logger.error("No stream with streamId '" + event.streamId + "' found for connection '" + event.connectionId + "' on 'streamPropertyChanged' event"); + logger.error( + "No stream with streamId '" + + event.streamId + + "' found for connection '" + + event.connectionId + + "' on 'streamPropertyChanged' event" + ); } }; @@ -948,10 +981,10 @@ export class Session extends EventDispatcher { callback(this.connection); } else { this.getRemoteConnection(event.connectionId, 'onStreamPropertyChanged') - .then(connection => { + .then((connection) => { callback(connection); }) - .catch(openViduError => { + .catch((openViduError) => { logger.error(openViduError); }); } @@ -960,7 +993,7 @@ export class Session extends EventDispatcher { /** * @hidden */ - onConnectionPropertyChanged(event: { property: string, newValue: any }): void { + onConnectionPropertyChanged(event: { property: string; newValue: any }): void { let oldValue; switch (event.property) { case 'role': @@ -975,21 +1008,27 @@ export class Session extends EventDispatcher { this.connection.localOptions!.record = event.newValue; break; } - this.ee.emitEvent('connectionPropertyChanged', [new ConnectionPropertyChangedEvent(this, this.connection, event.property, event.newValue, oldValue)]); + this.ee.emitEvent('connectionPropertyChanged', [ + new ConnectionPropertyChangedEvent(this, this.connection, event.property, event.newValue, oldValue) + ]); } /** * @hidden */ - onNetworkQualityLevelChangedChanged(event: { connectionId: string, newValue: number, oldValue: number }): void { + onNetworkQualityLevelChangedChanged(event: { connectionId: string; newValue: number; oldValue: number }): void { if (event.connectionId === this.connection.connectionId) { - this.ee.emitEvent('networkQualityLevelChanged', [new NetworkQualityLevelChangedEvent(this, event.newValue, event.oldValue, this.connection)]); + this.ee.emitEvent('networkQualityLevelChanged', [ + new NetworkQualityLevelChangedEvent(this, event.newValue, event.oldValue, this.connection) + ]); } else { this.getConnection(event.connectionId, 'Connection not found for connectionId ' + event.connectionId) .then((connection: Connection) => { - this.ee.emitEvent('networkQualityLevelChanged', [new NetworkQualityLevelChangedEvent(this, event.newValue, event.oldValue, connection)]); + this.ee.emitEvent('networkQualityLevelChanged', [ + new NetworkQualityLevelChangedEvent(this, event.newValue, event.oldValue, connection) + ]); }) - .catch(openViduError => { + .catch((openViduError) => { logger.error(openViduError); }); } @@ -998,26 +1037,44 @@ export class Session extends EventDispatcher { /** * @hidden */ - recvIceCandidate(event: { senderConnectionId: string, endpointName: string, sdpMLineIndex: number, sdpMid: string, candidate: string }): void { + recvIceCandidate(event: { + senderConnectionId: string; + endpointName: string; + sdpMLineIndex: number; + sdpMid: string; + candidate: string; + }): void { // The event contains fields that can be used to obtain a proper candidate, // using the RTCIceCandidate constructor: // https://w3c.github.io/webrtc-pc/#dom-rtcicecandidate-constructor const candidateInit: RTCIceCandidateInit = { candidate: event.candidate, sdpMLineIndex: event.sdpMLineIndex, - sdpMid: event.sdpMid, + sdpMid: event.sdpMid }; const iceCandidate = new RTCIceCandidate(candidateInit); - this.getConnection(event.senderConnectionId, 'Connection not found for connectionId ' + event.senderConnectionId + ' owning endpoint ' + event.endpointName + '. Ice candidate will be ignored: ' + iceCandidate) - .then(connection => { + this.getConnection( + event.senderConnectionId, + 'Connection not found for connectionId ' + + event.senderConnectionId + + ' owning endpoint ' + + event.endpointName + + '. Ice candidate will be ignored: ' + + iceCandidate + ) + .then((connection) => { const stream: Stream = connection.stream!; - stream.getWebRtcPeer().addIceCandidate(iceCandidate).catch(error => { - logger.error('Error adding candidate for ' + stream!.streamId - + ' stream of endpoint ' + event.endpointName + ': ' + error); - }); + stream + .getWebRtcPeer() + .addIceCandidate(iceCandidate) + .catch((error) => { + logger.error( + 'Error adding candidate for ' + stream!.streamId + ' stream of endpoint ' + event.endpointName + ': ' + error + ); + }); }) - .catch(openViduError => { + .catch((openViduError) => { logger.error(openViduError); }); } @@ -1029,9 +1086,11 @@ export class Session extends EventDispatcher { logger.info('Session closed: ' + JSON.stringify(msg)); const s = msg.sessionId; if (s !== undefined) { - this.ee.emitEvent('session-closed', [{ - session: s - }]); + this.ee.emitEvent('session-closed', [ + { + session: s + } + ]); } else { logger.warn('Session undefined on session closed', msg); } @@ -1063,9 +1122,11 @@ export class Session extends EventDispatcher { logger.error('Media error: ' + JSON.stringify(event)); const err = event.error; if (err) { - this.ee.emitEvent('error-media', [{ - error: err - }]); + this.ee.emitEvent('error-media', [ + { + error: err + } + ]); } else { logger.warn('Received undefined media error:', event); } @@ -1074,46 +1135,53 @@ export class Session extends EventDispatcher { /** * @hidden */ - onRecordingStarted(event: { id: string, name: string }): void { + onRecordingStarted(event: { id: string; name: string }): void { this.ee.emitEvent('recordingStarted', [new RecordingEvent(this, 'recordingStarted', event.id, event.name)]); } /** * @hidden */ - onRecordingStopped(event: { id: string, name: string, reason: string }): void { + onRecordingStopped(event: { id: string; name: string; reason: string }): void { this.ee.emitEvent('recordingStopped', [new RecordingEvent(this, 'recordingStopped', event.id, event.name, event.reason)]); } /** * @hidden */ - onFilterEventDispatched(event: { connectionId: string, streamId: string, filterType: string, eventType: string, data: string }): void { + onFilterEventDispatched(event: { connectionId: string; streamId: string; filterType: string; eventType: string; data: string }): void { const connectionId: string = event.connectionId; - this.getConnection(connectionId, 'No connection found for connectionId ' + connectionId) - .then(connection => { - logger.info(`Filter event of type "${event.eventType}" dispatched`); - const stream: Stream = connection.stream!; - if (!stream || !stream.filter) { - return logger.error(`Filter event of type "${event.eventType}" dispatched for stream ${stream.streamId} but there is no ${!stream ? 'stream' : 'filter'} defined`); - } - const eventHandler = stream.filter.handlers.get(event.eventType); - if (!eventHandler || typeof eventHandler !== 'function') { - const actualHandlers: string[] = Array.from(stream.filter.handlers.keys()); - return logger.error(`Filter event of type "${event.eventType}" not handled or not a function! Active filter events: ${actualHandlers.join(',')}`); - } else { - eventHandler.call(this, new FilterEvent(stream.filter, event.eventType, event.data)); - } - }); + this.getConnection(connectionId, 'No connection found for connectionId ' + connectionId).then((connection) => { + logger.info(`Filter event of type "${event.eventType}" dispatched`); + const stream: Stream = connection.stream!; + if (!stream || !stream.filter) { + return logger.error( + `Filter event of type "${event.eventType}" dispatched for stream ${stream.streamId} but there is no ${ + !stream ? 'stream' : 'filter' + } defined` + ); + } + const eventHandler = stream.filter.handlers.get(event.eventType); + if (!eventHandler || typeof eventHandler !== 'function') { + const actualHandlers: string[] = Array.from(stream.filter.handlers.keys()); + return logger.error( + `Filter event of type "${event.eventType}" not handled or not a function! Active filter events: ${actualHandlers.join( + ',' + )}` + ); + } else { + eventHandler.call(this, new FilterEvent(stream.filter, event.eventType, event.data)); + } + }); } /** * @hidden */ - onForciblyReconnectSubscriber(event: { connectionId: string, streamId: string, sdpOffer: string }): Promise { + onForciblyReconnectSubscriber(event: { connectionId: string; streamId: string; sdpOffer: string }): Promise { return new Promise((resolve, reject) => { this.getRemoteConnection(event.connectionId, 'onForciblyReconnectSubscriber') - .then(connection => { + .then((connection) => { if (!!connection.stream && connection.stream.streamId === event.streamId) { const stream = connection.stream; @@ -1135,7 +1203,7 @@ export class Session extends EventDispatcher { const eventAux = stream.reconnectionEventEmitter!['onForciblyReconnectSubscriberLastEvent']; delete stream.reconnectionEventEmitter!['onForciblyReconnectSubscriberLastEvent']; this.onForciblyReconnectSubscriber(eventAux); - } + }; stream.reconnectionEventEmitter!.once('success', () => { callback(); }); @@ -1146,16 +1214,28 @@ export class Session extends EventDispatcher { return; } - stream.completeWebRtcPeerReceive(true, true, event.sdpOffer) + stream + .completeWebRtcPeerReceive(true, true, event.sdpOffer) .then(() => stream.finalResolveForSubscription(true, resolve)) - .catch(error => stream.finalRejectForSubscription(true, `Error while forcibly reconnecting remote stream ${event.streamId}: ${error.toString()}`, reject)); + .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"; + const errMsg = + "No stream with streamId '" + + event.streamId + + "' found for connection '" + + event.connectionId + + "' on 'streamPropertyChanged' event"; logger.error(errMsg); return reject(errMsg); } }) - .catch(openViduError => { + .catch((openViduError) => { logger.error(openViduError); return reject(openViduError); }); @@ -1175,7 +1255,7 @@ export class Session extends EventDispatcher { someReconnection = true; } // Re-establish Subscriber streams - this.remoteConnections.forEach(remoteConnection => { + this.remoteConnections.forEach((remoteConnection) => { if (!!remoteConnection.stream && remoteConnection.stream.streamIceConnectionStateBroken()) { logger.warn('Re-establishing Subscriber ' + remoteConnection.stream.streamId); remoteConnection.stream.initWebRtcPeerReceive(true); @@ -1198,7 +1278,6 @@ export class Session extends EventDispatcher { * @hidden */ leave(forced: boolean, reason: string): void { - forced = !!forced; logger.info('Leaving Session (forced=' + forced + ')'); this.stopVideoDataIntervals(); @@ -1234,7 +1313,7 @@ export class Session extends EventDispatcher { */ initializeParams(token: string) { const joinParams = { - token: (!!token) ? token : '', + token: !!token ? token : '', session: this.sessionId, platform: !!platform.getDescription() ? platform.getDescription() : 'unknown', sdkVersion: this.openvidu.libraryVersion, @@ -1250,35 +1329,47 @@ export class Session extends EventDispatcher { */ sendVideoData(streamManager: StreamManager, intervalSeconds: number = 1, doInterval: boolean = false, maxLoops: number = 1) { if ( - platform.isChromeBrowser() || platform.isChromeMobileBrowser() || platform.isOperaBrowser() || - platform.isOperaMobileBrowser() || platform.isEdgeBrowser() || platform.isEdgeMobileBrowser() || platform.isElectron() || - (platform.isSafariBrowser() && !platform.isIonicIos()) || platform.isAndroidBrowser() || - platform.isSamsungBrowser() || platform.isIonicAndroid() || platform.isIOSWithSafari() + platform.isChromeBrowser() || + platform.isChromeMobileBrowser() || + platform.isOperaBrowser() || + platform.isOperaMobileBrowser() || + platform.isEdgeBrowser() || + platform.isEdgeMobileBrowser() || + platform.isElectron() || + (platform.isSafariBrowser() && !platform.isIonicIos()) || + platform.isAndroidBrowser() || + platform.isSamsungBrowser() || + platform.isIonicAndroid() || + platform.isIOSWithSafari() ) { const obtainAndSendVideo = async () => { const pc = streamManager.stream.getRTCPeerConnection(); if (pc.connectionState === 'connected') { const statsMap = await pc.getStats(); const arr: any[] = []; - statsMap.forEach(stats => { - if (("frameWidth" in stats) && ("frameHeight" in stats) && (arr.length === 0)) { + statsMap.forEach((stats) => { + if ('frameWidth' in stats && 'frameHeight' in stats && arr.length === 0) { arr.push(stats); } }); if (arr.length > 0) { - this.openvidu.sendRequest('videoData', { - height: arr[0].frameHeight, - width: arr[0].frameWidth, - videoActive: streamManager.stream.videoActive != null ? streamManager.stream.videoActive : false, - audioActive: streamManager.stream.audioActive != null ? streamManager.stream.audioActive : false - }, (error, response) => { - if (error) { - logger.error("Error sending 'videoData' event", error); + this.openvidu.sendRequest( + 'videoData', + { + height: arr[0].frameHeight, + width: arr[0].frameWidth, + videoActive: streamManager.stream.videoActive != null ? streamManager.stream.videoActive : false, + audioActive: streamManager.stream.audioActive != null ? streamManager.stream.audioActive : false + }, + (error, response) => { + if (error) { + logger.error("Error sending 'videoData' event", error); + } } - }); + ); } } - } + }; if (doInterval) { let loops = 1; this.videoDataInterval = setInterval(() => { @@ -1294,18 +1385,30 @@ export class Session extends EventDispatcher { } } else if (platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser() || platform.isIonicIos() || platform.isReactNative()) { // Basic version for Firefox and Ionic iOS. They do not support stats - this.openvidu.sendRequest('videoData', { - height: streamManager.stream.videoDimensions?.height || 0, - width: streamManager.stream.videoDimensions?.width || 0, - videoActive: streamManager.stream.videoActive != null ? streamManager.stream.videoActive : false, - audioActive: streamManager.stream.audioActive != null ? streamManager.stream.audioActive : false - }, (error, response) => { - if (error) { - logger.error("Error sending 'videoData' event", error); + this.openvidu.sendRequest( + 'videoData', + { + height: streamManager.stream.videoDimensions?.height || 0, + width: streamManager.stream.videoDimensions?.width || 0, + videoActive: streamManager.stream.videoActive != null ? streamManager.stream.videoActive : false, + audioActive: streamManager.stream.audioActive != null ? streamManager.stream.audioActive : false + }, + (error, response) => { + if (error) { + logger.error("Error sending 'videoData' event", error); + } } - }); + ); } else { - logger.error('Browser ' + platform.getName() + ' (version ' + platform.getVersion() + ') for ' + platform.getFamily() + ' is not supported in OpenVidu for Network Quality'); + logger.error( + 'Browser ' + + platform.getName() + + ' (version ' + + platform.getVersion() + + ') for ' + + platform.getFamily() + + ' is not supported in OpenVidu for Network Quality' + ); } } @@ -1320,7 +1423,10 @@ export class Session extends EventDispatcher { * @hidden */ notConnectedError(): OpenViduError { - return new OpenViduError(OpenViduErrorName.OPENVIDU_NOT_CONNECTED, "There is no connection to the session. Method 'Session.connect' must be successfully completed first"); + return new OpenViduError( + OpenViduErrorName.OPENVIDU_NOT_CONNECTED, + "There is no connection to the session. Method 'Session.connect' must be successfully completed first" + ); } /** @@ -1329,7 +1435,7 @@ export class Session extends EventDispatcher { anySpeechEventListenerEnabled(event: string, onlyOnce: boolean, streamManager?: StreamManager): boolean { let handlersInSession = this.ee.getListeners(event); if (onlyOnce) { - handlersInSession = handlersInSession.filter(h => (h as any).once); + handlersInSession = handlersInSession.filter((h) => (h as any).once); } let listenersInSession = handlersInSession.length; if (listenersInSession > 0) return true; @@ -1337,7 +1443,7 @@ export class Session extends EventDispatcher { if (!!streamManager) { let handlersInStreamManager = streamManager.ee.getListeners(event); if (onlyOnce) { - handlersInStreamManager = handlersInStreamManager.filter(h => (h as any).once); + handlersInStreamManager = handlersInStreamManager.filter((h) => (h as any).once); } listenersInStreamManager = handlersInStreamManager.length; } @@ -1363,10 +1469,10 @@ export class Session extends EventDispatcher { const params = token.split('?'); const queryParams = decodeURI(params[1]) .split('&') - .map(param => param.split('=')) + .map((param) => param.split('=')) .reduce((values, [key, value]) => { - values[key] = value - return values + values[key] = value; + return values; }, {}); return { @@ -1379,7 +1485,6 @@ export class Session extends EventDispatcher { wsUri: url.protocol + '://' + url.host + '/openvidu', httpUri: 'https://' + url.host }; - } else { throw new Error(`Token not valid: "${token}"`); } @@ -1393,14 +1498,12 @@ export class Session extends EventDispatcher { if (!!error) { return reject(error); } else { - const joinParams = this.initializeParams(token); this.openvidu.sendRequest('joinRoom', joinParams, (error, response: LocalConnectionOptions) => { if (!!error) { return reject(error); } else { - // Process join room response this.processJoinRoomResponse(response, token); @@ -1424,20 +1527,26 @@ export class Session extends EventDispatcher { }); // Own 'connectionCreated' event - this.ee.emitEvent('connectionCreated', [new ConnectionEvent(false, this, 'connectionCreated', this.connection, '')]); + this.ee.emitEvent('connectionCreated', [ + new ConnectionEvent(false, this, 'connectionCreated', this.connection, '') + ]); // One 'connectionCreated' event for each existing connection in the session - events.connections.forEach(connection => { - this.ee.emitEvent('connectionCreated', [new ConnectionEvent(false, this, 'connectionCreated', connection, '')]); + events.connections.forEach((connection) => { + this.ee.emitEvent('connectionCreated', [ + new ConnectionEvent(false, this, 'connectionCreated', connection, '') + ]); }); // One 'streamCreated' event for each active stream in the session - events.streams.forEach(stream => { + events.streams.forEach((stream) => { this.ee.emitEvent('streamCreated', [new StreamEvent(false, this, 'streamCreated', stream, '')]); }); if (!!response.recordingId && !!response.recordingName) { - this.ee.emitEvent('recordingStarted', [new RecordingEvent(this, 'recordingStarted', response.recordingId, response.recordingName)]); + this.ee.emitEvent('recordingStarted', [ + new RecordingEvent(this, 'recordingStarted', response.recordingId, response.recordingName) + ]); } return resolve(); @@ -1498,8 +1607,14 @@ export class Session extends EventDispatcher { return resolve(connection); } else { // Remote connection not found. Reject with OpenViduError - const errorMessage = 'Remote connection ' + connectionId + " unknown when '" + operation + "'. " + - 'Existing remote connections: ' + JSON.stringify(this.remoteConnections.keys()); + const errorMessage = + 'Remote connection ' + + connectionId + + " unknown when '" + + operation + + "'. " + + 'Existing remote connections: ' + + JSON.stringify(this.remoteConnections.keys()); return reject(new OpenViduError(OpenViduErrorName.GENERIC_ERROR, errorMessage)); } }); @@ -1535,8 +1650,8 @@ export class Session extends EventDispatcher { for (const iceServer of opts.customIceServers) { let rtcIceServer: RTCIceServer = { urls: [iceServer.url] - } - logger.log("STUN/TURN server IP: " + iceServer.url); + }; + logger.log('STUN/TURN server IP: ' + iceServer.url); if (iceServer.username != null && iceServer.credential != null) { rtcIceServer.username = iceServer.username; rtcIceServer.credential = iceServer.credential; @@ -1555,18 +1670,24 @@ export class Session extends EventDispatcher { forceUnpublish: this.openvidu.role === 'MODERATOR', forceDisconnect: this.openvidu.role === 'MODERATOR' }; - logger.info("openvidu-server version: " + opts.version); + logger.info('openvidu-server version: ' + opts.version); if (opts.life != null) { this.openvidu.life = opts.life; } const minorDifference: number = semverMinor(opts.version) - semverMinor(this.openvidu.libraryVersion); - if ((semverMajor(opts.version) !== semverMajor(this.openvidu.libraryVersion)) || !(minorDifference == 0 || minorDifference == 1)) { - logger.error(`openvidu-browser (${this.openvidu.libraryVersion}) and openvidu-server (${opts.version}) versions are incompatible. ` - + 'Errors are likely to occur. openvidu-browser SDK is only compatible with the same version or the immediately following minor version of an OpenVidu deployment'); + if (semverMajor(opts.version) !== semverMajor(this.openvidu.libraryVersion) || !(minorDifference == 0 || minorDifference == 1)) { + logger.error( + `openvidu-browser (${this.openvidu.libraryVersion}) and openvidu-server (${opts.version}) versions are incompatible. ` + + 'Errors are likely to occur. openvidu-browser SDK is only compatible with the same version or the immediately following minor version of an OpenVidu deployment' + ); } else if (minorDifference == 1) { - logger.warn(`openvidu-browser version ${this.openvidu.libraryVersion} does not match openvidu-server version ${opts.version}. ` - + `These versions are still compatible with each other, but openvidu-browser version must be updated as soon as possible to ${semverMajor(opts.version)}.${semverMinor(opts.version)}.x. ` - + `This client using openvidu-browser ${this.openvidu.libraryVersion} will become incompatible with the next release of openvidu-server`); + logger.warn( + `openvidu-browser version ${this.openvidu.libraryVersion} does not match openvidu-server version ${opts.version}. ` + + `These versions are still compatible with each other, but openvidu-browser version must be updated as soon as possible to ${semverMajor( + opts.version + )}.${semverMinor(opts.version)}.x. ` + + `This client using openvidu-browser ${this.openvidu.libraryVersion} will become incompatible with the next release of openvidu-server` + ); } // Configure JSNLogs @@ -1575,5 +1696,4 @@ export class Session extends EventDispatcher { // Store token this.token = token; } - } diff --git a/openvidu-browser/src/OpenVidu/Stream.ts b/openvidu-browser/src/OpenVidu/Stream.ts index fe97d6cd..95d75b46 100644 --- a/openvidu-browser/src/OpenVidu/Stream.ts +++ b/openvidu-browser/src/OpenVidu/Stream.ts @@ -23,7 +23,13 @@ import { StreamManager } from './StreamManager'; import { Subscriber } from './Subscriber'; import { InboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/InboundStreamOptions'; import { OutboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/OutboundStreamOptions'; -import { WebRtcPeer, WebRtcPeerSendonly, WebRtcPeerRecvonly, WebRtcPeerSendrecv, WebRtcPeerConfiguration } from '../OpenViduInternal/WebRtcPeer/WebRtcPeer'; +import { + WebRtcPeer, + WebRtcPeerSendonly, + WebRtcPeerRecvonly, + WebRtcPeerSendrecv, + WebRtcPeerConfiguration +} from '../OpenViduInternal/WebRtcPeer/WebRtcPeer'; import { WebRtcStats } from '../OpenViduInternal/WebRtcStats/WebRtcStats'; import { ExceptionEvent, ExceptionEventName } from '../OpenViduInternal/Events/ExceptionEvent'; import { PublisherSpeakingEvent } from '../OpenViduInternal/Events/PublisherSpeakingEvent'; @@ -60,7 +66,6 @@ let platform: PlatformUtils; * to one of them (sending and receiving it, respectively) */ export class Stream { - /** * The Connection object that is publishing the stream */ @@ -137,7 +142,7 @@ export class Stream { * * Whenever this happens a [[StreamPropertyChangedEvent]] will be dispatched by the Session object as well as by the affected Subscriber/Publisher object */ - videoDimensions: { width: number, height: number }; + videoDimensions: { width: number; height: number }; /** * **WARNING**: experimental option. This interface may change in the near future @@ -154,11 +159,11 @@ export class Stream { private isSubscribeToRemote = false; - private virtualBackgroundSourceElements?: { videoClone: HTMLVideoElement, mediaStreamClone: MediaStream }; + private virtualBackgroundSourceElements?: { videoClone: HTMLVideoElement; mediaStreamClone: MediaStream }; /** * @hidden */ - virtualBackgroundSinkElements?: { VB: any, video: HTMLVideoElement }; + virtualBackgroundSinkElements?: { VB: any; video: HTMLVideoElement }; /** * @hidden @@ -237,12 +242,10 @@ export class Stream { */ lastVBFilter?: Filter; - /** * @hidden */ constructor(session: Session, options: InboundStreamOptions | OutboundStreamOptions | {}) { - platform = PlatformUtils.getInstance(); this.session = session; @@ -258,12 +261,15 @@ export class Stream { } if (this.hasVideo) { this.videoActive = this.inboundStreamOpts.videoActive; - this.typeOfVideo = (!this.inboundStreamOpts.typeOfVideo) ? undefined : this.inboundStreamOpts.typeOfVideo; - this.frameRate = (this.inboundStreamOpts.frameRate === -1) ? undefined : this.inboundStreamOpts.frameRate; + this.typeOfVideo = !this.inboundStreamOpts.typeOfVideo ? undefined : this.inboundStreamOpts.typeOfVideo; + this.frameRate = this.inboundStreamOpts.frameRate === -1 ? undefined : this.inboundStreamOpts.frameRate; this.videoDimensions = this.inboundStreamOpts.videoDimensions; } - if (!!this.inboundStreamOpts.filter && (Object.keys(this.inboundStreamOpts.filter).length > 0)) { - if (!!this.inboundStreamOpts.filter.lastExecMethod && Object.keys(this.inboundStreamOpts.filter.lastExecMethod).length === 0) { + if (!!this.inboundStreamOpts.filter && Object.keys(this.inboundStreamOpts.filter).length > 0) { + if ( + !!this.inboundStreamOpts.filter.lastExecMethod && + Object.keys(this.inboundStreamOpts.filter.lastExecMethod).length === 0 + ) { delete this.inboundStreamOpts.filter.lastExecMethod; } this.filter = this.inboundStreamOpts.filter; @@ -281,7 +287,10 @@ export class Stream { if (this.hasVideo) { this.videoActive = !!this.outboundStreamOpts.publisherProperties.publishVideo; this.frameRate = this.outboundStreamOpts.publisherProperties.frameRate; - if (typeof MediaStreamTrack !== 'undefined' && this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack) { + if ( + typeof MediaStreamTrack !== 'undefined' && + this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack + ) { this.typeOfVideo = TypeOfVideo.CUSTOM; } else { this.typeOfVideo = this.isSendScreen() ? TypeOfVideo.SCREEN : TypeOfVideo.CAMERA; @@ -298,7 +307,6 @@ export class Stream { }); } - /** * Recreates the media connection with the server. This entails the disposal of the previous RTCPeerConnection and the re-negotiation * of a new one, that will apply the same properties. @@ -322,16 +330,19 @@ export class Stream { */ applyFilter(type: string, options: Object): Promise { return new Promise(async (resolve, reject) => { - if (!!this.filter) { - return reject(new OpenViduError(OpenViduErrorName.GENERIC_ERROR, 'There is already a filter applied to Stream ' + this.streamId)); + return reject( + new OpenViduError(OpenViduErrorName.GENERIC_ERROR, 'There is already a filter applied to Stream ' + this.streamId) + ); } const resolveApplyFilter = (error, triggerEvent) => { if (error) { logger.error('Error applying filter for Stream ' + this.streamId, error); if (error.code === 401) { - return reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to apply a filter")); + return reject( + new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to apply a filter") + ); } else { return reject(error); } @@ -341,22 +352,35 @@ export class Stream { this.filter = new Filter(type, options); this.filter.stream = this; if (triggerEvent) { - this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, this, 'filter', this.filter, oldValue, 'applyFilter')]); - this.streamManager.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.streamManager, this, 'filter', this.filter, oldValue, 'applyFilter')]); + this.session.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent(this.session, this, 'filter', this.filter, oldValue, 'applyFilter') + ]); + this.streamManager.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent(this.streamManager, this, 'filter', this.filter, oldValue, 'applyFilter') + ]); } return resolve(this.filter); } - } + }; if (type.startsWith('VB:')) { - // Client filters if (!this.hasVideo) { - return reject(new OpenViduError(OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR, 'The Virtual Background filter requires a video track to be applied')); + return reject( + new OpenViduError( + OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR, + 'The Virtual Background filter requires a video track to be applied' + ) + ); } if (!this.mediaStream || this.streamManager.videos.length === 0) { - return reject(new OpenViduError(OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR, 'The StreamManager requires some video element to be attached to it in order to apply a Virtual Background filter')); + return reject( + new OpenViduError( + OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR, + 'The StreamManager requires some video element to be attached to it in order to apply a Virtual Background filter' + ) + ); } let openviduToken: string; @@ -366,12 +390,22 @@ export class Stream { openviduToken = options['token']; } if (!openviduToken) { - return reject(new OpenViduError(OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR, 'Virtual Background requires the client to be connected to a Session or to have a "token" property available in "options" parameter with a valid OpenVidu token')); + return reject( + new OpenViduError( + OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR, + 'Virtual Background requires the client to be connected to a Session or to have a "token" property available in "options" parameter with a valid OpenVidu token' + ) + ); } const tokenParams = this.session.getTokenParams(openviduToken); if (tokenParams.edition !== 'pro' && tokenParams.edition !== 'enterprise') { - return reject(new OpenViduError(OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR, 'OpenVidu Virtual Background API is available from OpenVidu Pro edition onwards')); + return reject( + new OpenViduError( + OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR, + 'OpenVidu Virtual Background API is available from OpenVidu Pro edition onwards' + ) + ); } openviduToken = encodeURIComponent(btoa(openviduToken)); @@ -425,13 +459,18 @@ export class Stream { videoClone.style.display = 'none'; if (this.streamManager.remote) { - this.streamManager.replaceTrackInMediaStream((this.virtualBackgroundSinkElements.video.srcObject as MediaStream).getVideoTracks()[0], false); + this.streamManager.replaceTrackInMediaStream( + (this.virtualBackgroundSinkElements.video.srcObject as MediaStream).getVideoTracks()[0], + false + ); } else { - (this.streamManager as Publisher).replaceTrackAux((this.virtualBackgroundSinkElements.video.srcObject as MediaStream).getVideoTracks()[0], false); + (this.streamManager as Publisher).replaceTrackAux( + (this.virtualBackgroundSinkElements.video.srcObject as MediaStream).getVideoTracks()[0], + false + ); } resolveApplyFilter(undefined, false); - } catch (error) { if (error.name === OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR) { resolveApplyFilter(new OpenViduError(OpenViduErrorName.VIRTUAL_BACKGROUND_ERROR, error.message), false); @@ -439,12 +478,12 @@ export class Stream { resolveApplyFilter(error, false); } } - } + }; // @ts-ignore - if (typeof VirtualBackground === "undefined") { - let script: HTMLScriptElement = document.createElement("script"); - script.type = "text/javascript"; + if (typeof VirtualBackground === 'undefined') { + let script: HTMLScriptElement = document.createElement('script'); + script.type = 'text/javascript'; script.src = tokenParams.httpUri + '/openvidu/virtual-background/openvidu-virtual-background.js?token=' + openviduToken; script.onload = async () => { try { @@ -458,11 +497,9 @@ export class Stream { } else { afterScriptLoaded() .then(() => resolve(new Filter(type, options))) - .catch(error => reject(error)); + .catch((error) => reject(error)); } - } else { - // Server filters if (!this.session.sessionConnected()) { @@ -482,9 +519,7 @@ export class Stream { resolveApplyFilter(error, true); } ); - } - }); } @@ -522,13 +557,14 @@ export class Stream { */ removeFilterAux(isDisposing: boolean): Promise { return new Promise(async (resolve, reject) => { - const resolveRemoveFilter = (error, triggerEvent) => { if (error) { delete this.filter; logger.error('Error removing filter for Stream ' + this.streamId, error); if (error.code === 401) { - return reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to remove a filter")); + return reject( + new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to remove a filter") + ); } else { return reject(error); } @@ -537,23 +573,24 @@ export class Stream { const oldValue = this.filter!; delete this.filter; if (triggerEvent) { - this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, this, 'filter', this.filter!, oldValue, 'applyFilter')]); - this.streamManager.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.streamManager, this, 'filter', this.filter!, oldValue, 'applyFilter')]); + this.session.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent(this.session, this, 'filter', this.filter!, oldValue, 'applyFilter') + ]); + this.streamManager.emitEvent('streamPropertyChanged', [ + new StreamPropertyChangedEvent(this.streamManager, this, 'filter', this.filter!, oldValue, 'applyFilter') + ]); } return resolve(); } - } + }; if (!!this.filter) { - // There is a filter applied if (this.filter?.type.startsWith('VB:')) { - // Client filters try { - const mediaStreamClone = this.virtualBackgroundSourceElements!.mediaStreamClone; if (!isDisposing) { if (this.streamManager.remote) { @@ -571,13 +608,10 @@ export class Stream { delete this.virtualBackgroundSourceElements; return resolveRemoveFilter(undefined, false); - } catch (error) { return resolveRemoveFilter(error, false); } - } else { - // Server filters if (!this.session.sessionConnected()) { @@ -585,20 +619,13 @@ export class Stream { } logger.info('Removing filter of stream ' + this.streamId); - this.session.openvidu.sendRequest( - 'removeFilter', - { streamId: this.streamId }, - (error, response) => { - return resolveRemoveFilter(error, true); - } - ); - + this.session.openvidu.sendRequest('removeFilter', { streamId: this.streamId }, (error, response) => { + return resolveRemoveFilter(error, true); + }); } } else { - // There is no filter applied - return reject(new OpenViduError(OpenViduErrorName.GENERIC_ERROR, "Stream " + this.streamId + " has no filter applied")); - + return reject(new OpenViduError(OpenViduErrorName.GENERIC_ERROR, 'Stream ' + this.streamId + ' has no filter applied')); } }); } @@ -645,7 +672,7 @@ export class Stream { return new Promise((resolve, reject) => { this.initWebRtcPeerReceive(false) .then(() => resolve()) - .catch(error => reject(error)); + .catch((error) => reject(error)); }); } @@ -657,12 +684,12 @@ export class Stream { if (this.isLocalStreamReadyToPublish) { this.initWebRtcPeerSend(false) .then(() => resolve()) - .catch(error => reject(error)); + .catch((error) => reject(error)); } else { this.ee.once('stream-ready-to-publish', () => { this.publish() .then(() => resolve()) - .catch(error => reject(error)); + .catch((error) => reject(error)); }); } }); @@ -678,7 +705,14 @@ export class Stream { webrtcId = this.webRtcPeer.getId(); } this.stopWebRtcStats(); - logger.info((!!this.outboundStreamOpts ? 'Outbound ' : 'Inbound ') + "RTCPeerConnection with id [" + webrtcId + "] from 'Stream' with id [" + this.streamId + '] is now closed'); + logger.info( + (!!this.outboundStreamOpts ? 'Outbound ' : 'Inbound ') + + 'RTCPeerConnection with id [' + + webrtcId + + "] from 'Stream' with id [" + + this.streamId + + '] is now closed' + ); } /** @@ -718,7 +752,9 @@ export class Stream { } delete this.speechEvent; } - logger.info((!!this.outboundStreamOpts ? 'Local ' : 'Remote ') + "MediaStream from 'Stream' with id [" + this.streamId + '] is now disposed'); + logger.info( + (!!this.outboundStreamOpts ? 'Local ' : 'Remote ') + "MediaStream from 'Stream' with id [" + this.streamId + '] is now disposed' + ); } /** @@ -732,18 +768,22 @@ export class Stream { * @hidden */ isSendAudio(): boolean { - return (!!this.outboundStreamOpts && + return ( + !!this.outboundStreamOpts && this.outboundStreamOpts.publisherProperties.audioSource !== null && - this.outboundStreamOpts.publisherProperties.audioSource !== false); + this.outboundStreamOpts.publisherProperties.audioSource !== false + ); } /** * @hidden */ isSendVideo(): boolean { - return (!!this.outboundStreamOpts && + return ( + !!this.outboundStreamOpts && this.outboundStreamOpts.publisherProperties.videoSource !== null && - this.outboundStreamOpts.publisherProperties.videoSource !== false); + this.outboundStreamOpts.publisherProperties.videoSource !== false + ); } /** @@ -752,7 +792,8 @@ export class Stream { isSendScreen(): boolean { let screen = this.outboundStreamOpts.publisherProperties.videoSource === 'screen'; if (platform.isElectron()) { - screen = typeof this.outboundStreamOpts.publisherProperties.videoSource === 'string' && + screen = + typeof this.outboundStreamOpts.publisherProperties.videoSource === 'string' && this.outboundStreamOpts.publisherProperties.videoSource.startsWith('screen:'); } return !!this.outboundStreamOpts && screen; @@ -766,8 +807,12 @@ export class Stream { if (!this.harkSpeakingEnabled && !!this.speechEvent) { this.harkSpeakingEnabled = true; this.speechEvent.on('speaking', () => { - this.session.emitEvent('publisherStartSpeaking', [new PublisherSpeakingEvent(this.session, 'publisherStartSpeaking', this.connection, this.streamId)]); - this.streamManager.emitEvent('publisherStartSpeaking', [new PublisherSpeakingEvent(this.streamManager, 'publisherStartSpeaking', this.connection, this.streamId)]); + this.session.emitEvent('publisherStartSpeaking', [ + new PublisherSpeakingEvent(this.session, 'publisherStartSpeaking', this.connection, this.streamId) + ]); + this.streamManager.emitEvent('publisherStartSpeaking', [ + new PublisherSpeakingEvent(this.streamManager, 'publisherStartSpeaking', this.connection, this.streamId) + ]); this.harkSpeakingEnabledOnce = false; // Disable 'once' version if 'on' version was triggered }); } @@ -783,8 +828,12 @@ export class Stream { this.speechEvent.once('speaking', () => { if (this.harkSpeakingEnabledOnce) { // If the listener has been disabled in the meantime (for example by the 'on' version) do not trigger the event - this.session.emitEvent('publisherStartSpeaking', [new PublisherSpeakingEvent(this.session, 'publisherStartSpeaking', this.connection, this.streamId)]); - this.streamManager.emitEvent('publisherStartSpeaking', [new PublisherSpeakingEvent(this.streamManager, 'publisherStartSpeaking', this.connection, this.streamId)]); + this.session.emitEvent('publisherStartSpeaking', [ + new PublisherSpeakingEvent(this.session, 'publisherStartSpeaking', this.connection, this.streamId) + ]); + this.streamManager.emitEvent('publisherStartSpeaking', [ + new PublisherSpeakingEvent(this.streamManager, 'publisherStartSpeaking', this.connection, this.streamId) + ]); } this.disableHarkSpeakingEvent(true); }); @@ -806,10 +855,12 @@ export class Stream { this.harkSpeakingEnabled = false; } // Shutting down the hark event - if (this.harkVolumeChangeEnabled || + if ( + this.harkVolumeChangeEnabled || this.harkVolumeChangeEnabledOnce || this.harkStoppedSpeakingEnabled || - this.harkStoppedSpeakingEnabledOnce) { + this.harkStoppedSpeakingEnabledOnce + ) { // Some other hark event is enabled. Cannot stop the hark process, just remove the specific listener this.speechEvent.off('speaking'); } else { @@ -828,8 +879,12 @@ export class Stream { if (!this.harkStoppedSpeakingEnabled && !!this.speechEvent) { this.harkStoppedSpeakingEnabled = true; this.speechEvent.on('stopped_speaking', () => { - this.session.emitEvent('publisherStopSpeaking', [new PublisherSpeakingEvent(this.session, 'publisherStopSpeaking', this.connection, this.streamId)]); - this.streamManager.emitEvent('publisherStopSpeaking', [new PublisherSpeakingEvent(this.streamManager, 'publisherStopSpeaking', this.connection, this.streamId)]); + this.session.emitEvent('publisherStopSpeaking', [ + new PublisherSpeakingEvent(this.session, 'publisherStopSpeaking', this.connection, this.streamId) + ]); + this.streamManager.emitEvent('publisherStopSpeaking', [ + new PublisherSpeakingEvent(this.streamManager, 'publisherStopSpeaking', this.connection, this.streamId) + ]); this.harkStoppedSpeakingEnabledOnce = false; // Disable 'once' version if 'on' version was triggered }); } @@ -845,8 +900,12 @@ export class Stream { this.speechEvent.once('stopped_speaking', () => { if (this.harkStoppedSpeakingEnabledOnce) { // If the listener has been disabled in the meantime (for example by the 'on' version) do not trigger the event - this.session.emitEvent('publisherStopSpeaking', [new PublisherSpeakingEvent(this.session, 'publisherStopSpeaking', this.connection, this.streamId)]); - this.streamManager.emitEvent('publisherStopSpeaking', [new PublisherSpeakingEvent(this.streamManager, 'publisherStopSpeaking', this.connection, this.streamId)]); + this.session.emitEvent('publisherStopSpeaking', [ + new PublisherSpeakingEvent(this.session, 'publisherStopSpeaking', this.connection, this.streamId) + ]); + this.streamManager.emitEvent('publisherStopSpeaking', [ + new PublisherSpeakingEvent(this.streamManager, 'publisherStopSpeaking', this.connection, this.streamId) + ]); } this.disableHarkStoppedSpeakingEvent(true); }); @@ -854,8 +913,8 @@ export class Stream { } /** - * @hidden - */ + * @hidden + */ disableHarkStoppedSpeakingEvent(disabledByOnce: boolean): void { if (!!this.speechEvent) { this.harkStoppedSpeakingEnabledOnce = false; @@ -869,10 +928,12 @@ export class Stream { this.harkStoppedSpeakingEnabled = false; } // Shutting down the hark event - if (this.harkVolumeChangeEnabled || + if ( + this.harkVolumeChangeEnabled || this.harkVolumeChangeEnabledOnce || this.harkSpeakingEnabled || - this.harkSpeakingEnabledOnce) { + this.harkSpeakingEnabledOnce + ) { // Some other hark event is enabled. Cannot stop the hark process, just remove the specific listener this.speechEvent.off('stopped_speaking'); } else { @@ -890,11 +951,13 @@ export class Stream { if (this.setHarkListenerIfNotExists()) { if (!this.harkVolumeChangeEnabled || force) { this.harkVolumeChangeEnabled = true; - this.speechEvent.on('volume_change', harkEvent => { + this.speechEvent.on('volume_change', (harkEvent) => { const oldValue = this.speechEvent.oldVolumeValue; const value = { newValue: harkEvent, oldValue }; this.speechEvent.oldVolumeValue = harkEvent; - this.streamManager.emitEvent('streamAudioVolumeChange', [new StreamManagerEvent(this.streamManager, 'streamAudioVolumeChange', value)]); + this.streamManager.emitEvent('streamAudioVolumeChange', [ + new StreamManagerEvent(this.streamManager, 'streamAudioVolumeChange', value) + ]); }); } } else { @@ -910,12 +973,14 @@ export class Stream { if (this.setHarkListenerIfNotExists()) { if (!this.harkVolumeChangeEnabledOnce || force) { this.harkVolumeChangeEnabledOnce = true; - this.speechEvent.once('volume_change', harkEvent => { + this.speechEvent.once('volume_change', (harkEvent) => { const oldValue = this.speechEvent.oldVolumeValue; const value = { newValue: harkEvent, oldValue }; this.speechEvent.oldVolumeValue = harkEvent; this.disableHarkVolumeChangeEvent(true); - this.streamManager.emitEvent('streamAudioVolumeChange', [new StreamManagerEvent(this.streamManager, 'streamAudioVolumeChange', value)]); + this.streamManager.emitEvent('streamAudioVolumeChange', [ + new StreamManagerEvent(this.streamManager, 'streamAudioVolumeChange', value) + ]); }); } } else { @@ -940,10 +1005,12 @@ export class Stream { this.harkVolumeChangeEnabled = false; } // Shutting down the hark event - if (this.harkSpeakingEnabled || + if ( + this.harkSpeakingEnabled || this.harkSpeakingEnabledOnce || this.harkStoppedSpeakingEnabled || - this.harkStoppedSpeakingEnabledOnce) { + this.harkStoppedSpeakingEnabledOnce + ) { // Some other hark event is enabled. Cannot stop the hark process, just remove the specific listener this.speechEvent.off('volume_change'); } else { @@ -959,7 +1026,7 @@ export class Stream { */ isLocal(): boolean { // inbound options undefined and outbound options defined - return (!this.inboundStreamOpts && !!this.outboundStreamOpts); + return !this.inboundStreamOpts && !!this.outboundStreamOpts; } /** @@ -967,9 +1034,10 @@ export class Stream { */ getSelectedIceCandidate(): Promise { return new Promise((resolve, reject) => { - this.webRtcStats.getSelectedIceCandidateInfo() - .then(report => resolve(report)) - .catch(error => reject(error)); + this.webRtcStats + .getSelectedIceCandidateInfo() + .then((report) => resolve(report)) + .catch((error) => reject(error)); }); } @@ -995,7 +1063,11 @@ export class Stream { return false; } if (this.isLocal() && !!this.session.openvidu.advancedConfiguration.forceMediaReconnectionAfterNetworkDrop) { - logger.warn(`OpenVidu Browser advanced configuration option "forceMediaReconnectionAfterNetworkDrop" is enabled. Stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) will force a reconnection`); + logger.warn( + `OpenVidu Browser advanced configuration option "forceMediaReconnectionAfterNetworkDrop" is enabled. Stream ${ + this.streamId + } (${this.isLocal() ? 'Publisher' : 'Subscriber'}) will force a reconnection` + ); return true; } const iceConnectionState: RTCIceConnectionState = this.getRTCPeerConnection().iceConnectionState; @@ -1007,9 +1079,11 @@ export class Stream { private setHarkListenerIfNotExists(): boolean { if (!!this.mediaStream) { if (!this.speechEvent) { - const harkOptions = !!this.harkOptions ? this.harkOptions : (this.session.openvidu.advancedConfiguration.publisherSpeakingEventsOptions || {}); - harkOptions.interval = (typeof harkOptions.interval === 'number') ? harkOptions.interval : 100; - harkOptions.threshold = (typeof harkOptions.threshold === 'number') ? harkOptions.threshold : -50; + const harkOptions = !!this.harkOptions + ? this.harkOptions + : this.session.openvidu.advancedConfiguration.publisherSpeakingEventsOptions || {}; + harkOptions.interval = typeof harkOptions.interval === 'number' ? harkOptions.interval : 100; + harkOptions.threshold = typeof harkOptions.threshold === 'number' ? harkOptions.threshold : -50; this.speechEvent = hark(this.mediaStream, harkOptions); } return true; @@ -1027,9 +1101,13 @@ export class Stream { 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...`); + 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)); + this.reconnectionEventEmitter.once('error', (error) => reject(error)); return true; } } @@ -1039,7 +1117,6 @@ export class Stream { */ initWebRtcPeerSend(reconnect: boolean): Promise { return new Promise((resolve, reject) => { - if (reconnect) { if (this.setupReconnectionEventEmitter(resolve, reject)) { // Ongoing reconnection @@ -1056,19 +1133,18 @@ export class Stream { delete this.reconnectionEventEmitter; } return resolve(); - } + }; - const finalReject = error => { + const finalReject = (error) => { if (reconnect) { this.reconnectionEventEmitter?.emitEvent('error', [error]); delete this.reconnectionEventEmitter; } return reject(error); - } + }; const successOfferCallback = (sdpOfferParam) => { - logger.debug('Sending SDP offer to publish as ' - + this.streamId, sdpOfferParam); + logger.debug('Sending SDP offer to publish as ' + this.streamId, sdpOfferParam); const method = reconnect ? 'reconnectStream' : 'publishVideo'; let params; @@ -1076,11 +1152,17 @@ export class Stream { params = { stream: this.streamId, sdpString: sdpOfferParam - } + }; } else { let typeOfVideo; if (this.isSendVideo()) { - typeOfVideo = (typeof MediaStreamTrack !== 'undefined' && this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack) ? TypeOfVideo.CUSTOM : (this.isSendScreen() ? TypeOfVideo.SCREEN : TypeOfVideo.CAMERA); + typeOfVideo = + typeof MediaStreamTrack !== 'undefined' && + this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack + ? TypeOfVideo.CUSTOM + : this.isSendScreen() + ? TypeOfVideo.SCREEN + : TypeOfVideo.CAMERA; } params = { doLoopback: this.displayMyRemote() || false, @@ -1093,18 +1175,21 @@ export class Stream { videoDimensions: JSON.stringify(this.videoDimensions), filter: this.outboundStreamOpts.publisherProperties.filter, sdpOffer: sdpOfferParam - } + }; } this.session.openvidu.sendRequest(method, params, (error, response) => { if (error) { if (error.code === 401) { - finalReject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to publish")); + finalReject( + new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to publish") + ); } else { finalReject('Error on publishVideo: ' + JSON.stringify(error)); } } else { - this.webRtcPeer.processRemoteAnswer(response.sdpAnswer) + this.webRtcPeer + .processRemoteAnswer(response.sdpAnswer) .then(() => { this.streamId = response.id; this.creationTime = response.createdAt; @@ -1120,11 +1205,17 @@ export class Stream { this.ee.emitEvent('stream-created-by-publisher', []); } this.initWebRtcStats(); - logger.info("'Publisher' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "published") + " to session"); + logger.info( + "'Publisher' (" + + this.streamId + + ') successfully ' + + (reconnect ? 'reconnected' : 'published') + + ' to session' + ); finalResolve(); }) - .catch(error => { + .catch((error) => { finalReject(error); }); } @@ -1134,16 +1225,17 @@ export class Stream { const config: WebRtcPeerConfiguration = { mediaConstraints: { audio: this.hasAudio, - video: this.hasVideo, + video: this.hasVideo }, - simulcast: - this.outboundStreamOpts.publisherProperties.videoSimulcast ?? this.session.openvidu.videoSimulcast, + simulcast: this.outboundStreamOpts.publisherProperties.videoSimulcast ?? this.session.openvidu.videoSimulcast, onIceCandidate: this.connection.sendIceCandidate.bind(this.connection), - onIceConnectionStateException: (exceptionName: ExceptionEventName, message: string, data?: any) => { this.session.emitEvent('exception', [new ExceptionEvent(this.session, exceptionName, this, message, data)]) }, + onIceConnectionStateException: (exceptionName: ExceptionEventName, message: string, data?: any) => { + this.session.emitEvent('exception', [new ExceptionEvent(this.session, exceptionName, this, message, data)]); + }, iceServers: this.getIceServersConf(), mediaStream: this.mediaStream, mediaServer: this.session.openvidu.mediaServer, - typeOfVideo: this.typeOfVideo ? TypeOfVideo[this.typeOfVideo] : undefined, + typeOfVideo: this.typeOfVideo ? TypeOfVideo[this.typeOfVideo] : undefined }; if (this.session.openvidu.mediaServer !== 'mediasoup') { @@ -1160,16 +1252,21 @@ export class Stream { this.webRtcPeer = new WebRtcPeerSendonly(config); } this.webRtcPeer.addIceConnectionStateChangeListener('publisher of ' + this.connection.connectionId); - this.webRtcPeer.createOffer().then(sdpOffer => { - this.webRtcPeer.processLocalOffer(sdpOffer) - .then(() => { - successOfferCallback(sdpOffer.sdp); - }).catch(error => { - finalReject(new Error('(publish) SDP process local offer error: ' + JSON.stringify(error))); - }); - }).catch(error => { - finalReject(new Error('(publish) SDP create offer error: ' + JSON.stringify(error))); - }); + this.webRtcPeer + .createOffer() + .then((sdpOffer) => { + this.webRtcPeer + .processLocalOffer(sdpOffer) + .then(() => { + successOfferCallback(sdpOffer.sdp); + }) + .catch((error) => { + finalReject(new Error('(publish) SDP process local offer error: ' + JSON.stringify(error))); + }); + }) + .catch((error) => { + finalReject(new Error('(publish) SDP create offer error: ' + JSON.stringify(error))); + }); }); } @@ -1177,7 +1274,7 @@ export class Stream { * @hidden */ finalResolveForSubscription(reconnect: boolean, resolve: (value: void | PromiseLike) => void) { - logger.info("'Subscriber' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "subscribed")); + logger.info("'Subscriber' (" + this.streamId + ') successfully ' + (reconnect ? 'reconnected' : 'subscribed')); this.remotePeerSuccessfullyEstablished(reconnect); this.initWebRtcStats(); if (reconnect) { @@ -1191,7 +1288,14 @@ export class Stream { * @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()); + 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; @@ -1204,7 +1308,6 @@ export class Stream { */ initWebRtcPeerReceive(reconnect: boolean): Promise { return new Promise((resolve, reject) => { - if (reconnect) { if (this.setupReconnectionEventEmitter(resolve, reject)) { // Ongoing reconnection @@ -1213,21 +1316,17 @@ export class Stream { } if (this.session.openvidu.mediaServer === 'mediasoup') { - // Server initiates negotiation this.initWebRtcPeerReceiveFromServer(reconnect) .then(() => this.finalResolveForSubscription(reconnect, resolve)) - .catch(error => this.finalRejectForSubscription(reconnect, error, reject)); - + .catch((error) => this.finalRejectForSubscription(reconnect, error, reject)); } else { - // Client initiates negotiation this.initWebRtcPeerReceiveFromClient(reconnect) .then(() => this.finalResolveForSubscription(reconnect, resolve)) - .catch(error => this.finalRejectForSubscription(reconnect, error, reject)); - + .catch((error) => this.finalRejectForSubscription(reconnect, error, reject)); } }); } @@ -1238,12 +1337,13 @@ export class Stream { initWebRtcPeerReceiveFromClient(reconnect: boolean): Promise { return new Promise((resolve, reject) => { this.completeWebRtcPeerReceive(reconnect, false) - .then(response => { - this.webRtcPeer.processRemoteAnswer(response.sdpAnswer) + .then((response) => { + this.webRtcPeer + .processRemoteAnswer(response.sdpAnswer) .then(() => resolve()) - .catch(error => reject(error)); + .catch((error) => reject(error)); }) - .catch(error => reject(error)); + .catch((error) => reject(error)); }); } @@ -1259,7 +1359,7 @@ export class Stream { } else { this.completeWebRtcPeerReceive(reconnect, false, response.sdpOffer) .then(() => resolve()) - .catch(error => reject(error)); + .catch((error) => reject(error)); } }); }); @@ -1270,12 +1370,10 @@ export class Stream { */ completeWebRtcPeerReceive(reconnect: boolean, forciblyReconnect: boolean, sdpOfferByServer?: string): Promise { return new Promise((resolve, reject) => { - logger.debug("'Session.subscribe(Stream)' called"); const sendSdpToServer = (sdpString: string) => { - - logger.debug(`Sending local SDP ${(!!sdpOfferByServer ? 'answer' : 'offer')} to subscribe to ${this.streamId}`, sdpString); + logger.debug(`Sending local SDP ${!!sdpOfferByServer ? 'answer' : 'offer'} to subscribe to ${this.streamId}`, sdpString); const method = reconnect ? 'reconnectStream' : 'receiveVideoFrom'; const params = {}; @@ -1301,14 +1399,16 @@ export class Stream { const config: WebRtcPeerConfiguration = { mediaConstraints: { audio: this.hasAudio, - video: this.hasVideo, + video: this.hasVideo }, simulcast: false, onIceCandidate: this.connection.sendIceCandidate.bind(this.connection), - onIceConnectionStateException: (exceptionName: ExceptionEventName, message: string, data?: any) => { this.session.emitEvent('exception', [new ExceptionEvent(this.session, exceptionName, this, message, data)]) }, + onIceConnectionStateException: (exceptionName: ExceptionEventName, message: string, data?: any) => { + this.session.emitEvent('exception', [new ExceptionEvent(this.session, exceptionName, this, message, data)]); + }, iceServers: this.getIceServersConf(), mediaServer: this.session.openvidu.mediaServer, - typeOfVideo: this.typeOfVideo ? TypeOfVideo[this.typeOfVideo] : undefined, + typeOfVideo: this.typeOfVideo ? TypeOfVideo[this.typeOfVideo] : undefined }; if (reconnect) { @@ -1319,33 +1419,44 @@ export class Stream { this.webRtcPeer.addIceConnectionStateChangeListener(this.streamId); if (!!sdpOfferByServer) { - - this.webRtcPeer.processRemoteOffer(sdpOfferByServer).then(() => { - this.webRtcPeer.createAnswer().then(sdpAnswer => { - this.webRtcPeer.processLocalAnswer(sdpAnswer).then(() => { - sendSdpToServer(sdpAnswer.sdp!); - }).catch(error => { - return reject(new Error('(subscribe) SDP process local answer error: ' + JSON.stringify(error))); - }); - }).catch(error => { - return reject(new Error('(subscribe) SDP create answer error: ' + JSON.stringify(error))); + this.webRtcPeer + .processRemoteOffer(sdpOfferByServer) + .then(() => { + this.webRtcPeer + .createAnswer() + .then((sdpAnswer) => { + this.webRtcPeer + .processLocalAnswer(sdpAnswer) + .then(() => { + sendSdpToServer(sdpAnswer.sdp!); + }) + .catch((error) => { + return reject(new Error('(subscribe) SDP process local answer error: ' + JSON.stringify(error))); + }); + }) + .catch((error) => { + return reject(new Error('(subscribe) SDP create answer error: ' + JSON.stringify(error))); + }); + }) + .catch((error) => { + return reject(new Error('(subscribe) SDP process remote offer error: ' + JSON.stringify(error))); }); - }).catch(error => { - return reject(new Error('(subscribe) SDP process remote offer error: ' + JSON.stringify(error))); - }); - } else { - - this.webRtcPeer.createOffer().then(sdpOffer => { - this.webRtcPeer.processLocalOffer(sdpOffer).then(() => { - sendSdpToServer(sdpOffer.sdp!); - }).catch(error => { - return reject(new Error('(subscribe) SDP process local offer error: ' + JSON.stringify(error))); + this.webRtcPeer + .createOffer() + .then((sdpOffer) => { + this.webRtcPeer + .processLocalOffer(sdpOffer) + .then(() => { + sendSdpToServer(sdpOffer.sdp!); + }) + .catch((error) => { + return reject(new Error('(subscribe) SDP process local offer error: ' + JSON.stringify(error))); + }); + }) + .catch((error) => { + return reject(new Error('(subscribe) SDP create offer error: ' + JSON.stringify(error))); }); - }).catch(error => { - return reject(new Error('(subscribe) SDP create offer error: ' + JSON.stringify(error))); - }); - } }); } @@ -1354,7 +1465,6 @@ export class Stream { * @hidden */ remotePeerSuccessfullyEstablished(reconnect: boolean): void { - if (reconnect && this.mediaStream != null) { // Now we can destroy the existing MediaStream this.disposeMediaStream(); @@ -1370,15 +1480,14 @@ export class Stream { logger.debug('Peer remote stream', this.mediaStream); if (!!this.mediaStream) { - if (this.streamManager instanceof Subscriber) { // Apply SubscriberProperties.subscribeToAudio and SubscriberProperties.subscribeToVideo if (!!this.mediaStream.getAudioTracks()[0]) { - const enabled = reconnect ? this.audioActive : !!((this.streamManager as Subscriber).properties.subscribeToAudio); + const enabled = reconnect ? this.audioActive : !!(this.streamManager as Subscriber).properties.subscribeToAudio; this.mediaStream.getAudioTracks()[0].enabled = enabled; } if (!!this.mediaStream.getVideoTracks()[0]) { - const enabled = reconnect ? this.videoActive : !!((this.streamManager as Subscriber).properties.subscribeToVideo); + const enabled = reconnect ? this.videoActive : !!(this.streamManager as Subscriber).properties.subscribeToVideo; this.mediaStream.getVideoTracks()[0].enabled = enabled; } } @@ -1429,30 +1538,50 @@ export class Stream { private onIceConnectionFailed() { // Immediately reconnect, as this is a terminal error - logger.log(`[ICE_CONNECTION_FAILED] Handling ICE_CONNECTION_FAILED event. Reconnecting stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')})`); + logger.log( + `[ICE_CONNECTION_FAILED] Handling ICE_CONNECTION_FAILED event. Reconnecting stream ${this.streamId} (${ + this.isLocal() ? 'Publisher' : 'Subscriber' + })` + ); this.reconnectStreamAndLogResultingIceConnectionState(ExceptionEventName.ICE_CONNECTION_FAILED); } private onIceConnectionDisconnected() { // Wait to see if the ICE connection is able to reconnect - logger.log(`[ICE_CONNECTION_DISCONNECTED] Handling ICE_CONNECTION_DISCONNECTED event. Waiting for ICE to be restored and reconnect stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) if not possible`); + logger.log( + `[ICE_CONNECTION_DISCONNECTED] Handling ICE_CONNECTION_DISCONNECTED event. Waiting for ICE to be restored and reconnect stream ${ + this.streamId + } (${this.isLocal() ? 'Publisher' : 'Subscriber'}) if not possible` + ); const timeout = this.session.openvidu.advancedConfiguration.iceConnectionDisconnectedExceptionTimeout || 4000; - this.awaitWebRtcPeerConnectionState(timeout).then(state => { + this.awaitWebRtcPeerConnectionState(timeout).then((state) => { switch (state) { case 'failed': // Do nothing, as an ICE_CONNECTION_FAILED event will have already raised - logger.warn(`[ICE_CONNECTION_DISCONNECTED] ICE connection of stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) is now failed after ICE_CONNECTION_DISCONNECTED`); + logger.warn( + `[ICE_CONNECTION_DISCONNECTED] ICE connection of stream ${this.streamId} (${ + this.isLocal() ? 'Publisher' : 'Subscriber' + }) is now failed after ICE_CONNECTION_DISCONNECTED` + ); break; case 'connected': case 'completed': - logger.log(`[ICE_CONNECTION_DISCONNECTED] ICE connection of stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) automatically restored after ICE_CONNECTION_DISCONNECTED. Current ICE connection state: ${state}`); + logger.log( + `[ICE_CONNECTION_DISCONNECTED] ICE connection of stream ${this.streamId} (${ + this.isLocal() ? 'Publisher' : 'Subscriber' + }) automatically restored after ICE_CONNECTION_DISCONNECTED. Current ICE connection state: ${state}` + ); break; case 'closed': case 'checking': case 'new': case 'disconnected': // Rest of states - logger.warn(`[ICE_CONNECTION_DISCONNECTED] ICE connection of stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) couldn't be restored after ICE_CONNECTION_DISCONNECTED event. Current ICE connection state after ${timeout} ms: ${state}`); + logger.warn( + `[ICE_CONNECTION_DISCONNECTED] ICE connection of stream ${this.streamId} (${ + this.isLocal() ? 'Publisher' : 'Subscriber' + }) couldn't be restored after ICE_CONNECTION_DISCONNECTED event. Current ICE connection state after ${timeout} ms: ${state}` + ); this.reconnectStreamAndLogResultingIceConnectionState(ExceptionEventName.ICE_CONNECTION_DISCONNECTED); break; } @@ -1465,25 +1594,39 @@ export class Stream { switch (finalIceStateAfterReconnection) { case 'connected': case 'completed': - logger.log(`[${event}] Stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) successfully reconnected after ${event}. Current ICE connection state: ${finalIceStateAfterReconnection}`); + logger.log( + `[${event}] Stream ${this.streamId} (${ + this.isLocal() ? 'Publisher' : 'Subscriber' + }) successfully reconnected after ${event}. Current ICE connection state: ${finalIceStateAfterReconnection}` + ); break; default: - logger.error(`[${event}] Stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) failed to reconnect after ${event}. Current ICE connection state: ${finalIceStateAfterReconnection}`); + logger.error( + `[${event}] Stream ${this.streamId} (${ + this.isLocal() ? 'Publisher' : 'Subscriber' + }) failed to reconnect after ${event}. Current ICE connection state: ${finalIceStateAfterReconnection}` + ); break; } } catch (error) { - logger.error(`[${event}] Error reconnecting stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) after ${event}: ${error}`); + logger.error( + `[${event}] Error reconnecting stream ${this.streamId} (${ + this.isLocal() ? 'Publisher' : 'Subscriber' + }) after ${event}: ${error}` + ); } } private async reconnectStreamAndReturnIceConnectionState(event: string): Promise { - logger.log(`[${event}] Reconnecting stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) after event ${event}`); + logger.log(`[${event}] Reconnecting stream ${this.streamId} (${this.isLocal() ? 'Publisher' : 'Subscriber'}) after event ${event}`); try { await this.reconnectStream(event); const timeout = this.session.openvidu.advancedConfiguration.iceConnectionDisconnectedExceptionTimeout || 4000; return this.awaitWebRtcPeerConnectionState(timeout); } catch (error) { - logger.warn(`[${event}] Error reconnecting stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}). Reason: ${error}`); + logger.warn( + `[${event}] Error reconnecting stream ${this.streamId} (${this.isLocal() ? 'Publisher' : 'Subscriber'}). Reason: ${error}` + ); return this.awaitWebRtcPeerConnectionState(1); } } @@ -1492,7 +1635,11 @@ export class Stream { const isWsConnected = await this.isWebsocketConnected(event, 3000); if (isWsConnected) { // There is connection to openvidu-server. The RTCPeerConnection is the only one broken - logger.log(`[${event}] Trying to reconnect stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) and the websocket is opened`); + logger.log( + `[${event}] Trying to reconnect stream ${this.streamId} (${ + this.isLocal() ? 'Publisher' : 'Subscriber' + }) and the websocket is opened` + ); if (this.isLocal()) { return this.initWebRtcPeerSend(true); } else { @@ -1501,7 +1648,9 @@ export class Stream { } else { // There is no connection to openvidu-server. Nothing can be done. The automatic reconnection // feature should handle a possible reconnection of RTCPeerConnection in case network comes back - const errorMsg = `[${event}] Trying to reconnect stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) but the websocket wasn't opened`; + const errorMsg = `[${event}] Trying to reconnect stream ${this.streamId} (${ + this.isLocal() ? 'Publisher' : 'Subscriber' + }) but the websocket wasn't opened`; logger.error(errorMsg); throw Error(errorMsg); } @@ -1580,9 +1729,10 @@ export class Stream { private getIceServersConf(): RTCIceServer[] | undefined { let returnValue; if (!!this.session.openvidu.advancedConfiguration.iceServers) { - returnValue = this.session.openvidu.advancedConfiguration.iceServers === 'freeice' ? - undefined : - this.session.openvidu.advancedConfiguration.iceServers; + returnValue = + this.session.openvidu.advancedConfiguration.iceServers === 'freeice' + ? undefined + : this.session.openvidu.advancedConfiguration.iceServers; } else if (this.session.openvidu.iceServers) { returnValue = this.session.openvidu.iceServers; } else { @@ -1594,16 +1744,14 @@ export class Stream { private gatherStatsForPeer(): Promise { return new Promise((resolve, reject) => { if (this.isLocal()) { - // Publisher stream stats - this.getRTCPeerConnection().getSenders().forEach(sender => sender.getStats() - .then( - response => { - response.forEach(report => { - + this.getRTCPeerConnection() + .getSenders() + .forEach((sender) => + sender.getStats().then((response) => { + response.forEach((report) => { if (this.isReportWanted(report)) { - const finalReport = {}; finalReport['type'] = report.type; @@ -1625,7 +1773,7 @@ export class Stream { finalReport['mediaType'] = report.mediaType; } else { // Safari does not have 'mediaType' defined for inbound-rtp. Must be inferred from 'id' field - finalReport['mediaType'] = (report.id.indexOf('VideoStream') !== -1) ? 'video' : 'audio'; + finalReport['mediaType'] = report.id.indexOf('VideoStream') !== -1 ? 'video' : 'audio'; } if (finalReport['mediaType'] === 'video') { @@ -1646,24 +1794,22 @@ export class Stream { // Only for Firefox >= 66.0 if (report.type === 'remote-inbound-rtp' || report.type === 'remote-outbound-rtp') { - } logger.log(finalReport); } }); - })); + }) + ); } else { - // Subscriber stream stats - this.getRTCPeerConnection().getReceivers().forEach(receiver => receiver.getStats() - .then( - response => { - response.forEach(report => { - + this.getRTCPeerConnection() + .getReceivers() + .forEach((receiver) => + receiver.getStats().then((response) => { + response.forEach((report) => { if (this.isReportWanted(report)) { - const finalReport = {}; finalReport['type'] = report.type; @@ -1685,7 +1831,7 @@ export class Stream { finalReport['mediaType'] = report.mediaType; } else { // Safari does not have 'mediaType' defined for inbound-rtp. Must be inferred from 'id' field - finalReport['mediaType'] = (report.id.indexOf('VideoStream') !== -1) ? 'video' : 'audio'; + finalReport['mediaType'] = report.id.indexOf('VideoStream') !== -1 ? 'video' : 'audio'; } if (finalReport['mediaType'] === 'video') { @@ -1708,21 +1854,21 @@ export class Stream { // Only for Firefox >= 66.0 if (report.type === 'remote-inbound-rtp' || report.type === 'remote-outbound-rtp') { - } logger.log(finalReport); } - }) + }); }) - ) + ); } }); } private isReportWanted(report: any): boolean { - return report.type === 'inbound-rtp' && !this.isLocal() || - report.type === 'outbound-rtp' && this.isLocal() || - (report.type === 'candidate-pair' && report.nominated && report.bytesSent > 0); + return ( + (report.type === 'inbound-rtp' && !this.isLocal()) || + (report.type === 'outbound-rtp' && this.isLocal()) || + (report.type === 'candidate-pair' && report.nominated && report.bytesSent > 0) + ); } - } diff --git a/openvidu-browser/src/OpenVidu/StreamManager.ts b/openvidu-browser/src/OpenVidu/StreamManager.ts index 56b06f40..3e90ecc2 100644 --- a/openvidu-browser/src/OpenVidu/StreamManager.ts +++ b/openvidu-browser/src/OpenVidu/StreamManager.ts @@ -48,7 +48,6 @@ let platform: PlatformUtils; * See available event listeners at [[StreamManagerEventMap]]. */ export abstract class StreamManager extends EventDispatcher { - /** * The Stream represented in the DOM by the Publisher/Subscriber */ @@ -126,7 +125,14 @@ export abstract class StreamManager extends EventDispatcher { id: '', canplayListenerAdded: false }; - if (platform.isSafariBrowser() || (platform.isIPhoneOrIPad() && (platform.isChromeMobileBrowser() || platform.isEdgeMobileBrowser() || platform.isOperaMobileBrowser() || platform.isFirefoxMobileBrowser()))) { + if ( + platform.isSafariBrowser() || + (platform.isIPhoneOrIPad() && + (platform.isChromeMobileBrowser() || + platform.isEdgeMobileBrowser() || + platform.isOperaMobileBrowser() || + platform.isFirefoxMobileBrowser())) + ) { this.firstVideoElement.video.playsInline = true; } this.targetElement = targEl; @@ -144,7 +150,6 @@ export abstract class StreamManager extends EventDispatcher { * See [[EventDispatcher.on]] */ on(type: K, handler: (event: StreamManagerEventMap[K]) => void): this { - super.onAux(type, "Event '" + type + "' triggered by '" + (this.remote ? 'Subscriber' : 'Publisher') + "'", handler); if (type === 'videoElementCreated') { @@ -154,11 +159,14 @@ export abstract class StreamManager extends EventDispatcher { } } if (type === 'streamPlaying') { - if (this.videos[0] && this.videos[0].video && + if ( + this.videos[0] && + this.videos[0].video && this.videos[0].video.currentTime > 0 && this.videos[0].video.paused === false && this.videos[0].video.ended === false && - this.videos[0].video.readyState === 4) { + this.videos[0].video.readyState === 4 + ) { this.ee.emitEvent('streamPlaying', [new StreamManagerEvent(this, 'streamPlaying', undefined)]); } } @@ -180,7 +188,6 @@ export abstract class StreamManager extends EventDispatcher { * See [[EventDispatcher.once]] */ once(type: K, handler: (event: StreamManagerEventMap[K]) => void): this { - super.onceAux(type, "Event '" + type + "' triggered once by '" + (this.remote ? 'Subscriber' : 'Publisher') + "'", handler); if (type === 'videoElementCreated') { @@ -189,11 +196,14 @@ export abstract class StreamManager extends EventDispatcher { } } if (type === 'streamPlaying') { - if (this.videos[0] && this.videos[0].video && + if ( + this.videos[0] && + this.videos[0].video && this.videos[0].video.currentTime > 0 && this.videos[0].video.paused === false && this.videos[0].video.ended === false && - this.videos[0].video.readyState === 4) { + this.videos[0].video.readyState === 4 + ) { this.ee.emitEvent('streamPlaying', [new StreamManagerEvent(this, 'streamPlaying', undefined)]); } } @@ -215,19 +225,20 @@ export abstract class StreamManager extends EventDispatcher { * See [[EventDispatcher.off]] */ off(type: K, handler?: (event: StreamManagerEventMap[K]) => void): this { - super.offAux(type, handler); if (type === 'publisherStartSpeaking') { // Both StreamManager and Session can have "publisherStartSpeaking" event listeners - const remainingStartSpeakingEventListeners = this.ee.getListeners(type).length + this.stream.session.ee.getListeners(type).length; + const remainingStartSpeakingEventListeners = + this.ee.getListeners(type).length + this.stream.session.ee.getListeners(type).length; if (remainingStartSpeakingEventListeners === 0) { this.stream.disableHarkSpeakingEvent(false); } } if (type === 'publisherStopSpeaking') { // Both StreamManager and Session can have "publisherStopSpeaking" event listeners - const remainingStopSpeakingEventListeners = this.ee.getListeners(type).length + this.stream.session.ee.getListeners(type).length; + const remainingStopSpeakingEventListeners = + this.ee.getListeners(type).length + this.stream.session.ee.getListeners(type).length; if (remainingStopSpeakingEventListeners === 0) { this.stream.disableHarkStoppedSpeakingEvent(false); } @@ -255,7 +266,6 @@ export abstract class StreamManager extends EventDispatcher { * Publisher/Subscriber and has been successfully disassociated from that one and properly added to this one. */ addVideoElement(video: HTMLVideoElement): number { - this.initializeVideoProperties(video); if (!this.remote && this.stream.displayMyRemote()) { @@ -280,7 +290,7 @@ export abstract class StreamManager extends EventDispatcher { } } - this.stream.session.streamManagers.forEach(streamManager => { + this.stream.session.streamManagers.forEach((streamManager) => { streamManager.disassociateVideo(video); }); @@ -370,12 +380,22 @@ export abstract class StreamManager extends EventDispatcher { * - `interval`: (number) how frequently the analyser polls the audio stream to check if speaking has started/stopped or audio volume has changed. Default **100** (ms) * - `threshold`: (number) the volume at which _publisherStartSpeaking_, _publisherStopSpeaking_ events will be fired. Default **-50** (dB) */ - updatePublisherSpeakingEventsOptions(publisherSpeakingEventsOptions: { interval?: number, threshold?: number }): void { - const currentHarkOptions = !!this.stream.harkOptions ? this.stream.harkOptions : (this.stream.session.openvidu.advancedConfiguration.publisherSpeakingEventsOptions || {}); - const newInterval = (typeof publisherSpeakingEventsOptions.interval === 'number') ? - publisherSpeakingEventsOptions.interval : ((typeof currentHarkOptions.interval === 'number') ? currentHarkOptions.interval : 100); - const newThreshold = (typeof publisherSpeakingEventsOptions.threshold === 'number') ? - publisherSpeakingEventsOptions.threshold : ((typeof currentHarkOptions.threshold === 'number') ? currentHarkOptions.threshold : -50); + updatePublisherSpeakingEventsOptions(publisherSpeakingEventsOptions: { interval?: number; threshold?: number }): void { + const currentHarkOptions = !!this.stream.harkOptions + ? this.stream.harkOptions + : this.stream.session.openvidu.advancedConfiguration.publisherSpeakingEventsOptions || {}; + const newInterval = + typeof publisherSpeakingEventsOptions.interval === 'number' + ? publisherSpeakingEventsOptions.interval + : typeof currentHarkOptions.interval === 'number' + ? currentHarkOptions.interval + : 100; + const newThreshold = + typeof publisherSpeakingEventsOptions.threshold === 'number' + ? publisherSpeakingEventsOptions.threshold + : typeof currentHarkOptions.threshold === 'number' + ? currentHarkOptions.threshold + : -50; this.stream.harkOptions = { interval: newInterval, threshold: newThreshold @@ -402,7 +422,14 @@ export abstract class StreamManager extends EventDispatcher { video.autoplay = true; video.controls = false; - if (platform.isSafariBrowser() || (platform.isIPhoneOrIPad() && (platform.isChromeMobileBrowser() || platform.isEdgeMobileBrowser() || platform.isOperaMobileBrowser() || platform.isFirefoxMobileBrowser()))) { + if ( + platform.isSafariBrowser() || + (platform.isIPhoneOrIPad() && + (platform.isChromeMobileBrowser() || + platform.isEdgeMobileBrowser() || + platform.isOperaMobileBrowser() || + platform.isFirefoxMobileBrowser())) + ) { video.playsInline = true; } @@ -440,7 +467,7 @@ export abstract class StreamManager extends EventDispatcher { } } - this.videos.forEach(streamManagerVideo => { + this.videos.forEach((streamManagerVideo) => { // Remove oncanplay event listener (only OpenVidu browser listener, not the user ones) if (!!streamManagerVideo.video && !!streamManagerVideo.video.removeEventListener) { streamManagerVideo.video.removeEventListener('canplay', this.canPlayListener); @@ -450,12 +477,14 @@ export abstract class StreamManager extends EventDispatcher { // Only remove from DOM videos created by OpenVidu Browser (those generated by passing a valid targetElement in OpenVidu.initPublisher // and Session.subscribe or those created by StreamManager.createVideoElement). All this videos triggered a videoElementCreated event streamManagerVideo.video.parentNode!.removeChild(streamManagerVideo.video); - this.ee.emitEvent('videoElementDestroyed', [new VideoElementEvent(streamManagerVideo.video, this, 'videoElementDestroyed')]); + this.ee.emitEvent('videoElementDestroyed', [ + new VideoElementEvent(streamManagerVideo.video, this, 'videoElementDestroyed') + ]); } // Remove srcObject from the video this.removeSrcObject(streamManagerVideo); // Remove from collection of videos every video managed by OpenVidu Browser - this.videos.filter(v => !v.targetElement); + this.videos.filter((v) => !v.targetElement); }); } @@ -480,7 +509,7 @@ export abstract class StreamManager extends EventDispatcher { * @hidden */ addPlayEventToFirstVideo() { - if ((!!this.videos[0]) && (!!this.videos[0].video) && (!this.videos[0].canplayListenerAdded)) { + if (!!this.videos[0] && !!this.videos[0].video && !this.videos[0].canplayListenerAdded) { this.activateStreamPlayingEventExceptionTimeout(); this.videos[0].video.addEventListener('canplay', this.canPlayListener); this.videos[0].canplayListenerAdded = true; @@ -491,7 +520,7 @@ export abstract class StreamManager extends EventDispatcher { * @hidden */ updateMediaStream(mediaStream: MediaStream) { - this.videos.forEach(streamManagerVideo => { + this.videos.forEach((streamManagerVideo) => { streamManagerVideo.video.srcObject = mediaStream; if (platform.isIonicIos()) { // iOS Ionic. LIMITATION: must reinsert the video in the DOM for @@ -512,8 +541,8 @@ export abstract class StreamManager extends EventDispatcher { } /** - * @hidden - */ + * @hidden + */ createVideo(): HTMLVideoElement { return document.createElement('video'); } @@ -569,9 +598,18 @@ export abstract class StreamManager extends EventDispatcher { // Trigger ExceptionEvent NO_STREAM_PLAYING_EVENT if after timeout there is no 'canplay' event const msTimeout = this.stream.session.openvidu.advancedConfiguration.noStreamPlayingEventExceptionTimeout || 4000; this.streamPlayingEventExceptionTimeout = setTimeout(() => { - const msg = 'StreamManager of Stream ' + this.stream.streamId + ' (' + (this.remote ? 'Subscriber' : 'Publisher') + ') did not trigger "streamPlaying" event in ' + msTimeout + ' ms'; + const msg = + 'StreamManager of Stream ' + + this.stream.streamId + + ' (' + + (this.remote ? 'Subscriber' : 'Publisher') + + ') did not trigger "streamPlaying" event in ' + + msTimeout + + ' ms'; logger.warn(msg); - this.stream.session.emitEvent('exception', [new ExceptionEvent(this.stream.session, ExceptionEventName.NO_STREAM_PLAYING_EVENT, (this) as Subscriber, msg)]); + this.stream.session.emitEvent('exception', [ + new ExceptionEvent(this.stream.session, ExceptionEventName.NO_STREAM_PLAYING_EVENT, (this) as Subscriber, msg) + ]); delete this.streamPlayingEventExceptionTimeout; }, msTimeout); } @@ -580,5 +618,4 @@ export abstract class StreamManager extends EventDispatcher { clearTimeout(this.streamPlayingEventExceptionTimeout as any); delete this.streamPlayingEventExceptionTimeout; } - } diff --git a/openvidu-browser/src/OpenVidu/Subscriber.ts b/openvidu-browser/src/OpenVidu/Subscriber.ts index 8210b91a..bcefd761 100644 --- a/openvidu-browser/src/OpenVidu/Subscriber.ts +++ b/openvidu-browser/src/OpenVidu/Subscriber.ts @@ -27,11 +27,10 @@ const logger: OpenViduLogger = OpenViduLogger.getInstance(); /** * Packs remote media streams. Participants automatically receive them when others publish their streams. Initialized with [[Session.subscribe]] method - * + * * See available event listeners at [[StreamManagerEventMap]]. */ export class Subscriber extends StreamManager { - /** * @hidden */ @@ -52,9 +51,12 @@ export class Subscriber extends StreamManager { * @param value `true` to subscribe to the audio stream, `false` to unsubscribe from it */ subscribeToAudio(value: boolean): Subscriber { - this.stream.getMediaStream().getAudioTracks().forEach((track) => { - track.enabled = value; - }); + this.stream + .getMediaStream() + .getAudioTracks() + .forEach((track) => { + track.enabled = value; + }); this.stream.audioActive = value; logger.info("'Subscriber' has " + (value ? 'subscribed to' : 'unsubscribed from') + ' its audio stream'); return this; @@ -65,9 +67,12 @@ export class Subscriber extends StreamManager { * @param value `true` to subscribe to the video stream, `false` to unsubscribe from it */ subscribeToVideo(value: boolean): Subscriber { - this.stream.getMediaStream().getVideoTracks().forEach((track) => { - track.enabled = value; - }); + this.stream + .getMediaStream() + .getVideoTracks() + .forEach((track) => { + track.enabled = value; + }); this.stream.videoActive = value; logger.info("'Subscriber' has " + (value ? 'subscribed to' : 'unsubscribed from') + ' its video stream'); return this; @@ -93,5 +98,4 @@ export class Subscriber extends StreamManager { removedTrack.stop(); mediaStream.addTrack(track); } - -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Enums/LocalRecorderState.ts b/openvidu-browser/src/OpenViduInternal/Enums/LocalRecorderState.ts index 1900b8b5..ac815d7a 100644 --- a/openvidu-browser/src/OpenViduInternal/Enums/LocalRecorderState.ts +++ b/openvidu-browser/src/OpenViduInternal/Enums/LocalRecorderState.ts @@ -20,4 +20,4 @@ export enum LocalRecorderState { RECORDING = 'RECORDING', PAUSED = 'PAUSED', FINISHED = 'FINISHED' -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Enums/OpenViduError.ts b/openvidu-browser/src/OpenViduInternal/Enums/OpenViduError.ts index eadfb0c8..2f74def1 100644 --- a/openvidu-browser/src/OpenViduInternal/Enums/OpenViduError.ts +++ b/openvidu-browser/src/OpenViduInternal/Enums/OpenViduError.ts @@ -19,7 +19,6 @@ * Defines property [[OpenViduError.name]] */ export enum OpenViduErrorName { - /** * Browser is not supported by OpenVidu. * Returned upon unsuccessful [[Session.connect]] @@ -38,7 +37,7 @@ export enum OpenViduErrorName { * error occurred at the OS, browser or web page level, which prevented access to the device. * Returned upon unsuccessful [[OpenVidu.initPublisher]] or [[OpenVidu.getUserMedia]] */ - DEVICE_ALREADY_IN_USE = "DEVICE_ALREADY_IN_USE", + DEVICE_ALREADY_IN_USE = 'DEVICE_ALREADY_IN_USE', /** * The user hasn't granted permissions to capture some desktop screen when the browser asked for them. @@ -122,7 +121,6 @@ export enum OpenViduErrorName { * Simple object to identify runtime errors on the client side */ export class OpenViduError { - /** * Uniquely identifying name of the error */ @@ -140,5 +138,4 @@ export class OpenViduError { this.name = name; this.message = message; } - -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Enums/TypeOfVideo.ts b/openvidu-browser/src/OpenViduInternal/Enums/TypeOfVideo.ts index 6edcb221..863d911b 100644 --- a/openvidu-browser/src/OpenViduInternal/Enums/TypeOfVideo.ts +++ b/openvidu-browser/src/OpenViduInternal/Enums/TypeOfVideo.ts @@ -20,4 +20,4 @@ export enum TypeOfVideo { SCREEN = 'SCREEN', CUSTOM = 'CUSTOM', IPCAM = 'IPCAM' -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Enums/VideoInsertMode.ts b/openvidu-browser/src/OpenViduInternal/Enums/VideoInsertMode.ts index 982a9ddd..136054b1 100644 --- a/openvidu-browser/src/OpenViduInternal/Enums/VideoInsertMode.ts +++ b/openvidu-browser/src/OpenViduInternal/Enums/VideoInsertMode.ts @@ -19,7 +19,6 @@ * How the video will be inserted in the DOM for Publishers and Subscribers. See [[PublisherProperties.insertMode]] and [[SubscriberProperties.insertMode]] */ export enum VideoInsertMode { - /** * Video inserted after the target element (as next sibling) */ @@ -40,5 +39,4 @@ export enum VideoInsertMode { * Video replaces target element */ REPLACE = 'REPLACE' - -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Events/ConnectionEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/ConnectionEvent.ts index 69ca9ac2..5a94b8d7 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/ConnectionEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/ConnectionEvent.ts @@ -19,14 +19,12 @@ import { Event } from './Event'; import { Connection } from '../../OpenVidu/Connection'; import { Session } from '../../OpenVidu/Session'; - /** * Triggered by: * - [[connectionCreated]] * - [[connectionDestroyed]] */ export class ConnectionEvent extends Event { - /** * Connection object that was created or destroyed */ @@ -58,6 +56,5 @@ export class ConnectionEvent extends Event { * @hidden */ // tslint:disable-next-line:no-empty - callDefaultBehavior() { } - -} \ No newline at end of file + callDefaultBehavior() {} +} diff --git a/openvidu-browser/src/OpenViduInternal/Events/ConnectionPropertyChangedEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/ConnectionPropertyChangedEvent.ts index 10e73273..b34eb172 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/ConnectionPropertyChangedEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/ConnectionPropertyChangedEvent.ts @@ -25,7 +25,6 @@ import { Event } from './Event'; * Triggered by [[connectionPropertyChanged]] */ export class ConnectionPropertyChangedEvent extends Event { - /** * The Connection whose property has changed */ @@ -61,6 +60,5 @@ export class ConnectionPropertyChangedEvent extends Event { * @hidden */ // tslint:disable-next-line:no-empty - callDefaultBehavior() { } - + callDefaultBehavior() {} } diff --git a/openvidu-browser/src/OpenViduInternal/Events/Event.ts b/openvidu-browser/src/OpenViduInternal/Events/Event.ts index 85ea7640..20d7c1d4 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/Event.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/Event.ts @@ -20,7 +20,6 @@ import { StreamManager } from '../../OpenVidu/StreamManager'; import { Session } from '../../OpenVidu/Session'; export abstract class Event { - /** * Whether the event has a default behavior that may be prevented by calling [[Event.preventDefault]] */ @@ -73,7 +72,7 @@ export abstract class Event { */ preventDefault() { // tslint:disable-next-line:no-empty - this.callDefaultBehavior = () => { }; + this.callDefaultBehavior = () => {}; this.hasBeenPrevented = true; } @@ -81,5 +80,4 @@ export abstract class Event { * @hidden */ abstract callDefaultBehavior(); - -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Events/EventMap/EventMap.ts b/openvidu-browser/src/OpenViduInternal/Events/EventMap/EventMap.ts index e381108a..3a74210e 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/EventMap/EventMap.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/EventMap/EventMap.ts @@ -18,4 +18,4 @@ /** * All OpenVidu Browser events inherit from this interface */ -export interface EventMap { } \ No newline at end of file +export interface EventMap {} diff --git a/openvidu-browser/src/OpenViduInternal/Events/EventMap/PublisherEventMap.ts b/openvidu-browser/src/OpenViduInternal/Events/EventMap/PublisherEventMap.ts index 44d47ffc..5ea6f818 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/EventMap/PublisherEventMap.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/EventMap/PublisherEventMap.ts @@ -21,19 +21,18 @@ import { StreamManagerEventMap } from './StreamManagerEventMap'; /** * Events dispatched by [[Publisher]] object. Manage event listeners with * [[Publisher.on]], [[Publisher.once]] and [[Publisher.off]] methods. - * + * * Example: - * + * * ```javascript - * publisher.on('accessDenied', () => { + * publisher.on('accessDenied', () => { * console.error('Camera access has been denied!'); * } - * + * * publisher.off('accessDenied'); * ``` */ export interface PublisherEventMap extends StreamManagerEventMap { - /** * Event dispatched when the [[Publisher]] has been published to the session (see [[Session.publish]]). */ @@ -46,7 +45,7 @@ export interface PublisherEventMap extends StreamManagerEventMap { /** * Event dispatched when a Publisher tries to access some media input device and has the required permissions to do so. - * + * * This happens when calling [[OpenVidu.initPublisher]] or [[OpenVidu.initPublisherAsync]] and the application * has permissions to use the devices. This usually means the user has accepted the permissions dialog that the * browser will show when trying to access the camera/microphone/screen. @@ -55,7 +54,7 @@ export interface PublisherEventMap extends StreamManagerEventMap { /** * Event dispatched when a Publisher tries to access some media input device and does NOT have the required permissions to do so. - * + * * This happens when calling [[OpenVidu.initPublisher]] or [[OpenVidu.initPublisherAsync]] and the application * lacks the required permissions to use the devices. This usually means the user has NOT accepted the permissions dialog that the * browser will show when trying to access the camera/microphone/screen. @@ -64,7 +63,7 @@ export interface PublisherEventMap extends StreamManagerEventMap { /** * Event dispatched when the pop-up shown by the browser to request permissions for the input media devices is opened. - * + * * You can use this event to alert the user about granting permissions for your website. Note that this event is artificially * generated based only on time intervals when accessing media devices. A heavily overloaded client device that simply takes more * than usual to access the media device could produce a false trigger of this event. @@ -74,8 +73,8 @@ export interface PublisherEventMap extends StreamManagerEventMap { /** * Event dispatched after the user clicks on "Allow" or "Block" in the pop-up shown by the browser to request permissions * for the input media devices. - * + * * This event can only be triggered after an [[accessDialogOpened]] event has been previously triggered. */ accessDialogClosed: never; -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Events/EventMap/SessionEventMap.ts b/openvidu-browser/src/OpenViduInternal/Events/EventMap/SessionEventMap.ts index 90eb4045..1656379a 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/EventMap/SessionEventMap.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/EventMap/SessionEventMap.ts @@ -30,19 +30,18 @@ import { StreamPropertyChangedEvent } from '../StreamPropertyChangedEvent'; /** * Events dispatched by [[Session]] object. Manage event listeners with * [[Session.on]], [[Session.once]] and [[Session.off]] methods. - * + * * Example: - * + * * ```javascript - * session.on('connectionCreated', (event) => { + * session.on('connectionCreated', (event) => { * console.log('Connection ' + event.connection.connectionId + ' created'); * } - * + * * session.off('connectionDestroyed'); * ``` */ export interface SessionEventMap extends EventMap { - /** * Event dispatched when a new user has connected to the session. * diff --git a/openvidu-browser/src/OpenViduInternal/Events/EventMap/StreamManagerEventMap.ts b/openvidu-browser/src/OpenViduInternal/Events/EventMap/StreamManagerEventMap.ts index a7e8dc53..b6fe1287 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/EventMap/StreamManagerEventMap.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/EventMap/StreamManagerEventMap.ts @@ -22,21 +22,20 @@ import { StreamPropertyChangedEvent } from '../StreamPropertyChangedEvent'; import { VideoElementEvent } from '../VideoElementEvent'; /** - * Events dispatched by [[StreamManager]] object. Manage event listeners with + * Events dispatched by [[StreamManager]] object. Manage event listeners with * [[StreamManager.on]], [[StreamManager.once]] and [[StreamManager.off]] methods. - * + * * Example: - * + * * ```javascript - * streamManager.on('videoElementCreated', (event) => { + * streamManager.on('videoElementCreated', (event) => { * console.log('New video element created:', event.element); * } - * + * * streamManager.off('videoElementCreated'); * ``` */ export interface StreamManagerEventMap extends EventMap { - /** * Event dispatched when a new HTML video element has been inserted into DOM by OpenVidu Browser library. See * [Manage video players](/en/stable/cheatsheet/manage-videos) section. @@ -73,7 +72,7 @@ export interface StreamManagerEventMap extends EventMap { /** * Event dispatched when the user owning the stream has started speaking. - * + * * Extra information: * - This event will only be triggered for **streams that have audio tracks** ([[Stream.hasAudio]] must be true). * - Further configuration can be applied on how the event is dispatched by setting property `publisherSpeakingEventsOptions` in the call of [[OpenVidu.setAdvancedConfiguration]]. @@ -82,10 +81,10 @@ export interface StreamManagerEventMap extends EventMap { /** * Event dispatched when the user owning the stream has stopped speaking. - * + * * Extra information: * - This event will only be triggered for **streams that have audio tracks** ([[Stream.hasAudio]] must be true). * - Further configuration can be applied on how the event is dispatched by setting property `publisherSpeakingEventsOptions` in the call of [[OpenVidu.setAdvancedConfiguration]]. */ publisherStopSpeaking: PublisherSpeakingEvent; -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Events/ExceptionEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/ExceptionEvent.ts index 5d923284..4f4da9a0 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/ExceptionEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/ExceptionEvent.ts @@ -20,15 +20,13 @@ import { Stream } from '../../OpenVidu/Stream'; import { Subscriber } from '../../OpenVidu/Subscriber'; import { Event } from './Event'; - /** * Defines property [[ExceptionEvent.name]] */ export enum ExceptionEventName { - /** * There was an unexpected error on the server-side processing an ICE candidate generated and sent by the client-side. - * + * * [[ExceptionEvent]] objects with this [[ExceptionEvent.name]] will have as [[ExceptionEvent.origin]] property a [[Session]] object. */ ICE_CANDIDATE_ERROR = 'ICE_CANDIDATE_ERROR', @@ -36,11 +34,11 @@ export enum ExceptionEventName { /** * The [ICE connection state](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceConnectionState) * of an [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) reached `failed` status. - * + * * This is a terminal error that won't have any kind of possible recovery. If the client is still connected to OpenVidu Server, * then an automatic reconnection process of the media stream is immediately performed. If the ICE connection has broken due to * a total network drop, then no automatic reconnection process will be possible. - * + * * [[ExceptionEvent]] objects with this [[ExceptionEvent.name]] will have as [[ExceptionEvent.origin]] property a [[Stream]] object. */ ICE_CONNECTION_FAILED = 'ICE_CONNECTION_FAILED', @@ -48,15 +46,15 @@ export enum ExceptionEventName { /** * The [ICE connection state](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceConnectionState) * of an [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) reached `disconnected` status. - * + * * This is not a terminal error, and it is possible for the ICE connection to be reconnected. If the client is still connected to * OpenVidu Server and after certain timeout the ICE connection has not reached a success or terminal status, then an automatic * reconnection process of the media stream is performed. If the ICE connection has broken due to a total network drop, then no * automatic reconnection process will be possible. - * + * * You can customize the timeout for the reconnection attempt with property [[OpenViduAdvancedConfiguration.iceConnectionDisconnectedExceptionTimeout]], * which by default is 4000 milliseconds. - * + * * [[ExceptionEvent]] objects with this [[ExceptionEvent.name]] will have as [[ExceptionEvent.origin]] property a [[Stream]] object. */ ICE_CONNECTION_DISCONNECTED = 'ICE_CONNECTION_DISCONNECTED', @@ -64,20 +62,20 @@ export enum ExceptionEventName { /** * A [[Subscriber]] object has not fired event `streamPlaying` after certain timeout. `streamPlaying` event belongs to [[StreamManagerEvent]] * category. It wraps Web API native event [canplay](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canplay_event). - * + * * OpenVidu Browser can take care of the video players (see [here](/en/stable/cheatsheet/manage-videos/#let-openvidu-take-care-of-the-video-players)), * or you can take care of video players on your own (see [here](/en/stable/cheatsheet/manage-videos/#you-take-care-of-the-video-players)). * Either way, whenever a [[Subscriber]] object is commanded to attach its [[Stream]] to a video element, it is supposed to fire `streamPlaying` - * event shortly after. If it does not, then we can safely assume that something wrong has happened while playing the remote video and the + * event shortly after. If it does not, then we can safely assume that something wrong has happened while playing the remote video and the * application may be notified through this specific ExceptionEvent. - * + * * The timeout can be configured with property [[OpenViduAdvancedConfiguration.noStreamPlayingEventExceptionTimeout]]. By default it is 4000 milliseconds. - * + * * This is just an informative exception. It only means that a remote Stream that is supposed to be playing by a video player has not done so * in a reasonable time. But the lack of the event can be caused by multiple reasons. If a Subscriber is not playing its Stream, the origin * of the problem could be located at the Publisher side. Or may be caused by a transient network problem. But it also could be a problem with * autoplay permissions. Bottom line, the cause can be very varied, and depending on the application the lack of the event could even be expected. - * + * * [[ExceptionEvent]] objects with this [[ExceptionEvent.name]] will have as [[ExceptionEvent.origin]] property a [[Subscriber]] object. */ NO_STREAM_PLAYING_EVENT = 'NO_STREAM_PLAYING_EVENT' @@ -87,7 +85,6 @@ export enum ExceptionEventName { * Triggered by [[SessionEventMap.exception]] */ export class ExceptionEvent extends Event { - /** * Name of the exception */ @@ -126,6 +123,5 @@ export class ExceptionEvent extends Event { * @hidden */ // tslint:disable-next-line:no-empty - callDefaultBehavior() { } - -} \ No newline at end of file + callDefaultBehavior() {} +} diff --git a/openvidu-browser/src/OpenViduInternal/Events/FilterEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/FilterEvent.ts index b3eb203a..b13963b1 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/FilterEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/FilterEvent.ts @@ -18,12 +18,10 @@ import { Event } from './Event'; import { Filter } from '../../OpenVidu/Filter'; - /** * Defines every event dispatched by audio/video stream filters. You can subscribe to filter events by calling [[Filter.addEventListener]] */ export class FilterEvent extends Event { - /** * Data of the event */ @@ -41,6 +39,5 @@ export class FilterEvent extends Event { * @hidden */ // tslint:disable-next-line:no-empty - callDefaultBehavior() { } - -} \ No newline at end of file + callDefaultBehavior() {} +} diff --git a/openvidu-browser/src/OpenViduInternal/Events/NetworkQualityLevelChangedEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/NetworkQualityLevelChangedEvent.ts index fe79c2e3..071b3b21 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/NetworkQualityLevelChangedEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/NetworkQualityLevelChangedEvent.ts @@ -23,7 +23,6 @@ import { Connection } from '../../OpenVidu/Connection'; * Triggered by [[networkQualityLevelChanged]] */ export class NetworkQualityLevelChangedEvent extends Event { - /** * New value of the network quality level */ @@ -37,7 +36,7 @@ export class NetworkQualityLevelChangedEvent extends Event { /** * Connection for whom the network quality level changed */ - connection: Connection + connection: Connection; /** * @hidden @@ -53,6 +52,5 @@ export class NetworkQualityLevelChangedEvent extends Event { * @hidden */ // tslint:disable-next-line:no-empty - callDefaultBehavior() { } - + callDefaultBehavior() {} } diff --git a/openvidu-browser/src/OpenViduInternal/Events/PublisherSpeakingEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/PublisherSpeakingEvent.ts index 89f82370..b6be41fd 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/PublisherSpeakingEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/PublisherSpeakingEvent.ts @@ -20,14 +20,12 @@ import { Connection } from '../../OpenVidu/Connection'; import { Session } from '../../OpenVidu/Session'; import { StreamManager } from '../../OpenVidu/StreamManager'; - /** * Triggered by: * - `publisherStartSpeaking` (available for [Session](/en/stable/api/openvidu-browser/interfaces/SessionEventMap.html#publisherStartSpeaking) and [StreamManager](/en/stable/api/openvidu-browser/interfaces/StreamManagerEventMap.html#publisherStartSpeaking) objects) * - `publisherStopSpeaking` (available for [Session](/en/stable/api/openvidu-browser/interfaces/SessionEventMap.html#publisherStopSpeaking) and [StreamManager](/en/stable/api/openvidu-browser/interfaces/StreamManagerEventMap.html#publisherStopSpeaking) objects) */ export class PublisherSpeakingEvent extends Event { - /** * The client that started or stopped speaking */ @@ -52,6 +50,5 @@ export class PublisherSpeakingEvent extends Event { * @hidden */ // tslint:disable-next-line:no-empty - callDefaultBehavior() { } - -} \ No newline at end of file + callDefaultBehavior() {} +} diff --git a/openvidu-browser/src/OpenViduInternal/Events/RecordingEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/RecordingEvent.ts index c1c366f6..2f205708 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/RecordingEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/RecordingEvent.ts @@ -18,14 +18,12 @@ import { Event } from './Event'; import { Session } from '../../OpenVidu/Session'; - /** * Triggered by: * - [[recordingStarted]] * - [[recordingStopped]] */ export class RecordingEvent extends Event { - /** * The recording ID generated in openvidu-server */ @@ -68,6 +66,5 @@ export class RecordingEvent extends Event { * @hidden */ // tslint:disable-next-line:no-empty - callDefaultBehavior() { } - -} \ No newline at end of file + callDefaultBehavior() {} +} diff --git a/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts index 986e1c72..fb663374 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts @@ -24,12 +24,10 @@ import { OpenViduLogger } from '../Logger/OpenViduLogger'; */ const logger: OpenViduLogger = OpenViduLogger.getInstance(); - /** * Triggered by [[sessionDisconnected]] */ export class SessionDisconnectedEvent extends Event { - /** * - "disconnect": you have called `Session.disconnect()` * - "forceDisconnectByUser": you have been evicted from the Session by other user calling `Session.forceDisconnect()` @@ -39,7 +37,7 @@ export class SessionDisconnectedEvent extends Event { * 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" - * - "nodeCrashed": a node has crashed in the server side. You can use this reason to ask your application's backend to reconnect + * - "nodeCrashed": a node has crashed in the server side. You can use this reason to ask your application's backend to reconnect * to a new session to replace the crashed one */ reason: string; @@ -56,13 +54,12 @@ export class SessionDisconnectedEvent extends Event { * @hidden */ callDefaultBehavior() { - logger.info("Calling default behavior upon '" + this.type + "' event dispatched by 'Session'"); const session = this.target; // Dispose and delete all remote Connections - session.remoteConnections.forEach(remoteConnection => { + session.remoteConnections.forEach((remoteConnection) => { const connectionId = remoteConnection.connectionId; if (!!session.remoteConnections.get(connectionId)?.stream) { session.remoteConnections.get(connectionId)?.stream!.disposeWebRtcPeer(); @@ -79,5 +76,4 @@ export class SessionDisconnectedEvent extends Event { session.remoteConnections.delete(connectionId); }); } - -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Events/SignalEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/SignalEvent.ts index 02fe723e..f5b7a441 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/SignalEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/SignalEvent.ts @@ -19,14 +19,12 @@ import { Event } from './Event'; import { Connection } from '../../OpenVidu/Connection'; import { Session } from '../../OpenVidu/Session'; - /** * Triggered by [[SessionEventMap.signal]] */ export class SignalEvent extends Event { - /** - * The type of signal. It is string `"signal"` for those signals sent with no [[SignalOptions.type]] property, and `"signal:type"` if was sent with a + * The type of signal. It is string `"signal"` for those signals sent with no [[SignalOptions.type]] property, and `"signal:type"` if was sent with a * valid [[SignalOptions.type]] property. * * The client must be specifically subscribed to `Session.on('signal:type', function(signalEvent) {...})` to trigger that type of signal. @@ -62,6 +60,5 @@ export class SignalEvent extends Event { * @hidden */ // tslint:disable-next-line:no-empty - callDefaultBehavior() { } - -} \ No newline at end of file + callDefaultBehavior() {} +} diff --git a/openvidu-browser/src/OpenViduInternal/Events/StreamEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/StreamEvent.ts index 8fa6b0e1..9de3040c 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/StreamEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/StreamEvent.ts @@ -32,7 +32,6 @@ const logger: OpenViduLogger = OpenViduLogger.getInstance(); * - `streamDestroyed` (available for [Session](/en/stable/api/openvidu-browser/interfaces/SessionEventMap.html#streamDestroyed) and [Publisher](/en/stable/api/openvidu-browser/interfaces/PublisherEventMap.html#streamDestroyed) objects) */ export class StreamEvent extends Event { - /** * Stream object that was created or destroyed */ @@ -68,7 +67,6 @@ export class StreamEvent extends Event { */ callDefaultBehavior() { if (this.type === 'streamDestroyed') { - if (this.target instanceof Session) { // Remote Stream logger.info("Calling default behavior upon '" + this.type + "' event dispatched by 'Session'"); @@ -82,7 +80,7 @@ export class StreamEvent extends Event { // Delete Publisher object from OpenVidu publishers array const openviduPublishers = (this.target).openvidu.publishers; for (let i = 0; i < openviduPublishers.length; i++) { - if (openviduPublishers[i] === (this.target)) { + if (openviduPublishers[i] === this.target) { openviduPublishers.splice(i, 1); break; } @@ -109,8 +107,6 @@ export class StreamEvent extends Event { } } } - } } - -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Events/StreamManagerEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/StreamManagerEvent.ts index 30c25bfb..c1a0c20d 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/StreamManagerEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/StreamManagerEvent.ts @@ -24,12 +24,11 @@ import { StreamManager } from '../../OpenVidu/StreamManager'; * - [[streamAudioVolumeChange]] */ export class StreamManagerEvent extends Event { - /** * For `streamAudioVolumeChange` event: * - `{newValue: number, oldValue: number}`: new and old audio volume values. These values are between -100 (silence) and 0 (loudest possible volume). * They are not exact and depend on how the browser is managing the audio track, but -100 and 0 can be taken as limit values. - * + * * For `streamPlaying` event undefined */ value: Object | undefined; @@ -46,6 +45,5 @@ export class StreamManagerEvent extends Event { * @hidden */ // tslint:disable-next-line:no-empty - callDefaultBehavior() { } - -} \ No newline at end of file + callDefaultBehavior() {} +} diff --git a/openvidu-browser/src/OpenViduInternal/Events/StreamPropertyChangedEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/StreamPropertyChangedEvent.ts index db5f7a29..fa262058 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/StreamPropertyChangedEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/StreamPropertyChangedEvent.ts @@ -24,7 +24,6 @@ import { StreamManager } from '../../OpenVidu/StreamManager'; * Triggered by `streamPropertyChanged` (available for [Session](/en/stable/api/openvidu-browser/interfaces/SessionEventMap.html#streamPropertyChanged) and [StreamManager](/en/stable/api/openvidu-browser/interfaces/StreamManagerEventMap.html#streamPropertyChanged) objects) */ export class StreamPropertyChangedEvent extends Event { - /** * The Stream whose property has changed. You can always identify the user publishing the changed stream by consulting property [[Stream.connection]] */ @@ -57,7 +56,14 @@ export class StreamPropertyChangedEvent extends Event { /** * @hidden */ - constructor(target: Session | StreamManager, stream: Stream, changedProperty: string, newValue: Object, oldValue: Object, reason: string) { + constructor( + target: Session | StreamManager, + stream: Stream, + changedProperty: string, + newValue: Object, + oldValue: Object, + reason: string + ) { super(false, target, 'streamPropertyChanged'); this.stream = stream; this.changedProperty = changedProperty; @@ -70,6 +76,5 @@ export class StreamPropertyChangedEvent extends Event { * @hidden */ // tslint:disable-next-line:no-empty - callDefaultBehavior() { } - -} \ No newline at end of file + callDefaultBehavior() {} +} diff --git a/openvidu-browser/src/OpenViduInternal/Events/VideoElementEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/VideoElementEvent.ts index e8010bf8..496ce61c 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/VideoElementEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/VideoElementEvent.ts @@ -18,14 +18,12 @@ import { Event } from './Event'; import { StreamManager } from '../../OpenVidu/StreamManager'; - /** * Triggered by: * - [[videoElementCreated]] * - [[videoElementDestroyed]] */ export class VideoElementEvent extends Event { - /** * Video element that was created or destroyed */ @@ -43,6 +41,5 @@ export class VideoElementEvent extends Event { * @hidden */ // tslint:disable-next-line:no-empty - callDefaultBehavior() { } - -} \ No newline at end of file + callDefaultBehavior() {} +} diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/CustomMediaStreamConstraints.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/CustomMediaStreamConstraints.ts index a371dc93..cddadb1e 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/CustomMediaStreamConstraints.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/CustomMediaStreamConstraints.ts @@ -19,4 +19,4 @@ export interface CustomMediaStreamConstraints { constraints: MediaStreamConstraints; audioTrack: MediaStreamTrack | undefined; videoTrack: MediaStreamTrack | undefined; -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/IceServerProperties.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/IceServerProperties.ts index 38402e51..533e9a3e 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/IceServerProperties.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/IceServerProperties.ts @@ -18,4 +18,4 @@ export interface IceServerProperties { url: string; username?: string; credential?: string; -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/InboundStreamOptions.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/InboundStreamOptions.ts index 6d41d2fc..0a9a724a 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/InboundStreamOptions.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/InboundStreamOptions.ts @@ -29,6 +29,6 @@ export interface InboundStreamOptions { videoActive: boolean; typeOfVideo: TypeOfVideo; frameRate: number; - videoDimensions: { width: number, height: number }; + videoDimensions: { width: number; height: number }; filter?: Filter; -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/OutboundStreamOptions.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/OutboundStreamOptions.ts index b392f54a..e954c8b3 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/OutboundStreamOptions.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/OutboundStreamOptions.ts @@ -20,4 +20,4 @@ import { PublisherProperties } from '../Public/PublisherProperties'; export interface OutboundStreamOptions { publisherProperties: PublisherProperties; mediaConstraints: MediaStreamConstraints; -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/SessionOptions.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/SessionOptions.ts index b863a67b..efe276bc 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/SessionOptions.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/SessionOptions.ts @@ -19,4 +19,4 @@ export interface SessionOptions { sessionId: string; participantId: string; metadata: string; -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/SignalOptions.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/SignalOptions.ts index bb7faf49..19737379 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/SignalOptions.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/SignalOptions.ts @@ -21,4 +21,4 @@ export interface SignalOptions { type?: string; to?: Connection[]; data?: string; -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/StreamOptionsServer.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/StreamOptionsServer.ts index c0bb366d..f88c4954 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/StreamOptionsServer.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/StreamOptionsServer.ts @@ -29,4 +29,4 @@ export interface StreamOptionsServer { frameRate: number; videoDimensions: string; filter: Filter; -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/Capabilities.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/Capabilities.ts index b48241f8..a8d3745d 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/Capabilities.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/Capabilities.ts @@ -19,7 +19,6 @@ * See [[Session.capabilities]] */ export interface Capabilities { - /** * `true` if the client can call [[Session.forceDisconnect]], `false` if not */ @@ -39,5 +38,4 @@ export interface Capabilities { * `true` if the client can call [[Session.subscribe]], `false` if not (true for every user for now) */ subscribe: boolean; - -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/Device.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/Device.ts index d2538ed0..a0ceaf2f 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/Device.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/Device.ts @@ -19,7 +19,6 @@ * See [[OpenVidu.getDevices]] */ export interface Device { - /** * `"videoinput"`, `"audioinput"` */ @@ -34,4 +33,4 @@ export interface Device { * Description of the device. An empty string if the user hasn't granted permissions to the site to access the device */ label: string; -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/OpenViduAdvancedConfiguration.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/OpenViduAdvancedConfiguration.ts index 34a5fc50..2947a5b4 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/OpenViduAdvancedConfiguration.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/OpenViduAdvancedConfiguration.ts @@ -19,7 +19,6 @@ * See [[OpenVidu.setAdvancedConfiguration]] */ export interface OpenViduAdvancedConfiguration { - /** * Array of [RTCIceServer](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer) to be used by OpenVidu Browser. By default OpenVidu will generate the required credentials to use the COTURN server hosted along OpenVidu Server * You can also set this property to string 'freeice' to force the use of free STUN servers instead (got thanks to [freeice](https://github.com/DamonOehlman/freeice) library). @@ -36,7 +35,7 @@ export interface OpenViduAdvancedConfiguration { * Custom configuration for the [[PublisherSpeakingEvent]] feature and the [StreamManagerEvent.streamAudioVolumeChange](/en/stable/api/openvidu-browser/classes/StreamManagerEvent.html) feature. It is an object which includes the following optional properties: * - `interval`: (number) how frequently the analyser polls the audio stream to check if speaking has started/stopped or audio volume has changed. Default **100** (ms) * - `threshold`: (number) the volume at which _publisherStartSpeaking_ and _publisherStopSpeaking_ events will be fired. Default **-50** (dB) - * + * * This sets the global default configuration that will affect all streams, but you can later customize these values for each specific stream by calling [[StreamManager.updatePublisherSpeakingEventsOptions]] */ publisherSpeakingEventsOptions?: { @@ -47,10 +46,10 @@ export interface OpenViduAdvancedConfiguration { /** * Determines the automatic reconnection process policy. Whenever the client's network drops, OpenVidu Browser starts a reconnection process with OpenVidu Server. After network is recovered, OpenVidu Browser automatically * inspects all of its media streams to see their status. For any of them that are broken, it asks OpenVidu Server for a forced and silent reconnection. - * + * * This policy is technically enough to recover any broken media connection after a network drop, but in practice it has been proven that OpenVidu Browser may think a media connection has properly recovered when in fact it has not. * This is not a common case, and it only affects Publisher streams, but it may occur. This property allows **forcing OpenVidu Browser to reconnect all of its outgoing media streams** after a network drop regardless of their supposed status. - * + * * Default to `false`. */ forceMediaReconnectionAfterNetworkDrop?: boolean; @@ -59,16 +58,15 @@ export interface OpenViduAdvancedConfiguration { * The milliseconds that must elapse after triggering [[ExceptionEvent]] of name [`ICE_CONNECTION_DISCONNECTED`](/en/stable/api/openvidu-browser/enums/ExceptionEventName.html#ICE_CONNECTION_DISCONNECTED) to perform an automatic reconnection process of the affected media stream. * This automatic reconnection process can only take place if the client still has network connection to OpenVidu Server. If the ICE connection has broken because of a total network drop, * then no reconnection process will be possible at all. - * + * * Default to `4000`. */ iceConnectionDisconnectedExceptionTimeout?: number; /** * The milliseconds that must elapse for the [[ExceptionEvent]] of name [`NO_STREAM_PLAYING_EVENT`](/en/stable/api/openvidu-browser/enums/ExceptionEventName.html#NO_STREAM_PLAYING_EVENT) to be fired. - * + * * Default to `4000`. */ noStreamPlayingEventExceptionTimeout?: number; - } diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/PublisherProperties.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/PublisherProperties.ts index a151fa6b..1fe73839 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/PublisherProperties.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/PublisherProperties.ts @@ -22,7 +22,6 @@ import { VideoInsertMode } from '../../Enums/VideoInsertMode'; * See [[OpenVidu.initPublisher]] */ export interface PublisherProperties { - /** * Which device should provide the audio source. Can be: * - Property `deviceId` of a [[Device]] @@ -98,5 +97,4 @@ export interface PublisherProperties { * Define a filter to apply in the Publisher's stream */ filter?: Filter; - } diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/SignalOptions.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/SignalOptions.ts index 436975ff..a5f7c3ce 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/SignalOptions.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/SignalOptions.ts @@ -21,7 +21,6 @@ import { Connection } from '../../../OpenVidu/Connection'; * See [[Session.signal]] */ export interface SignalOptions { - /** * The actual message of the signal. */ @@ -38,4 +37,4 @@ export interface SignalOptions { * receive it. Participants subscribed to `Session.on('signal')` will receive all signals. */ type?: string; -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/StreamManagerVideo.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/StreamManagerVideo.ts index 1a6e1939..7b06dc71 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/StreamManagerVideo.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/StreamManagerVideo.ts @@ -17,9 +17,7 @@ import { VideoInsertMode } from '../../Enums/VideoInsertMode'; - export interface StreamManagerVideo { - /** * DOM video element displaying the StreamManager's stream */ @@ -56,6 +54,4 @@ export interface StreamManagerVideo { * @hidden */ canplayListenerAdded: boolean; - - -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/SubscriberProperties.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/SubscriberProperties.ts index 0baed6bf..53092d41 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/SubscriberProperties.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/SubscriberProperties.ts @@ -21,7 +21,6 @@ import { VideoInsertMode } from '../../Enums/VideoInsertMode'; * See [[Session.subscribe]] */ export interface SubscriberProperties { - /** * How the video element of the subscriber should be inserted in the DOM * @default VideoInsertMode.APPEND @@ -39,5 +38,4 @@ export interface SubscriberProperties { * @default true */ subscribeToVideo?: boolean; - -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/Mapper.js b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/Mapper.js index 55aeb85e..83c515d3 100644 --- a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/Mapper.js +++ b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/Mapper.js @@ -1,61 +1,52 @@ function Mapper() { - var sources = {}; + var sources = {}; + this.forEach = function (callback) { + for (var key in sources) { + var source = sources[key]; - this.forEach = function (callback) { - for (var key in sources) { - var source = sources[key]; - - for (var key2 in source) - callback(source[key2]); + for (var key2 in source) callback(source[key2]); + } }; - }; - this.get = function (id, source) { - var ids = sources[source]; - if (ids == undefined) - return undefined; + this.get = function (id, source) { + var ids = sources[source]; + if (ids == undefined) return undefined; - return ids[id]; - }; + return ids[id]; + }; - this.remove = function (id, source) { - var ids = sources[source]; - if (ids == undefined) - return; + this.remove = function (id, source) { + var ids = sources[source]; + if (ids == undefined) return; - delete ids[id]; + delete ids[id]; - // Check it's empty - for (var i in ids) { - return false - } + // Check it's empty + for (var i in ids) { + return false; + } - delete sources[source]; - }; + delete sources[source]; + }; - this.set = function (value, id, source) { - if (value == undefined) - return this.remove(id, source); + this.set = function (value, id, source) { + if (value == undefined) return this.remove(id, source); - var ids = sources[source]; - if (ids == undefined) - sources[source] = ids = {}; - - ids[id] = value; - }; -}; + var ids = sources[source]; + if (ids == undefined) sources[source] = ids = {}; + ids[id] = value; + }; +} Mapper.prototype.pop = function (id, source) { - var value = this.get(id, source); - if (value == undefined) - return undefined; + var value = this.get(id, source); + if (value == undefined) return undefined; - this.remove(id, source); + this.remove(id, source); - return value; + return value; }; - -module.exports = Mapper; \ No newline at end of file +module.exports = Mapper; diff --git a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/index.js b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/index.js index 149cc2df..46b9e3ab 100644 --- a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/index.js +++ b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/index.js @@ -17,5 +17,4 @@ var JsonRpcClient = require('./jsonrpcclient'); - -exports.JsonRpcClient = JsonRpcClient; \ No newline at end of file +exports.JsonRpcClient = JsonRpcClient; diff --git a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/jsonrpcclient.js b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/jsonrpcclient.js index b50a2fa7..ace0dd83 100644 --- a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/jsonrpcclient.js +++ b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/jsonrpcclient.js @@ -19,9 +19,11 @@ var RpcBuilder = require('../'); var WebSocketWithReconnection = require('./transports/webSocketWithReconnection'); var OpenViduLogger = require('../../../Logger/OpenViduLogger').OpenViduLogger; -Date.now = Date.now || function () { - return +new Date; -}; +Date.now = + Date.now || + function () { + return +new Date(); + }; var PING_INTERVAL = 5000; @@ -51,7 +53,6 @@ var Logger = OpenViduLogger.getInstance(); * */ function JsonRpcClient(configuration) { - var self = this; var wsConfig = configuration.ws; @@ -71,13 +72,13 @@ function JsonRpcClient(configuration) { var onerror = wsConfig.onerror; configuration.rpc.pull = function (params, request) { - request.reply(null, "push"); - } + request.reply(null, 'push'); + }; wsConfig.onreconnecting = function () { - Logger.debug("--------- ONRECONNECTING -----------"); + Logger.debug('--------- ONRECONNECTING -----------'); if (status === RECONNECTING) { - Logger.error("Websocket already in RECONNECTING state when receiving a new ONRECONNECTING message. Ignoring it"); + Logger.error('Websocket already in RECONNECTING state when receiving a new ONRECONNECTING message. Ignoring it'); return; } @@ -87,12 +88,12 @@ function JsonRpcClient(configuration) { if (onreconnecting) { onreconnecting(); } - } + }; wsConfig.onreconnected = function () { - Logger.debug("--------- ONRECONNECTED -----------"); + Logger.debug('--------- ONRECONNECTED -----------'); if (status === CONNECTED) { - Logger.error("Websocket already in CONNECTED state when receiving a new ONRECONNECTED message. Ignoring it"); + Logger.error('Websocket already in CONNECTED state when receiving a new ONRECONNECTED message. Ignoring it'); return; } status = CONNECTED; @@ -102,12 +103,12 @@ function JsonRpcClient(configuration) { if (onreconnected) { onreconnected(); } - } + }; wsConfig.onconnected = function () { - Logger.debug("--------- ONCONNECTED -----------"); + Logger.debug('--------- ONCONNECTED -----------'); if (status === CONNECTED) { - Logger.error("Websocket already in CONNECTED state when receiving a new ONCONNECTED message. Ignoring it"); + Logger.error('Websocket already in CONNECTED state when receiving a new ONCONNECTED message. Ignoring it'); return; } status = CONNECTED; @@ -118,10 +119,10 @@ function JsonRpcClient(configuration) { if (onconnected) { onconnected(); } - } + }; wsConfig.onerror = function (error) { - Logger.debug("--------- ONERROR -----------"); + Logger.debug('--------- ONERROR -----------'); status = DISCONNECTED; @@ -130,7 +131,7 @@ function JsonRpcClient(configuration) { if (onerror) { onerror(error); } - } + }; var ws = new WebSocketWithReconnection(wsConfig); @@ -141,37 +142,41 @@ function JsonRpcClient(configuration) { ping_request_timeout: configuration.rpc.heartbeatRequestTimeout }; - var rpc = new RpcBuilder(RpcBuilder.packers.JsonRPC, rpcBuilderOptions, ws, - function (request) { + var rpc = new RpcBuilder(RpcBuilder.packers.JsonRPC, rpcBuilderOptions, ws, function (request) { + Logger.debug('Received request: ' + JSON.stringify(request)); - Logger.debug('Received request: ' + JSON.stringify(request)); + try { + var func = configuration.rpc[request.method]; - try { - var func = configuration.rpc[request.method]; - - if (func === undefined) { - Logger.error("Method " + request.method + " not registered in client"); - } else { - func(request.params, request); - } - } catch (err) { - Logger.error('Exception processing request: ' + JSON.stringify(request)); - Logger.error(err); + if (func === undefined) { + Logger.error('Method ' + request.method + ' not registered in client'); + } else { + func(request.params, request); } - }); + } catch (err) { + Logger.error('Exception processing request: ' + JSON.stringify(request)); + Logger.error(err); + } + }); this.send = function (method, params, callback) { - var requestTime = Date.now(); rpc.encode(method, params, function (error, result) { if (error) { try { - Logger.error("ERROR:" + error.message + " in Request: method:" + - method + " params:" + JSON.stringify(params) + " request:" + - error.request); + Logger.error( + 'ERROR:' + + error.message + + ' in Request: method:' + + method + + ' params:' + + JSON.stringify(params) + + ' request:' + + error.request + ); if (error.data) { - Logger.error("ERROR DATA:" + JSON.stringify(error.data)); + Logger.error('ERROR DATA:' + JSON.stringify(error.data)); } } catch (e) {} error.requestTime = requestTime; @@ -183,11 +188,10 @@ function JsonRpcClient(configuration) { callback(error, result); } }); - } + }; function updateNotReconnectIfLessThan() { - Logger.debug("notReconnectIfNumLessThan = " + pingNextNum + ' (old=' + - notReconnectIfNumLessThan + ')'); + Logger.debug('notReconnectIfNumLessThan = ' + pingNextNum + ' (old=' + notReconnectIfNumLessThan + ')'); notReconnectIfNumLessThan = pingNextNum; } @@ -201,23 +205,25 @@ function JsonRpcClient(configuration) { } pingNextNum++; - self.send('ping', params, (function (pingNum) { - return function (error, result) { - if (error) { - Logger.debug("Error in ping request #" + pingNum + " (" + - error.message + ")"); - if (pingNum > notReconnectIfNumLessThan) { - enabledPings = false; - updateNotReconnectIfLessThan(); - Logger.debug("Server did not respond to ping message #" + - pingNum + ". Reconnecting... "); - ws.reconnectWs(); + self.send( + 'ping', + params, + (function (pingNum) { + return function (error, result) { + if (error) { + Logger.debug('Error in ping request #' + pingNum + ' (' + error.message + ')'); + if (pingNum > notReconnectIfNumLessThan) { + enabledPings = false; + updateNotReconnectIfLessThan(); + Logger.debug('Server did not respond to ping message #' + pingNum + '. Reconnecting... '); + ws.reconnectWs(); + } } - } - } - })(pingNextNum)); + }; + })(pingNextNum) + ); } else { - Logger.debug("Trying to send ping, but ping is not enabled"); + Logger.debug('Trying to send ping, but ping is not enabled'); } } @@ -227,7 +233,7 @@ function JsonRpcClient(configuration) { */ function usePing() { if (!pingPongStarted) { - Logger.debug("Starting ping (if configured)") + Logger.debug('Starting ping (if configured)'); pingPongStarted = true; if (configuration.heartbeat != undefined) { @@ -246,30 +252,29 @@ function JsonRpcClient(configuration) { } this.close = function (code, reason) { - Logger.debug("Closing with code: " + code + " because: " + reason); + Logger.debug('Closing with code: ' + code + ' because: ' + reason); if (pingInterval != undefined) { - Logger.debug("Clearing ping interval"); + Logger.debug('Clearing ping interval'); clearInterval(pingInterval); } pingPongStarted = false; enabledPings = false; ws.close(code, reason); - } + }; this.reconnect = function () { ws.reconnectWs(); - } + }; this.resetPing = function () { enabledPings = true; pingNextNum = 0; usePing(); - } + }; this.getReadyState = function () { return ws.getReadyState(); - } + }; } - -module.exports = JsonRpcClient; \ No newline at end of file +module.exports = JsonRpcClient; diff --git a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/transports/index.js b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/transports/index.js index c9bceb36..59c85285 100644 --- a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/transports/index.js +++ b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/transports/index.js @@ -17,4 +17,4 @@ var WebSocketWithReconnection = require('./webSocketWithReconnection'); -exports.WebSocketWithReconnection = WebSocketWithReconnection; \ No newline at end of file +exports.WebSocketWithReconnection = WebSocketWithReconnection; diff --git a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/transports/webSocketWithReconnection.js b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/transports/webSocketWithReconnection.js index 33b9821a..abc1bbb5 100644 --- a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/transports/webSocketWithReconnection.js +++ b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/transports/webSocketWithReconnection.js @@ -14,7 +14,7 @@ * limitations under the License. */ -"use strict"; +'use strict'; var OpenViduLogger = require('../../../../Logger/OpenViduLogger').OpenViduLogger; var Logger = OpenViduLogger.getInstance(); @@ -45,17 +45,14 @@ function WebSocketWithReconnection(config) { var ws = new WebSocket(wsUri); ws.onopen = () => { - Logger.debug("WebSocket connected to " + wsUri); + Logger.debug('WebSocket connected to ' + wsUri); if (config.onconnected) { config.onconnected(); } }; - ws.onerror = error => { - Logger.error( - "Could not connect to " + wsUri + " (invoking onerror if defined)", - error - ); + ws.onerror = (error) => { + Logger.error('Could not connect to ' + wsUri + ' (invoking onerror if defined)', error); if (config.onerror) { config.onerror(error); } @@ -64,31 +61,27 @@ function WebSocketWithReconnection(config) { var reconnectionOnClose = () => { if (ws.readyState === CLOSED) { if (closing) { - Logger.debug("Connection closed by user"); + Logger.debug('Connection closed by user'); } else { if (config.ismasternodecrashed()) { - Logger.error("Master Node has crashed. Stopping reconnection process"); + Logger.error('Master Node has crashed. Stopping reconnection process'); } else { - Logger.debug("Connection closed unexpectedly. Reconnecting..."); + Logger.debug('Connection closed unexpectedly. Reconnecting...'); reconnect(MAX_RETRIES, 1); } } } else { - Logger.debug("Close callback from previous websocket. Ignoring it"); + Logger.debug('Close callback from previous websocket. Ignoring it'); } }; ws.onclose = reconnectionOnClose; function reconnect(maxRetries, numRetries) { - Logger.debug( - "reconnect (attempt #" + numRetries + ", max=" + maxRetries + ")" - ); + Logger.debug('reconnect (attempt #' + numRetries + ', max=' + maxRetries + ')'); if (numRetries === 1) { if (reconnecting) { - Logger.warn( - "Trying to reconnect when already reconnecting... Ignoring this reconnection." - ); + Logger.warn('Trying to reconnect when already reconnecting... Ignoring this reconnection.'); return; } else { reconnecting = true; @@ -101,24 +94,22 @@ function WebSocketWithReconnection(config) { } function addReconnectionQueryParamsIfMissing(uriString) { - var searchParams = new URLSearchParams((new URL(uriString)).search); - if (!searchParams.has("reconnect")) { - uriString = (Array.from(searchParams).length > 0) ? (uriString + '&reconnect=true') : (uriString + '?reconnect=true'); + var searchParams = new URLSearchParams(new URL(uriString).search); + if (!searchParams.has('reconnect')) { + uriString = Array.from(searchParams).length > 0 ? uriString + '&reconnect=true' : uriString + '?reconnect=true'; } return uriString; } function reconnectAux(maxRetries, numRetries) { - Logger.debug("Reconnection attempt #" + numRetries); + Logger.debug('Reconnection attempt #' + numRetries); ws.close(4104, 'Connection closed for reconnection'); wsUri = addReconnectionQueryParamsIfMissing(wsUri); ws = new WebSocket(wsUri); ws.onopen = () => { - Logger.debug( - "Reconnected to " + wsUri + " after " + numRetries + " attempts..." - ); + Logger.debug('Reconnected to ' + wsUri + ' after ' + numRetries + ' attempts...'); reconnecting = false; registerMessageHandler(); if (config.onreconnected()) { @@ -127,8 +118,8 @@ function WebSocketWithReconnection(config) { ws.onclose = reconnectionOnClose; }; - ws.onerror = error => { - Logger.warn("Reconnection error: ", error); + ws.onerror = (error) => { + Logger.warn('Reconnection error: ', error); if (numRetries === maxRetries) { if (config.ondisconnect) { config.ondisconnect(); @@ -147,11 +138,11 @@ function WebSocketWithReconnection(config) { }; this.reconnectWs = () => { - Logger.debug("reconnectWs"); + Logger.debug('reconnectWs'); reconnect(MAX_RETRIES, 1); }; - this.send = message => { + this.send = (message) => { ws.send(message); }; @@ -164,7 +155,7 @@ function WebSocketWithReconnection(config) { this.getReadyState = () => { return ws.readyState; - } + }; } -module.exports = WebSocketWithReconnection; \ No newline at end of file +module.exports = WebSocketWithReconnection; diff --git a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/index.js b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/index.js index 7eb9c949..fb448400 100644 --- a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/index.js +++ b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/index.js @@ -15,43 +15,38 @@ * */ - -var defineProperty_IE8 = false +var defineProperty_IE8 = false; if (Object.defineProperty) { - try { - Object.defineProperty({}, "x", {}); - } catch (e) { - defineProperty_IE8 = true - } + try { + Object.defineProperty({}, 'x', {}); + } catch (e) { + defineProperty_IE8 = true; + } } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind if (!Function.prototype.bind) { - Function.prototype.bind = function (oThis) { - if (typeof this !== 'function') { - // closest thing possible to the ECMAScript 5 - // internal IsCallable function - throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); - } + Function.prototype.bind = function (oThis) { + if (typeof this !== 'function') { + // closest thing possible to the ECMAScript 5 + // internal IsCallable function + throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); + } - var aArgs = Array.prototype.slice.call(arguments, 1), - fToBind = this, - fNOP = function () {}, - fBound = function () { - return fToBind.apply(this instanceof fNOP && oThis ? - this : - oThis, - aArgs.concat(Array.prototype.slice.call(arguments))); - }; + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function () {}, + fBound = function () { + return fToBind.apply(this instanceof fNOP && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); + }; - fNOP.prototype = this.prototype; - fBound.prototype = new fNOP(); + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); - return fBound; - }; + return fBound; + }; } - var EventEmitter = require('events').EventEmitter; var inherits = require('inherits'); @@ -59,57 +54,53 @@ var inherits = require('inherits'); var packers = require('./packers'); var Mapper = require('./Mapper'); - var BASE_TIMEOUT = 5000; - function unifyResponseMethods(responseMethods) { - if (!responseMethods) return {}; + if (!responseMethods) return {}; - for (var key in responseMethods) { - var value = responseMethods[key]; + for (var key in responseMethods) { + var value = responseMethods[key]; - if (typeof value == 'string') - responseMethods[key] = { - response: value - } - }; + if (typeof value == 'string') + responseMethods[key] = { + response: value + }; + } - return responseMethods; -}; + return responseMethods; +} function unifyTransport(transport) { - if (!transport) return; + if (!transport) return; - // Transport as a function - if (transport instanceof Function) - return { - send: transport - }; + // Transport as a function + if (transport instanceof Function) + return { + send: transport + }; - // WebSocket & DataChannel - if (transport.send instanceof Function) - return transport; + // WebSocket & DataChannel + if (transport.send instanceof Function) return transport; - // Message API (Inter-window & WebWorker) - if (transport.postMessage instanceof Function) { - transport.send = transport.postMessage; - return transport; - } + // Message API (Inter-window & WebWorker) + if (transport.postMessage instanceof Function) { + transport.send = transport.postMessage; + return transport; + } - // Stream API - if (transport.write instanceof Function) { - transport.send = transport.write; - return transport; - } + // Stream API + if (transport.write instanceof Function) { + transport.send = transport.write; + return transport; + } - // Transports that only can receive messages, but not send - if (transport.onmessage !== undefined) return; - if (transport.pause instanceof Function) return; - - throw new SyntaxError("Transport is not a function nor a valid object"); -}; + // Transports that only can receive messages, but not send + if (transport.onmessage !== undefined) return; + if (transport.pause instanceof Function) return; + throw new SyntaxError('Transport is not a function nor a valid object'); +} /** * Representation of a RPC notification @@ -122,21 +113,20 @@ function unifyTransport(transport) { * @param params - parameters of the notification */ function RpcNotification(method, params) { - if (defineProperty_IE8) { - this.method = method - this.params = params - } else { - Object.defineProperty(this, 'method', { - value: method, - enumerable: true - }); - Object.defineProperty(this, 'params', { - value: params, - enumerable: true - }); - } -}; - + if (defineProperty_IE8) { + this.method = method; + this.params = params; + } else { + Object.defineProperty(this, 'method', { + value: method, + enumerable: true + }); + Object.defineProperty(this, 'params', { + value: params, + enumerable: true + }); + } +} /** * @class @@ -152,598 +142,541 @@ function RpcNotification(method, params) { * @param {Function} [onRequest] */ function RpcBuilder(packer, options, transport, onRequest) { - var self = this; + var self = this; - if (!packer) - throw new SyntaxError('Packer is not defined'); + if (!packer) throw new SyntaxError('Packer is not defined'); - if (!packer.pack || !packer.unpack) - throw new SyntaxError('Packer is invalid'); + if (!packer.pack || !packer.unpack) throw new SyntaxError('Packer is invalid'); - var responseMethods = unifyResponseMethods(packer.responseMethods); + var responseMethods = unifyResponseMethods(packer.responseMethods); + if (options instanceof Function) { + if (transport != undefined) throw new SyntaxError("There can't be parameters after onRequest"); - if (options instanceof Function) { - if (transport != undefined) - throw new SyntaxError("There can't be parameters after onRequest"); + onRequest = options; + transport = undefined; + options = undefined; + } - onRequest = options; - transport = undefined; - options = undefined; - }; + if (options && options.send instanceof Function) { + if (transport && !(transport instanceof Function)) throw new SyntaxError('Only a function can be after transport'); - if (options && options.send instanceof Function) { - if (transport && !(transport instanceof Function)) - throw new SyntaxError("Only a function can be after transport"); + onRequest = transport; + transport = options; + options = undefined; + } - onRequest = transport; - transport = options; - options = undefined; - }; + if (transport instanceof Function) { + if (onRequest != undefined) throw new SyntaxError("There can't be parameters after onRequest"); - if (transport instanceof Function) { - if (onRequest != undefined) - throw new SyntaxError("There can't be parameters after onRequest"); + onRequest = transport; + transport = undefined; + } - onRequest = transport; - transport = undefined; - }; + if (transport && transport.send instanceof Function) + if (onRequest && !(onRequest instanceof Function)) throw new SyntaxError('Only a function can be after transport'); - if (transport && transport.send instanceof Function) - if (onRequest && !(onRequest instanceof Function)) - throw new SyntaxError("Only a function can be after transport"); + options = options || {}; - options = options || {}; + EventEmitter.call(this); + if (onRequest) this.on('request', onRequest); - EventEmitter.call(this); + if (defineProperty_IE8) this.peerID = options.peerID; + else + Object.defineProperty(this, 'peerID', { + value: options.peerID + }); - if (onRequest) - this.on('request', onRequest); + var max_retries = options.max_retries || 0; - - if (defineProperty_IE8) - this.peerID = options.peerID - else - Object.defineProperty(this, 'peerID', { - value: options.peerID - }); - - var max_retries = options.max_retries || 0; - - - function transportMessage(event) { - self.decode(event.data || event); - }; - - this.getTransport = function () { - return transport; - } - this.setTransport = function (value) { - // Remove listener from old transport - if (transport) { - // W3C transports - if (transport.removeEventListener) - transport.removeEventListener('message', transportMessage); - - // Node.js Streams API - else if (transport.removeListener) - transport.removeListener('data', transportMessage); - }; - - // Set listener on new transport - if (value) { - // W3C transports - if (value.addEventListener) - value.addEventListener('message', transportMessage); - - // Node.js Streams API - else if (value.addListener) - value.addListener('data', transportMessage); - }; - - transport = unifyTransport(value); - } - - if (!defineProperty_IE8) - Object.defineProperty(this, 'transport', { - get: this.getTransport.bind(this), - set: this.setTransport.bind(this) - }) - - this.setTransport(transport); - - - var request_timeout = options.request_timeout || BASE_TIMEOUT; - var ping_request_timeout = options.ping_request_timeout || request_timeout; - var response_timeout = options.response_timeout || BASE_TIMEOUT; - var duplicates_timeout = options.duplicates_timeout || BASE_TIMEOUT; - - - var requestID = 0; - - var requests = new Mapper(); - var responses = new Mapper(); - var processedResponses = new Mapper(); - - var message2Key = {}; - - - /** - * Store the response to prevent to process duplicate request later - */ - function storeResponse(message, id, dest) { - var response = { - message: message, - /** Timeout to auto-clean old responses */ - timeout: setTimeout(function () { - responses.remove(id, dest); - }, - response_timeout) - }; - - responses.set(response, id, dest); - }; - - /** - * Store the response to ignore duplicated messages later - */ - function storeProcessedResponse(ack, from) { - var timeout = setTimeout(function () { - processedResponses.remove(ack, from); - }, - duplicates_timeout); - - processedResponses.set(timeout, ack, from); - }; - - - /** - * Representation of a RPC request - * - * @class - * @extends RpcNotification - * - * @constructor - * - * @param {String} method -method of the notification - * @param params - parameters of the notification - * @param {Integer} id - identifier of the request - * @param [from] - source of the notification - */ - function RpcRequest(method, params, id, from, transport) { - RpcNotification.call(this, method, params); + function transportMessage(event) { + self.decode(event.data || event); + } this.getTransport = function () { - return transport; - } + return transport; + }; this.setTransport = function (value) { - transport = unifyTransport(value); - } + // Remove listener from old transport + if (transport) { + // W3C transports + if (transport.removeEventListener) transport.removeEventListener('message', transportMessage); + // Node.js Streams API + else if (transport.removeListener) transport.removeListener('data', transportMessage); + } + + // Set listener on new transport + if (value) { + // W3C transports + if (value.addEventListener) value.addEventListener('message', transportMessage); + // Node.js Streams API + else if (value.addListener) value.addListener('data', transportMessage); + } + + transport = unifyTransport(value); + }; if (!defineProperty_IE8) - Object.defineProperty(this, 'transport', { - get: this.getTransport.bind(this), - set: this.setTransport.bind(this) - }) + Object.defineProperty(this, 'transport', { + get: this.getTransport.bind(this), + set: this.setTransport.bind(this) + }); - var response = responses.get(id, from); + this.setTransport(transport); + + var request_timeout = options.request_timeout || BASE_TIMEOUT; + var ping_request_timeout = options.ping_request_timeout || request_timeout; + var response_timeout = options.response_timeout || BASE_TIMEOUT; + var duplicates_timeout = options.duplicates_timeout || BASE_TIMEOUT; + + var requestID = 0; + + var requests = new Mapper(); + var responses = new Mapper(); + var processedResponses = new Mapper(); + + var message2Key = {}; /** - * @constant {Boolean} duplicated + * Store the response to prevent to process duplicate request later */ - if (!(transport || self.getTransport())) { - if (defineProperty_IE8) - this.duplicated = Boolean(response) - else - Object.defineProperty(this, 'duplicated', { - value: Boolean(response) - }); + function storeResponse(message, id, dest) { + var response = { + message: message, + /** Timeout to auto-clean old responses */ + timeout: setTimeout(function () { + responses.remove(id, dest); + }, response_timeout) + }; + + responses.set(response, id, dest); } - var responseMethod = responseMethods[method]; + /** + * Store the response to ignore duplicated messages later + */ + function storeProcessedResponse(ack, from) { + var timeout = setTimeout(function () { + processedResponses.remove(ack, from); + }, duplicates_timeout); - this.pack = packer.pack.bind(packer, this, id) + processedResponses.set(timeout, ack, from); + } /** - * Generate a response to this request + * Representation of a RPC request * - * @param {Error} [error] - * @param {*} [result] + * @class + * @extends RpcNotification * - * @returns {string} + * @constructor + * + * @param {String} method -method of the notification + * @param params - parameters of the notification + * @param {Integer} id - identifier of the request + * @param [from] - source of the notification */ - this.reply = function (error, result, transport) { - // Fix optional parameters - if (error instanceof Function || error && error.send instanceof Function) { - if (result != undefined) - throw new SyntaxError("There can't be parameters after callback"); + function RpcRequest(method, params, id, from, transport) { + RpcNotification.call(this, method, params); - transport = error; - result = null; - error = undefined; - } else if (result instanceof Function || - result && result.send instanceof Function) { - if (transport != undefined) - throw new SyntaxError("There can't be parameters after callback"); + this.getTransport = function () { + return transport; + }; + this.setTransport = function (value) { + transport = unifyTransport(value); + }; - transport = result; - result = null; - }; + if (!defineProperty_IE8) + Object.defineProperty(this, 'transport', { + get: this.getTransport.bind(this), + set: this.setTransport.bind(this) + }); - transport = unifyTransport(transport); + var response = responses.get(id, from); - // Duplicated request, remove old response timeout - if (response) - clearTimeout(response.timeout); - - if (from != undefined) { - if (error) - error.dest = from; - - if (result) - result.dest = from; - }; - - var message; - - // New request or overriden one, create new response with provided data - if (error || result != undefined) { - if (self.peerID != undefined) { - if (error) - error.from = self.peerID; - else - result.from = self.peerID; + /** + * @constant {Boolean} duplicated + */ + if (!(transport || self.getTransport())) { + if (defineProperty_IE8) this.duplicated = Boolean(response); + else + Object.defineProperty(this, 'duplicated', { + value: Boolean(response) + }); } - // Protocol indicates that responses has own request methods - if (responseMethod) { - if (responseMethod.error == undefined && error) - message = { - error: error - }; + var responseMethod = responseMethods[method]; - else { - var method = error ? - responseMethod.error : - responseMethod.response; + this.pack = packer.pack.bind(packer, this, id); - message = { - method: method, - params: error || result - }; - } - } else - message = { - error: error, - result: result - }; + /** + * Generate a response to this request + * + * @param {Error} [error] + * @param {*} [result] + * + * @returns {string} + */ + this.reply = function (error, result, transport) { + // Fix optional parameters + if (error instanceof Function || (error && error.send instanceof Function)) { + if (result != undefined) throw new SyntaxError("There can't be parameters after callback"); - message = packer.pack(message, id); - } + transport = error; + result = null; + error = undefined; + } else if (result instanceof Function || (result && result.send instanceof Function)) { + if (transport != undefined) throw new SyntaxError("There can't be parameters after callback"); - // Duplicate & not-overriden request, re-send old response - else if (response) - message = response.message; + transport = result; + result = null; + } - // New empty reply, response null value - else - message = packer.pack({ - result: null - }, id); + transport = unifyTransport(transport); - // Store the response to prevent to process a duplicated request later - storeResponse(message, id, from); + // Duplicated request, remove old response timeout + if (response) clearTimeout(response.timeout); - // Return the stored response so it can be directly send back - transport = transport || this.getTransport() || self.getTransport(); + if (from != undefined) { + if (error) error.dest = from; - if (transport) - return transport.send(message); + if (result) result.dest = from; + } - return message; - } - }; - inherits(RpcRequest, RpcNotification); + var message; + // New request or overriden one, create new response with provided data + if (error || result != undefined) { + if (self.peerID != undefined) { + if (error) error.from = self.peerID; + else result.from = self.peerID; + } - function cancel(message) { - var key = message2Key[message]; - if (!key) return; + // Protocol indicates that responses has own request methods + if (responseMethod) { + if (responseMethod.error == undefined && error) + message = { + error: error + }; + else { + var method = error ? responseMethod.error : responseMethod.response; - delete message2Key[message]; + message = { + method: method, + params: error || result + }; + } + } else + message = { + error: error, + result: result + }; - var request = requests.pop(key.id, key.dest); - if (!request) return; + message = packer.pack(message, id); + } - clearTimeout(request.timeout); + // Duplicate & not-overriden request, re-send old response + else if (response) message = response.message; + // New empty reply, response null value + else + message = packer.pack( + { + result: null + }, + id + ); - // Start duplicated responses timeout - storeProcessedResponse(key.id, key.dest); - }; + // Store the response to prevent to process a duplicated request later + storeResponse(message, id, from); - /** - * Allow to cancel a request and don't wait for a response - * - * If `message` is not given, cancel all the request - */ - this.cancel = function (message) { - if (message) return cancel(message); + // Return the stored response so it can be directly send back + transport = transport || this.getTransport() || self.getTransport(); - for (var message in message2Key) - cancel(message); - }; + if (transport) return transport.send(message); - - this.close = function () { - // Prevent to receive new messages - var transport = this.getTransport(); - if (transport && transport.close) - transport.close(4003, "Cancel request"); - - // Request & processed responses - this.cancel(); - - processedResponses.forEach(clearTimeout); - - // Responses - responses.forEach(function (response) { - clearTimeout(response.timeout); - }); - }; - - - /** - * Generates and encode a JsonRPC 2.0 message - * - * @param {String} method -method of the notification - * @param params - parameters of the notification - * @param [dest] - destination of the notification - * @param {object} [transport] - transport where to send the message - * @param [callback] - function called when a response to this request is - * received. If not defined, a notification will be send instead - * - * @returns {string} A raw JsonRPC 2.0 request or notification string - */ - this.encode = function (method, params, dest, transport, callback) { - // Fix optional parameters - if (params instanceof Function) { - if (dest != undefined) - throw new SyntaxError("There can't be parameters after callback"); - - callback = params; - transport = undefined; - dest = undefined; - params = undefined; - } else if (dest instanceof Function) { - if (transport != undefined) - throw new SyntaxError("There can't be parameters after callback"); - - callback = dest; - transport = undefined; - dest = undefined; - } else if (transport instanceof Function) { - if (callback != undefined) - throw new SyntaxError("There can't be parameters after callback"); - - callback = transport; - transport = undefined; - }; - - if (self.peerID != undefined) { - params = params || {}; - - params.from = self.peerID; - }; - - if (dest != undefined) { - params = params || {}; - - params.dest = dest; - }; - - // Encode message - var message = { - method: method, - params: params - }; - - if (callback) { - var id = requestID++; - var retried = 0; - - message = packer.pack(message, id); - - function dispatchCallback(error, result) { - self.cancel(message); - - callback(error, result); - }; - - var request = { - message: message, - callback: dispatchCallback, - responseMethods: responseMethods[method] || {} - }; - - var encode_transport = unifyTransport(transport); - - function sendRequest(transport) { - var rt = (method === 'ping' ? ping_request_timeout : request_timeout); - request.timeout = setTimeout(timeout, rt * Math.pow(2, retried++)); - message2Key[message] = { - id: id, - dest: dest + return message; }; - requests.set(request, id, dest); + } + inherits(RpcRequest, RpcNotification); - transport = transport || encode_transport || self.getTransport(); - if (transport) - return transport.send(message); + function cancel(message) { + var key = message2Key[message]; + if (!key) return; + + delete message2Key[message]; + + var request = requests.pop(key.id, key.dest); + if (!request) return; + + clearTimeout(request.timeout); + + // Start duplicated responses timeout + storeProcessedResponse(key.id, key.dest); + } + + /** + * Allow to cancel a request and don't wait for a response + * + * If `message` is not given, cancel all the request + */ + this.cancel = function (message) { + if (message) return cancel(message); + + for (var message in message2Key) cancel(message); + }; + + this.close = function () { + // Prevent to receive new messages + var transport = this.getTransport(); + if (transport && transport.close) transport.close(4003, 'Cancel request'); + + // Request & processed responses + this.cancel(); + + processedResponses.forEach(clearTimeout); + + // Responses + responses.forEach(function (response) { + clearTimeout(response.timeout); + }); + }; + + /** + * Generates and encode a JsonRPC 2.0 message + * + * @param {String} method -method of the notification + * @param params - parameters of the notification + * @param [dest] - destination of the notification + * @param {object} [transport] - transport where to send the message + * @param [callback] - function called when a response to this request is + * received. If not defined, a notification will be send instead + * + * @returns {string} A raw JsonRPC 2.0 request or notification string + */ + this.encode = function (method, params, dest, transport, callback) { + // Fix optional parameters + if (params instanceof Function) { + if (dest != undefined) throw new SyntaxError("There can't be parameters after callback"); + + callback = params; + transport = undefined; + dest = undefined; + params = undefined; + } else if (dest instanceof Function) { + if (transport != undefined) throw new SyntaxError("There can't be parameters after callback"); + + callback = dest; + transport = undefined; + dest = undefined; + } else if (transport instanceof Function) { + if (callback != undefined) throw new SyntaxError("There can't be parameters after callback"); + + callback = transport; + transport = undefined; + } + + if (self.peerID != undefined) { + params = params || {}; + + params.from = self.peerID; + } + + if (dest != undefined) { + params = params || {}; + + params.dest = dest; + } + + // Encode message + var message = { + method: method, + params: params + }; + + if (callback) { + var id = requestID++; + var retried = 0; + + message = packer.pack(message, id); + + function dispatchCallback(error, result) { + self.cancel(message); + + callback(error, result); + } + + var request = { + message: message, + callback: dispatchCallback, + responseMethods: responseMethods[method] || {} + }; + + var encode_transport = unifyTransport(transport); + + function sendRequest(transport) { + var rt = method === 'ping' ? ping_request_timeout : request_timeout; + request.timeout = setTimeout(timeout, rt * Math.pow(2, retried++)); + message2Key[message] = { + id: id, + dest: dest + }; + requests.set(request, id, dest); + + transport = transport || encode_transport || self.getTransport(); + if (transport) return transport.send(message); + + return message; + } + + function retry(transport) { + transport = unifyTransport(transport); + + console.warn(retried + ' retry for request message:', message); + + var timeout = processedResponses.pop(id, dest); + clearTimeout(timeout); + + return sendRequest(transport); + } + + function timeout() { + if (retried < max_retries) return retry(transport); + + var error = new Error('Request has timed out'); + error.request = message; + + error.retry = retry; + + dispatchCallback(error); + } + + return sendRequest(transport); + } + + // Return the packed message + message = packer.pack(message); + + transport = transport || this.getTransport(); + if (transport) return transport.send(message); return message; - }; - - function retry(transport) { - transport = unifyTransport(transport); - - console.warn(retried + ' retry for request message:', message); - - var timeout = processedResponses.pop(id, dest); - clearTimeout(timeout); - - return sendRequest(transport); - }; - - function timeout() { - if (retried < max_retries) - return retry(transport); - - var error = new Error('Request has timed out'); - error.request = message; - - error.retry = retry; - - dispatchCallback(error) - }; - - return sendRequest(transport); }; - // Return the packed message - message = packer.pack(message); + /** + * Decode and process a JsonRPC 2.0 message + * + * @param {string} message - string with the content of the message + * + * @returns {RpcNotification|RpcRequest|undefined} - the representation of the + * notification or the request. If a response was processed, it will return + * `undefined` to notify that it was processed + * + * @throws {TypeError} - Message is not defined + */ + this.decode = function (message, transport) { + if (!message) throw new TypeError('Message is not defined'); - transport = transport || this.getTransport(); - if (transport) - return transport.send(message); - - return message; - }; - - /** - * Decode and process a JsonRPC 2.0 message - * - * @param {string} message - string with the content of the message - * - * @returns {RpcNotification|RpcRequest|undefined} - the representation of the - * notification or the request. If a response was processed, it will return - * `undefined` to notify that it was processed - * - * @throws {TypeError} - Message is not defined - */ - this.decode = function (message, transport) { - if (!message) - throw new TypeError("Message is not defined"); - - try { - message = packer.unpack(message); - } catch (e) { - // Ignore invalid messages - return console.debug(e, message); - }; - - var id = message.id; - var ack = message.ack; - var method = message.method; - var params = message.params || {}; - - var from = params.from; - var dest = params.dest; - - // Ignore messages send by us - if (self.peerID != undefined && from == self.peerID) return; - - // Notification - if (id == undefined && ack == undefined) { - var notification = new RpcNotification(method, params); - - if (self.emit('request', notification)) return; - return notification; - }; - - - function processRequest() { - // If we have a transport and it's a duplicated request, reply inmediatly - transport = unifyTransport(transport) || self.getTransport(); - if (transport) { - var response = responses.get(id, from); - if (response) - return transport.send(response.message); - }; - - var idAck = (id != undefined) ? id : ack; - var request = new RpcRequest(method, params, idAck, from, transport); - - if (self.emit('request', request)) return; - return request; - }; - - function processResponse(request, error, result) { - request.callback(error, result); - }; - - function duplicatedResponse(timeout) { - console.warn("Response already processed", message); - - // Update duplicated responses timeout - clearTimeout(timeout); - storeProcessedResponse(ack, from); - }; - - - // Request, or response with own method - if (method) { - // Check if it's a response with own method - if (dest == undefined || dest == self.peerID) { - var request = requests.get(ack, from); - if (request) { - var responseMethods = request.responseMethods; - - if (method == responseMethods.error) - return processResponse(request, params); - - if (method == responseMethods.response) - return processResponse(request, null, params); - - return processRequest(); + try { + message = packer.unpack(message); + } catch (e) { + // Ignore invalid messages + return console.debug(e, message); } - var processed = processedResponses.get(ack, from); - if (processed) - return duplicatedResponse(processed); - } + var id = message.id; + var ack = message.ack; + var method = message.method; + var params = message.params || {}; - // Request - return processRequest(); + var from = params.from; + var dest = params.dest; + + // Ignore messages send by us + if (self.peerID != undefined && from == self.peerID) return; + + // Notification + if (id == undefined && ack == undefined) { + var notification = new RpcNotification(method, params); + + if (self.emit('request', notification)) return; + return notification; + } + + function processRequest() { + // If we have a transport and it's a duplicated request, reply inmediatly + transport = unifyTransport(transport) || self.getTransport(); + if (transport) { + var response = responses.get(id, from); + if (response) return transport.send(response.message); + } + + var idAck = id != undefined ? id : ack; + var request = new RpcRequest(method, params, idAck, from, transport); + + if (self.emit('request', request)) return; + return request; + } + + function processResponse(request, error, result) { + request.callback(error, result); + } + + function duplicatedResponse(timeout) { + console.warn('Response already processed', message); + + // Update duplicated responses timeout + clearTimeout(timeout); + storeProcessedResponse(ack, from); + } + + // Request, or response with own method + if (method) { + // Check if it's a response with own method + if (dest == undefined || dest == self.peerID) { + var request = requests.get(ack, from); + if (request) { + var responseMethods = request.responseMethods; + + if (method == responseMethods.error) return processResponse(request, params); + + if (method == responseMethods.response) return processResponse(request, null, params); + + return processRequest(); + } + + var processed = processedResponses.get(ack, from); + if (processed) return duplicatedResponse(processed); + } + + // Request + return processRequest(); + } + + var error = message.error; + var result = message.result; + + // Ignore responses not send to us + if (error && error.dest && error.dest != self.peerID) return; + if (result && result.dest && result.dest != self.peerID) return; + + // Response + var request = requests.get(ack, from); + if (!request) { + var processed = processedResponses.get(ack, from); + if (processed) return duplicatedResponse(processed); + + return console.warn('No callback was defined for this message', message); + } + + // Process response + processResponse(request, error, result); }; - - var error = message.error; - var result = message.result; - - // Ignore responses not send to us - if (error && error.dest && error.dest != self.peerID) return; - if (result && result.dest && result.dest != self.peerID) return; - - // Response - var request = requests.get(ack, from); - if (!request) { - var processed = processedResponses.get(ack, from); - if (processed) - return duplicatedResponse(processed); - - return console.warn("No callback was defined for this message", message); - }; - - // Process response - processResponse(request, error, result); - }; -}; +} inherits(RpcBuilder, EventEmitter); - RpcBuilder.RpcNotification = RpcNotification; - module.exports = RpcBuilder; var clients = require('./clients'); @@ -751,4 +684,4 @@ var transports = require('./clients/transports'); RpcBuilder.clients = clients; RpcBuilder.clients.transports = transports; -RpcBuilder.packers = packers; \ No newline at end of file +RpcBuilder.packers = packers; diff --git a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/packers/JsonRPC.js b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/packers/JsonRPC.js index 36016054..b318c6b9 100644 --- a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/packers/JsonRPC.js +++ b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/packers/JsonRPC.js @@ -11,39 +11,34 @@ * @return {String} - the stringified JsonRPC 2.0 message */ function pack(message, id) { - var result = { - jsonrpc: "2.0" - }; + var result = { + jsonrpc: '2.0' + }; - // Request - if (message.method) { - result.method = message.method; + // Request + if (message.method) { + result.method = message.method; - if (message.params) - result.params = message.params; + if (message.params) result.params = message.params; - // Request is a notification - if (id != undefined) - result.id = id; - } + // Request is a notification + if (id != undefined) result.id = id; + } - // Response - else if (id != undefined) { - if (message.error) { - if (message.result !== undefined) - throw new TypeError("Both result and error are defined"); + // Response + else if (id != undefined) { + if (message.error) { + if (message.result !== undefined) throw new TypeError('Both result and error are defined'); - result.error = message.error; - } else if (message.result !== undefined) - result.result = message.result; - else - throw new TypeError("No result or error is defined"); + result.error = message.error; + } else if (message.result !== undefined) result.result = message.result; + else throw new TypeError('No result or error is defined'); - result.id = id; - }; + result.id = id; + } - return JSON.stringify(result); -}; + return JSON.stringify(result); +} /** * Unpack a JsonRPC 2.0 message @@ -55,41 +50,36 @@ function pack(message, id) { * @return {Object} - object filled with the JsonRPC 2.0 message content */ function unpack(message) { - var result = message; + var result = message; - if (typeof message === 'string' || message instanceof String) { - result = JSON.parse(message); - } + if (typeof message === 'string' || message instanceof String) { + result = JSON.parse(message); + } - // Check if it's a valid message + // Check if it's a valid message - var version = result.jsonrpc; - if (version !== '2.0') - throw new TypeError("Invalid JsonRPC version '" + version + "': " + message); + var version = result.jsonrpc; + if (version !== '2.0') throw new TypeError("Invalid JsonRPC version '" + version + "': " + message); - // Response - if (result.method == undefined) { - if (result.id == undefined) - throw new TypeError("Invalid message: " + message); + // Response + if (result.method == undefined) { + if (result.id == undefined) throw new TypeError('Invalid message: ' + message); - var result_defined = result.result !== undefined; - var error_defined = result.error !== undefined; + var result_defined = result.result !== undefined; + var error_defined = result.error !== undefined; - // Check only result or error is defined, not both or none - if (result_defined && error_defined) - throw new TypeError("Both result and error are defined: " + message); + // Check only result or error is defined, not both or none + if (result_defined && error_defined) throw new TypeError('Both result and error are defined: ' + message); - if (!result_defined && !error_defined) - throw new TypeError("No result or error is defined: " + message); + if (!result_defined && !error_defined) throw new TypeError('No result or error is defined: ' + message); - result.ack = result.id; - delete result.id; - } - - // Return unpacked message - return result; -}; + result.ack = result.id; + delete result.id; + } + // Return unpacked message + return result; +} exports.pack = pack; -exports.unpack = unpack; \ No newline at end of file +exports.unpack = unpack; diff --git a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/packers/XmlRPC.js b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/packers/XmlRPC.js index 73cb8879..0a437e68 100644 --- a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/packers/XmlRPC.js +++ b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/packers/XmlRPC.js @@ -1,10 +1,10 @@ function pack(message) { - throw new TypeError("Not yet implemented"); -}; + throw new TypeError('Not yet implemented'); +} function unpack(message) { - throw new TypeError("Not yet implemented"); -}; + throw new TypeError('Not yet implemented'); +} exports.pack = pack; -exports.unpack = unpack; \ No newline at end of file +exports.unpack = unpack; diff --git a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/packers/index.js b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/packers/index.js index 69c7f189..34949b95 100644 --- a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/packers/index.js +++ b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/packers/index.js @@ -1,6 +1,5 @@ var JsonRPC = require('./JsonRPC'); var XmlRPC = require('./XmlRPC'); - exports.JsonRPC = JsonRPC; -exports.XmlRPC = XmlRPC; \ No newline at end of file +exports.XmlRPC = XmlRPC; diff --git a/openvidu-browser/src/OpenViduInternal/Logger/ConsoleLogger.ts b/openvidu-browser/src/OpenViduInternal/Logger/ConsoleLogger.ts index da103bdc..58584fdd 100644 --- a/openvidu-browser/src/OpenViduInternal/Logger/ConsoleLogger.ts +++ b/openvidu-browser/src/OpenViduInternal/Logger/ConsoleLogger.ts @@ -1,42 +1,41 @@ type ConsoleFunction = (...data: any) => void; export class ConsoleLogger { + /** + * @hidden + */ + logger: Console; /** * @hidden */ - logger: Console + log: ConsoleFunction; /** * @hidden */ - log: ConsoleFunction + info: ConsoleFunction; /** * @hidden */ - info: ConsoleFunction + debug: ConsoleFunction; /** * @hidden */ - debug: ConsoleFunction + warn: ConsoleFunction; /** * @hidden */ - warn: ConsoleFunction - - /** - * @hidden - */ - error: ConsoleFunction + error: ConsoleFunction; constructor(console: Console) { this.logger = console; - this.log = console.log, - this.info = console.info, - this.debug = console.debug, - this.warn = console.warn, - this.error = console.error + (this.log = console.log), + (this.info = console.info), + (this.debug = console.debug), + (this.warn = console.warn), + (this.error = console.error); } } diff --git a/openvidu-browser/src/OpenViduInternal/Logger/OpenViduLogger.ts b/openvidu-browser/src/OpenViduInternal/Logger/OpenViduLogger.ts index 56d52289..deaad660 100644 --- a/openvidu-browser/src/OpenViduInternal/Logger/OpenViduLogger.ts +++ b/openvidu-browser/src/OpenViduInternal/Logger/OpenViduLogger.ts @@ -1,283 +1,285 @@ -import { JL } from 'jsnlog' -import { OpenVidu } from "../../OpenVidu/OpenVidu"; +import { JL } from 'jsnlog'; +import { OpenVidu } from '../../OpenVidu/OpenVidu'; import { ConsoleLogger } from './ConsoleLogger'; -import { OpenViduLoggerConfiguration } from "./OpenViduLoggerConfiguration"; +import { OpenViduLoggerConfiguration } from './OpenViduLoggerConfiguration'; export class OpenViduLogger { + private static instance: OpenViduLogger; - private static instance: OpenViduLogger; + private JSNLOG_URL: string = '/openvidu/elk/openvidu-browser-logs'; + private MAX_JSNLOG_BATCH_LOG_MESSAGES: number = 100; + private MAX_MSECONDS_BATCH_MESSAGES: number = 5000; + private MAX_LENGTH_STRING_JSON: number = 1000; - private JSNLOG_URL: string = "/openvidu/elk/openvidu-browser-logs"; - private MAX_JSNLOG_BATCH_LOG_MESSAGES: number = 100; - private MAX_MSECONDS_BATCH_MESSAGES: number = 5000; - private MAX_LENGTH_STRING_JSON: number = 1000; + private defaultConsoleLogger: ConsoleLogger = new ConsoleLogger(globalThis.console); - private defaultConsoleLogger: ConsoleLogger = new ConsoleLogger(globalThis.console); + private currentAppender: any; - private currentAppender: any; + private isProdMode = false; + private isJSNLogSetup = false; - private isProdMode = false; - private isJSNLogSetup = false; + // This two variables are used to restart JSNLog + // on different sessions and different userIds + private loggingSessionId: string | undefined; - // This two variables are used to restart JSNLog - // on different sessions and different userIds - private loggingSessionId: string | undefined; + /** + * @hidden + */ + static configureJSNLog(openVidu: OpenVidu, token: string) { + try { + // If dev mode or... + if ( + globalThis['LOG_JSNLOG_RESULTS'] || + // If instance is created and it is OpenVidu Pro + (this.instance && + openVidu.isAtLeastPro && + // If logs are enabled + this.instance.isOpenViduBrowserLogsDebugActive(openVidu) && + // Only reconfigure it if session or finalUserId has changed + this.instance.canConfigureJSNLog(openVidu, this.instance)) + ) { + // Check if app logs can be sent + // and replace console.log function to send + // logs of the application + if (openVidu.sendBrowserLogs === OpenViduLoggerConfiguration.debug_app) { + this.instance.replaceWindowConsole(); + } - /** - * @hidden - */ - static configureJSNLog(openVidu: OpenVidu, token: string) { - try { - // If dev mode or... - if ((globalThis['LOG_JSNLOG_RESULTS']) || - // If instance is created and it is OpenVidu Pro - (this.instance && openVidu.isAtLeastPro - // If logs are enabled - && this.instance.isOpenViduBrowserLogsDebugActive(openVidu) - // Only reconfigure it if session or finalUserId has changed - && this.instance.canConfigureJSNLog(openVidu, this.instance))) { + // isJSNLogSetup will not be true until completed setup + this.instance.isJSNLogSetup = false; + this.instance.info('Configuring JSNLogs.'); - // Check if app logs can be sent - // and replace console.log function to send - // logs of the application - if (openVidu.sendBrowserLogs === OpenViduLoggerConfiguration.debug_app) { - this.instance.replaceWindowConsole(); - } + const finalUserId = openVidu.finalUserId; + const sessionId = openVidu.session.sessionId; - // isJSNLogSetup will not be true until completed setup - this.instance.isJSNLogSetup = false; - this.instance.info("Configuring JSNLogs."); + const beforeSendCallback = (xhr) => { + // If 401 or 403 or 404 modify ready and status so JSNLog don't retry to send logs + // https://github.com/mperdeck/jsnlog.js/blob/v2.30.0/jsnlog.ts#L805-L818 + const parentReadyStateFunction = xhr.onreadystatechange; + xhr.onreadystatechange = () => { + if (this.isInvalidResponse(xhr)) { + Object.defineProperty(xhr, 'readyState', { value: 4 }); + Object.defineProperty(xhr, 'status', { value: 200 }); + // Disable JSNLog too to not send periodically errors + this.instance.disableLogger(); + } + parentReadyStateFunction(); + }; - const finalUserId = openVidu.finalUserId; - const sessionId = openVidu.session.sessionId; + // Headers to identify and authenticate logs + xhr.setRequestHeader('Authorization', 'Basic ' + btoa(`${finalUserId}%/%${sessionId}` + ':' + token)); + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + // Additional headers for OpenVidu + xhr.setRequestHeader('OV-Final-User-Id', finalUserId); + xhr.setRequestHeader('OV-Session-Id', sessionId); + xhr.setRequestHeader('OV-Token', token); + }; - const beforeSendCallback = (xhr) => { - // If 401 or 403 or 404 modify ready and status so JSNLog don't retry to send logs - // https://github.com/mperdeck/jsnlog.js/blob/v2.30.0/jsnlog.ts#L805-L818 - const parentReadyStateFunction = xhr.onreadystatechange; - xhr.onreadystatechange = () => { - if (this.isInvalidResponse(xhr)) { - Object.defineProperty(xhr, "readyState", { value: 4 }); - Object.defineProperty(xhr, "status", { value: 200 }); - // Disable JSNLog too to not send periodically errors - this.instance.disableLogger(); - } - parentReadyStateFunction(); - } + // Creation of the appender. + this.instance.currentAppender = JL.createAjaxAppender(`appender-${finalUserId}-${sessionId}`); + this.instance.currentAppender.setOptions({ + beforeSend: beforeSendCallback, + maxBatchSize: 1000, + batchSize: this.instance.MAX_JSNLOG_BATCH_LOG_MESSAGES, + batchTimeout: this.instance.MAX_MSECONDS_BATCH_MESSAGES + }); - // Headers to identify and authenticate logs - xhr.setRequestHeader('Authorization', "Basic " + btoa(`${finalUserId}%/%${sessionId}` + ":" + token)); - xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest') - // Additional headers for OpenVidu - xhr.setRequestHeader('OV-Final-User-Id', finalUserId); - xhr.setRequestHeader('OV-Session-Id', sessionId); - xhr.setRequestHeader('OV-Token', token); - } + // Avoid circular dependencies + const logSerializer = (obj): string => { + const getCircularReplacer = () => { + const seen = new WeakSet(); + return (key, value) => { + if (typeof value === 'object' && value != null) { + if (seen.has(value) || (globalThis.HTMLElement && value instanceof HTMLElement)) { + return; + } + seen.add(value); + } + return value; + }; + }; - // Creation of the appender. - this.instance.currentAppender = JL.createAjaxAppender(`appender-${finalUserId}-${sessionId}`); - this.instance.currentAppender.setOptions({ - beforeSend: beforeSendCallback, - maxBatchSize: 1000, - batchSize: this.instance.MAX_JSNLOG_BATCH_LOG_MESSAGES, - batchTimeout: this.instance.MAX_MSECONDS_BATCH_MESSAGES - }); + // Cut long messages + let stringifyJson = JSON.stringify(obj, getCircularReplacer()); + if (stringifyJson.length > this.instance.MAX_LENGTH_STRING_JSON) { + stringifyJson = `${stringifyJson.substring(0, this.instance.MAX_LENGTH_STRING_JSON)}...`; + } - // Avoid circular dependencies - const logSerializer = (obj): string => { - const getCircularReplacer = () => { - const seen = new WeakSet(); - return (key, value) => { - if (typeof value === "object" && value != null) { - if (seen.has(value) || (globalThis.HTMLElement && value instanceof HTMLElement)) { - return; - } - seen.add(value); - } - return value; - }; - }; + if (globalThis['LOG_JSNLOG_RESULTS']) { + console.log(stringifyJson); + } - // Cut long messages - let stringifyJson = JSON.stringify(obj, getCircularReplacer()); - if (stringifyJson.length > this.instance.MAX_LENGTH_STRING_JSON) { - stringifyJson = `${stringifyJson.substring(0, this.instance.MAX_LENGTH_STRING_JSON)}...`; - } + return stringifyJson; + }; - if (globalThis['LOG_JSNLOG_RESULTS']) { - console.log(stringifyJson); - } + // Initialize JL to send logs + JL.setOptions({ + defaultAjaxUrl: openVidu.httpUri + this.instance.JSNLOG_URL, + serialize: logSerializer, + enabled: true + }); + JL().setOptions({ + appenders: [this.instance.currentAppender] + }); - return stringifyJson; - }; + this.instance.isJSNLogSetup = true; + this.instance.loggingSessionId = sessionId; + this.instance.info('JSNLog configured.'); + } + } catch (e) { + // Print error + console.error('Error configuring JSNLog: '); + console.error(e); + // Restore defaults values just in case any exception happen- + this.instance.disableLogger(); + } + } - // Initialize JL to send logs - JL.setOptions({ - defaultAjaxUrl: openVidu.httpUri + this.instance.JSNLOG_URL, - serialize: logSerializer, - enabled: true - }); - JL().setOptions({ - appenders: [this.instance.currentAppender] - }); + /** + * @hidden + */ + static getInstance(): OpenViduLogger { + if (!OpenViduLogger.instance) { + OpenViduLogger.instance = new OpenViduLogger(); + } + return OpenViduLogger.instance; + } - this.instance.isJSNLogSetup = true; - this.instance.loggingSessionId = sessionId; - this.instance.info("JSNLog configured."); - } - } catch (e) { - // Print error - console.error("Error configuring JSNLog: "); - console.error(e); - // Restore defaults values just in case any exception happen- - this.instance.disableLogger(); - } - } + private static isInvalidResponse(xhr: XMLHttpRequest) { + return xhr.status == 401 || xhr.status == 403 || xhr.status == 404 || xhr.status == 0; + } - /** - * @hidden - */ - static getInstance(): OpenViduLogger { - if (!OpenViduLogger.instance) { - OpenViduLogger.instance = new OpenViduLogger(); - } - return OpenViduLogger.instance; - } + private canConfigureJSNLog(openVidu: OpenVidu, logger: OpenViduLogger): boolean { + return openVidu.session.sessionId != logger.loggingSessionId; + } - private static isInvalidResponse(xhr: XMLHttpRequest) { - return xhr.status == 401 || xhr.status == 403 || xhr.status == 404 || xhr.status == 0; - } + private isOpenViduBrowserLogsDebugActive(openVidu: OpenVidu) { + return ( + openVidu.sendBrowserLogs === OpenViduLoggerConfiguration.debug || + openVidu.sendBrowserLogs === OpenViduLoggerConfiguration.debug_app + ); + } - private canConfigureJSNLog(openVidu: OpenVidu, logger: OpenViduLogger): boolean { - return openVidu.session.sessionId != logger.loggingSessionId - } + // Return console functions with jsnlog integration + private getConsoleWithJSNLog() { + return (function (openViduLogger: OpenViduLogger) { + return { + log: function (...args) { + openViduLogger.defaultConsoleLogger.log.apply(openViduLogger.defaultConsoleLogger.logger, arguments); + if (openViduLogger.isJSNLogSetup) { + JL().info(arguments); + } + }, + info: function (...args) { + openViduLogger.defaultConsoleLogger.info.apply(openViduLogger.defaultConsoleLogger.logger, arguments); + if (openViduLogger.isJSNLogSetup) { + JL().info(arguments); + } + }, + debug: function (...args) { + openViduLogger.defaultConsoleLogger.debug.apply(openViduLogger.defaultConsoleLogger.logger, arguments); + }, + warn: function (...args) { + openViduLogger.defaultConsoleLogger.warn.apply(openViduLogger.defaultConsoleLogger.logger, arguments); + if (openViduLogger.isJSNLogSetup) { + JL().warn(arguments); + } + }, + error: function (...args) { + openViduLogger.defaultConsoleLogger.error.apply(openViduLogger.defaultConsoleLogger.logger, arguments); + if (openViduLogger.isJSNLogSetup) { + JL().error(arguments); + } + } + }; + })(this); + } - private isOpenViduBrowserLogsDebugActive(openVidu: OpenVidu) { - return openVidu.sendBrowserLogs === OpenViduLoggerConfiguration.debug || - openVidu.sendBrowserLogs === OpenViduLoggerConfiguration.debug_app; - } + private replaceWindowConsole() { + globalThis.console = this.defaultConsoleLogger.logger; + globalThis.console.log = this.getConsoleWithJSNLog().log; + globalThis.console.info = this.getConsoleWithJSNLog().info; + globalThis.console.debug = this.getConsoleWithJSNLog().debug; + globalThis.console.warn = this.getConsoleWithJSNLog().warn; + globalThis.console.error = this.getConsoleWithJSNLog().error; + } - // Return console functions with jsnlog integration - private getConsoleWithJSNLog() { - return function (openViduLogger: OpenViduLogger) { - return { - log: function (...args) { - openViduLogger.defaultConsoleLogger.log.apply(openViduLogger.defaultConsoleLogger.logger, arguments); - if (openViduLogger.isJSNLogSetup) { - JL().info(arguments); - } - }, - info: function (...args) { - openViduLogger.defaultConsoleLogger.info.apply(openViduLogger.defaultConsoleLogger.logger, arguments); - if (openViduLogger.isJSNLogSetup) { - JL().info(arguments); - } - }, - debug: function (...args) { - openViduLogger.defaultConsoleLogger.debug.apply(openViduLogger.defaultConsoleLogger.logger, arguments); - }, - warn: function (...args) { - openViduLogger.defaultConsoleLogger.warn.apply(openViduLogger.defaultConsoleLogger.logger, arguments); - if (openViduLogger.isJSNLogSetup) { - JL().warn(arguments); - } - }, - error: function (...args) { - openViduLogger.defaultConsoleLogger.error.apply(openViduLogger.defaultConsoleLogger.logger, arguments); - if (openViduLogger.isJSNLogSetup) { - JL().error(arguments); - } - } - }; - }(this); - } + private disableLogger() { + JL.setOptions({ enabled: false }); + this.isJSNLogSetup = false; + this.loggingSessionId = undefined; + this.currentAppender = undefined; + globalThis.console = this.defaultConsoleLogger.logger; + globalThis.console.log = this.defaultConsoleLogger.log; + globalThis.console.info = this.defaultConsoleLogger.info; + globalThis.console.debug = this.defaultConsoleLogger.debug; + globalThis.console.warn = this.defaultConsoleLogger.warn; + globalThis.console.error = this.defaultConsoleLogger.error; + } - private replaceWindowConsole() { - globalThis.console = this.defaultConsoleLogger.logger; - globalThis.console.log = this.getConsoleWithJSNLog().log; - globalThis.console.info = this.getConsoleWithJSNLog().info; - globalThis.console.debug = this.getConsoleWithJSNLog().debug; - globalThis.console.warn = this.getConsoleWithJSNLog().warn; - globalThis.console.error = this.getConsoleWithJSNLog().error; - } + /** + * @hidden + */ + log(...args: any[]) { + if (!this.isProdMode) { + this.defaultConsoleLogger.log.apply(this.defaultConsoleLogger.logger, arguments); + } + if (this.isJSNLogSetup) { + JL().info(arguments); + } + } - private disableLogger() { - JL.setOptions({ enabled: false }); - this.isJSNLogSetup = false; - this.loggingSessionId = undefined; - this.currentAppender = undefined; - globalThis.console = this.defaultConsoleLogger.logger; - globalThis.console.log = this.defaultConsoleLogger.log; - globalThis.console.info = this.defaultConsoleLogger.info; - globalThis.console.debug = this.defaultConsoleLogger.debug; - globalThis.console.warn = this.defaultConsoleLogger.warn; - globalThis.console.error = this.defaultConsoleLogger.error; - } + /** + * @hidden + */ + debug(...args: any[]) { + if (!this.isProdMode) { + this.defaultConsoleLogger.debug.apply(this.defaultConsoleLogger.logger, arguments); + } + } - /** - * @hidden - */ - log(...args: any[]) { - if (!this.isProdMode) { - this.defaultConsoleLogger.log.apply(this.defaultConsoleLogger.logger, arguments); - } - if (this.isJSNLogSetup) { - JL().info(arguments); - } - } + /** + * @hidden + */ + info(...args: any[]) { + if (!this.isProdMode) { + this.defaultConsoleLogger.info.apply(this.defaultConsoleLogger.logger, arguments); + } + if (this.isJSNLogSetup) { + JL().info(arguments); + } + } - /** - * @hidden - */ - debug(...args: any[]) { - if (!this.isProdMode) { - this.defaultConsoleLogger.debug.apply(this.defaultConsoleLogger.logger, arguments); - } - } + /** + * @hidden + */ + warn(...args: any[]) { + this.defaultConsoleLogger.warn.apply(this.defaultConsoleLogger.logger, arguments); + if (this.isJSNLogSetup) { + JL().warn(arguments); + } + } - /** - * @hidden - */ - info(...args: any[]) { - if (!this.isProdMode) { - this.defaultConsoleLogger.info.apply(this.defaultConsoleLogger.logger, arguments); - } - if (this.isJSNLogSetup) { - JL().info(arguments); - } - } + /** + * @hidden + */ + error(...args: any[]) { + this.defaultConsoleLogger.error.apply(this.defaultConsoleLogger.logger, arguments); + if (this.isJSNLogSetup) { + JL().error(arguments); + } + } - /** - * @hidden - */ - warn(...args: any[]) { - this.defaultConsoleLogger.warn.apply(this.defaultConsoleLogger.logger, arguments); - if (this.isJSNLogSetup) { - JL().warn(arguments); - } - } - - /** - * @hidden - */ - error(...args: any[]) { - this.defaultConsoleLogger.error.apply(this.defaultConsoleLogger.logger, arguments); - if (this.isJSNLogSetup) { - JL().error(arguments); - } - } - - /** - * @hidden - */ - flush() { - if (this.isJSNLogSetup && this.currentAppender != null) { - this.currentAppender.sendBatch(); - } - } - - enableProdMode() { - this.isProdMode = true; - } + /** + * @hidden + */ + flush() { + if (this.isJSNLogSetup && this.currentAppender != null) { + this.currentAppender.sendBatch(); + } + } + enableProdMode() { + this.isProdMode = true; + } } diff --git a/openvidu-browser/src/OpenViduInternal/Logger/OpenViduLoggerConfiguration.ts b/openvidu-browser/src/OpenViduInternal/Logger/OpenViduLoggerConfiguration.ts index 8c4c518c..ff8d34ac 100644 --- a/openvidu-browser/src/OpenViduInternal/Logger/OpenViduLoggerConfiguration.ts +++ b/openvidu-browser/src/OpenViduInternal/Logger/OpenViduLoggerConfiguration.ts @@ -2,4 +2,4 @@ export enum OpenViduLoggerConfiguration { disabled = 'disabled', debug = 'debug', debug_app = 'debug_app' -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenViduInternal/ScreenSharing/Screen-Capturing-Auto.js b/openvidu-browser/src/OpenViduInternal/ScreenSharing/Screen-Capturing-Auto.js index f7810ce5..250ca60d 100644 --- a/openvidu-browser/src/OpenViduInternal/ScreenSharing/Screen-Capturing-Auto.js +++ b/openvidu-browser/src/OpenViduInternal/ScreenSharing/Screen-Capturing-Auto.js @@ -54,7 +54,11 @@ globalThis.getScreenId = function (firefoxString, callback, custom_parameter) { if (event.data.chromeMediaSourceId === 'PermissionDeniedError') { callback('permission-denied'); } else { - callback(null, event.data.chromeMediaSourceId, getScreenConstraints(null, event.data.chromeMediaSourceId, event.data.canRequestAudioTrack)); + callback( + null, + event.data.chromeMediaSourceId, + getScreenConstraints(null, event.data.chromeMediaSourceId, event.data.canRequestAudioTrack) + ); } // this event listener is no more needed @@ -71,8 +75,7 @@ globalThis.getScreenId = function (firefoxString, callback, custom_parameter) { if (!custom_parameter) { setTimeout(postGetSourceIdMessage, 100); - } - else { + } else { setTimeout(function () { postGetSourceIdMessage(custom_parameter); }, 100); @@ -95,7 +98,7 @@ function getScreenConstraints(error, sourceId, canRequestAudioTrack) { if (!!canRequestAudioTrack) { screen_constraints.audio = { mandatory: { - chromeMediaSource: error ? 'screen' : 'desktop', + chromeMediaSource: error ? 'screen' : 'desktop' // echoCancellation: true }, optional: [] @@ -129,19 +132,26 @@ function postGetSourceIdMessage(custom_parameter) { } if (!custom_parameter) { - iframe.contentWindow.postMessage({ - captureSourceId: true - }, '*'); - } - else if (!!custom_parameter.forEach) { - iframe.contentWindow.postMessage({ - captureCustomSourceId: custom_parameter - }, '*'); - } - else { - iframe.contentWindow.postMessage({ - captureSourceIdWithAudio: true - }, '*'); + iframe.contentWindow.postMessage( + { + captureSourceId: true + }, + '*' + ); + } else if (!!custom_parameter.forEach) { + iframe.contentWindow.postMessage( + { + captureCustomSourceId: custom_parameter + }, + '*' + ); + } else { + iframe.contentWindow.postMessage( + { + captureSourceIdWithAudio: true + }, + '*' + ); } } @@ -212,9 +222,12 @@ function postGetChromeExtensionStatusMessage() { return; } - iframe.contentWindow.postMessage({ - getChromeExtensionStatus: true - }, '*'); + iframe.contentWindow.postMessage( + { + getChromeExtensionStatus: true + }, + '*' + ); } exports.getScreenId = globalThis.getScreenId; diff --git a/openvidu-browser/src/OpenViduInternal/ScreenSharing/Screen-Capturing.js b/openvidu-browser/src/OpenViduInternal/ScreenSharing/Screen-Capturing.js index 49b5a442..0eb3cfd3 100644 --- a/openvidu-browser/src/OpenViduInternal/ScreenSharing/Screen-Capturing.js +++ b/openvidu-browser/src/OpenViduInternal/ScreenSharing/Screen-Capturing.js @@ -3,7 +3,7 @@ var chromeMediaSource = 'screen'; var sourceId; var screenCallback; -if(typeof window !== 'undefined' && typeof navigator !== 'undefined' && typeof navigator.userAgent !== 'undefined'){ +if (typeof window !== 'undefined' && typeof navigator !== 'undefined' && typeof navigator.userAgent !== 'undefined') { var isFirefox = typeof window.InstallTrigger !== 'undefined'; var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0; var isChrome = !!window.chrome && !isOpera; @@ -20,10 +20,8 @@ if(typeof window !== 'undefined' && typeof navigator !== 'undefined' && typeof n function onMessageCallback(data) { // "cancel" button is clicked if (data == 'PermissionDeniedError') { - if (screenCallback) - return screenCallback('PermissionDeniedError'); - else - throw new Error('PermissionDeniedError'); + if (screenCallback) return screenCallback('PermissionDeniedError'); + else throw new Error('PermissionDeniedError'); } // extension notified his presence if (data == 'rtcmulticonnection-extension-loaded') { @@ -31,7 +29,7 @@ function onMessageCallback(data) { } // extension shared temp sourceId if (data.sourceId && screenCallback) { - screenCallback(sourceId = data.sourceId, data.canRequestAudioTrack === true); + screenCallback((sourceId = data.sourceId), data.canRequestAudioTrack === true); } } @@ -51,10 +49,8 @@ function isChromeExtensionAvailable(callback) { // this function can be used to get "source-id" from the extension function getSourceId(callback) { - if (!callback) - throw '"callback" parameter is mandatory.'; - if (sourceId) - return callback(sourceId); + if (!callback) throw '"callback" parameter is mandatory.'; + if (sourceId) return callback(sourceId); screenCallback = callback; window.postMessage('get-sourceId', '*'); } @@ -67,9 +63,12 @@ function getCustomSourceId(arr, callback) { if (sourceId) return callback(sourceId); screenCallback = callback; - window.postMessage({ - 'get-custom-sourceId': arr - }, '*'); + window.postMessage( + { + 'get-custom-sourceId': arr + }, + '*' + ); } // this function can be used to get "source-id" from the extension @@ -82,8 +81,7 @@ function getSourceIdWithAudio(callback) { } function getChromeExtensionStatus(extensionid, callback) { - if (isFirefox) - return callback('not-chrome'); + if (isFirefox) return callback('not-chrome'); if (arguments.length != 2) { callback = extensionid; extensionid = 'lfcgfepafnobdloecchnfaclibenjold'; // default extension-id @@ -96,8 +94,7 @@ function getChromeExtensionStatus(extensionid, callback) { setTimeout(function () { if (chromeMediaSource == 'screen') { callback('installed-disabled'); - } else - callback('installed-enabled'); + } else callback('installed-enabled'); }, 2000); }; image.onerror = function () { @@ -116,8 +113,7 @@ function getScreenConstraints(callback, captureSourceIdWithAudio) { mozMediaSource: 'window', mediaSource: 'window' }; - if (isFirefox) - return callback(null, firefoxScreenConstraints); + if (isFirefox) return callback(null, firefoxScreenConstraints); // this statement defines getUserMedia constraints // that will be used to capture content of screen var screen_constraints = { @@ -141,8 +137,7 @@ function getScreenConstraints(callback, captureSourceIdWithAudio) { } callback(sourceId == 'PermissionDeniedError' ? sourceId : null, screen_constraints); }); - } - else { + } else { getSourceId(function (sourceId) { screen_constraints.mandatory.chromeMediaSourceId = sourceId; callback(sourceId == 'PermissionDeniedError' ? sourceId : null, screen_constraints); @@ -164,4 +159,4 @@ exports.getScreenConstraints = getScreenConstraints; exports.getScreenConstraintsWithAudio = getScreenConstraintsWithAudio; exports.isChromeExtensionAvailable = isChromeExtensionAvailable; exports.getChromeExtensionStatus = getChromeExtensionStatus; -exports.getSourceId = getSourceId; \ No newline at end of file +exports.getSourceId = getSourceId; diff --git a/openvidu-browser/src/OpenViduInternal/Utils/Platform.ts b/openvidu-browser/src/OpenViduInternal/Utils/Platform.ts index a8dece9a..3281ea7f 100644 --- a/openvidu-browser/src/OpenViduInternal/Utils/Platform.ts +++ b/openvidu-browser/src/OpenViduInternal/Utils/Platform.ts @@ -1,222 +1,221 @@ import platform = require('platform'); export class PlatformUtils { - protected static instance: PlatformUtils; - constructor() { } + protected static instance: PlatformUtils; + constructor() {} - static getInstance(): PlatformUtils { - if (!this.instance) { - this.instance = new PlatformUtils(); - } - return PlatformUtils.instance; - } + static getInstance(): PlatformUtils { + if (!this.instance) { + this.instance = new PlatformUtils(); + } + return PlatformUtils.instance; + } - public isChromeBrowser(): boolean { - return platform.name === "Chrome"; - } + public isChromeBrowser(): boolean { + return platform.name === 'Chrome'; + } - /** - * @hidden - */ - public isSafariBrowser(): boolean { - return platform.name === "Safari"; - } + /** + * @hidden + */ + public isSafariBrowser(): boolean { + return platform.name === 'Safari'; + } - /** - * @hidden - */ - public isChromeMobileBrowser(): boolean { - return platform.name === "Chrome Mobile"; - } + /** + * @hidden + */ + public isChromeMobileBrowser(): boolean { + return platform.name === 'Chrome Mobile'; + } - /** - * @hidden - */ - public isFirefoxBrowser(): boolean { - return platform.name === "Firefox"; - } + /** + * @hidden + */ + public isFirefoxBrowser(): boolean { + return platform.name === 'Firefox'; + } - /** - * @hidden - */ - public isFirefoxMobileBrowser(): boolean { - return platform.name === "Firefox Mobile" || platform.name === "Firefox for iOS"; - } + /** + * @hidden + */ + public isFirefoxMobileBrowser(): boolean { + return platform.name === 'Firefox Mobile' || platform.name === 'Firefox for iOS'; + } - /** - * @hidden - */ - public isOperaBrowser(): boolean { - return platform.name === "Opera"; - } + /** + * @hidden + */ + public isOperaBrowser(): boolean { + return platform.name === 'Opera'; + } - /** - * @hidden - */ - public isOperaMobileBrowser(): boolean { - return platform.name === "Opera Mobile"; - } + /** + * @hidden + */ + public isOperaMobileBrowser(): boolean { + return platform.name === 'Opera Mobile'; + } - /** - * @hidden - */ - public isEdgeBrowser(): boolean { - const version = platform?.version ? parseFloat(platform.version) : -1; - return platform.name === "Microsoft Edge" && version >= 80; - } + /** + * @hidden + */ + public isEdgeBrowser(): boolean { + const version = platform?.version ? parseFloat(platform.version) : -1; + return platform.name === 'Microsoft Edge' && version >= 80; + } - /** - * @hidden - */ - public isEdgeMobileBrowser(): boolean { - const version = platform?.version ? parseFloat(platform.version) : -1; - return platform.name === "Microsoft Edge" && (platform.os?.family === 'Android' || platform.os?.family === 'iOS') && version > 45; - } + /** + * @hidden + */ + public isEdgeMobileBrowser(): boolean { + const version = platform?.version ? parseFloat(platform.version) : -1; + return platform.name === 'Microsoft Edge' && (platform.os?.family === 'Android' || platform.os?.family === 'iOS') && version > 45; + } - /** - * @hidden - */ - public isAndroidBrowser(): boolean { - return platform.name === "Android Browser"; - } + /** + * @hidden + */ + public isAndroidBrowser(): boolean { + return platform.name === 'Android Browser'; + } - /** - * @hidden - */ - public isElectron(): boolean { - return platform.name === "Electron"; - } + /** + * @hidden + */ + public isElectron(): boolean { + return platform.name === 'Electron'; + } - /** - * @hidden - */ - public isNodeJs(): boolean { - return platform.name === "Node.js"; - } + /** + * @hidden + */ + public isNodeJs(): boolean { + return platform.name === 'Node.js'; + } - /** - * @hidden - */ - public isSamsungBrowser(): boolean { - return ( - platform.name === "Samsung Internet Mobile" || - platform.name === "Samsung Internet" - ); - } + /** + * @hidden + */ + public isSamsungBrowser(): boolean { + return platform.name === 'Samsung Internet Mobile' || platform.name === 'Samsung Internet'; + } - /** - * @hidden - */ - public isIPhoneOrIPad(): boolean { - const userAgent = !!platform.ua ? platform.ua : navigator.userAgent; - const isTouchable = "ontouchend" in document; - const isIPad = /\b(\w*Macintosh\w*)\b/.test(userAgent) && isTouchable; - const isIPhone = - /\b(\w*iPhone\w*)\b/.test(userAgent) && - /\b(\w*Mobile\w*)\b/.test(userAgent) && - isTouchable; - return isIPad || isIPhone; - } + /** + * @hidden + */ + public isIPhoneOrIPad(): boolean { + const userAgent = !!platform.ua ? platform.ua : navigator.userAgent; + const isTouchable = 'ontouchend' in document; + const isIPad = /\b(\w*Macintosh\w*)\b/.test(userAgent) && isTouchable; + const isIPhone = /\b(\w*iPhone\w*)\b/.test(userAgent) && /\b(\w*Mobile\w*)\b/.test(userAgent) && isTouchable; + return isIPad || isIPhone; + } - /** - * @hidden - */ - public isIOSWithSafari(): boolean { - const userAgent = !!platform.ua ? platform.ua : navigator.userAgent; - return this.isIPhoneOrIPad() && ( - /\b(\w*Apple\w*)\b/.test(navigator.vendor) && - /\b(\w*Safari\w*)\b/.test(userAgent) && - !/\b(\w*CriOS\w*)\b/.test(userAgent) && - !/\b(\w*FxiOS\w*)\b/.test(userAgent) - ); - } + /** + * @hidden + */ + public isIOSWithSafari(): boolean { + const userAgent = !!platform.ua ? platform.ua : navigator.userAgent; + return ( + this.isIPhoneOrIPad() && + /\b(\w*Apple\w*)\b/.test(navigator.vendor) && + /\b(\w*Safari\w*)\b/.test(userAgent) && + !/\b(\w*CriOS\w*)\b/.test(userAgent) && + !/\b(\w*FxiOS\w*)\b/.test(userAgent) + ); + } - /** - * @hidden - */ - public isIonicIos(): boolean { - return this.isIPhoneOrIPad() && platform.ua!!.indexOf("Safari") === -1; - } + /** + * @hidden + */ + public isIonicIos(): boolean { + return this.isIPhoneOrIPad() && platform.ua!!.indexOf('Safari') === -1; + } - /** - * @hidden - */ - public isIonicAndroid(): boolean { - return ( - platform.os!!.family === "Android" && platform.name == "Android Browser" - ); - } + /** + * @hidden + */ + public isIonicAndroid(): boolean { + return platform.os!!.family === 'Android' && platform.name == 'Android Browser'; + } - /** - * @hidden - */ - public isMobileDevice(): boolean { - return platform.os!!.family === "iOS" || platform.os!!.family === "Android"; - } + /** + * @hidden + */ + public isMobileDevice(): boolean { + return platform.os!!.family === 'iOS' || platform.os!!.family === 'Android'; + } - /** - * @hidden - */ - public isReactNative(): boolean { - return false; - } + /** + * @hidden + */ + public isReactNative(): boolean { + return false; + } - /** - * @hidden - */ - public isChromium(): boolean { - return this.isChromeBrowser() || this.isChromeMobileBrowser() || - this.isOperaBrowser() || this.isOperaMobileBrowser() || - this.isEdgeBrowser() || this.isEdgeMobileBrowser() || - this.isSamsungBrowser() || - this.isIonicAndroid() || this.isIonicIos() || - this.isElectron(); - } + /** + * @hidden + */ + public isChromium(): boolean { + return ( + this.isChromeBrowser() || + this.isChromeMobileBrowser() || + this.isOperaBrowser() || + this.isOperaMobileBrowser() || + this.isEdgeBrowser() || + this.isEdgeMobileBrowser() || + this.isSamsungBrowser() || + this.isIonicAndroid() || + this.isIonicIos() || + this.isElectron() + ); + } - /** - * @hidden - */ - public canScreenShare(): boolean { - const version = platform?.version ? parseFloat(platform.version) : -1; - // Reject mobile devices - if (this.isMobileDevice()) { - return false; - } - return ( - this.isChromeBrowser() || - this.isFirefoxBrowser() || - this.isOperaBrowser() || - this.isElectron() || - this.isEdgeBrowser() || - (this.isSafariBrowser() && version >= 13) - ); - } + /** + * @hidden + */ + public canScreenShare(): boolean { + const version = platform?.version ? parseFloat(platform.version) : -1; + // Reject mobile devices + if (this.isMobileDevice()) { + return false; + } + return ( + this.isChromeBrowser() || + this.isFirefoxBrowser() || + this.isOperaBrowser() || + this.isElectron() || + this.isEdgeBrowser() || + (this.isSafariBrowser() && version >= 13) + ); + } - /** - * @hidden - */ - public getName(): string { - return platform.name || ""; - } + /** + * @hidden + */ + public getName(): string { + return platform.name || ''; + } - /** - * @hidden - */ - public getVersion(): string { - return platform.version || ""; - } + /** + * @hidden + */ + public getVersion(): string { + return platform.version || ''; + } - /** - * @hidden - */ - public getFamily(): string { - return platform.os!!.family || ""; - } + /** + * @hidden + */ + public getFamily(): string { + return platform.os!!.family || ''; + } - /** - * @hidden - */ - public getDescription(): string { - return platform.description || ""; - } + /** + * @hidden + */ + public getDescription(): string { + return platform.description || ''; + } } diff --git a/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts b/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts index e8ebd58f..d7df8d4d 100644 --- a/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts +++ b/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts @@ -63,24 +63,17 @@ export class WebRtcPeer { this.configuration = { ...configuration, - iceServers: - !!configuration.iceServers && - configuration.iceServers.length > 0 - ? configuration.iceServers - : freeice(), - mediaStream: - configuration.mediaStream !== undefined - ? configuration.mediaStream - : null, - mode: !!configuration.mode ? configuration.mode : "sendrecv", - id: !!configuration.id ? configuration.id : this.generateUniqueId(), + iceServers: !!configuration.iceServers && configuration.iceServers.length > 0 ? configuration.iceServers : freeice(), + mediaStream: configuration.mediaStream !== undefined ? configuration.mediaStream : null, + mode: !!configuration.mode ? configuration.mode : 'sendrecv', + id: !!configuration.id ? configuration.id : this.generateUniqueId() }; // prettier-ignore logger.debug(`[WebRtcPeer] configuration:\n${JSON.stringify(this.configuration, null, 2)}`); this.pc = new RTCPeerConnection({ iceServers: this.configuration.iceServers }); - this.pc.addEventListener("icecandidate", (event: RTCPeerConnectionIceEvent) => { + this.pc.addEventListener('icecandidate', (event: RTCPeerConnectionIceEvent) => { if (event.candidate !== null) { // `RTCPeerConnectionIceEvent.candidate` is supposed to be an RTCIceCandidate: // https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnectioniceevent-candidate @@ -140,11 +133,11 @@ export class WebRtcPeer { const hasVideo = this.configuration.mediaConstraints.video; const options: RTCOfferOptions = { - offerToReceiveAudio: this.configuration.mode !== "sendonly" && hasAudio, - offerToReceiveVideo: this.configuration.mode !== "sendonly" && hasVideo, + offerToReceiveAudio: this.configuration.mode !== 'sendonly' && hasAudio, + offerToReceiveVideo: this.configuration.mode !== 'sendonly' && hasVideo }; - logger.debug("[createOfferLegacy] RTCPeerConnection.createOffer() options:", JSON.stringify(options)); + logger.debug('[createOfferLegacy] RTCPeerConnection.createOffer() options:', JSON.stringify(options)); return this.pc.createOffer(options); } @@ -156,18 +149,18 @@ export class WebRtcPeer { async createOffer(): Promise { // TODO: Delete this conditional when all supported browsers are // modern enough to implement the Transceiver methods. - if (!("addTransceiver" in this.pc)) { + if (!('addTransceiver' in this.pc)) { logger.warn( - "[createOffer] Method RTCPeerConnection.addTransceiver() is NOT available; using LEGACY offerToReceive{Audio,Video}" + '[createOffer] Method RTCPeerConnection.addTransceiver() is NOT available; using LEGACY offerToReceive{Audio,Video}' ); return this.createOfferLegacy(); } else { - logger.debug("[createOffer] Method RTCPeerConnection.addTransceiver() is available; using it"); + logger.debug('[createOffer] Method RTCPeerConnection.addTransceiver() is available; using it'); } // Spec doc: https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-addtransceiver - if (this.configuration.mode !== "recvonly") { + if (this.configuration.mode !== 'recvonly') { // To send media, assume that all desired media tracks have been // already added by higher level code to our MediaStream. @@ -180,24 +173,18 @@ export class WebRtcPeer { for (const track of this.configuration.mediaStream.getTracks()) { const tcInit: RTCRtpTransceiverInit = { direction: this.configuration.mode, - streams: [this.configuration.mediaStream], + streams: [this.configuration.mediaStream] }; - if (track.kind === "video" && this.configuration.simulcast) { + if (track.kind === 'video' && this.configuration.simulcast) { // Check if the requested size is enough to ask for 3 layers. const trackSettings = track.getSettings(); const trackConsts = track.getConstraints(); const trackWidth: number = - trackSettings.width ?? - (trackConsts.width as ConstrainULongRange).ideal ?? - (trackConsts.width as number) ?? - 0; + trackSettings.width ?? (trackConsts.width as ConstrainULongRange).ideal ?? (trackConsts.width as number) ?? 0; const trackHeight: number = - trackSettings.height ?? - (trackConsts.height as ConstrainULongRange).ideal ?? - (trackConsts.height as number) ?? - 0; + trackSettings.height ?? (trackConsts.height as ConstrainULongRange).ideal ?? (trackConsts.height as number) ?? 0; logger.info(`[createOffer] Video track dimensions: ${trackWidth}x${trackHeight}`); const trackPixels = trackWidth * trackHeight; @@ -215,13 +202,13 @@ export class WebRtcPeer { const layerDiv = 2 ** (maxLayers - l - 1); const encoding: RTCRtpEncodingParameters = { - rid: "rdiv" + layerDiv.toString(), + rid: 'rdiv' + layerDiv.toString(), // @ts-ignore -- Property missing from DOM types. - scalabilityMode: "L1T1", + scalabilityMode: 'L1T1' }; - if (["detail", "text"].includes(track.contentHint)) { + if (['detail', 'text'].includes(track.contentHint)) { // Prioritize best resolution, for maximum picture detail. encoding.scaleResolutionDownBy = 1.0; @@ -237,22 +224,20 @@ export class WebRtcPeer { const tc = this.pc.addTransceiver(track, tcInit); - if (track.kind === "video") { + if (track.kind === 'video') { let sendParams = tc.sender.getParameters(); let needSetParams = false; if (!sendParams.degradationPreference?.length) { // degradationPreference for video: "balanced", "maintain-framerate", "maintain-resolution". // https://www.w3.org/TR/2018/CR-webrtc-20180927/#dom-rtcdegradationpreference - if (["detail", "text"].includes(track.contentHint)) { - sendParams.degradationPreference = "maintain-resolution"; + if (['detail', 'text'].includes(track.contentHint)) { + sendParams.degradationPreference = 'maintain-resolution'; } else { - sendParams.degradationPreference = "balanced"; + sendParams.degradationPreference = 'balanced'; } - logger.info( - `[createOffer] Video sender Degradation Preference set: ${sendParams.degradationPreference}` - ); + logger.info(`[createOffer] Video sender Degradation Preference set: ${sendParams.degradationPreference}`); // FIXME: Firefox implements degradationPreference on each individual encoding! // (set it on every element of the sendParams.encodings array) @@ -310,7 +295,7 @@ export class WebRtcPeer { } } else { // To just receive media, create new recvonly transceivers. - for (const kind of ["audio", "video"]) { + for (const kind of ['audio', 'video']) { // Check if the media kind should be used. if (!this.configuration.mediaConstraints[kind]) { continue; @@ -319,7 +304,7 @@ export class WebRtcPeer { this.configuration.mediaStream = new MediaStream(); this.pc.addTransceiver(kind, { direction: this.configuration.mode, - streams: [this.configuration.mediaStream], + streams: [this.configuration.mediaStream] }); } } @@ -352,23 +337,21 @@ export class WebRtcPeer { return new Promise((resolve, reject) => { // TODO: Delete this conditional when all supported browsers are // modern enough to implement the Transceiver methods. - if ("getTransceivers" in this.pc) { - logger.debug("[createAnswer] Method RTCPeerConnection.getTransceivers() is available; using it"); + if ('getTransceivers' in this.pc) { + logger.debug('[createAnswer] Method RTCPeerConnection.getTransceivers() is available; using it'); // Ensure that the PeerConnection already contains one Transceiver // for each kind of media. // The Transceivers should have been already created internally by // the PC itself, when `pc.setRemoteDescription(sdpOffer)` was called. - for (const kind of ["audio", "video"]) { + for (const kind of ['audio', 'video']) { // Check if the media kind should be used. if (!this.configuration.mediaConstraints[kind]) { continue; } - let tc = this.pc - .getTransceivers() - .find((tc) => tc.receiver.track.kind === kind); + let tc = this.pc.getTransceivers().find((tc) => tc.receiver.track.kind === kind); if (tc) { // Enforce our desired direction. @@ -382,27 +365,25 @@ export class WebRtcPeer { .createAnswer() .then((sdpAnswer) => resolve(sdpAnswer)) .catch((error) => reject(error)); - } else { - // TODO: Delete else branch when all supported browsers are // modern enough to implement the Transceiver methods - let offerAudio, offerVideo = true; + let offerAudio, + offerVideo = true; if (!!this.configuration.mediaConstraints) { - offerAudio = (typeof this.configuration.mediaConstraints.audio === 'boolean') ? - this.configuration.mediaConstraints.audio : true; - offerVideo = (typeof this.configuration.mediaConstraints.video === 'boolean') ? - this.configuration.mediaConstraints.video : true; + offerAudio = + typeof this.configuration.mediaConstraints.audio === 'boolean' ? this.configuration.mediaConstraints.audio : true; + offerVideo = + typeof this.configuration.mediaConstraints.video === 'boolean' ? this.configuration.mediaConstraints.video : true; const constraints: RTCOfferOptions = { offerToReceiveAudio: offerAudio, offerToReceiveVideo: offerVideo }; this.pc!.createAnswer(constraints) - .then(sdpAnswer => resolve(sdpAnswer)) - .catch(error => reject(error)); + .then((sdpAnswer) => resolve(sdpAnswer)) + .catch((error) => reject(error)); } - } // else, there is nothing to do; the legacy createAnswer() options do @@ -415,7 +396,8 @@ export class WebRtcPeer { */ processLocalOffer(offer: RTCSessionDescriptionInit): Promise { return new Promise((resolve, reject) => { - this.pc.setLocalDescription(offer) + this.pc + .setLocalDescription(offer) .then(() => { const localDescription = this.pc.localDescription; if (!!localDescription) { @@ -425,7 +407,7 @@ export class WebRtcPeer { return reject('Local description is not defined'); } }) - .catch(error => reject(error)); + .catch((error) => reject(error)); }); } @@ -445,7 +427,7 @@ export class WebRtcPeer { } this.setRemoteDescription(offer) .then(() => resolve()) - .catch(error => reject(error)); + .catch((error) => reject(error)); }); } @@ -458,9 +440,10 @@ export class WebRtcPeer { if (this.pc.signalingState === 'closed') { return reject('RTCPeerConnection is closed when trying to set local description'); } - this.pc.setLocalDescription(answer) + this.pc + .setLocalDescription(answer) .then(() => resolve()) - .catch(error => reject(error)); + .catch((error) => reject(error)); }); } @@ -513,7 +496,10 @@ export class WebRtcPeer { break; case 'stable': if (!!this.pc.remoteDescription) { - this.pc.addIceCandidate(iceCandidate).then(() => resolve()).catch(error => reject(error)); + this.pc + .addIceCandidate(iceCandidate) + .then(() => resolve()) + .catch((error) => reject(error)); } else { this.iceCandidateList.push(iceCandidate); resolve(); @@ -532,7 +518,12 @@ export class WebRtcPeer { switch (iceConnectionState) { case 'disconnected': // Possible network disconnection - const msg1 = 'IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "disconnected". Possible network disconnection'; + const msg1 = + 'IceConnectionState of RTCPeerConnection ' + + this.configuration.id + + ' (' + + otherId + + ') change to "disconnected". Possible network disconnection'; logger.warn(msg1); this.configuration.onIceConnectionStateException(ExceptionEventName.ICE_CONNECTION_DISCONNECTED, msg1); break; @@ -542,19 +533,27 @@ export class WebRtcPeer { this.configuration.onIceConnectionStateException(ExceptionEventName.ICE_CONNECTION_FAILED, msg2); break; case 'closed': - logger.log('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "closed"'); + logger.log( + 'IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "closed"' + ); break; case 'new': logger.log('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "new"'); break; case 'checking': - logger.log('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "checking"'); + logger.log( + 'IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "checking"' + ); break; case 'connected': - logger.log('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "connected"'); + logger.log( + 'IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "connected"' + ); break; case 'completed': - logger.log('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "completed"'); + logger.log( + 'IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "completed"' + ); break; } }); @@ -566,10 +565,8 @@ export class WebRtcPeer { generateUniqueId(): string { return uuidv4(); } - } - export class WebRtcPeerRecvonly extends WebRtcPeer { constructor(configuration: WebRtcPeerConfiguration) { configuration.mode = 'recvonly'; diff --git a/openvidu-browser/src/OpenViduInternal/WebRtcStats/WebRtcStats.ts b/openvidu-browser/src/OpenViduInternal/WebRtcStats/WebRtcStats.ts index 624c1324..adc9572a 100644 --- a/openvidu-browser/src/OpenViduInternal/WebRtcStats/WebRtcStats.ts +++ b/openvidu-browser/src/OpenViduInternal/WebRtcStats/WebRtcStats.ts @@ -30,18 +30,18 @@ const logger: OpenViduLogger = OpenViduLogger.getInstance(); let platform: PlatformUtils; interface WebrtcStatsConfig { - interval: number, - httpEndpoint: string + interval: number; + httpEndpoint: string; } interface JSONStatsResponse { - '@timestamp': string, - participant_id: string, - session_id: string, - platform: string, - platform_description: string, - stream: string, - webrtc_stats: IWebrtcStats + '@timestamp': string; + participant_id: string; + session_id: string; + platform: string; + platform_description: string; + stream: string; + webrtc_stats: IWebrtcStats; } /** @@ -49,55 +49,62 @@ interface JSONStatsResponse { */ interface IWebrtcStats { inbound?: { - audio: { - bytesReceived: number, - packetsReceived: number, - packetsLost: number, - jitter: number - } | {}, - video: { - bytesReceived: number, - packetsReceived: number, - packetsLost: number, - jitter?: number, // Firefox - jitterBufferDelay?: number, // Chrome - framesDecoded: number, - firCount: number, - nackCount: number, - pliCount: number, - frameHeight?: number, // Chrome - frameWidth?: number, // Chrome - framesDropped?: number, // Chrome - framesReceived?: number // Chrome - } | {} - }, + audio: + | { + bytesReceived: number; + packetsReceived: number; + packetsLost: number; + jitter: number; + } + | {}; + video: + | { + bytesReceived: number; + packetsReceived: number; + packetsLost: number; + jitter?: number; // Firefox + jitterBufferDelay?: number; // Chrome + framesDecoded: number; + firCount: number; + nackCount: number; + pliCount: number; + frameHeight?: number; // Chrome + frameWidth?: number; // Chrome + framesDropped?: number; // Chrome + framesReceived?: number; // Chrome + } + | {}; + }; outbound?: { - audio: { - bytesSent: number, - packetsSent: number, - } | {}, - video: { - bytesSent: number, - packetsSent: number, - firCount: number, - framesEncoded: number, - nackCount: number, - pliCount: number, - qpSum: number, - frameHeight?: number, // Chrome - frameWidth?: number, // Chrome - framesSent?: number // Chrome - } | {} - }, + audio: + | { + bytesSent: number; + packetsSent: number; + } + | {}; + video: + | { + bytesSent: number; + packetsSent: number; + firCount: number; + framesEncoded: number; + nackCount: number; + pliCount: number; + qpSum: number; + frameHeight?: number; // Chrome + frameWidth?: number; // Chrome + framesSent?: number; // Chrome + } + | {}; + }; candidatepair?: { - currentRoundTripTime?: number // Chrome - availableOutgoingBitrate?: number //Chrome + currentRoundTripTime?: number; // Chrome + availableOutgoingBitrate?: number; //Chrome // availableIncomingBitrate?: number // No support for any browsers (https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidatePairStats/availableIncomingBitrate) - } -}; + }; +} export class WebRtcStats { - private readonly STATS_ITEM_NAME = 'webrtc-stats-config'; private webRtcStatsEnabled = false; @@ -114,23 +121,23 @@ export class WebRtcStats { } public initWebRtcStats(): void { - const webrtcObj = localStorage.getItem(this.STATS_ITEM_NAME); if (!!webrtcObj) { this.webRtcStatsEnabled = true; const webrtcStatsConfig: WebrtcStatsConfig = JSON.parse(webrtcObj); // webrtc object found in local storage - logger.warn('WebRtc stats enabled for stream ' + this.stream.streamId + ' of connection ' + this.stream.connection.connectionId); + logger.warn( + 'WebRtc stats enabled for stream ' + this.stream.streamId + ' of connection ' + this.stream.connection.connectionId + ); logger.warn('localStorage item: ' + JSON.stringify(webrtcStatsConfig)); this.POST_URL = webrtcStatsConfig.httpEndpoint; - this.statsInterval = webrtcStatsConfig.interval; // Interval in seconds + this.statsInterval = webrtcStatsConfig.interval; // Interval in seconds this.webRtcStatsIntervalId = setInterval(async () => { await this.sendStatsToHttpEndpoint(); }, this.statsInterval * 1000); - } else { logger.debug('WebRtc stats not enabled'); } @@ -206,7 +213,6 @@ export class WebRtcStats { // - ¿React Native? public getSelectedIceCandidateInfo(): Promise { return new Promise(async (resolve, reject) => { - const statsReport: any = await this.stream.getRTCPeerConnection().getStats(); let transportStat; const candidatePairs: Map = new Map(); @@ -230,7 +236,7 @@ export class WebRtcStats { }); let selectedCandidatePair; if (transportStat != null) { - const selectedCandidatePairId = transportStat.selectedCandidatePairId + const selectedCandidatePairId = transportStat.selectedCandidatePairId; selectedCandidatePair = candidatePairs.get(selectedCandidatePairId); } else { // This is basically Firefox @@ -250,9 +256,11 @@ export class WebRtcStats { if (!!finalLocalCandidate) { const candList = this.stream.getLocalIceCandidateList(); const cand = candList.filter((c: RTCIceCandidate) => { - return (!!c.candidate && + return ( + !!c.candidate && (c.candidate.indexOf(finalLocalCandidate.ip) >= 0 || c.candidate.indexOf(finalLocalCandidate.address) >= 0) && - c.candidate.indexOf(finalLocalCandidate.port) >= 0); + c.candidate.indexOf(finalLocalCandidate.port) >= 0 + ); }); finalLocalCandidate.raw = []; for (let c of cand) { @@ -266,9 +274,11 @@ export class WebRtcStats { if (!!finalRemoteCandidate) { const candList = this.stream.getRemoteIceCandidateList(); const cand = candList.filter((c: RTCIceCandidate) => { - return (!!c.candidate && + return ( + !!c.candidate && (c.candidate.indexOf(finalRemoteCandidate.ip) >= 0 || c.candidate.indexOf(finalRemoteCandidate.address) >= 0) && - c.candidate.indexOf(finalRemoteCandidate.port) >= 0); + c.candidate.indexOf(finalRemoteCandidate.port) >= 0 + ); }); finalRemoteCandidate.raw = []; for (let c of cand) { @@ -288,7 +298,9 @@ export class WebRtcStats { public stopWebRtcStats() { if (this.webRtcStatsEnabled) { clearInterval(this.webRtcStatsIntervalId); - logger.warn('WebRtc stats stopped for disposed stream ' + this.stream.streamId + ' of connection ' + this.stream.connection.connectionId); + logger.warn( + 'WebRtc stats stopped for disposed stream ' + this.stream.streamId + ' of connection ' + this.stream.connection.connectionId + ); } } @@ -299,10 +311,9 @@ export class WebRtcStats { 'Content-type': 'application/json' }, body: JSON.stringify(response), - method: 'POST', + method: 'POST' }; await fetch(url, configuration); - } catch (error) { logger.error(`sendStats error: ${JSON.stringify(error)}`); } @@ -350,9 +361,7 @@ export class WebRtcStats { // - ¿Ionic? // - ¿React Native? public async getCommonStats(): Promise { - return new Promise(async (resolve, reject) => { - try { const statsReport: any = await this.stream.getRTCPeerConnection().getStats(); const response: IWebrtcStats = this.getWebRtcStatsResponseOutline(); @@ -360,24 +369,23 @@ export class WebRtcStats { const candidatePairStats = ['availableOutgoingBitrate', 'currentRoundTripTime']; statsReport.forEach((stat: any) => { - let mediaType = stat.mediaType != null ? stat.mediaType : stat.kind; const addStat = (direction: string, key: string): void => { if (stat[key] != null && response[direction] != null) { - if (!mediaType && (videoTrackStats.indexOf(key) > -1)) { + if (!mediaType && videoTrackStats.indexOf(key) > -1) { mediaType = 'video'; } if (direction != null && mediaType != null && key != null && response[direction][mediaType] != null) { response[direction][mediaType][key] = Number(stat[key]); - } else if(direction != null && key != null && candidatePairStats.includes(key)) { + } else if (direction != null && key != null && candidatePairStats.includes(key)) { // candidate-pair-stats response[direction][key] = Number(stat[key]); } } - } + }; switch (stat.type) { - case "outbound-rtp": + case 'outbound-rtp': addStat('outbound', 'bytesSent'); addStat('outbound', 'packetsSent'); addStat('outbound', 'framesEncoded'); @@ -386,7 +394,7 @@ export class WebRtcStats { addStat('outbound', 'pliCount'); addStat('outbound', 'qpSum'); break; - case "inbound-rtp": + case 'inbound-rtp': addStat('inbound', 'bytesReceived'); addStat('inbound', 'packetsReceived'); addStat('inbound', 'packetsLost'); @@ -412,7 +420,7 @@ export class WebRtcStats { }); // Delete candidatepair from response if null - if(!response?.candidatepair || Object.keys(response.candidatepair).length === 0){ + if (!response?.candidatepair || Object.keys(response.candidatepair).length === 0) { delete response.candidatepair; } @@ -421,7 +429,6 @@ export class WebRtcStats { logger.error('Error getting common stats: ', error); return reject(error); } - }); } @@ -455,5 +462,4 @@ export class WebRtcStats { }; } } - -} \ No newline at end of file +} diff --git a/openvidu-browser/src/index.ts b/openvidu-browser/src/index.ts index 90a7ba0e..fb94bb8d 100644 --- a/openvidu-browser/src/index.ts +++ b/openvidu-browser/src/index.ts @@ -45,4 +45,4 @@ export { StreamManagerEventMap } from './OpenViduInternal/Events/EventMap/Stream export { PublisherEventMap } from './OpenViduInternal/Events/EventMap/PublisherEventMap'; // Disable jsnlog when library is loaded -JL.setOptions({ enabled: false }) +JL.setOptions({ enabled: false });