diff --git a/README.md b/README.md index 95bc35ca..46a34a43 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Visit [openvidu.io](https://openvidu.io) ## Contributors This project exists thanks to all the people who contribute. - + ## Backers diff --git a/openvidu-browser/config/typedoc.js b/openvidu-browser/config/typedoc.js index 5516ee51..12b52250 100644 --- a/openvidu-browser/config/typedoc.js +++ b/openvidu-browser/config/typedoc.js @@ -19,6 +19,7 @@ module.exports = { ], excludeExternals: true, excludePrivate: true, + excludeProtected: true, excludeNotExported: true, theme: "default", readme: "none", diff --git a/openvidu-browser/package.json b/openvidu-browser/package.json index a2df896b..f5e4b720 100644 --- a/openvidu-browser/package.json +++ b/openvidu-browser/package.json @@ -1,31 +1,31 @@ { "author": "OpenVidu", "dependencies": { - "@types/node": "13.13.2", - "@types/platform": "1.3.2", "freeice": "2.2.2", "hark": "1.2.3", - "platform": "1.3.5", - "uuid": "7.0.3", + "platform": "1.3.6", + "uuid": "8.3.1", "wolfy87-eventemitter": "5.2.9" }, "description": "OpenVidu Browser", "devDependencies": { - "browserify": "16.5.1", - "grunt": "1.1.0", + "@types/node": "14.14.7", + "@types/platform": "1.3.3", + "browserify": "17.0.0", + "grunt": "1.3.0", "grunt-cli": "1.3.2", "grunt-contrib-copy": "1.0.0", - "grunt-contrib-sass": "1.0.0", - "grunt-contrib-uglify": "4.0.1", + "grunt-contrib-sass": "2.0.0", + "grunt-contrib-uglify": "5.0.0", "grunt-contrib-watch": "1.1.0", "grunt-postcss": "0.9.0", "grunt-string-replace": "1.3.1", "grunt-ts": "6.0.0-beta.22", - "terser": "4.6.11", - "tsify": "4.0.1", - "tslint": "6.1.1", - "typedoc": "0.17.4", - "typescript": "3.8.3" + "terser": "5.3.8", + "tsify": "5.0.2", + "tslint": "6.1.3", + "typedoc": "0.19.2", + "typescript": "4.0.5" }, "license": "Apache-2.0", "main": "lib/index.js", @@ -35,11 +35,11 @@ "url": "git://github.com/OpenVidu/openvidu" }, "scripts": { - "browserify": "VERSION=${VERSION:-dev}; cd src && ../node_modules/browserify/bin/cmd.js Main.ts -p [ tsify ] --exclude kurento-browser-extensions --debug -o ../static/js/openvidu-browser-$VERSION.js -v", - "browserify-prod": "VERSION=${VERSION:-dev}; cd src && ../node_modules/browserify/bin/cmd.js --debug Main.ts -p [ tsify ] --exclude kurento-browser-extensions | ../node_modules/terser/bin/terser --source-map content=inline --output ../static/js/openvidu-browser-$VERSION.min.js", + "browserify": "VERSION=${VERSION:-dev}; mkdir -p static/js/ && cd src && ../node_modules/browserify/bin/cmd.js Main.ts -p [ tsify ] --exclude kurento-browser-extensions --debug -o ../static/js/openvidu-browser-$VERSION.js -v", + "browserify-prod": "VERSION=${VERSION:-dev}; mkdir -p static/js/ && cd src && ../node_modules/browserify/bin/cmd.js --debug Main.ts -p [ tsify ] --exclude kurento-browser-extensions | ../node_modules/terser/bin/terser --source-map content=inline --output ../static/js/openvidu-browser-$VERSION.min.js", "build": "cd src/OpenVidu && ./../../node_modules/typescript/bin/tsc && cd ../.. && ./node_modules/typescript/bin/tsc --declaration src/index.ts --outDir ./lib --sourceMap --lib dom,es5,es2015.promise,scripthost", "docs": "./generate-docs.sh" }, "types": "lib/index.d.ts", - "version": "2.15.0" + "version": "2.16.0" } diff --git a/openvidu-browser/reactnative.sh b/openvidu-browser/reactnative.sh deleted file mode 100755 index 6da39f95..00000000 --- a/openvidu-browser/reactnative.sh +++ /dev/null @@ -1,4 +0,0 @@ -npm run build -npm pack -cp openvidu-browser-2.14.0.tgz ../../openvidu-tutorials/openvidu-react-native -cp openvidu-browser-2.14.0.tgz ../../openvidu-react-native-adapter/ diff --git a/openvidu-browser/src/OpenVidu/Connection.ts b/openvidu-browser/src/OpenVidu/Connection.ts index 9fa378d5..0dd9c82a 100644 --- a/openvidu-browser/src/OpenVidu/Connection.ts +++ b/openvidu-browser/src/OpenVidu/Connection.ts @@ -18,7 +18,8 @@ import { Session } from './Session'; import { Stream } from './Stream'; import { StreamLEGACY } from './StreamLEGACY'; -import { ConnectionOptions } from '../OpenViduInternal/Interfaces/Private/ConnectionOptions'; +import { LocalConnectionOptions } from '../OpenViduInternal/Interfaces/Private/LocalConnectionOptions'; +import { RemoteConnectionOptions } from '../OpenViduInternal/Interfaces/Private/RemoteConnectionOptions'; import { InboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/InboundStreamOptions'; import { StreamOptionsServer } from '../OpenViduInternal/Interfaces/Private/StreamOptionsServer'; import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger'; @@ -52,14 +53,36 @@ export class Connection { data: string; /** - * @hidden + * Role of the connection. + * - `SUBSCRIBER`: can subscribe to published Streams of other users by calling [[Session.subscribe]] + * - `PUBLISHER`: SUBSCRIBER permissions + can publish their own Streams by calling [[Session.publish]] + * - `MODERATOR`: SUBSCRIBER + PUBLISHER permissions + can force the unpublishing or disconnection over a third-party Stream or Connection by call [[Session.forceUnpublish]] and [[Session.forceDisconnect]] + * + * **Only defined for the local connection. In remote connections will be `undefined`** */ - stream: Stream; + role: string; + + /** + * Whether the streams published by this connection will be recorded or not. This only affects [INDIVIDUAL recording](/en/stable/advanced-features/recording#selecting-streams-to-be-recorded) PRO + * + * **Only defined for the local connection. In remote connections will be `undefined`** + */ + record: boolean; /** * @hidden */ - options: ConnectionOptions | undefined; + stream?: Stream; + + /** + * @hidden + */ + localOptions: LocalConnectionOptions | undefined; + + /** + * @hidden + */ + remoteOptions: RemoteConnectionOptions | undefined; /** * @hidden @@ -74,23 +97,30 @@ export class Connection { /** * @hidden */ - constructor(private session: Session, opts?: ConnectionOptions) { + constructor(private session: Session, connectionOptions: LocalConnectionOptions | RemoteConnectionOptions) { let msg = "'Connection' created "; - if (!!opts) { - // Connection is remote - msg += "(remote) with 'connectionId' [" + opts.id + ']'; - this.options = opts; - this.connectionId = opts.id; - this.creationTime = opts.createdAt; - if (opts.metadata) { - this.data = opts.metadata; - } - if (opts.streams) { - this.initRemoteStreams(opts.streams); - } - } else { + if (!!(connectionOptions).role) { // Connection is local + this.localOptions = connectionOptions; + this.connectionId = this.localOptions.id; + this.creationTime = this.localOptions.createdAt; + this.data = this.localOptions.metadata; + this.rpcSessionId = this.localOptions.sessionId; + this.role = this.localOptions.role; + this.record = this.localOptions.record; msg += '(local)'; + } else { + // Connection is remote + this.remoteOptions = connectionOptions; + this.connectionId = this.remoteOptions.id; + this.creationTime = this.remoteOptions.createdAt; + if (this.remoteOptions.metadata) { + this.data = this.remoteOptions.metadata; + } + if (this.remoteOptions.streams) { + this.initRemoteStreams(this.remoteOptions.streams); + } + msg += "(remote) with 'connectionId' [" + this.remoteOptions.id + ']'; } logger.info(msg); } @@ -103,7 +133,7 @@ export class Connection { */ sendIceCandidate(candidate: RTCIceCandidate): void { - logger.debug((!!this.stream.outboundStreamOpts ? 'Local' : 'Remote') + 'candidate for' + + logger.debug((!!this.stream!.outboundStreamOpts ? 'Local' : 'Remote') + 'candidate for' + this.connectionId, candidate); this.session.openvidu.sendRequest('onIceCandidate', { @@ -149,7 +179,7 @@ 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); } /** @@ -177,4 +207,4 @@ export class Connection { this.disposed = true; } -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenVidu/Filter.ts b/openvidu-browser/src/OpenVidu/Filter.ts index 134c56c4..a7f0f760 100644 --- a/openvidu-browser/src/OpenVidu/Filter.ts +++ b/openvidu-browser/src/OpenVidu/Filter.ts @@ -119,9 +119,9 @@ export class Filter { } else { logger.info('Filter method successfully executed on Stream ' + this.stream.streamId); const oldValue = (Object).assign({}, this.stream.filter); - this.stream.filter.lastExecMethod = { method, params: JSON.parse(stringParams) }; - 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.filter!.lastExecMethod = { method, params: JSON.parse(stringParams) }; + 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')]); resolve(); } } diff --git a/openvidu-browser/src/OpenVidu/LocalRecorder.ts b/openvidu-browser/src/OpenVidu/LocalRecorder.ts index 958cd913..98a0bd8e 100644 --- a/openvidu-browser/src/OpenVidu/LocalRecorder.ts +++ b/openvidu-browser/src/OpenVidu/LocalRecorder.ts @@ -17,8 +17,8 @@ import { Stream } from './Stream'; import { LocalRecorderState } from '../OpenViduInternal/Enums/LocalRecorderState'; -import platform = require('platform'); import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger'; +import { PlatformUtils } from '../OpenViduInternal/Utils/Platform'; /** @@ -30,6 +30,11 @@ declare var MediaRecorder: any; */ const logger: OpenViduLogger = OpenViduLogger.getInstance(); +/** + * @hidden + */ +let platform: PlatformUtils; + /** * Easy recording of [[Stream]] objects straightaway from the browser. Initialized with [[OpenVidu.initLocalRecorder]] method @@ -45,7 +50,7 @@ export class LocalRecorder { private connectionId: string; private mediaRecorder: any; private chunks: any[] = []; - private blob: Blob; + private blob?: Blob; private id: string; private videoPreviewSrc: string; private videoPreview: HTMLVideoElement; @@ -54,6 +59,7 @@ export class LocalRecorder { * @hidden */ constructor(private stream: Stream) { + platform = PlatformUtils.getInstance(); this.connectionId = (!!this.stream.connection) ? this.stream.connection.connectionId : 'default-connection'; this.id = this.stream.streamId + '_' + this.connectionId + '_localrecord'; this.state = LocalRecorderState.READY; @@ -170,6 +176,7 @@ export class LocalRecorder { } this.mediaRecorder.pause(); this.state = LocalRecorderState.PAUSED; + resolve(); } catch (error) { reject(error); } @@ -188,6 +195,7 @@ export class LocalRecorder { } this.mediaRecorder.resume(); this.state = LocalRecorderState.RECORDING; + resolve(); } catch (error) { reject(error); } @@ -209,7 +217,7 @@ export class LocalRecorder { this.videoPreview.id = this.id; this.videoPreview.autoplay = true; - if (platform.name === 'Safari') { + if (platform.isSafariBrowser()) { this.videoPreview.setAttribute('playsinline', 'true'); } @@ -274,7 +282,7 @@ export class LocalRecorder { if (this.state !== LocalRecorderState.FINISHED) { throw (Error('Call \'LocalRecord.stop()\' before getting Blob file')); } else { - return this.blob; + return this.blob!; } } @@ -344,7 +352,7 @@ export class LocalRecorder { } const sendable = new FormData(); - sendable.append('file', this.blob, this.id + '.webm'); + sendable.append('file', this.blob!, this.id + '.webm'); http.onreadystatechange = () => { if (http.readyState === 4) { diff --git a/openvidu-browser/src/OpenVidu/OpenVidu.ts b/openvidu-browser/src/OpenVidu/OpenVidu.ts index 8b00d977..c3c01b1a 100644 --- a/openvidu-browser/src/OpenVidu/OpenVidu.ts +++ b/openvidu-browser/src/OpenVidu/OpenVidu.ts @@ -19,6 +19,7 @@ import { LocalRecorder } from './LocalRecorder'; import { Publisher } from './Publisher'; import { Session } from './Session'; import { Stream } from './Stream'; +import { SessionDisconnectedEvent } from '../OpenViduInternal/Events/SessionDisconnectedEvent'; import { StreamPropertyChangedEvent } from '../OpenViduInternal/Events/StreamPropertyChangedEvent'; import { Device } from '../OpenViduInternal/Interfaces/Public/Device'; import { OpenViduAdvancedConfiguration } from '../OpenViduInternal/Interfaces/Public/OpenViduAdvancedConfiguration'; @@ -27,6 +28,7 @@ import { CustomMediaStreamConstraints } from '../OpenViduInternal/Interfaces/Pri import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/Enums/OpenViduError'; import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode'; import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger'; +import { PlatformUtils } from '../OpenViduInternal/Utils/Platform'; import * as screenSharingAuto from '../OpenViduInternal/ScreenSharing/Screen-Capturing-Auto'; import * as screenSharing from '../OpenViduInternal/ScreenSharing/Screen-Capturing'; @@ -38,13 +40,6 @@ import EventEmitter = require('wolfy87-eventemitter'); * @hidden */ import RpcBuilder = require('../OpenViduInternal/KurentoUtils/kurento-jsonrpc'); -/** - * @hidden - */ -import platform = require('platform'); - -platform['isIonicIos'] = (platform.product === 'iPhone' || platform.product === 'iPad') && platform.ua!!.indexOf('Safari') === -1; -platform['isIonicAndroid'] = platform.os!!.family === 'Android' && platform.name == "Android Browser"; /** * @hidden @@ -59,6 +54,11 @@ declare var cordova: any; */ const logger: OpenViduLogger = OpenViduLogger.getInstance(); +/** + * @hidden + */ +let platform: PlatformUtils; + /** * Entrypoint of OpenVidu Browser library. * Use it to initialize objects of type [[Session]], [[Publisher]] and [[LocalRecorder]] @@ -121,11 +121,12 @@ export class OpenVidu { ee = new EventEmitter() constructor() { + platform = PlatformUtils.getInstance(); this.libraryVersion = packageJson.version; logger.info("'OpenVidu' initialized"); logger.info("openvidu-browser version: " + this.libraryVersion); - if (platform.os!!.family === 'iOS' || platform.os!!.family === 'Android') { + if (platform.isMobileDevice()) { // Listen to orientationchange only on mobile devices (window).addEventListener('orientationchange', () => { this.publishers.forEach(publisher => { @@ -138,7 +139,7 @@ export class OpenVidu { const getNewVideoDimensions = (): Promise<{ newWidth: number, newHeight: number }> => { return new Promise((resolve, reject) => { - if (platform['isIonicIos']) { + if (platform.isIonicIos()) { // iOS Ionic. Limitation: must get new dimensions from an existing video element already inserted into DOM resolve({ newWidth: publisher.stream.streamManager.videos[0].video.videoWidth, @@ -149,8 +150,8 @@ export class OpenVidu { // New resolution got from different places for Chrome and Firefox. Chrome needs a videoWidth and videoHeight of a videoElement. // Firefox needs getSettings from the videoTrack const firefoxSettings = publisher.stream.getMediaStream().getVideoTracks()[0].getSettings(); - const newWidth = ((platform.name!!.toLowerCase().indexOf('firefox') !== -1) ? firefoxSettings.width : publisher.videoReference.videoWidth); - const newHeight = ((platform.name!!.toLowerCase().indexOf('firefox') !== -1) ? firefoxSettings.height : publisher.videoReference.videoHeight); + const newWidth = ((platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) ? firefoxSettings.width : publisher.videoReference.videoWidth); + const newHeight = ((platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) ? firefoxSettings.height : publisher.videoReference.videoHeight); resolve({ newWidth, newHeight }); } }); @@ -186,6 +187,7 @@ export class OpenVidu { } else { this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, publisher.stream, 'videoDimensions', publisher.stream.videoDimensions, { width: oldWidth, height: oldHeight }, 'deviceRotated')]); publisher.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(publisher, publisher.stream, 'videoDimensions', publisher.stream.videoDimensions, { width: oldWidth, height: oldHeight }, 'deviceRotated')]); + this.session.sendVideoData(publisher); } }); clearTimeout(repeatUntilChange); @@ -337,26 +339,20 @@ export class OpenVidu { * @returns 1 if the browser supports OpenVidu, 0 otherwise */ checkSystemRequirements(): number { - const browser = platform.name; - const family = platform.os!!.family; - const userAgent = !!platform.ua ? platform.ua : navigator.userAgent; - if(this.isIPhoneOrIPad(userAgent)) { - if(this.isIOSWithSafari(userAgent) || platform['isIonicIos']){ - return 1; - } + if (platform.isIPhoneOrIPad()) { + if (platform.isIOSWithSafari() || platform.isIonicIos()) { + return 1; + } return 0; } // Accept: Chrome (desktop and Android), Firefox (desktop and Android), Opera (desktop and Android), - // Safari (OSX and iOS), Ionic (Android and iOS), Samsung Internet Browser (Android) - if ( - (browser === 'Safari') || - (browser === 'Chrome') || (browser === 'Chrome Mobile') || - (browser === 'Firefox') || (browser === 'Firefox Mobile') || - (browser === 'Opera') || (browser === 'Opera Mobile') || - (browser === 'Android Browser') || (browser === 'Electron') || - (browser === 'Samsung Internet Mobile') || (browser === 'Samsung Internet') + // Safari (OSX and iOS), Edge Chromium (>= 80), Ionic (Android and iOS), Samsung Internet Browser (Android) + if (platform.isChromeBrowser() || platform.isChromeMobileBrowser() || + platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser() || platform.isOperaBrowser() || + platform.isOperaMobileBrowser() || platform.isEdgeBrowser() || platform.isEdgeMobileBrowser() || + platform.isSafariBrowser() || platform.isAndroidBrowser() || platform.isElectron() || platform.isSamsungBrowser() ) { return 1; } @@ -370,22 +366,8 @@ export class OpenVidu { * 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(): number { - const browser = platform.name; - const version = platform?.version ? parseFloat(platform.version) : -1; - const family = platform.os!!.family; - - // Reject mobile devices - if (family === 'iOS' || family === 'Android') { - return 0; - } - - if ((browser !== 'Chrome') && (browser !== 'Firefox') && (browser !== 'Opera') && (browser !== 'Electron') && - (browser === 'Safari' && version < 13)) { - return 0; - } else { - return 1; - } + checkScreenSharingCapabilities(): boolean { + return platform.canScreenShare(); } @@ -398,7 +380,7 @@ export class OpenVidu { const devices: Device[] = []; // Ionic Android devices - if (platform['isIonicAndroid'] && typeof cordova != "undefined" && cordova?.plugins?.EnumerateDevicesPlugin) { + if (platform.isIonicAndroid() && typeof cordova != "undefined" && cordova?.plugins?.EnumerateDevicesPlugin) { cordova.plugins.EnumerateDevicesPlugin.getEnumerateDevices().then((pluginDevices: Device[]) => { let pluginAudioDevices: Device[] = []; let videoDevices: Device[] = []; @@ -590,10 +572,10 @@ export class OpenVidu { // Video is deviceId or screen sharing if (options.videoSource === 'screen' || options.videoSource === 'window' || - (platform.name === 'Electron' && options.videoSource.startsWith('screen:'))) { + (platform.isElectron() && options.videoSource.startsWith('screen:'))) { // Video is screen sharing mustAskForAudioTrackLater = !myConstraints.audioTrack && (options.audioSource !== null && options.audioSource !== false); - if (navigator.mediaDevices['getDisplayMedia'] && platform.name !== 'Electron') { + if (navigator.mediaDevices['getDisplayMedia'] && !platform.isElectron()) { // getDisplayMedia supported navigator.mediaDevices['getDisplayMedia']({ video: true }) .then(mediaStream => { @@ -663,6 +645,7 @@ export class OpenVidu { * - `iceServers`: set custom STUN/TURN servers to be used by OpenVidu Browser * - `screenShareChromeExtension`: url to a custom screen share extension for Chrome to be used instead of the default one, based on ours [https://github.com/OpenVidu/openvidu-screen-sharing-chrome-extension](https://github.com/OpenVidu/openvidu-screen-sharing-chrome-extension) * - `publisherSpeakingEventsOptions`: custom configuration for the [[PublisherSpeakingEvent]] feature and the [StreamManagerEvent.streamAudioVolumeChange](/en/stable/api/openvidu-browser/classes/streammanagerevent.html) feature + * - `forceMediaReconnectionAfterNetworkDrop`: always force WebRTC renegotiation of all the streams of a client after a network loss and reconnection. This can help reducing frozen videos in low quality networks. * * Call this method to override previous values at any moment. */ @@ -763,9 +746,8 @@ export class OpenVidu { startWs(onConnectSucces: (error: Error) => void): void { const config = { heartbeat: 5000, - sendCloseMessage: false, ws: { - uri: this.wsUri, + uri: this.wsUri + '?sessionId=' + this.session.sessionId, onconnected: onConnectSucces, ondisconnect: this.disconnectCallback.bind(this), onreconnecting: this.reconnectingCallback.bind(this), @@ -782,6 +764,8 @@ export class OpenVidu { 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) @@ -899,12 +883,12 @@ export class OpenVidu { // 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) or Electron. Detected client: ' + platform.name); + 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); reject(error); } else { - if (platform.name === 'Electron') { + if (platform.isElectron()) { const prefix = "screen:"; const videoSourceString: string = videoSource; const electronScreenId = videoSourceString.substr(videoSourceString.indexOf(prefix) + prefix.length); @@ -918,7 +902,7 @@ export class OpenVidu { } else { - if (!!this.advancedConfiguration.screenShareChromeExtension && !(platform.name!.indexOf('Firefox') !== -1) && !navigator.mediaDevices['getDisplayMedia']) { + 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() @@ -957,7 +941,7 @@ export class OpenVidu { resolve(myConstraints); } else { // Default screen sharing extension for Chrome/Opera, or is Firefox < 66 - const firefoxString = platform.name!.indexOf('Firefox') !== -1 ? publisherProperties.videoSource : undefined; + const firefoxString = (platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) ? publisherProperties.videoSource : undefined; screenSharingAuto.getScreenId(firefoxString, (error, sourceId, screenConstraints) => { if (!!error) { @@ -1029,17 +1013,25 @@ export class OpenVidu { private reconnectedCallback(): void { logger.warn('Websocket reconnected'); if (this.isRoomAvailable()) { - this.sendRequest('connect', { sessionId: this.session.connection.rpcSessionId }, (error, response) => { - if (!!error) { - logger.error(error); - logger.warn('Websocket was able to reconnect to OpenVidu Server, but your Connection was already destroyed due to timeout. You are no longer a participant of the Session and your media streams have been destroyed'); - this.session.onLostConnection("networkDisconnect"); - this.jsonRpcClient.close(4101, "Reconnection fault"); - } else { - this.jsonRpcClient.resetPing(); - this.session.onRecoveredConnection(); - } - }); + if (!!this.session.connection) { + this.sendRequest('connect', { sessionId: this.session.connection.rpcSessionId }, (error, response) => { + if (!!error) { + logger.error(error); + logger.warn('Websocket was able to reconnect to OpenVidu Server, but your Connection was already destroyed due to timeout. You are no longer a participant of the Session and your media streams have been destroyed'); + this.session.onLostConnection("networkDisconnect"); + this.jsonRpcClient.close(4101, "Reconnection fault"); + } else { + this.jsonRpcClient.resetPing(); + this.session.onRecoveredConnection(); + } + }); + } 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.'); } @@ -1057,21 +1049,7 @@ export class OpenVidu { private isScreenShare(videoSource: string) { return videoSource === 'screen' || videoSource === 'window' || - (platform.name === 'Electron' && videoSource.startsWith('screen:')) + (platform.isElectron() && videoSource.startsWith('screen:')) } - private isIPhoneOrIPad(userAgent): boolean { - 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; - } - - private isIOSWithSafari(userAgent): boolean{ - return /\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); - } - - } \ No newline at end of file diff --git a/openvidu-browser/src/OpenVidu/Publisher.ts b/openvidu-browser/src/OpenVidu/Publisher.ts index 4c4cdc9e..c4e48615 100644 --- a/openvidu-browser/src/OpenVidu/Publisher.ts +++ b/openvidu-browser/src/OpenVidu/Publisher.ts @@ -28,15 +28,19 @@ import { StreamPropertyChangedEvent } from '../OpenViduInternal/Events/StreamPro import { VideoElementEvent } from '../OpenViduInternal/Events/VideoElementEvent'; import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/Enums/OpenViduError'; import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode'; - -import platform = require('platform'); import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger'; +import { PlatformUtils } from '../OpenViduInternal/Utils/Platform'; /** * @hidden */ const logger: OpenViduLogger = OpenViduLogger.getInstance(); +/** + * @hidden + */ +let platform: PlatformUtils; + /** * Packs local media streams. Participants can publish it to a session. Initialized with [[OpenVidu.initPublisher]] method * @@ -95,10 +99,32 @@ export class Publisher extends StreamManager { // TODO: CLEAN 2.15.0 LEGACY CODE // THIS LINE: - super(!openvidu.openviduServerVersion ? /*2.15.0 or 2.16.0 NOT CONNECTED (LEGACY)*/ new StreamLEGACY((!!openvidu.session) ? openvidu.session : new Session(openvidu), { publisherProperties: properties, mediaConstraints: {} }) : /*2.16.0 CONNECTED (NORMAL API)*/ new Stream(openvidu.session, { publisherProperties: properties, mediaConstraints: {} }), targEl); + super( + !openvidu.openviduServerVersion + // 2.15.0 or 2.16.0 NOT CONNECTED (LEGACY) + ? new StreamLEGACY( + !!openvidu.session + ? openvidu.session + : new Session(openvidu), + { publisherProperties: properties, mediaConstraints: {} } + ) + // 2.16.0 CONNECTED (NORMAL API) + : new Stream(openvidu.session, { + publisherProperties: properties, + mediaConstraints: {}, + }), + targEl + ); // SHOULD GET BACK TO: - // 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; @@ -130,7 +156,7 @@ export class Publisher extends StreamManager { */ publishAudio(value: boolean): void { if (this.stream.audioActive !== value) { - 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 = value; }); @@ -149,6 +175,7 @@ export class Publisher extends StreamManager { } else { this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, this.stream, 'audioActive', value, !value, 'publishAudio')]); this.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this, this.stream, 'audioActive', value, !value, 'publishAudio')]); + this.session.sendVideoData(this.stream.streamManager); } }); } @@ -177,7 +204,7 @@ export class Publisher extends StreamManager { */ publishVideo(value: boolean): void { if (this.stream.videoActive !== value) { - 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.getVideoTracks().forEach((track) => { track.enabled = value; }); @@ -196,6 +223,7 @@ export class Publisher extends StreamManager { } else { this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, this.stream, 'videoActive', value, !value, 'publishVideo')]); this.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this, this.stream, 'videoActive', value, !value, 'publishVideo')]); + this.session.sendVideoData(this.stream.streamManager); } }); } @@ -305,7 +333,7 @@ export class Publisher extends StreamManager { replaceTrack(track: MediaStreamTrack): Promise { const replaceMediaStreamTrack = () => { - 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]; @@ -315,6 +343,7 @@ export class Publisher extends StreamManager { mediaStream.removeTrack(removedTrack); removedTrack.stop(); mediaStream.addTrack(track); + this.session.sendVideoData(this.stream.streamManager, 5, true, 5); } return new Promise((resolve, reject) => { @@ -404,7 +433,7 @@ export class Publisher extends StreamManager { if (this.stream.isSendVideo()) { if (!this.stream.isSendScreen()) { - if (platform['isIonicIos'] || platform.name === 'Safari') { + if (platform.isIonicIos() || platform.isSafariBrowser()) { // iOS Ionic or Safari. Limitation: cannot set videoDimensions directly, as the videoReference is not loaded // if not added to DOM. Must add it to DOM and wait for videoWidth and videoHeight properties to be defined @@ -440,7 +469,7 @@ export class Publisher extends StreamManager { // Orientation must be checked for mobile devices (width and height are reversed) const { width, height } = this.getVideoDimensions(mediaStream); - if ((platform.os!!.family === 'iOS' || platform.os!!.family === 'Android') && (window.innerHeight > window.innerWidth)) { + if (platform.isMobileDevice() && (window.innerHeight > window.innerWidth)) { // Mobile portrait mode this.stream.videoDimensions = { width: height || 0, @@ -464,8 +493,8 @@ export class Publisher extends StreamManager { }; this.screenShareResizeInterval = setInterval(() => { const firefoxSettings = mediaStream.getVideoTracks()[0].getSettings(); - const newWidth = (platform.name === 'Chrome' || platform.name === 'Opera') ? this.videoReference.videoWidth : firefoxSettings.width; - const newHeight = (platform.name === 'Chrome' || platform.name === 'Opera') ? this.videoReference.videoHeight : firefoxSettings.height; + const newWidth = (platform.isChromeBrowser() || platform.isOperaBrowser()) ? this.videoReference.videoWidth : firefoxSettings.width; + const newHeight = (platform.isChromeBrowser() || platform.isOperaBrowser()) ? this.videoReference.videoHeight : firefoxSettings.height; if (this.stream.isLocalStreamPublished && (newWidth !== this.stream.videoDimensions.width || newHeight !== this.stream.videoDimensions.height)) { @@ -488,6 +517,7 @@ export class Publisher extends StreamManager { } else { this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, this.stream, 'videoDimensions', this.stream.videoDimensions, oldValue, 'screenResized')]); this.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this, this.stream, 'videoDimensions', this.stream.videoDimensions, oldValue, 'screenResized')]); + this.session.sendVideoData(this.stream.streamManager); } }); } @@ -634,7 +664,7 @@ export class Publisher extends StreamManager { startTime = Date.now(); this.setPermissionDialogTimer(timeForDialogEvent); - if (this.stream.isSendScreen() && navigator.mediaDevices['getDisplayMedia'] && platform.name !== 'Electron') { + if (this.stream.isSendScreen() && navigator.mediaDevices['getDisplayMedia'] && !platform.isElectron()) { navigator.mediaDevices['getDisplayMedia']({ video: true }) .then(mediaStream => { this.openvidu.addAlreadyProvidedTracks(myConstraints, mediaStream); @@ -683,7 +713,7 @@ export class Publisher extends StreamManager { initializeVideoReference(mediaStream: MediaStream) { this.videoReference = document.createElement('video'); - if (platform.name === 'Safari') { + if (platform.isSafariBrowser()) { this.videoReference.setAttribute('playsinline', 'true'); } diff --git a/openvidu-browser/src/OpenVidu/Session.ts b/openvidu-browser/src/OpenVidu/Session.ts index 52f4f245..e33d50fc 100644 --- a/openvidu-browser/src/OpenVidu/Session.ts +++ b/openvidu-browser/src/OpenVidu/Session.ts @@ -26,7 +26,8 @@ import { Capabilities } from '../OpenViduInternal/Interfaces/Public/Capabilities import { EventDispatcher } from './EventDispatcher'; import { SignalOptions } from '../OpenViduInternal/Interfaces/Public/SignalOptions'; import { SubscriberProperties } from '../OpenViduInternal/Interfaces/Public/SubscriberProperties'; -import { ConnectionOptions } from '../OpenViduInternal/Interfaces/Private/ConnectionOptions'; +import { RemoteConnectionOptions } from '../OpenViduInternal/Interfaces/Private/RemoteConnectionOptions'; +import { LocalConnectionOptions } from '../OpenViduInternal/Interfaces/Private/LocalConnectionOptions'; import { ObjMap } from '../OpenViduInternal/Interfaces/Private/ObjMap'; import { SessionOptions } from '../OpenViduInternal/Interfaces/Private/SessionOptions'; import { ConnectionEvent } from '../OpenViduInternal/Events/ConnectionEvent'; @@ -37,17 +38,23 @@ import { SessionDisconnectedEvent } from '../OpenViduInternal/Events/SessionDisc import { SignalEvent } from '../OpenViduInternal/Events/SignalEvent'; import { StreamEvent } from '../OpenViduInternal/Events/StreamEvent'; import { StreamPropertyChangedEvent } from '../OpenViduInternal/Events/StreamPropertyChangedEvent'; +import { ConnectionPropertyChangedEvent } from '../OpenViduInternal/Events/ConnectionPropertyChangedEvent'; +import { NetworkQualityLevelChangedEvent } from '../OpenViduInternal/Events/NetworkQualityLevelChangedEvent'; import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/Enums/OpenViduError'; import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode'; - -import platform = require('platform'); import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger'; +import { PlatformUtils } from '../OpenViduInternal/Utils/Platform'; /** * @hidden */ const logger: OpenViduLogger = OpenViduLogger.getInstance(); +/** + * @hidden + */ +let platform: PlatformUtils; + /** * Represents a video call. It can also be seen as a videoconference room where multiple users can connect. * Participants who publish their videos to a session can be seen by the rest of users connected to that specific session. @@ -57,6 +64,7 @@ const logger: OpenViduLogger = OpenViduLogger.getInstance(); * * - connectionCreated ([[ConnectionEvent]]) * - connectionDestroyed ([[ConnectionEvent]]) + * - connectionPropertyChanged ([[ConnectionPropertyChangedEvent]]) PRO * - sessionDisconnected ([[SessionDisconnectedEvent]]) * - streamCreated ([[StreamEvent]]) * - streamDestroyed ([[StreamEvent]]) @@ -66,9 +74,9 @@ const logger: OpenViduLogger = OpenViduLogger.getInstance(); * - signal ([[SignalEvent]]) * - recordingStarted ([[RecordingEvent]]) * - recordingStopped ([[RecordingEvent]]) + * - networkQualityLevelChanged ([[NetworkQualityLevelChangedEvent]]) * - reconnecting * - reconnected - * */ export class Session extends EventDispatcher { @@ -88,7 +96,7 @@ export class Session extends EventDispatcher { streamManagers: StreamManager[] = []; /** - * Object defining the methods that the client is able to call. These are defined by the role of the token used to connect to the Session. + * Object defining the methods that the client is able to call. These are defined by the [[Connection.role]]. * This object is only defined after [[Session.connect]] has been successfully resolved */ capabilities: Capabilities; @@ -127,15 +135,23 @@ export class Session extends EventDispatcher { * @hidden */ stopSpeakingEventsEnabledOnce = false; + /** + * @hidden + */ + private videoDataInterval: NodeJS.Timeout; + /** + * @hidden + */ + private videoDataTimeout: NodeJS.Timeout; // TODO: CLEAN 2.15.0 LEGACY CODE /** - * @hidden + * @hidden */ isFirstIonicIosSubscriber = true; /** - * @hidden + * @hidden */ countDownForIonicIosSubscribersActive = true; // END LEGACY CODE @@ -146,6 +162,7 @@ export class Session extends EventDispatcher { */ constructor(openvidu: OpenVidu) { super(); + platform = PlatformUtils.getInstance(); this.openvidu = openvidu; } @@ -197,7 +214,7 @@ export class Session extends EventDispatcher { reject(error); }); } else { - reject(new OpenViduError(OpenViduErrorName.BROWSER_NOT_SUPPORTED, 'Browser ' + platform.name + ' (version ' + platform.version + ') for ' + platform.os!!.family + ' is not supported in OpenVidu')); + reject(new OpenViduError(OpenViduErrorName.BROWSER_NOT_SUPPORTED, 'Browser ' + platform.getName() + ' (version ' + platform.getVersion() + ') for ' + platform.getFamily() + ' is not supported in OpenVidu')); } }); } @@ -382,6 +399,7 @@ export class Session extends EventDispatcher { this.connection.addStream(publisher.stream); publisher.stream.publish() .then(() => { + this.sendVideoData(publisher, 8, true, 5); resolve(); }) .catch(error => { @@ -395,6 +413,7 @@ export class Session extends EventDispatcher { publisher.reestablishStreamPlayingEvent(); publisher.stream.publish() .then(() => { + this.sendVideoData(publisher, 8, true, 5); resolve(); }) .catch(error => { @@ -596,7 +615,7 @@ export class Session extends EventDispatcher { /** * See [[EventDispatcher.on]] */ - on(type: string, handler: (event: SessionDisconnectedEvent | SignalEvent | StreamEvent | ConnectionEvent | PublisherSpeakingEvent | RecordingEvent) => void): EventDispatcher { + on(type: string, handler: (event: SessionDisconnectedEvent | SignalEvent | StreamEvent | ConnectionEvent | PublisherSpeakingEvent | RecordingEvent | NetworkQualityLevelChangedEvent) => void): EventDispatcher { super.onAux(type, "Event '" + type + "' triggered by 'Session'", handler); @@ -628,7 +647,7 @@ export class Session extends EventDispatcher { /** * See [[EventDispatcher.once]] */ - once(type: string, handler: (event: SessionDisconnectedEvent | SignalEvent | StreamEvent | ConnectionEvent | PublisherSpeakingEvent | RecordingEvent) => void): Session { + once(type: string, handler: (event: SessionDisconnectedEvent | SignalEvent | StreamEvent | ConnectionEvent | PublisherSpeakingEvent | RecordingEvent | NetworkQualityLevelChangedEvent) => void): Session { super.onceAux(type, "Event '" + type + "' triggered once by 'Session'", handler); @@ -660,7 +679,7 @@ export class Session extends EventDispatcher { /** * See [[EventDispatcher.off]] */ - off(type: string, handler?: (event: SessionDisconnectedEvent | SignalEvent | StreamEvent | ConnectionEvent | PublisherSpeakingEvent | RecordingEvent) => void): Session { + off(type: string, handler?: (event: SessionDisconnectedEvent | SignalEvent | StreamEvent | ConnectionEvent | PublisherSpeakingEvent | RecordingEvent | NetworkQualityLevelChangedEvent) => void): Session { super.off(type, handler); @@ -699,7 +718,7 @@ export class Session extends EventDispatcher { /** * @hidden */ - onParticipantJoined(response: ConnectionOptions): void { + onParticipantJoined(response: RemoteConnectionOptions): void { // Connection shouldn't exist this.getConnection(response.id, '') @@ -751,9 +770,10 @@ export class Session extends EventDispatcher { /** * @hidden */ - onParticipantPublished(response: ConnectionOptions): void { + onParticipantPublished(response: RemoteConnectionOptions): void { const afterConnectionFound = (connection) => { + this.remoteConnections[connection.connectionId] = connection; if (!this.remoteStreamsCreated[connection.stream.streamId]) { @@ -777,7 +797,7 @@ export class Session extends EventDispatcher { // Update existing Connection connection = con; response.metadata = con.data; - connection.options = response; + connection.remoteOptions = response; connection.initRemoteStreams(response.streams); afterConnectionFound(connection); }) @@ -801,12 +821,12 @@ export class Session extends EventDispatcher { .then(connection => { - const streamEvent = new StreamEvent(true, this, 'streamDestroyed', connection.stream, msg.reason); + const streamEvent = new StreamEvent(true, this, 'streamDestroyed', connection.stream!, msg.reason); this.ee.emitEvent('streamDestroyed', [streamEvent]); streamEvent.callDefaultBehavior(); // Deleting the remote stream - const streamId: string = connection.stream.streamId; + const streamId: string = connection.stream!.streamId; delete this.remoteStreamsCreated[streamId]; @@ -934,6 +954,44 @@ export class Session extends EventDispatcher { } } + /** + * @hidden + */ + onConnectionPropertyChanged(msg): void { + let oldValue; + switch (msg.property) { + case 'role': + oldValue = this.connection.role.slice(); + this.connection.role = msg.newValue; + this.connection.localOptions!.role = msg.newValue; + break; + case 'record': + oldValue = this.connection.record; + msg.newValue = msg.newValue === 'true'; + this.connection.record = msg.newValue; + this.connection.localOptions!.record = msg.newValue; + break; + } + this.ee.emitEvent('connectionPropertyChanged', [new ConnectionPropertyChangedEvent(this, this.connection, msg.property, msg.newValue, oldValue)]); + } + + /** + * @hidden + */ + onNetworkQualityLevelChangedChanged(msg): void { + if (msg.connectionId === this.connection.connectionId) { + this.ee.emitEvent('networkQualityLevelChanged', [new NetworkQualityLevelChangedEvent(this, msg.newValue, msg.oldValue, this.connection)]); + } else { + this.getConnection(msg.connectionId, 'Connection not found for connectionId ' + msg.connectionId) + .then((connection: Connection) => { + this.ee.emitEvent('networkQualityLevelChanged', [new NetworkQualityLevelChangedEvent(this, msg.newValue, msg.oldValue, connection)]); + }) + .catch(openViduError => { + logger.error(openViduError); + }); + } + } + /** * @hidden */ @@ -958,9 +1016,9 @@ export class Session extends EventDispatcher { }; this.getConnection(msg.senderConnectionId, 'Connection not found for connectionId ' + msg.senderConnectionId + ' owning endpoint ' + msg.endpointName + '. Ice candidate will be ignored: ' + candidate) .then(connection => { - const stream = connection.stream; + const stream: Stream = connection.stream!; stream.getWebRtcPeer().addIceCandidate(candidate).catch(error => { - logger.error('Error adding candidate for ' + stream.streamId + logger.error('Error adding candidate for ' + stream!.streamId + ' stream of endpoint ' + msg.endpointName + ': ' + error); }); }) @@ -989,7 +1047,7 @@ export class Session extends EventDispatcher { */ onLostConnection(reason: string): void { logger.warn('Lost connection in Session ' + this.sessionId); - if (!!this.sessionId && !this.connection.disposed) { + if (!!this.sessionId && !!this.connection && !this.connection.disposed) { this.leave(true, reason); } } @@ -1042,8 +1100,8 @@ export class Session extends EventDispatcher { this.getConnection(connectionId, 'No connection found for connectionId ' + connectionId) .then(connection => { logger.info('Filter event dispatched'); - const stream: Stream = connection.stream; - stream.filter.handlers[response.eventType](new FilterEvent(stream.filter, response.eventType, response.data)); + const stream: Stream = connection.stream!; + stream.filter!.handlers[response.eventType](new FilterEvent(stream.filter!, response.eventType, response.data)); }); } @@ -1086,6 +1144,7 @@ export class Session extends EventDispatcher { forced = !!forced; logger.info('Leaving Session (forced=' + forced + ')'); + this.stopVideoDataIntervals(); if (!!this.connection) { if (!this.connection.disposed && !forced) { @@ -1119,7 +1178,7 @@ export class Session extends EventDispatcher { const joinParams = { token: (!!token) ? token : '', session: this.sessionId, - platform: !!platform.description ? platform.description : 'unknown', + platform: !!platform.getDescription() ? platform.getDescription() : 'unknown', metadata: !!this.options.metadata ? this.options.metadata : '', secret: this.openvidu.getSecret(), recorder: this.openvidu.getRecorder() @@ -1127,6 +1186,63 @@ export class Session extends EventDispatcher { return joinParams; } + sendVideoData(streamManager: StreamManager, intervalSeconds: number = 1, doInterval: boolean = false, maxLoops: number = 1) { + if ( + platform.isChromeBrowser() || platform.isChromeMobileBrowser() || platform.isOperaBrowser() || + platform.isOperaMobileBrowser() || platform.isEdgeBrowser() || platform.isElectron() || + (platform.isSafariBrowser() && !platform.isIonicIos()) || platform.isAndroidBrowser() || + platform.isSamsungBrowser() || platform.isIonicAndroid() || (platform.isIPhoneOrIPad() && platform.isIOSWithSafari()) + ) { + const obtainAndSendVideo = async () => { + const statsMap = await streamManager.stream.getRTCPeerConnection().getStats(); + const arr: any[] = []; + 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); + } + }); + } + } + if (doInterval) { + let loops = 1; + this.videoDataInterval = setInterval(() => { + if (loops < maxLoops) { + loops++; + obtainAndSendVideo(); + }else { + clearInterval(this.videoDataInterval); + } + }, intervalSeconds * 1000); + } else { + this.videoDataTimeout = setTimeout(obtainAndSendVideo, intervalSeconds * 1000); + } + } 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); + } + }); + } else { + logger.error('Browser ' + platform.getName() + ' (version ' + platform.getVersion() + ') for ' + platform.getFamily() + ' is not supported in OpenVidu for Network Quality'); + } + } /* Private methods */ @@ -1139,35 +1255,33 @@ export class Session extends EventDispatcher { const joinParams = this.initializeParams(token); - this.openvidu.sendRequest('joinRoom', joinParams, (error, response) => { + this.openvidu.sendRequest('joinRoom', joinParams, (error, response: LocalConnectionOptions) => { if (!!error) { reject(error); } else { - // Initialize capabilities object with the role - this.capabilities = { - subscribe: true, - publish: this.openvidu.role !== 'SUBSCRIBER', - forceUnpublish: this.openvidu.role === 'MODERATOR', - forceDisconnect: this.openvidu.role === 'MODERATOR' - }; + this.processJoinRoomResponse(response); // Initialize local Connection object with values returned by openvidu-server - this.connection = new Connection(this); + +// FIXME CONFLICT WITH MASTER +// In modern code, the OpenVidu version is obtained through other means. +// However in the mediasoup branch this value is used in several of the +// *LEGACY.ts files. +<<<<<<< HEAD this.openvidu.openviduServerVersion = response.openviduServerVersion; - this.connection.connectionId = response.id; - this.connection.creationTime = response.createdAt; - this.connection.data = response.metadata; - this.connection.rpcSessionId = response.sessionId; +======= + this.connection = new Connection(this, response); +>>>>>>> master // Initialize remote Connections with value returned by openvidu-server const events = { connections: new Array(), streams: new Array() }; - const existingParticipants: ConnectionOptions[] = response.value; - existingParticipants.forEach(participant => { - const connection = new Connection(this, participant); + const existingParticipants: RemoteConnectionOptions[] = response.value; + existingParticipants.forEach((remoteConnectionOptions: RemoteConnectionOptions) => { + const connection = new Connection(this, remoteConnectionOptions); this.remoteConnections[connection.connectionId] = connection; events.connections.push(connection); if (!!connection.stream) { @@ -1208,6 +1322,11 @@ export class Session extends EventDispatcher { } } + private stopVideoDataIntervals(): void { + clearInterval(this.videoDataInterval); + clearTimeout(this.videoDataTimeout); + } + private stringClientMetadata(metadata: any): string { if (typeof metadata !== 'string') { return JSON.stringify(metadata); @@ -1272,12 +1391,7 @@ export class Session extends EventDispatcher { this.sessionId = queryParams['sessionId']; const secret = queryParams['secret']; const recorder = queryParams['recorder']; - const coturnIp = queryParams['coturnIp']; - const turnUsername = queryParams['turnUsername']; - const turnCredential = queryParams['turnCredential']; - const role = queryParams['role']; const webrtcStatsInterval = queryParams['webrtcStatsInterval']; - const openviduServerVersion = queryParams['version']; if (!!secret) { this.openvidu.secret = secret; @@ -1285,31 +1399,9 @@ export class Session extends EventDispatcher { if (!!recorder) { this.openvidu.recorder = true; } - if (!!turnUsername && !!turnCredential) { - const stunUrl = 'stun:' + coturnIp + ':3478'; - const turnUrl1 = 'turn:' + coturnIp + ':3478'; - const turnUrl2 = turnUrl1 + '?transport=tcp'; - this.openvidu.iceServers = [ - { urls: [stunUrl] }, - { urls: [turnUrl1, turnUrl2], username: turnUsername, credential: turnCredential } - ]; - logger.log("STUN/TURN server IP: " + coturnIp); - logger.log('TURN temp credentials [' + turnUsername + ':' + turnCredential + ']'); - } - if (!!role) { - this.openvidu.role = role; - } if (!!webrtcStatsInterval) { this.openvidu.webrtcStatsInterval = +webrtcStatsInterval; } - if (!!openviduServerVersion) { - logger.info("openvidu-server version: " + openviduServerVersion); - if (openviduServerVersion !== this.openvidu.libraryVersion) { - logger.warn('OpenVidu Server (' + openviduServerVersion + - ') and OpenVidu Browser (' + this.openvidu.libraryVersion + - ') versions do NOT match. There may be incompatibilities') - } - } this.openvidu.wsUri = 'wss://' + url.host + '/openvidu'; this.openvidu.httpUri = 'https://' + url.host; @@ -1319,4 +1411,32 @@ export class Session extends EventDispatcher { } } + private processJoinRoomResponse(opts: LocalConnectionOptions) { + this.sessionId = opts.session; + if (opts.coturnIp != null && opts.turnUsername != null && opts.turnCredential != null) { + const stunUrl = 'stun:' + opts.coturnIp + ':3478'; + const turnUrl1 = 'turn:' + opts.coturnIp + ':3478'; + const turnUrl2 = turnUrl1 + '?transport=tcp'; + this.openvidu.iceServers = [ + { urls: [stunUrl] }, + { urls: [turnUrl1, turnUrl2], username: opts.turnUsername, credential: opts.turnCredential } + ]; + logger.log("STUN/TURN server IP: " + opts.coturnIp); + logger.log('TURN temp credentials [' + opts.turnUsername + ':' + opts.turnCredential + ']'); + } + this.openvidu.role = opts.role; + this.capabilities = { + subscribe: true, + publish: this.openvidu.role !== 'SUBSCRIBER', + forceUnpublish: this.openvidu.role === 'MODERATOR', + forceDisconnect: this.openvidu.role === 'MODERATOR' + }; + logger.info("openvidu-server version: " + opts.version); + if (opts.version !== this.openvidu.libraryVersion) { + logger.warn('OpenVidu Server (' + opts.version + + ') and OpenVidu Browser (' + this.openvidu.libraryVersion + + ') versions do NOT match. There may be incompatibilities') + } + } + } diff --git a/openvidu-browser/src/OpenVidu/Stream.ts b/openvidu-browser/src/OpenVidu/Stream.ts index bb06f680..5198dc14 100644 --- a/openvidu-browser/src/OpenVidu/Stream.ts +++ b/openvidu-browser/src/OpenVidu/Stream.ts @@ -30,19 +30,22 @@ import { PublisherSpeakingEvent } from '../OpenViduInternal/Events/PublisherSpea import { StreamManagerEvent } from '../OpenViduInternal/Events/StreamManagerEvent'; import { StreamPropertyChangedEvent } from '../OpenViduInternal/Events/StreamPropertyChangedEvent'; import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/Enums/OpenViduError'; +import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger'; +import { PlatformUtils } from '../OpenViduInternal/Utils/Platform'; /** * @hidden */ import hark = require('hark'); -import platform = require('platform'); -import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger'; /** * @hidden */ const logger: OpenViduLogger = OpenViduLogger.getInstance(); - +/** + * @hidden + */ +let platform: PlatformUtils; /** * Represents each one of the media streams available in OpenVidu Server for certain session. @@ -108,7 +111,7 @@ export class Stream extends EventDispatcher { * - `"SCREEN"`: when the video source comes from screen-sharing. * - `"CUSTOM"`: when [[PublisherProperties.videoSource]] has been initialized in the Publisher side with a custom MediaStreamTrack when calling [[OpenVidu.initPublisher]]). * - `"IPCAM"`: when the video source comes from an IP camera participant instead of a regular participant (see [IP cameras](/en/stable/advanced-features/ip-cameras/)). - * + * * If [[hasVideo]] is false, this property is undefined */ typeOfVideo?: string; @@ -136,10 +139,10 @@ export class Stream extends EventDispatcher { * [[Filter.execMethod]] and remove it with [[Stream.removeFilter]]. Be aware that the client calling this methods must have the * necessary permissions: the token owned by the client must have been initialized with the appropriated `allowedFilters` array. */ - filter: Filter; + filter?: Filter; protected webRtcPeer: WebRtcPeer; - protected mediaStream: MediaStream; + protected mediaStream?: MediaStream; private webRtcStats: WebRtcStats; private isSubscribeToRemote = false; @@ -203,7 +206,7 @@ export class Stream extends EventDispatcher { /** * @hidden */ - localMediaStreamWhenSubscribedToRemote: MediaStream; + localMediaStreamWhenSubscribedToRemote?: MediaStream; /** @@ -212,7 +215,7 @@ export class Stream extends EventDispatcher { constructor(session: Session, options: InboundStreamOptions | OutboundStreamOptions | {}) { super(); - + platform = PlatformUtils.getInstance(); this.session = session; if (options.hasOwnProperty('id')) { @@ -262,7 +265,7 @@ export class Stream extends EventDispatcher { } this.ee.on('mediastream-updated', () => { - this.streamManager.updateMediaStream(this.mediaStream); + this.streamManager.updateMediaStream(this.mediaStream!); logger.debug('Video srcObject [' + this.mediaStream + '] updated in stream [' + this.streamId + ']'); }); } @@ -323,7 +326,7 @@ export class Stream extends EventDispatcher { } } else { logger.info('Filter successfully applied on Stream ' + this.streamId); - const oldValue: Filter = this.filter; + const oldValue: Filter = this.filter!; this.filter = new Filter(type, options); this.filter.stream = this; this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, this, 'filter', this.filter, oldValue, 'applyFilter')]); @@ -356,10 +359,10 @@ export class Stream extends EventDispatcher { } } else { logger.info('Filter successfully removed from Stream ' + this.streamId); - const oldValue = this.filter; + const oldValue = this.filter!; delete this.filter; - 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')]); resolve(); } } @@ -382,7 +385,7 @@ export class Stream extends EventDispatcher { * @returns Native MediaStream Web API object */ getMediaStream(): MediaStream { - return this.mediaStream; + return this.mediaStream!; } /* Hidden methods */ @@ -537,7 +540,7 @@ export class Stream extends EventDispatcher { */ isSendScreen(): boolean { let screen = this.outboundStreamOpts.publisherProperties.videoSource === 'screen'; - if (platform.name === 'Electron') { + if (platform.isElectron()) { screen = typeof this.outboundStreamOpts.publisherProperties.videoSource === 'string' && this.outboundStreamOpts.publisherProperties.videoSource.startsWith('screen:'); } @@ -776,7 +779,7 @@ export class Stream extends EventDispatcher { if (!this.getWebRtcPeer() || !this.getRTCPeerConnection()) { return false; } - if (this.isLocal && !!this.session.openvidu.advancedConfiguration.forceMediaReconnectionAfterNetworkDrop) { + if (this.isLocal() && !!this.session.openvidu.advancedConfiguration.forceMediaReconnectionAfterNetworkDrop) { logger.warn('OpenVidu Browser advanced configuration option "forceMediaReconnectionAfterNetworkDrop" is enabled. Publisher stream ' + this.streamId + 'will force a reconnection'); return true; } @@ -1022,8 +1025,8 @@ export class Stream extends EventDispatcher { } } - protected initHarkEvents(): void { - if (!!this.mediaStream.getAudioTracks()[0]) { + private initHarkEvents(): void { + if (!!this.mediaStream!.getAudioTracks()[0]) { // Hark events can only be set if audio track is available if (this.streamManager.remote) { // publisherStartSpeaking/publisherStopSpeaking is only defined for remote streams @@ -1226,4 +1229,4 @@ export class Stream extends EventDispatcher { (report.type === 'candidate-pair' && report.nominated && report.bytesSent > 0); } -} \ No newline at end of file +} diff --git a/openvidu-browser/src/OpenVidu/StreamManager.ts b/openvidu-browser/src/OpenVidu/StreamManager.ts index 2025d69b..a2d3e03f 100644 --- a/openvidu-browser/src/OpenVidu/StreamManager.ts +++ b/openvidu-browser/src/OpenVidu/StreamManager.ts @@ -22,14 +22,19 @@ import { Event } from '../OpenViduInternal/Events/Event'; import { StreamManagerEvent } from '../OpenViduInternal/Events/StreamManagerEvent'; import { VideoElementEvent } from '../OpenViduInternal/Events/VideoElementEvent'; import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode'; - -import platform = require('platform'); import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger'; +import { PlatformUtils } from '../OpenViduInternal/Utils/Platform'; + /** * @hidden */ const logger: OpenViduLogger = OpenViduLogger.getInstance(); +/** + * @hidden + */ +let platform: PlatformUtils; + /** * Interface in charge of displaying the media streams in the HTML DOM. This wraps any [[Publisher]] and [[Subscriber]] object. * You can insert as many video players fo the same Stream as you want by calling [[StreamManager.addVideoElement]] or @@ -82,7 +87,7 @@ export class StreamManager extends EventDispatcher { /** * @hidden */ - firstVideoElement: StreamManagerVideo; + firstVideoElement?: StreamManagerVideo; /** * @hidden */ @@ -101,7 +106,7 @@ export class StreamManager extends EventDispatcher { */ constructor(stream: Stream, targetElement?: HTMLElement | string) { super(); - + platform = PlatformUtils.getInstance(); this.stream = stream; this.stream.streamManager = this; this.remote = !this.stream.isLocal(); @@ -121,7 +126,7 @@ export class StreamManager extends EventDispatcher { id: '', canplayListenerAdded: false }; - if (platform.name === 'Safari') { + if (platform.isSafariBrowser()) { this.firstVideoElement.video.setAttribute('playsinline', 'true'); } this.targetElement = targEl; @@ -379,7 +384,7 @@ export class StreamManager extends EventDispatcher { video.autoplay = true; video.controls = false; - if (platform.name === 'Safari') { + if (platform.isSafariBrowser()) { video.setAttribute('playsinline', 'true'); } @@ -463,7 +468,7 @@ export class StreamManager extends EventDispatcher { updateMediaStream(mediaStream: MediaStream) { this.videos.forEach(streamManagerVideo => { streamManagerVideo.video.srcObject = mediaStream; - if (platform['isIonicIos']) { + if (platform.isIonicIos()) { // iOS Ionic. LIMITATION: must reinsert the video in the DOM for // the media stream to be updated const vParent = streamManagerVideo.video.parentElement; @@ -506,7 +511,7 @@ export class StreamManager extends EventDispatcher { } private mirrorVideo(video): void { - if (!platform['isIonicIos']) { + if (!platform.isIonicIos()) { video.style.transform = 'rotateY(180deg)'; video.style.webkitTransform = 'rotateY(180deg)'; } diff --git a/openvidu-browser/src/OpenViduInternal/Events/ConnectionPropertyChangedEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/ConnectionPropertyChangedEvent.ts new file mode 100644 index 00000000..2747925e --- /dev/null +++ b/openvidu-browser/src/OpenViduInternal/Events/ConnectionPropertyChangedEvent.ts @@ -0,0 +1,74 @@ +/* + * (C) Copyright 2017-2020 OpenVidu (https://openvidu.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Connection } from '../../OpenVidu/Connection'; +import { Session } from '../../OpenVidu/Session'; +import { Event } from './Event'; + +/** + * **This feature is part of OpenVidu Pro tier** PRO + * + * Defines event `connectionPropertyChanged` dispatched by [[Session]] object. + * This event is fired when any property of the local [[Connection]] object changes. + * The properties that may change are [[Connection.role]] and [[Connection.record]]. + * + * The only way the Connection properties may change is by updating them through: + * + * - [API REST](/en/stable/reference-docs/REST-API/#patch-openviduapisessionsltsession_idgtconnectionltconnection_idgt) + * - [openvidu-java-client](/en/stable/reference-docs/openvidu-java-client/#update-a-connection) + * - [openvidu-node-client](/en/stable/reference-docs/openvidu-node-client/#update-a-connection)

+ */ +export class ConnectionPropertyChangedEvent extends Event { + + /** + * The Connection whose property has changed + */ + connection: Connection; + + /** + * The property of the stream that changed. This value is either `"role"` or `"record"` + */ + changedProperty: string; + + /** + * New value of the property (after change, current value) + */ + newValue: Object; + + /** + * Previous value of the property (before change) + */ + oldValue: Object; + + /** + * @hidden + */ + constructor(target: Session, connection: Connection, changedProperty: string, newValue: Object, oldValue: Object) { + super(false, target, 'connectionPropertyChanged'); + this.connection = connection; + this.changedProperty = changedProperty; + this.newValue = newValue; + this.oldValue = oldValue; + } + + /** + * @hidden + */ + // tslint:disable-next-line:no-empty + callDefaultBehavior() { } + +} \ No newline at end of file diff --git a/openvidu-browser/src/OpenViduInternal/Events/FilterEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/FilterEvent.ts index bdb0fa58..ab327f17 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/FilterEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/FilterEvent.ts @@ -16,7 +16,6 @@ */ import { Event } from './Event'; -import { Stream } from '../../OpenVidu/Stream'; import { Filter } from '../../OpenVidu/Filter'; diff --git a/openvidu-browser/src/OpenViduInternal/Events/NetworkQualityLevelChangedEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/NetworkQualityLevelChangedEvent.ts new file mode 100644 index 00000000..592e62a7 --- /dev/null +++ b/openvidu-browser/src/OpenViduInternal/Events/NetworkQualityLevelChangedEvent.ts @@ -0,0 +1,61 @@ +/* + * (C) Copyright 2017-2020 OpenVidu (https://openvidu.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Event } from './Event'; +import { Session } from '../../OpenVidu/Session'; +import { Connection } from '../../OpenVidu/Connection'; + +/** + * **This feature is part of OpenVidu Pro tier** PRO + * + * Defines event `networkQualityLevelChanged` dispatched by [[Session]]. + * This event is fired when the network quality level of a [[Connection]] changes. See [network quality](/en/stable/advanced-features/network-quality/) + */ +export class NetworkQualityLevelChangedEvent extends Event { + + /** + * New value of the network quality level + */ + newValue: number; + + /** + * Old value of the network quality level + */ + oldValue: number; + + /** + * Connection for whom the network quality level changed + */ + connection: Connection + + /** + * @hidden + */ + constructor(target: Session, newValue: number, oldValue: number, connection: Connection) { + super(false, target, 'networkQualityLevelChanged'); + this.newValue = newValue; + this.oldValue = oldValue; + this.connection = connection; + } + + /** + * @hidden + */ + // tslint:disable-next-line:no-empty + callDefaultBehavior() { } + +} diff --git a/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts index 050c694e..c7eff22d 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts @@ -62,12 +62,12 @@ export class SessionDisconnectedEvent extends Event { // Dispose and delete all remote Connections for (const connectionId in session.remoteConnections) { if (!!session.remoteConnections[connectionId].stream) { - session.remoteConnections[connectionId].stream.disposeWebRtcPeer(); - session.remoteConnections[connectionId].stream.disposeMediaStream(); - if (session.remoteConnections[connectionId].stream.streamManager) { - session.remoteConnections[connectionId].stream.streamManager.removeAllVideos(); + session.remoteConnections[connectionId].stream!.disposeWebRtcPeer(); + session.remoteConnections[connectionId].stream!.disposeMediaStream(); + if (session.remoteConnections[connectionId].stream!.streamManager) { + session.remoteConnections[connectionId].stream!.streamManager.removeAllVideos(); } - delete session.remoteStreamsCreated[session.remoteConnections[connectionId].stream.streamId]; + delete session.remoteStreamsCreated[session.remoteConnections[connectionId].stream!.streamId]; session.remoteConnections[connectionId].dispose(); } delete session.remoteConnections[connectionId]; diff --git a/openvidu-browser/src/OpenViduInternal/Events/StreamEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/StreamEvent.ts index 88d57dff..93ff0809 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/StreamEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/StreamEvent.ts @@ -101,8 +101,8 @@ export class StreamEvent extends Event { // Delete StreamOptionsServer from remote Connection const remoteConnection = this.stream.session.remoteConnections[this.stream.connection.connectionId]; - if (!!remoteConnection && !!remoteConnection.options) { - const streamOptionsServer = remoteConnection.options.streams; + if (!!remoteConnection && !!remoteConnection.remoteOptions) { + const streamOptionsServer = remoteConnection.remoteOptions.streams; for (let i = streamOptionsServer.length - 1; i >= 0; --i) { if (streamOptionsServer[i].id === this.stream.streamId) { streamOptionsServer.splice(i, 1); diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/LocalConnectionOptions.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/LocalConnectionOptions.ts new file mode 100644 index 00000000..b53c4940 --- /dev/null +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/LocalConnectionOptions.ts @@ -0,0 +1,33 @@ +/* + * (C) Copyright 2017-2020 OpenVidu (https://openvidu.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { RemoteConnectionOptions } from './RemoteConnectionOptions'; + +export interface LocalConnectionOptions { + id: string; + createdAt: number; + metadata: string; + value: RemoteConnectionOptions[]; + session: string; // OpenVidu Session identifier + sessionId: string; // JSON-RPC session identifier + role: string; + record: boolean; + coturnIp: string; + turnUsername: string; + turnCredential: string; + version: string; +} \ No newline at end of file diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/ConnectionOptions.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/RemoteConnectionOptions.ts similarity index 94% rename from openvidu-browser/src/OpenViduInternal/Interfaces/Private/ConnectionOptions.ts rename to openvidu-browser/src/OpenViduInternal/Interfaces/Private/RemoteConnectionOptions.ts index 12b38df7..250d406d 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Private/ConnectionOptions.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Private/RemoteConnectionOptions.ts @@ -17,7 +17,7 @@ import { StreamOptionsServer } from './StreamOptionsServer'; -export interface ConnectionOptions { +export interface RemoteConnectionOptions { id: string; createdAt: number; metadata: string; 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 c4a5b7a8..e443a56c 100644 --- a/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/jsonrpcclient.js +++ b/openvidu-browser/src/OpenViduInternal/KurentoUtils/kurento-jsonrpc/clients/jsonrpcclient.js @@ -17,6 +17,7 @@ var RpcBuilder = require('../'); var WebSocketWithReconnection = require('./transports/webSocketWithReconnection'); +var OpenViduLogger = require('../../../Logger/OpenViduLogger').OpenViduLogger; Date.now = Date.now || function () { return +new Date; @@ -28,12 +29,11 @@ var RECONNECTING = 'RECONNECTING'; var CONNECTED = 'CONNECTED'; var DISCONNECTED = 'DISCONNECTED'; -var Logger = console; +var Logger = OpenViduLogger.getInstance(); /** * * heartbeat: interval in ms for each heartbeat message, - * sendCloseMessage : true / false, before closing the connection, it sends a closeSession message *
  * ws : {
  * 	uri : URI to conntect to,
@@ -250,25 +250,13 @@ function JsonRpcClient(configuration) {
 
     this.close = function (code, reason) {
         Logger.debug("Closing  with code: " + code + " because: " + reason);
-
         if (pingInterval != undefined) {
             Logger.debug("Clearing ping interval");
             clearInterval(pingInterval);
         }
         pingPongStarted = false;
         enabledPings = false;
-
-        if (configuration.sendCloseMessage) {
-            Logger.debug("Sending close message")
-            this.send('closeSession', null, function (error, result) {
-                if (error) {
-                    Logger.error("Error sending close message: " + JSON.stringify(error));
-                }
-                ws.close(code, reason);
-            });
-        } else {
-            ws.close(code, reason);
-        }
+        ws.close(code, reason);
     }
 
     // This method is only for testing
diff --git a/openvidu-browser/src/OpenViduInternal/ScreenSharing/Screen-Capturing-Auto.js b/openvidu-browser/src/OpenViduInternal/ScreenSharing/Screen-Capturing-Auto.js
index 5947e717..4042f6a3 100644
--- a/openvidu-browser/src/OpenViduInternal/ScreenSharing/Screen-Capturing-Auto.js
+++ b/openvidu-browser/src/OpenViduInternal/ScreenSharing/Screen-Capturing-Auto.js
@@ -13,7 +13,7 @@
 getScreenId(function (error, sourceId, screen_constraints) {
     // error    == null || 'permission-denied' || 'not-installed' || 'installed-disabled' || 'not-chrome'
     // sourceId == null || 'string' || 'firefox'
-    
+
     if(microsoftEdge) {
         navigator.getDisplayMedia(screen_constraints).then(onSuccess, onFailure);
     }
@@ -217,4 +217,4 @@ function postGetChromeExtensionStatusMessage() {
     }, '*');
 }
 
-exports.getScreenId = getScreenId;
\ No newline at end of file
+exports.getScreenId = window.getScreenId;
\ No newline at end of file
diff --git a/openvidu-browser/src/OpenViduInternal/Utils/Platform.ts b/openvidu-browser/src/OpenViduInternal/Utils/Platform.ts
new file mode 100644
index 00000000..3f4181a3
--- /dev/null
+++ b/openvidu-browser/src/OpenViduInternal/Utils/Platform.ts
@@ -0,0 +1,205 @@
+import platform = require("platform");
+
+export class PlatformUtils {
+	protected static instance: PlatformUtils;
+	constructor() {}
+
+	static getInstance(): PlatformUtils {
+		if (!this.instance) {
+			this.instance = new PlatformUtils();
+		}
+		return PlatformUtils.instance;
+	}
+
+	public isChromeBrowser(): boolean {
+		return platform.name === "Chrome";
+	}
+
+	/**
+	 * @hidden
+	 */
+	public isSafariBrowser(): boolean {
+		return platform.name === "Safari";
+	}
+
+	/**
+	 * @hidden
+	 */
+	public isChromeMobileBrowser(): boolean {
+		return platform.name === "Chrome Mobile";
+	}
+
+	/**
+	 * @hidden
+	 */
+	public isFirefoxBrowser(): boolean {
+		return platform.name === "Firefox";
+	}
+
+	/**
+	 * @hidden
+	 */
+	public isFirefoxMobileBrowser(): boolean {
+		return platform.name === "Firefox Mobile";
+	}
+
+	/**
+	 * @hidden
+	 */
+	public isOperaBrowser(): boolean {
+		return platform.name === "Opera";
+	}
+
+	/**
+	 * @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 isEdgeMobileBrowser(): boolean {
+		const version = platform?.version ? parseFloat(platform.version) : -1;
+		return platform.name === "Microsoft Edge" && platform.os?.family === 'Android' && version > 45;
+	}
+
+	/**
+	 * @hidden
+	 */
+	public isAndroidBrowser(): boolean {
+		return platform.name === "Android Browser";
+	}
+
+	/**
+	 * @hidden
+	 */
+	public isElectron(): boolean {
+		return platform.name === "Electron";
+	}
+
+	/**
+	 * @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 isIOSWithSafari(): boolean {
+		const userAgent = !!platform.ua ? platform.ua : navigator.userAgent;
+		return (
+			/\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 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 isReactNative(): boolean {
+		return false;
+	}
+
+	/**
+	 * @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 getVersion(): string {
+		return platform.version || "";
+	}
+
+	/**
+	 * @hidden
+	 */
+	public getFamily(): string {
+		return platform.os!!.family || "";
+	}
+
+	/**
+	 * @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 b36488d2..1fdf895a 100644
--- a/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts
+++ b/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts
@@ -17,12 +17,17 @@
 
 import freeice = require('freeice');
 import uuid = require('uuid');
-import platform = require('platform');
 import { OpenViduLogger } from '../Logger/OpenViduLogger';
+import { PlatformUtils } from '../Utils/Platform';
+
 /**
  * @hidden
  */
 const logger: OpenViduLogger = OpenViduLogger.getInstance();
+/**
+ * @hidden
+ */
+let platform: PlatformUtils;
 
 
 export interface WebRtcPeerConfiguration {
@@ -50,6 +55,7 @@ export class WebRtcPeer {
     private candidategatheringdone = false;
 
     constructor(protected configuration: WebRtcPeerConfiguration) {
+        platform = PlatformUtils.getInstance();
         this.configuration.iceServers = (!!this.configuration.iceServers && this.configuration.iceServers.length > 0) ? this.configuration.iceServers : freeice();
 
         this.pc = new RTCPeerConnection({ iceServers: this.configuration.iceServers });
@@ -139,8 +145,7 @@ export class WebRtcPeer {
 
             logger.debug('RTCPeerConnection constraints: ' + JSON.stringify(constraints));
 
-            if (platform.name === 'Safari' && platform.ua!!.indexOf('Safari') !== -1) {
-
+            if (platform.isSafariBrowser() && !platform.isIonicIos()) {
                 // Safari (excluding Ionic), at least on iOS just seems to support unified plan, whereas in other browsers is not yet ready and considered experimental
                 if (offerAudio) {
                     this.pc.addTransceiver('audio', {
@@ -280,8 +285,35 @@ export class WebRtcPeer {
     /**
      * @hidden
      */
+
+// FIXME CONFLICT WITH MASTER
+// In the mediasoup branch, the special treatment for ionic was removed:
+//   openvidu-browser: removed Ionic iOS timeout on first subscription
+//   https://github.com/OpenVidu/openvidu/commit/23d64be8063f8fdb2a212ca845e304762f2803f5
+// and also the method was converted into async and returned a Promise.
+// However in master the code is still like the old one.
+<<<<<<< HEAD
     async setRemoteDescription(sdp: RTCSessionDescriptionInit): Promise {
         return this.pc.setRemoteDescription(sdp);
+=======
+    setRemoteDescription(answer: RTCSessionDescriptionInit, needsTimeoutOnProcessAnswer: boolean, resolve: (value?: string | PromiseLike | undefined) => void, reject: (reason?: any) => void) {
+        if (platform.isIonicIos()) {
+            // Ionic iOS platform
+            if (needsTimeoutOnProcessAnswer) {
+                // 400 ms have not elapsed yet since first remote stream triggered Stream#initWebRtcPeerReceive
+                setTimeout(() => {
+                    logger.info('setRemoteDescription run after timeout for Ionic iOS device');
+                    this.pc.setRemoteDescription(new RTCSessionDescription(answer)).then(() => resolve()).catch(error => reject(error));
+                }, 250);
+            } else {
+                // 400 ms have elapsed
+                this.pc.setRemoteDescription(new RTCSessionDescription(answer)).then(() => resolve()).catch(error => reject(error));
+            }
+        } else {
+            // Rest of platforms
+            this.pc.setRemoteDescription(answer).then(() => resolve()).catch(error => reject(error));
+        }
+>>>>>>> master
     }
 
     /**
@@ -369,4 +401,4 @@ export class WebRtcPeerSendrecv extends WebRtcPeer {
         configuration.mode = 'sendrecv';
         super(configuration);
     }
-}
\ No newline at end of file
+}
diff --git a/openvidu-browser/src/OpenViduInternal/WebRtcStats/WebRtcStats.ts b/openvidu-browser/src/OpenViduInternal/WebRtcStats/WebRtcStats.ts
index 502adc0e..3c1327b7 100644
--- a/openvidu-browser/src/OpenViduInternal/WebRtcStats/WebRtcStats.ts
+++ b/openvidu-browser/src/OpenViduInternal/WebRtcStats/WebRtcStats.ts
@@ -18,13 +18,16 @@
 // tslint:disable:no-string-literal
 
 import { Stream } from '../../OpenVidu/Stream';
-import platform = require('platform');
 import { OpenViduLogger } from '../Logger/OpenViduLogger';
+import { PlatformUtils } from '../Utils/Platform';
 /**
  * @hidden
  */
 const logger: OpenViduLogger = OpenViduLogger.getInstance();
-
+/**
+ * @hidden
+ */
+let platform: PlatformUtils;
 
 export class WebRtcStats {
 
@@ -60,7 +63,9 @@ export class WebRtcStats {
         }
     };
 
-    constructor(private stream: Stream) { }
+    constructor(private stream: Stream) {
+        platform = PlatformUtils.getInstance();
+    }
 
     public isEnabled(): boolean {
         return this.webRtcStatsEnabled;
@@ -103,7 +108,7 @@ export class WebRtcStats {
         return new Promise((resolve, reject) => {
             this.getStatsAgnostic(this.stream.getRTCPeerConnection(),
                 (stats) => {
-                    if ((platform.name!.indexOf('Chrome') !== -1) || (platform.name!.indexOf('Opera') !== -1)) {
+                    if (platform.isChromeBrowser() || platform.isChromeMobileBrowser() || platform.isOperaBrowser() || platform.isOperaMobileBrowser()) {
                         let localCandidateId, remoteCandidateId, googCandidatePair;
                         const localCandidates = {};
                         const remoteCandidates = {};
@@ -181,7 +186,7 @@ export class WebRtcStats {
 
         const f = (stats) => {
 
-            if (platform.name!.indexOf('Firefox') !== -1) {
+            if (platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) {
                 stats.forEach((stat) => {
 
                     let json = {};
@@ -278,7 +283,7 @@ export class WebRtcStats {
                         sendPost(JSON.stringify(json));
                     }
                 });
-            } else if ((platform.name!.indexOf('Chrome') !== -1) || (platform.name!.indexOf('Opera') !== -1)) {
+            } else if (platform.isChromeBrowser() || platform.isChromeMobileBrowser() || platform.isOperaBrowser() || platform.isOperaMobileBrowser()) {
                 for (const key of Object.keys(stats)) {
                     const stat = stats[key];
                     if (stat.type === 'ssrc') {
@@ -377,7 +382,7 @@ export class WebRtcStats {
         logger.log(response);
         const standardReport = {};
 
-        if (platform.name!.indexOf('Firefox') !== -1) {
+        if (platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) {
             Object.keys(response).forEach(key => {
                 logger.log(response[key]);
             });
@@ -400,13 +405,13 @@ export class WebRtcStats {
     }
 
     private getStatsAgnostic(pc, successCb, failureCb) {
-        if (platform.name!.indexOf('Firefox') !== -1) {
+        if (platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) {
             // getStats takes args in different order in Chrome and Firefox
             return pc.getStats(null).then(response => {
                 const report = this.standardizeReport(response);
                 successCb(report);
             }).catch(failureCb);
-        } else if ((platform.name!.indexOf('Chrome') !== -1) || (platform.name!.indexOf('Opera') !== -1)) {
+        } else if (platform.isChromeBrowser() || platform.isChromeMobileBrowser() || platform.isOperaBrowser() || platform.isOperaMobileBrowser()) {
             // In Chrome, the first two arguments are reversed
             return pc.getStats((response) => {
                 const report = this.standardizeReport(response);
diff --git a/openvidu-browser/src/index.ts b/openvidu-browser/src/index.ts
index 828697bf..8576b022 100644
--- a/openvidu-browser/src/index.ts
+++ b/openvidu-browser/src/index.ts
@@ -22,7 +22,9 @@ export { StreamEvent } from './OpenViduInternal/Events/StreamEvent';
 export { StreamManagerEvent } from './OpenViduInternal/Events/StreamManagerEvent';
 export { VideoElementEvent } from './OpenViduInternal/Events/VideoElementEvent';
 export { StreamPropertyChangedEvent } from './OpenViduInternal/Events/StreamPropertyChangedEvent';
+export { ConnectionPropertyChangedEvent } from './OpenViduInternal/Events/ConnectionPropertyChangedEvent';
 export { FilterEvent } from './OpenViduInternal/Events/FilterEvent';
+export { NetworkQualityLevelChangedEvent } from './OpenViduInternal/Events/NetworkQualityLevelChangedEvent';
 
 export { Capabilities } from './OpenViduInternal/Interfaces/Public/Capabilities';
 export { Device } from './OpenViduInternal/Interfaces/Public/Device';
diff --git a/openvidu-client/src/main/java/io/openvidu/client/OpenViduException.java b/openvidu-client/src/main/java/io/openvidu/client/OpenViduException.java
index c2c47515..c39ece51 100644
--- a/openvidu-client/src/main/java/io/openvidu/client/OpenViduException.java
+++ b/openvidu-client/src/main/java/io/openvidu/client/OpenViduException.java
@@ -50,8 +50,10 @@ public class OpenViduException extends JsonRpcErrorException {
 		DOCKER_NOT_FOUND(709), RECORDING_PATH_NOT_VALID(708), RECORDING_FILE_EMPTY_ERROR(707),
 		RECORDING_DELETE_ERROR_CODE(706), RECORDING_LIST_ERROR_CODE(705), RECORDING_STOP_ERROR_CODE(704),
 		RECORDING_START_ERROR_CODE(703), RECORDING_REPORT_ERROR_CODE(702), RECORDING_COMPLETION_ERROR_CODE(701),
-		
-		FORCED_CODEC_NOT_FOUND_IN_SDPOFFER(800);
+
+		FORCED_CODEC_NOT_FOUND_IN_SDPOFFER(800),
+
+		MEDIA_NODE_NOT_FOUND(900), MEDIA_NODE_STATUS_WRONG(901);
 
 		private int value;
 
diff --git a/openvidu-client/src/main/java/io/openvidu/client/internal/ProtocolElements.java b/openvidu-client/src/main/java/io/openvidu/client/internal/ProtocolElements.java
index f27a9e78..1c495679 100644
--- a/openvidu-client/src/main/java/io/openvidu/client/internal/ProtocolElements.java
+++ b/openvidu-client/src/main/java/io/openvidu/client/internal/ProtocolElements.java
@@ -98,6 +98,15 @@ public class ProtocolElements {
 	public static final String STREAMPROPERTYCHANGED_NEWVALUE_PARAM = "newValue";
 	public static final String STREAMPROPERTYCHANGED_REASON_PARAM = "reason";
 
+	public static final String CONNECTIONPERTYCHANGED_METHOD = "connectionPropertyChanged";
+	public static final String CONNECTIONROPERTYCHANGED_PROPERTY_PARAM = "property";
+	public static final String CONNECTIONPROPERTYCHANGED_NEWVALUE_PARAM = "newValue";
+
+	public static final String NETWORKQUALITYLEVELCHANGED_METHOD = "networkQualityLevelChanged";
+	public static final String NETWORKQUALITYCHANGED_CONNECTIONID_PARAM = "connectionId";
+	public static final String NETWORKQUALITYCHANGED_NEWVALUE_PARAM = "newValue";
+	public static final String NETWORKQUALITYCHANGED_OLDVALUE_PARAM = "oldValue";
+
 	public static final String FORCEDISCONNECT_METHOD = "forceDisconnect";
 	public static final String FORCEDISCONNECT_CONNECTIONID_PARAM = "connectionId";
 
@@ -127,12 +136,22 @@ public class ProtocolElements {
 	public static final String RECONNECTSTREAM_STREAM_PARAM = "stream";
 	public static final String RECONNECTSTREAM_SDPSTRING_PARAM = "sdpString";
 
+	public static final String VIDEODATA_METHOD = "videoData";
+
 	// ---------------------------- SERVER RESPONSES & EVENTS -----------------
 
 	public static final String PARTICIPANTJOINED_METHOD = "participantJoined";
 	public static final String PARTICIPANTJOINED_USER_PARAM = "id";
 	public static final String PARTICIPANTJOINED_CREATEDAT_PARAM = "createdAt";
 	public static final String PARTICIPANTJOINED_METADATA_PARAM = "metadata";
+	public static final String PARTICIPANTJOINED_VALUE_PARAM = "value";
+	public static final String PARTICIPANTJOINED_SESSION_PARAM = "session";
+	public static final String PARTICIPANTJOINED_VERSION_PARAM = "version";
+	public static final String PARTICIPANTJOINED_RECORD_PARAM = "record";
+	public static final String PARTICIPANTJOINED_ROLE_PARAM = "role";
+	public static final String PARTICIPANTJOINED_COTURNIP_PARAM = "coturnIp";
+	public static final String PARTICIPANTJOINED_TURNUSERNAME_PARAM = "turnUsername";
+	public static final String PARTICIPANTJOINED_TURNCREDENTIAL_PARAM = "turnCredential";
 
 	public static final String PARTICIPANTLEFT_METHOD = "participantLeft";
 	public static final String PARTICIPANTLEFT_NAME_PARAM = "connectionId";
diff --git a/openvidu-java-client/pom.xml b/openvidu-java-client/pom.xml
index 5520e10f..e7f39c9b 100644
--- a/openvidu-java-client/pom.xml
+++ b/openvidu-java-client/pom.xml
@@ -10,7 +10,7 @@
 	
 
 	openvidu-java-client
-	2.15.1
+	2.16.0
 	jar
 
 	OpenVidu Java Client
diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/Connection.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/Connection.java
index c962c03a..bef16873 100644
--- a/openvidu-java-client/src/main/java/io/openvidu/java/client/Connection.java
+++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/Connection.java
@@ -18,77 +18,194 @@
 package io.openvidu.java.client;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
 
 /**
- * See {@link io.openvidu.java.client.Session#getActiveConnections()}
+ * See {@link io.openvidu.java.client.Session#getConnections()}
  */
 public class Connection {
 
 	private String connectionId;
-	private long createdAt;
-	private OpenViduRole role;
-	private String token;
+	private String status;
+	private Long createdAt;
+	private Long activeAt;
 	private String location;
 	private String platform;
-	private String serverData;
 	private String clientData;
+	private ConnectionProperties connectionProperties;
+	private String token;
 
-	protected Map publishers;
-	protected List subscribers;
+	protected Map publishers = new ConcurrentHashMap<>();
+	protected List subscribers = new ArrayList<>();
 
-	protected Connection(String connectionId, long createdAt, OpenViduRole role, String token, String location,
-			String platform, String serverData, String clientData, Map publishers,
-			List subscribers) {
-		this.connectionId = connectionId;
-		this.createdAt = createdAt;
-		this.role = role;
-		this.token = token;
-		this.location = location;
-		this.platform = platform;
-		this.serverData = serverData;
-		this.clientData = clientData;
-		this.publishers = publishers;
-		this.subscribers = subscribers;
+	protected Connection(JsonObject json) {
+		this.resetWithJson(json);
 	}
 
 	/**
-	 * Returns the identifier of the connection. You can call
-	 * {@link io.openvidu.java.client.Session#forceDisconnect(String)} passing this
-	 * property as parameter
+	 * Returns the identifier of the Connection. You can call methods
+	 * {@link io.openvidu.java.client.Session#forceDisconnect(String)} or
+	 * {@link io.openvidu.java.client.Session#updateConnection(String, ConnectionProperties)}
+	 * passing this property as parameter
 	 */
 	public String getConnectionId() {
 		return connectionId;
 	}
 
 	/**
-	 * Timestamp when this connection was established, in UTC milliseconds (ms since
-	 * Jan 1, 1970, 00:00:00 UTC)
+	 * Returns the status of the Connection. Can be:
+	 * 
    + *
  • pending: if the Connection is waiting for any user to use + * its internal token to connect to the session, calling method Session.connect in OpenVidu Browser.
  • + *
  • active: if the internal token of the Connection has already + * been used by some user to connect to the session, and it cannot be used + * again.
  • */ - public long createdAt() { + public String getStatus() { + return this.status; + } + + /** + * Timestamp when this Connection was created, in UTC milliseconds (ms since Jan + * 1, 1970, 00:00:00 UTC) + */ + public Long createdAt() { return this.createdAt; } /** - * Returns the role of the connection + * Timestamp when this Connection was taken by a user (passing from status + * "pending" to "active"), in UTC milliseconds (ms since Jan 1, 1970, 00:00:00 + * UTC) + */ + public Long activeAt() { + return this.activeAt; + } + + /** + * Returns the type of Connection. + */ + public ConnectionType getType() { + return this.connectionProperties.getType(); + } + + /** + * Returns the data associated to the Connection on the server-side. This value + * is set with {@link io.openvidu.java.client.TokenOptions.Builder#data(String)} + * when calling {@link io.openvidu.java.client.Session#generateToken()} + */ + public String getServerData() { + return this.connectionProperties.getData(); + } + + /** + * Whether the streams published by this Connection will be recorded or not. + * This only affects INDIVIDUAL recording. + */ + public boolean record() { + return this.connectionProperties.record(); + } + + /** + * Returns the role of the Connection. + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#WEBRTC} */ public OpenViduRole getRole() { - return role; + return this.connectionProperties.getRole(); } /** - * Returns the token associated to the connection + * Returns the RTSP URI of the Connection. + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#IPCAM} + */ + public String getRtspUri() { + return this.connectionProperties.getRtspUri(); + } + + /** + * Whether the Connection uses adaptative bitrate (and therefore adaptative + * quality) or not. For local network connections that do not require media + * transcoding this can be disabled to save CPU power. If you are not sure if + * transcoding might be necessary, setting this property to false may + * result in media connections not being established. + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#IPCAM} + */ + public Boolean adaptativeBitrate() { + return this.connectionProperties.adaptativeBitrate(); + } + + /** + * Whether the IP camera stream of this Connection will only be enabled when + * some user is subscribed to it, or not. This allows you to reduce power + * consumption and network bandwidth in your server while nobody is asking to + * receive the camera's video. On the counterpart, first user subscribing to the + * IP camera stream will take a little longer to receive its video. + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#IPCAM} + */ + public Boolean onlyPlayWithSubscribers() { + return this.connectionProperties.onlyPlayWithSubscribers(); + } + + /** + * Returns the size of the buffer of the endpoint receiving the IP camera's + * stream, in milliseconds. The smaller it is, the less delay the signal will + * have, but more problematic will be in unstable networks. Use short buffers + * only if there is a quality connection between the IP camera and OpenVidu + * Server. + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#IPCAM} + */ + public Integer getNetworkCache() { + return this.connectionProperties.getNetworkCache(); + } + + /** + * Returns the token string associated to the Connection. This is the value that + * must be sent to the client-side to be consumed in OpenVidu Browser method + * Session.connect. */ public String getToken() { - return token; + return this.token; } /** - * PRO + * PRO * * Returns the geo location of the connection, with the following format: * "CITY, COUNTRY" ("unknown" if it wasn't possible to @@ -106,20 +223,11 @@ public class Connection { return platform; } - /** - * Returns the data associated to the connection on the server-side. This value - * is set with {@link io.openvidu.java.client.TokenOptions.Builder#data(String)} - * when calling {@link io.openvidu.java.client.Session#generateToken()} - */ - public String getServerData() { - return serverData; - } - /** * Returns the data associated to the connection on the client-side. This value - * is set with second parameter of method - * Session.connect in OpenVidu Browser + * is set with second parameter of method Session.connect in OpenVidu Browser */ public String getClientData() { return clientData; @@ -147,8 +255,171 @@ public class Connection { return this.subscribers; } + protected JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("connectionId", this.getConnectionId()); + json.addProperty("status", this.getStatus()); + json.addProperty("createdAt", this.createdAt()); + json.addProperty("activeAt", this.activeAt()); + json.addProperty("location", this.getLocation()); + json.addProperty("platform", this.getPlatform()); + json.addProperty("clientData", this.getClientData()); + json.addProperty("token", this.getToken()); + + JsonObject jsonConnectionProperties = this.connectionProperties.toJson(""); + jsonConnectionProperties.remove("session"); + json.addProperty("serverData", jsonConnectionProperties.get("data").getAsString()); + jsonConnectionProperties.remove("data"); + jsonConnectionProperties.entrySet().forEach(entry -> { + json.add(entry.getKey(), entry.getValue()); + }); + + JsonArray pubs = new JsonArray(); + this.getPublishers().forEach(p -> { + pubs.add(p.toJson()); + }); + JsonArray subs = new JsonArray(); + this.getSubscribers().forEach(s -> { + subs.add(s); + }); + json.add("publishers", pubs); + json.add("subscribers", subs); + return json; + } + + protected void overrideConnectionProperties(ConnectionProperties newConnectionProperties) { + ConnectionProperties.Builder builder = new ConnectionProperties.Builder(); + // For now only properties role and record can be updated + if (newConnectionProperties.getRole() != null) { + builder.role(newConnectionProperties.getRole()); + } else { + builder.role(this.connectionProperties.getRole()); + } + if (newConnectionProperties.record() != null) { + builder.record(newConnectionProperties.record()); + } else { + builder.record(this.connectionProperties.record()); + } + // Keep old configuration in the rest of properties + builder.type(this.connectionProperties.getType()).data(this.connectionProperties.getData()) + .kurentoOptions(this.connectionProperties.getKurentoOptions()) + .rtspUri(this.connectionProperties.getRtspUri()); + if (this.connectionProperties.adaptativeBitrate() != null) { + builder.adaptativeBitrate(this.connectionProperties.adaptativeBitrate()); + } + if (this.connectionProperties.onlyPlayWithSubscribers() != null) { + builder.onlyPlayWithSubscribers(this.connectionProperties.onlyPlayWithSubscribers()); + } + if (this.connectionProperties.getNetworkCache() != null) { + builder.networkCache(this.connectionProperties.getNetworkCache()); + } + this.connectionProperties = builder.build(); + } + protected void setSubscribers(List subscribers) { this.subscribers = subscribers; } + protected Connection resetWithJson(JsonObject json) { + + this.connectionId = json.get("connectionId").getAsString(); + this.status = json.get("status").getAsString(); + this.token = !json.get("token").isJsonNull() ? json.get("token").getAsString() : null; + + if (!json.get("publishers").isJsonNull()) { + JsonArray jsonArrayPublishers = json.get("publishers").getAsJsonArray(); + + // 1. Set to store fetched publishers and later remove closed ones + Set fetchedPublisherIds = new HashSet<>(); + jsonArrayPublishers.forEach(publisherJsonElement -> { + + JsonObject publisherJson = publisherJsonElement.getAsJsonObject(); + Publisher publisherObj = new Publisher(publisherJson); + String id = publisherObj.getStreamId(); + fetchedPublisherIds.add(id); + + // 2. Update existing Publisher + this.publishers.computeIfPresent(id, (pId, p) -> { + p = p.resetWithJson(publisherJson); + return p; + }); + + // 3. Add new Publisher + this.publishers.computeIfAbsent(id, pId -> { + return publisherObj; + }); + }); + + // 4. Remove closed connections from local collection + this.publishers.entrySet().removeIf(entry -> !fetchedPublisherIds.contains(entry.getValue().getStreamId())); + } + + if (!json.get("subscribers").isJsonNull()) { + JsonArray jsonArraySubscribers = json.get("subscribers").getAsJsonArray(); + + // 1. Array to store fetched Subscribers and later remove closed ones + Set fetchedSubscriberIds = new HashSet<>(); + jsonArraySubscribers.forEach(subscriber -> { + + String sub = subscriber.getAsJsonObject().get("streamId").getAsString(); + fetchedSubscriberIds.add(sub); + + if (!this.subscribers.contains(sub)) { + // 2. Add new Subscriber + this.subscribers.add(sub); + } + }); + + // 3. Remove closed Subscribers from local collection + this.subscribers.removeIf(subId -> !fetchedSubscriberIds.contains(subId)); + } + + if (!json.get("createdAt").isJsonNull()) { + this.createdAt = json.get("createdAt").getAsLong(); + } + if (!json.get("activeAt").isJsonNull()) { + this.activeAt = json.get("activeAt").getAsLong(); + } + if (!json.get("location").isJsonNull()) { + this.location = json.get("location").getAsString(); + } + if (!json.get("platform").isJsonNull()) { + this.platform = json.get("platform").getAsString(); + } + if (!json.get("clientData").isJsonNull()) { + this.clientData = json.get("clientData").getAsString(); + } + + // COMMON + ConnectionType type = ConnectionType.valueOf(json.get("type").getAsString()); + String data = (json.has("serverData") && !json.get("serverData").isJsonNull()) + ? json.get("serverData").getAsString() + : null; + Boolean record = (json.has("record") && !json.get("record").isJsonNull()) ? json.get("record").getAsBoolean() + : null; + + // WEBRTC + OpenViduRole role = (json.has("role") && !json.get("role").isJsonNull()) + ? OpenViduRole.valueOf(json.get("role").getAsString()) + : null; + + // IPCAM + String rtspUri = (json.has("rtspUri") && !json.get("rtspUri").isJsonNull()) ? json.get("rtspUri").getAsString() + : null; + Boolean adaptativeBitrate = (json.has("adaptativeBitrate") && !json.get("adaptativeBitrate").isJsonNull()) + ? json.get("adaptativeBitrate").getAsBoolean() + : null; + Boolean onlyPlayWithSubscribers = (json.has("onlyPlayWithSubscribers") + && !json.get("onlyPlayWithSubscribers").isJsonNull()) + ? json.get("onlyPlayWithSubscribers").getAsBoolean() + : null; + Integer networkCache = (json.has("networkCache") && !json.get("networkCache").isJsonNull()) + ? json.get("networkCache").getAsInt() + : null; + this.connectionProperties = new ConnectionProperties(type, data, record, role, null, rtspUri, adaptativeBitrate, + onlyPlayWithSubscribers, networkCache); + + return this; + } + } diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/ConnectionProperties.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/ConnectionProperties.java new file mode 100644 index 00000000..b68504c3 --- /dev/null +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/ConnectionProperties.java @@ -0,0 +1,403 @@ +package io.openvidu.java.client; + +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; + +/** + * See + * {@link io.openvidu.java.client.Session#createConnection(ConnectionProperties)} + */ +public class ConnectionProperties { + + private ConnectionType type; + // COMMON + private String data; + private Boolean record; + // WEBRTC + private OpenViduRole role; + private KurentoOptions kurentoOptions; + // IPCAM + private String rtspUri; + private Boolean adaptativeBitrate; + private Boolean onlyPlayWithSubscribers; + private Integer networkCache; + + /** + * + * Builder for {@link io.openvidu.java.client.ConnectionProperties} + * + */ + public static class Builder { + + private ConnectionType type; + // COMMON + private String data; + private Boolean record; + // WEBRTC + private OpenViduRole role; + private KurentoOptions kurentoOptions; + // IPCAM + private String rtspUri; + private Boolean adaptativeBitrate; + private Boolean onlyPlayWithSubscribers; + private Integer networkCache; + + /** + * Builder for {@link io.openvidu.java.client.ConnectionProperties}. + */ + public ConnectionProperties build() { + return new ConnectionProperties(this.type, this.data, this.record, this.role, this.kurentoOptions, + this.rtspUri, this.adaptativeBitrate, this.onlyPlayWithSubscribers, this.networkCache); + } + + /** + * Call this method to set the type of Connection. The + * {@link io.openvidu.java.client.ConnectionType} dictates what properties will + * have effect: + *
      + *
    • {@link io.openvidu.java.client.ConnectionType#WEBRTC}: + * {@link io.openvidu.java.client.ConnectionProperties.Builder#data(String) + * data}, + * {@link io.openvidu.java.client.ConnectionProperties.Builder#record(boolean) + * record}, + * {@link io.openvidu.java.client.ConnectionProperties.Builder#role(OpenViduRole) + * role}, + * {@link io.openvidu.java.client.ConnectionProperties.Builder#kurentoOptions(KurentoOptions) + * kurentoOptions}
    • + *
    • {@link io.openvidu.java.client.ConnectionType#IPCAM}: + * {@link io.openvidu.java.client.ConnectionProperties.Builder#data(String) + * data}, + * {@link io.openvidu.java.client.ConnectionProperties.Builder#record(boolean) + * record}, + * {@link io.openvidu.java.client.ConnectionProperties.Builder#rtspUri(String) + * rtspUri}, + * {@link io.openvidu.java.client.ConnectionProperties.Builder#adaptativeBitrate(boolean) + * adaptativeBitrate}, + * {@link io.openvidu.java.client.ConnectionProperties.Builder#onlyPlayWithSubscribers(boolean) + * onlyPlayWithSubscribers}, + * {@link io.openvidu.java.client.ConnectionProperties.Builder#networkCache(int) + * networkCache}
    • + *
    + * If not set by default will be + * {@link io.openvidu.java.client.ConnectionType#WEBRTC}. + */ + public Builder type(ConnectionType type) { + this.type = type; + return this; + } + + /** + * Call this method to set the secure (server-side) data associated to this + * Connection. Every client will receive this data in property + * Connection.data. Object Connection can be retrieved + * by subscribing to event connectionCreated of Session object in + * your clients. + *
      + *
    • If you have provided no data in your clients when calling method + * Session.connect(TOKEN, DATA) (DATA not defined), + * then Connection.data will only have this + * {@link io.openvidu.java.client.ConnectionProperties.Builder#data(String)} + * property.
    • + *
    • If you have provided some data when calling + * Session.connect(TOKEN, DATA) (DATA defined), then + * Connection.data will have the following structure: + * "CLIENT_DATA%/%SERVER_DATA", being + * CLIENT_DATA the second parameter passed in OpenVidu Browser in + * method Session.connect and SERVER_DATA this + * {@link io.openvidu.java.client.ConnectionProperties.Builder#data(String)} + * property.
    • + *
    + */ + public Builder data(String data) { + this.data = data; + return this; + } + + /** + * Call this method to flag the streams published by this Connection to be + * recorded or not. This only affects INDIVIDUAL recording. If not set by default will be true. + */ + public Builder record(boolean record) { + this.record = record; + return this; + } + + /** + * Call this method to set the role assigned to this Connection. If not set by + * default will be {@link io.openvidu.java.client.OpenViduRole#PUBLISHER + * PUBLISHER}. + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#WEBRTC} + */ + public Builder role(OpenViduRole role) { + this.role = role; + return this; + } + + /** + * Call this method to set a {@link io.openvidu.java.client.KurentoOptions} + * object for this Connection. + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#WEBRTC} + */ + public Builder kurentoOptions(KurentoOptions kurentoOptions) { + this.kurentoOptions = kurentoOptions; + return this; + } + + /** + * Call this method to set the RTSP URI of an IP camera. For example: + * rtsp://your.camera.ip:7777/path + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#IPCAM} + */ + public Builder rtspUri(String rtspUri) { + this.rtspUri = rtspUri; + return this; + } + + /** + * Call this method to set whether to use adaptative bitrate (and therefore + * adaptative quality) or not. For local network connections that do not require + * media transcoding this can be disabled to save CPU power. If you are not sure + * if transcoding might be necessary, setting this property to false may + * result in media connections not being established. Default to + * true. + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#IPCAM} + */ + public Builder adaptativeBitrate(boolean adaptativeBitrate) { + this.adaptativeBitrate = adaptativeBitrate; + return this; + } + + /** + * Call this method to set whether to enable the IP camera stream only when some + * user is subscribed to it, or not. This allows you to reduce power consumption + * and network bandwidth in your server while nobody is asking to receive the + * camera's video. On the counterpart, first user subscribing to the IP camera + * stream will take a little longer to receive its video. Default to + * true. + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#IPCAM} + */ + public Builder onlyPlayWithSubscribers(boolean onlyPlayWithSubscribers) { + this.onlyPlayWithSubscribers = onlyPlayWithSubscribers; + return this; + } + + /** + * Call this method to set the size of the buffer of the endpoint receiving the + * IP camera's stream, in milliseconds. The smaller it is, the less delay the + * signal will have, but more problematic will be in unstable networks. Use + * short buffers only if there is a quality connection between the IP camera and + * OpenVidu Server. Default to 2000. + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#IPCAM} + */ + public Builder networkCache(int networkCache) { + this.networkCache = networkCache; + return this; + } + } + + ConnectionProperties(ConnectionType type, String data, Boolean record, OpenViduRole role, + KurentoOptions kurentoOptions, String rtspUri, Boolean adaptativeBitrate, Boolean onlyPlayWithSubscribers, + Integer networkCache) { + this.type = type; + this.data = data; + this.record = record; + this.role = role; + this.kurentoOptions = kurentoOptions; + this.rtspUri = rtspUri; + this.adaptativeBitrate = adaptativeBitrate; + this.onlyPlayWithSubscribers = onlyPlayWithSubscribers; + this.networkCache = networkCache; + } + + /** + * Returns the type of Connection. + */ + public ConnectionType getType() { + return this.type; + } + + /** + * Returns the secure (server-side) metadata assigned to this Connection. + */ + public String getData() { + return this.data; + } + + /** + * PRO Whether the streams published by this Connection will be + * recorded or not. This only affects INDIVIDUAL recording. + */ + public Boolean record() { + return this.record; + } + + /** + * Returns the role assigned to this Connection. + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#WEBRTC} + */ + public OpenViduRole getRole() { + return this.role; + } + + /** + * Returns the KurentoOptions assigned to this Connection. + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#WEBRTC} + */ + public KurentoOptions getKurentoOptions() { + return this.kurentoOptions; + } + + /** + * Returns the RTSP URI of this Connection. + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#IPCAM} + */ + public String getRtspUri() { + return this.rtspUri; + } + + /** + * Whether this Connection uses adaptative bitrate (and therefore adaptative + * quality) or not. For local network connections that do not require media + * transcoding this can be disabled to save CPU power. If you are not sure if + * transcoding might be necessary, setting this property to false may + * result in media connections not being established. + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#IPCAM} + */ + public Boolean adaptativeBitrate() { + return this.adaptativeBitrate; + } + + /** + * Whether to enable the IP camera stream only when some user is subscribed to + * it. This allows you to reduce power consumption and network bandwidth in your + * server while nobody is asking to receive the camera's video. On the + * counterpart, first user subscribing to the IP camera stream will take a + * little longer to receive its video. + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#IPCAM} + */ + public Boolean onlyPlayWithSubscribers() { + return this.onlyPlayWithSubscribers; + } + + /** + * Size of the buffer of the endpoint receiving the IP camera's stream, in + * milliseconds. The smaller it is, the less delay the signal will have, but + * more problematic will be in unstable networks. Use short buffers only if + * there is a quality connection between the IP camera and OpenVidu Server. + * + *
    + *
    + * Only for + * {@link io.openvidu.java.client.ConnectionType#IPCAM} + */ + public Integer getNetworkCache() { + return this.networkCache; + } + + public JsonObject toJson(String sessionId) { + JsonObject json = new JsonObject(); + json.addProperty("session", sessionId); + // COMMON + if (getType() != null) { + json.addProperty("type", getType().name()); + } else { + json.add("type", JsonNull.INSTANCE); + } + if (getData() != null) { + json.addProperty("data", getData()); + } else { + json.add("data", JsonNull.INSTANCE); + } + if (record() != null) { + json.addProperty("record", record()); + } else { + json.add("record", JsonNull.INSTANCE); + } + // WEBRTC + if (getRole() != null) { + json.addProperty("role", getRole().name()); + } else { + json.add("role", JsonNull.INSTANCE); + } + if (this.kurentoOptions != null) { + json.add("kurentoOptions", kurentoOptions.toJson()); + } else { + json.add("kurentoOptions", JsonNull.INSTANCE); + } + // IPCAM + if (getRtspUri() != null) { + json.addProperty("rtspUri", getRtspUri()); + } else { + json.add("rtspUri", JsonNull.INSTANCE); + } + if (adaptativeBitrate() != null) { + json.addProperty("adaptativeBitrate", adaptativeBitrate()); + } else { + json.add("adaptativeBitrate", JsonNull.INSTANCE); + } + if (onlyPlayWithSubscribers() != null) { + json.addProperty("onlyPlayWithSubscribers", onlyPlayWithSubscribers()); + } else { + json.add("onlyPlayWithSubscribers", JsonNull.INSTANCE); + } + if (getNetworkCache() != null) { + json.addProperty("networkCache", getNetworkCache()); + } else { + json.add("networkCache", JsonNull.INSTANCE); + } + return json; + } + +} diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/ConnectionType.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/ConnectionType.java new file mode 100644 index 00000000..11c1d2a8 --- /dev/null +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/ConnectionType.java @@ -0,0 +1,20 @@ +package io.openvidu.java.client; + +/** + * See + * {@link io.openvidu.java.client.Session#createConnection(ConnectionProperties)} + */ +public enum ConnectionType { + + /** + * WebRTC connection. This is the normal type of Connection for a regular user + * connecting to a session from an application. + */ + WEBRTC, + + /** + * IP camera connection. This is the type of Connection used by IP cameras to + * connect to a session. + */ + IPCAM +} diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/KurentoOptions.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/KurentoOptions.java index c29730ac..0f5f5fa3 100644 --- a/openvidu-java-client/src/main/java/io/openvidu/java/client/KurentoOptions.java +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/KurentoOptions.java @@ -17,6 +17,11 @@ package io.openvidu.java.client; +import java.util.Arrays; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + /** * See {@link io.openvidu.java.client.TokenOptions#getKurentoOptions()} */ @@ -105,10 +110,10 @@ public class KurentoOptions { } /** - * Defines the maximum number of Kbps that the client owning the token will be - * able to receive from Kurento Media Server. 0 means unconstrained. Giving a - * value to this property will override the global configuration set in OpenVidu Server configuration (parameter * OPENVIDU_STREAMS_VIDEO_MAX_RECV_BANDWIDTH) for every incoming * stream of the user owning the token.
    @@ -122,10 +127,10 @@ public class KurentoOptions { } /** - * Defines the minimum number of Kbps that the client owning the token will try - * to receive from Kurento Media Server. 0 means unconstrained. Giving a value - * to this property will override the global configuration set in OpenVidu Server configuration (parameter * OPENVIDU_STREAMS_VIDEO_MIN_RECV_BANDWIDTH) for every incoming * stream of the user owning the token. @@ -135,10 +140,10 @@ public class KurentoOptions { } /** - * Defines the maximum number of Kbps that the client owning the token will be - * able to send to Kurento Media Server. 0 means unconstrained. Giving a value - * to this property will override the global configuration set in OpenVidu Server configuration (parameter * OPENVIDU_STREAMS_VIDEO_MAX_SEND_BANDWIDTH) for every outgoing * stream of the user owning the token.
    @@ -151,10 +156,10 @@ public class KurentoOptions { } /** - * Defines the minimum number of Kbps that the client owning the token will try - * to send to Kurento Media Server. 0 means unconstrained. Giving a value to - * this property will override the global configuration set in OpenVidu Server configuration (parameter * OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH) for every outgoing * stream of the user owning the token. @@ -164,13 +169,50 @@ public class KurentoOptions { } /** - * Defines the names of the filters the user owning the token will be able to - * apply. See - * Voice and - * video filters + * Defines the names of the filters the Connection will be able to apply to its + * published streams. See + * Voice and video filters. */ public String[] getAllowedFilters() { return allowedFilters; } + /** + * See if the Connection can apply certain filter. See + * Voice and video filters. + */ + public boolean isFilterAllowed(String filterType) { + if (filterType == null) { + return false; + } + return Arrays.stream(allowedFilters).anyMatch(filterType::equals); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + if (this.getVideoMaxRecvBandwidth() != null) { + json.addProperty("videoMaxRecvBandwidth", this.getVideoMaxRecvBandwidth()); + } + if (this.getVideoMinRecvBandwidth() != null) { + json.addProperty("videoMinRecvBandwidth", this.getVideoMinRecvBandwidth()); + } + if (this.getVideoMaxSendBandwidth() != null) { + json.addProperty("videoMaxSendBandwidth", this.getVideoMaxSendBandwidth()); + } + if (this.getVideoMinSendBandwidth() != null) { + json.addProperty("videoMinSendBandwidth", this.getVideoMinSendBandwidth()); + } + if (this.getAllowedFilters().length > 0) { + JsonArray filtersJson = new JsonArray(); + String[] filters = this.getAllowedFilters(); + for (String filter : filters) { + filtersJson.add(filter); + } + json.add("allowedFilters", filtersJson); + } + return json; + } + } diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/OpenVidu.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/OpenVidu.java index 298c07ee..502a458d 100644 --- a/openvidu-java-client/src/main/java/io/openvidu/java/client/OpenVidu.java +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/OpenVidu.java @@ -31,7 +31,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import javax.net.ssl.SSLContext; @@ -70,16 +69,19 @@ public class OpenVidu { protected HttpClient httpClient; protected Map activeSessions = new ConcurrentHashMap<>(); - protected final static String API_SESSIONS = "api/sessions"; - protected final static String API_TOKENS = "api/tokens"; - protected final static String API_RECORDINGS = "api/recordings"; - protected final static String API_RECORDINGS_START = "/start"; - protected final static String API_RECORDINGS_STOP = "/stop"; + protected final static String API_PATH = "openvidu/api"; + protected final static String API_SESSIONS = API_PATH + "/sessions"; + protected final static String API_TOKENS = API_PATH + "/tokens"; + protected final static String API_RECORDINGS = API_PATH + "/recordings"; + protected final static String API_RECORDINGS_START = API_RECORDINGS + "/start"; + protected final static String API_RECORDINGS_STOP = API_RECORDINGS + "/stop"; /** - * @param urlOpenViduServer Public accessible IP where your instance of OpenVidu - * Server is up an running - * @param secret Secret used on OpenVidu Server initialization + * @param hostname URL where your instance of OpenVidu Server is up an running. + * It must be the full URL (e.g. + * https://12.34.56.78:1234/) + * + * @param secret Secret used on OpenVidu Server initialization */ public OpenVidu(String hostname, String secret) { @@ -192,16 +194,19 @@ public class OpenVidu { public Recording startRecording(String sessionId, RecordingProperties properties) throws OpenViduJavaClientException, OpenViduHttpException { - HttpPost request = new HttpPost(this.hostname + API_RECORDINGS + API_RECORDINGS_START); + HttpPost request = new HttpPost(this.hostname + API_RECORDINGS_START); JsonObject json = new JsonObject(); json.addProperty("session", sessionId); json.addProperty("name", properties.name()); - json.addProperty("outputMode", properties.outputMode().name()); + json.addProperty("outputMode", properties.outputMode() != null ? properties.outputMode().name() : null); json.addProperty("hasAudio", properties.hasAudio()); json.addProperty("hasVideo", properties.hasVideo()); + json.addProperty("shmSize", properties.shmSize()); + json.addProperty("mediaNode", properties.mediaNode()); - if ((Recording.OutputMode.COMPOSED.equals(properties.outputMode()) || (Recording.OutputMode.COMPOSED_QUICK_START.equals(properties.outputMode()))) + if ((properties.outputMode() == null || Recording.OutputMode.COMPOSED.equals(properties.outputMode()) + || (Recording.OutputMode.COMPOSED_QUICK_START.equals(properties.outputMode()))) && properties.hasVideo()) { json.addProperty("resolution", properties.resolution()); json.addProperty("recordingLayout", @@ -236,8 +241,9 @@ public class OpenVidu { if (activeSession != null) { activeSession.setIsBeingRecorded(true); } else { - log.warn("No active session found for sessionId '" + r.getSessionId() - + "'. This instance of OpenVidu Java Client didn't create this session"); + log.warn( + "No active session found for sessionId '{}'. This instance of OpenVidu Java Client didn't create this session", + r.getSessionId()); } return r; } else { @@ -352,7 +358,7 @@ public class OpenVidu { *
*/ public Recording stopRecording(String recordingId) throws OpenViduJavaClientException, OpenViduHttpException { - HttpPost request = new HttpPost(this.hostname + API_RECORDINGS + API_RECORDINGS_STOP + "/" + recordingId); + HttpPost request = new HttpPost(this.hostname + API_RECORDINGS_STOP + "/" + recordingId); HttpResponse response; try { response = this.httpClient.execute(request); @@ -368,8 +374,9 @@ public class OpenVidu { if (activeSession != null) { activeSession.setIsBeingRecorded(false); } else { - log.warn("No active session found for sessionId '" + r.getSessionId() - + "'. This instance of OpenVidu Java Client didn't create this session"); + log.warn( + "No active session found for sessionId '{}'. This instance of OpenVidu Java Client didn't create this session", + r.getSessionId()); } return r; } else { @@ -492,6 +499,10 @@ public class OpenVidu { * since the last time method {@link io.openvidu.java.client.OpenVidu#fetch()} * was called. Exceptions to this rule are: *
    + *
  • Calling + * {@link io.openvidu.java.client.OpenVidu#createSession(SessionProperties) + * OpenVidu.createSession} automatically adds the new Session object to the + * local collection.
  • *
  • Calling {@link io.openvidu.java.client.Session#fetch()} updates that * specific Session status
  • *
  • Calling {@link io.openvidu.java.client.Session#close()} automatically @@ -531,7 +542,7 @@ public class OpenVidu { * @throws OpenViduJavaClientException */ public boolean fetch() throws OpenViduJavaClientException, OpenViduHttpException { - HttpGet request = new HttpGet(this.hostname + API_SESSIONS); + HttpGet request = new HttpGet(this.hostname + API_SESSIONS + "?pendingConnections=true"); HttpResponse response; try { @@ -543,44 +554,55 @@ public class OpenVidu { try { int statusCode = response.getStatusLine().getStatusCode(); if ((statusCode == org.apache.http.HttpStatus.SC_OK)) { + JsonObject jsonSessions = httpResponseToJson(response); JsonArray jsonArraySessions = jsonSessions.get("content").getAsJsonArray(); - // Set to store fetched sessionIds and later remove closed sessions - Set fetchedSessionIds = new HashSet<>(); // Boolean to store if any Session has changed final boolean[] hasChanged = { false }; - jsonArraySessions.forEach(session -> { - String sessionId = (session.getAsJsonObject()).get("sessionId").getAsString(); - fetchedSessionIds.add(sessionId); - this.activeSessions.computeIfPresent(sessionId, (sId, s) -> { + + // 1. Set to store fetched sessionIds and later remove closed ones + Set fetchedSessionIds = new HashSet<>(); + jsonArraySessions.forEach(sessionJsonElement -> { + + JsonObject sessionJson = sessionJsonElement.getAsJsonObject(); + final Session sessionObj = new Session(this, sessionJson); + String id = sessionObj.getSessionId(); + fetchedSessionIds.add(id); + + // 2. Update existing Session + this.activeSessions.computeIfPresent(id, (sId, s) -> { String beforeJSON = s.toJson(); - s = s.resetSessionWithJson(session.getAsJsonObject()); + s = s.resetWithJson(sessionJson); String afterJSON = s.toJson(); boolean changed = !beforeJSON.equals(afterJSON); hasChanged[0] = hasChanged[0] || changed; - log.info("Available session '{}' info fetched. Any change: {}", sessionId, changed); + log.info("Available session '{}' info fetched. Any change: {}", id, changed); return s; }); - this.activeSessions.computeIfAbsent(sessionId, sId -> { - log.info("New session '{}' fetched", sessionId); + + // 3. Add new Session + this.activeSessions.computeIfAbsent(id, sId -> { + log.info("New session '{}' fetched", id); hasChanged[0] = true; - return new Session(this, session.getAsJsonObject()); + return sessionObj; }); }); - // Remove closed sessions from activeSessions map - this.activeSessions = this.activeSessions.entrySet().stream().filter(entry -> { + // 4. Remove closed sessions from local collection + this.activeSessions.entrySet().removeIf(entry -> { if (fetchedSessionIds.contains(entry.getKey())) { - return true; + return false; } else { log.info("Removing closed session {}", entry.getKey()); hasChanged[0] = true; - return false; + return true; } - }).collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())); + }); + log.info("Active sessions info fetched: {}", this.activeSessions.keySet()); return hasChanged[0]; + } else { throw new OpenViduHttpException(statusCode); } diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/Publisher.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/Publisher.java index 89030b51..7eeefe0e 100644 --- a/openvidu-java-client/src/main/java/io/openvidu/java/client/Publisher.java +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/Publisher.java @@ -17,16 +17,15 @@ package io.openvidu.java.client; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; /** * See {@link io.openvidu.java.client.Connection#getPublishers()}. * *
    - * This is a backend representation of a published media stream (see - * OpenVidu - * Browser Stream class). + * This is a backend representation of a published media stream (see OpenVidu Browser Stream class). */ public class Publisher { @@ -40,36 +39,18 @@ public class Publisher { private String typeOfVideo; private String videoDimensions; - protected Publisher(String streamId, long createdAt, boolean hasAudio, boolean hasVideo, JsonElement audioActive, - JsonElement videoActive, JsonElement frameRate, JsonElement typeOfVideo, JsonElement videoDimensions) { - this.streamId = streamId; - this.createdAt = createdAt; - this.hasAudio = hasAudio; - this.hasVideo = hasVideo; - if (audioActive != null && !audioActive.isJsonNull()) { - this.audioActive = audioActive.getAsBoolean(); - } - if (videoActive != null && !videoActive.isJsonNull()) { - this.videoActive = videoActive.getAsBoolean(); - } - if (frameRate != null && !frameRate.isJsonNull()) { - this.frameRate = frameRate.getAsInt(); - } - if (typeOfVideo != null && !typeOfVideo.isJsonNull()) { - this.typeOfVideo = typeOfVideo.getAsString(); - } - if (videoDimensions != null && !videoDimensions.isJsonNull()) { - this.videoDimensions = videoDimensions.getAsString(); - } + protected Publisher(JsonObject json) { + this.resetWithJson(json); } /** - * Returns the unique identifier of the - * Stream associated to this Publisher. Each Publisher is paired - * with only one Stream, so you can identify each Publisher by its - * Stream.streamId + * Returns the unique identifier of the Stream associated to this Publisher. Each Publisher is + * paired with only one Stream, so you can identify each Publisher by its + * Stream.streamId */ public String getStreamId() { return streamId; @@ -84,56 +65,70 @@ public class Publisher { } /** - * See properties of Stream object in OpenVidu Browser library to find out more + * See properties of Stream object in OpenVidu Browser library to find out + * more */ public boolean hasVideo() { return this.hasVideo; } /** - * See properties of Stream object in OpenVidu Browser library to find out more + * See properties of Stream object in OpenVidu Browser library to find out + * more */ public boolean hasAudio() { return this.hasAudio; } /** - * See properties of Stream object in OpenVidu Browser library to find out more + * See properties of Stream object in OpenVidu Browser library to find out + * more */ public Boolean isAudioActive() { return this.audioActive; } /** - * See properties of Stream object in OpenVidu Browser library to find out more + * See properties of Stream object in OpenVidu Browser library to find out + * more */ public Boolean isVideoActive() { return this.videoActive; } /** - * See properties of Stream object in OpenVidu Browser library to find out more + * See properties of Stream object in OpenVidu Browser library to find out + * more */ public Integer getFrameRate() { return this.frameRate; } /** - * See properties of Stream object in OpenVidu Browser library to find out more + * See properties of Stream object in OpenVidu Browser library to find out + * more */ public String getTypeOfVideo() { return this.typeOfVideo; } /** - * See properties of Stream object in OpenVidu Browser library to find out more + * See properties of Stream object in OpenVidu Browser library to find out + * more */ public String getVideoDimensions() { return this.videoDimensions; @@ -152,4 +147,32 @@ public class Publisher { return json; } + protected Publisher resetWithJson(JsonObject json) { + this.streamId = json.get("streamId").getAsString(); + this.createdAt = json.get("createdAt").getAsLong(); + + if (json.has("mediaOptions") && !json.get("mediaOptions").isJsonNull()) { + JsonObject mediaOptions = json.get("mediaOptions").getAsJsonObject(); + this.hasAudio = mediaOptions.get("hasAudio").getAsBoolean(); + this.hasVideo = mediaOptions.get("hasVideo").getAsBoolean(); + if (mediaOptions.has("audioActive") && !mediaOptions.get("audioActive").isJsonNull()) { + this.audioActive = mediaOptions.get("audioActive").getAsBoolean(); + } + if (mediaOptions.has("videoActive") && !mediaOptions.get("videoActive").isJsonNull()) { + this.videoActive = mediaOptions.get("videoActive").getAsBoolean(); + } + if (mediaOptions.has("frameRate") && !mediaOptions.get("frameRate").isJsonNull()) { + this.frameRate = mediaOptions.get("frameRate").getAsInt(); + } + if (mediaOptions.has("typeOfVideo") && !mediaOptions.get("typeOfVideo").isJsonNull()) { + this.typeOfVideo = mediaOptions.get("typeOfVideo").getAsString(); + } + if (mediaOptions.has("videoDimensions") && !mediaOptions.get("videoDimensions").isJsonNull()) { + this.videoDimensions = mediaOptions.get("videoDimensions").getAsString(); + } + } + + return this; + } + } diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/Recording.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/Recording.java index 305201c6..b984ed8f 100644 --- a/openvidu-java-client/src/main/java/io/openvidu/java/client/Recording.java +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/Recording.java @@ -48,9 +48,8 @@ public class Recording { stopped, /** - * The recording has finished OK and is available for download through OpenVidu - * Server recordings endpoint: - * https://YOUR_OPENVIDUSERVER_IP/recordings/{RECORDING_ID}/{RECORDING_NAME}.{EXTENSION} + * The recording has finished being processed and is available for download + * through property {@link Recording#getUrl} */ ready, @@ -75,27 +74,25 @@ public class Recording { * Record each stream individually */ INDIVIDUAL, - + /** - * Works the same way as COMPOSED mode, but the necessary recorder - * service module will start some time in advance and won't be terminated - * once a specific session recording has ended. This module will remain - * up and running as long as the session remains active.

    + * Works the same way as COMPOSED mode, but the necessary recorder service + * module will start some time in advance and won't be terminated once a + * specific session recording has ended. This module will remain up and running + * as long as the session remains active.
    + *
    * *
      - *
    • - * Pros vs COMPOSED: the process of starting the recording will be noticeably - * faster. This can be very useful in use cases where a session needs to be - * recorded multiple times over time, when a better response time is usually - * desirable. - *
    • - *
    • - * Cons vs COMPOSED: for every session initialized with COMPOSED_QUICK_START - * recording output mode, extra CPU power will be required in OpenVidu Server. - * The recording module will be continuously rendering all of the streams being - * published to the session even when the session is not being recorded. And that - * is for every session configured with COMPOSED_QUICK_START. - *
    • + *
    • Pros vs COMPOSED: the process of starting the recording + * will be noticeably faster. This can be very useful in use cases where a + * session needs to be recorded multiple times over time, when a better response + * time is usually desirable.
    • + *
    • Cons vs COMPOSED: for every session initialized with + * COMPOSED_QUICK_START recording output mode, extra CPU power will be required + * in OpenVidu Server. The recording module will be continuously rendering all + * of the streams being published to the session even when the session is not + * being recorded. And that is for every session configured with + * COMPOSED_QUICK_START.
    • *
    */ COMPOSED_QUICK_START; @@ -129,7 +126,8 @@ public class Recording { OutputMode outputMode = OutputMode.valueOf(json.get("outputMode").getAsString()); RecordingProperties.Builder builder = new RecordingProperties.Builder().name(json.get("name").getAsString()) .outputMode(outputMode).hasAudio(hasAudio).hasVideo(hasVideo); - if ((OutputMode.COMPOSED.equals(outputMode) || OutputMode.COMPOSED_QUICK_START.equals(outputMode)) && hasVideo) { + if ((OutputMode.COMPOSED.equals(outputMode) || OutputMode.COMPOSED_QUICK_START.equals(outputMode)) + && hasVideo) { builder.resolution(json.get("resolution").getAsString()); builder.recordingLayout(RecordingLayout.valueOf(json.get("recordingLayout").getAsString())); JsonElement customLayout = json.get("customLayout"); @@ -219,8 +217,8 @@ public class Recording { /** * URL of the recording. You can access the file from there. It is * null until recording reaches "ready" or "failed" status. If - * OpenVidu Server configuration property + * OpenVidu Server configuration property * OPENVIDU_RECORDING_PUBLIC_ACCESS is false, this path will be * secured with OpenVidu credentials */ @@ -230,7 +228,8 @@ public class Recording { /** * Resolution of the video file. Only defined if OutputMode of the Recording is - * set to {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START} + * set to {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or + * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START} */ public String getResolution() { return this.recordingProperties.resolution(); diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/RecordingProperties.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/RecordingProperties.java index 47f526ae..a88092b1 100644 --- a/openvidu-java-client/src/main/java/io/openvidu/java/client/RecordingProperties.java +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/RecordingProperties.java @@ -17,8 +17,6 @@ package io.openvidu.java.client; -import io.openvidu.java.client.Recording.OutputMode; - /** * See * {@link io.openvidu.java.client.OpenVidu#startRecording(String, RecordingProperties)} @@ -33,6 +31,7 @@ public class RecordingProperties { private boolean hasAudio; private boolean hasVideo; private long shmSize; // For COMPOSED recording + private String mediaNode; /** * Builder for {@link io.openvidu.java.client.RecordingProperties} @@ -40,27 +39,21 @@ public class RecordingProperties { public static class Builder { private String name = ""; - private Recording.OutputMode outputMode = Recording.OutputMode.COMPOSED; + private Recording.OutputMode outputMode; private RecordingLayout recordingLayout; private String customLayout; private String resolution; private boolean hasAudio = true; private boolean hasVideo = true; private long shmSize = 536870912L; + private String mediaNode; /** * Builder for {@link io.openvidu.java.client.RecordingProperties} */ public RecordingProperties build() { - if (OutputMode.COMPOSED.equals(this.outputMode) || OutputMode.COMPOSED_QUICK_START.equals(this.outputMode)) { - this.recordingLayout = this.recordingLayout != null ? this.recordingLayout : RecordingLayout.BEST_FIT; - this.resolution = this.resolution != null ? this.resolution : "1920x1080"; - if (RecordingLayout.CUSTOM.equals(this.recordingLayout)) { - this.customLayout = this.customLayout != null ? this.customLayout : ""; - } - } return new RecordingProperties(this.name, this.outputMode, this.recordingLayout, this.customLayout, - this.resolution, this.hasAudio, this.hasVideo, this.shmSize); + this.resolution, this.hasAudio, this.hasVideo, this.shmSize, this.mediaNode); } /** @@ -102,11 +95,14 @@ public class RecordingProperties { * method to set the relative path to the specific custom layout you want to * use.
    * Will only have effect if - * {@link io.openvidu.java.client.RecordingProperties.Builder#outputMode(Recording.OutputMode)} - * has been called with value - * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or - * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}.
    - * See + * See Custom recording layouts to learn more */ public RecordingProperties.Builder customLayout(String path) { @@ -119,13 +115,15 @@ public class RecordingProperties { * format "WIDTHxHEIGHT", being both WIDTH and HEIGHT the number of pixels * between 100 and 1999.
    * Will only have effect if - * {@link io.openvidu.java.client.RecordingProperties.Builder#outputMode(Recording.OutputMode)} - * has been called with value - * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or - * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}. For - * {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL} all - * individual video files will have the native resolution of the published - * stream + * {@link io.openvidu.java.client.RecordingProperties.Builder#outputMode(Recording.OutputMode) + * Builder.outputMode()} has been called with value + * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED + * OutputMode.COMPOSED} or + * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START + * OutputMode.COMPOSED_QUICK_START}. For + * {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL + * OutputMode.INDIVIDUAL} all individual video files will have the native + * resolution of the published stream */ public RecordingProperties.Builder resolution(String resolution) { this.resolution = resolution; @@ -134,7 +132,8 @@ public class RecordingProperties { /** * Call this method to specify whether to record audio or not. Cannot be set to - * false at the same time as {@link hasVideo(boolean)} + * false at the same time as + * {@link RecordingProperties.Builder#hasVideo(boolean)} */ public RecordingProperties.Builder hasAudio(boolean hasAudio) { this.hasAudio = hasAudio; @@ -143,7 +142,8 @@ public class RecordingProperties { /** * Call this method to specify whether to record video or not. Cannot be set to - * false at the same time as {@link hasAudio(boolean)} + * false at the same time as + * {@link RecordingProperties.Builder#hasAudio(boolean)} */ public RecordingProperties.Builder hasVideo(boolean hasVideo) { this.hasVideo = hasVideo; @@ -160,10 +160,26 @@ public class RecordingProperties { return this; } + /** + * PRO Call this method to force the recording to be hosted in + * the Media Node with identifier mediaNodeId. This property only + * applies to COMPOSED recordings and is ignored for INDIVIDUAL recordings, that + * are always hosted in the same Media Node hosting its Session + */ + public RecordingProperties.Builder mediaNode(String mediaNodeId) { + this.mediaNode = mediaNodeId; + return this; + } + } protected RecordingProperties(String name, Recording.OutputMode outputMode, RecordingLayout layout, - String customLayout, String resolution, boolean hasAudio, boolean hasVideo, long shmSize) { + String customLayout, String resolution, boolean hasAudio, boolean hasVideo, long shmSize, + String mediaNode) { this.name = name; this.outputMode = outputMode; this.recordingLayout = layout; @@ -172,6 +188,7 @@ public class RecordingProperties { this.hasAudio = hasAudio; this.hasVideo = hasVideo; this.shmSize = shmSize; + this.mediaNode = mediaNode; } /** @@ -185,12 +202,12 @@ public class RecordingProperties { /** * Defines the mode of recording: {@link Recording.OutputMode#COMPOSED} or - * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START} for a - * single archive in a grid layout or {@link Recording.OutputMode#INDIVIDUAL} + * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START} for + * a single archive in a grid layout or {@link Recording.OutputMode#INDIVIDUAL} * for one archive for each stream.
    *
    * - * Default to {@link Recording.OutputMode#COMPOSED} + * Default to {@link Recording.OutputMode#COMPOSED OutputMode.COMPOSED} */ public Recording.OutputMode outputMode() { return this.outputMode; @@ -199,12 +216,14 @@ public class RecordingProperties { /** * Defines the layout to be used in the recording.
    * Will only have effect if - * {@link io.openvidu.java.client.RecordingProperties.Builder#outputMode(Recording.OutputMode)} - * has been called with value {@link Recording.OutputMode#COMPOSED} or - * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}.
    + * {@link io.openvidu.java.client.RecordingProperties.Builder#outputMode(Recording.OutputMode) + * Builder.outputMode()} has been called with value + * {@link Recording.OutputMode#COMPOSED OutputMode.COMPOSED} or + * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START + * OutputMode.COMPOSED_QUICK_START}.
    *
    * - * Default to {@link RecordingLayout#BEST_FIT} + * Default to {@link RecordingLayout#BEST_FIT RecordingLayout.BEST_FIT} */ public RecordingLayout recordingLayout() { return this.recordingLayout; @@ -214,7 +233,8 @@ public class RecordingProperties { * If {@link io.openvidu.java.client.RecordingProperties#recordingLayout()} is * set to {@link io.openvidu.java.client.RecordingLayout#CUSTOM}, this property * defines the relative path to the specific custom layout you want to use.
    - * See Custom recording layouts to learn more */ public String customLayout() { @@ -227,8 +247,8 @@ public class RecordingProperties { * {@link io.openvidu.java.client.RecordingProperties.Builder#outputMode(Recording.OutputMode)} * has been called with value * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or - * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}. For - * {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL} all + * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}. + * For {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL} all * individual video files will have the native resolution of the published * stream.
    *
    @@ -241,7 +261,7 @@ public class RecordingProperties { /** * Defines whether to record audio or not. Cannot be set to false at the same - * time as {@link hasVideo()}.
    + * time as {@link RecordingProperties#hasVideo()}.
    *
    * * Default to true @@ -252,7 +272,7 @@ public class RecordingProperties { /** * Defines whether to record video or not. Cannot be set to false at the same - * time as {@link hasAudio()}.
    + * time as {@link RecordingProperties#hasAudio()}.
    *
    * * Default to true @@ -273,4 +293,18 @@ public class RecordingProperties { return this.shmSize; } + /** + * PRO The Media Node where to host the recording. The default + * option if this property is not defined is the same Media Node hosting the + * Session to record. This property only applies to COMPOSED recordings and is + * ignored for INDIVIDUAL recordings + */ + public String mediaNode() { + return this.mediaNode; + } + } diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/Session.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/Session.java index a7fc8d56..1fbaeab4 100644 --- a/openvidu-java-client/src/main/java/io/openvidu/java/client/Session.java +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/Session.java @@ -19,9 +19,10 @@ package io.openvidu.java.client; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @@ -34,6 +35,7 @@ import org.apache.http.HttpHeaders; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPatch; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; @@ -48,7 +50,7 @@ public class Session { private long createdAt; private OpenVidu openVidu; private SessionProperties properties; - private Map activeConnections = new ConcurrentHashMap<>(); + private Map connections = new ConcurrentHashMap<>(); private boolean recording = false; protected Session(OpenVidu openVidu) throws OpenViduJavaClientException, OpenViduHttpException { @@ -66,11 +68,11 @@ public class Session { protected Session(OpenVidu openVidu, JsonObject json) { this.openVidu = openVidu; - this.resetSessionWithJson(json); + this.resetWithJson(json); } /** - * Gets the unique identifier of the Session + * Gets the unique identifier of the Session. * * @return The sessionId */ @@ -80,78 +82,49 @@ public class Session { /** * Timestamp when this session was created, in UTC milliseconds (ms since Jan 1, - * 1970, 00:00:00 UTC) + * 1970, 00:00:00 UTC). */ public long createdAt() { return this.createdAt; } /** - * Gets a new token associated to Session object with default values for - * {@link io.openvidu.java.client.TokenOptions}. This always translates into a - * new request to OpenVidu Server + * @deprecated Use {@link Session#createConnection() Session.createConnection()} + * instead to get a {@link io.openvidu.java.client.Connection} + * object. + * + * @return The generated token String * - * @return The generated token - * * @throws OpenViduJavaClientException * @throws OpenViduHttpException */ + @Deprecated public String generateToken() throws OpenViduJavaClientException, OpenViduHttpException { - return this.generateToken(new TokenOptions.Builder().role(OpenViduRole.PUBLISHER).build()); + return generateToken(new TokenOptions.Builder().data("").role(OpenViduRole.PUBLISHER).build()); } /** - * Gets a new token associated to Session object configured with - * tokenOptions. This always translates into a new request to - * OpenVidu Server + * @deprecated Use + * {@link Session#createConnection(io.openvidu.java.client.ConnectionProperties) + * Session.createConnection(ConnectionProperties)} instead to get a + * {@link io.openvidu.java.client.Connection} object. + * + * @return The generated token String * - * @return The generated token - * * @throws OpenViduJavaClientException * @throws OpenViduHttpException */ + @Deprecated public String generateToken(TokenOptions tokenOptions) throws OpenViduJavaClientException, OpenViduHttpException { - if (!this.hasSessionId()) { this.getSessionId(); } HttpPost request = new HttpPost(this.openVidu.hostname + OpenVidu.API_TOKENS); - JsonObject json = new JsonObject(); - json.addProperty("session", this.sessionId); - json.addProperty("role", tokenOptions.getRole().name()); - json.addProperty("data", tokenOptions.getData()); - if (tokenOptions.getKurentoOptions() != null) { - JsonObject kurentoOptions = new JsonObject(); - if (tokenOptions.getKurentoOptions().getVideoMaxRecvBandwidth() != null) { - kurentoOptions.addProperty("videoMaxRecvBandwidth", - tokenOptions.getKurentoOptions().getVideoMaxRecvBandwidth()); - } - if (tokenOptions.getKurentoOptions().getVideoMinRecvBandwidth() != null) { - kurentoOptions.addProperty("videoMinRecvBandwidth", - tokenOptions.getKurentoOptions().getVideoMinRecvBandwidth()); - } - if (tokenOptions.getKurentoOptions().getVideoMaxSendBandwidth() != null) { - kurentoOptions.addProperty("videoMaxSendBandwidth", - tokenOptions.getKurentoOptions().getVideoMaxSendBandwidth()); - } - if (tokenOptions.getKurentoOptions().getVideoMinSendBandwidth() != null) { - kurentoOptions.addProperty("videoMinSendBandwidth", - tokenOptions.getKurentoOptions().getVideoMinSendBandwidth()); - } - if (tokenOptions.getKurentoOptions().getAllowedFilters().length > 0) { - JsonArray allowedFilters = new JsonArray(); - for (String filter : tokenOptions.getKurentoOptions().getAllowedFilters()) { - allowedFilters.add(filter); - } - kurentoOptions.add("allowedFilters", allowedFilters); - } - json.add("kurentoOptions", kurentoOptions); - } StringEntity params; try { - params = new StringEntity(json.toString()); + params = new StringEntity(tokenOptions.toJsonObject(sessionId).toString()); } catch (UnsupportedEncodingException e1) { throw new OpenViduJavaClientException(e1.getMessage(), e1.getCause()); } @@ -180,10 +153,79 @@ public class Session { } } + /** + * Same as + * {@link io.openvidu.java.client.Session#createConnection(ConnectionProperties)} + * but with default ConnectionProperties values. + * + * @return The generated {@link io.openvidu.java.client.Connection Connection} + * object. + * + * @throws OpenViduJavaClientException + * @throws OpenViduHttpException + */ + public Connection createConnection() throws OpenViduJavaClientException, OpenViduHttpException { + return createConnection( + new ConnectionProperties.Builder().data("").role(OpenViduRole.PUBLISHER).record(true).build()); + } + + /** + * Creates a new Connection object associated to Session object and configured + * with connectionProperties. Each user connecting to the Session + * requires a Connection. The token string value to send to the client side can + * be retrieved with {@link io.openvidu.java.client.Connection#getToken() + * Connection.getToken()}. + * + * @return The generated {@link io.openvidu.java.client.Connection Connection} + * object. + * + * @throws OpenViduJavaClientException + * @throws OpenViduHttpException + */ + public Connection createConnection(ConnectionProperties connectionProperties) + throws OpenViduJavaClientException, OpenViduHttpException { + if (!this.hasSessionId()) { + this.getSessionId(); + } + + HttpPost request = new HttpPost( + this.openVidu.hostname + OpenVidu.API_SESSIONS + "/" + this.sessionId + "/connection"); + + StringEntity params; + try { + params = new StringEntity(connectionProperties.toJson(sessionId).toString()); + } catch (UnsupportedEncodingException e1) { + throw new OpenViduJavaClientException(e1.getMessage(), e1.getCause()); + } + + request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json"); + request.setEntity(params); + + HttpResponse response; + try { + response = this.openVidu.httpClient.execute(request); + } catch (IOException e2) { + throw new OpenViduJavaClientException(e2.getMessage(), e2.getCause()); + } + + try { + int statusCode = response.getStatusLine().getStatusCode(); + if ((statusCode == org.apache.http.HttpStatus.SC_OK)) { + Connection connection = new Connection(httpResponseToJson(response)); + this.connections.put(connection.getConnectionId(), connection); + return connection; + } else { + throw new OpenViduHttpException(statusCode); + } + } finally { + EntityUtils.consumeQuietly(response.getEntity()); + } + } + /** * Gracefully closes the Session: unpublishes all streams and evicts every - * participant - * + * participant. + * * @throws OpenViduJavaClientException * @throws OpenViduHttpException */ @@ -217,22 +259,25 @@ public class Session { * connections to the Session * ({@link io.openvidu.java.client.Session#getActiveConnections()}) and use * those values to call - * {@link io.openvidu.java.client.Session#forceDisconnect(Connection)} or - * {@link io.openvidu.java.client.Session#forceUnpublish(Publisher)}.
    - * - * To update every Session object owned by OpenVidu object, call - * {@link io.openvidu.java.client.OpenVidu#fetch()} - * + * {@link io.openvidu.java.client.Session#forceDisconnect(Connection)}, + * {@link io.openvidu.java.client.Session#forceUnpublish(Publisher)} or + * {@link io.openvidu.java.client.Session#updateConnection(String, ConnectionProperties)}.
    + *
    + * + * To update all Session objects owned by OpenVidu object at once, call + * {@link io.openvidu.java.client.OpenVidu#fetch()}. + * * @return true if the Session status has changed with respect to the server, * false if not. This applies to any property or sub-property of the - * object - * + * object. + * * @throws OpenViduHttpException * @throws OpenViduJavaClientException */ public boolean fetch() throws OpenViduJavaClientException, OpenViduHttpException { - String beforeJSON = this.toJson(); - HttpGet request = new HttpGet(this.openVidu.hostname + OpenVidu.API_SESSIONS + "/" + this.sessionId); + final String beforeJSON = this.toJson(); + HttpGet request = new HttpGet( + this.openVidu.hostname + OpenVidu.API_SESSIONS + "/" + this.sessionId + "?pendingConnections=true"); request.setHeader(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded"); HttpResponse response; @@ -245,8 +290,8 @@ public class Session { try { int statusCode = response.getStatusLine().getStatusCode(); if ((statusCode == org.apache.http.HttpStatus.SC_OK)) { - this.resetSessionWithJson(httpResponseToJson(response)); - String afterJSON = this.toJson(); + this.resetWithJson(httpResponseToJson(response)); + final String afterJSON = this.toJson(); boolean hasChanged = !beforeJSON.equals(afterJSON); log.info("Session info fetched for session '{}'. Any change: {}", this.sessionId, hasChanged); return hasChanged; @@ -259,16 +304,31 @@ public class Session { } /** - * Forces the user represented by connection to leave the session. - * OpenVidu Browser will trigger the proper events on the client-side - * (streamDestroyed, connectionDestroyed, + * Removes the Connection from the Session. This can translate into a forced + * eviction of a user from the Session if the Connection had status + * active, or into a token invalidation if no user had taken the + * Connection yet (status pending).
    + *
    + * + * In the first case, OpenVidu Browser will trigger the proper events on the + * client-side (streamDestroyed, connectionDestroyed, * sessionDisconnected) with reason set to - * "forceDisconnectByServer"
    - * - * You can get connection parameter with - * {@link io.openvidu.java.client.Session#fetch()} and then - * {@link io.openvidu.java.client.Session#getActiveConnections()} - * + * "forceDisconnectByServer".
    + *
    + * + * In the second case, the token of the Connection will be invalidated and no + * user will be able to connect to the session with it.
    + *
    + * + * This method automatically updates the properties of the local affected + * objects. This means that there is no need to call + * {@link io.openvidu.java.client.Session#fetch() Session.fetch()} or + * {@link io.openvidu.java.client.OpenVidu#fetch() OpenVidu.fetch()} to see the + * changes consequence of the execution of this method applied in the local + * objects. + * + * @param connection The Connection to remove + * * @throws OpenViduJavaClientException * @throws OpenViduHttpException */ @@ -277,17 +337,13 @@ public class Session { } /** - * Forces the user with Connection connectionId to leave the - * session. OpenVidu Browser will trigger the proper events on the client-side - * (streamDestroyed, connectionDestroyed, - * sessionDisconnected) with reason set to - * "forceDisconnectByServer"
    - * - * You can get connectionId parameter with - * {@link io.openvidu.java.client.Session#fetch()} (use - * {@link io.openvidu.java.client.Connection#getConnectionId()} to get the - * `connectionId` you want) - * + * Same as {@link io.openvidu.java.client.Session#forceDisconnect(Connection) + * forceDisconnect(ConnectionProperties)} but providing the + * {@link io.openvidu.java.client.Connection#getConnectionId() connectionId} + * instead of the Connection object. + * + * @param connectionId The identifier of the Connection object to remove + * * @throws OpenViduJavaClientException * @throws OpenViduHttpException */ @@ -307,13 +363,13 @@ public class Session { int statusCode = response.getStatusLine().getStatusCode(); if ((statusCode == org.apache.http.HttpStatus.SC_NO_CONTENT)) { // Remove connection from activeConnections map - Connection connectionClosed = this.activeConnections.remove(connectionId); + Connection connectionClosed = this.connections.remove(connectionId); // Remove every Publisher of the closed connection from every subscriber list of // other connections if (connectionClosed != null) { for (Publisher publisher : connectionClosed.getPublishers()) { String streamId = publisher.getStreamId(); - for (Connection connection : this.activeConnections.values()) { + for (Connection connection : this.connections.values()) { connection.setSubscribers(connection.getSubscribers().stream() .filter(subscriber -> !streamId.equals(subscriber)).collect(Collectors.toList())); } @@ -334,17 +390,28 @@ public class Session { } /** - * Forces some user to unpublish a Stream. OpenVidu Browser will trigger the - * proper events on the client-side (streamDestroyed) with reason - * set to "forceUnpublishByServer".
    - * + * Forces some Connection to unpublish a Stream. OpenVidu Browser will trigger + * the proper events in the client-side (streamDestroyed) with + * reason set to "forceUnpublishByServer".
    + *
    + * * You can get publisher parameter with * {@link io.openvidu.java.client.Session#getActiveConnections()} and then for * each Connection you can call * {@link io.openvidu.java.client.Connection#getPublishers()}. Remember to call * {@link io.openvidu.java.client.Session#fetch()} before to fetch the current - * actual properties of the Session from OpenVidu Server - * + * actual properties of the Session from OpenVidu Server.
    + *
    + * + * This method automatically updates the properties of the local affected + * objects. This means that there is no need to call + * {@link io.openvidu.java.client.Session#fetch() Session.fetch()} or + * {@link io.openvidu.java.client.OpenVidu#fetch() OpenVidu.fetch()} to see the + * changes consequence of the execution of this method applied in the local + * objects. + * + * @param publisher The Publisher object to unpublish + * * @throws OpenViduJavaClientException * @throws OpenViduHttpException */ @@ -353,19 +420,13 @@ public class Session { } /** - * Forces some user to unpublish a Stream. OpenVidu Browser will trigger the - * proper events on the client-side (streamDestroyed) with reason - * set to "forceUnpublishByServer".
    - * - * You can get streamId parameter with - * {@link io.openvidu.java.client.Session#getActiveConnections()} and then for - * each Connection you can call - * {@link io.openvidu.java.client.Connection#getPublishers()}. Finally - * {@link io.openvidu.java.client.Publisher#getStreamId()}) will give you the - * streamId. Remember to call - * {@link io.openvidu.java.client.Session#fetch()} before to fetch the current - * actual properties of the Session from OpenVidu Server - * + * Same as {@link io.openvidu.java.client.Session#forceUnpublish(Publisher) + * forceUnpublish(Publisher)} but providing the + * {@link io.openvidu.java.client.Publisher#getStreamId() streamId} instead of + * the Publisher object. + * + * @param streamId The identifier of the Publisher object to remove + * * @throws OpenViduJavaClientException * @throws OpenViduHttpException */ @@ -384,7 +445,7 @@ public class Session { try { int statusCode = response.getStatusLine().getStatusCode(); if ((statusCode == org.apache.http.HttpStatus.SC_NO_CONTENT)) { - for (Connection connection : this.activeConnections.values()) { + for (Connection connection : this.connections.values()) { // Try to remove the Publisher from the Connection publishers collection if (connection.publishers.remove(streamId) != null) { continue; @@ -402,44 +463,194 @@ public class Session { } /** - * Returns the list of active connections to the session. This value - * will remain unchanged since the last time method - * {@link io.openvidu.java.client.Session#fetch()} was called. - * Exceptions to this rule are: + * PRO Updates the properties of a Connection with a + * {@link io.openvidu.java.client.ConnectionProperties} object. Only these + * properties can be updated: *
      - *
    • Calling {@link io.openvidu.java.client.Session#forceUnpublish(String)} - * updates each affected Connection status
    • - *
    • Calling {@link io.openvidu.java.client.Session#forceDisconnect(String)} - * updates each affected Connection status
    • + *
    • {@link io.openvidu.java.client.ConnectionProperties.Builder#role(OpenViduRole) + * ConnectionProperties.Builder.role(OpenViduRole)}
    • + *
    • {@link io.openvidu.java.client.ConnectionProperties.Builder#record(boolean) + * ConnectionProperties.Builder.record(boolean)}
    • *
    *
    - * To get the list of active connections with their current actual value, you - * must call first {@link io.openvidu.java.client.Session#fetch()} and then - * {@link io.openvidu.java.client.Session#getActiveConnections()} + * + * This method automatically updates the properties of the local affected + * objects. This means that there is no need to call + * {@link io.openvidu.java.client.Session#fetch() Session.fetch()} or + * {@link io.openvidu.java.client.OpenVidu#fetch() OpenVidu.fetch()} to see the + * changes consequence of the execution of this method applied in the local + * objects.
    + *
    + * + * The affected client will trigger one ConnectionPropertyChangedEvent for each modified + * property. + * + * @param connectionId The Connection to modify + * @param connectionProperties A ConnectionProperties object with the new values + * to apply + * + * @return The updated {@link io.openvidu.java.client.Connection Connection} + * object + * + * @throws OpenViduJavaClientException + * @throws OpenViduHttpException */ - public List getActiveConnections() { - return new ArrayList<>(this.activeConnections.values()); + public Connection updateConnection(String connectionId, ConnectionProperties connectionProperties) + throws OpenViduJavaClientException, OpenViduHttpException { + + HttpPatch request = new HttpPatch( + this.openVidu.hostname + OpenVidu.API_SESSIONS + "/" + this.sessionId + "/connection/" + connectionId); + + StringEntity params; + try { + params = new StringEntity(connectionProperties.toJson(this.sessionId).toString()); + } catch (UnsupportedEncodingException e1) { + throw new OpenViduJavaClientException(e1.getMessage(), e1.getCause()); + } + request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json"); + request.setEntity(params); + + HttpResponse response; + try { + response = this.openVidu.httpClient.execute(request); + } catch (IOException e) { + throw new OpenViduJavaClientException(e.getMessage(), e.getCause()); + } + + try { + int statusCode = response.getStatusLine().getStatusCode(); + if ((statusCode == org.apache.http.HttpStatus.SC_OK)) { + log.info("Connection {} updated", connectionId); + } else if ((statusCode == org.apache.http.HttpStatus.SC_NO_CONTENT)) { + log.info("Properties of Connection {} remain the same", connectionId); + } else { + throw new OpenViduHttpException(statusCode); + } + JsonObject json = httpResponseToJson(response); + + // Update the actual Connection object with the new options + Connection existingConnection = this.connections.get(connectionId); + + if (existingConnection == null) { + // The updated Connection is not available in local map + Connection newConnection = new Connection(json); + this.connections.put(connectionId, newConnection); + return newConnection; + } else { + // The updated Connection was available in local map + existingConnection.overrideConnectionProperties(connectionProperties); + return existingConnection; + } + + } finally { + EntityUtils.consumeQuietly(response.getEntity()); + } } /** - * Returns whether the session is being recorded or not + * Returns a Connection of the Session. This method only returns the local + * available object and does not query OpenVidu Server. To get the current + * actual value you must call first + * {@link io.openvidu.java.client.Session#fetch() Session.fetch()} or + * {@link io.openvidu.java.client.OpenVidu#fetch() OpenVidu.fetch()}. + * + * @param id The Connection to get + * + * @return The {@link io.openvidu.java.client.Connection Connection} object, or + * null if no Connection is found for param id + */ + public Connection getConnection(String id) { + return this.connections.get(id); + } + + /** + * Returns all the Connections of the Session. This method only returns the + * local available objects and does not query OpenVidu Server. To get the + * current actual value you must call first + * {@link io.openvidu.java.client.Session#fetch() Session.fetch()} or + * {@link io.openvidu.java.client.OpenVidu#fetch() OpenVidu.fetch()}. + * + * The list of Connections will remain unchanged since the last time + * method {@link io.openvidu.java.client.Session#fetch() Session.fetch()} or + * {@link io.openvidu.java.client.OpenVidu#fetch() OpenVidu.fetch()} was + * called. Exceptions to this rule are: + *
      + *
    • Calling + * {@link io.openvidu.java.client.Session#createConnection(ConnectionProperties) + * createConnection(ConnectionProperties)} automatically adds the new Connection + * object to the local collection.
    • + *
    • Calling {@link io.openvidu.java.client.Session#forceUnpublish(String)} + * automatically updates each affected local Connection object.
    • + *
    • Calling {@link io.openvidu.java.client.Session#forceDisconnect(String)} + * automatically updates each affected local Connection object.
    • + *
    • Calling + * {@link io.openvidu.java.client.Session#updateConnection(String, ConnectionProperties)} + * automatically updates the attributes of the affected local Connection + * object.
    • + *
    + *
    + * To get the list of connections with their current actual value, you must call + * first {@link io.openvidu.java.client.Session#fetch() Session.fetch()} or + * {@link io.openvidu.java.client.OpenVidu#fetch() OpenVidu.fetch()}. + */ + public List getConnections() { + return this.connections.values().stream().collect(Collectors.toList()); + } + + /** + * Returns the list of active Connections of the Session. These are the + * Connections returning active in response to + * {@link io.openvidu.java.client.Connection#getStatus()}. This method only + * returns the local available objects and does not query OpenVidu Server. + * The list of active Connections will remain unchanged since the last + * time method {@link io.openvidu.java.client.Session#fetch() Session.fetch()} + * or {@link io.openvidu.java.client.OpenVidu#fetch() OpenVidu.fetch()} was + * called. Exceptions to this rule are: + *
      + *
    • Calling + * {@link io.openvidu.java.client.Session#createConnection(ConnectionProperties) + * createConnection(ConnectionProperties)} automatically adds the new Connection + * object to the local collection.
    • + *
    • Calling {@link io.openvidu.java.client.Session#forceUnpublish(String)} + * automatically updates each affected local Connection object.
    • + *
    • Calling {@link io.openvidu.java.client.Session#forceDisconnect(String)} + * automatically updates each affected local Connection object.
    • + *
    • Calling + * {@link io.openvidu.java.client.Session#updateConnection(String, ConnectionProperties)} + * automatically updates the attributes of the affected local Connection + * object.
    • + *
    + *
    + * To get the list of active connections with their current actual value, you + * must call first {@link io.openvidu.java.client.Session#fetch() + * Session.fetch()} or {@link io.openvidu.java.client.OpenVidu#fetch() + * OpenVidu.fetch()} OpenVidu.fetch()}. + */ + public List getActiveConnections() { + return this.connections.values().stream().filter(con -> "active".equals(con.getStatus())) + .collect(Collectors.toList()); + } + + /** + * Returns whether the session is being recorded or not. */ public boolean isBeingRecorded() { return this.recording; } /** - * Returns the properties defining the session + * Returns the properties defining the session. */ public SessionProperties getProperties() { return this.properties; } - @Override - public String toString() { - return this.sessionId; - } - private boolean hasSessionId() { return (this.sessionId != null && !this.sessionId.isEmpty()); } @@ -450,27 +661,9 @@ public class Session { } HttpPost request = new HttpPost(this.openVidu.hostname + OpenVidu.API_SESSIONS); - - JsonObject json = new JsonObject(); - json.addProperty("mediaMode", properties.mediaMode().name()); - json.addProperty("recordingMode", properties.recordingMode().name()); - json.addProperty("defaultOutputMode", properties.defaultOutputMode().name()); - json.addProperty("defaultRecordingLayout", properties.defaultRecordingLayout().name()); - json.addProperty("defaultCustomLayout", properties.defaultCustomLayout()); - json.addProperty("customSessionId", properties.customSessionId()); - - // forcedVideoCodec codec and allowTranscoding could be null because - // both default values are loaded by openvidu server - if (properties.forcedVideoCodec() != null) { - json.addProperty("forcedVideoCodec", properties.forcedVideoCodec().name()); - } - if (properties.isTranscodingAllowed() != null) { - json.addProperty("allowTranscoding", properties.isTranscodingAllowed()); - } - StringEntity params = null; try { - params = new StringEntity(json.toString()); + params = new StringEntity(properties.toJson().toString()); } catch (UnsupportedEncodingException e1) { throw new OpenViduJavaClientException(e1.getMessage(), e1.getCause()); } @@ -490,19 +683,24 @@ public class Session { JsonObject responseJson = httpResponseToJson(response); this.sessionId = responseJson.get("id").getAsString(); this.createdAt = responseJson.get("createdAt").getAsLong(); + + // forcedVideoCodec and allowTranscoding values are configured in OpenVidu Server + // via configuration or session VideoCodec forcedVideoCodec = VideoCodec.valueOf(responseJson.get("forcedVideoCodec").getAsString()); Boolean allowTranscoding = responseJson.get("allowTranscoding").getAsBoolean(); - + SessionProperties responseProperties = new SessionProperties.Builder() - .mediaMode(properties.mediaMode()) - .recordingMode(properties.recordingMode()) - .defaultOutputMode(properties.defaultOutputMode()) - .defaultRecordingLayout(properties.defaultRecordingLayout()) - .defaultCustomLayout(properties.defaultCustomLayout()) - .forcedVideoCodec(forcedVideoCodec) - .allowTranscoding(allowTranscoding) - .build(); - + .customSessionId(properties.customSessionId()) + .mediaMode(properties.mediaMode()) + .recordingMode(properties.recordingMode()) + .defaultOutputMode(properties.defaultOutputMode()) + .defaultRecordingLayout(properties.defaultRecordingLayout()) + .defaultCustomLayout(properties.defaultCustomLayout()) + .mediaNode(properties.mediaNode()) + .forcedVideoCodec(forcedVideoCodec) + .allowTranscoding(allowTranscoding) + .build(); + this.properties = responseProperties; log.info("Session '{}' created", this.sessionId); } else if (statusCode == org.apache.http.HttpStatus.SC_CONFLICT) { @@ -530,7 +728,7 @@ public class Session { this.recording = recording; } - protected Session resetSessionWithJson(JsonObject json) { + protected Session resetWithJson(JsonObject json) { this.sessionId = json.get("sessionId").getAsString(); this.createdAt = json.get("createdAt").getAsLong(); this.recording = json.get("recording").getAsBoolean(); @@ -544,50 +742,44 @@ public class Session { if (json.has("defaultCustomLayout")) { builder.defaultCustomLayout(json.get("defaultCustomLayout").getAsString()); } + if (json.has("customSessionId")) { + builder.customSessionId(json.get("customSessionId").getAsString()); + } + if (json.has("forcedVideoCodec")) { builder.forcedVideoCodec(VideoCodec.valueOf(json.get("forcedVideoCodec").getAsString())); } if (json.has("allowTranscoding")) { builder.allowTranscoding(json.get("allowTranscoding").getAsBoolean()); } - if (this.properties != null && this.properties.customSessionId() != null) { - builder.customSessionId(this.properties.customSessionId()); - } else if (json.has("customSessionId")) { - builder.customSessionId(json.get("customSessionId").getAsString()); - } + this.properties = builder.build(); JsonArray jsonArrayConnections = (json.get("connections").getAsJsonObject()).get("content").getAsJsonArray(); - this.activeConnections.clear(); - jsonArrayConnections.forEach(connection -> { - JsonObject con = connection.getAsJsonObject(); - Map publishers = new ConcurrentHashMap<>(); - JsonArray jsonArrayPublishers = con.get("publishers").getAsJsonArray(); - jsonArrayPublishers.forEach(publisher -> { - JsonObject pubJson = publisher.getAsJsonObject(); - JsonObject mediaOptions = pubJson.get("mediaOptions").getAsJsonObject(); - Publisher pub = new Publisher(pubJson.get("streamId").getAsString(), - pubJson.get("createdAt").getAsLong(), mediaOptions.get("hasAudio").getAsBoolean(), - mediaOptions.get("hasVideo").getAsBoolean(), mediaOptions.get("audioActive"), - mediaOptions.get("videoActive"), mediaOptions.get("frameRate"), mediaOptions.get("typeOfVideo"), - mediaOptions.get("videoDimensions")); - publishers.put(pub.getStreamId(), pub); + // 1. Set to store fetched connections and later remove closed ones + Set fetchedConnectionIds = new HashSet<>(); + jsonArrayConnections.forEach(connectionJsonElement -> { + + JsonObject connectionJson = connectionJsonElement.getAsJsonObject(); + Connection connectionObj = new Connection(connectionJson); + String id = connectionObj.getConnectionId(); + fetchedConnectionIds.add(id); + + // 2. Update existing Connection + this.connections.computeIfPresent(id, (cId, c) -> { + c = c.resetWithJson(connectionJson); + return c; }); - List subscribers = new ArrayList<>(); - JsonArray jsonArraySubscribers = con.get("subscribers").getAsJsonArray(); - jsonArraySubscribers.forEach(subscriber -> { - subscribers.add((subscriber.getAsJsonObject()).get("streamId").getAsString()); + // 3. Add new Connection + this.connections.computeIfAbsent(id, cId -> { + return connectionObj; }); - - Connection c = new Connection(con.get("connectionId").getAsString(), con.get("createdAt").getAsLong(), - OpenViduRole.valueOf(con.get("role").getAsString()), - (con.has("token") ? con.get("token").getAsString() : null), con.get("location").getAsString(), - con.get("platform").getAsString(), con.get("serverData").getAsString(), - con.get("clientData").getAsString(), publishers, subscribers); - - this.activeConnections.put(con.get("connectionId").getAsString(), c); }); + + // 4. Remove closed connections from local collection + this.connections.entrySet() + .removeIf(entry -> !fetchedConnectionIds.contains(entry.getValue().getConnectionId())); return this; } @@ -609,26 +801,10 @@ public class Session { json.addProperty("allowTranscoding", this.properties.isTranscodingAllowed()); } JsonObject connections = new JsonObject(); - connections.addProperty("numberOfElements", this.getActiveConnections().size()); + connections.addProperty("numberOfElements", this.getConnections().size()); JsonArray jsonArrayConnections = new JsonArray(); - this.getActiveConnections().forEach(con -> { - JsonObject c = new JsonObject(); - c.addProperty("connectionId", con.getConnectionId()); - c.addProperty("role", con.getRole().name()); - c.addProperty("token", con.getToken()); - c.addProperty("clientData", con.getClientData()); - c.addProperty("serverData", con.getServerData()); - JsonArray pubs = new JsonArray(); - con.getPublishers().forEach(p -> { - pubs.add(p.toJson()); - }); - JsonArray subs = new JsonArray(); - con.getSubscribers().forEach(s -> { - subs.add(s); - }); - c.add("publishers", pubs); - c.add("subscribers", subs); - jsonArrayConnections.add(c); + this.getConnections().forEach(con -> { + jsonArrayConnections.add(con.toJson()); }); connections.add("content", jsonArrayConnections); json.add("connections", connections); diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/SessionProperties.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/SessionProperties.java index ef50557f..dc1b263b 100644 --- a/openvidu-java-client/src/main/java/io/openvidu/java/client/SessionProperties.java +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/SessionProperties.java @@ -17,6 +17,8 @@ package io.openvidu.java.client; +import com.google.gson.JsonObject; + import io.openvidu.java.client.Recording.OutputMode; /** @@ -30,6 +32,7 @@ public class SessionProperties { private RecordingLayout defaultRecordingLayout; private String defaultCustomLayout; private String customSessionId; + private String mediaNode; private VideoCodec forcedVideoCodec; private Boolean allowTranscoding; @@ -44,6 +47,7 @@ public class SessionProperties { private RecordingLayout defaultRecordingLayout = RecordingLayout.BEST_FIT; private String defaultCustomLayout = ""; private String customSessionId = ""; + private String mediaNode; private VideoCodec forcedVideoCodec; private Boolean allowTranscoding; @@ -53,7 +57,7 @@ public class SessionProperties { */ public SessionProperties build() { return new SessionProperties(this.mediaMode, this.recordingMode, this.defaultOutputMode, - this.defaultRecordingLayout, this.defaultCustomLayout, this.customSessionId, + this.defaultRecordingLayout, this.defaultCustomLayout, this.customSessionId, this.mediaNode, this.forcedVideoCodec, this.allowTranscoding); } @@ -62,7 +66,7 @@ public class SessionProperties { * your clients: routed through OpenVidu Media Node * (MediaMode.ROUTED) or attempting direct p2p connections * (MediaMode.RELAYED, not available yet) - * + * * Default value is MediaMode.ROUTED */ public SessionProperties.Builder mediaMode(MediaMode mediaMode) { @@ -120,11 +124,11 @@ public class SessionProperties { * {@link io.openvidu.java.client.RecordingProperties.Builder#customLayout(String)} * with any other value.
    *
    - * + * * Custom layouts are only applicable to recordings with OutputMode - * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} (or - * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}) and - * RecordingLayout {@link io.openvidu.java.client.RecordingLayout#CUSTOM} + * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} (or + * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}) + * and RecordingLayout {@link io.openvidu.java.client.RecordingLayout#CUSTOM} */ public SessionProperties.Builder defaultCustomLayout(String path) { this.defaultCustomLayout = path; @@ -142,20 +146,55 @@ public class SessionProperties { this.customSessionId = customSessionId; return this; } - + /** * Call this method to define which video codec do you want to be forcibly used for this session. * This allows browsers/clients to use the same codec avoiding transcoding in the media server. - * If the browser/client is not compatible with the specified codec and {@link #allowTranscoding(Boolean)} + * If the browser/client is not compatible with the specified codec and {@link #allowTranscoding(Boolean)} * is false and exception will occur. - * - * If forcedVideoCodec is set to NONE, no codec will be forced. + * + * If forcedVideoCodec is set to NONE, no codec will be forced. */ public SessionProperties.Builder forcedVideoCodec(VideoCodec forcedVideoCodec) { this.forcedVideoCodec = forcedVideoCodec; return this; } - + + /** + * Call this method to define if you want to allow transcoding in the media server or not + * when {@link #forcedVideoCodec(VideoCodec)} is not compatible with the browser/client. + */ + public SessionProperties.Builder allowTranscoding(Boolean allowTranscoding) { + this.allowTranscoding = allowTranscoding; + return this; + } + + /** + * PRO Call this method to force the session to be hosted in the + * Media Node with identifier mediaNodeId + */ + public SessionProperties.Builder mediaNode(String mediaNodeId) { + this.mediaNode = mediaNodeId; + return this; + } + + /** + * Call this method to define which video codec do you want to be forcibly used for this session. + * This allows browsers/clients to use the same codec avoiding transcoding in the media server. + * If the browser/client is not compatible with the specified codec and {@link #allowTranscoding(Boolean)} + * is false and exception will occur. + * + * If forcedVideoCodec is set to NONE, no codec will be forced. + */ + public SessionProperties.Builder forcedVideoCodec(VideoCodec forcedVideoCodec) { + this.forcedVideoCodec = forcedVideoCodec; + return this; + } + /** * Call this method to define if you want to allow transcoding in the media server or not * when {@link #forcedVideoCodec(VideoCodec)} is not compatible with the browser/client. @@ -174,11 +213,11 @@ public class SessionProperties { this.defaultRecordingLayout = RecordingLayout.BEST_FIT; this.defaultCustomLayout = ""; this.customSessionId = ""; - this.allowTranscoding = false; + this.mediaNode = ""; } private SessionProperties(MediaMode mediaMode, RecordingMode recordingMode, OutputMode outputMode, - RecordingLayout layout, String defaultCustomLayout, String customSessionId, + RecordingLayout layout, String defaultCustomLayout, String customSessionId, String mediaNode, VideoCodec forcedVideoCodec, Boolean allowTranscoding) { this.mediaMode = mediaMode; this.recordingMode = recordingMode; @@ -186,6 +225,7 @@ public class SessionProperties { this.defaultRecordingLayout = layout; this.defaultCustomLayout = defaultCustomLayout; this.customSessionId = customSessionId; + this.mediaNode = mediaNode; this.forcedVideoCodec = forcedVideoCodec; this.allowTranscoding = allowTranscoding; } @@ -244,8 +284,8 @@ public class SessionProperties { * with any other value.
    * Custom layouts are only applicable to recordings with OutputMode * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} (or - * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}) and - * RecordingLayout {@link io.openvidu.java.client.RecordingLayout#CUSTOM} + * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}) + * and RecordingLayout {@link io.openvidu.java.client.RecordingLayout#CUSTOM} */ public String defaultCustomLayout() { return this.defaultCustomLayout; @@ -261,14 +301,14 @@ public class SessionProperties { public String customSessionId() { return this.customSessionId; } - + /** * Defines which video codec is being forced to be used in the browser/client */ public VideoCodec forcedVideoCodec() { return this.forcedVideoCodec; } - + /** * Defines if transcoding is allowed or not when {@link #forcedVideoCodec} * is not a compatible codec with the browser/client. @@ -277,4 +317,54 @@ public class SessionProperties { return this.allowTranscoding; } -} \ No newline at end of file + /** + * PRO The Media Node where to host the session. The default + * option if this property is not defined is the less loaded Media Node at the + * moment the first user joins the session. + */ + public String mediaNode() { + return this.mediaNode; + } + + /** + * Defines which video codec is being forced to be used in the browser/client + */ + public VideoCodec forcedVideoCodec() { + return this.forcedVideoCodec; + } + + /** + * Defines if transcoding is allowed or not when {@link #forcedVideoCodec} + * is not a compatible codec with the browser/client. + */ + public Boolean isTranscodingAllowed() { + return this.allowTranscoding; + } + + protected JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("mediaMode", mediaMode().name()); + json.addProperty("recordingMode", recordingMode().name()); + json.addProperty("defaultOutputMode", defaultOutputMode().name()); + json.addProperty("defaultRecordingLayout", defaultRecordingLayout().name()); + json.addProperty("defaultCustomLayout", defaultCustomLayout()); + json.addProperty("customSessionId", customSessionId()); + if (mediaNode() != null) { + JsonObject mediaNodeJson = new JsonObject(); + mediaNodeJson.addProperty("id", mediaNode()); + json.add("mediaNode", mediaNodeJson); + } + if (forcedVideoCodec() != null) { + json.addProperty("forcedVideoCodec", forcedVideoCodec().name()); + } + if (isTranscodingAllowed() != null) { + json.addProperty("allowTranscoding", isTranscodingAllowed()); + } + return json; + } + +} diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/TokenOptions.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/TokenOptions.java index 96c39fd6..90629781 100644 --- a/openvidu-java-client/src/main/java/io/openvidu/java/client/TokenOptions.java +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/TokenOptions.java @@ -17,31 +17,42 @@ package io.openvidu.java.client; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; + /** - * See {@link io.openvidu.java.client.Session#generateToken(TokenOptions)} + * @deprecated Use {@link io.openvidu.java.client.ConnectionProperties + * ConnectionProperties} instead */ public class TokenOptions { - private String data; private OpenViduRole role; + private String data; private KurentoOptions kurentoOptions; /** - * - * Builder for {@link io.openvidu.java.client.TokenOptions} - * + * @deprecated Use {@link io.openvidu.java.client.ConnectionProperties.Builder + * ConnectionProperties.Builder} instead */ public static class Builder { - private String data = ""; private OpenViduRole role = OpenViduRole.PUBLISHER; + private String data; private KurentoOptions kurentoOptions; /** - * Builder for {@link io.openvidu.java.client.TokenOptions} + * Builder for {@link io.openvidu.java.client.TokenOptions}. */ public TokenOptions build() { - return new TokenOptions(this.data, this.role, this.kurentoOptions); + return new TokenOptions(this.role, this.data, this.kurentoOptions); + } + + /** + * Call this method to set the role assigned to this token. + */ + public Builder role(OpenViduRole role) { + this.role = role; + return this; } /** @@ -71,17 +82,9 @@ public class TokenOptions { return this; } - /** - * Call this method to set the role assigned to this token - */ - public Builder role(OpenViduRole role) { - this.role = role; - return this; - } - /** * Call this method to set a {@link io.openvidu.java.client.KurentoOptions} - * object for this token + * object for this token. */ public Builder kurentoOptions(KurentoOptions kurentoOptions) { this.kurentoOptions = kurentoOptions; @@ -90,26 +93,26 @@ public class TokenOptions { } - private TokenOptions(String data, OpenViduRole role, KurentoOptions kurentoOptions) { - this.data = data; + TokenOptions(OpenViduRole role, String data, KurentoOptions kurentoOptions) { this.role = role; + this.data = data; this.kurentoOptions = kurentoOptions; } /** - * Returns the secure (server-side) metadata assigned to this token - */ - public String getData() { - return this.data; - } - - /** - * Returns the role assigned to this token + * Returns the role assigned to this token. */ public OpenViduRole getRole() { return this.role; } + /** + * Returns the secure (server-side) metadata assigned to this token. + */ + public String getData() { + return this.data; + } + /** * Returns the Kurento options assigned to this token */ @@ -117,4 +120,23 @@ public class TokenOptions { return this.kurentoOptions; } + protected JsonObject toJsonObject(String sessionId) { + JsonObject json = new JsonObject(); + json.addProperty("session", sessionId); + if (getRole() != null) { + json.addProperty("role", getRole().name()); + } else { + json.add("role", JsonNull.INSTANCE); + } + if (getData() != null) { + json.addProperty("data", getData()); + } else { + json.add("data", JsonNull.INSTANCE); + } + if (this.kurentoOptions != null) { + json.add("kurentoOptions", kurentoOptions.toJson()); + } + return json; + } + } diff --git a/openvidu-node-client/.gitignore b/openvidu-node-client/.gitignore index 51757794..3cdfcc6e 100644 --- a/openvidu-node-client/.gitignore +++ b/openvidu-node-client/.gitignore @@ -45,4 +45,6 @@ Thumbs.db /lib/ docs/ -lib/ \ No newline at end of file +lib/ + +*.tgz diff --git a/openvidu-node-client/config/typedoc.js b/openvidu-node-client/config/typedoc.js index 62bd81de..987eccd7 100644 --- a/openvidu-node-client/config/typedoc.js +++ b/openvidu-node-client/config/typedoc.js @@ -12,6 +12,7 @@ module.exports = { externalPattern: "node_modules", excludeExternals: true, excludePrivate: true, + excludeProtected: true, excludeNotExported: true, theme: "default", readme: "none", diff --git a/openvidu-node-client/package-lock.json b/openvidu-node-client/package-lock.json index 82fbaa40..189f5f83 100644 --- a/openvidu-node-client/package-lock.json +++ b/openvidu-node-client/package-lock.json @@ -1,39 +1,91 @@ { "name": "openvidu-node-client", - "version": "2.15.0", + "version": "2.16.0", "lockfileVersion": 1, "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", - "integrity": "sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.1" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz", - "integrity": "sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz", - "integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.1", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "@types/node": { - "version": "14.0.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.13.tgz", - "integrity": "sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA==", + "version": "14.14.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.7.tgz", + "integrity": "sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg==", "dev": true }, "abbrev": { @@ -49,12 +101,12 @@ "dev": true }, "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" } }, "anymatch": { @@ -125,12 +177,6 @@ "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", "dev": true }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true - }, "array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", @@ -161,6 +207,12 @@ "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", "dev": true }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -168,11 +220,11 @@ "dev": true }, "axios": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", - "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", "requires": { - "follow-redirects": "1.5.10" + "follow-redirects": "^1.10.0" } }, "balanced-match": { @@ -309,12 +361,12 @@ } }, "buffer": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", - "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.2.tgz", + "integrity": "sha512-XeXCUm+F7uY7fIzq4pKy+BLbZk4SgYS5xwlZOFYD3UEcAD+PwOoTaFr/SaXvhR1yRa8SKyPSZ7LNX4N65w7h8A==", "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, "buffer-from": { @@ -352,31 +404,14 @@ "unset-value": "^1.0.0" } }, - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - } - }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, "chokidar": { @@ -433,12 +468,6 @@ } } }, - "coffeescript": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.10.0.tgz", - "integrity": "sha1-56qDAZF+9iGzXYo580jc3R234z4=", - "dev": true - }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -450,18 +479,18 @@ } }, "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "color-name": "1.1.3" + "color-name": "~1.1.4" } }, "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "colors": { @@ -507,12 +536,16 @@ "dev": true }, "cross-spawn": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-0.2.9.tgz", - "integrity": "sha1-vWf5bAfvtjA7f+lMHpefiEeOCjk=", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "dev": true, "requires": { - "lru-cache": "^2.5.0" + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" } }, "csproj2ts": { @@ -535,48 +568,27 @@ } } }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "requires": { - "array-find-index": "^1.0.1" - } - }, "dargs": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", - "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-6.1.0.tgz", + "integrity": "sha512-5dVBvpBLBnPwSsYXqfybFyehMmC/EenKEcf23AhCTgTf48JFBbmJKqoZBsERDnjL0FyiVTYWdFsRfTLHxLyKdQ==", + "dev": true }, "dateformat": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", - "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1", - "meow": "^3.3.0" - } + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true }, "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "requires": { "ms": "2.0.0" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -652,9 +664,9 @@ "dev": true }, "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, "error": { @@ -666,15 +678,6 @@ "string-template": "~0.2.1" } }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, "es6-promise": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-0.1.2.tgz", @@ -720,15 +723,6 @@ "to-regex": "^3.0.1" }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", @@ -905,16 +899,6 @@ } } }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, "findup-sync": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", @@ -959,12 +943,9 @@ "dev": true }, "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" - } + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", + "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==" }, "for-in": { "version": "1.0.2", @@ -991,14 +972,15 @@ } }, "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", "dev": true, "requires": { + "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" } }, "fs.realpath": { @@ -1018,6 +1000,12 @@ "nan": "^2.12.1" } }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, "gaze": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", @@ -1027,12 +1015,6 @@ "globule": "^1.0.0" } }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -1046,15 +1028,15 @@ "dev": true }, "glob": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", - "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.2", + "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } @@ -1102,22 +1084,6 @@ "glob": "~7.1.1", "lodash": "~4.17.10", "minimatch": "~3.0.2" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } } }, "graceful-fs": { @@ -1127,48 +1093,26 @@ "dev": true }, "grunt": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.1.0.tgz", - "integrity": "sha512-+NGod0grmviZ7Nzdi9am7vuRS/h76PcWDsV635mEXF0PEQMUV6Kb+OjTdsVxbi0PZmfQOjCMKb3w8CVZcqsn1g==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.3.0.tgz", + "integrity": "sha512-6ILlMXv11/4cxuhSMfSU+SfvbxrPuqZrAtLN64+tZpQ3DAKfSQPQHRbTjSbdtxfyQhGZPtN0bDZJ/LdCM5WXXA==", "dev": true, "requires": { - "coffeescript": "~1.10.0", - "dateformat": "~1.0.12", + "dateformat": "~3.0.3", "eventemitter2": "~0.4.13", - "exit": "~0.1.1", + "exit": "~0.1.2", "findup-sync": "~0.3.0", - "glob": "~7.0.0", - "grunt-cli": "~1.2.0", + "glob": "~7.1.6", + "grunt-cli": "~1.3.2", "grunt-known-options": "~1.1.0", - "grunt-legacy-log": "~2.0.0", - "grunt-legacy-util": "~1.1.1", + "grunt-legacy-log": "~3.0.0", + "grunt-legacy-util": "~2.0.0", "iconv-lite": "~0.4.13", - "js-yaml": "~3.13.1", - "minimatch": "~3.0.2", - "mkdirp": "~1.0.3", + "js-yaml": "~3.14.0", + "minimatch": "~3.0.4", + "mkdirp": "~1.0.4", "nopt": "~3.0.6", - "path-is-absolute": "~1.0.0", - "rimraf": "~2.6.2" - }, - "dependencies": { - "grunt-cli": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz", - "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=", - "dev": true, - "requires": { - "findup-sync": "~0.3.0", - "grunt-known-options": "~1.1.0", - "nopt": "~3.0.6", - "resolve": "~1.1.0" - } - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - } + "rimraf": "~3.0.2" } }, "grunt-cli": { @@ -1234,61 +1178,141 @@ } }, "grunt-contrib-sass": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-sass/-/grunt-contrib-sass-1.0.0.tgz", - "integrity": "sha1-gGg4JRy8DhqU1k1RXN00z2dNcBs=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-sass/-/grunt-contrib-sass-2.0.0.tgz", + "integrity": "sha512-RxZ3dlZZTX4YBPu2zMu84NPYgJ2AYAlIdEqlBaixNVyLNbgvJBGUr5Gi0ec6IiOQbt/I/z7uZVN9HsRxgznIRw==", "dev": true, "requires": { - "async": "^0.9.0", - "chalk": "^1.0.0", - "cross-spawn": "^0.2.3", - "dargs": "^4.0.0", - "which": "^1.0.5" + "async": "^2.6.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "dargs": "^6.0.0", + "which": "^1.3.1" }, "dependencies": { "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "color-convert": "^1.9.0" } }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } } } }, "grunt-contrib-uglify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-4.0.1.tgz", - "integrity": "sha512-dwf8/+4uW1+7pH72WButOEnzErPGmtUvc8p08B0eQS/6ON0WdeQu0+WFeafaPTbbY1GqtS25lsHWaDeiTQNWPg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-5.0.0.tgz", + "integrity": "sha512-rIFFPJMWKnh6oxDe2b810Ysg5SKoiI0u/FvuvAVpvJ7VHILkKtGqA4jgJ1JWruWQ+1m5FtB1lVSK81YyzIgDUw==", "dev": true, "requires": { "chalk": "^2.4.1", "maxmin": "^2.1.0", "uglify-js": "^3.5.0", "uri-path": "^1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "grunt-contrib-watch": { @@ -1321,39 +1345,39 @@ "dev": true }, "grunt-legacy-log": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-2.0.0.tgz", - "integrity": "sha512-1m3+5QvDYfR1ltr8hjiaiNjddxGdQWcH0rw1iKKiQnF0+xtgTazirSTGu68RchPyh1OBng1bBUjLmX8q9NpoCw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz", + "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==", "dev": true, "requires": { "colors": "~1.1.2", - "grunt-legacy-log-utils": "~2.0.0", + "grunt-legacy-log-utils": "~2.1.0", "hooker": "~0.2.3", - "lodash": "~4.17.5" + "lodash": "~4.17.19" } }, "grunt-legacy-log-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.0.1.tgz", - "integrity": "sha512-o7uHyO/J+i2tXG8r2bZNlVk20vlIFJ9IEYyHMCQGfWYru8Jv3wTqKZzvV30YW9rWEjq0eP3cflQ1qWojIe9VFA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz", + "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==", "dev": true, "requires": { - "chalk": "~2.4.1", - "lodash": "~4.17.10" + "chalk": "~4.1.0", + "lodash": "~4.17.19" } }, "grunt-legacy-util": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-1.1.1.tgz", - "integrity": "sha512-9zyA29w/fBe6BIfjGENndwoe1Uy31BIXxTH3s8mga0Z5Bz2Sp4UCjkeyv2tI449ymkx3x26B+46FV4fXEddl5A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.0.tgz", + "integrity": "sha512-ZEmYFB44bblwPE2oz3q3ygfF6hseQja9tx8I3UZIwbUik32FMWewA+d1qSFicMFB+8dNXDkh35HcDCWlpRsGlA==", "dev": true, "requires": { "async": "~1.5.2", "exit": "~0.1.1", "getobject": "~0.1.0", "hooker": "~0.2.3", - "lodash": "~4.17.10", - "underscore.string": "~3.3.4", + "lodash": "~4.17.20", + "underscore.string": "~3.3.5", "which": "~1.3.0" } }, @@ -1366,6 +1390,58 @@ "chalk": "^2.1.0", "diff": "^3.0.0", "postcss": "^6.0.11" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "grunt-string-replace": { @@ -1471,6 +1547,15 @@ } } }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -1481,9 +1566,9 @@ } }, "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, "has-value": { @@ -1519,9 +1604,9 @@ } }, "highlight.js": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.0.3.tgz", - "integrity": "sha512-9FG7SSzv9yOY5CGGxfI6NDm7xLYtMOjKtPBxw7Zff3t5UcRcUNTGEeS8lNjhceL34KeetLMoGMFTGoaa83HwyQ==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.3.2.tgz", + "integrity": "sha512-3jRT7OUYsVsKvukNKZCtnvRcFyCJqSEIuIMsEybAXRiFSwpt65qjPd/Pr+UOdYt7WJlt+lj3+ypUsHiySBp/Jw==", "dev": true }, "homedir-polyfill": { @@ -1539,12 +1624,6 @@ "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", "dev": true }, - "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", - "dev": true - }, "http-parser-js": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz", @@ -1561,18 +1640,9 @@ } }, "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "inflight": { "version": "1.0.6", @@ -1632,12 +1702,6 @@ } } }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, "is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", @@ -1653,6 +1717,15 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, + "is-core-module": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz", + "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -1803,9 +1876,9 @@ "dev": true }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -1819,12 +1892,21 @@ "dev": true }, "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "requires": { - "graceful-fs": "^4.1.6" + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + }, + "dependencies": { + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } } }, "kind-of": { @@ -1869,45 +1951,16 @@ "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==", "dev": true }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "lru-cache": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "lunr": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.8.tgz", - "integrity": "sha512-oxMeX/Y35PNFuZoHp+jUj5OSEmLCaIH4KTFJh7a93cHBoFmpw2IoPs22VIz7vyO2YUnx2Tn9dzIwO2P/4quIRg==", + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", "dev": true }, "make-error": { @@ -1931,12 +1984,6 @@ "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", "dev": true }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", @@ -1947,9 +1994,9 @@ } }, "marked": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-1.0.0.tgz", - "integrity": "sha512-Wo+L1pWTVibfrSr+TTtMuiMfNzmZWiOPeO7rZsQUY5bgsxpHesBEcIWJloWVTFnrMXnf/TL30eTFSGJddmQAng==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-1.2.3.tgz", + "integrity": "sha512-RQuL2i6I6Gn+9n81IDNGbL0VHnta4a+8ZhqvryXEniTb/hQNtf3i26hi1XWUhzb9BgVyWHKR3UO8MaHtKoYibw==", "dev": true }, "maxmin": { @@ -1991,24 +2038,6 @@ } } }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - } - }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -2075,12 +2104,13 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "nan": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", - "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", "dev": true, "optional": true }, @@ -2110,9 +2140,15 @@ "dev": true }, "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, "nopt": { @@ -2124,18 +2160,6 @@ "abbrev": "1" } }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2267,15 +2291,6 @@ "path-root": "^0.1.1" } }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, "parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", @@ -2294,21 +2309,18 @@ "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", "dev": true }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", @@ -2330,38 +2342,6 @@ "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", "dev": true }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -2379,11 +2359,61 @@ "supports-color": "^5.4.0" }, "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } } } }, @@ -2424,27 +2454,6 @@ "string_decoder": "0.10" } }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -2497,16 +2506,6 @@ "resolve": "^1.1.6" } }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - } - }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -2545,11 +2544,12 @@ } }, "resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz", + "integrity": "sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==", "dev": true, "requires": { + "is-core-module": "^2.0.0", "path-parse": "^1.0.6" } }, @@ -2576,28 +2576,12 @@ "dev": true }, "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } } }, "safe-buffer": { @@ -2662,6 +2646,21 @@ } } }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, "shelljs": { "version": "0.8.4", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", @@ -2673,12 +2672,6 @@ "rechoir": "^0.6.2" } }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -2695,15 +2688,6 @@ "use": "^3.1.0" }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", @@ -2838,38 +2822,6 @@ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", "dev": true }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -2936,22 +2888,13 @@ "is-utf8": "^0.2.0" } }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1" - } - }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" } }, "tiny-lr": { @@ -2966,6 +2909,23 @@ "livereload-js": "^2.3.0", "object-assign": "^4.1.0", "qs": "^6.4.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } } }, "to-object-path": { @@ -3010,16 +2970,10 @@ "repeat-string": "^1.6.1" } }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true - }, "ts-node": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", - "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz", + "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==", "dev": true, "requires": { "arg": "^4.1.0", @@ -3038,15 +2992,15 @@ } }, "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, "tslint": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.1.tgz", - "integrity": "sha512-kd6AQ/IgPRpLn6g5TozqzPdGNZ0q0jtXW4//hRcj10qLYBaa3mTUU2y2MCG+RXZm8Zx+KZi0eA+YCrMyNlF4UA==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", + "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -3060,29 +3014,56 @@ "mkdirp": "^0.5.3", "resolve": "^1.3.2", "semver": "^5.3.0", - "tslib": "^1.10.0", + "tslib": "^1.13.0", "tsutils": "^2.29.0" }, "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true }, "mkdirp": { "version": "0.5.5", @@ -3092,6 +3073,15 @@ "requires": { "minimist": "^1.2.5" } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } } } }, @@ -3105,31 +3095,37 @@ } }, "typedoc": { - "version": "0.17.7", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.17.7.tgz", - "integrity": "sha512-PEnzjwQAGjb0O8a6VDE0lxyLAadqNujN5LltsTUhZETolRMiIJv6Ox+Toa8h0XhKHqAOh8MOmB0eBVcWz6nuAw==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.19.2.tgz", + "integrity": "sha512-oDEg1BLEzi1qvgdQXc658EYgJ5qJLVSeZ0hQ57Eq4JXy6Vj2VX4RVo18qYxRWz75ifAaYuYNBUCnbhjd37TfOg==", "dev": true, "requires": { - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "handlebars": "^4.7.6", - "highlight.js": "^10.0.0", - "lodash": "^4.17.15", - "lunr": "^2.3.8", - "marked": "1.0.0", + "highlight.js": "^10.2.0", + "lodash": "^4.17.20", + "lunr": "^2.3.9", + "marked": "^1.1.1", "minimatch": "^3.0.0", "progress": "^2.0.3", + "semver": "^7.3.2", "shelljs": "^0.8.4", - "typedoc-default-themes": "^0.10.1" + "typedoc-default-themes": "^0.11.4" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } } }, "typedoc-default-themes": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.10.1.tgz", - "integrity": "sha512-SuqAQI0CkwhqSJ2kaVTgl37cWs733uy9UGUqwtcds8pkFK8oRF4rZmCq+FXTGIb9hIUOu40rf5Kojg0Ha6akeg==", - "dev": true, - "requires": { - "lunr": "^2.3.8" - } + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.11.4.tgz", + "integrity": "sha512-Y4Lf+qIb9NTydrexlazAM46SSLrmrQRqWiD52593g53SsmUFioAsMWt8m834J6qsp+7wHRjxCXSZeiiW5cMUdw==", + "dev": true }, "typescript": { "version": "3.8.3", @@ -3138,13 +3134,10 @@ "dev": true }, "uglify-js": { - "version": "3.9.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.9.4.tgz", - "integrity": "sha512-8RZBJq5smLOa7KslsNsVcSH+KOXf1uDU8yqLeNuVKwmT0T3FA0ZoXlinQfRad7SDcbZZRZE4ov+2v71EnxNyCA==", - "dev": true, - "requires": { - "commander": "~2.20.3" - } + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.11.5.tgz", + "integrity": "sha512-btvv/baMqe7HxP7zJSF7Uc16h1mSfuuSplT0/qdjxseesDU+yYzH33eHBH+eMdeRXwujXspaCTooWHQVVBh09w==", + "dev": true }, "unc-path-regex": { "version": "0.1.2", @@ -3175,9 +3168,9 @@ } }, "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", "dev": true }, "unset-value": { @@ -3259,16 +3252,6 @@ "homedir-polyfill": "^1.0.1" } }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", diff --git a/openvidu-node-client/package.json b/openvidu-node-client/package.json index 9958fe5a..40ea4b8e 100644 --- a/openvidu-node-client/package.json +++ b/openvidu-node-client/package.json @@ -1,24 +1,24 @@ { "author": "OpenVidu", "dependencies": { - "axios": "0.19.2", - "buffer": "5.6.0" + "axios": "0.21.1", + "buffer": "6.0.2" }, "description": "OpenVidu Node Client", "devDependencies": { - "@types/node": "14.0.13", - "grunt": "1.1.0", + "@types/node": "14.14.7", + "grunt": "1.3.0", "grunt-cli": "1.3.2", "grunt-contrib-copy": "1.0.0", - "grunt-contrib-sass": "1.0.0", - "grunt-contrib-uglify": "4.0.1", + "grunt-contrib-sass": "2.0.0", + "grunt-contrib-uglify": "5.0.0", "grunt-contrib-watch": "1.1.0", "grunt-postcss": "0.9.0", "grunt-string-replace": "1.3.1", "grunt-ts": "6.0.0-beta.22", - "ts-node": "8.10.2", - "tslint": "6.1.1", - "typedoc": "0.17.7", + "ts-node": "9.0.0", + "tslint": "6.1.3", + "typedoc": "0.19.2", "typescript": "3.8.3" }, "license": "Apache-2.0", @@ -33,5 +33,5 @@ "docs": "./generate-docs.sh" }, "typings": "lib/index.d.ts", - "version": "2.15.0" + "version": "2.16.0" } diff --git a/openvidu-node-client/src/Connection.ts b/openvidu-node-client/src/Connection.ts index 836eaa81..88e4263d 100644 --- a/openvidu-node-client/src/Connection.ts +++ b/openvidu-node-client/src/Connection.ts @@ -15,37 +15,47 @@ * */ -import { OpenViduRole } from './OpenViduRole'; import { Publisher } from './Publisher'; +import { ConnectionProperties } from './ConnectionProperties'; +import { OpenViduRole } from './OpenViduRole'; /** - * See [[Session.activeConnections]] + * See [[Session.connections]] */ export class Connection { /** - * Identifier of the connection. You can call [[Session.forceDisconnect]] passing this property as parameter + * Identifier of the Connection. You can call methods [[Session.forceDisconnect]] + * or [[Session.updateConnection]] passing this property as parameter */ connectionId: string; /** - * Timestamp when this connection was established, in UTC milliseconds (ms since Jan 1, 1970, 00:00:00 UTC) + * Returns the status of the Connection. Can be: + * - `pending`: if the Connection is waiting for any user to use + * its internal token to connect to the session, calling method + * [Session.connect](https://docs.openvidu.io/en/stable/api/openvidu-browser/classes/session.html#connect) + * in OpenVidu Browser. + * - `active`: if the internal token of the Connection has already + * been used by some user to connect to the session, and it cannot be used + * again. + */ + status: string; + + /** + * Timestamp when the Connection was created, in UTC milliseconds (ms since Jan 1, 1970, 00:00:00 UTC) */ createdAt: number; /** - * Role of the connection + * Timestamp when the Connection was taken by a user (passing from status "pending" to "active") + * in UTC milliseconds (ms since Jan 1, 1970, 00:00:00 UTC) */ - role: OpenViduRole; - - /** - * Token associated to the connection - */ - token: string; + activeAt: number; /** * PRO - * Geo location of the connection, with the following format: `"CITY, COUNTRY"` (`"unknown"` if it wasn't possible to locate it) + * Geo location of the Connection, with the following format: `"CITY, COUNTRY"` (`"unknown"` if it wasn't possible to locate it) */ location: string; @@ -55,16 +65,22 @@ export class Connection { platform: string; /** - * Data associated to the connection on the server-side. This value is set with property [[TokenOptions.data]] when calling [[Session.generateToken]] - */ - serverData: string; - - /** - * Data associated to the connection on the client-side. This value is set with second parameter of method + * Data associated to the Connection on the client-side. This value is set with second parameter of method * [Session.connect](/en/stable/api/openvidu-browser/classes/session.html#connect) in OpenVidu Browser */ clientData: string; + /** + * The [[ConnectionProperties]] assigned to the Connection + */ + connectionProperties: ConnectionProperties; + + /** + * Token associated to the Connection. This is the value that must be sent to the client-side to be consumed in OpenVidu Browser + * method [Session.connect](https://docs.openvidu.io/en/stable/api/openvidu-browser/classes/session.html#connect). + */ + token: string; + /** * Array of Publisher objects this particular Connection is publishing to the Session (each Publisher object has one Stream, uniquely * identified by its `streamId`). You can call [[Session.forceUnpublish]] passing any of this values as parameter @@ -77,21 +93,112 @@ export class Connection { */ subscribers: string[] = []; + /** + * @hidden deprecated. Inside ConnectionProperties + */ + role?: OpenViduRole; + /** + * @hidden deprecated. Inside ConnectionProperties + */ + serverData?: string; + /** * @hidden */ - constructor(connectionId: string, createdAt: number, role: OpenViduRole, token: string, location: string, platform: string, serverData: string, clientData: string, - publishers: Publisher[], subscribers: string[]) { - this.connectionId = connectionId; - this.createdAt = createdAt; - this.role = role; - this.token = token; - this.location = location; - this.platform = platform; - this.serverData = serverData; - this.clientData = clientData; - this.publishers = publishers; - this.subscribers = subscribers; + constructor(json) { + this.resetWithJson(json); + } + + /** + * @hidden + */ + resetWithJson(json): Connection { + + this.connectionId = json.connectionId; + this.status = json.status; + this.createdAt = json.createdAt; + this.activeAt = json.activeAt; + this.location = json.location; + this.platform = json.platform; + this.clientData = json.clientData; + this.token = json.token; + if (this.connectionProperties != null) { + this.connectionProperties.type = json.type; + this.connectionProperties.data = json.serverData; + this.connectionProperties.record = json.record; + this.connectionProperties.role = json.role; + this.connectionProperties.kurentoOptions = json.kurentoOptions; + this.connectionProperties.rtspUri = json.rtspUri; + this.connectionProperties.adaptativeBitrate = json.adaptativeBitrate; + this.connectionProperties.onlyPlayWithSubscribers = json.onlyPlayWithSubscribers; + this.connectionProperties.networkCache = json.networkCache; + } else { + this.connectionProperties = { + type: json.type, + data: json.serverData, + record: json.record, + role: json.role, + kurentoOptions: json.kurentoOptions, + rtspUri: json.rtspUri, + adaptativeBitrate: json.adaptativeBitrate, + onlyPlayWithSubscribers: json.onlyPlayWithSubscribers, + networkCache: json.networkCache + } + } + this.role = json.role; + this.serverData = json.serverData; + + // publishers may be null + if (json.publishers != null) { + + // 1. Array to store fetched Publishers and later remove closed ones + const fetchedPublisherIds: string[] = []; + json.publishers.forEach(jsonPublisher => { + + const publisherObj: Publisher = new Publisher(jsonPublisher); + fetchedPublisherIds.push(publisherObj.streamId); + let storedPublisher = this.publishers.find(c => c.streamId === publisherObj.streamId); + + if (!!storedPublisher) { + // 2. Update existing Publisher + storedPublisher.resetWithJson(jsonPublisher); + } else { + // 3. Add new Publisher + this.publishers.push(publisherObj); + } + }); + + // 4. Remove closed Publishers from local collection + for (var i = this.publishers.length - 1; i >= 0; --i) { + if (!fetchedPublisherIds.includes(this.publishers[i].streamId)) { + this.publishers.splice(i, 1); + } + } + + } + + // subscribers may be null + if (json.subscribers != null) { + + // 1. Array to store fetched Subscribers and later remove closed ones + const fetchedSubscriberIds: string[] = []; + json.subscribers.forEach(jsonSubscriber => { + fetchedSubscriberIds.push(jsonSubscriber.streamId) + if (this.subscribers.indexOf(jsonSubscriber.streamId) === -1) { + // 2. Add new Subscriber + this.subscribers.push(jsonSubscriber.streamId); + } + }); + + // 3. Remove closed Subscribers from local collection + for (var i = this.subscribers.length - 1; i >= 0; --i) { + if (!fetchedSubscriberIds.includes(this.subscribers[i])) { + this.subscribers.splice(i, 1); + } + } + } + + return this; } /** @@ -100,17 +207,32 @@ export class Connection { equalTo(other: Connection): boolean { let equals: boolean = ( this.connectionId === other.connectionId && + this.status === other.status && this.createdAt === other.createdAt && - this.role === other.role && + this.activeAt === other.activeAt && + this.connectionProperties.type === other.connectionProperties.type && + this.connectionProperties.data === other.connectionProperties.data && + this.connectionProperties.record === other.connectionProperties.record && + this.connectionProperties.role === other.connectionProperties.role && + this.connectionProperties.rtspUri === other.connectionProperties.rtspUri && + this.connectionProperties.adaptativeBitrate === other.connectionProperties.adaptativeBitrate && + this.connectionProperties.onlyPlayWithSubscribers === other.connectionProperties.onlyPlayWithSubscribers && + this.connectionProperties.networkCache === other.connectionProperties.networkCache && this.token === other.token && this.location === other.location && this.platform === other.platform && - this.serverData === other.serverData && this.clientData === other.clientData && this.subscribers.length === other.subscribers.length && this.publishers.length === other.publishers.length); if (equals) { - equals = JSON.stringify(this.subscribers) === JSON.stringify(other.subscribers); + if (this.connectionProperties.kurentoOptions != null) { + equals = JSON.stringify(this.connectionProperties.kurentoOptions) === JSON.stringify(other.connectionProperties.kurentoOptions); + } else { + equals = (this.connectionProperties.kurentoOptions === other.connectionProperties.kurentoOptions); + } + } + if (equals) { + equals = JSON.stringify(this.subscribers.sort()) === JSON.stringify(other.subscribers.sort()); if (equals) { let i = 0; while (equals && i < this.publishers.length) { @@ -125,4 +247,18 @@ export class Connection { return false; } } + + /** + * @hidden + */ + overrideConnectionProperties(newConnectionProperties: ConnectionProperties): void { + // For now only properties record and role + if (newConnectionProperties.record != null) { + this.connectionProperties.record = newConnectionProperties.record; + } + if (newConnectionProperties.role != null) { + this.connectionProperties.role = newConnectionProperties.role; + } + } + } \ No newline at end of file diff --git a/openvidu-node-client/src/ConnectionProperties.ts b/openvidu-node-client/src/ConnectionProperties.ts new file mode 100644 index 00000000..ca88e962 --- /dev/null +++ b/openvidu-node-client/src/ConnectionProperties.ts @@ -0,0 +1,130 @@ +/* + * (C) Copyright 2017-2020 OpenVidu (https://openvidu.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { ConnectionType } from './ConnectionType'; +import { OpenViduRole } from './OpenViduRole'; + +export interface ConnectionProperties { + + /** + * Type of Connection. The [[ConnectionType]] dictates what properties will have effect: + * + * - **[[ConnectionType.WEBRTC]]**: [[data]], [[record]], [[role]], [[kurentoOptions]] + * - **[[ConnectionType.IPCAM]]**: [[data]], [[record]], [[rtspUri]], [[adaptativeBitrate]], [[onlyPlayWithSubscribers]], [[networkCache]] + * + * @default WEBRTC + */ + type?: ConnectionType; + + /** + * Secure (server-side) data associated to this Connection. Every client will receive this data in property `Connection.data`. Object `Connection` can be retrieved by subscribing to event `connectionCreated` of Session object. + * - If you have provided no data in your clients when calling method `Session.connect(TOKEN, DATA)` (`DATA` not defined), then `Connection.data` will only have this [[ConnectionProperties.data]] property. + * - If you have provided some data when calling `Session.connect(TOKEN, DATA)` (`DATA` defined), then `Connection.data` will have the following structure: `"CLIENT_DATA%/%SERVER_DATA"`, being `CLIENT_DATA` the second + * parameter passed in OpenVidu Browser in method `Session.connect` and `SERVER_DATA` this [[ConnectionProperties.data]] property. + */ + data?: string; + + /** + * **This feature is part of OpenVidu Pro tier** PRO + * + * Whether to record the streams published by this Connection or not. This only affects [INDIVIDUAL recording](/en/stable/advanced-features/recording#selecting-streams-to-be-recorded) + * + * @default true + */ + record?: boolean; + + /** + * The role assigned to this Connection + * + * **Only for [[ConnectionType.WEBRTC]]** + * + * @default PUBLISHER + */ + role?: OpenViduRole; + + /** + * **WARNING**: experimental option. This interface may change in the near future + * + * Some advanced properties setting the configuration that the WebRTC streams of the Connection will have in Kurento Media Server. + * You can adjust: + * - `videoMaxRecvBandwidth`: maximum number of Kbps that the Connection will be able to receive from Kurento Media Server. 0 means unconstrained. Giving a value to this property will override + * the global configuration set in [OpenVidu Server configuration](/en/stable/reference-docs/openvidu-config/) + * (parameter `OPENVIDU_STREAMS_VIDEO_MAX_RECV_BANDWIDTH`) for every incoming stream of the Connection. + * _**WARNING**: the lower value set to this property limits every other bandwidth of the WebRTC pipeline this server-to-client stream belongs to. This includes the user publishing the stream and every other user subscribed to the stream_ + * - `videoMinRecvBandwidth`: minimum number of Kbps that the cConnection will try to receive from Kurento Media Server. 0 means unconstrained. Giving a value to this property will override + * the global configuration set in [OpenVidu Server configuration](/en/stable/reference-docs/openvidu-config/) + * (parameter `OPENVIDU_STREAMS_VIDEO_MIN_RECV_BANDWIDTH`) for every incoming stream of the Connection. + * - `videoMaxSendBandwidth`: maximum number of Kbps that the Connection will be able to send to Kurento Media Server. 0 means unconstrained. Giving a value to this property will override + * the global configuration set in [OpenVidu Server configuration](/en/stable/reference-docs/openvidu-config/) + * (parameter `OPENVIDU_STREAMS_VIDEO_MAX_SEND_BANDWIDTH`) for every outgoing stream of the Connection. + * _**WARNING**: this value limits every other bandwidth of the WebRTC pipeline this client-to-server stream belongs to. This includes every other user subscribed to the stream_ + * - `videoMinSendBandwidth`: minimum number of Kbps that the Connection will try to send to Kurento Media Server. 0 means unconstrained. Giving a value to this property will override + * the global configuration set in [OpenVidu Server configuration](/en/stable/reference-docs/openvidu-config/) + * (parameter `OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH`) for every outgoing stream of the Connection. + * - `allowedFilters`: names of the filters the Connection will be able to apply. See [Voice and video filters](/en/stable/advanced-features/filters/) + * + * **Only for [[ConnectionType.WEBRTC]]** + */ + kurentoOptions?: { + videoMaxRecvBandwidth?: number, + videoMinRecvBandwidth?: number, + videoMaxSendBandwidth?: number, + videoMinSendBandwidth?: number, + allowedFilters?: string[] + }; + + /** + * RTSP URI of an IP camera. For example: `rtsp://your.camera.ip:7777/path` + * + * **Only for [[ConnectionType.IPCAM]]** + */ + rtspUri?: string; + + /** + * Whether to use adaptative bitrate (and therefore adaptative quality) or not. For local network connections + * that do not require media transcoding this can be disabled to save CPU power. If you are not sure if transcoding + * might be necessary, setting this property to false **may result in media connections not being established**. + * + * **Only for [[ConnectionType.IPCAM]]** + * + * @default true + */ + adaptativeBitrate?: boolean; + + /** + * Whether to enable the IP camera stream only when some user is subscribed to it, or not. This allows you to reduce + * power consumption and network bandwidth in your server while nobody is asking to receive the camera's video. + * On the counterpart, first user subscribing to the IP camera stream will take a little longer to receive its video. + * + * **Only for [[ConnectionType.IPCAM]]** + * + * @default true + */ + onlyPlayWithSubscribers?: boolean; + + /** + * Size of the buffer of the endpoint receiving the IP camera's stream, in milliseconds. The smaller it is, the less + * delay the signal will have, but more problematic will be in unstable networks. Use short buffers only if there is + * a quality connection between the IP camera and OpenVidu Server. + * + * **Only for [[ConnectionType.IPCAM]]** + * + * @default 2000 + */ + networkCache?: number; + +} \ No newline at end of file diff --git a/openvidu-node-client/src/ConnectionType.ts b/openvidu-node-client/src/ConnectionType.ts new file mode 100644 index 00000000..29268f4b --- /dev/null +++ b/openvidu-node-client/src/ConnectionType.ts @@ -0,0 +1,34 @@ +/* + * (C) Copyright 2017-2020 OpenVidu (https://openvidu.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * See [[Session.createConnection]] + */ +export enum ConnectionType { + + /** + * WebRTC connection. This is the normal type of Connection for a regular user + * connecting to a session from an application. + */ + WEBRTC = 'WEBRTC', + + /** + * IP camera connection. This is the type of Connection used by IP cameras to + * connect to a session. + */ + IPCAM = 'IPCAM' +} \ No newline at end of file diff --git a/openvidu-node-client/src/OpenVidu.ts b/openvidu-node-client/src/OpenVidu.ts index 7b14ab71..42d06120 100644 --- a/openvidu-node-client/src/OpenVidu.ts +++ b/openvidu-node-client/src/OpenVidu.ts @@ -17,13 +17,11 @@ import axios from 'axios'; import { Connection } from './Connection'; -import { Publisher } from './Publisher'; import { Recording } from './Recording'; import { RecordingProperties } from './RecordingProperties'; import { Session } from './Session'; import { SessionProperties } from './SessionProperties'; import { RecordingLayout } from './RecordingLayout'; -import { RecordingMode } from 'RecordingMode'; /** * @hidden @@ -47,29 +45,36 @@ export class OpenVidu { /** * @hidden */ - static readonly API_RECORDINGS: string = '/api/recordings'; + static readonly API_PATH: string = '/openvidu/api'; /** * @hidden */ - static readonly API_RECORDINGS_START: string = '/start'; + static readonly API_SESSIONS = OpenVidu.API_PATH + '/sessions'; /** * @hidden */ - static readonly API_RECORDINGS_STOP: string = '/stop'; + static readonly API_TOKENS = OpenVidu.API_PATH + '/tokens'; /** * @hidden */ - static readonly API_SESSIONS = '/api/sessions'; + static readonly API_RECORDINGS: string = OpenVidu.API_PATH + '/recordings'; /** * @hidden */ - static readonly API_TOKENS = '/api/tokens'; + static readonly API_RECORDINGS_START: string = OpenVidu.API_RECORDINGS + '/start'; + /** + * @hidden + */ + static readonly API_RECORDINGS_STOP: string = OpenVidu.API_RECORDINGS + '/stop'; + + /** * Array of active sessions. **This value will remain unchanged since the last time method [[OpenVidu.fetch]] * was called**. Exceptions to this rule are: * + * - Calling [[OpenVidu.createSession]] automatically adds the new Session object to the local collection. * - Calling [[Session.fetch]] updates that specific Session status * - Calling [[Session.close]] automatically removes the Session from the list of active Sessions * - Calling [[Session.forceDisconnect]] automatically updates the inner affected connections for that specific Session @@ -83,16 +88,18 @@ export class OpenVidu { activeSessions: Session[] = []; /** - * @param urlOpenViduServer Public accessible IP where your instance of OpenVidu Server is up an running + * @param hostname URL where your instance of OpenVidu Server is up an running. + * It must be the full URL (e.g. `https://12.34.56.78:1234/`) + * * @param secret Secret used on OpenVidu Server initialization */ - constructor(private urlOpenViduServer: string, secret: string) { + constructor(private hostname: string, secret: string) { this.setHostnameAndPort(); this.basicAuth = this.getBasicAuth(secret); } /** - * Creates an OpenVidu session. You can call [[Session.getSessionId]] inside the resolved promise to retrieve the `sessionId` + * Creates an OpenVidu session. The session identifier will be available at property [[Session.sessionId]] * * @returns A Promise that is resolved to the [[Session]] if success and rejected with an Error object if not. */ @@ -140,14 +147,16 @@ export class OpenVidu { data = { session: sessionId, name: !!properties.name ? properties.name : '', - outputMode: !!properties.outputMode ? properties.outputMode : Recording.OutputMode.COMPOSED, - hasAudio: !!(properties.hasAudio), - hasVideo: !!(properties.hasVideo) + outputMode: properties.outputMode, + hasAudio: properties.hasAudio != null ? properties.hasAudio : null, + hasVideo: properties.hasVideo != null ? properties.hasVideo : null, + shmSize: properties.shmSize, + mediaNode: properties.mediaNode }; - if (data.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED] - || data.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED_QUICK_START]) { - data.resolution = !!properties.resolution ? properties.resolution : '1920x1080'; - data.recordingLayout = !!properties.recordingLayout ? properties.recordingLayout : RecordingLayout.BEST_FIT; + if ((data.hasVideo == null || data.hasVideo) && (data.outputMode == null || data.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED] + || data.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED_QUICK_START])) { + data.resolution = properties.resolution; + data.recordingLayout = !!properties.recordingLayout ? properties.recordingLayout : ''; if (data.recordingLayout.toString() === RecordingLayout[RecordingLayout.CUSTOM]) { data.customLayout = !!properties.customLayout ? properties.customLayout : ''; } @@ -156,20 +165,18 @@ export class OpenVidu { } else { data = JSON.stringify({ session: sessionId, - name: param2, - outputMode: Recording.OutputMode.COMPOSED + name: param2 }); } } else { data = JSON.stringify({ session: sessionId, - name: '', - outputMode: Recording.OutputMode.COMPOSED + name: '' }); } axios.post( - this.host + OpenVidu.API_RECORDINGS + OpenVidu.API_RECORDINGS_START, + this.host + OpenVidu.API_RECORDINGS_START, data, { headers: { @@ -223,7 +230,7 @@ export class OpenVidu { return new Promise((resolve, reject) => { axios.post( - this.host + OpenVidu.API_RECORDINGS + OpenVidu.API_RECORDINGS_STOP + '/' + recordingId, + this.host + OpenVidu.API_RECORDINGS_STOP + '/' + recordingId, undefined, { headers: { @@ -412,7 +419,7 @@ export class OpenVidu { public fetch(): Promise { return new Promise((resolve, reject) => { axios.get( - this.host + OpenVidu.API_SESSIONS, + this.host + OpenVidu.API_SESSIONS + '?pendingConnections=true', { headers: { Authorization: this.basicAuth @@ -422,47 +429,44 @@ export class OpenVidu { .then(res => { if (res.status === 200) { - // Array to store fetched sessionIds and later remove closed sessions - const fetchedSessionIds: string[] = []; // Boolean to store if any Session has changed let hasChanged = false; - res.data.content.forEach(session => { - fetchedSessionIds.push(session.sessionId); - let sessionIndex = -1; - let storedSession = this.activeSessions.find((s, index) => { - if (s.sessionId === session.sessionId) { - sessionIndex = index; - return true; - } else { - return false; - } - }); + // 1. Array to store fetched sessionIds and later remove closed ones + const fetchedSessionIds: string[] = []; + res.data.content.forEach(jsonSession => { + + const fetchedSession: Session = new Session(this, jsonSession); + fetchedSessionIds.push(fetchedSession.sessionId); + let storedSession = this.activeSessions.find(s => s.sessionId === fetchedSession.sessionId); + if (!!storedSession) { - const fetchedSession: Session = new Session(this).resetSessionWithJson(session); + + // 2. Update existing Session const changed: boolean = !storedSession.equalTo(fetchedSession); - if (changed) { - storedSession = fetchedSession; - this.activeSessions[sessionIndex] = storedSession; - } + storedSession.resetWithJson(jsonSession); console.log("Available session '" + storedSession.sessionId + "' info fetched. Any change: " + changed); hasChanged = hasChanged || changed; + } else { - this.activeSessions.push(new Session(this, session)); - console.log("New session '" + session.sessionId + "' info fetched"); + + // 3. Add new Session + this.activeSessions.push(fetchedSession); + console.log("New session '" + fetchedSession.sessionId + "' info fetched"); hasChanged = true; } }); - // Remove closed sessions from activeSessions array - this.activeSessions = this.activeSessions.filter(session => { - if (fetchedSessionIds.includes(session.sessionId)) { - return true; - } else { - console.log("Removing closed session '" + session.sessionId + "'"); + + // 4. Remove closed sessions from local collection + for (var i = this.activeSessions.length - 1; i >= 0; --i) { + let sessionId = this.activeSessions[i].sessionId; + if (!fetchedSessionIds.includes(sessionId)) { + console.log("Removing closed session '" + sessionId + "'"); hasChanged = true; - return false; + this.activeSessions.splice(i, 1); } - }); + } + console.log('Active sessions info fetched: ', fetchedSessionIds); resolve(hasChanged); } else { @@ -498,26 +502,9 @@ export class OpenVidu { const addWebRtcStatsToConnections = (connection: Connection, connectionsExtendedInfo: any) => { const connectionExtended = connectionsExtendedInfo.find(c => c.connectionId === connection.connectionId); if (!!connectionExtended) { - const publisherArray = []; connection.publishers.forEach(pub => { const publisherExtended = connectionExtended.publishers.find(p => p.streamId === pub.streamId); - const pubAux = {}; - // Standard properties - pubAux['streamId'] = pub.streamId; - pubAux['createdAt'] = pub.createdAt; - const mediaOptions = { - audioActive: pub.audioActive, - videoActive: pub.videoActive, - hasAudio: pub.hasAudio, - hasVideo: pub.hasVideo, - typeOfVideo: pub.typeOfVideo, - frameRate: pub.frameRate, - videoDimensions: pub.videoDimensions - }; - pubAux['mediaOptions'] = mediaOptions; - const newPublisher = new Publisher(pubAux); - // WebRtc properties - newPublisher['webRtc'] = { + pub['webRtc'] = { kms: { events: publisherExtended.events, localCandidate: publisherExtended.localCandidate, @@ -529,11 +516,10 @@ export class OpenVidu { remoteSdp: publisherExtended.remoteSdp } }; - newPublisher['localCandidatePair'] = parseRemoteCandidatePair(newPublisher['webRtc'].kms.remoteCandidate); + pub['localCandidatePair'] = parseRemoteCandidatePair(pub['webRtc'].kms.remoteCandidate); if (!!publisherExtended.serverStats) { - newPublisher['webRtc'].kms.serverStats = publisherExtended.serverStats; + pub['webRtc'].kms.serverStats = publisherExtended.serverStats; } - publisherArray.push(newPublisher); }); const subscriberArray = []; connection.subscribers.forEach(sub => { @@ -562,7 +548,6 @@ export class OpenVidu { } subscriberArray.push(subAux); }); - connection.publishers = publisherArray; connection.subscribers = subscriberArray; } }; @@ -594,68 +579,64 @@ export class OpenVidu { .then(res => { if (res.status === 200) { - // Array to store fetched sessionIds and later remove closed sessions - const fetchedSessionIds: string[] = []; // Global changes let globalChanges = false; // Collection of sessionIds telling whether each one of them has changed or not const sessionChanges: ObjMap = {}; - res.data.content.forEach(session => { - fetchedSessionIds.push(session.sessionId); - let sessionIndex = -1; - let storedSession = this.activeSessions.find((s, index) => { - if (s.sessionId === session.sessionId) { - sessionIndex = index; - return true; - } else { - return false; - } - }); - if (!!storedSession) { - const fetchedSession: Session = new Session(this).resetSessionWithJson(session); - fetchedSession.activeConnections.forEach(connection => { - addWebRtcStatsToConnections(connection, session.connections.content); - }); + // 1. Array to store fetched sessionIds and later remove closed ones + const fetchedSessionIds: string[] = []; + res.data.content.forEach(jsonSession => { + const fetchedSession: Session = new Session(this, jsonSession); + fetchedSession.connections.forEach(connection => { + addWebRtcStatsToConnections(connection, jsonSession.connections.content); + }); + fetchedSessionIds.push(fetchedSession.sessionId); + let storedSession = this.activeSessions.find(s => s.sessionId === fetchedSession.sessionId); + + if (!!storedSession) { + + // 2. Update existing Session let changed = !storedSession.equalTo(fetchedSession); if (!changed) { // Check if server webrtc information has changed in any Publisher object (Session.equalTo does not check Publisher.webRtc auxiliary object) - fetchedSession.activeConnections.forEach((connection, index1) => { + fetchedSession.connections.forEach((connection, index1) => { for (let index2 = 0; (index2 < connection['publishers'].length && !changed); index2++) { - changed = changed || JSON.stringify(connection['publishers'][index2]['webRtc']) !== JSON.stringify(storedSession.activeConnections[index1]['publishers'][index2]['webRtc']); + changed = changed || JSON.stringify(connection['publishers'][index2]['webRtc']) !== JSON.stringify(storedSession.connections[index1]['publishers'][index2]['webRtc']); } }); } - if (changed) { - storedSession = fetchedSession; - this.activeSessions[sessionIndex] = storedSession; - } + storedSession.resetWithJson(jsonSession); + storedSession.connections.forEach(connection => { + addWebRtcStatsToConnections(connection, jsonSession.connections.content); + }); console.log("Available session '" + storedSession.sessionId + "' info fetched. Any change: " + changed); sessionChanges[storedSession.sessionId] = changed; globalChanges = globalChanges || changed; + } else { - const newSession = new Session(this, session); - newSession.activeConnections.forEach(connection => { - addWebRtcStatsToConnections(connection, session.connections.content); - }); - this.activeSessions.push(newSession); - console.log("New session '" + session.sessionId + "' info fetched"); - sessionChanges[session.sessionId] = true; + + // 3. Add new Session + this.activeSessions.push(fetchedSession); + console.log("New session '" + fetchedSession.sessionId + "' info fetched"); + sessionChanges[fetchedSession.sessionId] = true; globalChanges = true; + } }); - // Remove closed sessions from activeSessions array - this.activeSessions = this.activeSessions.filter(session => { - if (fetchedSessionIds.includes(session.sessionId)) { - return true; - } else { - console.log("Removing closed session '" + session.sessionId + "'"); - sessionChanges[session.sessionId] = true; + + // 4. Remove closed sessions from local collection + for (var i = this.activeSessions.length - 1; i >= 0; --i) { + let sessionId = this.activeSessions[i].sessionId; + if (!fetchedSessionIds.includes(sessionId)) { + console.log("Removing closed session '" + sessionId + "'"); + sessionChanges[sessionId] = true; globalChanges = true; - return false; + this.activeSessions.splice(i, 1); } - }); + } + console.log('Active sessions info fetched: ', fetchedSessionIds); resolve({ changes: globalChanges, sessionChanges }); } else { @@ -687,7 +668,7 @@ export class OpenVidu { private setHostnameAndPort(): void { let url: URL; try { - url = new URL(this.urlOpenViduServer); + url = new URL(this.hostname); } catch (error) { console.error('URL format incorrect', error); throw new Error('URL format incorrect: ' + error); diff --git a/openvidu-node-client/src/Publisher.ts b/openvidu-node-client/src/Publisher.ts index d88a009a..fa3ce8d2 100644 --- a/openvidu-node-client/src/Publisher.ts +++ b/openvidu-node-client/src/Publisher.ts @@ -74,6 +74,13 @@ export class Publisher { * @hidden */ constructor(json) { + this.resetWithJson(json); + } + + /** + * @hidden + */ + resetWithJson(json): Publisher { this.streamId = json.streamId; this.createdAt = json.createdAt; this.hasAudio = json.mediaOptions.hasAudio; @@ -83,6 +90,7 @@ export class Publisher { this.frameRate = json.mediaOptions.frameRate; this.typeOfVideo = json.mediaOptions.typeOfVideo; this.videoDimensions = json.mediaOptions.videoDimensions; + return this; } /** diff --git a/openvidu-node-client/src/Recording.ts b/openvidu-node-client/src/Recording.ts index 85635e3a..cc0989a6 100644 --- a/openvidu-node-client/src/Recording.ts +++ b/openvidu-node-client/src/Recording.ts @@ -83,7 +83,7 @@ export class Recording { hasVideo: !!json['hasVideo'] }; if (this.properties.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED] - || this.properties.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED_QUICK_START]) { + || this.properties.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED_QUICK_START]) { this.properties.resolution = !!(json['resolution']) ? json['resolution'] : '1920x1080'; this.properties.recordingLayout = !!(json['recordingLayout']) ? json['recordingLayout'] : RecordingLayout.BEST_FIT; if (this.properties.recordingLayout.toString() === RecordingLayout[RecordingLayout.CUSTOM]) { @@ -103,7 +103,7 @@ export namespace Recording { /** * The recording is starting (cannot be stopped). Some recording may not go - * through this status and directly reach "started" status + * through this status and directly reach "started" status */ starting = 'starting', @@ -113,21 +113,20 @@ export namespace Recording { started = 'started', /** - * The recording has stopped and is being processed. At some point it will reach - * "ready" status - */ + * The recording has stopped and is being processed. At some point it will reach + * "ready" status + */ stopped = 'stopped', /** - * The recording has finished OK and is available for download through OpenVidu - * Server recordings endpoint: - * https://YOUR_OPENVIDUSERVER_IP/recordings/{RECORDING_ID}/{RECORDING_NAME}.{EXTENSION} + * The recording has finished being processed and is available for download through + * property [[Recording.url]] */ ready = 'ready', /** * The recording has failed. This status may be reached from "starting", - * "started" and "stopped" status + * "started" and "stopped" status */ failed = 'failed' } @@ -141,24 +140,24 @@ export namespace Recording { * Record all streams in a grid layout in a single archive */ COMPOSED = 'COMPOSED', - - /** - * Works the same way as COMPOSED mode, but the necessary recorder - * service module will start some time in advance and won't be terminated - * once a specific session recording has ended. This module will remain - * up and running as long as the session remains active. - * - * - **Pros vs COMPOSED**: the process of starting the recording will be noticeably - * faster. This can be very useful in use cases where a session needs to be - * recorded multiple times over time, when a better response time is usually - * desirable. - * - **Cons vs COMPOSED**: for every session initialized with COMPOSED_QUICK_START - * recording output mode, extra CPU power will be required in OpenVidu Server. - * The recording module will be continuously rendering all of the streams being - * published to the session even when the session is not being recorded. And that - * is for every session configured with COMPOSED_QUICK_START. - */ + /** + * Works the same way as COMPOSED mode, but the necessary recorder + * service module will start some time in advance and won't be terminated + * once a specific session recording has ended. This module will remain + * up and running as long as the session remains active. + * + * - **Pros vs COMPOSED**: the process of starting the recording will be noticeably + * faster. This can be very useful in use cases where a session needs to be + * recorded multiple times over time, when a better response time is usually + * desirable. + + * - **Cons vs COMPOSED**: for every session initialized with COMPOSED_QUICK_START + * recording output mode, extra CPU power will be required in OpenVidu Server. + * The recording module will be continuously rendering all of the streams being + * published to the session even when the session is not being recorded. And that + * is for every session configured with COMPOSED_QUICK_START. + */ COMPOSED_QUICK_START = 'COMPOSED_QUICK_START', /** diff --git a/openvidu-node-client/src/RecordingProperties.ts b/openvidu-node-client/src/RecordingProperties.ts index 585b53c0..3682c406 100644 --- a/openvidu-node-client/src/RecordingProperties.ts +++ b/openvidu-node-client/src/RecordingProperties.ts @@ -67,4 +67,22 @@ export interface RecordingProperties { * Whether or not to record video. Cannot be set to false at the same time as [[RecordingProperties.hasAudio]] */ hasVideo?: boolean; + + /** + * If COMPOSED recording, the amount of shared memory reserved for the recording process in bytes. + * Minimum 134217728 (128MB). Property ignored if INDIVIDUAL recording. Default to 536870912 (512 MB) + */ + shmSize?: number; + + /** + * **This feature is part of OpenVidu Pro tier** PRO + * + * The Media Node where to host the recording. The default option if this property is not defined is the same + * Media Node hosting the Session to record. This object defines the following properties as Media Node selector: + * - `id`: Media Node unique identifier + */ + mediaNode?: { + id: string; + } + } \ No newline at end of file diff --git a/openvidu-node-client/src/Session.ts b/openvidu-node-client/src/Session.ts index 3eb7c3de..9e4ff374 100644 --- a/openvidu-node-client/src/Session.ts +++ b/openvidu-node-client/src/Session.ts @@ -15,11 +15,11 @@ * */ -import axios from 'axios'; +import axios, { AxiosError } from 'axios'; import { Connection } from './Connection'; +import { ConnectionProperties } from './ConnectionProperties'; import { MediaMode } from './MediaMode'; import { OpenVidu } from './OpenVidu'; -import { OpenViduRole } from './OpenViduRole'; import { Publisher } from './Publisher'; import { Recording } from './Recording'; import { RecordingLayout } from './RecordingLayout'; @@ -28,7 +28,6 @@ import { SessionProperties } from './SessionProperties'; import { TokenOptions } from './TokenOptions'; import { VideoCodec } from './VideoCodec'; - export class Session { /** @@ -47,14 +46,26 @@ export class Session { properties: SessionProperties; /** - * Array of active connections to the session. This property always initialize as an empty array and - * **will remain unchanged since the last time method [[Session.fetch]] was called**. Exceptions to this rule are: + * Array of Connections to the Session. This property always initialize as an empty array and + * **will remain unchanged since the last time method [[Session.fetch]] or [[OpenVidu.fetch]] was called**. + * Exceptions to this rule are: * - * - Calling [[Session.forceUnpublish]] also automatically updates each affected Connection status - * - Calling [[Session.forceDisconnect]] automatically updates each affected Connection status + * - Calling [[Session.createConnection]] automatically adds the new Connection object to the local collection. + * - Calling [[Session.forceUnpublish]] automatically updates each affected local Connection object. + * - Calling [[Session.forceDisconnect]] automatically updates each affected local Connection object. + * - Calling [[Session.updateConnection]] automatically updates the attributes of the affected local Connection object. * - * To get the array of active connections with their current actual value, you must call [[Session.fetch]] before consulting - * property [[activeConnections]] + * To get the array of Connections with their current actual value, you must call [[Session.fetch]] or [[OpenVidu.fetch]] + * before consulting property [[connections]] + */ + connections: Connection[] = []; + + /** + * Array containing the active Connections of the Session. It is a subset of [[Session.connections]] array containing only + * those Connections with property [[Connection.status]] to `active`. + * + * To get the array of active Connections with their current actual value, you must call [[Session.fetch]] or [[OpenVidu.fetch]] + * before consulting property [[activeConnections]] */ activeConnections: Connection[] = []; @@ -71,7 +82,7 @@ export class Session { // Defined parameter if (!!propertiesOrJson.sessionId) { // Parameter is a JSON representation of Session ('sessionId' property always defined) - this.resetSessionWithJson(propertiesOrJson); + this.resetWithJson(propertiesOrJson); } else { // Parameter is a SessionProperties object this.properties = propertiesOrJson; @@ -84,32 +95,23 @@ export class Session { this.properties.recordingMode = !!this.properties.recordingMode ? this.properties.recordingMode : RecordingMode.MANUAL; this.properties.defaultOutputMode = !!this.properties.defaultOutputMode ? this.properties.defaultOutputMode : Recording.OutputMode.COMPOSED; this.properties.defaultRecordingLayout = !!this.properties.defaultRecordingLayout ? this.properties.defaultRecordingLayout : RecordingLayout.BEST_FIT; - this.properties.allowTranscoding = !!this.properties.allowTranscoding ? this.properties.allowTranscoding : null; - this.properties.forcedVideoCodec = !!this.properties.forcedVideoCodec ? this.properties.forcedVideoCodec : null; + this.properties.forcedVideoCodec = !!this.properties.forcedVideoCodec ? this.properties.forcedVideoCodec : undefined; + this.properties.allowTranscoding = this.properties.allowTranscoding != null ? this.properties.allowTranscoding : undefined; } /** - * Gets the unique identifier of the Session - */ - public getSessionId(): string { - return this.sessionId; - } - - /** - * Gets a new token associated to Session object + * @deprecated Use [[Session.createConnection]] instead to get a [[Connection]] object. * - * @returns A Promise that is resolved to the _token_ if success and rejected with an Error object if not + * @returns A Promise that is resolved to the generated _token_ string if success and rejected with an Error object if not */ public generateToken(tokenOptions?: TokenOptions): Promise { return new Promise((resolve, reject) => { - const data = JSON.stringify({ session: this.sessionId, - role: (!!tokenOptions && !!tokenOptions.role) ? tokenOptions.role : OpenViduRole.PUBLISHER, - data: (!!tokenOptions && !!tokenOptions.data) ? tokenOptions.data : '', - kurentoOptions: (!!tokenOptions && !!tokenOptions.kurentoOptions) ? tokenOptions.kurentoOptions : {}, + role: (!!tokenOptions && !!tokenOptions.role) ? tokenOptions.role : null, + data: (!!tokenOptions && !!tokenOptions.data) ? tokenOptions.data : null, + kurentoOptions: (!!tokenOptions && !!tokenOptions.kurentoOptions) ? tokenOptions.kurentoOptions : null }); - axios.post( this.ov.host + OpenVidu.API_TOKENS, data, @@ -123,26 +125,57 @@ export class Session { .then(res => { if (res.status === 200) { // SUCCESS response from openvidu-server. Resolve token - resolve(res.data.id); + resolve(res.data.token); } else { // ERROR response from openvidu-server. Resolve HTTP status reject(new Error(res.status.toString())); } }).catch(error => { - if (error.response) { - // The request was made and the server responded with a status code (not 2xx) - reject(new Error(error.response.status.toString())); - } else if (error.request) { - // The request was made but no response was received - // `error.request` is an instance of XMLHttpRequest in the browser and an instance of - // http.ClientRequest in node.js - console.error(error.request); - reject(new Error(error.request)); - } else { - // Something happened in setting up the request that triggered an Error - console.error('Error', error.message); - reject(new Error(error.message)); + this.handleError(error, reject); + }); + }); + } + + /** + * Creates a new Connection object associated to Session object and configured with + * `connectionProperties`. Each user connecting to the Session requires a Connection. + * The token string value to send to the client side is available at [[Connection.token]]. + * + * @returns A Promise that is resolved to the generated [[Connection]] object if success and rejected with an Error object if not + */ + public createConnection(connectionProperties?: ConnectionProperties): Promise { + return new Promise((resolve, reject) => { + const data = JSON.stringify({ + role: (!!connectionProperties && !!connectionProperties.role) ? connectionProperties.role : null, + data: (!!connectionProperties && !!connectionProperties.data) ? connectionProperties.data : null, + record: !!connectionProperties ? connectionProperties.record : null, + kurentoOptions: (!!connectionProperties && !!connectionProperties.kurentoOptions) ? connectionProperties.kurentoOptions : null + }); + axios.post( + this.ov.host + OpenVidu.API_SESSIONS + '/' + this.sessionId + '/connection', + data, + { + headers: { + 'Authorization': this.ov.basicAuth, + 'Content-Type': 'application/json' } + } + ) + .then(res => { + if (res.status === 200) { + // SUCCESS response from openvidu-server. Store and resolve Connection + const connection = new Connection(res.data); + this.connections.push(connection); + if (connection.status === 'active') { + this.activeConnections.push(connection); + } + resolve(new Connection(res.data)); + } else { + // ERROR response from openvidu-server. Resolve HTTP status + reject(new Error(res.status.toString())); + } + }).catch(error => { + this.handleError(error, reject); }); }); } @@ -174,29 +207,17 @@ export class Session { reject(new Error(res.status.toString())); } }).catch(error => { - if (error.response) { - // The request was made and the server responded with a status code (not 2xx) - reject(new Error(error.response.status.toString())); - } else if (error.request) { - // The request was made but no response was received - // `error.request` is an instance of XMLHttpRequest in the browser and an instance of - // http.ClientRequest in node.js - console.error(error.request); - reject(new Error(error.request)); - } else { - // Something happened in setting up the request that triggered an Error - console.error('Error', error.message); - reject(new Error(error.message)); - } + this.handleError(error, reject); }); }); } /** - * Updates every property of the Session with the current status it has in OpenVidu Server. This is especially useful for accessing the list of active - * connections of the Session ([[Session.activeConnections]]) and use those values to call [[Session.forceDisconnect]] or [[Session.forceUnpublish]]. + * Updates every property of the Session with the current status it has in OpenVidu Server. This is especially useful for accessing the list of + * Connections of the Session ([[Session.connections]], [[Session.activeConnections]]) and use those values to call [[Session.forceDisconnect]], + * [[Session.forceUnpublish]] or [[Session.updateConnection]]. * - * To update every Session object owned by OpenVidu object, call [[OpenVidu.fetch]] + * To update all Session objects owned by OpenVidu object at once, call [[OpenVidu.fetch]] * * @returns A promise resolved to true if the Session status has changed with respect to the server, or to false if not. * This applies to any property or sub-property of the Session object @@ -205,7 +226,7 @@ export class Session { return new Promise((resolve, reject) => { const beforeJSON: string = JSON.stringify(this, this.removeCircularOpenViduReference); axios.get( - this.ov.host + OpenVidu.API_SESSIONS + '/' + this.sessionId, + this.ov.host + OpenVidu.API_SESSIONS + '/' + this.sessionId + '?pendingConnections=true', { headers: { 'Authorization': this.ov.basicAuth, @@ -216,7 +237,7 @@ export class Session { .then(res => { if (res.status === 200) { // SUCCESS response from openvidu-server - this.resetSessionWithJson(res.data); + this.resetWithJson(res.data); const afterJSON: string = JSON.stringify(this, this.removeCircularOpenViduReference); const hasChanged: boolean = !(beforeJSON === afterJSON); console.log("Session info fetched for session '" + this.sessionId + "'. Any change: " + hasChanged); @@ -226,32 +247,26 @@ export class Session { reject(new Error(res.status.toString())); } }).catch(error => { - if (error.response) { - // The request was made and the server responded with a status code (not 2xx) - reject(new Error(error.response.status.toString())); - } else if (error.request) { - // The request was made but no response was received - // `error.request` is an instance of XMLHttpRequest in the browser and an instance of - // http.ClientRequest in node.js - console.error(error.request); - reject(new Error(error.request)); - } else { - // Something happened in setting up the request that triggered an Error - console.error('Error', error.message); - reject(new Error(error.message)); - } + this.handleError(error, reject); }); }); } /** - * Forces the user with Connection `connectionId` to leave the session. OpenVidu Browser will trigger the proper events on the client-side - * (`streamDestroyed`, `connectionDestroyed`, `sessionDisconnected`) with reason set to `"forceDisconnectByServer"` + * Removes the Connection from the Session. This can translate into a forced eviction of a user from the Session if the + * Connection had status `active` or into a token invalidation if no user had taken the Connection yet (status `pending`). * - * You can get `connection` parameter from [[Session.activeConnections]] array ([[Connection.connectionId]] for getting each `connectionId` property). - * Remember to call [[Session.fetch]] before to fetch the current actual properties of the Session from OpenVidu Server + * In the first case, OpenVidu Browser will trigger the proper events in the client-side (`streamDestroyed`, `connectionDestroyed`, + * `sessionDisconnected`) with reason set to `"forceDisconnectByServer"`. * - * @returns A Promise that is resolved if the user was successfully disconnected and rejected with an Error object if not + * In the second case, the token of the Connection will be invalidated and no user will be able to connect to the session with it. + * + * This method automatically updates the properties of the local affected objects. This means that there is no need to call + * [[Session.fetch]] or [[OpenVidu.fetch]]] to see the changes consequence of the execution of this method applied in the local objects. + * + * @param connection The Connection object to remove from the session, or its `connectionId` property + * + * @returns A Promise that is resolved if the Connection was successfully removed from the Session and rejected with an Error object if not */ public forceDisconnect(connection: string | Connection): Promise { return new Promise((resolve, reject) => { @@ -267,9 +282,9 @@ export class Session { .then(res => { if (res.status === 204) { // SUCCESS response from openvidu-server - // Remove connection from activeConnections array + // Remove connection from connections array let connectionClosed; - this.activeConnections = this.activeConnections.filter(con => { + this.connections = this.connections.filter(con => { if (con.connectionId !== connectionId) { return true; } else { @@ -280,7 +295,7 @@ export class Session { // Remove every Publisher of the closed connection from every subscriber list of other connections if (!!connectionClosed) { connectionClosed.publishers.forEach(publisher => { - this.activeConnections.forEach(con => { + this.connections.forEach(con => { con.subscribers = con.subscribers.filter(subscriber => { // tslint:disable:no-string-literal if (!!subscriber['streamId']) { @@ -295,8 +310,9 @@ export class Session { }); }); } else { - console.warn("The closed connection wasn't fetched in OpenVidu Java Client. No changes in the collection of active connections of the Session"); + console.warn("The closed connection wasn't fetched in OpenVidu Node Client. No changes in the collection of active connections of the Session"); } + this.updateActiveConnectionsArray(); console.log("Connection '" + connectionId + "' closed"); resolve(); } else { @@ -305,30 +321,22 @@ export class Session { } }) .catch(error => { - if (error.response) { - // The request was made and the server responded with a status code (not 2xx) - reject(new Error(error.response.status.toString())); - } else if (error.request) { - // The request was made but no response was received - // `error.request` is an instance of XMLHttpRequest in the browser and an instance of - // http.ClientRequest in node.js - console.error(error.request); - reject(new Error(error.request)); - } else { - // Something happened in setting up the request that triggered an Error - console.error('Error', error.message); - reject(new Error(error.message)); - } + this.handleError(error, reject); }); }); } /** - * Forces some user to unpublish a Stream (identified by its `streamId` or the corresponding [[Publisher]] object owning it). + * Forces some Connection to unpublish a Stream (identified by its `streamId` or the corresponding [[Publisher]] object owning it). * OpenVidu Browser will trigger the proper events on the client-side (`streamDestroyed`) with reason set to `"forceUnpublishByServer"`. * * You can get `publisher` parameter from [[Connection.publishers]] array ([[Publisher.streamId]] for getting each `streamId` property). - * Remember to call [[Session.fetch]] before to fetch the current actual properties of the Session from OpenVidu Server + * Remember to call [[Session.fetch]] or [[OpenVidu.fetch]] before to fetch the current actual properties of the Session from OpenVidu Server + * + * This method automatically updates the properties of the local affected objects. This means that there is no need to call + * [[Session.fetch]] or [[OpenVidu.fetch]] to see the changes consequence of the execution of this method applied in the local objects. + * + * @param publisher The Publisher object to unpublish, or its `streamId` property * * @returns A Promise that is resolved if the stream was successfully unpublished and rejected with an Error object if not */ @@ -347,7 +355,7 @@ export class Session { .then(res => { if (res.status === 204) { // SUCCESS response from openvidu-server - this.activeConnections.forEach(connection => { + this.connections.forEach(connection => { // Try to remove the Publisher from the Connection publishers collection connection.publishers = connection.publishers.filter(pub => pub.streamId !== streamId); // Try to remove the Publisher from the Connection subscribers collection @@ -363,6 +371,7 @@ export class Session { } } }); + this.updateActiveConnectionsArray(); console.log("Stream '" + streamId + "' unpublished"); resolve(); } else { @@ -370,24 +379,79 @@ export class Session { reject(new Error(res.status.toString())); } }).catch(error => { - if (error.response) { - // The request was made and the server responded with a status code (not 2xx) - reject(new Error(error.response.status.toString())); - } else if (error.request) { - // The request was made but no response was received - // `error.request` is an instance of XMLHttpRequest in the browser and an instance of - // http.ClientRequest in node.js - console.error(error.request); - reject(new Error(error.request)); - } else { - // Something happened in setting up the request that triggered an Error - console.error('Error', error.message); - reject(new Error(error.message)); - } + this.handleError(error, reject); }); }); } + /** + * **This feature is part of OpenVidu Pro tier** PRO + * + * Updates the properties of a Connection with a [[ConnectionProperties]] object. + * Only these properties can be updated: + * + * - [[ConnectionProperties.role]] + * - [[ConnectionProperties.record]] + * + * This method automatically updates the properties of the local affected objects. This means that there is no need to call + * [[Session.fetch]] or [[OpenVidu.fetch]] to see the changes consequence of the execution of this method applied in the local objects. + * + * The affected client will trigger one [ConnectionPropertyChangedEvent](/en/stable/api/openvidu-browser/classes/connectionpropertychangedevent.html) + * for each modified property. + * + * @param connectionId The [[Connection.connectionId]] of the Connection object to modify + * @param connectionProperties A new [[ConnectionProperties]] object with the updated values to apply + * + * @returns A Promise that is resolved to the updated [[Connection]] object if the operation was + * successful and rejected with an Error object if not + */ + public updateConnection(connectionId: string, connectionProperties: ConnectionProperties): Promise { + return new Promise((resolve, reject) => { + axios.patch( + this.ov.host + OpenVidu.API_SESSIONS + "/" + this.sessionId + "/connection/" + connectionId, + connectionProperties, + { + headers: { + 'Authorization': this.ov.basicAuth, + 'Content-Type': 'application/json' + } + } + ) + .then(res => { + if (res.status === 200) { + console.log('Connection ' + connectionId + ' updated'); + } else { + // ERROR response from openvidu-server. Resolve HTTP status + reject(new Error(res.status.toString())); + return; + } + // Update the actual Connection object with the new options + const existingConnection: Connection = this.connections.find(con => con.connectionId === connectionId); + if (!existingConnection) { + // The updated Connection is not available in local map + const newConnection: Connection = new Connection(res.data); + this.connections.push(newConnection); + this.updateActiveConnectionsArray(); + resolve(newConnection); + } else { + // The updated Connection was available in local map + existingConnection.overrideConnectionProperties(connectionProperties); + this.updateActiveConnectionsArray(); + resolve(existingConnection); + } + }).catch(error => { + this.handleError(error, reject); + }); + }); + } + + /** + * @hidden + */ + public getSessionId(): string { + return this.sessionId; + } + /** * @hidden */ @@ -399,18 +463,17 @@ export class Session { } const mediaMode = !!this.properties.mediaMode ? this.properties.mediaMode : MediaMode.ROUTED; - const recordingMode = !!this.properties.recordingMode ? this.properties.recordingMode : RecordingMode.MANUAL; + const recordingMode = !!this.properties.recordingMode ? this.properties.recordingMode : RecordingMode.MANUAL; const defaultOutputMode = !!this.properties.defaultOutputMode ? this.properties.defaultOutputMode : Recording.OutputMode.COMPOSED; - const defaultRecordingLayout = !!this.properties.defaultRecordingLayout ? this.properties.defaultRecordingLayout : RecordingLayout.BEST_FIT; - const defaultCustomLayout = !!this.properties.defaultCustomLayout ? this.properties.defaultCustomLayout : ''; + const defaultRecordingLayout = !!this.properties.defaultRecordingLayout ? this.properties.defaultRecordingLayout : RecordingLayout.BEST_FIT; + const defaultCustomLayout = !!this.properties.defaultCustomLayout ? this.properties.defaultCustomLayout : ''; const customSessionId = !!this.properties.customSessionId ? this.properties.customSessionId : ''; - const forcedVideoCodec = !!this.properties.forcedVideoCodec ? this.properties.forcedVideoCodec : null; - const allowTranscoding = !!this.properties.allowTranscoding ? this.properties.allowTranscoding : null; - - const data = JSON.stringify({ mediaMode, recordingMode, - defaultOutputMode, defaultRecordingLayout, defaultCustomLayout, - customSessionId, forcedVideoCodec, allowTranscoding - }); + const mediaNode = !!this.properties.mediaNode ? this.properties.mediaNode : undefined; + const forcedVideoCodec = !!this.properties.forcedVideoCodec ? this.properties.forcedVideoCodec : undefined; + const allowTranscoding = this.properties.allowTranscoding != null ? this.properties.allowTranscoding : undefined; + + const data = JSON.stringify({mediaMode, recordingMode, defaultOutputMode, defaultRecordingLayout, defaultCustomLayout, + customSessionId, mediaNode, forcedVideoCodec, allowTranscoding}); axios.post( this.ov.host + OpenVidu.API_SESSIONS, @@ -431,7 +494,9 @@ export class Session { this.properties.recordingMode = recordingMode; this.properties.defaultOutputMode = defaultOutputMode; this.properties.defaultRecordingLayout = defaultRecordingLayout; + this.properties.defaultCustomLayout = defaultCustomLayout; this.properties.customSessionId = customSessionId; + this.properties.mediaNode = mediaNode; this.properties.forcedVideoCodec = res.data.forcedVideoCodec; this.properties.allowTranscoding = res.data.allowTranscoding; resolve(this.sessionId); @@ -467,58 +532,67 @@ export class Session { /** * @hidden */ - public resetSessionWithJson(json): Session { + public resetWithJson(json): Session { this.sessionId = json.sessionId; this.createdAt = json.createdAt; this.recording = json.recording; - let customSessionId: string; - let defaultCustomLayout: string; - if (!!this.properties) { - customSessionId = this.properties.customSessionId; - defaultCustomLayout = !!json.defaultCustomLayout ? json.defaultCustomLayout : this.properties.defaultCustomLayout; - } this.properties = { + customSessionId: json.customSessionId, mediaMode: json.mediaMode, recordingMode: json.recordingMode, defaultOutputMode: json.defaultOutputMode, defaultRecordingLayout: json.defaultRecordingLayout, - forcedVideoCodec: !!json.forcedVideoCodec ? json.forcedVideoCodec : null, - allowTranscoding: !!json.allowTranscoding ? json.allowTranscoding : null + defaultCustomLayout: json.defaultCustomLayout, + forcedVideoCodec: json.forcedVideoCodec, + allowTranscoding: json.allowTranscoding }; - if (!!customSessionId) { - this.properties.customSessionId = customSessionId; - } else if (!!json.customSessionId) { - this.properties.customSessionId = json.customSessionId; + if (json.defaultRecordingLayout == null) { + delete this.properties.defaultRecordingLayout; } - if (!!defaultCustomLayout) { - this.properties.defaultCustomLayout = defaultCustomLayout; + if (json.customSessionId == null) { + delete this.properties.customSessionId; + } + if (json.defaultCustomLayout == null) { + delete this.properties.defaultCustomLayout; + } + if (json.mediaNode == null) { + delete this.properties.mediaNode; + } + if (json.forcedVideoCodec == null) { + delete this.properties.forcedVideoCodec; + } + if (json.allowTranscoding == null) { + delete this.properties.allowTranscoding; } - this.activeConnections = []; - json.connections.content.forEach(connection => { - const publishers: Publisher[] = []; - connection.publishers.forEach(publisher => { - publishers.push(new Publisher(publisher)); - }); - const subscribers: string[] = []; - connection.subscribers.forEach(subscriber => { - subscribers.push(subscriber.streamId); - }); - this.activeConnections.push( - new Connection( - connection.connectionId, - connection.createdAt, - connection.role, - connection.token, - connection.location, - connection.platform, - connection.serverData, - connection.clientData, - publishers, - subscribers)); + // 1. Array to store fetched connections and later remove closed ones + const fetchedConnectionIds: string[] = []; + json.connections.content.forEach(jsonConnection => { + + const connectionObj: Connection = new Connection(jsonConnection); + fetchedConnectionIds.push(connectionObj.connectionId); + let storedConnection = this.connections.find(c => c.connectionId === connectionObj.connectionId); + + if (!!storedConnection) { + // 2. Update existing Connection + storedConnection.resetWithJson(jsonConnection); + } else { + // 3. Add new Connection + this.connections.push(connectionObj); + } }); + + // 4. Remove closed sessions from local collection + for (var i = this.connections.length - 1; i >= 0; --i) { + if (!fetchedConnectionIds.includes(this.connections[i].connectionId)) { + this.connections.splice(i, 1); + } + } + // Order connections by time of creation - this.activeConnections.sort((c1, c2) => (c1.createdAt > c2.createdAt) ? 1 : ((c2.createdAt > c1.createdAt) ? -1 : 0)); + this.connections.sort((c1, c2) => (c1.createdAt > c2.createdAt) ? 1 : ((c2.createdAt > c1.createdAt) ? -1 : 0)); + // Populate activeConnections array + this.updateActiveConnectionsArray(); return this; } @@ -530,13 +604,13 @@ export class Session { this.sessionId === other.sessionId && this.createdAt === other.createdAt && this.recording === other.recording && - this.activeConnections.length === other.activeConnections.length && + this.connections.length === other.connections.length && JSON.stringify(this.properties) === JSON.stringify(other.properties) ); if (equals) { let i = 0; - while (equals && i < this.activeConnections.length) { - equals = this.activeConnections[i].equalTo(other.activeConnections[i]); + while (equals && i < this.connections.length) { + equals = this.connections[i].equalTo(other.connections[i]); i++; } return equals; @@ -556,4 +630,36 @@ export class Session { } } -} \ No newline at end of file + /** + * @hidden + */ + private updateActiveConnectionsArray() { + this.activeConnections = []; + this.connections.forEach(con => { + if (con.status === 'active') { + this.activeConnections.push(con); + } + }); + } + + /** + * @hidden + */ + private handleError(error: AxiosError, reject: (reason?: any) => void) { + if (error.response) { + // The request was made and the server responded with a status code (not 2xx) + reject(new Error(error.response.status.toString())); + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + console.error(error.request); + reject(new Error(error.request)); + } else { + // Something happened in setting up the request that triggered an Error + console.error('Error', error.message); + reject(new Error(error.message)); + } + } + +} diff --git a/openvidu-node-client/src/SessionProperties.ts b/openvidu-node-client/src/SessionProperties.ts index 91e182c8..9115a284 100644 --- a/openvidu-node-client/src/SessionProperties.ts +++ b/openvidu-node-client/src/SessionProperties.ts @@ -65,21 +65,31 @@ export interface SessionProperties { * If this parameter is undefined or an empty string, OpenVidu Server will generate a random sessionId for you. */ customSessionId?: string; - + + /** + * **This feature is part of OpenVidu Pro tier** PRO + * + * The Media Node where to host the session. The default option if this property is not defined is the less loaded + * Media Node at the moment the first user joins the session. This object defines the following properties as Media Node selector: + * - `id`: Media Node unique identifier + */ + mediaNode?: { + id: string; + } + /** * It defines which video codec do you want to be forcibly used for this session. * This allows browsers/clients to use the same codec avoiding transcoding in the media server. - * If the browser/client is not compatible with the specified codec and [[allowTranscoding]] + * If the browser/client is not compatible with the specified codec and [[allowTranscoding]] * is false and exception will occur. - * - * If forcedVideoCodec is set to NONE, no codec will be forced. + * + * If forcedVideoCodec is set to NONE, no codec will be forced. */ - forcedVideoCodec?: VideoCodec; - + forcedVideoCodec?: string; + /** * It defines if you want to allow transcoding in the media server or not * when [[forcedVideoCodec]] is not compatible with the browser/client. */ allowTranscoding?: boolean; - } diff --git a/openvidu-node-client/src/TokenOptions.ts b/openvidu-node-client/src/TokenOptions.ts index 910cd657..5dd41539 100644 --- a/openvidu-node-client/src/TokenOptions.ts +++ b/openvidu-node-client/src/TokenOptions.ts @@ -18,10 +18,17 @@ import { OpenViduRole } from './OpenViduRole'; /** - * See [[Session.generateToken]] + * @deprecated Use [[ConnectionProperties]] instead */ export interface TokenOptions { + /** + * The role assigned to this token + * + * @default PUBLISHER + */ + role?: OpenViduRole; + /** * Secure (server-side) data associated to this token. Every client will receive this data in property `Connection.data`. Object `Connection` can be retrieved by subscribing to event `connectionCreated` of Session object. * - If you have provided no data in your clients when calling method `Session.connect(TOKEN, DATA)` (`DATA` not defined), then `Connection.data` will only have this [[TokenOptions.data]] property. @@ -30,11 +37,6 @@ export interface TokenOptions { */ data?: string; - /** - * The role assigned to this token - */ - role?: OpenViduRole; - /** * **WARNING**: experimental option. This interface may change in the near future * diff --git a/openvidu-node-client/src/index.ts b/openvidu-node-client/src/index.ts index 4113ac84..669b4e90 100644 --- a/openvidu-node-client/src/index.ts +++ b/openvidu-node-client/src/index.ts @@ -3,6 +3,8 @@ export * from './OpenViduRole'; export * from './Session'; export * from './SessionProperties'; export * from './TokenOptions'; +export * from './ConnectionProperties'; +export * from './ConnectionType'; export * from './MediaMode'; export * from './RecordingLayout'; export * from './RecordingMode'; diff --git a/openvidu-server/deployments/ce/aws/CF-OpenVidu.yaml.template b/openvidu-server/deployments/ce/aws/CF-OpenVidu.yaml.template index f6607d9c..caedbb64 100644 --- a/openvidu-server/deployments/ce/aws/CF-OpenVidu.yaml.template +++ b/openvidu-server/deployments/ce/aws/CF-OpenVidu.yaml.template @@ -44,18 +44,18 @@ Parameters: # OpenVidu configuration OpenViduSecret: - Description: "Secret to connect to this OpenVidu Platform. No whitespaces or quotations allowed" + Description: "Secret to connect to this OpenVidu Platform. Cannot be empty and must contain only alphanumeric characters [a-zA-Z0-9], hypens ('-') and underscores ('_')" Type: String - AllowedPattern: ^((?!")(?! ).)+$ + AllowedPattern: ^[a-zA-Z0-9_-]+$ NoEcho: true - ConstraintDescription: OpenVidu Secret is mandatory + ConstraintDescription: "Cannot be empty and must contain only alphanumeric characters [a-zA-Z0-9], hypens ('-') and underscores ('_')" # EC2 Instance configuration InstanceType: Description: "Specifies the EC2 instance type for your OpenVidu instance" Type: String - Default: t2.xlarge + Default: c5.xlarge AllowedValues: - t2.large - t2.xlarge @@ -115,10 +115,12 @@ Parameters: - false Default: true +#start_mappings Mappings: OVAMIMAP: eu-west-1: AMI: OV_AMI_ID +#end_mappings Metadata: 'AWS::CloudFormation::Interface': @@ -272,6 +274,10 @@ Resources: # Restart all services pushd /opt/openvidu + docker-compose up -d kms + until docker-compose ps | grep kms | grep healthy; do + echo "Waiting kms..." + done docker-compose up -d popd mode: "000755" @@ -293,6 +299,8 @@ Resources: cfn-init --region ${AWS::Region} --stack ${AWS::StackId} --resource OpenviduServer + export HOME="/root" + # Replace .env variables /usr/local/bin/feedGroupVars.sh || { echo "[Openvidu] Parameters incorrect/insufficient"; exit 1; } @@ -306,6 +314,10 @@ Resources: # Start openvidu application pushd /opt/openvidu + docker-compose up -d kms + until docker-compose ps | grep kms | grep healthy; do + echo "Waiting kms..." + done docker-compose up -d popd @@ -327,10 +339,9 @@ Resources: BlockDeviceMappings: - DeviceName: /dev/sda1 Ebs: - VolumeType: io1 - Iops: 200 + VolumeType: gp2 DeleteOnTermination: true - VolumeSize: 100 + VolumeSize: 200 MyEIP: Type: 'AWS::EC2::EIPAssociation' @@ -373,11 +384,11 @@ Resources: CidrIp: 0.0.0.0/0 - IpProtocol: udp FromPort: 40000 - ToPort: 65535 + ToPort: 57000 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 40000 - ToPort: 65535 + ToPort: 57000 CidrIp: 0.0.0.0/0 Outputs: diff --git a/openvidu-server/deployments/ce/aws/cfn-mkt-ov-ce-ami.yaml.template b/openvidu-server/deployments/ce/aws/cfn-mkt-ov-ce-ami.yaml.template index c1a951f9..7223023f 100644 --- a/openvidu-server/deployments/ce/aws/cfn-mkt-ov-ce-ami.yaml.template +++ b/openvidu-server/deployments/ce/aws/cfn-mkt-ov-ce-ami.yaml.template @@ -141,5 +141,5 @@ Resources: Type: AWS::CloudFormation::WaitCondition CreationPolicy: ResourceSignal: - Timeout: PT10M + Timeout: PT20M Count: 1 diff --git a/openvidu-server/deployments/ce/aws/createAMI.sh b/openvidu-server/deployments/ce/aws/createAMI.sh index adeb10d7..c65f4af8 100755 --- a/openvidu-server/deployments/ce/aws/createAMI.sh +++ b/openvidu-server/deployments/ce/aws/createAMI.sh @@ -1,20 +1,15 @@ #!/bin/bash -x set -eu -o pipefail -CF_OVP_TARGET=${CF_OVP_TARGET:-nomarket} CF_RELEASE=${CF_RELEASE:-false} +AWS_KEY_NAME=${AWS_KEY_NAME:-} if [[ $CF_RELEASE == "true" ]]; then git checkout v$OPENVIDU_VERSION fi -if [ ${CF_OVP_TARGET} == "market" ]; then - export AWS_ACCESS_KEY_ID=${NAEVA_AWS_ACCESS_KEY_ID} - export AWS_SECRET_ACCESS_KEY=${NAEVA_AWS_SECRET_ACCESS_KEY} - export AWS_DEFAULT_REGION=us-east-1 -else - export AWS_DEFAULT_REGION=eu-west-1 -fi +export AWS_DEFAULT_REGION=eu-west-1 + DATESTAMP=$(date +%s) TEMPJSON=$(mktemp -t cloudformation-XXX --suffix .json) @@ -22,13 +17,13 @@ TEMPJSON=$(mktemp -t cloudformation-XXX --suffix .json) # Get Latest Ubuntu AMI id from specified region # Parameters # $1 Aws region + getUbuntuAmiId() { local AMI_ID=$( aws --region ${1} ec2 describe-images \ - --filters Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64* \ - --query 'Images[*].[ImageId,CreationDate]' \ - --output text \ - | sort -k2 -r | head -n1 | cut -d$'\t' -f1 + --filters "Name=name,Values=*ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*" \ + --query "sort_by(Images, &CreationDate)" \ + | jq -r 'del(.[] | select(.ImageOwnerAlias != null)) | .[-1].ImageId' ) echo $AMI_ID } @@ -59,6 +54,16 @@ TEMPLATE_URL=https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/cfn-mkt-ov-ce-am # Update installation script if [[ ${UPDATE_INSTALLATION_SCRIPT} == "true" ]]; then + # Avoid overriding existing versions + # Only master and non existing versions can be overriden + if [[ ${OPENVIDU_VERSION} != "master" ]]; then + INSTALL_SCRIPT_EXISTS=true + aws s3api head-object --bucket aws.openvidu.io --key install_openvidu_$OPENVIDU_VERSION.sh || INSTALL_SCRIPT_EXISTS=false + if [[ ${INSTALL_SCRIPT_EXISTS} == "true" ]]; then + echo "Aborting updating s3://aws.openvidu.io/install_openvidu_${OPENVIDU_VERSION}.sh. File actually exists." + exit 1 + fi + fi aws s3 cp ../docker-compose/install_openvidu.sh s3://aws.openvidu.io/install_openvidu_$OPENVIDU_VERSION.sh --acl public-read fi @@ -85,11 +90,34 @@ echo "Cleaning up" aws cloudformation delete-stack --stack-name openvidu-ce-${DATESTAMP} # Wait for the instance -aws ec2 wait image-available --image-ids ${OV_RAW_AMI_ID} +# Unfortunately, aws cli does not have a way to increase timeout +WAIT_RETRIES=0 +WAIT_MAX_RETRIES=3 +until [ "${WAIT_RETRIES}" -ge "${WAIT_MAX_RETRIES}" ] +do + aws ec2 wait image-available --image-ids ${OV_RAW_AMI_ID} && break + WAIT_RETRIES=$((WAIT_RETRIES+1)) + sleep 5 +done # Updating the template sed "s/OV_AMI_ID/${OV_RAW_AMI_ID}/" CF-OpenVidu.yaml.template > CF-OpenVidu-${OPENVIDU_VERSION}.yaml sed -i "s/OPENVIDU_VERSION/${OPENVIDU_VERSION}/g" CF-OpenVidu-${OPENVIDU_VERSION}.yaml +# Update CF template +if [[ ${UPDATE_CF} == "true" ]]; then + # Avoid overriding existing versions + # Only master and non existing versions can be overriden + if [[ ${OPENVIDU_VERSION} != "master" ]]; then + CF_EXIST=true + aws s3api head-object --bucket aws.openvidu.io --key CF-OpenVidu-${OPENVIDU_VERSION}.yaml || CF_EXIST=false + if [[ ${CF_EXIST} == "true" ]]; then + echo "Aborting updating s3://aws.openvidu.io/CF-OpenVidu-${OPENVIDU_VERSION}.yaml. File actually exists." + exit 1 + fi + fi + aws s3 cp CF-OpenVidu-${OPENVIDU_VERSION}.yaml s3://aws.openvidu.io/CF-OpenVidu-${OPENVIDU_VERSION}.yaml --acl public-read +fi + rm $TEMPJSON rm cfn-mkt-ov-ce-ami.yaml diff --git a/openvidu-server/deployments/ce/aws/replicate_amis.sh b/openvidu-server/deployments/ce/aws/replicate_amis.sh index 838da435..f2708121 100755 --- a/openvidu-server/deployments/ce/aws/replicate_amis.sh +++ b/openvidu-server/deployments/ce/aws/replicate_amis.sh @@ -5,14 +5,12 @@ set -eu -o pipefail # # Input parameters: # -# OV_AMI_NAME OpenVidu AMI Name -# OV_AMI_ID OpenVidu AMI ID +# OV_AMI_NAME OpenVidu AMI Name +# OV_AMI_ID OpenVidu AMI ID +# UPDATE_CF Boolean, true if you want to update CF template by OPENVIDU_VERSION +# OPENVIDU_VERSION OpenVidu Version of the CF you want to update. It will update CF-OpenVidu-OPENVIDU_VERSION export AWS_DEFAULT_REGION=eu-west-1 -if [ ${CF_OVP_TARGET} == "market" ]; then - export AWS_ACCESS_KEY_ID=${NAEVA_AWS_ACCESS_KEY_ID} - export AWS_SECRET_ACCESS_KEY=${NAEVA_AWS_SECRET_ACCESS_KEY} -fi echo "Making original AMI public" aws ec2 wait image-exists --image-ids ${OV_AMI_ID} @@ -71,13 +69,54 @@ do ITER=$(expr $ITER + 1) done +# Print and generate replicated AMIS +REPLICATED_AMIS_FILE="replicated_amis.yaml" echo "OV IDs" +{ + echo "#start_mappings" + echo "Mappings:" + echo " OVAMIMAP:" + ITER=0 + for i in "${AMI_IDS[@]}" + do + AMI_ID=${AMI_IDS[$ITER]} + REGION=${REGIONS[$ITER]} + echo " ${REGION}:" + echo " AMI: ${AMI_ID}" + ITER=$(expr $ITER + 1) + done + echo "#end_mappings" + echo "" +} > "${REPLICATED_AMIS_FILE}" 2>&1 + +# Print replicated AMIs +cat "${REPLICATED_AMIS_FILE}" + +if [[ ${UPDATE_CF} == "true" ]]; then + if [[ ! -z ${OPENVIDU_VERSION} ]]; then + # Download s3 file + aws s3 cp s3://aws.openvidu.io/CF-OpenVidu-${OPENVIDU_VERSION}.yaml CF-OpenVidu-${OPENVIDU_VERSION}.yaml + sed -e "/^#end_mappings/r ${REPLICATED_AMIS_FILE}" -e '/^#start_mappings/,/^#end_mappings/d' -i CF-OpenVidu-${OPENVIDU_VERSION}.yaml + aws s3 cp CF-OpenVidu-${OPENVIDU_VERSION}.yaml s3://aws.openvidu.io/CF-OpenVidu-${OPENVIDU_VERSION}.yaml --acl public-read + fi +fi + +# Print AMI_LIST for delete_amis.sh +AMI_LIST="" ITER=0 for i in "${AMI_IDS[@]}" do AMI_ID=${AMI_IDS[$ITER]} REGION=${REGIONS[$ITER]} - echo " ${REGION}:" - echo " AMI: ${AMI_ID}" + if [[ ${ITER} -eq 0 ]]; then + AMI_LIST="${REGION}:${AMI_ID}" + else + AMI_LIST="${AMI_LIST},${REGION}:${AMI_ID}" + fi ITER=$(expr $ITER + 1) -done \ No newline at end of file +done +echo "AMI_LIST: ${AMI_LIST}" + +# Cleaning the house +rm "${REPLICATED_AMIS_FILE}" +rm CF-OpenVidu-${OPENVIDU_VERSION}.yaml \ No newline at end of file diff --git a/openvidu-server/deployments/ce/docker-compose/.env b/openvidu-server/deployments/ce/docker-compose/.env index fe1e7693..ac5e69ab 100644 --- a/openvidu-server/deployments/ce/docker-compose/.env +++ b/openvidu-server/deployments/ce/docker-compose/.env @@ -39,6 +39,22 @@ LETSENCRYPT_EMAIL=user@example.com # SDKs, REST clients and browsers will have to connect to this port # HTTPS_PORT=443 +# Old paths are considered now deprecated, but still supported by default. +# OpenVidu Server will log a WARN message every time a deprecated path is called, indicating +# the new path that should be used instead. You can set property SUPPORT_DEPRECATED_API=false +# to stop allowing the use of old paths. +# Default value is true +# SUPPORT_DEPRECATED_API=true + +# If true request to with www will be redirected to non-www requests +# Default value is false +# REDIRECT_WWW=false + +# How many workers to configure in nginx proxy. +# The more workers, the more requests will be handled +# Default value is 10240 +# WORKER_CONNECTIONS=10240 + # Access restrictions # In this section you will be able to restrict the IPs from which you can access to # Openvidu API and the Administration Panel @@ -110,6 +126,15 @@ OPENVIDU_STREAMS_VIDEO_MAX_SEND_BANDWIDTH=1000 # 0 means unconstrained OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH=300 +# All sessions of OpenVidu will try to force this codec. If OPENVIDU_STREAMS_ALLOW_TRANSCODING=true +# when a codec can not be forced, transcoding will be allowed +# Default value is VP8 +# OPENVIDU_STREAMS_FORCED_VIDEO_CODEC=VP8 + +# Allow transcoding if codec specified in OPENVIDU_STREAMS_FORCED_VIDEO_CODEC can not be applied +# Default value is false +# OPENVIDU_STREAMS_ALLOW_TRANSCODING=false + # true to enable OpenVidu Webhook service. false' otherwise # Values: true | false OPENVIDU_WEBHOOK=false @@ -138,10 +163,10 @@ OPENVIDU_SESSIONS_GARBAGE_INTERVAL=900 OPENVIDU_SESSIONS_GARBAGE_THRESHOLD=3600 # All sessions of OpenVidu will try to force this codec. If OPENVIDU_ALLOW_TRANSCODING=true -# when a codec can not be forced, transcoding will be allowed +# when a codec can not be forced, transcoding will be allowed # OPENVIDU_FORCED_CODEC=VP8 -# Allow transcoding if codec specified in OPENVIDU_FORCED_CODEC can not be applied +# Allow transcoding if codec specified in OPENVIDU_FORCED_CODEC can not be applied # OPENVIDU_ALLOW_TRANSCODING=false # Call Detail Record enabled @@ -154,16 +179,16 @@ OPENVIDU_CDR_PATH=/opt/openvidu/cdr # Kurento Media Server image # -------------------------- -# Docker hub kurento media server: https://hub.docker.com/r/kurento/kurento-media-server-dev +# Docker hub kurento media server: https://hub.docker.com/r/kurento/kurento-media-server # Uncomment the next line and define this variable with KMS image that you want use -# KMS_IMAGE=kurento/kurento-media-server:6.14.0 +# KMS_IMAGE=kurento/kurento-media-server:6.15.0 # Kurento Media Server Level logs # ------------------------------- # Uncomment the next line and define this variable to change # the verbosity level of the logs of KMS # Documentation: https://doc-kurento.readthedocs.io/en/stable/features/logging.html -# KMS_DEBUG_LEVEL=3,Kurento*:4,kms*:4,sdp*:4,webrtc*:4,*rtpendpoint:4,rtp*handler:4,rtpsynchronizer:4,agnosticbin:4 +# KMS_DOCKER_ENV_GST_DEBUG= # Openvidu Server Level logs # -------------------------- diff --git a/openvidu-server/deployments/ce/docker-compose/docker-compose.override.yml b/openvidu-server/deployments/ce/docker-compose/docker-compose.override.yml index c52129f9..d4796d20 100644 --- a/openvidu-server/deployments/ce/docker-compose/docker-compose.override.yml +++ b/openvidu-server/deployments/ce/docker-compose/docker-compose.override.yml @@ -9,11 +9,11 @@ services: # # Default Application # - # Openvidu-Call Version: 2.15.0 + # Openvidu-Call Version: 2.16.0 # # -------------------------------------------------------------- app: - image: openvidu/openvidu-call:2.15.0 + image: openvidu/openvidu-call:2.16.0 restart: on-failure network_mode: host environment: @@ -21,3 +21,6 @@ services: - OPENVIDU_URL=http://localhost:5443 - OPENVIDU_SECRET=${OPENVIDU_SECRET} - CALL_OPENVIDU_CERTTYPE=${CERTIFICATE_TYPE} + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" diff --git a/openvidu-server/deployments/ce/docker-compose/docker-compose.yml b/openvidu-server/deployments/ce/docker-compose/docker-compose.yml index 299b62d2..452d0968 100644 --- a/openvidu-server/deployments/ce/docker-compose/docker-compose.yml +++ b/openvidu-server/deployments/ce/docker-compose/docker-compose.yml @@ -7,11 +7,11 @@ # Application based on OpenVidu should be specified in # docker-compose.override.yml file # -# This docker-compose file coordinates all services of OpenVidu CE Platform. +# This docker-compose file coordinates all services of OpenVidu CE Platform # # This file will be overridden when update OpenVidu Platform # -# Openvidu Version: 2.15.0 +# Openvidu Version: 2.16.0 # # Installation Mode: On Premises # @@ -22,10 +22,10 @@ version: '3.1' services: openvidu-server: - image: openvidu/openvidu-server:2.15.0 + image: openvidu/openvidu-server:2.17.0-dev2 restart: on-failure network_mode: host - entrypoint: ['/bin/bash', '-c', 'export COTURN_IP=`/usr/local/bin/discover_my_public_ip.sh`; /usr/local/bin/entrypoint.sh'] + entrypoint: ['/usr/local/bin/entrypoint.sh'] volumes: - /var/run/docker.sock:/var/run/docker.sock - ${OPENVIDU_RECORDING_PATH}:${OPENVIDU_RECORDING_PATH} @@ -39,9 +39,13 @@ services: - KMS_URIS=["ws://localhost:8888/kurento"] - COTURN_REDIS_IP=127.0.0.1 - COTURN_REDIS_PASSWORD=${OPENVIDU_SECRET} + - COTURN_IP=${COTURN_IP:-auto-ipv4} + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" kms: - image: ${KMS_IMAGE:-kurento/kurento-media-server:6.14.0} + image: ${KMS_IMAGE:-kurento/kurento-media-server:6.15.0} restart: always network_mode: host ulimits: @@ -49,20 +53,29 @@ services: volumes: - /opt/openvidu/kms-crashes:/opt/openvidu/kms-crashes - ${OPENVIDU_RECORDING_PATH}:${OPENVIDU_RECORDING_PATH} + - /opt/openvidu/kurento-logs:/opt/openvidu/kurento-logs environment: - KMS_MIN_PORT=40000 - KMS_MAX_PORT=57000 - - GST_DEBUG=${KMS_DEBUG_LEVEL:-} + - GST_DEBUG=${KMS_DOCKER_ENV_GST_DEBUG:-} + - KURENTO_LOG_FILE_SIZE=${KMS_DOCKER_ENV_KURENTO_LOG_FILE_SIZE:-100} + - KURENTO_LOGS_PATH=/opt/openvidu/kurento-logs + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" redis: - image: openvidu/openvidu-redis:1.0.0 + image: openvidu/openvidu-redis:2.0.0-dev2 restart: always network_mode: host environment: - REDIS_PASSWORD=${OPENVIDU_SECRET} + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" coturn: - image: openvidu/openvidu-coturn:1.0.0 + image: openvidu/openvidu-coturn:3.0.0-dev2 restart: on-failure network_mode: host environment: @@ -72,14 +85,19 @@ services: - DB_PASSWORD=${OPENVIDU_SECRET} - MIN_PORT=57001 - MAX_PORT=65535 + - ENABLE_COTURN_LOGS=true + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" nginx: - image: openvidu/openvidu-proxy:3.0.0 + image: openvidu/openvidu-proxy:5.0.0-dev2 restart: on-failure network_mode: host volumes: - ./certificates:/etc/letsencrypt - ./owncert:/owncert + - ./custom-nginx-vhosts:/etc/nginx/vhost.d/ - ${OPENVIDU_RECORDING_CUSTOM_LAYOUT}:/opt/openvidu/custom-layout environment: - DOMAIN_OR_PUBLIC_IP=${DOMAIN_OR_PUBLIC_IP} @@ -91,3 +109,10 @@ services: - ALLOWED_ACCESS_TO_RESTAPI=${ALLOWED_ACCESS_TO_RESTAPI:-} - PROXY_MODE=CE - WITH_APP=true + - SUPPORT_DEPRECATED_API=${SUPPORT_DEPRECATED_API:-true} + - REDIRECT_WWW=${REDIRECT_WWW:-false} + - WORKER_CONNECTIONS=${WORKER_CONNECTIONS:-10240} + - PUBLIC_IP=${PROXY_PUBLIC_IP:-auto-ipv4} + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" diff --git a/openvidu-server/deployments/ce/docker-compose/install_openvidu.sh b/openvidu-server/deployments/ce/docker-compose/install_openvidu.sh index 634d104d..48a18790 100755 --- a/openvidu-server/deployments/ce/docker-compose/install_openvidu.sh +++ b/openvidu-server/deployments/ce/docker-compose/install_openvidu.sh @@ -3,6 +3,7 @@ # Global variables OPENVIDU_FOLDER=openvidu OPENVIDU_VERSION=master +OPENVIDU_UPGRADABLE_VERSION="2.16" DOWNLOAD_URL=https://raw.githubusercontent.com/OpenVidu/openvidu/${OPENVIDU_VERSION} fatal_error() { @@ -42,10 +43,6 @@ new_ov_installation() { --output "${OPENVIDU_FOLDER}/openvidu" || fatal_error "Error when downloading the file 'openvidu'" printf '\n - openvidu' - curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/ce/docker-compose/readme.md \ - --output "${OPENVIDU_FOLDER}/readme.md" || fatal_error "Error when downloading the file 'readme.md'" - printf '\n - readme.md' - # Add execution permissions printf "\n => Adding permission to 'openvidu' program..." chmod +x "${OPENVIDU_FOLDER}/openvidu" || fatal_error "Error while adding permission to 'openvidu' program" @@ -54,6 +51,10 @@ new_ov_installation() { printf "\n => Creating folder 'owncert'..." mkdir "${OPENVIDU_FOLDER}/owncert" || fatal_error "Error while creating the folder 'owncert'" + # Create vhost nginx folder + printf "\n => Creating folder 'custom-nginx-vhosts'..." + mkdir "${OPENVIDU_FOLDER}/custom-nginx-vhosts" || fatal_error "Error while creating the folder 'custom-nginx-vhosts'" + # Ready to use printf '\n' printf '\n' @@ -105,10 +106,13 @@ upgrade_ov() { # Uppgrade Openvidu OPENVIDU_PREVIOUS_VERSION=$(grep 'Openvidu Version:' "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml" | awk '{ print $4 }') - [ -z "${OPENVIDU_PREVIOUS_VERSION}" ] && OPENVIDU_PREVIOUS_VERSION=2.13.0 + [ -z "${OPENVIDU_PREVIOUS_VERSION}" ] && fatal_error "Can't find previous OpenVidu version" # In this point using the variable 'OPENVIDU_PREVIOUS_VERSION' we can verify if the upgrade is # posible or not. If it is not posible launch a warning and stop the upgrade. + if [[ "${OPENVIDU_PREVIOUS_VERSION}" != "${OPENVIDU_UPGRADABLE_VERSION}."* ]]; then + fatal_error "You can't update from version ${OPENVIDU_PREVIOUS_VERSION} to ${OPENVIDU_VERSION}.\nNever upgrade across multiple major versions." + fi printf '\n' printf '\n =======================================' @@ -146,10 +150,6 @@ upgrade_ov() { --output "${TMP_FOLDER}/openvidu" || fatal_error "Error when downloading the file 'openvidu'" printf '\n - openvidu' - curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/ce/docker-compose/readme.md \ - --output "${TMP_FOLDER}/readme.md" || fatal_error "Error when downloading the file 'readme.md'" - printf '\n - readme.md' - # Dowloading new images and stoped actual Openvidu printf '\n => Dowloading new images...' printf '\n' @@ -187,12 +187,14 @@ upgrade_ov() { mv "${OPENVIDU_PREVIOUS_FOLDER}/openvidu" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'openvidu'" printf '\n - openvidu' - mv "${OPENVIDU_PREVIOUS_FOLDER}/readme.md" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'readme.md'" - printf '\n - readme.md' - cp "${OPENVIDU_PREVIOUS_FOLDER}/.env" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous '.env'" printf '\n - .env' + if [ -d "${OPENVIDU_PREVIOUS_FOLDER}/custom-nginx-vhosts" ]; then + mv "${OPENVIDU_PREVIOUS_FOLDER}/custom-nginx-vhosts" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous directory 'custom-nginx-vhosts'" + printf '\n - custom-nginx-vhosts' + fi + # Move tmp files to Openvidu printf '\n => Updating files:' @@ -213,9 +215,6 @@ upgrade_ov() { mv "${TMP_FOLDER}/openvidu" "${OPENVIDU_PREVIOUS_FOLDER}" || fatal_error "Error while updating 'openvidu'" printf '\n - openvidu' - mv "${TMP_FOLDER}/readme.md" "${OPENVIDU_PREVIOUS_FOLDER}" || fatal_error "Error while updating 'readme.md'" - printf '\n - readme.md' - printf "\n => Deleting 'tmp' folder" rm -rf "${TMP_FOLDER}" || fatal_error "Error deleting 'tmp' folder" @@ -249,7 +248,9 @@ upgrade_ov() { printf '\n' printf "\n If you want to rollback, all the files from the previous installation have been copied to folder '.old-%s'" "${OPENVIDU_PREVIOUS_VERSION}" printf '\n' - printf '\n For more information, check readme.md' + printf '\n For more information, check:' + printf "\n https://docs.openvidu.io/en/${OPENVIDU_VERSION//v}/deployment/deploying-on-premises/" + printf "\n https://docs.openvidu.io/en/${OPENVIDU_VERSION//v}/deployment/upgrading/" printf '\n' printf '\n' } diff --git a/openvidu-server/deployments/ce/docker-compose/openvidu b/openvidu-server/deployments/ce/docker-compose/openvidu index 64a67c36..3e607918 100755 --- a/openvidu-server/deployments/ce/docker-compose/openvidu +++ b/openvidu-server/deployments/ce/docker-compose/openvidu @@ -159,6 +159,16 @@ generate_report() { printf '\n' done + printf '\n' + printf "\n ---------------------------------------" + printf "\n KMS" + printf "\n ---------------------------------------" + printf '\n' + kurento_logs + printf "\n ---------------------------------------" + printf '\n' + printf '\n' + printf "\n =======================================" printf "\n = CONTAINER ENVS VARIABLES =" printf "\n =======================================" @@ -189,7 +199,8 @@ usage() { printf "\n\tstart\t\t\tStart all services" printf "\n\tstop\t\t\tStop all services" printf "\n\trestart\t\t\tRestart all stoped and running services" - printf "\n\tlogs\t\t\tShow openvidu-server logs" + printf "\n\tlogs [-f]\t\tShow openvidu logs." + printf "\n\tkms-logs [-f]\t\tShow kms logs" printf "\n\tupgrade\t\t\tUpgrade to the lastest Openvidu version" printf "\n\tupgrade [version]\tUpgrade to the specific Openvidu version" printf "\n\tversion\t\t\tShow version of Openvidu Server" @@ -198,6 +209,14 @@ usage() { printf "\n" } +kurento_logs() { + if [[ "$1" == "-f" ]]; then + tail -f /opt/openvidu/kurento-logs/*.log + else + cat /opt/openvidu/kurento-logs/*.log + fi +} + case $1 in start) docker-compose up -d @@ -215,7 +234,18 @@ case $1 in ;; logs) - docker-compose logs -f openvidu-server + case $2 in + "-f") + docker-compose logs -f openvidu-server + ;; + *) + docker-compose logs openvidu-server + ;; + esac + ;; + + kms-logs) + kurento_logs $2 ;; upgrade) diff --git a/openvidu-server/deployments/ce/taskcat/taskcat.yml b/openvidu-server/deployments/ce/taskcat/taskcat.yml index 67169614..f43a41f7 100644 --- a/openvidu-server/deployments/ce/taskcat/taskcat.yml +++ b/openvidu-server/deployments/ce/taskcat/taskcat.yml @@ -1,6 +1,6 @@ --- global: - owner: openvidu@gmail.com + owner: info@openvidu.io qsname: openvidu-ce regions: - us-east-1 diff --git a/openvidu-server/deployments/pro/aws/cfn-mkt-kms-ami.yaml.template b/openvidu-server/deployments/pro/aws/cfn-mkt-kms-ami.yaml.template index 8cfb9169..d1ee758d 100644 --- a/openvidu-server/deployments/pro/aws/cfn-mkt-kms-ami.yaml.template +++ b/openvidu-server/deployments/pro/aws/cfn-mkt-kms-ami.yaml.template @@ -142,5 +142,5 @@ Resources: Type: AWS::CloudFormation::WaitCondition CreationPolicy: ResourceSignal: - Timeout: PT10M + Timeout: PT20M Count: 1 diff --git a/openvidu-server/deployments/pro/aws/cfn-mkt-ov-ami.yaml.template b/openvidu-server/deployments/pro/aws/cfn-mkt-ov-ami.yaml.template index e2a5fda5..d27c1e1b 100644 --- a/openvidu-server/deployments/pro/aws/cfn-mkt-ov-ami.yaml.template +++ b/openvidu-server/deployments/pro/aws/cfn-mkt-ov-ami.yaml.template @@ -104,6 +104,12 @@ Resources: Tags: - Key: Name Value: !Ref AWS::StackName + BlockDeviceMappings: + - DeviceName: /dev/sda1 + Ebs: + VolumeType: gp2 + DeleteOnTermination: true + VolumeSize: 10 UserData: "Fn::Base64": !Sub | @@ -138,5 +144,5 @@ Resources: Type: AWS::CloudFormation::WaitCondition CreationPolicy: ResourceSignal: - Timeout: PT10M + Timeout: PT20M Count: 1 diff --git a/openvidu-server/deployments/pro/aws/cfn-openvidu-server-pro-no-market.yaml.template b/openvidu-server/deployments/pro/aws/cfn-openvidu-server-pro-no-market.yaml.template index 6d60034f..939fb6cd 100644 --- a/openvidu-server/deployments/pro/aws/cfn-openvidu-server-pro-no-market.yaml.template +++ b/openvidu-server/deployments/pro/aws/cfn-openvidu-server-pro-no-market.yaml.template @@ -41,6 +41,22 @@ Parameters: LetsEncryptEmail: Description: "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications" Type: String + + Recording: + Description: | + If 'disabled', recordings will not be active. + If 'local' recordings will be saved in EC2 instance locally. + If 's3', recordings will be stored in a S3 bucket" + Type: String + AllowedValues: + - disabled + - local + - s3 + Default: local + + S3RecordingsBucketName: + Description: "S3 Bucket Name" + Type: String # OpenVidu Configuration @@ -52,39 +68,51 @@ Parameters: ConstraintDescription: OpenVidu Pro License is mandatory OpenViduSecret: - Description: "Secret to connect to this OpenVidu Platform. No whitespaces or quotations allowed" + Description: "Secret to connect to this OpenVidu Platform. Cannot be empty and must contain only alphanumeric characters [a-zA-Z0-9], hypens ('-') and underscores ('_')" Type: String - AllowedPattern: ^((?!")(?! ).)+$ + AllowedPattern: ^[a-zA-Z0-9_-]+$ NoEcho: true - ConstraintDescription: OpenVidu Secret is mandatory (no whitespaces or quotations allowed) + ConstraintDescription: "Cannot be empty and must contain only alphanumeric characters [a-zA-Z0-9], hypens ('-') and underscores ('_')" MediaNodesStartNumber: Description: "How many Media Nodes do you want on startup (EC2 instances will be launched)" Type: Number Default: 1 - # Kibana configuration - - KibanaUser: - Description: "Username for Kibana Dashboard" + # Elasticsearch configuration + ElasticsearchUser: + Description: "Username for Elasticsearch and Kibana" Type: String AllowedPattern: ^((?!")(?! ).)+$ - ConstraintDescription: Kibana user is mandatory (no whitespaces or quotations allowed) - Default: kibanaadmin + ConstraintDescription: Elasticsearch user is mandatory (no whitespaces or quotations allowed) + Default: elasticadmin - KibanaPassword: - Description: "Password for Kibana Dashboard" + ElasticsearchPassword: + Description: "Password for Elasticsearch and Kibana" Type: String AllowedPattern: ^((?!")(?! ).)+$ NoEcho: true - ConstraintDescription: Kibana password is mandatory (no whitespaces or quotations allowed) + ConstraintDescription: Elasticsearch password is mandatory (no whitespaces or quotations allowed) + + # Elasticsearch configuration + ElasticsearchUrl: + Description: "If you have an external Elasticsearch service running, put here the url to the service. If empty, an Elasticsearch service will be deployed next to OpenVidu." + Type: String + AllowedPattern: (^(http|https):\/\/.*:[1-9]{1,5}+.*$|^$) + ConstraintDescription: "It is very important to specify the Elasticsearch URL with the port used by this service. For example: https://es-example:443" + + KibanaUrl: + Description: "If you have an external Kibana service running, put here the url to the service. If empty, a Kibana service will be deployed next to OpenVidu." + Type: String + AllowedPattern: (^(http|https):\/\/.*:[1-9]{1,5}+.*$|^$) + ConstraintDescription: "It is very important to specify the url with port used by this service. For example: https://kibana-example:443" # EC2 Instance configuration AwsInstanceTypeOV: Description: "Specifies the EC2 instance type for your OpenVidu Server Pro Node" Type: String - Default: t2.xlarge + Default: c5.xlarge AllowedValues: - t2.large - t2.xlarge @@ -124,7 +152,7 @@ Parameters: AwsInstanceTypeKMS: Description: "Specifies the EC2 instance type for your Media Nodes" Type: String - Default: t2.xlarge + Default: c5.xlarge AllowedValues: - t2.large - t2.xlarge @@ -186,6 +214,7 @@ Parameters: - false Default: true +#start_mappings Mappings: OVAMIMAP: eu-west-1: @@ -194,6 +223,7 @@ Mappings: KMSAMIMAP: eu-west-1: AMI: KMS_AMI_ID +#end_mappings Metadata: 'AWS::CloudFormation::Interface': @@ -214,10 +244,17 @@ Metadata: - OpenViduSecret - MediaNodesStartNumber - Label: - default: Kibana configuration + default: OpenVidu Recording Configuration Parameters: - - KibanaUser - - KibanaPassword + - Recording + - S3RecordingsBucketName + - Label: + default: Elasticsearch and Kibana configuration + Parameters: + - ElasticsearchUrl + - KibanaUrl + - ElasticsearchUser + - ElasticsearchPassword - Label: default: EC2 Instance configuration Parameters: @@ -248,6 +285,10 @@ Metadata: default: "URL to the key file (owncert)" LetsEncryptEmail: default: "Email for Let's Encrypt (letsencrypt)" + Recording: + default: "OpenVidu Recording" + S3RecordingsBucketName: + default: "S3 Bucket where recordings will be stored" # OpenVidu configuration OpenViduLicense: default: "OpenVidu Pro License key" @@ -256,10 +297,14 @@ Metadata: OpenViduSecret: default: "Openvidu Secret" # Kibana configuration - KibanaUser: - default: "Kibana username" - KibanaPassword: - default: "Kibana password" + ElasticsearchUrl: + default: "Elasticsearch URL" + KibanaUrl: + default: "Kibana URL" + ElasticsearchUser: + default: "Elasticsearch and Kibana username" + ElasticsearchPassword: + default: "Elasticsearch and Kibana password" # EC2 instance configuration AwsInstanceTypeOV: default: "Instance type for Openvidu Server Pro Node" @@ -277,8 +322,22 @@ Metadata: default: "Deploy OpenVidu Call application" Conditions: - WhichCertPresent: !Not [ !Equals [!Ref WhichCert, ""] ] - PublicElasticIPPresent: !Not [ !Equals [!Ref PublicElasticIP, ""] ] + WhichCertPresent: !Not [ !Equals [!Ref WhichCert, ''] ] + PublicElasticIPPresent: !Not [ !Equals [!Ref PublicElasticIP, ''] ] + RecordingStorageIsS3: !Equals [ !Ref Recording, 's3' ] + CreateS3Bucket: !And + - !Equals [!Ref Recording, 's3' ] + - !Equals [!Ref S3RecordingsBucketName, ''] + +Rules: + + RecordingValidation: + RuleCondition: + Fn::Or: [ !Equals [!Ref Recording, 'disabled' ], !Equals [!Ref Recording, 'local' ] ] + Assertions: + - AssertDescription: If recording Storage is 'disabled' or 'local', you don't need to specify a S3 bucket. + Assert: + Fn::Equals: [ !Ref S3RecordingsBucketName, "" ] Resources: @@ -313,6 +372,45 @@ Resources: - 'route53:ChangeResourceRecordSets' - 'route53:ListHostedZones' Resource: '*' + - Fn::If: + # Only apply this policy if S3 is configured + - RecordingStorageIsS3 + - Effect: Allow + Action: + - 's3:DeleteObject' + - 's3:GetObject' + - 's3:PutObject' + Resource: + - Fn::If: + # Get bucket name depending if the user defines a bucket name or not + - CreateS3Bucket + ### Unique bucket name using Stack ID + - !Join [ "", [ 'arn:aws:s3:::', 'openvidu-recordings-', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]], "/*" ]] + - !Join [ "", [ 'arn:aws:s3:::', !Ref S3RecordingsBucketName, '/*'] ] + - Ref: AWS::NoValue + - Fn::If: + # Only apply this policy if S3 is configured + - RecordingStorageIsS3 + - Effect: Allow + Action: + - 's3:ListBucket' + - 's3:GetBucketLocation' + Resource: + - Fn::If: + # Get bucket name depending if the user defines a bucket name or not + - CreateS3Bucket + ### Unique bucket name using Stack ID + - !Join [ "", [ 'arn:aws:s3:::', 'openvidu-recordings-', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]]]] + - !Join [ "", [ 'arn:aws:s3:::', !Ref S3RecordingsBucketName ] ] + - Ref: AWS::NoValue + - Fn::If: + # Only apply this policy if S3 is configured + - RecordingStorageIsS3 + - Effect: Allow + Action: + - s3:ListAllMyBuckets + Resource: 'arn:aws:s3:::' + - Ref: AWS::NoValue RoleName: !Join [ "-", [ OpenViduManageEC2Role, !Ref 'AWS::StackName', !Ref 'AWS::Region'] ] OpenviduInstancesProfile: @@ -325,6 +423,21 @@ Resources: DependsOn: - OpenViduManageEC2Role + S3bucket: + Type: 'AWS::S3::Bucket' + Properties: + ### Unique bucket name using Stack ID + BucketName: !Join ["-" , [ 'openvidu-recordings', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]]]] + AccessControl: Private + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls : true + RestrictPublicBuckets: true + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Condition: CreateS3Bucket + OpenViduServer: Type: AWS::EC2::Instance Metadata: @@ -383,9 +496,15 @@ Resources: sed -i "s/CERTIFICATE_TYPE=selfsigned/CERTIFICATE_TYPE=${WhichCert}/" $WORKINGDIR/.env sed -i "s/LETSENCRYPT_EMAIL=user@example.com/LETSENCRYPT_EMAIL=${LetsEncryptEmail}/" $WORKINGDIR/.env - # Replace Kibana Conf - sed -i "s/KIBANA_USER=kibanaadmin/KIBANA_USER=${KibanaUser}/" $WORKINGDIR/.env - sed -i "s/KIBANA_PASSWORD=/KIBANA_PASSWORD=${KibanaPassword}/" $WORKINGDIR/.env + # Replace Elastic Search Conf + if [[ ! -z "${ElasticsearchUrl}" ]]; then + sed -i "s,#OPENVIDU_PRO_ELASTICSEARCH_HOST=,OPENVIDU_PRO_ELASTICSEARCH_HOST=${ElasticsearchUrl}," $WORKINGDIR/.env + fi + if [[ ! -z "${KibanaUrl}" ]]; then + sed -i "s,#OPENVIDU_PRO_KIBANA_HOST=,OPENVIDU_PRO_KIBANA_HOST=${KibanaUrl}," $WORKINGDIR/.env + fi + sed -i "s/ELASTICSEARCH_USERNAME=elasticadmin/ELASTICSEARCH_USERNAME=${ElasticsearchUser}/" $WORKINGDIR/.env + sed -i "s/ELASTICSEARCH_PASSWORD=/ELASTICSEARCH_PASSWORD=${ElasticsearchPassword}/" $WORKINGDIR/.env # Replace vars AWS sed -i "s/#AWS_DEFAULT_REGION=/AWS_DEFAULT_REGION=${AWS::Region}/" $WORKINGDIR/.env @@ -395,6 +514,8 @@ Resources: sed -i "s/#AWS_SUBNET_ID=/AWS_SUBNET_ID=${OpenViduSubnet}/" $WORKINGDIR/.env sed -i "s/#AWS_STACK_ID=/AWS_STACK_ID=$(echo ${AWS::StackId} | sed 's#/#\\/#g')/" $WORKINGDIR/.env sed -i "s/#AWS_STACK_NAME=/AWS_STACK_NAME=${AWS::StackName}/" $WORKINGDIR/.env + sed -i "s/#AWS_CLI_DOCKER_TAG=/AWS_CLI_DOCKER_TAG=AWS_DOCKER_TAG/" $WORKINGDIR/.env + sed -i "s/#AWS_VOLUME_SIZE=/AWS_VOLUME_SIZE=50/" $WORKINGDIR/.env # Get security group id of kms and use it as env variable SECGRPIDKMS=$(/usr/local/bin/getSecurityGroupKms.sh) @@ -405,7 +526,20 @@ Resources: sed -i "s/WITH_APP=true/WITH_APP=false/" $WORKINGDIR/docker-compose.yml rm $WORKINGDIR/docker-compose.override.yml fi + + # Recording Configuration + if [ "${Recording}" != "disabled" ]; then + sed -i "s/OPENVIDU_RECORDING=false/OPENVIDU_RECORDING=true/" $WORKINGDIR/.env + sed -i "s/#OPENVIDU_PRO_RECORDING_STORAGE=/OPENVIDU_PRO_RECORDING_STORAGE=${Recording}/" $WORKINGDIR/.env + if [ ! -z "${S3RecordingsBucketName}" ]; then + sed -i "s/#OPENVIDU_PRO_AWS_S3_BUCKET=/OPENVIDU_PRO_AWS_S3_BUCKET=${S3RecordingsBucketName}/" $WORKINGDIR/.env + elif [ "${Recording}" == "s3" ]; then + sed -i "s/#OPENVIDU_PRO_AWS_S3_BUCKET=/OPENVIDU_PRO_AWS_S3_BUCKET=${s3BucketName}/" $WORKINGDIR/.env + fi + fi - kmsAmi: !FindInMap [KMSAMIMAP, !Ref 'AWS::Region', AMI] + ### Unique bucket name using Stack ID + s3BucketName: !Join ["" , [ 'openvidu-recordings-', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]]]] mode: "000755" owner: "root" group: "root" @@ -421,7 +555,7 @@ Resources: '/usr/local/bin/getSecurityGroupKms.sh': content: !Sub | #!/bin/bash -x - docker run --rm amazon/aws-cli:2.0.7 ec2 describe-security-groups \ + docker run --rm amazon/aws-cli:AWS_DOCKER_TAG ec2 describe-security-groups \ --region ${AWS::Region} \ --output text \ --filters "Name=tag:aws:cloudformation:logical-id,Values=KMSSecurityGroup" \ @@ -433,7 +567,7 @@ Resources: '/usr/local/bin/getSecurityGroupOpenVidu.sh': content: !Sub | #!/bin/bash -x - docker run --rm amazon/aws-cli:2.0.7 ec2 describe-security-groups \ + docker run --rm amazon/aws-cli:AWS_DOCKER_TAG ec2 describe-security-groups \ --region ${AWS::Region} \ --output text \ --filters "Name=tag:aws:cloudformation:logical-id,Values=OpenViduSecurityGroup" \ @@ -445,7 +579,7 @@ Resources: '/usr/local/bin/getCidrBlocKSubnet.sh': content: !Sub | #!/bin/bash -x - docker run --rm amazon/aws-cli:2.0.7 ec2 describe-subnets \ + docker run --rm amazon/aws-cli:AWS_DOCKER_TAG ec2 describe-subnets \ --region ${AWS::Region} \ --output text \ --filters "Name=subnet-id,Values=${OpenViduSubnet}" \ @@ -454,25 +588,25 @@ Resources: owner: "root" group: "root" '/usr/local/bin/create_security_group_rules.sh': - content: !Sub | + content: | #!/bin/bash -x SECGRPIDKMS=$(/usr/local/bin/getSecurityGroupKms.sh) SECGRPIDOV=$(/usr/local/bin/getSecurityGroupOpenVidu.sh) SUBNET_CIDR=$(/usr/local/bin/getCidrBlocKSubnet.sh) # Create Security group rules OpenVidu - docker run --rm amazon/aws-cli:2.0.7 ec2 authorize-security-group-ingress --group-id $SECGRPIDOV --protocol tcp --port 5044 --cidr $SUBNET_CIDR - docker run --rm amazon/aws-cli:2.0.7 ec2 authorize-security-group-ingress --group-id $SECGRPIDOV --protocol tcp --port 9200 --cidr $SUBNET_CIDR + docker run --rm amazon/aws-cli:AWS_DOCKER_TAG ec2 authorize-security-group-ingress --group-id $SECGRPIDOV --protocol tcp --port 5044 --cidr $SUBNET_CIDR + docker run --rm amazon/aws-cli:AWS_DOCKER_TAG ec2 authorize-security-group-ingress --group-id $SECGRPIDOV --protocol tcp --port 9200 --cidr $SUBNET_CIDR # Create security group rules for KMS - docker run --rm amazon/aws-cli:2.0.7 ec2 authorize-security-group-ingress --group-id $SECGRPIDKMS --protocol tcp --port 8888 --cidr $SUBNET_CIDR - docker run --rm amazon/aws-cli:2.0.7 ec2 authorize-security-group-ingress --group-id $SECGRPIDKMS --protocol tcp --port 3000 --cidr $SUBNET_CIDR + docker run --rm amazon/aws-cli:AWS_DOCKER_TAG ec2 authorize-security-group-ingress --group-id $SECGRPIDKMS --protocol tcp --port 8888 --cidr $SUBNET_CIDR + docker run --rm amazon/aws-cli:AWS_DOCKER_TAG ec2 authorize-security-group-ingress --group-id $SECGRPIDKMS --protocol tcp --port 3000 --cidr $SUBNET_CIDR mode: "000755" owner: "root" group: "root" '/usr/local/bin/restartPRO.sh': - content: !Sub | + content: | #!/bin/bash -x WORKINGDIR=/opt/openvidu @@ -484,7 +618,8 @@ Resources: # Restart all services pushd /opt/openvidu - docker-compose up -d + export FOLLOW_OPENVIDU_LOGS=false + ./openvidu start popd mode: "000755" owner: "root" @@ -509,6 +644,8 @@ Resources: cfn-init --region ${AWS::Region} --stack ${AWS::StackId} --resource OpenViduServer + export HOME="/root" + # Replace .env variables /usr/local/bin/feedGroupVars.sh || { echo "[Openvidu] Parameters incorrect/insufficient"; exit 1; } @@ -525,7 +662,8 @@ Resources: # Start openvidu application pushd /opt/openvidu - docker-compose up -d + export FOLLOW_OPENVIDU_LOGS=false + ./openvidu start popd # Wait for the app @@ -537,10 +675,9 @@ Resources: BlockDeviceMappings: - DeviceName: /dev/sda1 Ebs: - VolumeType: io1 - Iops: 200 + VolumeType: gp2 DeleteOnTermination: true - VolumeSize: 50 + VolumeSize: 200 KMSSecurityGroup: Type: 'AWS::EC2::SecurityGroup' diff --git a/openvidu-server/deployments/pro/aws/createAMIs.sh b/openvidu-server/deployments/pro/aws/createAMIs.sh index 09e7bc84..81e06087 100755 --- a/openvidu-server/deployments/pro/aws/createAMIs.sh +++ b/openvidu-server/deployments/pro/aws/createAMIs.sh @@ -1,18 +1,14 @@ #!/bin/bash -x +set -eu -o pipefail -CF_OVP_TARGET=${CF_OVP_TARGET:-nomarket} +CF_RELEASE=${CF_RELEASE:-false} +AWS_KEY_NAME=${AWS_KEY_NAME:-} -if [ ${CF_OVP_TARGET} == "market" ]; then - export AWS_ACCESS_KEY_ID=${NAEVA_AWS_ACCESS_KEY_ID} - export AWS_SECRET_ACCESS_KEY=${NAEVA_AWS_SECRET_ACCESS_KEY} - export AWS_DEFAULT_REGION=us-east-1 -else - export AWS_DEFAULT_REGION=eu-west-1 +if [[ $CF_RELEASE == "true" ]]; then + git checkout v$OPENVIDU_PRO_VERSION fi -if [ "${OPENVIDU_PRO_IS_SNAPSHOT}" == "true" ]; then - OPENVIDU_PRO_VERSION=${OPENVIDU_PRO_VERSION}-SNAPSHOT -fi +export AWS_DEFAULT_REGION=eu-west-1 DATESTAMP=$(date +%s) TEMPJSON=$(mktemp -t cloudformation-XXX --suffix .json) @@ -23,10 +19,9 @@ TEMPJSON=$(mktemp -t cloudformation-XXX --suffix .json) getUbuntuAmiId() { local AMI_ID=$( aws --region ${1} ec2 describe-images \ - --filters Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64* \ - --query 'Images[*].[ImageId,CreationDate]' \ - --output text \ - | sort -k2 -r | head -n1 | cut -d$'\t' -f1 + --filters "Name=name,Values=*ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*" \ + --query "sort_by(Images, &CreationDate)" \ + | jq -r 'del(.[] | select(.ImageOwnerAlias != null)) | .[-1].ImageId' ) echo $AMI_ID } @@ -61,12 +56,22 @@ sed -i "s/AMIUSEAST1/${AMIUSEAST1}/g" cfn-mkt-kms-ami.yaml ## KMS AMI # Copy template to S3 -if [ ${CF_OVP_TARGET} == "market" ]; then - aws s3 cp cfn-mkt-kms-ami.yaml s3://naeva-openvidu-pro - TEMPLATE_URL=https://s3-eu-west-1.amazonaws.com/naeva-openvidu-pro/cfn-mkt-kms-ami.yaml -else - aws s3 cp cfn-mkt-kms-ami.yaml s3://aws.openvidu.io - TEMPLATE_URL=https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/cfn-mkt-kms-ami.yaml +aws s3 cp cfn-mkt-kms-ami.yaml s3://aws.openvidu.io +TEMPLATE_URL=https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/cfn-mkt-kms-ami.yaml + +# Update installation script +if [[ ${UPDATE_INSTALLATION_SCRIPT} == "true" ]]; then + # Avoid overriding existing versions + # Only master and non existing versions can be overriden + if [[ ${OPENVIDU_PRO_VERSION} != "master" ]]; then + INSTALL_SCRIPT_EXISTS=true + aws s3api head-object --bucket aws.openvidu.io --key install_media_node_$OPENVIDU_PRO_VERSION.sh || INSTALL_SCRIPT_EXISTS=false + if [[ ${INSTALL_SCRIPT_EXISTS} == "true" ]]; then + echo "Aborting updating s3://aws.openvidu.io/install_media_node_${OPENVIDU_PRO_VERSION}.sh. File actually exists." + exit 1 + fi + fi + aws s3 cp ../docker-compose/media-node/install_media_node.sh s3://aws.openvidu.io/install_media_node_$OPENVIDU_PRO_VERSION.sh --acl public-read fi aws cloudformation create-stack \ @@ -94,12 +99,22 @@ aws cloudformation delete-stack --stack-name kms-${DATESTAMP} ## OpenVidu AMI # Copy template to S3 -if [ ${CF_OVP_TARGET} == "market" ]; then - aws s3 cp cfn-mkt-ov-ami.yaml s3://naeva-openvidu-pro - TEMPLATE_URL=https://s3-eu-west-1.amazonaws.com/naeva-openvidu-pro/cfn-mkt-ov-ami.yaml -else aws s3 cp cfn-mkt-ov-ami.yaml s3://aws.openvidu.io TEMPLATE_URL=https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/cfn-mkt-ov-ami.yaml + +# Update installation script +if [[ ${UPDATE_INSTALLATION_SCRIPT} == "true" ]]; then + # Avoid overriding existing versions + # Only master and non existing versions can be overriden + if [[ ${OPENVIDU_PRO_VERSION} != "master" ]]; then + INSTALL_SCRIPT_EXISTS=true + aws s3api head-object --bucket aws.openvidu.io --key install_openvidu_pro_$OPENVIDU_PRO_VERSION.sh || INSTALL_SCRIPT_EXISTS=false + if [[ ${INSTALL_SCRIPT_EXISTS} == "true" ]]; then + echo "Aborting updating s3://aws.openvidu.io/install_openvidu_pro_${OPENVIDU_PRO_VERSION}.sh. File actually exists." + exit 1 + fi + fi + aws s3 cp ../docker-compose/openvidu-server-pro/install_openvidu_pro.sh s3://aws.openvidu.io/install_openvidu_pro_$OPENVIDU_PRO_VERSION.sh --acl public-read fi aws cloudformation create-stack \ @@ -125,15 +140,35 @@ echo "Cleaning up" aws cloudformation delete-stack --stack-name openvidu-${DATESTAMP} # Wait for the instance -aws ec2 wait image-available --image-ids ${OV_RAW_AMI_ID} +# Unfortunately, aws cli does not have a way to increase timeout +WAIT_RETRIES=0 +WAIT_MAX_RETRIES=3 +until [ "${WAIT_RETRIES}" -ge "${WAIT_MAX_RETRIES}" ] +do + aws ec2 wait image-available --image-ids ${OV_RAW_AMI_ID} && break + WAIT_RETRIES=$((WAIT_RETRIES+1)) + sleep 5 +done + # Updating the template -if [ ${CF_OVP_TARGET} == "market" ]; then - sed "s/OV_AMI_ID/${OV_RAW_AMI_ID}/" cfn-mkt-openvidu-server-pro.yaml.template > cfn-mkt-openvidu-server-pro-${OPENVIDU_PRO_VERSION}.yaml - sed -i "s/KMS_AMI_ID/${KMS_RAW_AMI_ID}/g" cfn-mkt-openvidu-server-pro-${OPENVIDU_PRO_VERSION}.yaml -else - sed "s/OV_AMI_ID/${OV_RAW_AMI_ID}/" cfn-openvidu-server-pro-no-market.yaml.template > cfn-openvidu-server-pro-no-market-${OPENVIDU_PRO_VERSION}.yaml - sed -i "s/KMS_AMI_ID/${KMS_RAW_AMI_ID}/g" cfn-openvidu-server-pro-no-market-${OPENVIDU_PRO_VERSION}.yaml +sed "s/OV_AMI_ID/${OV_RAW_AMI_ID}/" cfn-openvidu-server-pro-no-market.yaml.template > cfn-openvidu-server-pro-no-market-${OPENVIDU_PRO_VERSION}.yaml +sed -i "s/KMS_AMI_ID/${KMS_RAW_AMI_ID}/g" cfn-openvidu-server-pro-no-market-${OPENVIDU_PRO_VERSION}.yaml +sed -i "s/AWS_DOCKER_TAG/${AWS_DOCKER_TAG}/g" cfn-openvidu-server-pro-no-market-${OPENVIDU_PRO_VERSION}.yaml + +# Update CF template +if [[ ${UPDATE_CF} == "true" ]]; then + # Avoid overriding existing versions + # Only master and non existing versions can be overriden + if [[ ${OPENVIDU_PRO_VERSION} != "master" ]]; then + CF_EXIST=true + aws s3api head-object --bucket aws.openvidu.io --key CF-OpenVidu-Pro-${OPENVIDU_PRO_VERSION}.yaml || CF_EXIST=false + if [[ ${CF_EXIST} == "true" ]]; then + echo "Aborting updating s3://aws.openvidu.io/CF-OpenVidu-Pro-${OPENVIDU_PRO_VERSION}.yaml. File actually exists." + exit 1 + fi + fi + aws s3 cp cfn-openvidu-server-pro-no-market-${OPENVIDU_PRO_VERSION}.yaml s3://aws.openvidu.io/CF-OpenVidu-Pro-${OPENVIDU_PRO_VERSION}.yaml --acl public-read fi rm $TEMPJSON diff --git a/openvidu-server/deployments/pro/aws/delete_amis.sh b/openvidu-server/deployments/pro/aws/delete_amis.sh index 123de55f..dbfd3a1c 100755 --- a/openvidu-server/deployments/pro/aws/delete_amis.sh +++ b/openvidu-server/deployments/pro/aws/delete_amis.sh @@ -1,47 +1,24 @@ #!/bin/bash -x set -eu -o pipefail +# Process AMI_LIST +AMI_LIST=($(echo "$AMI_LIST" | tr ',' '\n')) + # Remove the list of AMIs in each region - -export AWS_ACCESS_KEY_ID= -export AWS_SECRET_ACCESS_KEY= - -LIST="us-east-2:ami-0b779580e2c11e904 - us-west-1:ami-085b7176f53c6d7fe - us-west-2:ami-029d0ac01cf0f56be - ap-south-1:ami-044a9335de8413f90 - ap-northeast-2:ami-031f6637449d2821d - ap-southeast-1:ami-0aba433c88526cc8a - ap-southeast-2:ami-0ee526f6103ac2bd9 - ap-northeast-1:ami-03b3cc03809d43b36 - ca-central-1:ami-071388f538500db04 - eu-central-1:ami-080a9cbd1d3e64583 - eu-west-1:ami-05e6bc185f28b6338 - eu-west-2:ami-0f06e2c003eef90f1 - eu-west-3:ami-0fd9b5eaf08fc0936 - eu-north-1:ami-0e7717a400ba2f1c1 - sa-east-1:ami-0cd51a71e9791197a - us-east-2:ami-0064508c09a32a93f - us-west-1:ami-088d1a7099fa57038 - us-west-2:ami-080d4d462cff92974 - ap-south-1:ami-00a60b079166e7dd4 - ap-northeast-2:ami-0f4edc3faf639e044 - ap-southeast-1:ami-0235dbfa3662608a0 - ap-southeast-2:ami-0f5f46178512e6e07 - ap-northeast-1:ami-047b086aa0745ce19 - ca-central-1:ami-0777b151c44ef9944 - eu-central-1:ami-00dd31f7b896f233f - eu-west-1:ami-0fb9f924ede905546 - eu-west-2:ami-0defcea3b8c198e1e - eu-west-3:ami-0c56da0b482bdf48e - eu-north-1:ami-054fc49723d3d516a - sa-east-1:ami-0dca8c31325d33c72" - -for line in ${LIST} +for line in "${AMI_LIST[@]}" do - REGION=$(echo ${line} | cut -d":" -f1) - AMI_ID=$(echo ${line} | cut -d":" -f2) - export AWS_DEFAULT_REGION=${REGION} - aws ec2 deregister-image --image-id $AMI_ID - sleep 1 + REGION=$(echo "${line}" | cut -d":" -f1) + AMI_ID=$(echo "${line}" | cut -d":" -f2) + export AWS_DEFAULT_REGION=${REGION} + + mapfile -t SNAPSHOTS < <(aws ec2 describe-images --image-ids "$AMI_ID" --output text --query 'Images[*].BlockDeviceMappings[*].Ebs.SnapshotId') + echo "Deregistering $AMI_ID" + aws ec2 deregister-image --image-id "${AMI_ID}" + sleep 1 + for snapshot in "${SNAPSHOTS[@]}"; + do + echo "Deleting Snapshot $snapshot from $AMI_ID" + aws ec2 delete-snapshot --snapshot-id "${snapshot}" + sleep 1 + done done \ No newline at end of file diff --git a/openvidu-server/deployments/pro/aws/replicate_amis.sh b/openvidu-server/deployments/pro/aws/replicate_amis.sh index 1838573f..3069849f 100755 --- a/openvidu-server/deployments/pro/aws/replicate_amis.sh +++ b/openvidu-server/deployments/pro/aws/replicate_amis.sh @@ -1,4 +1,4 @@ -#!/bin/bash -x +#!/bin/bash set -eu -o pipefail # Replicate AMIs in all regions @@ -10,12 +10,11 @@ set -eu -o pipefail # # OV_AMI_NAME OpenVidu AMI Name # OV_AMI_ID OpenVidu AMI ID +# +# UPDATE_CF Boolean, true if you want to update CF template by OPENVIDU_PRO_VERSION +# OPENVIDU_VERSION OpenVidu Version of the CF you want to update. It will update CF-OpenVidu-Pro-OPENVIDU_PRO_VERSION export AWS_DEFAULT_REGION=eu-west-1 -if [ ${CF_OVP_TARGET} == "market" ]; then - export AWS_ACCESS_KEY_ID=${NAEVA_AWS_ACCESS_KEY_ID} - export AWS_SECRET_ACCESS_KEY=${NAEVA_AWS_SECRET_ACCESS_KEY} -fi echo "Making original AMIs public" @@ -96,26 +95,82 @@ do ITER=$(expr $ITER + 1) done -echo -echo "OpenVidu Server Pro Node AMI IDs" + +# Print and generate replicated AMIS +REPLICATED_AMIS_FILE="replicated_amis.yaml" +echo "OV AMIs and KMS AMIs replication:" +{ + echo "#start_mappings" + echo "Mappings:" + echo " OVAMIMAP:" + ITER=0 + for i in "${OPENVIDU_SERVER_PRO_AMI_IDS[@]}" + do + AMI_ID=${OPENVIDU_SERVER_PRO_AMI_IDS[$ITER]} + REGION=${REGIONS[$ITER]} + echo " ${REGION}:" + echo " AMI: ${AMI_ID}" + ITER=$(expr $ITER + 1) + done + echo "" + echo " KMSAMIMAP:" + ITER=0 + for i in "${MEDIA_NODE_AMI_IDS[@]}" + do + AMI_ID=${MEDIA_NODE_AMI_IDS[$ITER]} + REGION=${REGIONS[$ITER]} + echo " ${REGION}:" + echo " AMI: ${AMI_ID}" + ITER=$(expr $ITER + 1) + done + echo "#end_mappings" + echo "" +} > "${REPLICATED_AMIS_FILE}" 2>&1 + +# Print replicated AMIs +cat "${REPLICATED_AMIS_FILE}" + +if [[ ${UPDATE_CF} == "true" ]]; then + if [[ ! -z ${OPENVIDU_PRO_VERSION} ]]; then + # Download s3 file + aws s3 cp s3://aws.openvidu.io/CF-OpenVidu-Pro-${OPENVIDU_PRO_VERSION}.yaml CF-OpenVidu-Pro-${OPENVIDU_PRO_VERSION}.yaml + sed -e "/^#end_mappings/r ${REPLICATED_AMIS_FILE}" -e '/^#start_mappings/,/^#end_mappings/d' -i CF-OpenVidu-Pro-${OPENVIDU_PRO_VERSION}.yaml + aws s3 cp CF-OpenVidu-Pro-${OPENVIDU_PRO_VERSION}.yaml s3://aws.openvidu.io/CF-OpenVidu-Pro-${OPENVIDU_PRO_VERSION}.yaml --acl public-read + fi +fi + +# Print AMI_LIST for delete_amis.sh +AMI_LIST="" ITER=0 for i in "${OPENVIDU_SERVER_PRO_AMI_IDS[@]}" do AMI_ID=${OPENVIDU_SERVER_PRO_AMI_IDS[$ITER]} REGION=${REGIONS[$ITER]} - echo " ${REGION}:" - echo " AMI: ${AMI_ID}" + if [[ ${ITER} -eq 0 ]]; then + AMI_LIST="${REGION}:${AMI_ID}" + else + AMI_LIST="${AMI_LIST},${REGION}:${AMI_ID}" + fi ITER=$(expr $ITER + 1) done +echo "AMI_LIST_OV: ${AMI_LIST}" -echo -echo "Media Node AMI IDs" +# Print AMI_LIST for delete_amis.sh +AMI_LIST="" ITER=0 for i in "${MEDIA_NODE_AMI_IDS[@]}" do AMI_ID=${MEDIA_NODE_AMI_IDS[$ITER]} REGION=${REGIONS[$ITER]} - echo " ${REGION}:" - echo " AMI: ${AMI_ID}" + if [[ ${ITER} -eq 0 ]]; then + AMI_LIST="${REGION}:${AMI_ID}" + else + AMI_LIST="${AMI_LIST},${REGION}:${AMI_ID}" + fi ITER=$(expr $ITER + 1) -done \ No newline at end of file +done +echo "AMI_LIST_KMS: ${AMI_LIST}" + +# Cleaning the house +rm "${REPLICATED_AMIS_FILE}" +rm CF-OpenVidu-Pro-${OPENVIDU_PRO_VERSION}.yaml \ No newline at end of file diff --git a/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/.env b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/.env new file mode 100644 index 00000000..59371b17 --- /dev/null +++ b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/.env @@ -0,0 +1,352 @@ +# OpenVidu configuration +# ---------------------- +# Documentation: https://docs.openvidu.io/en/stable/reference-docs/openvidu-config/ + +# NOTE: This file doesn't need to quote assignment values, like most shells do. +# All values are stored as-is, even if they contain spaces, so don't quote them. + +# Domain name. If you do not have one, the public IP of the machine. +# For example: 198.51.100.1, or openvidu.example.com +DOMAIN_OR_PUBLIC_IP= + +# OpenVidu PRO License +OPENVIDU_PRO_LICENSE= + +# OpenVidu SECRET used for apps to connect to OpenVidu server and users to access to OpenVidu Dashboard +OPENVIDU_SECRET= + +# Certificate type: +# - selfsigned: Self signed certificate. Not recommended for production use. +# Users will see an ERROR when connected to web page. +# - owncert: Valid certificate purchased in a Internet services company. +# Please put the certificates files inside folder ./owncert +# with names certificate.key and certificate.cert +# - letsencrypt: Generate a new certificate using letsencrypt. Please set the +# required contact email for Let's Encrypt in LETSENCRYPT_EMAIL +# variable. +CERTIFICATE_TYPE=selfsigned + +# If CERTIFICATE_TYPE=letsencrypt, you need to configure a valid email for notifications +LETSENCRYPT_EMAIL=user@example.com + +# Proxy configuration +# If you want to change the ports on which openvidu listens, uncomment the following lines + +# Allows any request to http://DOMAIN_OR_PUBLIC_IP:HTTP_PORT/ to be automatically +# redirected to https://DOMAIN_OR_PUBLIC_IP:HTTPS_PORT/. +# WARNING: the default port 80 cannot be changed during the first boot +# if you have chosen to deploy with the option CERTIFICATE_TYPE=letsencrypt +# HTTP_PORT=80 + +# Changes the port of all services exposed by OpenVidu. +# SDKs, REST clients and browsers will have to connect to this port +# HTTPS_PORT=443 + +# Old paths are considered now deprecated, but still supported by default. +# OpenVidu Server will log a WARN message every time a deprecated path is called, indicating +# the new path that should be used instead. You can set property SUPPORT_DEPRECATED_API=false +# to stop allowing the use of old paths. +# Default value is true +# SUPPORT_DEPRECATED_API=true + +# If true request to with www will be redirected to non-www requests +# Default value is false +# REDIRECT_WWW=false + +# How many workers to configure in nginx proxy. +# The more workers, the more requests will be handled +# Default value is 10240 +# WORKER_CONNECTIONS=10240 + +# Access restrictions +# In this section you will be able to restrict the IPs from which you can access to +# Openvidu API and the Administration Panel +# WARNING! If you touch this configuration you can lose access to the platform from some IPs. +# Use it carefully. + +# This section limits access to the /dashboard (OpenVidu CE) and /inspector (OpenVidu Pro) pages. +# The form for a single IP or an IP range is: +# ALLOWED_ACCESS_TO_DASHBOARD=198.51.100.1 and ALLOWED_ACCESS_TO_DASHBOARD=198.51.100.0/24 +# To limit multiple IPs or IP ranges, separate by commas like this: +# ALLOWED_ACCESS_TO_DASHBOARD=198.51.100.1, 198.51.100.0/24 +# ALLOWED_ACCESS_TO_DASHBOARD= + +# This section limits access to the Openvidu REST API. +# The form for a single IP or an IP range is: +# ALLOWED_ACCESS_TO_RESTAPI=198.51.100.1 and ALLOWED_ACCESS_TO_RESTAPI=198.51.100.0/24 +# To limit multiple IPs or or IP ranges, separate by commas like this: +# ALLOWED_ACCESS_TO_RESTAPI=198.51.100.1, 198.51.100.0/24 +# ALLOWED_ACCESS_TO_RESTAPI= + +# Mode of cluster management. Can be auto (OpenVidu manages Media Nodes on its own. +# Parameter KMS_URIS is ignored) or manual (user must manage Media Nodes. Parameter +# KMS_URIS is used: if any uri is provided it must be valid) +OPENVIDU_PRO_CLUSTER_MODE=manual + +# Which environment are you using +# Possibles values: aws, on_premise +OPENVIDU_PRO_CLUSTER_ENVIRONMENT=on_premise + +# Unique identifier of your cluster. Each OpenVidu Server Pro instance corresponds to one cluster. +# You can launch as many clusters as you want with your license key. +# Cluster ID will always be stored to disk so restarting OpenVidu Server Pro will keep the same previous cluster ID +# if this configuration parameter is not given a distinct value. +# OPENVIDU_PRO_CLUSTER_ID= + +# The desired number of Media Nodes on startup. First the autodiscovery process is performed. +# If there are too many Media Nodes after that, they will be dropped until this number is reached. +# If there are not enough, more will be launched. +# This only takes place if OPENVIDU_PRO_CLUSTER_MODE is set to auto +# If set to zero no media servers will be lauched. +# Type: number >= 0 +#OPENVIDU_PRO_CLUSTER_MEDIA_NODES= + +# How often each running Media Node will send OpenVidu Server Pro Node load metrics, in seconds. +# This property is only used when OPENVIDU_PRO_CLUSTER_LOAD_STRATEGY is 'cpu'. Other load strategies +# gather information synchronously when required +# Type: number >= 0 +# OPENVIDU_PRO_CLUSTER_LOAD_INTERVAL= + +# Whether to enable or disable autoscaling. With autoscaling the number of Media Nodes will +# be automatically adjusted according to existing load +# Values: true | false +# OPENVIDU_PRO_CLUSTER_AUTOSCALING=false + +# How often the autoscaling algorithm runs, in seconds +# Type number >= 0 +# OPENVIDU_PRO_CLUSTER_AUTOSCALING_INTERVAL= + +# If autoscaling is enabled, the upper limit of Media Nodes that can be reached. +# Even when the average load exceeds the threshold, no more Media Nodes will be added to cluster +# Type number >= 0 +# OPENVIDU_PRO_CLUSTER_AUTOSCALING_MAX_NODES= + +# If autoscaling is enabled, the lower limit of Media Nodes that can be reached. +# Even when the average load is inferior to the threshold, no more Media Nodes will +# be removed from the cluster +# OPENVIDU_PRO_CLUSTER_AUTOSCALING_MIN_NODES= + +# If autoscaling is enabled, the upper average load threshold that will trigger the addition +# of a new Media Node. +# Percentage value (0 min, 100 max) +# OPENVIDU_PRO_CLUSTER_AUTOSCALING_MAX_LOAD= + +# If autoscaling is enabled, the lower average load threshold that will trigger the removal +# of an existing Media Node. +# Percentage value (0 min, 100 max) +# OPENVIDU_PRO_CLUSTER_AUTOSCALING_MIN_LOAD= + +# What parameter should be used to distribute the creation of new sessions +# (and therefore distribution of load) among all available Media Nodes +OPENVIDU_PRO_CLUSTER_LOAD_STRATEGY=streams + +# Whether to enable or disable Network Quality API. You can monitor and +# warn users about the quality of their networks with this feature +# OPENVIDU_PRO_NETWORK_QUALITY=false + +# If OPENVIDU_PRO_NETWORK_QUALITY=true, how often the network quality +# algorithm will be invoked for each user, in seconds +# OPENVIDU_PRO_NETWORK_QUALITY_INTERVAL=5 + +# Max days until delete indexes in state of rollover on Elasticsearch +# Type number >= 0 +# Default Value is 15 +# OPENVIDU_PRO_ELASTICSEARCH_MAX_DAYS_DELETE= + +# If you have an external Elasticsearch and Kibana already running, put here the url to elasticsearch and kibana services. +# It is very important that both url have the port specified in the url. +# If you want to use the deployed Elasticsearch and Kibana locally, keep these variables commented. +#OPENVIDU_PRO_ELASTICSEARCH_HOST= +#OPENVIDU_PRO_KIBANA_HOST= + +# Where to store recording files. Can be 'local' (local storage) or 's3' (AWS bucket). +# You will need to define a OPENVIDU_PRO_AWS_S3_BUCKET if you use it. +#OPENVIDU_PRO_RECORDING_STORAGE= + +# S3 Bucket where to store recording files. May include paths to allow navigating +# folder structures inside the bucket. This property is only taken into account +# if OPENVIDU_PRO_RECORDING_STORAGE=s3 +#OPENVIDU_PRO_AWS_S3_BUCKET= + +# If OPENVIDU_PRO_RECORDING_STORAGE=s3, the collection of HTTP header values that the internal AWS client will use during +# the upload process. The property is a key-value map of strings, following the format of a JSON object. For example, for applying +# server-side encryption with AES-256, this header is mandatory: {"x-amz-server-side-encryption":"AES256"}. +# The list of available headers can be found here: https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/Headers.html +# This property is only taken into account if OPENVIDU_PRO_RECORDING_STORAGE=s3 +#OPENVIDU_PRO_AWS_S3_HEADERS= + +# If you're instance has a role which has access to read +# and write into the s3 bucket, you don't need this parameter +# OPENVIDU_PRO_AWS_ACCESS_KEY= + +# AWS credentials secret key from OPENVIDU_PRO_AWS_ACCESS_KEY. This property is only +# taken into account if OPENVIDU_PRO_RECORDING_STORAGE=s3 +# If you're instance has a role which has access to read +# and write into the s3 bucket, you don't need this parameter +# OPENVIDU_PRO_AWS_SECRET_KEY= + +# AWS region in which the S3 bucket is located (e.g. eu-west-1). If not provided, +# the region will try to be discovered automatically, although this is not always possible. +# This property is only taken into account if OPENVIDU_PRO_RECORDING_STORAGE=s3 +# OPENVIDU_PRO_AWS_REGION= + +# Whether to enable recording module or not +OPENVIDU_RECORDING=false + +# Use recording module with debug mode. +OPENVIDU_RECORDING_DEBUG=false + +# Openvidu Folder Record used for save the openvidu recording videos. Change it +# with the folder you want to use from your host. +OPENVIDU_RECORDING_PATH=/opt/openvidu/recordings + +# System path where OpenVidu Server should look for custom recording layouts +OPENVIDU_RECORDING_CUSTOM_LAYOUT=/opt/openvidu/custom-layout + +# if true any client can connect to +# https://OPENVIDU_SERVER_IP:OPENVIDU_PORT/recordings/any_session_file.mp4 +# and access any recorded video file. If false this path will be secured with +# OPENVIDU_SECRET param just as OpenVidu Server dashboard at +# https://OPENVIDU_SERVER_IP:OPENVIDU_PORT +# Values: true | false +OPENVIDU_RECORDING_PUBLIC_ACCESS=false + +# Which users should receive the recording events in the client side +# (recordingStarted, recordingStopped). Can be all (every user connected to +# the session), publisher_moderator (users with role 'PUBLISHER' or +# 'MODERATOR'), moderator (only users with role 'MODERATOR') or none +# (no user will receive these events) +OPENVIDU_RECORDING_NOTIFICATION=publisher_moderator + +# Timeout in seconds for recordings to automatically stop (and the session involved to be closed) +# when conditions are met: a session recording is started but no user is publishing to it or a session +# is being recorded and last user disconnects. If a user publishes within the timeout in either case, +# the automatic stop of the recording is cancelled +# 0 means no timeout +OPENVIDU_RECORDING_AUTOSTOP_TIMEOUT=120 + +# Maximum video bandwidth sent from clients to OpenVidu Server, in kbps. +# 0 means unconstrained +OPENVIDU_STREAMS_VIDEO_MAX_RECV_BANDWIDTH=1000 + +# Minimum video bandwidth sent from clients to OpenVidu Server, in kbps. +# 0 means unconstrained +OPENVIDU_STREAMS_VIDEO_MIN_RECV_BANDWIDTH=300 + +# Maximum video bandwidth sent from OpenVidu Server to clients, in kbps. +# 0 means unconstrained +OPENVIDU_STREAMS_VIDEO_MAX_SEND_BANDWIDTH=1000 + +# Minimum video bandwidth sent from OpenVidu Server to clients, in kbps. +# 0 means unconstrained +OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH=300 + +# All sessions of OpenVidu will try to force this codec. If OPENVIDU_STREAMS_ALLOW_TRANSCODING=true +# when a codec can not be forced, transcoding will be allowed +# Default value is VP8 +# OPENVIDU_STREAMS_FORCED_VIDEO_CODEC=VP8 + +# Allow transcoding if codec specified in OPENVIDU_STREAMS_FORCED_VIDEO_CODEC can not be applied +# Default value is false +# OPENVIDU_STREAMS_ALLOW_TRANSCODING=false + +# true to enable OpenVidu Webhook service. false' otherwise +# Values: true | false +OPENVIDU_WEBHOOK=false + +# HTTP endpoint where OpenVidu Server will send Webhook HTTP POST messages +# Must be a valid URL: http(s)://ENDPOINT +#OPENVIDU_WEBHOOK_ENDPOINT= + +# List of headers that OpenVidu Webhook service will attach to HTTP POST messages +#OPENVIDU_WEBHOOK_HEADERS= + +# List of events that will be sent by OpenVidu Webhook service +# Default value is all available events +OPENVIDU_WEBHOOK_EVENTS=[sessionCreated,sessionDestroyed,participantJoined,participantLeft,webrtcConnectionCreated,webrtcConnectionDestroyed,recordingStatusChanged,filterEventDispatched,mediaNodeStatusChanged] + +# How often the garbage collector of non active sessions runs. +# This helps cleaning up sessions that have been initialized through +# REST API (and maybe tokens have been created for them) but have had no users connected. +# Default to 900s (15 mins). 0 to disable non active sessions garbage collector +OPENVIDU_SESSIONS_GARBAGE_INTERVAL=900 + +# Minimum time in seconds that a non active session must have been in existence +# for the garbage collector of non active sessions to remove it. Default to 3600s (1 hour). +# If non active sessions garbage collector is disabled +# (property 'OPENVIDU_SESSIONS_GARBAGE_INTERVAL' to 0) this property is ignored +OPENVIDU_SESSIONS_GARBAGE_THRESHOLD=3600 + +# Call Detail Record enabled +# Whether to enable Call Detail Record or not +# Values: true | false +OPENVIDU_CDR=false + +# Path where the cdr log files are hosted +OPENVIDU_CDR_PATH=/opt/openvidu/cdr + +# Openvidu Server Level logs +# -------------------------- +# Uncomment the next line and define this variable to change +# the verbosity level of the logs of Openvidu Service +# RECOMENDED VALUES: INFO for normal logs DEBUG for more verbose logs +# OV_CE_DEBUG_LEVEL=INFO + +# OpenVidu Java Options +# -------------------------- +# Uncomment the next line and define this to add options to java command +# Documentation: https://docs.oracle.com/cd/E37116_01/install.111210/e23737/configuring_jvm.htm#OUDIG00058 +# JAVA_OPTIONS=-Xms2048m -Xmx4096m + +# ElasticSearch Java Options +# -------------------------- +# Uncomment the next line and define this to add options to java command of Elasticsearch +# Documentation: https://docs.oracle.com/cd/E37116_01/install.111210/e23737/configuring_jvm.htm#OUDIG00058 +# By default ElasticSearch is configured to use "-Xms2g -Xmx2g" as Java Min and Max memory heap allocation +# ES_JAVA_OPTS=-Xms2048m -Xmx4096m + +# Kibana And ElasticSearch Credentials Configuration +# -------------------------- +# Kibana And ElasticSearch Basic Auth configuration (Credentials) +# This credentials will aso be valid for Kibana dashboard +ELASTICSEARCH_USERNAME=elasticadmin +ELASTICSEARCH_PASSWORD= + +# Media Node Configuration +# -------------------------- +# You can add any KMS environment variable as described in the +# documentation of the docker image: https://hub.docker.com/r/kurento/kurento-media-server +# If you want to add an environment variable to KMS, you must add a variable using this prefix: 'KMS_DOCKER_ENV_', +# followed by the environment variable you want to setup. +# For example if you want to setup KMS_MIN_PORT to 50000, it would be KMS_DOCKER_ENV_KMS_MIN_PORT=50000 + +# Docker hub kurento media server: https://hub.docker.com/r/kurento/kurento-media-server +# Uncomment the next line and define this variable with KMS image that you want use +# By default, KMS_IMAGE is defined in media nodes and it does not need to be specified unless +# you want to use a specific version of KMS +# KMS_IMAGE=kurento/kurento-media-server:6.15.0 + +# Uncomment the next line and define this variable to change +# the verbosity level of the logs of KMS +# Documentation: https://doc-kurento.readthedocs.io/en/stable/features/logging.html +# KMS_DOCKER_ENV_GST_DEBUG= + +# Cloudformation configuration +# -------------------------- +# If you're working outside AWS ignore this section +#AWS_DEFAULT_REGION= +#AWS_IMAGE_ID= +#AWS_INSTANCE_TYPE= +#AWS_KEY_NAME= +#AWS_SUBNET_ID= +#AWS_SECURITY_GROUP= +#AWS_STACK_ID= +#AWS_STACK_NAME= +#AWS_CLI_DOCKER_TAG= +#AWS_VOLUME_SIZE= + +# AWS ASG configuration +# -------------------------- +RM_REDIS_IP= +RM_REDIS_PORT= +RM_SQS_QUEUE= diff --git a/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/beats/filebeat.yml b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/beats/filebeat.yml new file mode 100644 index 00000000..369daa45 --- /dev/null +++ b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/beats/filebeat.yml @@ -0,0 +1,48 @@ +filebeat.inputs: + - type: container + paths: + - '/var/lib/docker/containers/*/*.log' + +processors: + - add_docker_metadata: + host: "unix:///var/run/docker.sock" + - add_host_metadata: + netinfo.enabled: true + + - decode_json_fields: + fields: ["message"] + target: "json" + overwrite_keys: true + - drop_event: + when.or: + - contains: + container.image.name: openvidu/openvidu-coturn + - contains: + container.image.name: docker.elastic.co/elasticsearch/elasticsearch + - contains: + container.image.name: docker.elastic.co/kibana/kibana + - contains: + container.image.name: docker.elastic.co/beats/filebeat-oss + - contains: + container.image.name: docker.elastic.co/beats/metricbeat-oss + - add_fields: + fields: + cluster-id: ${OPENVIDU_PRO_CLUSTER_ID:undefined} + +output: + elasticsearch: + hosts: ["${OPENVIDU_PRO_ELASTICSEARCH_HOST}"] + indices: + - index: "filebeat-redis-%{+yyyy.MM.dd}" + when.or: + - contains: + container.image.name: openvidu/openvidu-redis + - index: "filebeat-nginx-%{+yyyy.MM.dd}" + when.or: + - contains: + container.image.name: openvidu/openvidu-proxy + + +logging.json: true +logging.metrics.enabled: false +setup.ilm.enabled: false \ No newline at end of file diff --git a/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/beats/metricbeat.yml b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/beats/metricbeat.yml new file mode 100644 index 00000000..09a68d9a --- /dev/null +++ b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/beats/metricbeat.yml @@ -0,0 +1,10 @@ +metricbeat.modules: +- module: nginx + metricsets: ["stubstatus"] + enabled: true + period: 10s + hosts: ["http://127.0.0.1"] + server_status_path: "nginx_status" +output: + elasticsearch: + hosts: ["${OPENVIDU_PRO_ELASTICSEARCH_HOST}"] diff --git a/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_autodiscover.sh b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_autodiscover.sh new file mode 100644 index 00000000..8689f3b7 --- /dev/null +++ b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_autodiscover.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -eu -o pipefail + +# Set debug mode +DEBUG=${DEBUG:-false} +[ "$DEBUG" == "true" ] && set -x + +OUTPUT=$(mktemp -t openvidu-autodiscover-XXX --suffix .json) + +docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} ec2 describe-instances \ + --output text \ + --filters "Name=instance-state-name,Values=running" \ + "Name=tag:ov-cluster-member,Values=kms" \ + "Name=tag:ov-stack-name,Values=${AWS_STACK_NAME}" \ + "Name=tag:ov-stack-region,Values=${AWS_DEFAULT_REGION}" \ + --query 'Reservations[*].Instances[*].{id:InstanceId,ip:PrivateIpAddress}' > ${OUTPUT} + +cat ${OUTPUT} | jq --raw-input --slurp 'split("\n") | map(split("\t")) | .[0:-1] | map( { "id": .[0], "ip": .[1] } )' diff --git a/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_drop.sh b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_drop.sh new file mode 100644 index 00000000..8d585480 --- /dev/null +++ b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_drop.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e -o pipefail + +# Set debug mode +DEBUG=${DEBUG:-false} +[ "$DEBUG" == "true" ] && set -x + +ID=$1 +[ -z "${ID}" ] && { echo "Must provide instance ID"; exit 1; } + +docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} ec2 terminate-instances --instance-ids ${ID} --output json diff --git a/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_launch_kms.sh b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_launch_kms.sh new file mode 100644 index 00000000..48b92bf7 --- /dev/null +++ b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_launch_kms.sh @@ -0,0 +1,69 @@ +#!/bin/bash +set -e -o pipefail + +# Set debug mode +DEBUG=${DEBUG:-false} +[ "$DEBUG" == "true" ] && set -x + +TMPFILE=$(mktemp -t openvidu-userdata-XXX --suffix .txt) +OUTPUT=$(mktemp -t openvidu-launch-kms-XXX --suffix .json) +ERROUTPUT=$(mktemp -t openvidu-launch-kms-XXX --suffix .err) + +trap exit_on_error ERR + +exit_on_error () { + ERROR_TYPE=$(cat ${ERROUTPUT} | awk '{ print $4 }' | sed -r 's/\(|\)//g' | tr -d '\n') + + case ${ERROR_TYPE} + in + "InvalidParameterValue") + echo -e "Parameter invalid " $(cat ${ERROUTPUT}) >&2 + exit 1 + ;; + + "UnauthorizedOperation") + MSG_COD=$(cat ${ERROUTPUT} | awk -F: '{ print $3 }') + MSG_DEC=$(docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} sts decode-authorization-message --encoded-message ${MSG_COD}) + + echo -e "Unauthorized " $(cat ${MSG_DEC}) >&2 + exit 1 + ;; + *) + echo -e "Unknown error " $(cat ${ERROUTPUT}) >&2 + exit 1 + ;; + esac +} + +docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} ec2 run-instances \ + --image-id ${AWS_IMAGE_ID} --count 1 \ + --instance-type ${AWS_INSTANCE_TYPE} \ + --key-name ${AWS_KEY_NAME} \ + --subnet-id ${AWS_SUBNET_ID} \ + --tag-specifications "ResourceType=instance,Tags=[{Key='Name',Value='Kurento Media Server'},{Key='ov-cluster-member',Value='kms'},{Key='ov-stack-name',Value='${AWS_STACK_NAME}'},{Key='ov-stack-region',Value='${AWS_DEFAULT_REGION}'}]" \ + --iam-instance-profile Name="OpenViduInstanceProfile-${AWS_STACK_NAME}-${AWS_DEFAULT_REGION}" \ + --block-device-mappings "DeviceName=/dev/sda1,Ebs={DeleteOnTermination=True,VolumeType='gp2',VolumeSize='${AWS_VOLUME_SIZE}'}" \ + --security-group-ids ${AWS_SECURITY_GROUP} > ${OUTPUT} 2> ${ERROUTPUT} + +docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} ec2 wait instance-running --instance-ids $(cat ${OUTPUT} | jq --raw-output ' .Instances[] | .InstanceId') + +# Generating the output +KMS_IP=$(cat ${OUTPUT} | jq --raw-output ' .Instances[] | .NetworkInterfaces[0] | .PrivateIpAddress') +KMS_ID=$(cat ${OUTPUT} | jq --raw-output ' .Instances[] | .InstanceId') + +# Wait media-node controller +attempt_counter=0 +max_attempts=10 + +until $(curl --output /dev/null --silent --head --fail -u OPENVIDUAPP:${OPENVIDU_SECRET} http://${KMS_IP}:3000/media-node/status); do + if [ ${attempt_counter} -eq ${max_attempts} ];then + exit 1 + fi + attempt_counter=$(($attempt_counter+1)) + sleep 5 +done + +jq -n \ + --arg id "${KMS_ID}" \ + --arg ip "${KMS_IP}" \ + '{ id: $id, ip: $ip }' diff --git a/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/docker-compose.override.yml b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/docker-compose.override.yml new file mode 100644 index 00000000..d4796d20 --- /dev/null +++ b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/docker-compose.override.yml @@ -0,0 +1,26 @@ +version: '3.1' + +services: + # -------------------------------------------------------------- + # + # Change this if your want use your own application. + # It's very important expose your application in port 5442 + # and use the http protocol. + # + # Default Application + # + # Openvidu-Call Version: 2.16.0 + # + # -------------------------------------------------------------- + app: + image: openvidu/openvidu-call:2.16.0 + restart: on-failure + network_mode: host + environment: + - SERVER_PORT=5442 + - OPENVIDU_URL=http://localhost:5443 + - OPENVIDU_SECRET=${OPENVIDU_SECRET} + - CALL_OPENVIDU_CERTTYPE=${CERTIFICATE_TYPE} + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" diff --git a/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/docker-compose.yml b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/docker-compose.yml new file mode 100644 index 00000000..3174c3a6 --- /dev/null +++ b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/docker-compose.yml @@ -0,0 +1,141 @@ +# ------------------------------------------------------------------------------ +# +# DO NOT MODIFY THIS FILE !!! +# +# Configuration properties should be specified in .env file +# +# Application based on OpenVidu should be specified in +# docker-compose.override.yml file +# +# This docker-compose file coordinates all services of OpenVidu Pro Platform +# +# This file will be overridden when update OpenVidu Platform +# +# Openvidu Version: 2.16.0 +# +# Installation Mode: On Premises +# +# ------------------------------------------------------------------------------ + +version: '3.1' + +services: + + openvidu-server: + image: openvidu/openvidu-server-pro:2.17.0-dev5 + restart: on-failure + network_mode: host + entrypoint: ['/usr/local/bin/entrypoint.sh'] + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ${OPENVIDU_RECORDING_PATH}:${OPENVIDU_RECORDING_PATH} + - ${OPENVIDU_RECORDING_CUSTOM_LAYOUT}:${OPENVIDU_RECORDING_CUSTOM_LAYOUT} + - ${OPENVIDU_CDR_PATH}:${OPENVIDU_CDR_PATH} + - ./cluster:/opt/openvidu/cluster + - .env:${PWD}/.env + env_file: + - .env + environment: + - SERVER_SSL_ENABLED=false + - SERVER_PORT=5443 + - KMS_URIS=[] + - OPENVIDU_WEBHOOK=false + - OPENVIDU_WEBHOOK_ENDPOINT=http://127.0.0.1:7777/webhook + - OPENVIDU_PRO_REPLICATION_MANAGER_WEBHOOK=http://127.0.0.1:4443/openvidu/replication-manager-webhook?OPENVIDU_SECRET=${OPENVIDU_SECRET} + - COTURN_REDIS_IP=127.0.0.1 + - COTURN_REDIS_PASSWORD=${OPENVIDU_SECRET} + - COTURN_IP=${COTURN_IP:-auto-ipv4} + - OPENVIDU_PRO_CLUSTER=true + - OPENVIDU_PRO_KIBANA_HOST=${OPENVIDU_PRO_KIBANA_HOST:-http://127.0.0.1/kibana} + - OPENVIDU_PRO_ELASTICSEARCH_HOST=${OPENVIDU_PRO_ELASTICSEARCH_HOST:-http://127.0.0.1:9200} + - WAIT_KIBANA_URL=${OPENVIDU_PRO_KIBANA_HOST:-http://127.0.0.1/kibana} + - DOTENV_PATH=${PWD} + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" + + replication-manager: + image: openvidu/replication-manager:1.0.0-dev5 + restart: on-failure + network_mode: host + environment: + - SERVER_PORT=4443 + - SERVER_SSL_ENABLED=false + - OPENVIDU_SECRET=${OPENVIDU_SECRET} + - LOCAL_OPENVIDU_SERVER_URI=http://127.0.0.1:5443/ + - REDIS_HOST=${RM_REDIS_IP} + - REDIS_PORT=${RM_REDIS_PORT} + - REDIS_PASS=${OPENVIDU_SECRET} + - REDIS_TIMEOUT=5 + - REDIS_DB=replicationmanager + - MEDIANODE_AS_NOTIFICATION_QUEUE=${RM_SQS_QUEUE} + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" + + redis: + image: openvidu/openvidu-redis:2.0.0-dev2 + restart: always + network_mode: host + environment: + - REDIS_PASSWORD=${OPENVIDU_SECRET} + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" + + coturn: + image: openvidu/openvidu-coturn:3.0.0-dev2 + restart: on-failure + network_mode: host + environment: + - REDIS_IP=127.0.0.1 + - TURN_LISTEN_PORT=3478 + - DB_NAME=0 + - DB_PASSWORD=${OPENVIDU_SECRET} + - MIN_PORT=40000 + - MAX_PORT=65535 + - TURN_PUBLIC_IP=${TURN_PUBLIC_IP:-auto-ipv4} + - ENABLE_COTURN_LOGS=true + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" + + metricbeat: + image: docker.elastic.co/beats/metricbeat-oss:7.8.0 + network_mode: host + restart: always + user: root + env_file: + - .env + environment: + - OPENVIDU_PRO_ELASTICSEARCH_HOST=${OPENVIDU_PRO_ELASTICSEARCH_HOST:-http://127.0.0.1:9200} + volumes: + - ./beats/metricbeat.yml:/usr/share/metricbeat/metricbeat.yml:ro + command: > + /bin/bash -c "metricbeat -e -strict.perms=false + `if [ ! -z $ELASTICSEARCH_USERNAME ]; then echo '-E output.elasticsearch.username=$ELASTICSEARCH_USERNAME'; fi` + `if [ ! -z $ELASTICSEARCH_PASSWORD ]; then echo '-E output.elasticsearch.password=$ELASTICSEARCH_PASSWORD'; fi`" + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" + + filebeat: + image: docker.elastic.co/beats/filebeat-oss:7.8.0 + network_mode: host + restart: always + user: root + env_file: + - .env + environment: + - OPENVIDU_PRO_ELASTICSEARCH_HOST=${OPENVIDU_PRO_ELASTICSEARCH_HOST:-http://127.0.0.1:9200} + volumes: + - ./beats/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro + - /var/lib/docker:/var/lib/docker:ro + - /var/run/docker.sock:/var/run/docker.sock + command: > + /bin/bash -c "filebeat -e -strict.perms=false + `if [ ! -z $ELASTICSEARCH_USERNAME ]; then echo '-E output.elasticsearch.username=$ELASTICSEARCH_USERNAME'; fi` + `if [ ! -z $ELASTICSEARCH_PASSWORD ]; then echo '-E output.elasticsearch.password=$ELASTICSEARCH_PASSWORD'; fi`" + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" diff --git a/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/install_openvidu_pro.sh b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/install_openvidu_pro.sh new file mode 100755 index 00000000..ec895dfb --- /dev/null +++ b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/install_openvidu_pro.sh @@ -0,0 +1,398 @@ +#!/usr/bin/env bash + +# Global variables +OPENVIDU_FOLDER=openvidu +OPENVIDU_VERSION=master +OPENVIDU_UPGRADABLE_VERSION="master" +AWS_SCRIPTS_FOLDER=${OPENVIDU_FOLDER}/cluster/aws +ELASTICSEARCH_FOLDER=${OPENVIDU_FOLDER}/elasticsearch +BEATS_FOLDER=${OPENVIDU_FOLDER}/beats +DOWNLOAD_URL=https://raw.githubusercontent.com/OpenVidu/openvidu/${OPENVIDU_VERSION} + +fatal_error() { + printf "\n =======¡ERROR!=======" + printf "\n %s" "$1" + printf "\n" + exit 0 +} + +new_ov_installation() { + printf '\n' + printf '\n =======================================' + printf '\n Install Openvidu PRO %s' "${OPENVIDU_VERSION}" + printf '\n =======================================' + printf '\n' + + # Create folder openvidu-docker-compose + printf '\n => Creating folder '%s'...' "${OPENVIDU_FOLDER}" + mkdir "${OPENVIDU_FOLDER}" || fatal_error "Error while creating the folder '${OPENVIDU_FOLDER}'" + + # Create aws scripts folder + printf "\n => Creating folder 'cluster/aws'..." + mkdir -p "${AWS_SCRIPTS_FOLDER}" || fatal_error "Error while creating the folder 'cluster/aws'" + + # Create beats folder + printf "\n => Creating folder 'beats'..." + mkdir -p "${BEATS_FOLDER}" || fatal_error "Error while creating the folder 'beats'" + + # Create elasticsearch folder + printf "\n => Creating folder 'elasticsearch'..." + mkdir -p "${ELASTICSEARCH_FOLDER}" || fatal_error "Error while creating the folder 'elasticsearch'" + + printf "\n => Changing permission to 'elasticsearch' folder..." + chown 1000:1000 "${ELASTICSEARCH_FOLDER}" || fatal_error "Error while changing permission to 'elasticsearch' folder" + + # Download necessary files + printf '\n => Downloading Openvidu PRO files:' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_autodiscover.sh \ + --output "${AWS_SCRIPTS_FOLDER}/openvidu_autodiscover.sh" || fatal_error "Error when downloading the file 'openvidu_autodiscover.sh'" + printf '\n - openvidu_autodiscover.sh' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_drop.sh \ + --output "${AWS_SCRIPTS_FOLDER}/openvidu_drop.sh" || fatal_error "Error when downloading the file 'openvidu_drop.sh'" + printf '\n - openvidu_drop.sh' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_launch_kms.sh \ + --output "${AWS_SCRIPTS_FOLDER}/openvidu_launch_kms.sh" || fatal_error "Error when downloading the file 'openvidu_launch_kms.sh'" + printf '\n - openvidu_launch_kms.sh' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/beats/filebeat.yml \ + --output "${BEATS_FOLDER}/filebeat.yml" || fatal_error "Error when downloading the file 'filebeat.yml'" + printf '\n - filebeat.yml' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/beats/metricbeat.yml \ + --output "${BEATS_FOLDER}/metricbeat.yml" || fatal_error "Error when downloading the file 'metricbeat.yml'" + printf '\n - metricbeat.yml' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/.env \ + --output "${OPENVIDU_FOLDER}/.env" || fatal_error "Error when downloading the file '.env'" + printf '\n - .env' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/docker-compose.override.yml \ + --output "${OPENVIDU_FOLDER}/docker-compose.override.yml" || fatal_error "Error when downloading the file 'docker-compose.override.yml'" + printf '\n - docker-compose.override.yml' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/docker-compose.yml \ + --output "${OPENVIDU_FOLDER}/docker-compose.yml" || fatal_error "Error when downloading the file 'docker-compose.yml'" + printf '\n - docker-compose.yml' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/openvidu \ + --output "${OPENVIDU_FOLDER}/openvidu" || fatal_error "Error when downloading the file 'openvidu'" + printf '\n - openvidu' + + # Add execution permissions + printf "\n => Adding permission:" + + chmod +x "${OPENVIDU_FOLDER}/openvidu" || fatal_error "Error while adding permission to 'openvidu' program" + printf '\n - openvidu' + + chmod +x "${AWS_SCRIPTS_FOLDER}/openvidu_autodiscover.sh" || fatal_error "Error while adding permission to 'openvidu_autodiscover.sh' program" + printf '\n - openvidu_autodiscover.sh' + + chmod +x "${AWS_SCRIPTS_FOLDER}/openvidu_drop.sh" || fatal_error "Error while adding permission to 'openvidu' openvidu_drop.sh" + printf '\n - openvidu_drop.sh' + + chmod +x "${AWS_SCRIPTS_FOLDER}/openvidu_launch_kms.sh" || fatal_error "Error while adding permission to 'openvidu_launch_kms.sh' program" + printf '\n - openvidu_launch_kms.sh' + + # Create own certificated folder + printf "\n => Creating folder 'owncert'..." + mkdir "${OPENVIDU_FOLDER}/owncert" || fatal_error "Error while creating the folder 'owncert'" + + # Create vhost nginx folder + printf "\n => Creating folder 'custom-nginx-vhosts'..." + mkdir "${OPENVIDU_FOLDER}/custom-nginx-vhosts" || fatal_error "Error while creating the folder 'custom-nginx-vhosts'" + + # Ready to use + printf '\n' + printf '\n' + printf '\n =======================================' + printf '\n Openvidu PRO successfully installed.' + printf '\n =======================================' + printf '\n' + printf '\n 1. Go to openvidu folder:' + printf '\n $ cd openvidu' + printf '\n' + printf '\n 2. Configure OPENVIDU_DOMAIN_OR_PUBLIC_IP, OPENVIDU_PRO_LICENSE, ' + printf '\n OPENVIDU_SECRET, and ELASTICSEARCH_PASSWORD in .env file:' + printf '\n $ nano .env' + printf '\n' + printf '\n 3. Start OpenVidu' + printf '\n $ ./openvidu start' + printf '\n' + printf "\n CAUTION: The folder 'openvidu/elasticsearch' use user and group 1000 permissions. " + printf "\n This folder is necessary for store elasticsearch data." + printf "\n For more information, check:" + printf "\n https://docs.openvidu.io/en/${OPENVIDU_VERSION//v}/openvidu-pro/deployment/on-premises/#deployment-instructions" + printf '\n' + printf '\n' + exit 0 +} + +upgrade_ov() { + # Search local Openvidu installation + printf '\n' + printf '\n ============================================' + printf '\n Search Previous Installation of Openvidu' + printf '\n ============================================' + printf '\n' + + SEARCH_IN_FOLDERS=( + "${PWD}" + "/opt/${OPENVIDU_FOLDER}" + ) + + for folder in "${SEARCH_IN_FOLDERS[@]}"; do + printf "\n => Searching in '%s' folder..." "${folder}" + + if [ -f "${folder}/docker-compose.yml" ]; then + OPENVIDU_PREVIOUS_FOLDER="${folder}" + + printf "\n => Found installation in folder '%s'" "${folder}" + break + fi + done + + [ -z "${OPENVIDU_PREVIOUS_FOLDER}" ] && fatal_error "No previous Openvidu installation found" + + # Uppgrade Openvidu + OPENVIDU_PREVIOUS_VERSION=$(grep 'Openvidu Version:' "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml" | awk '{ print $4 }') + [ -z "${OPENVIDU_PREVIOUS_VERSION}" ] && fatal_error "Can't find previous OpenVidu version" + + # In this point using the variable 'OPENVIDU_PREVIOUS_VERSION' we can verify if the upgrade is + # posible or not. If it is not posible launch a warning and stop the upgrade. + if [[ "${OPENVIDU_PREVIOUS_VERSION}" != "${OPENVIDU_UPGRADABLE_VERSION}."* ]]; then + fatal_error "You can't update from version ${OPENVIDU_PREVIOUS_VERSION} to ${OPENVIDU_VERSION}.\nNever upgrade across multiple major versions." + fi + + printf '\n' + printf '\n =======================================' + printf '\n Upgrade Openvidu PRO %s to %s' "${OPENVIDU_PREVIOUS_VERSION}" "${OPENVIDU_VERSION}" + printf '\n =======================================' + printf '\n' + + ROLL_BACK_FOLDER="${OPENVIDU_PREVIOUS_FOLDER}/.old-${OPENVIDU_PREVIOUS_VERSION}" + TMP_FOLDER="${OPENVIDU_PREVIOUS_FOLDER}/tmp" + ACTUAL_FOLDER="${PWD}" + USE_OV_CALL=$(grep -E '^ image: openvidu/openvidu-call:.*$' "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.override.yml" | tr -d '[:space:]') + + printf "\n Creating rollback folder '%s'..." ".old-${OPENVIDU_PREVIOUS_VERSION}" + mkdir "${ROLL_BACK_FOLDER}" || fatal_error "Error while creating the folder '.old-${OPENVIDU_PREVIOUS_VERSION}'" + + printf "\n Creating temporal folder 'tmp'..." + mkdir "${TMP_FOLDER}" || fatal_error "Error while creating the folder 'temporal'" + + # Download necessary files + printf '\n => Downloading new Openvidu PRO files:' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_autodiscover.sh \ + --output "${TMP_FOLDER}/openvidu_autodiscover.sh" || fatal_error "Error when downloading the file 'openvidu_autodiscover.sh'" + printf '\n - openvidu_autodiscover.sh' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_drop.sh \ + --output "${TMP_FOLDER}/openvidu_drop.sh" || fatal_error "Error when downloading the file 'openvidu_drop.sh'" + printf '\n - openvidu_drop.sh' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_launch_kms.sh \ + --output "${TMP_FOLDER}/openvidu_launch_kms.sh" || fatal_error "Error when downloading the file 'openvidu_launch_kms.sh'" + printf '\n - openvidu_launch_kms.sh' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/beats/filebeat.yml \ + --output "${TMP_FOLDER}/filebeat.yml" || fatal_error "Error when downloading the file 'filebeat.yml'" + printf '\n - filebeat.yml' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/beats/metricbeat.yml \ + --output "${TMP_FOLDER}/metricbeat.yml" || fatal_error "Error when downloading the file 'metricbeat.yml'" + printf '\n - metricbeat.yml' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/.env \ + --output "${TMP_FOLDER}/.env" || fatal_error "Error when downloading the file '.env'" + printf '\n - .env' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/docker-compose.override.yml \ + --output "${TMP_FOLDER}/docker-compose.override.yml" || fatal_error "Error when downloading the file 'docker-compose.override.yml'" + printf '\n - docker-compose.override.yml' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/docker-compose.yml \ + --output "${TMP_FOLDER}/docker-compose.yml" || fatal_error "Error when downloading the file 'docker-compose.yml'" + printf '\n - docker-compose.yml' + + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/openvidu \ + --output "${TMP_FOLDER}/openvidu" || fatal_error "Error when downloading the file 'openvidu'" + printf '\n - openvidu' + + # Dowloading new images and stoped actual Openvidu + printf '\n => Dowloading new images...' + printf '\n' + sleep 1 + + printf "\n => Moving to 'tmp' folder..." + printf '\n' + cd "${TMP_FOLDER}" || fatal_error "Error when moving to 'tmp' folder" + printf '\n' + docker-compose pull | true + + printf '\n => Stoping Openvidu...' + printf '\n' + sleep 1 + + printf "\n => Moving to 'openvidu' folder..." + printf '\n' + cd "${OPENVIDU_PREVIOUS_FOLDER}" || fatal_error "Error when moving to 'openvidu' folder" + printf '\n' + docker-compose down | true + + printf '\n' + printf '\n => Moving to working dir...' + cd "${ACTUAL_FOLDER}" || fatal_error "Error when moving to working dir" + + # Move old files to rollback folder + printf '\n => Moving previous installation files to rollback folder:' + + mv "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'docker-compose.yml'" + printf '\n - docker-compose.yml' + + if [ ! -z "${USE_OV_CALL}" ]; then + mv "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.override.yml" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'docker-compose.override.yml'" + printf '\n - docker-compose.override.yml' + fi + + mv "${OPENVIDU_PREVIOUS_FOLDER}/openvidu" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'openvidu'" + printf '\n - openvidu' + + mv "${OPENVIDU_PREVIOUS_FOLDER}/cluster/aws" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'cluster/aws'" + printf '\n - cluster/aws' + + mv "${OPENVIDU_PREVIOUS_FOLDER}/beats" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'beats'" + printf '\n - beats' + + cp "${OPENVIDU_PREVIOUS_FOLDER}/.env" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous '.env'" + printf '\n - .env' + + if [ -d "${OPENVIDU_PREVIOUS_FOLDER}/custom-nginx-vhosts" ]; then + mv "${OPENVIDU_PREVIOUS_FOLDER}/custom-nginx-vhosts" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous directory 'custom-nginx-vhosts'" + printf '\n - custom-nginx-vhosts' + fi + + # Move tmp files to Openvidu + printf '\n => Updating files:' + + mv "${TMP_FOLDER}/docker-compose.yml" "${OPENVIDU_PREVIOUS_FOLDER}" || fatal_error "Error while updating 'docker-compose.yml'" + printf '\n - docker-compose.yml' + + if [ ! -z "${USE_OV_CALL}" ]; then + mv "${TMP_FOLDER}/docker-compose.override.yml" "${OPENVIDU_PREVIOUS_FOLDER}" || fatal_error "Error while updating 'docker-compose.override.yml'" + printf '\n - docker-compose.override.yml' + else + mv "${TMP_FOLDER}/docker-compose.override.yml" "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.override.yml-${OPENVIDU_VERSION}" || fatal_error "Error while updating 'docker-compose.override.yml'" + printf '\n - docker-compose.override.yml-%s' "${OPENVIDU_VERSION}" + fi + + mv "${TMP_FOLDER}/.env" "${OPENVIDU_PREVIOUS_FOLDER}/.env-${OPENVIDU_VERSION}" || fatal_error "Error while moving previous '.env'" + printf '\n - .env-%s' "${OPENVIDU_VERSION}" + + mv "${TMP_FOLDER}/openvidu" "${OPENVIDU_PREVIOUS_FOLDER}" || fatal_error "Error while updating 'openvidu'" + printf '\n - openvidu' + + mkdir "${OPENVIDU_PREVIOUS_FOLDER}/cluster/aws" || fatal_error "Error while creating the folder 'cluster/aws'" + + mkdir "${OPENVIDU_PREVIOUS_FOLDER}/beats" || fatal_error "Error while creating the folder 'beats'" + + mv "${TMP_FOLDER}/openvidu_autodiscover.sh" "${OPENVIDU_PREVIOUS_FOLDER}/cluster/aws" || fatal_error "Error while updating 'openvidu_autodiscover.sh'" + printf '\n - openvidu_autodiscover.sh' + + mv "${TMP_FOLDER}/openvidu_drop.sh" "${OPENVIDU_PREVIOUS_FOLDER}/cluster/aws" || fatal_error "Error while updating 'openvidu_drop.sh'" + printf '\n - openvidu_drop.sh' + + mv "${TMP_FOLDER}/openvidu_launch_kms.sh" "${OPENVIDU_PREVIOUS_FOLDER}/cluster/aws" || fatal_error "Error while updating 'openvidu_launch_kms.sh'" + printf '\n - openvidu_launch_kms.sh' + + mv "${TMP_FOLDER}/filebeat.yml" "${OPENVIDU_PREVIOUS_FOLDER}/beats/filebeat.yml" || fatal_error "Error while updating 'filebeat.yml'" + printf '\n - filebeat.yml' + + mv "${TMP_FOLDER}/metricbeat.yml" "${OPENVIDU_PREVIOUS_FOLDER}/beats/metricbeat.yml" || fatal_error "Error while updating 'metricbeat.yml'" + printf '\n - metricbeat.yml' + + printf "\n => Deleting 'tmp' folder" + rm -rf "${TMP_FOLDER}" || fatal_error "Error deleting 'tmp' folder" + + # Add execution permissions + printf "\n => Adding permission to 'openvidu' program..." + + chmod +x "${OPENVIDU_PREVIOUS_FOLDER}/openvidu" || fatal_error "Error while adding permission to 'openvidu' program" + printf '\n - openvidu' + + chmod +x "${OPENVIDU_PREVIOUS_FOLDER}/cluster/aws/openvidu_autodiscover.sh" || fatal_error "Error while adding permission to 'openvidu_autodiscover.sh' program" + printf '\n - openvidu_autodiscover.sh' + + chmod +x "${OPENVIDU_PREVIOUS_FOLDER}/cluster/aws/openvidu_drop.sh" || fatal_error "Error while adding permission to 'openvidu' openvidu_drop.sh" + printf '\n - openvidu_drop.sh' + + chmod +x "${OPENVIDU_PREVIOUS_FOLDER}/cluster/aws/openvidu_launch_kms.sh" || fatal_error "Error while adding permission to 'openvidu_launch_kms.sh' program" + printf '\n - openvidu_launch_kms.sh' + + # Define old mode: On Premise or Cloud Formation + OLD_MODE=$(grep -E "Installation Mode:.*$" "${ROLL_BACK_FOLDER}/docker-compose.yml" | awk '{ print $4,$5 }') + [ ! -z "${OLD_MODE}" ] && sed -i -r "s/Installation Mode:.+/Installation Mode: ${OLD_MODE}/" "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml" + + # In Aws, update AMI ID + AWS_REGION=$(grep -E "AWS_DEFAULT_REGION=.*$" "${OPENVIDU_PREVIOUS_FOLDER}/.env" | cut -d'=' -f2) + if [[ ! -z ${AWS_REGION} ]]; then + NEW_AMI_ID=$(curl https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/CF-OpenVidu-Pro-${OPENVIDU_VERSION//v}.yaml --silent | + sed -n -e '/KMSAMIMAP:/,/Metadata:/ p' | + grep -A 1 ${AWS_REGION} | grep AMI | tr -d " " | cut -d":" -f2) + [[ -z ${NEW_AMI_ID} ]] && fatal_error "Error while getting new AWS_IMAGE_ID for Media Nodes" + sed -i "s/.*AWS_IMAGE_ID=.*/AWS_IMAGE_ID=${NEW_AMI_ID}/" "${OPENVIDU_PREVIOUS_FOLDER}/.env" || fatal_error "Error while updating new AWS_IMAGE_ID for Media Nodes" + fi + + + # Ready to use + printf '\n' + printf '\n' + printf '\n ================================================' + printf "\n Openvidu successfully upgraded to version %s" "${OPENVIDU_VERSION}" + printf '\n ================================================' + printf '\n' + printf "\n 1. A new file 'docker-compose.yml' has been created with the new OpenVidu %s services" "${OPENVIDU_VERSION}" + printf '\n' + printf "\n 2. The previous file '.env' remains intact, but a new file '.env-%s' has been created." "${OPENVIDU_VERSION}" + printf "\n Transfer any configuration you wish to keep in the upgraded version from '.env' to '.env-%s'." "${OPENVIDU_VERSION}" + printf "\n When you are OK with it, rename and leave as the only '.env' file of the folder the new '.env-%s'." "${OPENVIDU_VERSION}" + printf '\n' + printf "\n 3. If you were using Openvidu Call application, it has been automatically updated in file 'docker-compose.override.yml'." + printf "\n However, if you were using your own application, a file called 'docker-compose.override.yml-%s'" "${OPENVIDU_VERSION}" + printf "\n has been created with the latest version of Openvidu Call. If you don't plan to use it you can delete it." + printf '\n' + printf '\n 4. Start new version of Openvidu' + printf '\n $ ./openvidu start' + printf '\n' + printf "\n If you want to rollback, all the files from the previous installation have been copied to folder '.old-%s'" "${OPENVIDU_PREVIOUS_VERSION}" + printf '\n' + printf '\n' + printf '\n' +} + +# Check docker and docker-compose installation +if ! command -v docker > /dev/null; then + echo "You don't have docker installed, please install it and re-run the command" + exit 0 +fi + +if ! command -v docker-compose > /dev/null; then + echo "You don't have docker-compose installed, please install it and re-run the command" + exit 0 +else + COMPOSE_VERSION=$(docker-compose version --short | sed "s/-rc[0-9]*//") + if ! printf '%s\n%s\n' "1.24" "$COMPOSE_VERSION" | sort -V -C; then + echo "You need a docker-compose version equal or higher than 1.24, please update your docker-compose and re-run the command"; \ + exit 0 + fi +fi + +# Check type of installation +if [[ ! -z "$1" && "$1" == "upgrade" ]]; then + upgrade_ov +else + new_ov_installation +fi diff --git a/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/openvidu b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/openvidu new file mode 100755 index 00000000..b4229d5a --- /dev/null +++ b/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/openvidu @@ -0,0 +1,301 @@ +#!/bin/bash + +upgrade_ov() { + UPGRADE_SCRIPT_URL="https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/install_openvidu_pro_OVVERSION.sh" + HTTP_STATUS=$(curl -s -o /dev/null -I -w "%{http_code}" ${UPGRADE_SCRIPT_URL//OVVERSION/$1}) + + printf " => Upgrading Openvidu PRO to '%s' version" "$1" + + if [ "$HTTP_STATUS" == "200" ]; then + printf "\n => Downloading and upgrading new version" + printf "\n" + + curl --silent ${UPGRADE_SCRIPT_URL//OVVERSION/$1} | bash -s upgrade + else + printf "\n =======¡ERROR!=======" + printf "\n Openvidu PRO Version '%s' not exist" "$1" + printf "\n" + exit 0 + fi +} + +collect_basic_information() { + LINUX_VERSION=$(lsb_release -d) + DOCKER_PS=$(docker ps) + DOCKER_VERSION=$(docker version --format '{{.Server.Version}}') + DOCKER_COMPOSE_VERSION=$(docker-compose version --short) + OV_FOLDER="${PWD}" + OV_VERSION=$(grep 'Openvidu Version:' "${OV_FOLDER}/docker-compose.yml" | awk '{ print $4 }') + CONTAINERS=$(docker ps | awk '{if(NR>1) print $NF}') + + if [ ! -z "$(grep -E '^ image: openvidu/openvidu-call:.*$' "${OV_FOLDER}/docker-compose.override.yml" | tr -d '[:space:]')" ]; then + OV_CALL_VERSION=$(grep -E 'Openvidu-Call Version:' "${OV_FOLDER}/docker-compose.override.yml" | awk '{ print $4 }') + fi + [ -z "${OV_CALL_VERSION}" ] && OV_CALL_VERSION="No present" + + OV_TYPE_INSTALLATION=$(grep 'Installation Mode:' "${OV_FOLDER}/docker-compose.yml" | awk '{ print $4,$5 }') + TREE_OV_DIRECTORY=$(find "." ! -path '*/0/*' | sed -e "s/[^-][^\/]*\// |/g" -e "s/|\([^ ]\)/|-\1/") +} + +version_ov() { + collect_basic_information + + printf '\nOpenvidu Information:' + printf '\n' + printf '\n Installation Type: %s' "${OV_TYPE_INSTALLATION}" + printf '\n Openvidu Version: %s' "${OV_VERSION}" + printf '\n Openvidu Call Version: %s' "${OV_CALL_VERSION}" + printf '\n' + printf '\nSystem Information:' + printf '\n' + printf '\n Linux Version:' + printf '\n - %s' "${LINUX_VERSION}" + printf '\n Docker Version: %s' "${DOCKER_VERSION}" + printf '\n Docker Compose Version: %s' "${DOCKER_COMPOSE_VERSION}" + printf '\n' + printf '\nInstallation Information:' + printf '\n' + printf '\n Installation Folder: %s' "${OV_FOLDER}" + printf '\n Installation Folder Tree:' + printf '\n%s' "$(echo "${TREE_OV_DIRECTORY}" | sed -e 's/.//' -e ':a' -e 'N;$!ba' -e 's/\n/\n\t/g')" + printf '\n' + printf '\nDocker Running Services:' + printf '\n' + printf '\n %s' "$(echo "${DOCKER_PS}" | sed -e ':a' -e 'N;$!ba' -e 's/\n/\n\t/g')" + printf '\n' +} + +generate_report() { + collect_basic_information + + REPORT_CREATION_DATE=$(date +"%d-%m-%Y") + REPORT_CREATION_TIME=$(date +"%H:%M:%S") + REPORT_NAME="openvidu-report-${REPORT_CREATION_DATE}-$(date +"%H-%M").txt" + REPORT_OUPUT="${OV_FOLDER}/${REPORT_NAME}" + + { + printf "\n =======================================" + printf "\n = REPORT INFORMATION =" + printf "\n =======================================" + printf '\n' + printf '\n Creation Date: %s' "${REPORT_CREATION_DATE}" + printf '\n Creation Time: %s' "${REPORT_CREATION_TIME}" + printf '\n' + printf "\n =======================================" + printf "\n = OPENVIDU INFORMATION =" + printf "\n =======================================" + printf '\n' + printf '\n Installation Type: %s' "${OV_TYPE_INSTALLATION}" + printf '\n Openvidu Version: %s' "${OV_VERSION}" + printf '\n Openvidu Call Version: %s' "${OV_CALL_VERSION}" + printf '\n' + printf "\n =======================================" + printf "\n = SYSTEM INFORMATION =" + printf "\n =======================================" + printf '\n' + printf '\n Linux Version:' + printf '\n - %s' "${LINUX_VERSION}" + printf '\n Docker Version: %s' "${DOCKER_VERSION}" + printf '\n Docker Compose Version: %s' "${DOCKER_COMPOSE_VERSION}" + printf '\n' + printf "\n =======================================" + printf "\n = INSTALLATION INFORMATION =" + printf "\n =======================================" + printf '\n' + printf '\n Installation Folder: %s' "${OV_FOLDER}" + printf '\n Installation Folder Tree:' + printf '\n%s' "$(echo "${TREE_OV_DIRECTORY}" | sed -e 's/.//' -e ':a' -e 'N;$!ba' -e 's/\n/\n\t/g')" + printf '\n' + printf "\n =======================================" + printf "\n = DOCKER RUNNING SERVICES =" + printf "\n =======================================" + printf '\n' + printf '\n %s' "$(echo "${DOCKER_PS}" | sed -e ':a' -e 'N;$!ba' -e 's/\n/\n\t/g')" + printf '\n' + printf "\n =======================================" + printf "\n = CONFIGURATION FILES =" + printf "\n =======================================" + printf '\n' + printf '\n ================ .env =================' + printf '\n' + printf '\n' + + cat < "${OV_FOLDER}/.env" | sed -r -e "s/OPENVIDU_SECRET=.+/OPENVIDU_SECRET=****/" -e "s/OPENVIDU_PRO_LICENSE=.+/OPENVIDU_PRO_LICENSE=****/" -e "s/ELASTICSEARCH_PASSWORD=.+/ELASTICSEARCH_PASSWORD=****/" + + printf '\n' + printf '\n ========= docker-compose.yml ==========' + printf '\n' + printf '\n' + + cat "${OV_FOLDER}/docker-compose.yml" + + printf '\n' + printf '\n ==== docker-compose.override.yml ====' + printf '\n' + printf '\n' + + if [ -f "${OV_FOLDER}/docker-compose.override.yml" ]; then + cat < "${OV_FOLDER}/docker-compose.override.yml" + else + printf '\n The docker-compose.override.yml file is not present' + fi + + printf '\n' + printf '\n' + printf "\n =======================================" + printf "\n = LOGS =" + printf "\n =======================================" + + for CONTAINER in $CONTAINERS + do + printf '\n' + printf "\n ---------------------------------------" + printf "\n %s" $CONTAINER + printf "\n ---------------------------------------" + printf '\n' + docker logs $CONTAINER + printf "\n ---------------------------------------" + printf '\n' + printf '\n' + done + + printf "\n =======================================" + printf "\n = CONTAINER ENVS VARIABLES =" + printf "\n =======================================" + + for CONTAINER in $CONTAINERS + do + printf '\n' + printf "\n =======================================" + printf "\n %s" $CONTAINER + printf "\n ---------------------------------------" + printf '\n' + docker exec $CONTAINER env + printf "\n ---------------------------------------" + printf '\n' + printf '\n' + done + + } >> "${REPORT_OUPUT}" 2>&1 + + printf "\n Generation of the report completed with success" + printf "\n You can get your report at path '%s'" "${REPORT_OUPUT}" + printf "\n" +} + +is_external_url() { + local URL=$1 + if [[ -z "$URL" ]]; then + return 1 + fi + if [[ "${URL}" == *"localhost"* ]] || [[ "${URL}" == *"127.0.0.1"* ]] || [[ "${URL}" == *"::1"* ]]; then + return 1 + else + return 0 + fi +} + +start_openvidu() { + local RUN_LOCAL_ES + local RUN_LOCAL_KIBANA + local CONFIGURED_ELASTICSEARCH_HOST + local CONFIGURED_KIBANA_HOST + CONFIGURED_ELASTICSEARCH_HOST=$(grep -v '^#' .env | grep OPENVIDU_PRO_ELASTICSEARCH_HOST | cut -d '=' -f2) + CONFIGURED_KIBANA_HOST=$(grep -v '^#' .env | grep OPENVIDU_PRO_KIBANA_HOST | cut -d '=' -f2) + RUN_LOCAL_ES=true + RUN_LOCAL_KIBANA=true + if is_external_url "${CONFIGURED_ELASTICSEARCH_HOST}"; then + printf "Configured external elasticsearch: %s" "${CONFIGURED_ELASTICSEARCH_HOST}" + printf "\n" + RUN_LOCAL_ES=false + fi + if is_external_url "${CONFIGURED_KIBANA_HOST}"; then + printf "Configured external kibana: %s" "${CONFIGURED_KIBANA_HOST}" + printf "\n" + RUN_LOCAL_KIBANA=false + fi + docker-compose up -d \ + $(if [ "${RUN_LOCAL_ES}" == "false" ]; then echo '--scale elasticsearch=0'; fi) \ + $(if [ "${RUN_LOCAL_KIBANA}" == "false" ]; then echo '--scale kibana=0'; fi) +} + +usage() { + printf "Usage: \n\t openvidu [command]" + printf "\n\nAvailable Commands:" + printf "\n\tstart\t\t\tStart all services" + printf "\n\tstop\t\t\tStop all services" + printf "\n\trestart\t\t\tRestart all stoped and running services" + printf "\n\tlogs\t\t\tShow openvidu-server logs" + printf "\n\tupgrade\t\t\tUpgrade to the lastest Openvidu version" + printf "\n\tupgrade [version]\tUpgrade to the specific Openvidu version" + printf "\n\tversion\t\t\tShow version of Openvidu Server" + printf "\n\treport\t\t\tGenerate a report with the current status of Openvidu" + printf "\n\thelp\t\t\tShow help for openvidu command" + printf "\n" +} + +[[ -z "${FOLLOW_OPENVIDU_LOGS}" ]] && FOLLOW_OPENVIDU_LOGS=true + +case $1 in + + start) + start_openvidu + if [[ "${FOLLOW_OPENVIDU_LOGS}" == "true" ]]; then + docker-compose logs -f openvidu-server + fi + ;; + + stop) + docker-compose down + ;; + + restart) + docker-compose down + start_openvidu + if [[ "${FOLLOW_OPENVIDU_LOGS}" == "true" ]]; then + docker-compose logs -f openvidu-server + fi + ;; + + logs) + docker-compose logs -f openvidu-server + ;; + + upgrade) + if [ -z "$2" ]; then + UPGRADE_VERSION="latest" + else + UPGRADE_VERSION="$2" + fi + + read -r -p " You're about to update Openvidu PRO to '${UPGRADE_VERSION}' version. Are you sure? [y/N]: " response + case "$response" in + [yY][eE][sS]|[yY]) + upgrade_ov "${UPGRADE_VERSION}" + ;; + *) + exit 0 + ;; + esac + ;; + + version) + version_ov + ;; + + report) + read -r -p " You are about to generate a report on the current status of Openvidu, this may take some time. Do you want to continue? [y/N]: " response + case "$response" in + [yY][eE][sS]|[yY]) + generate_report + ;; + *) + exit 0 + ;; + esac + ;; + + *) + usage + ;; +esac \ No newline at end of file diff --git a/openvidu-server/deployments/pro/docker-compose/media-node/.env b/openvidu-server/deployments/pro/docker-compose/media-node/.env deleted file mode 100644 index 8108613b..00000000 --- a/openvidu-server/deployments/pro/docker-compose/media-node/.env +++ /dev/null @@ -1,30 +0,0 @@ -# KMS Configuration -# -------------------------- - -# NOTE: This file doesn't need to quote assignment values, like most shells do. -# All values are stored as-is, even if they contain spaces, so don't quote them. - -# Kurento Media Server image -# -------------------------- -# Docker hub kurento media server: https://hub.docker.com/r/kurento/kurento-media-server-dev -# Uncomment the next line and define this variable with KMS image that you want use -# KMS_IMAGE=kurento/kurento-media-server:6.14.0 - -# Kurento Media Server Level logs -# ------------------------------- -# Uncomment the next line and define this variable to change -# the verbosity level of the logs of KMS -# Documentation: https://doc-kurento.readthedocs.io/en/stable/features/logging.html -# KMS_DEBUG_LEVEL=3,Kurento*:4,kms*:4,sdp*:4,webrtc*:4,*rtpendpoint:4,rtp*handler:4,rtpsynchronizer:4,agnosticbin:4 - -# Metricbeat ElasticSearch Image -# ------------------------------- -# Docker hub kurento media server: https://www.docker.elastic.co/ -# Uncomment the next line and define this variable with METRICBEAT_IMAGE image that you want use -# METRICBEAT_IMAGE=docker.elastic.co/beats/metricbeat:7.8.0 - -# FileBeat ElasticSearch Image -# ------------------------------- -# Docker hub kurento media server: https://www.docker.elastic.co/ -# Uncomment the next line and define this variable with FILEBEAT_IMAGE image that you want use -# FILEBEAT_IMAGE=docker.elastic.co/beats/filebeat:7.8.0 \ No newline at end of file diff --git a/openvidu-server/deployments/pro/docker-compose/media-node/beats/copy_config_files.sh b/openvidu-server/deployments/pro/docker-compose/media-node/beats/copy_config_files.sh index 1dfaf54d..57c71e1a 100644 --- a/openvidu-server/deployments/pro/docker-compose/media-node/beats/copy_config_files.sh +++ b/openvidu-server/deployments/pro/docker-compose/media-node/beats/copy_config_files.sh @@ -3,5 +3,4 @@ echo "Creating dir for beats" mkdir -p /opt/openvidu/beats echo "Copying beat config files" cp /beats/filebeat.yml /opt/openvidu/beats/filebeat.yml -cp /beats/metricbeat-elasticsearch.yml /opt/openvidu/beats/metricbeat-elasticsearch.yml -cp /beats/metricbeat-openvidu.yml /opt/openvidu/beats/metricbeat-openvidu.yml \ No newline at end of file +cp /beats/metricbeat-elasticsearch.yml /opt/openvidu/beats/metricbeat-elasticsearch.yml \ No newline at end of file diff --git a/openvidu-server/deployments/pro/docker-compose/media-node/beats/filebeat.yml b/openvidu-server/deployments/pro/docker-compose/media-node/beats/filebeat.yml index a6e22122..7a54ffaf 100644 --- a/openvidu-server/deployments/pro/docker-compose/media-node/beats/filebeat.yml +++ b/openvidu-server/deployments/pro/docker-compose/media-node/beats/filebeat.yml @@ -5,6 +5,14 @@ filebeat.inputs: multiline.pattern: '^\d*:\d*:\d*' multiline.negate: true multiline.match: after + - type: log + paths: + - /opt/openvidu/kurento-logs/*.log + fields: + kurento-media-server: true + ip: ${MEDIA_NODE_IP} + cluster-id: ${CLUSTER_ID} + fields_under_root: true processors: - add_docker_metadata: @@ -19,23 +27,21 @@ processors: - add_fields: fields: ip: ${MEDIA_NODE_IP} - + cluster-id: ${CLUSTER_ID} output: elasticsearch: - hosts: ["${OPENVIDU_SERVER_PRO_IP}:9200"] indices: - index: "filebeat-kurento-%{+yyyy.MM.dd}" when.or: - - contains: - container.image.name: kurento/kurento-media-server + - equals: + kurento-media-server: true pipelines: - pipeline: kurento-pipeline when.or: - - contains: - container.image.name: kurento/kurento-media-server + - equals: + kurento-media-server: true logging.json: true logging.metrics.enabled: false -setup.ilm.enabled: true -setup.ilm.policy_name: "openvidu_cleanup_policy" \ No newline at end of file +setup.ilm.enabled: false \ No newline at end of file diff --git a/openvidu-server/deployments/pro/docker-compose/media-node/beats/metricbeat-elasticsearch.yml b/openvidu-server/deployments/pro/docker-compose/media-node/beats/metricbeat-elasticsearch.yml index 0ddb27e9..5a2309d9 100644 --- a/openvidu-server/deployments/pro/docker-compose/media-node/beats/metricbeat-elasticsearch.yml +++ b/openvidu-server/deployments/pro/docker-compose/media-node/beats/metricbeat-elasticsearch.yml @@ -1,14 +1,34 @@ -output: - elasticsearch: - hosts: ["${OPENVIDU_SERVER_PRO_IP}:9200"] metricbeat.modules: - module: system - metricsets: [cpu] - enabled: true - period: ${OPENVIDU_PRO_CLUSTER_LOAD_INTERVAL}s + metricsets: + - cpu + #- diskio + - memory + - network + - filesystem + - fsstat + #- process + - process_summary + - uptime + filesystem.ignore_types: [nfs, smbfs, autofs, devtmpfs, devpts, hugetlbfs, tmpfs, sysfs, securityfs, cgroup2, cgroup, pstore, debugfs, configfs, fusectl, proc, fuse.lxcfs, squashfs] processes: ['.*'] + # process.include_top_n: + # by_cpu: 2 + # by_memory: 2 + processors: + - drop_event: + when: + or: + - regexp: + system.network.name: '^(veth|lo|docker|br-)($|)' + - regexp: + system.filesystem.mount_point: '^/(sys|cgroup|proc|dev|etc|host)($|/)' + - regexp: + system.filesystem.mount_point: '^/hostfs/(sys|cgroup|proc|dev|etc|host)($|/)' + enabled: true + period: ${OPENVIDU_PRO_CLUSTER_LOAD_INTERVAL}0s cpu.metrics: [normalized_percentages] -fields: {ip: "${MEDIA_NODE_IP}"} +fields: {ip: "${MEDIA_NODE_IP}", cluster-id: "${CLUSTER_ID}"} pipeline: queue.mem.events: 0 -setup.ilm.enabled: false \ No newline at end of file +setup.ilm.enabled: false diff --git a/openvidu-server/deployments/pro/docker-compose/media-node/beats/metricbeat-openvidu.yml b/openvidu-server/deployments/pro/docker-compose/media-node/beats/metricbeat-openvidu.yml deleted file mode 100644 index bbd5f726..00000000 --- a/openvidu-server/deployments/pro/docker-compose/media-node/beats/metricbeat-openvidu.yml +++ /dev/null @@ -1,12 +0,0 @@ -output: - logstash.hosts: ["${OPENVIDU_SERVER_PRO_IP}:5044"] -metricbeat.modules: - - module: system - metricsets: [cpu] - enabled: true - period: ${OPENVIDU_PRO_CLUSTER_LOAD_INTERVAL}s - processes: ['.*'] - cpu.metrics: [normalized_percentages] -fields: {ip: "${MEDIA_NODE_IP}"} -pipeline: - queue.mem.events: 0 \ No newline at end of file diff --git a/openvidu-server/deployments/pro/docker-compose/media-node/docker-compose.yml b/openvidu-server/deployments/pro/docker-compose/media-node/docker-compose.yml index fe9ab5b4..4f1f4119 100644 --- a/openvidu-server/deployments/pro/docker-compose/media-node/docker-compose.yml +++ b/openvidu-server/deployments/pro/docker-compose/media-node/docker-compose.yml @@ -6,7 +6,7 @@ # # This docker-compose file coordinates all services of OpenVidu CE Platform. # -# Openvidu Version: 2.15.0 +# Openvidu Version: 2.16.0 # # Installation Mode: On Premises # @@ -16,20 +16,24 @@ version: '3.1' services: media-node-controller: - image: openvidu/media-node-controller:1.0.0 + image: openvidu/media-node-controller:3.0.0-dev4 restart: always ulimits: core: -1 entrypoint: ['/bin/sh', '-c', '/beats/copy_config_files.sh && /usr/local/bin/entrypoint.sh'] environment: - - KMS_IMAGE=${KMS_IMAGE:-kurento/kurento-media-server:6.14.0} - - KMS_DEBUG_LEVEL=${KMS_DEBUG_LEVEL:-} - - METRICBEAT_IMAGE=${METRICBEAT_IMAGE:-docker.elastic.co/beats/metricbeat:7.8.0} - - FILEBEAT_IMAGE=${FILEBEAT_IMAGE:-docker.elastic.co/beats/filebeat:7.8.0} + - KMS_IMAGE=kurento/kurento-media-server:6.15.0 + - METRICBEAT_IMAGE=docker.elastic.co/beats/metricbeat-oss:7.8.0 + - FILEBEAT_IMAGE=docker.elastic.co/beats/filebeat-oss:7.8.0 + - OPENVIDU_RECORDING_IMAGE=openvidu/openvidu-recording:2.17.0-dev1 ports: - 3000:3000 volumes: - /opt/openvidu/recordings:/opt/openvidu/recordings - /opt/openvidu/beats:/opt/openvidu/beats - /var/run/docker.sock:/var/run/docker.sock + - /opt/openvidu/kurento-logs:/opt/openvidu/kurento-logs - ./beats:/beats + logging: + options: + max-size: "100M" diff --git a/openvidu-server/deployments/pro/docker-compose/media-node/install_media_node.sh b/openvidu-server/deployments/pro/docker-compose/media-node/install_media_node.sh index 45e0eb89..403b801e 100755 --- a/openvidu-server/deployments/pro/docker-compose/media-node/install_media_node.sh +++ b/openvidu-server/deployments/pro/docker-compose/media-node/install_media_node.sh @@ -2,8 +2,16 @@ MEDIA_NODE_FOLDER=kms MEDIA_NODE_VERSION=master +OPENVIDU_UPGRADABLE_VERSION="2.16" BEATS_FOLDER=${MEDIA_NODE_FOLDER}/beats DOWNLOAD_URL=https://raw.githubusercontent.com/OpenVidu/openvidu/${MEDIA_NODE_VERSION} +IMAGES=( + "kurento-media-server" + "docker.elastic.co/beats/filebeat" + "docker.elastic.co/beats/metricbeat" + "openvidu/media-node-controller" +) + fatal_error() { printf "\n =======¡ERROR!=======" printf "\n %s" "$1" @@ -11,6 +19,27 @@ fatal_error() { exit 0 } +docker_command_by_container_image() { + IMAGE_NAME=$1 + COMMAND=$2 + if [[ ! -z "${IMAGE_NAME}" ]]; then + CONTAINERS=$(docker ps -a | grep "${IMAGE_NAME}" | awk '{print $1}') + for CONTAINER_ID in ${CONTAINERS[@]}; do + if [[ ! -z "${CONTAINER_ID}" ]] && [[ ! -z "${COMMAND}" ]]; then + bash -c "docker ${COMMAND} ${CONTAINER_ID}" + fi + done + fi +} + + +stop_containers() { + printf "Stopping containers..." + for IMAGE in ${IMAGES[@]}; do + docker_command_by_container_image "${IMAGE}" "rm -f" + done +} + new_media_node_installation() { printf '\n' printf '\n =======================================' @@ -33,10 +62,6 @@ new_media_node_installation() { --output "${MEDIA_NODE_FOLDER}/docker-compose.yml" || fatal_error "Error when downloading the file 'docker-compose.yml'" printf '\n - docker-compose.yml' - curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/media-node/.env \ - --output "${MEDIA_NODE_FOLDER}/.env" || fatal_error "Error when downloading the file '.env'" - printf '\n - .env' - curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/media-node/media_node \ --output "${MEDIA_NODE_FOLDER}/media_node" || fatal_error "Error when downloading the file 'media_node'" printf '\n - media_node' @@ -49,10 +74,6 @@ new_media_node_installation() { --output "${BEATS_FOLDER}/metricbeat-elasticsearch.yml" || fatal_error "Error when downloading the file 'metricbeat-elasticsearch.yml'" printf '\n - metricbeat-elasticsearch.yml' - curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/media-node/beats/metricbeat-openvidu.yml \ - --output "${BEATS_FOLDER}/metricbeat-openvidu.yml" || fatal_error "Error when downloading the file 'metricbeat-openvidu.yml'" - printf '\n - metricbeat-openvidu.yml' - curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/media-node/beats/copy_config_files.sh \ --output "${BEATS_FOLDER}/copy_config_files.sh" || fatal_error "Error when downloading the file 'copy_config_files.sh'" printf '\n - copy_config_files.sh' @@ -68,12 +89,14 @@ new_media_node_installation() { # Pull images printf "\n => Pulling images...\n" cd "${MEDIA_NODE_FOLDER}" || fatal_error "Error when moving to '${MEDIA_NODE_FOLDER}' folder" - KMS_IMAGE=$(cat docker-compose.yml | grep KMS_IMAGE | sed 's/\(^.*KMS_IMAGE:-\)\(.*\)\(\}.*$\)/\2/') - METRICBEAT_IMAGE=$(cat docker-compose.yml | grep METRICBEAT_IMAGE | sed 's/\(^.*METRICBEAT_IMAGE:-\)\(.*\)\(\}.*$\)/\2/') - FILEBEAT_IMAGE=$(cat docker-compose.yml | grep FILEBEAT_IMAGE | sed 's/\(^.*FILEBEAT_IMAGE:-\)\(.*\)\(\}.*$\)/\2/') + KMS_IMAGE=$(cat docker-compose.yml | grep KMS_IMAGE | cut -d"=" -f2) + METRICBEAT_IMAGE=$(cat docker-compose.yml | grep METRICBEAT_IMAGE | cut -d"=" -f2) + FILEBEAT_IMAGE=$(cat docker-compose.yml | grep FILEBEAT_IMAGE | cut -d"=" -f2) + OPENVIDU_RECORDING_IMAGE=$(cat docker-compose.yml | grep OPENVIDU_RECORDING_IMAGE | cut -d"=" -f2) docker pull $KMS_IMAGE || fatal "Error while pulling docker image: $KMS_IMAGE" docker pull $METRICBEAT_IMAGE || fatal "Error while pulling docker image: $METRICBEAT_IMAGE" docker pull $FILEBEAT_IMAGE || fatal "Error while pulling docker image: $FILEBEAT_IMAGE" + docker pull $OPENVIDU_RECORDING_IMAGE || fatal "Error while pulling docker image: $OPENVIDU_RECORDING_IMAGE" docker-compose pull | true # Ready to use @@ -81,16 +104,28 @@ new_media_node_installation() { printf '\n =======================================' printf "\n Media Node successfully installed." printf '\n =======================================' - printf "\n" + printf '\n' printf '\n 1. Go to kms folder:' printf '\n $ cd kms' - printf "\n" + printf '\n' printf '\n 2. Start Media Node Controller' printf '\n $ ./media_node start' printf '\n' - printf "\n For more information, check:" - printf "\n https://docs.openvidu.io/en/${OPENVIDU_VERSION//v}/openvidu-pro/deployment/on-premises/#deployment-instructions" + printf '\n 3. This will run a service at port 3000 which OpenVidu will use to deploy necessary containers.' + printf '\n Add the private ip of this media node in "KMS_URIS=[]" in OpenVidu Pro machine' + printf '\n in file located at "/opt/openvidu/.env" with this format:' + printf '\n ...' + printf '\n KMS_URIS=["ws://:8888/kurento"]' + printf '\n ...' + printf '\n You can also add this node from inspector' printf '\n' + printf '\n 4. Start or restart OpenVidu Pro and all containers will be provisioned' + printf '\n automatically to all the media nodes configured in "KMS_URIS"' + printf '\n More info about Media Nodes deployment here:' + printf "\n --> https://docs.openvidu.io/en/${OPENVIDU_VERSION//v}/openvidu-pro/deployment/on-premises/#set-the-number-of-media-nodes-on-startup" + printf '\n' + printf '\n' + printf "\n If you want to rollback, all the files from the previous installation have been copied to folder '.old-%s'" "${OPENVIDU_PREVIOUS_VERSION}" printf '\n' exit 0 } @@ -123,10 +158,13 @@ upgrade_media_node() { # Uppgrade Media Node OPENVIDU_PREVIOUS_VERSION=$(grep 'Openvidu Version:' "${MEDIA_NODE_PREVIOUS_FOLDER}/docker-compose.yml" | awk '{ print $4 }') - [ -z "${OPENVIDU_PREVIOUS_VERSION}" ] && OPENVIDU_PREVIOUS_VERSION=2.14.0 + [ -z "${OPENVIDU_PREVIOUS_VERSION}" ] && fatal_error "Can't find previous OpenVidu version" # In this point using the variable 'OPENVIDU_PREVIOUS_VERSION' we can verify if the upgrade is # posible or not. If it is not posible launch a warning and stop the upgrade. + if [[ "${OPENVIDU_PREVIOUS_VERSION}" != "${OPENVIDU_UPGRADABLE_VERSION}."* ]]; then + fatal_error "You can't update from version ${OPENVIDU_PREVIOUS_VERSION} to ${OPENVIDU_VERSION}.\nNever upgrade across multiple major versions." + fi printf '\n' printf '\n =======================================' @@ -151,10 +189,6 @@ upgrade_media_node() { --output "${TMP_FOLDER}/docker-compose.yml" || fatal_error "Error when downloading the file 'docker-compose.yml'" printf '\n - docker-compose.yml' - curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/media-node/.env \ - --output "${TMP_FOLDER}/.env" || fatal_error "Error when downloading the file '.env'" - printf '\n - .env' - curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/media-node/media_node \ --output "${TMP_FOLDER}/media_node" || fatal_error "Error when downloading the file 'media_node'" printf '\n - media_node' @@ -167,10 +201,6 @@ upgrade_media_node() { --output "${TMP_FOLDER}/metricbeat-elasticsearch.yml" || fatal_error "Error when downloading the file 'metricbeat-elasticsearch.yml'" printf '\n - metricbeat-elasticsearch.yml' - curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/media-node/beats/metricbeat-openvidu.yml \ - --output "${TMP_FOLDER}/metricbeat-openvidu.yml" || fatal_error "Error when downloading the file 'metricbeat-openvidu.yml'" - printf '\n - metricbeat-openvidu.yml' - curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/media-node/beats/copy_config_files.sh \ --output "${TMP_FOLDER}/copy_config_files.sh" || fatal_error "Error when downloading the file 'copy_config_files.sh'" printf '\n - copy_config_files.sh' @@ -182,16 +212,24 @@ upgrade_media_node() { printf "\n => Moving to 'tmp' folder..." printf '\n' + cd "${TMP_FOLDER}" || fatal_error "Error when moving to '${TMP_FOLDER}' folder" + + printf '\n => Stoping Media Node containers...' + printf '\n' + sleep 1 + + stop_containers # Pull images printf "\n => Pulling images...\n" - cd "${TMP_FOLDER}" || fatal_error "Error when moving to '${TMP_FOLDER}' folder" - KMS_IMAGE=$(cat docker-compose.yml | grep KMS_IMAGE | sed 's/\(^.*KMS_IMAGE:-\)\(.*\)\(\}.*$\)/\2/') - METRICBEAT_IMAGE=$(cat docker-compose.yml | grep METRICBEAT_IMAGE | sed 's/\(^.*METRICBEAT_IMAGE:-\)\(.*\)\(\}.*$\)/\2/') - FILEBEAT_IMAGE=$(cat docker-compose.yml | grep FILEBEAT_IMAGE | sed 's/\(^.*FILEBEAT_IMAGE:-\)\(.*\)\(\}.*$\)/\2/') + KMS_IMAGE=$(cat docker-compose.yml | grep KMS_IMAGE | cut -d"=" -f2) + METRICBEAT_IMAGE=$(cat docker-compose.yml | grep METRICBEAT_IMAGE | cut -d"=" -f2) + FILEBEAT_IMAGE=$(cat docker-compose.yml | grep FILEBEAT_IMAGE | cut -d"=" -f2) + OPENVIDU_RECORDING_IMAGE=$(cat docker-compose.yml | grep OPENVIDU_RECORDING_IMAGE | cut -d"=" -f2) docker pull $KMS_IMAGE || fatal "Error while pulling docker image: $KMS_IMAGE" docker pull $METRICBEAT_IMAGE || fatal "Error while pulling docker image: $METRICBEAT_IMAGE" docker pull $FILEBEAT_IMAGE || fatal "Error while pulling docker image: $FILEBEAT_IMAGE" + docker pull $OPENVIDU_RECORDING_IMAGE || fatal "Error while pulling docker image: $OPENVIDU_RECORDING_IMAGE" docker-compose pull | true printf '\n => Stoping Media Node...' @@ -216,14 +254,8 @@ upgrade_media_node() { mv "${MEDIA_NODE_PREVIOUS_FOLDER}/media_node" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'openvidu'" printf '\n - media_node' - mv "${MEDIA_NODE_PREVIOUS_FOLDER}/readme.md" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'readme.md'" - printf '\n - readme.md' - - mv "${MEDIA_NODE_PREVIOUS_FOLDER}/nginx_conf" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'nginx_conf'" - printf '\n - nginx_conf' - - cp "${MEDIA_NODE_PREVIOUS_FOLDER}/.env" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous '.env'" - printf '\n - .env' + mv "${MEDIA_NODE_PREVIOUS_FOLDER}/beats" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'beats' folder" + printf '\n - beats' # Move tmp files to Openvidu printf '\n => Updating files:' @@ -231,9 +263,6 @@ upgrade_media_node() { mv "${TMP_FOLDER}/docker-compose.yml" "${MEDIA_NODE_PREVIOUS_FOLDER}" || fatal_error "Error while updating 'docker-compose.yml'" printf '\n - docker-compose.yml' - mv "${TMP_FOLDER}/.env" "${MEDIA_NODE_PREVIOUS_FOLDER}/.env-${MEDIA_NODE_VERSION}" || fatal_error "Error while moving previous '.env'" - printf '\n - .env-%s' "${MEDIA_NODE_VERSION}" - mv "${TMP_FOLDER}/media_node" "${MEDIA_NODE_PREVIOUS_FOLDER}" || fatal_error "Error while updating 'media_node'" printf '\n - media_node' @@ -245,9 +274,6 @@ upgrade_media_node() { mv "${TMP_FOLDER}/metricbeat-elasticsearch.yml" "${MEDIA_NODE_PREVIOUS_FOLDER}/beats" || fatal_error "Error while updating 'metricbeat-elasticsearch.yml'" printf '\n - metricbeat-elasticsearch.yml' - mv "${TMP_FOLDER}/metricbeat-openvidu.yml" "${MEDIA_NODE_PREVIOUS_FOLDER}/beats" || fatal_error "Error while updating 'metricbeat-openvidu.yml'" - printf '\n - metricbeat-openvidu.yml' - mv "${TMP_FOLDER}/copy_config_files.sh" "${MEDIA_NODE_PREVIOUS_FOLDER}/beats" || fatal_error "Error while updating 'copy_config_files.sh'" printf '\n - copy_config_files.sh' @@ -275,17 +301,27 @@ upgrade_media_node() { printf '\n' printf "\n 1. A new file 'docker-compose.yml' has been created with the new OpenVidu %s services" "${OPENVIDU_VERSION}" printf '\n' - printf "\n 2. The previous file '.env' remains intact, but a new file '.env-%s' has been created." "${OPENVIDU_VERSION}" - printf "\n Transfer any configuration you wish to keep in the upgraded version from '.env' to '.env-%s'." "${OPENVIDU_VERSION}" - printf "\n When you are OK with it, rename and leave as the only '.env' file of the folder the new '.env-%s'." "${OPENVIDU_VERSION}" + printf "\n 2. This new version %s does not need any .env file. Everything is configured from OpenVidu Pro" "${OPENVIDU_VERSION}" printf '\n' printf '\n 3. Start new version of Media Node' printf '\n $ ./media_node start' printf '\n' + printf '\n 4. This will run a service at port 3000 which OpenVidu will use to deploy necessary containers.' + printf '\n Add the private ip of this media node in "KMS_URIS=[]" in OpenVidu Pro machine' + printf '\n in file located at "/opt/openvidu/.env" with this format:' + printf '\n ...' + printf '\n KMS_URIS=["ws://:8888/kurento"]' + printf '\n ...' + printf '\n You can also add Media Nodes from inspector' + printf '\n' + printf '\n 5. Start or restart OpenVidu Pro and all containers will be provisioned' + printf '\n automatically to all the media nodes configured in "KMS_URIS"' + printf '\n More info about Media Nodes deployment here:' + printf "\n --> https://docs.openvidu.io/en/${OPENVIDU_VERSION//v}/openvidu-pro/deployment/on-premises/#set-the-number-of-media-nodes-on-startup" + printf '\n' + printf '\n' printf "\n If you want to rollback, all the files from the previous installation have been copied to folder '.old-%s'" "${OPENVIDU_PREVIOUS_VERSION}" printf '\n' - printf '\n' - printf '\n' } # Check docker and docker-compose installation diff --git a/openvidu-server/deployments/pro/docker-compose/media-node/media_node b/openvidu-server/deployments/pro/docker-compose/media-node/media_node index b55a4122..8b142871 100755 --- a/openvidu-server/deployments/pro/docker-compose/media-node/media_node +++ b/openvidu-server/deployments/pro/docker-compose/media-node/media_node @@ -1,5 +1,41 @@ #!/bin/bash +# Deployed images in media-node +IMAGES=( + "kurento-media-server" + "docker.elastic.co/beats/filebeat" + "docker.elastic.co/beats/metricbeat" + "openvidu/media-node-controller" +) + +docker_command_by_container_image() { + IMAGE_NAME=$1 + COMMAND=$2 + if [[ ! -z "${IMAGE_NAME}" ]]; then + CONTAINERS=$(docker ps -a | grep "${IMAGE_NAME}" | awk '{print $1}') + for CONTAINER_ID in ${CONTAINERS[@]}; do + if [[ ! -z "${CONTAINER_ID}" ]] && [[ ! -z "${COMMAND}" ]]; then + bash -c "docker ${COMMAND} ${CONTAINER_ID}" + fi + done + fi +} + + +stop_containers() { + printf "Stopping containers..." + for IMAGE in ${IMAGES[@]}; do + docker_command_by_container_image "${IMAGE}" "rm -f" + done +} +kurento_logs() { + if [[ "$1" == "-f" ]]; then + tail -f /opt/openvidu/kurento-logs/*.log + else + cat /opt/openvidu/kurento-logs/*.log + fi +} + upgrade_media_node() { UPGRADE_SCRIPT_URL="https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/install_media_node_OVVERSION.sh" HTTP_STATUS=$(curl -s -o /dev/null -I -w "%{http_code}" ${UPGRADE_SCRIPT_URL//OVVERSION/$1}) @@ -140,6 +176,16 @@ generate_report() { printf '\n' done + printf '\n' + printf "\n ---------------------------------------" + printf "\n KMS" + printf "\n ---------------------------------------" + printf '\n' + kurento_logs + printf "\n ---------------------------------------" + printf '\n' + printf '\n' + printf "\n =======================================" printf "\n = CONTAINER ENVS VARIABLES =" printf "\n =======================================" @@ -170,7 +216,8 @@ usage() { printf "\n\tstart\t\t\tStart media node service" printf "\n\tstop\t\t\tStop media node service" printf "\n\trestart\t\t\tRestart media node service" - printf "\n\tlogs\t\t\tShow media node logs" + printf "\n\tlogs [-f]\t\tShow media-node-controller logs." + printf "\n\tkms-logs [-f]\t\tShow kms logs" printf "\n\tupgrade\t\t\tUpgrade to the lastest Media Node version" printf "\n\tupgrade [version]\tUpgrade to the specific Media Node version" printf "\n\tversion\t\t\tShow version of Media Node" @@ -179,15 +226,6 @@ usage() { printf "\n" } -stop_containers() { - CONTAINERS=$(docker ps | awk '{if(NR>1) print $NF}') - for CONTAINER in $CONTAINERS - do - [ "$(docker ps -a | grep ${CONTAINER})" ] && docker stop ${CONTAINER} - done - -} - case $1 in start) @@ -208,7 +246,18 @@ case $1 in ;; logs) - docker-compose logs -f media-node-controller + case $2 in + "-f") + docker-compose logs -f media-node-controller + ;; + *) + docker-compose logs media-node-controller + ;; + esac + ;; + + kms-logs) + kurento_logs $2 ;; upgrade) diff --git a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/.env b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/.env index 458697fa..911977b2 100644 --- a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/.env +++ b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/.env @@ -42,6 +42,22 @@ LETSENCRYPT_EMAIL=user@example.com # SDKs, REST clients and browsers will have to connect to this port # HTTPS_PORT=443 +# Old paths are considered now deprecated, but still supported by default. +# OpenVidu Server will log a WARN message every time a deprecated path is called, indicating +# the new path that should be used instead. You can set property SUPPORT_DEPRECATED_API=false +# to stop allowing the use of old paths. +# Default value is true +# SUPPORT_DEPRECATED_API=true + +# If true request to with www will be redirected to non-www requests +# Default value is false +# REDIRECT_WWW=false + +# How many workers to configure in nginx proxy. +# The more workers, the more requests will be handled +# Default value is 10240 +# WORKER_CONNECTIONS=10240 + # Access restrictions # In this section you will be able to restrict the IPs from which you can access to # Openvidu API and the Administration Panel @@ -71,6 +87,12 @@ OPENVIDU_PRO_CLUSTER_MODE=manual # Possibles values: aws, on_premise OPENVIDU_PRO_CLUSTER_ENVIRONMENT=on_premise +# Unique identifier of your cluster. Each OpenVidu Server Pro instance corresponds to one cluster. +# You can launch as many clusters as you want with your license key. +# Cluster ID will always be stored to disk so restarting OpenVidu Server Pro will keep the same previous cluster ID +# if this configuration parameter is not given a distinct value. +# OPENVIDU_PRO_CLUSTER_ID= + # The desired number of Media Nodes on startup. First the autodiscovery process is performed. # If there are too many Media Nodes after that, they will be dropped until this number is reached. # If there are not enough, more will be launched. @@ -85,7 +107,6 @@ OPENVIDU_PRO_CLUSTER_ENVIRONMENT=on_premise # Type: number >= 0 # OPENVIDU_PRO_CLUSTER_LOAD_INTERVAL= - # Whether to enable or disable autoscaling. With autoscaling the number of Media Nodes will # be automatically adjusted according to existing load # Values: true | false @@ -119,13 +140,55 @@ OPENVIDU_PRO_CLUSTER_AUTOSCALING=false # (and therefore distribution of load) among all available Media Nodes OPENVIDU_PRO_CLUSTER_LOAD_STRATEGY=streams +# Whether to enable or disable Network Quality API. You can monitor and +# warn users about the quality of their networks with this feature +# OPENVIDU_PRO_NETWORK_QUALITY=false + +# If OPENVIDU_PRO_NETWORK_QUALITY=true, how often the network quality +# algorithm will be invoked for each user, in seconds +# OPENVIDU_PRO_NETWORK_QUALITY_INTERVAL=5 + # Max days until delete indexes in state of rollover on Elasticsearch # Type number >= 0 +# Default Value is 15 # OPENVIDU_PRO_ELASTICSEARCH_MAX_DAYS_DELETE= -# Private IP of OpenVidu Server Pro -# For example 192.168.1.101 -# OPENVIDU_PRO_PRIVATE_IP= +# If you have an external Elasticsearch and Kibana already running, put here the url to elasticsearch and kibana services. +# It is very important that both url have the port specified in the url. +# If you want to use the deployed Elasticsearch and Kibana locally, keep these variables commented. +#OPENVIDU_PRO_ELASTICSEARCH_HOST= +#OPENVIDU_PRO_KIBANA_HOST= + +# Where to store recording files. Can be 'local' (local storage) or 's3' (AWS bucket). +# You will need to define a OPENVIDU_PRO_AWS_S3_BUCKET if you use it. +#OPENVIDU_PRO_RECORDING_STORAGE= + +# S3 Bucket where to store recording files. May include paths to allow navigating +# folder structures inside the bucket. This property is only taken into account +# if OPENVIDU_PRO_RECORDING_STORAGE=s3 +#OPENVIDU_PRO_AWS_S3_BUCKET= + +# If OPENVIDU_PRO_RECORDING_STORAGE=s3, the collection of HTTP header values that the internal AWS client will use during +# the upload process. The property is a key-value map of strings, following the format of a JSON object. For example, for applying +# server-side encryption with AES-256, this header is mandatory: {"x-amz-server-side-encryption":"AES256"}. +# The list of available headers can be found here: https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/Headers.html +# This property is only taken into account if OPENVIDU_PRO_RECORDING_STORAGE=s3 +#OPENVIDU_PRO_AWS_S3_HEADERS= + +# If you're instance has a role which has access to read +# and write into the s3 bucket, you don't need this parameter +# OPENVIDU_PRO_AWS_ACCESS_KEY= + +# AWS credentials secret key from OPENVIDU_PRO_AWS_ACCESS_KEY. This property is only +# taken into account if OPENVIDU_PRO_RECORDING_STORAGE=s3 +# If you're instance has a role which has access to read +# and write into the s3 bucket, you don't need this parameter +# OPENVIDU_PRO_AWS_SECRET_KEY= + +# AWS region in which the S3 bucket is located (e.g. eu-west-1). If not provided, +# the region will try to be discovered automatically, although this is not always possible. +# This property is only taken into account if OPENVIDU_PRO_RECORDING_STORAGE=s3 +# OPENVIDU_PRO_AWS_REGION= # Whether to enable recording module or not OPENVIDU_RECORDING=false @@ -178,12 +241,14 @@ OPENVIDU_STREAMS_VIDEO_MAX_SEND_BANDWIDTH=1000 # 0 means unconstrained OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH=300 -# All sessions of OpenVidu will try to force this codec. If OPENVIDU_ALLOW_TRANSCODING=true -# when a codec can not be forced, transcoding will be allowed -# OPENVIDU_FORCED_CODEC=VP8 +# All sessions of OpenVidu will try to force this codec. If OPENVIDU_STREAMS_ALLOW_TRANSCODING=true +# when a codec can not be forced, transcoding will be allowed +# Default value is VP8 +# OPENVIDU_STREAMS_FORCED_VIDEO_CODEC=VP8 -# Allow transcoding if codec specified in OPENVIDU_FORCED_CODEC can not be applied -# OPENVIDU_ALLOW_TRANSCODING=false +# Allow transcoding if codec specified in OPENVIDU_STREAMS_FORCED_VIDEO_CODEC can not be applied +# Default value is false +# OPENVIDU_STREAMS_ALLOW_TRANSCODING=false # true to enable OpenVidu Webhook service. false' otherwise # Values: true | false @@ -227,17 +292,44 @@ OPENVIDU_CDR_PATH=/opt/openvidu/cdr # RECOMENDED VALUES: INFO for normal logs DEBUG for more verbose logs # OV_CE_DEBUG_LEVEL=INFO -# Java Options +# OpenVidu Java Options # -------------------------- # Uncomment the next line and define this to add options to java command # Documentation: https://docs.oracle.com/cd/E37116_01/install.111210/e23737/configuring_jvm.htm#OUDIG00058 # JAVA_OPTIONS=-Xms2048m -Xmx4096m -# Kibana And ElasticSearch Configuration +# ElasticSearch Java Options # -------------------------- -# Kibana dashboard configuration (Credentials) -KIBANA_USER=kibanaadmin -KIBANA_PASSWORD= +# Uncomment the next line and define this to add options to java command of Elasticsearch +# Documentation: https://docs.oracle.com/cd/E37116_01/install.111210/e23737/configuring_jvm.htm#OUDIG00058 +# By default ElasticSearch is configured to use "-Xms2g -Xmx2g" as Java Min and Max memory heap allocation +# ES_JAVA_OPTS=-Xms2048m -Xmx4096m + +# Kibana And ElasticSearch Credentials Configuration +# -------------------------- +# Kibana And ElasticSearch Basic Auth configuration (Credentials) +# This credentials will aso be valid for Kibana dashboard +ELASTICSEARCH_USERNAME=elasticadmin +ELASTICSEARCH_PASSWORD= + +# Media Node Configuration +# -------------------------- +# You can add any KMS environment variable as described in the +# documentation of the docker image: https://hub.docker.com/r/kurento/kurento-media-server +# If you want to add an environment variable to KMS, you must add a variable using this prefix: 'KMS_DOCKER_ENV_', +# followed by the environment variable you want to setup. +# For example if you want to setup KMS_MIN_PORT to 50000, it would be KMS_DOCKER_ENV_KMS_MIN_PORT=50000 + +# Docker hub kurento media server: https://hub.docker.com/r/kurento/kurento-media-server +# Uncomment the next line and define this variable with KMS image that you want use +# By default, KMS_IMAGE is defined in media nodes and it does not need to be specified unless +# you want to use a specific version of KMS +# KMS_IMAGE=kurento/kurento-media-server:6.15.0 + +# Uncomment the next line and define this variable to change +# the verbosity level of the logs of KMS +# Documentation: https://doc-kurento.readthedocs.io/en/stable/features/logging.html +# KMS_DOCKER_ENV_GST_DEBUG= # Cloudformation configuration # -------------------------- @@ -250,3 +342,5 @@ KIBANA_PASSWORD= #AWS_SECURITY_GROUP= #AWS_STACK_ID= #AWS_STACK_NAME= +#AWS_CLI_DOCKER_TAG= +#AWS_VOLUME_SIZE= diff --git a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/beats/filebeat.yml b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/beats/filebeat.yml index c84977e8..369daa45 100644 --- a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/beats/filebeat.yml +++ b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/beats/filebeat.yml @@ -13,20 +13,36 @@ processors: fields: ["message"] target: "json" overwrite_keys: true + - drop_event: + when.or: + - contains: + container.image.name: openvidu/openvidu-coturn + - contains: + container.image.name: docker.elastic.co/elasticsearch/elasticsearch + - contains: + container.image.name: docker.elastic.co/kibana/kibana + - contains: + container.image.name: docker.elastic.co/beats/filebeat-oss + - contains: + container.image.name: docker.elastic.co/beats/metricbeat-oss + - add_fields: + fields: + cluster-id: ${OPENVIDU_PRO_CLUSTER_ID:undefined} output: elasticsearch: - hosts: ["elasticsearch:9200"] + hosts: ["${OPENVIDU_PRO_ELASTICSEARCH_HOST}"] indices: - index: "filebeat-redis-%{+yyyy.MM.dd}" when.or: - contains: container.image.name: openvidu/openvidu-redis - - index: "filebeat-coturn-%{+yyyy.MM.dd}" + - index: "filebeat-nginx-%{+yyyy.MM.dd}" when.or: - contains: - container.image.name: openvidu/openvidu-coturn + container.image.name: openvidu/openvidu-proxy logging.json: true -logging.metrics.enabled: false \ No newline at end of file +logging.metrics.enabled: false +setup.ilm.enabled: false \ No newline at end of file diff --git a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/beats/metricbeat.yml b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/beats/metricbeat.yml new file mode 100644 index 00000000..09a68d9a --- /dev/null +++ b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/beats/metricbeat.yml @@ -0,0 +1,10 @@ +metricbeat.modules: +- module: nginx + metricsets: ["stubstatus"] + enabled: true + period: 10s + hosts: ["http://127.0.0.1"] + server_status_path: "nginx_status" +output: + elasticsearch: + hosts: ["${OPENVIDU_PRO_ELASTICSEARCH_HOST}"] diff --git a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/cluster/aws/openvidu_autodiscover.sh b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/cluster/aws/openvidu_autodiscover.sh index 0eb86058..8689f3b7 100644 --- a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/cluster/aws/openvidu_autodiscover.sh +++ b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/cluster/aws/openvidu_autodiscover.sh @@ -7,7 +7,7 @@ DEBUG=${DEBUG:-false} OUTPUT=$(mktemp -t openvidu-autodiscover-XXX --suffix .json) -docker run --rm amazon/aws-cli:2.0.7 ec2 describe-instances \ +docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} ec2 describe-instances \ --output text \ --filters "Name=instance-state-name,Values=running" \ "Name=tag:ov-cluster-member,Values=kms" \ diff --git a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/cluster/aws/openvidu_drop.sh b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/cluster/aws/openvidu_drop.sh index 27cc8d76..8d585480 100644 --- a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/cluster/aws/openvidu_drop.sh +++ b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/cluster/aws/openvidu_drop.sh @@ -8,4 +8,4 @@ DEBUG=${DEBUG:-false} ID=$1 [ -z "${ID}" ] && { echo "Must provide instance ID"; exit 1; } -docker run --rm amazon/aws-cli:2.0.7 ec2 terminate-instances --instance-ids ${ID} --output json +docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} ec2 terminate-instances --instance-ids ${ID} --output json diff --git a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/cluster/aws/openvidu_launch_kms.sh b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/cluster/aws/openvidu_launch_kms.sh index 186ce5a0..48b92bf7 100644 --- a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/cluster/aws/openvidu_launch_kms.sh +++ b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/cluster/aws/openvidu_launch_kms.sh @@ -23,7 +23,7 @@ exit_on_error () { "UnauthorizedOperation") MSG_COD=$(cat ${ERROUTPUT} | awk -F: '{ print $3 }') - MSG_DEC=$(docker run --rm amazon/aws-cli:2.0.7 sts decode-authorization-message --encoded-message ${MSG_COD}) + MSG_DEC=$(docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} sts decode-authorization-message --encoded-message ${MSG_COD}) echo -e "Unauthorized " $(cat ${MSG_DEC}) >&2 exit 1 @@ -35,16 +35,17 @@ exit_on_error () { esac } -docker run --rm amazon/aws-cli:2.0.7 ec2 run-instances \ +docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} ec2 run-instances \ --image-id ${AWS_IMAGE_ID} --count 1 \ --instance-type ${AWS_INSTANCE_TYPE} \ --key-name ${AWS_KEY_NAME} \ --subnet-id ${AWS_SUBNET_ID} \ --tag-specifications "ResourceType=instance,Tags=[{Key='Name',Value='Kurento Media Server'},{Key='ov-cluster-member',Value='kms'},{Key='ov-stack-name',Value='${AWS_STACK_NAME}'},{Key='ov-stack-region',Value='${AWS_DEFAULT_REGION}'}]" \ --iam-instance-profile Name="OpenViduInstanceProfile-${AWS_STACK_NAME}-${AWS_DEFAULT_REGION}" \ + --block-device-mappings "DeviceName=/dev/sda1,Ebs={DeleteOnTermination=True,VolumeType='gp2',VolumeSize='${AWS_VOLUME_SIZE}'}" \ --security-group-ids ${AWS_SECURITY_GROUP} > ${OUTPUT} 2> ${ERROUTPUT} -docker run --rm amazon/aws-cli:2.0.7 ec2 wait instance-running --instance-ids $(cat ${OUTPUT} | jq --raw-output ' .Instances[] | .InstanceId') +docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} ec2 wait instance-running --instance-ids $(cat ${OUTPUT} | jq --raw-output ' .Instances[] | .InstanceId') # Generating the output KMS_IP=$(cat ${OUTPUT} | jq --raw-output ' .Instances[] | .NetworkInterfaces[0] | .PrivateIpAddress') diff --git a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/docker-compose.override.yml b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/docker-compose.override.yml index c52129f9..d4796d20 100644 --- a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/docker-compose.override.yml +++ b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/docker-compose.override.yml @@ -9,11 +9,11 @@ services: # # Default Application # - # Openvidu-Call Version: 2.15.0 + # Openvidu-Call Version: 2.16.0 # # -------------------------------------------------------------- app: - image: openvidu/openvidu-call:2.15.0 + image: openvidu/openvidu-call:2.16.0 restart: on-failure network_mode: host environment: @@ -21,3 +21,6 @@ services: - OPENVIDU_URL=http://localhost:5443 - OPENVIDU_SECRET=${OPENVIDU_SECRET} - CALL_OPENVIDU_CERTTYPE=${CERTIFICATE_TYPE} + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" diff --git a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/docker-compose.yml b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/docker-compose.yml index 5cd67732..3eb8d3cd 100644 --- a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/docker-compose.yml +++ b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/docker-compose.yml @@ -7,11 +7,11 @@ # Application based on OpenVidu should be specified in # docker-compose.override.yml file # -# This docker-compose file coordinates all services of OpenVidu CE Platform. +# This docker-compose file coordinates all services of OpenVidu Pro Platform # # This file will be overridden when update OpenVidu Platform # -# Openvidu Version: 2.15.1 +# Openvidu Version: 2.16.0 # # Installation Mode: On Premises # @@ -22,10 +22,10 @@ version: '3.1' services: openvidu-server: - image: openvidu/openvidu-server-pro:2.15.1 + image: openvidu/openvidu-server-pro:2.17.0-dev5 restart: on-failure network_mode: host - entrypoint: ['/bin/bash', '-c', 'export COTURN_IP=`/usr/local/bin/discover_my_public_ip.sh`; /usr/local/bin/entrypoint.sh'] + entrypoint: ['/usr/local/bin/entrypoint.sh'] volumes: - /var/run/docker.sock:/var/run/docker.sock - ${OPENVIDU_RECORDING_PATH}:${OPENVIDU_RECORDING_PATH} @@ -41,21 +41,28 @@ services: - KMS_URIS=[] - COTURN_REDIS_IP=127.0.0.1 - COTURN_REDIS_PASSWORD=${OPENVIDU_SECRET} + - COTURN_IP=${COTURN_IP:-auto-ipv4} - OPENVIDU_PRO_CLUSTER=true - - OPENVIDU_PRO_KIBANA_HOST=http://127.0.0.1/kibana - - OPENVIDU_PRO_ELASTICSEARCH_HOST=http://127.0.0.1:9200 - - WAIT_KIBANA_URL=http://127.0.0.1:5601/api/status + - OPENVIDU_PRO_KIBANA_HOST=${OPENVIDU_PRO_KIBANA_HOST:-http://127.0.0.1/kibana} + - OPENVIDU_PRO_ELASTICSEARCH_HOST=${OPENVIDU_PRO_ELASTICSEARCH_HOST:-http://127.0.0.1:9200} + - WAIT_KIBANA_URL=${OPENVIDU_PRO_KIBANA_HOST:-http://127.0.0.1/kibana} - DOTENV_PATH=${PWD} + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" redis: - image: openvidu/openvidu-redis:1.0.0 + image: openvidu/openvidu-redis:2.0.0-dev2 restart: always network_mode: host environment: - REDIS_PASSWORD=${OPENVIDU_SECRET} + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" coturn: - image: openvidu/openvidu-coturn:1.0.0 + image: openvidu/openvidu-coturn:3.0.0-dev2 restart: on-failure network_mode: host environment: @@ -63,17 +70,22 @@ services: - TURN_LISTEN_PORT=3478 - DB_NAME=0 - DB_PASSWORD=${OPENVIDU_SECRET} - - MIN_PORT=57001 + - MIN_PORT=40000 - MAX_PORT=65535 + - TURN_PUBLIC_IP=${TURN_PUBLIC_IP:-auto-ipv4} + - ENABLE_COTURN_LOGS=true + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" nginx: - image: openvidu/openvidu-proxy:3.0.0 + image: openvidu/openvidu-proxy:5.0.0-dev2 restart: on-failure network_mode: host - entrypoint: ['/bin/sh', '-c', 'htpasswd -bc /etc/nginx/kibana.htpasswd "${KIBANA_USER}" "${KIBANA_PASSWORD}" && /usr/local/bin/entrypoint.sh'] volumes: - ./certificates:/etc/letsencrypt - ./owncert:/owncert + - ./custom-nginx-vhosts:/etc/nginx/vhost.d/ - ${OPENVIDU_RECORDING_CUSTOM_LAYOUT}:/opt/openvidu/custom-layout environment: - DOMAIN_OR_PUBLIC_IP=${DOMAIN_OR_PUBLIC_IP} @@ -85,31 +97,84 @@ services: - ALLOWED_ACCESS_TO_RESTAPI=${ALLOWED_ACCESS_TO_RESTAPI:-} - PROXY_MODE=PRO - WITH_APP=true + - SUPPORT_DEPRECATED_API=${SUPPORT_DEPRECATED_API:-true} + - REDIRECT_WWW=${REDIRECT_WWW:-false} + - WORKER_CONNECTIONS=${WORKER_CONNECTIONS:-10240} + - PUBLIC_IP=${PROXY_PUBLIC_IP:-auto-ipv4} + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" + elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.8.0 restart: always environment: - discovery.type=single-node + - xpack.security.enabled=true + - "ES_JAVA_OPTS=${ES_JAVA_OPTS:--Xms2g -Xmx2g}" ports: - 9200:9200 volumes: - ./elasticsearch:/usr/share/elasticsearch/data + command: > + /bin/bash -c "elasticsearch-users useradd ${ELASTICSEARCH_USERNAME} + -p ${ELASTICSEARCH_PASSWORD} -r superuser; + docker-entrypoint.sh" + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" kibana: image: docker.elastic.co/kibana/kibana:7.8.0 restart: always environment: - SERVER_BASEPATH="/kibana" + - xpack.security.enabled=true + - ELASTICSEARCH_USERNAME=${ELASTICSEARCH_USERNAME} + - ELASTICSEARCH_PASSWORD=${ELASTICSEARCH_PASSWORD} ports: - 5601:5601 + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" - filebeat: - image: docker.elastic.co/beats/filebeat:7.8.0 + metricbeat: + image: docker.elastic.co/beats/metricbeat-oss:7.8.0 + network_mode: host restart: always user: root + env_file: + - .env + environment: + - OPENVIDU_PRO_ELASTICSEARCH_HOST=${OPENVIDU_PRO_ELASTICSEARCH_HOST:-http://127.0.0.1:9200} + volumes: + - ./beats/metricbeat.yml:/usr/share/metricbeat/metricbeat.yml:ro + command: > + /bin/bash -c "metricbeat -e -strict.perms=false + `if [ ! -z $ELASTICSEARCH_USERNAME ]; then echo '-E output.elasticsearch.username=$ELASTICSEARCH_USERNAME'; fi` + `if [ ! -z $ELASTICSEARCH_PASSWORD ]; then echo '-E output.elasticsearch.password=$ELASTICSEARCH_PASSWORD'; fi`" + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" + + filebeat: + image: docker.elastic.co/beats/filebeat-oss:7.8.0 + network_mode: host + restart: always + user: root + env_file: + - .env + environment: + - OPENVIDU_PRO_ELASTICSEARCH_HOST=${OPENVIDU_PRO_ELASTICSEARCH_HOST:-http://127.0.0.1:9200} volumes: - ./beats/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro - /var/lib/docker:/var/lib/docker:ro - /var/run/docker.sock:/var/run/docker.sock - command: filebeat -e -strict.perms=false + command: > + /bin/bash -c "filebeat -e -strict.perms=false + `if [ ! -z $ELASTICSEARCH_USERNAME ]; then echo '-E output.elasticsearch.username=$ELASTICSEARCH_USERNAME'; fi` + `if [ ! -z $ELASTICSEARCH_PASSWORD ]; then echo '-E output.elasticsearch.password=$ELASTICSEARCH_PASSWORD'; fi`" + logging: + options: + max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" diff --git a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/install_openvidu_pro.sh b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/install_openvidu_pro.sh index 5dd1ca1f..2dc933a8 100755 --- a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/install_openvidu_pro.sh +++ b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/install_openvidu_pro.sh @@ -3,6 +3,7 @@ # Global variables OPENVIDU_FOLDER=openvidu OPENVIDU_VERSION=master +OPENVIDU_UPGRADABLE_VERSION="2.16" AWS_SCRIPTS_FOLDER=${OPENVIDU_FOLDER}/cluster/aws ELASTICSEARCH_FOLDER=${OPENVIDU_FOLDER}/elasticsearch BEATS_FOLDER=${OPENVIDU_FOLDER}/beats @@ -60,6 +61,10 @@ new_ov_installation() { --output "${BEATS_FOLDER}/filebeat.yml" || fatal_error "Error when downloading the file 'filebeat.yml'" printf '\n - filebeat.yml' + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/beats/metricbeat.yml \ + --output "${BEATS_FOLDER}/metricbeat.yml" || fatal_error "Error when downloading the file 'metricbeat.yml'" + printf '\n - metricbeat.yml' + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/.env \ --output "${OPENVIDU_FOLDER}/.env" || fatal_error "Error when downloading the file '.env'" printf '\n - .env' @@ -95,6 +100,10 @@ new_ov_installation() { printf "\n => Creating folder 'owncert'..." mkdir "${OPENVIDU_FOLDER}/owncert" || fatal_error "Error while creating the folder 'owncert'" + # Create vhost nginx folder + printf "\n => Creating folder 'custom-nginx-vhosts'..." + mkdir "${OPENVIDU_FOLDER}/custom-nginx-vhosts" || fatal_error "Error while creating the folder 'custom-nginx-vhosts'" + # Ready to use printf '\n' printf '\n' @@ -105,13 +114,15 @@ new_ov_installation() { printf '\n 1. Go to openvidu folder:' printf '\n $ cd openvidu' printf '\n' - printf '\n 2. Configure OPENVIDU_DOMAIN_OR_PUBLIC_IP, OPENVIDU_PRO_LICENSE, OPENVIDU_SECRET, and KIBANA_PASSWORD in .env file:' + printf '\n 2. Configure OPENVIDU_DOMAIN_OR_PUBLIC_IP, OPENVIDU_PRO_LICENSE, ' + printf '\n OPENVIDU_SECRET, and ELASTICSEARCH_PASSWORD in .env file:' printf '\n $ nano .env' printf '\n' printf '\n 3. Start OpenVidu' printf '\n $ ./openvidu start' printf '\n' - printf "\n CAUTION: The folder 'openvidu/elasticsearch' use user and group 1000 permissions. This folder is necessary for store elasticsearch data." + printf "\n CAUTION: The folder 'openvidu/elasticsearch' use user and group 1000 permissions. " + printf "\n This folder is necessary for store elasticsearch data." printf "\n For more information, check:" printf "\n https://docs.openvidu.io/en/${OPENVIDU_VERSION//v}/openvidu-pro/deployment/on-premises/#deployment-instructions" printf '\n' @@ -147,10 +158,13 @@ upgrade_ov() { # Uppgrade Openvidu OPENVIDU_PREVIOUS_VERSION=$(grep 'Openvidu Version:' "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml" | awk '{ print $4 }') - [ -z "${OPENVIDU_PREVIOUS_VERSION}" ] && OPENVIDU_PREVIOUS_VERSION=2.13.0 + [ -z "${OPENVIDU_PREVIOUS_VERSION}" ] && fatal_error "Can't find previous OpenVidu version" # In this point using the variable 'OPENVIDU_PREVIOUS_VERSION' we can verify if the upgrade is # posible or not. If it is not posible launch a warning and stop the upgrade. + if [[ "${OPENVIDU_PREVIOUS_VERSION}" != "${OPENVIDU_UPGRADABLE_VERSION}."* ]]; then + fatal_error "You can't update from version ${OPENVIDU_PREVIOUS_VERSION} to ${OPENVIDU_VERSION}.\nNever upgrade across multiple major versions." + fi printf '\n' printf '\n =======================================' @@ -188,6 +202,10 @@ upgrade_ov() { --output "${TMP_FOLDER}/filebeat.yml" || fatal_error "Error when downloading the file 'filebeat.yml'" printf '\n - filebeat.yml' + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/beats/metricbeat.yml \ + --output "${TMP_FOLDER}/metricbeat.yml" || fatal_error "Error when downloading the file 'metricbeat.yml'" + printf '\n - metricbeat.yml' + curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/.env \ --output "${TMP_FOLDER}/.env" || fatal_error "Error when downloading the file '.env'" printf '\n - .env' @@ -243,15 +261,20 @@ upgrade_ov() { mv "${OPENVIDU_PREVIOUS_FOLDER}/openvidu" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'openvidu'" printf '\n - openvidu' - mv "${OPENVIDU_PREVIOUS_FOLDER}/readme.md" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'readme.md'" - printf '\n - readme.md' - mv "${OPENVIDU_PREVIOUS_FOLDER}/cluster/aws" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'cluster/aws'" printf '\n - cluster/aws' + mv "${OPENVIDU_PREVIOUS_FOLDER}/beats" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'beats'" + printf '\n - beats' + cp "${OPENVIDU_PREVIOUS_FOLDER}/.env" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous '.env'" printf '\n - .env' + if [ -d "${OPENVIDU_PREVIOUS_FOLDER}/custom-nginx-vhosts" ]; then + mv "${OPENVIDU_PREVIOUS_FOLDER}/custom-nginx-vhosts" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous directory 'custom-nginx-vhosts'" + printf '\n - custom-nginx-vhosts' + fi + # Move tmp files to Openvidu printf '\n => Updating files:' @@ -288,6 +311,9 @@ upgrade_ov() { mv "${TMP_FOLDER}/filebeat.yml" "${OPENVIDU_PREVIOUS_FOLDER}/beats/filebeat.yml" || fatal_error "Error while updating 'filebeat.yml'" printf '\n - filebeat.yml' + mv "${TMP_FOLDER}/metricbeat.yml" "${OPENVIDU_PREVIOUS_FOLDER}/beats/metricbeat.yml" || fatal_error "Error while updating 'metricbeat.yml'" + printf '\n - metricbeat.yml' + printf "\n => Deleting 'tmp' folder" rm -rf "${TMP_FOLDER}" || fatal_error "Error deleting 'tmp' folder" @@ -311,16 +337,15 @@ upgrade_ov() { [ ! -z "${OLD_MODE}" ] && sed -i -r "s/Installation Mode:.+/Installation Mode: ${OLD_MODE}/" "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml" # In Aws, update AMI ID - CHECK_AWS=$(curl -s -o /dev/null -w "%{http_code}" http://169.254.169.254) - if [[ ${CHECK_AWS} == "200" ]]; then - AWS_REGION=$(grep -E "AWS_DEFAULT_REGION=.*$" "${OPENVIDU_PREVIOUS_FOLDER}/.env" | cut -d'=' -f2) - [[ -z ${AWS_REGION} ]] && fatal_error "Error while getting AWS_REGION" + AWS_REGION=$(grep -E "AWS_DEFAULT_REGION=.*$" "${OPENVIDU_PREVIOUS_FOLDER}/.env" | cut -d'=' -f2) + if [[ ! -z ${AWS_REGION} ]]; then NEW_AMI_ID=$(curl https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/CF-OpenVidu-Pro-${OPENVIDU_VERSION//v}.yaml --silent | sed -n -e '/KMSAMIMAP:/,/Metadata:/ p' | grep -A 1 ${AWS_REGION} | grep AMI | tr -d " " | cut -d":" -f2) [[ -z ${NEW_AMI_ID} ]] && fatal_error "Error while getting new AWS_IMAGE_ID for Media Nodes" sed -i "s/.*AWS_IMAGE_ID=.*/AWS_IMAGE_ID=${NEW_AMI_ID}/" "${OPENVIDU_PREVIOUS_FOLDER}/.env" || fatal_error "Error while updating new AWS_IMAGE_ID for Media Nodes" fi + # Ready to use printf '\n' diff --git a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/openvidu b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/openvidu index 9f948e61..b4229d5a 100755 --- a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/openvidu +++ b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/openvidu @@ -120,7 +120,7 @@ generate_report() { printf '\n' printf '\n' - cat < "${OV_FOLDER}/.env" | sed -r -e "s/OPENVIDU_SECRET=.+/OPENVIDU_SECRET=****/" -e "s/OPENVIDU_PRO_LICENSE=.+/OPENVIDU_PRO_LICENSE=****/" -e "s/KIBANA_PASSWORD=.+/KIBANA_PASSWORD=****/" + cat < "${OV_FOLDER}/.env" | sed -r -e "s/OPENVIDU_SECRET=.+/OPENVIDU_SECRET=****/" -e "s/OPENVIDU_PRO_LICENSE=.+/OPENVIDU_PRO_LICENSE=****/" -e "s/ELASTICSEARCH_PASSWORD=.+/ELASTICSEARCH_PASSWORD=****/" printf '\n' printf '\n ========= docker-compose.yml ==========' @@ -183,6 +183,42 @@ generate_report() { printf "\n" } +is_external_url() { + local URL=$1 + if [[ -z "$URL" ]]; then + return 1 + fi + if [[ "${URL}" == *"localhost"* ]] || [[ "${URL}" == *"127.0.0.1"* ]] || [[ "${URL}" == *"::1"* ]]; then + return 1 + else + return 0 + fi +} + +start_openvidu() { + local RUN_LOCAL_ES + local RUN_LOCAL_KIBANA + local CONFIGURED_ELASTICSEARCH_HOST + local CONFIGURED_KIBANA_HOST + CONFIGURED_ELASTICSEARCH_HOST=$(grep -v '^#' .env | grep OPENVIDU_PRO_ELASTICSEARCH_HOST | cut -d '=' -f2) + CONFIGURED_KIBANA_HOST=$(grep -v '^#' .env | grep OPENVIDU_PRO_KIBANA_HOST | cut -d '=' -f2) + RUN_LOCAL_ES=true + RUN_LOCAL_KIBANA=true + if is_external_url "${CONFIGURED_ELASTICSEARCH_HOST}"; then + printf "Configured external elasticsearch: %s" "${CONFIGURED_ELASTICSEARCH_HOST}" + printf "\n" + RUN_LOCAL_ES=false + fi + if is_external_url "${CONFIGURED_KIBANA_HOST}"; then + printf "Configured external kibana: %s" "${CONFIGURED_KIBANA_HOST}" + printf "\n" + RUN_LOCAL_KIBANA=false + fi + docker-compose up -d \ + $(if [ "${RUN_LOCAL_ES}" == "false" ]; then echo '--scale elasticsearch=0'; fi) \ + $(if [ "${RUN_LOCAL_KIBANA}" == "false" ]; then echo '--scale kibana=0'; fi) +} + usage() { printf "Usage: \n\t openvidu [command]" printf "\n\nAvailable Commands:" @@ -198,11 +234,15 @@ usage() { printf "\n" } +[[ -z "${FOLLOW_OPENVIDU_LOGS}" ]] && FOLLOW_OPENVIDU_LOGS=true + case $1 in start) - docker-compose up -d - docker-compose logs -f openvidu-server + start_openvidu + if [[ "${FOLLOW_OPENVIDU_LOGS}" == "true" ]]; then + docker-compose logs -f openvidu-server + fi ;; stop) @@ -211,8 +251,10 @@ case $1 in restart) docker-compose down - docker-compose up -d - docker-compose logs -f openvidu-server + start_openvidu + if [[ "${FOLLOW_OPENVIDU_LOGS}" == "true" ]]; then + docker-compose logs -f openvidu-server + fi ;; logs) diff --git a/openvidu-server/deployments/pro/taskcat/taskcat.yml b/openvidu-server/deployments/pro/taskcat/taskcat.yml index 288b8305..fed9a306 100644 --- a/openvidu-server/deployments/pro/taskcat/taskcat.yml +++ b/openvidu-server/deployments/pro/taskcat/taskcat.yml @@ -1,6 +1,6 @@ --- global: - owner: openvidu@gmail.com + owner: info@openvidu.io qsname: openvidu-pro-clustering regions: - us-east-1 diff --git a/openvidu-server/docker/openvidu-coturn/Dockerfile b/openvidu-server/docker/openvidu-coturn/Dockerfile index eac2ee94..bcaf1acd 100644 --- a/openvidu-server/docker/openvidu-coturn/Dockerfile +++ b/openvidu-server/docker/openvidu-coturn/Dockerfile @@ -1,7 +1,7 @@ FROM ubuntu:16.04 RUN apt-get update \ - && apt-get install -y coturn curl + && apt-get install -y coturn curl dnsutils COPY ./configuration-files.sh /tmp/ COPY ./entrypoint.sh /usr/local/bin @@ -11,4 +11,4 @@ RUN chmod +x /tmp/configuration-files.sh \ && chmod +x /usr/local/bin/entrypoint.sh \ && chmod +x /usr/local/bin/discover_my_public_ip.sh -CMD /usr/local/bin/entrypoint.sh \ No newline at end of file +ENTRYPOINT [ "/usr/local/bin/entrypoint.sh" ] \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-coturn/README.md b/openvidu-server/docker/openvidu-coturn/README.md index df542138..4b1ea95e 100644 --- a/openvidu-server/docker/openvidu-coturn/README.md +++ b/openvidu-server/docker/openvidu-coturn/README.md @@ -28,9 +28,15 @@ docker run --rm --name some-redis -d -p 6379:6379 redis Get the ip of the container and after that, run coturn, you can use url as ip too, in this example I am running coturn with nip.io: ``` -docker run -it -e REDIS_IP=172.17.0.2 -e DB_NAME=0 -e DB_PASSWORD=turn -e MIN_PORT=40000 -e MAX_PORT=65535 -e TURN_LISTEN_PORT=3478 --network=host coturn-openvidu +docker run -it -e REDIS_IP=172.17.0.2 -e DB_NAME=0 -e DB_PASSWORD=turn -e MIN_PORT=40000 -e MAX_PORT=65535 -e TURN_PUBLIC_IP=auto -e TURN_LISTEN_PORT=3478 --network=host openvidu/openvidu-coturn ``` +## Execute turn locally with fixed username and password +``` +docker run -it -e TURN_PUBLIC_IP=auto -e TURN_USERNAME_PASSWORD=: -e MIN_PORT=40000 -e MAX_PORT=65535 -e TURN_LISTEN_PORT=3478 --network=host openvidu/openvidu-coturn +``` + + # Kubernetes TODO diff --git a/openvidu-server/docker/openvidu-coturn/configuration-files.sh b/openvidu-server/docker/openvidu-coturn/configuration-files.sh index 3d94420c..4a88e4fb 100644 --- a/openvidu-server/docker/openvidu-coturn/configuration-files.sh +++ b/openvidu-server/docker/openvidu-coturn/configuration-files.sh @@ -7,15 +7,25 @@ EOF # Turn server configuration cat>/etc/turnserver.conf<> /etc/turnserver.conf +fi + +if [[ ! -z "${REDIS_IP}" ]] && [[ ! -z "${DB_NAME}" ]] && [[ ! -z "${DB_PASSWORD}" ]]; then + echo "redis-userdb=\"ip=${REDIS_IP} dbname=${DB_NAME} password=${DB_PASSWORD} connect_timeout=30\"" >> /etc/turnserver.conf +fi + +if [[ ! -z "${TURN_USERNAME_PASSWORD}" ]]; then + echo "user=${TURN_USERNAME_PASSWORD}" >> /etc/turnserver.conf +fi \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-coturn/entrypoint.sh b/openvidu-server/docker/openvidu-coturn/entrypoint.sh index 8a87a719..25863902 100644 --- a/openvidu-server/docker/openvidu-coturn/entrypoint.sh +++ b/openvidu-server/docker/openvidu-coturn/entrypoint.sh @@ -5,19 +5,15 @@ DEBUG=${DEBUG:-false} [ "$DEBUG" == "true" ] && set -x #Check parameters -[[ ! -z "${TURN_PUBLIC_IP}" ]] || export TURN_PUBLIC_IP=$(/usr/local/bin/discover_my_public_ip.sh) +[[ "${TURN_PUBLIC_IP}" == "auto-ipv4" ]] && export TURN_PUBLIC_IP=$(/usr/local/bin/discover_my_public_ip.sh) +[[ "${TURN_PUBLIC_IP}" == "auto-ipv6" ]] && export TURN_PUBLIC_IP=$(/usr/local/bin/discover_my_public_ip.sh --ipv6) +[[ -z "${ENABLE_COTURN_LOGS}" ]] && export ENABLE_COTURN_LOGS=true -echo "TURN public IP: ${TURN_PUBLIC_IP}" +echo "TURN public IP: ${TURN_PUBLIC_IP:-"empty"}" [[ ! -z "${TURN_LISTEN_PORT}" ]] && echo "TURN listening port: ${TURN_LISTEN_PORT}" || { echo "TURN_LISTEN_PORT environment variable is not defined"; exit 1; } -[[ ! -z "${REDIS_IP}" ]] && echo "REDIS IP: ${REDIS_IP}" || { echo "REDIS_IP environment variable is not defined"; exit 1; } - -[[ ! -z "${DB_NAME}" ]] || { echo "DB_NAME environment variable is not defined"; exit 1; } - -[[ ! -z "${DB_PASSWORD}" ]] || { echo "DB_PASSWORD environment variable is not defined"; exit 1; } - [[ ! -z "${MIN_PORT}" ]] && echo "Defined min port coturn: ${MIN_PORT}" || echo "Min port coturn: 40000" [[ ! -z "${MAX_PORT}" ]] && echo "Defined max port coturn: ${MAX_PORT}" || echo "Max port coturn: 65535" @@ -28,19 +24,8 @@ source /tmp/configuration-files.sh # Remove temp file with configuration parameters rm /tmp/configuration-files.sh -# Save coturn External IP for other services - - -# Execute turn daemon -/usr/bin/turnserver -c /etc/turnserver.conf -v & - - -MAX_SECONDS=30 -# K8s only show turn server log in this way -while [ -z $(ls /var/log/ | grep turn_) ] && [ $SECONDS -lt $MAX_SECONDS ] -do - echo "Waiting turnserver to be running" - sleep 2 -done - -tail -f /var/log/turn_*.log +if [[ "${ENABLE_COTURN_LOGS}" == "true" ]]; then + /usr/bin/turnserver -c /etc/turnserver.conf -v --log-file /dev/null +else + /usr/bin/turnserver -c /etc/turnserver.conf -v --log-file /dev/null --no-stdout-log +fi diff --git a/openvidu-server/docker/openvidu-proxy/Dockerfile b/openvidu-server/docker/openvidu-proxy/Dockerfile index b29fd066..9272c08d 100644 --- a/openvidu-server/docker/openvidu-proxy/Dockerfile +++ b/openvidu-server/docker/openvidu-proxy/Dockerfile @@ -5,7 +5,9 @@ RUN apk update && \ apk add bash \ certbot \ openssl \ - apache2-utils && \ + apache2-utils \ + bind-tools \ + perl pcre grep && \ rm -rf /var/cache/apk/* # Default nginx conf @@ -14,9 +16,16 @@ COPY ./default_nginx_conf /default_nginx_conf # Entrypoint and discover public ip scripts COPY ./discover_my_public_ip.sh /usr/local/bin + +# Copy nginx.conf +COPY ./nginx.conf /etc/nginx/nginx.conf + +# Entrypoint COPY ./entrypoint.sh /usr/local/bin RUN mkdir -p /var/www/certbot && \ + mkdir -p /etc/nginx/vhost.d/ && \ + mkdir -p /custom-nginx && \ chmod +x /usr/local/bin/entrypoint.sh && \ chmod +x /usr/local/bin/discover_my_public_ip.sh diff --git a/openvidu-server/docker/openvidu-proxy/default.conf b/openvidu-server/docker/openvidu-proxy/default.conf index 8a95ee30..7378f5a4 100644 --- a/openvidu-server/docker/openvidu-proxy/default.conf +++ b/openvidu-server/docker/openvidu-proxy/default.conf @@ -1,5 +1,6 @@ server { listen {http_port}; + listen [::]:{http_port}; location /.well-known/acme-challenge/ { root /var/www/certbot; diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/ce/default-app-without-demos.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/ce/default-app-without-demos.conf deleted file mode 100644 index cdff24fe..00000000 --- a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/ce/default-app-without-demos.conf +++ /dev/null @@ -1,103 +0,0 @@ -# Your app -#upstream yourapp { -# server localhost:5442; -#} - -upstream openviduserver { - server localhost:5443; -} - -server { - listen {https_port} ssl; - server_name {domain_name}; - - ssl_certificate /etc/letsencrypt/live/{domain_name}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/{domain_name}/privkey.pem; - ssl_trusted_certificate /etc/letsencrypt/live/{domain_name}/fullchain.pem; - - ssl_session_cache shared:SSL:50m; - ssl_session_timeout 5m; - ssl_stapling on; - ssl_stapling_verify on; - - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; - - ssl_prefer_server_ciphers on; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Proto https; - proxy_headers_hash_bucket_size 512; - proxy_redirect off; - - # Websockets - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - # Welcome - root /var/www/html; - - # Your app - #location / { - # proxy_pass http://yourapp; # Openvidu call by default - #} - - # Openvidu Admin Panel - location /dashboard { - {rules_access_dashboard} - deny all; - proxy_pass http://openviduserver; - } - - # Openvidu Server - location /layouts/custom { - rewrite ^/layouts/custom/(.*)$ /custom-layout/$1 break; - root /opt/openvidu; - } - - location /recordings { - proxy_pass http://openviduserver; - } - - location /api { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - location ~ /openvidu$ { - proxy_pass http://openviduserver; - } - - location /info { - {rules_access_dashboard} - deny all; - proxy_pass http://openviduserver; - } - - location /config { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - location /accept-certificate { - proxy_pass http://openviduserver; - } - - location /cdr { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - # letsencrypt - location /.well-known/acme-challenge { - root /var/www/certbot; - try_files $uri $uri/ =404; - } -} diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/ce/default-app.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/ce/default-app.conf deleted file mode 100644 index 0fcd2142..00000000 --- a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/ce/default-app.conf +++ /dev/null @@ -1,103 +0,0 @@ -# Openvidu call -upstream yourapp { - server localhost:5442; -} - -upstream openviduserver { - server localhost:5443; -} - -server { - listen {https_port} ssl; - server_name {domain_name}; - - ssl_certificate /etc/letsencrypt/live/{domain_name}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/{domain_name}/privkey.pem; - ssl_trusted_certificate /etc/letsencrypt/live/{domain_name}/fullchain.pem; - - ssl_session_cache shared:SSL:50m; - ssl_session_timeout 5m; - ssl_stapling on; - ssl_stapling_verify on; - - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; - - ssl_prefer_server_ciphers on; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Proto https; - proxy_headers_hash_bucket_size 512; - proxy_redirect off; - - # Websockets - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - # Welcome - #root /var/www/html; - - # Your app - location / { - proxy_pass http://yourapp; # Openvidu call by default - } - - # Openvidu Admin Panel - location /dashboard { - {rules_access_dashboard} - deny all; - proxy_pass http://openviduserver; - } - - # Openvidu Server - location /layouts/custom { - rewrite ^/layouts/custom/(.*)$ /custom-layout/$1 break; - root /opt/openvidu; - } - - location /recordings { - proxy_pass http://openviduserver; - } - - location /api { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - location ~ /openvidu$ { - proxy_pass http://openviduserver; - } - - location /info { - {rules_access_dashboard} - deny all; - proxy_pass http://openviduserver; - } - - location /config { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - location /accept-certificate { - proxy_pass http://openviduserver; - } - - location /cdr { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - # letsencrypt - location /.well-known/acme-challenge { - root /var/www/certbot; - try_files $uri $uri/ =404; - } -} diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/ce/default.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/ce/default.conf index 6afc2bbd..60769f42 100644 --- a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/ce/default.conf +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/ce/default.conf @@ -1,5 +1,12 @@ +{app_upstream} + +upstream openviduserver { + server localhost:5443; +} + server { listen {http_port}; + listen [::]:{http_port}; server_name {domain_name}; # Redirect to https @@ -11,4 +18,39 @@ server { location /.well-known/acme-challenge/ { root /var/www/certbot; } + + {nginx_status} +} + +{redirect_www} + +{redirect_www_ssl} + +server { + listen {https_port} ssl; + listen [::]:{https_port} ssl; + server_name {domain_name}; + + {ssl_config} + + {proxy_config} + + {app_config} + + ######################## + # OpenVidu Locations # + ######################## + {common_api_ce} + + {deprecated_api_ce} + + {new_api_ce} + + ################################# + # LetsEncrypt # + ################################# + location /.well-known/acme-challenge { + root /var/www/certbot; + try_files $uri $uri/ =404; + } } diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/app_config.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/app_config.conf new file mode 100644 index 00000000..2dd7bcee --- /dev/null +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/app_config.conf @@ -0,0 +1,4 @@ + # Your App + location / { + proxy_pass http://yourapp; # Openvidu call by default + } \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/app_config_default.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/app_config_default.conf new file mode 100644 index 00000000..82f76720 --- /dev/null +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/app_config_default.conf @@ -0,0 +1,2 @@ + # Welcome + root /var/www/html; \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/app_upstream.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/app_upstream.conf new file mode 100644 index 00000000..f4048ec8 --- /dev/null +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/app_upstream.conf @@ -0,0 +1,4 @@ +# Your App +upstream yourapp { + server localhost:5442; +} \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/ce/common_api_ce.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/ce/common_api_ce.conf new file mode 100644 index 00000000..1ac0df12 --- /dev/null +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/ce/common_api_ce.conf @@ -0,0 +1,14 @@ + ################################# + # Common rules # + ################################# + # Dashboard rule + location /dashboard { + {rules_access_dashboard} + deny all; + proxy_pass http://openviduserver; + } + + # Websocket rule + location ~ /openvidu$ { + proxy_pass http://openviduserver; + } \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/ce/deprecated_api_ce.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/ce/deprecated_api_ce.conf new file mode 100644 index 00000000..a572356d --- /dev/null +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/ce/deprecated_api_ce.conf @@ -0,0 +1,40 @@ + ################################# + # Deprecated API # + ################################# + # Openvidu Server + location /layouts/custom { + rewrite ^/layouts/custom/(.*)$ /custom-layout/$1 break; + root /opt/openvidu; + } + + location /recordings { + proxy_pass http://openviduserver; + } + + location /api { + {rules_acess_api} + deny all; + proxy_pass http://openviduserver; + } + + location /info { + {rules_access_dashboard} + deny all; + proxy_pass http://openviduserver; + } + + location /config { + {rules_acess_api} + deny all; + proxy_pass http://openviduserver; + } + + location /accept-certificate { + proxy_pass http://openviduserver; + } + + location /cdr { + {rules_acess_api} + deny all; + proxy_pass http://openviduserver; + } diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/ce/new_api_ce.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/ce/new_api_ce.conf new file mode 100644 index 00000000..4b9d940b --- /dev/null +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/ce/new_api_ce.conf @@ -0,0 +1,33 @@ + ################################# + # New API # + ################################# + location /openvidu/layouts { + rewrite ^/openvidu/layouts/(.*)$ /custom-layout/$1 break; + root /opt/openvidu; + } + + location /openvidu/recordings { + proxy_pass http://openviduserver; + } + + location /openvidu/api { + {rules_acess_api} + deny all; + proxy_pass http://openviduserver; + } + + location /openvidu/info { + {rules_access_dashboard} + deny all; + proxy_pass http://openviduserver; + } + + location /openvidu/accept-certificate { + proxy_pass http://openviduserver; + } + + location /openvidu/cdr { + {rules_acess_api} + deny all; + proxy_pass http://openviduserver; + } \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/ce/redirect_www.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/ce/redirect_www.conf new file mode 100644 index 00000000..bef132f8 --- /dev/null +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/ce/redirect_www.conf @@ -0,0 +1,15 @@ +server { + listen {http_port}; + listen [::]:{http_port}; + server_name www.{domain_name}; + + # Redirect to https + location / { + rewrite ^(.*) https://{domain_name}:{https_port}$1 permanent; + } + + # letsencrypt + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } +} \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/nginx_status.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/nginx_status.conf new file mode 100644 index 00000000..be33d323 --- /dev/null +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/nginx_status.conf @@ -0,0 +1,5 @@ + location /nginx_status { + stub_status; + allow 127.0.0.1; #only allow requests from localhost + deny all; #deny all other hosts + } \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/pro/common_api_pro.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/pro/common_api_pro.conf new file mode 100644 index 00000000..6df8b233 --- /dev/null +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/pro/common_api_pro.conf @@ -0,0 +1,27 @@ + ################################# + # Common rules # + ################################# + location /dashboard { + {rules_access_dashboard} + deny all; + rewrite ^/dashboard/(.*)$ /$1 break; + proxy_pass http://openviduserver/; + } + + location /inspector { + {rules_access_dashboard} + deny all; + proxy_pass http://openviduserver; + } + + location ~ /openvidu$ { + proxy_pass http://openviduserver; + } + + location /kibana { + {rules_access_dashboard} + deny all; + + rewrite ^/kibana/(.*)$ /$1 break; + proxy_pass http://kibana/; + } \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/pro/deprecated_api_pro.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/pro/deprecated_api_pro.conf new file mode 100644 index 00000000..1c8a45eb --- /dev/null +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/pro/deprecated_api_pro.conf @@ -0,0 +1,59 @@ + ################################# + # Deprecated API # + ################################# + # Openvidu Server + location /layouts/custom { + rewrite ^/layouts/custom/(.*)$ /custom-layout/$1 break; + root /opt/openvidu; + } + + location /recordings { + proxy_pass http://openviduserver; + } + + location /api { + {rules_acess_api} + deny all; + proxy_pass http://openviduserver; + } + + location /info { + {rules_access_dashboard} + deny all; + proxy_pass http://openviduserver; + } + + location /config { + {rules_acess_api} + deny all; + proxy_pass http://openviduserver; + } + + location /accept-certificate { + proxy_pass http://openviduserver; + } + + location /cdr { + {rules_acess_api} + deny all; + proxy_pass http://openviduserver; + } + + # Openvidu Server Pro + location /pro { + {rules_acess_api} + deny all; + proxy_pass http://openviduserver; + } + + location /api-login { + {rules_acess_api} + deny all; + proxy_pass http://openviduserver; + } + + location /elasticsearch { + {rules_acess_api} + deny all; + proxy_pass http://openviduserver; + } diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/pro/new_api_pro.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/pro/new_api_pro.conf new file mode 100644 index 00000000..ea426b5c --- /dev/null +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/pro/new_api_pro.conf @@ -0,0 +1,46 @@ + ################################# + # New API # + ################################# + # OpenVidu Server + location /openvidu/layouts { + rewrite ^/openvidu/layouts/(.*)$ /custom-layout/$1 break; + root /opt/openvidu; + } + + location /openvidu/recordings { + proxy_pass http://openviduserver; + } + + location /openvidu/api { + {rules_acess_api} + deny all; + proxy_pass http://openviduserver; + } + + location /openvidu/info { + {rules_access_dashboard} + deny all; + proxy_pass http://openviduserver; + } + + location /openvidu/accept-certificate { + proxy_pass http://openviduserver; + } + + location /openvidu/cdr { + {rules_acess_api} + deny all; + proxy_pass http://openviduserver; + } + # OpenVidu Server PRO + location /openvidu/elk { + {rules_acess_api} + deny all; + proxy_pass http://openviduserver; + } + + location /openvidu/inspector-api { + {rules_acess_api} + deny all; + proxy_pass http://openviduserver; + } \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/pro/redirect_www.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/pro/redirect_www.conf new file mode 100644 index 00000000..bd17a798 --- /dev/null +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/pro/redirect_www.conf @@ -0,0 +1,3 @@ + if ($host = www.{domain_name}) { + rewrite ^(.*) https://{domain_name}:{https_port}$1 permanent; + } \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/proxy_config.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/proxy_config.conf new file mode 100644 index 00000000..9c3add1c --- /dev/null +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/proxy_config.conf @@ -0,0 +1,13 @@ + # Proxy + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto https; + proxy_headers_hash_bucket_size 512; + proxy_redirect off; + + # Websockets + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/redirect_www_ssl.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/redirect_www_ssl.conf new file mode 100644 index 00000000..fd1c7803 --- /dev/null +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/redirect_www_ssl.conf @@ -0,0 +1,18 @@ +server { + listen 443 ssl; + server_name www.{domain_name}; + + {ssl_config} + + {proxy_config} + + # Redirect to non-www + location / { + rewrite ^(.*) https://{domain_name}:{https_port}$1 permanent; + } + + # letsencrypt + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } +} \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/ssl_config.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/ssl_config.conf new file mode 100644 index 00000000..77ba1dc0 --- /dev/null +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/global/ssl_config.conf @@ -0,0 +1,16 @@ + # SSL Config + ssl_certificate /etc/letsencrypt/live/{domain_name}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{domain_name}/privkey.pem; + ssl_trusted_certificate /etc/letsencrypt/live/{domain_name}/fullchain.pem; + + ssl_session_cache shared:SSL:50m; + ssl_session_timeout 5m; + ssl_stapling on; + ssl_stapling_verify on; + + ssl_protocols {ssl_protocols}; + ssl_ciphers "{ssl_ciphers}"; + + ssl_prefer_server_ciphers on; + + add_header Strict-Transport-Security "{add_header_hsts}" always; \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/pro/default-app-without-demos.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/pro/default-app-without-demos.conf deleted file mode 100644 index 1473ac20..00000000 --- a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/pro/default-app-without-demos.conf +++ /dev/null @@ -1,165 +0,0 @@ -add_header X-Frame-Options SAMEORIGIN; -add_header X-Content-Type-Options nosniff; -add_header X-XSS-Protection "1; mode=block"; - -upstream kibana { - server localhost:5601; -} - -upstream openviduserver { - server localhost:5443; -} - -server { - # Redirect to https - if ($host = {domain_name}) { - rewrite ^(.*) https://{domain_name}:{https_port}$1 permanent; - } # managed by Certbot - - listen {http_port} default_server; - server_name {domain_name}; - - # letsencrypt - location /.well-known/acme-challenge { - root /var/www/certbot; - try_files $uri $uri/ =404; - } - - # Kibana panel - location /kibana { - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - - rewrite ^/kibana/(.*)$ /$1 break; - proxy_pass http://kibana/; - } -} - -server { - listen {https_port} ssl default deferred; - server_name {domain_name}; - - ssl_certificate /etc/letsencrypt/live/{domain_name}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/{domain_name}/privkey.pem; - ssl_trusted_certificate /etc/letsencrypt/live/{domain_name}/fullchain.pem; - - ssl_session_cache shared:SSL:50m; - ssl_session_timeout 5m; - ssl_stapling on; - ssl_stapling_verify on; - - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; - - ssl_prefer_server_ciphers on; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Proto https; - proxy_headers_hash_bucket_size 512; - proxy_redirect off; - - # Websockets - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - # Welcome - root /var/www/html; - - # Openvidu Admin Panel - location /dashboard { - {rules_access_dashboard} - deny all; - rewrite ^/dashboard/(.*)$ /$1 break; - proxy_pass http://openviduserver/; - } - - location /inspector { - {rules_access_dashboard} - deny all; - proxy_pass http://openviduserver; - } - - location /kibana { - {rules_access_dashboard} - deny all; - auth_basic "Openvidu Monitoring"; - auth_basic_user_file /etc/nginx/kibana.htpasswd; - - rewrite ^/kibana/(.*)$ /$1 break; - proxy_pass http://kibana/; - } - - # Openvidu Server - location /layouts/custom { - rewrite ^/layouts/custom/(.*)$ /custom-layout/$1 break; - root /opt/openvidu; - } - - location /recordings { - proxy_pass http://openviduserver; - } - - location /api { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - location ~ /openvidu$ { - proxy_pass http://openviduserver; - } - - location /info { - {rules_access_dashboard} - deny all; - proxy_pass http://openviduserver; - } - - location /config { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - location /accept-certificate { - proxy_pass http://openviduserver; - } - - location /cdr { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - # Openvidu Server Pro - location /pro { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - location /api-login { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - location /elasticsearch { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - # letsencrypt - location /.well-known/acme-challenge { - root /var/www/certbot; - try_files $uri $uri/ =404; - } -} diff --git a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/pro/default.conf b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/pro/default.conf index d5ada4eb..f88768cd 100644 --- a/openvidu-server/docker/openvidu-proxy/default_nginx_conf/pro/default.conf +++ b/openvidu-server/docker/openvidu-proxy/default_nginx_conf/pro/default.conf @@ -2,10 +2,7 @@ add_header X-Frame-Options SAMEORIGIN; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; -# Openvidu call -upstream yourapp { - server localhost:5442; -} +{app_upstream} upstream kibana { server localhost:5601; @@ -15,13 +12,18 @@ upstream openviduserver { server localhost:5443; } +{redirect_www_ssl} + server { # Redirect to https if ($host = {domain_name}) { rewrite ^(.*) https://{domain_name}:{https_port}$1 permanent; } # managed by Certbot + + {redirect_www} listen {http_port} default_server; + listen [::]:{http_port} default_server; server_name {domain_name}; # letsencrypt @@ -41,133 +43,34 @@ server { rewrite ^/kibana/(.*)$ /$1 break; proxy_pass http://kibana/; } + + {nginx_status} } server { listen {https_port} ssl default deferred; + listen [::]:{https_port} ssl default deferred; server_name {domain_name}; - ssl_certificate /etc/letsencrypt/live/{domain_name}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/{domain_name}/privkey.pem; - ssl_trusted_certificate /etc/letsencrypt/live/{domain_name}/fullchain.pem; + {ssl_config} - ssl_session_cache shared:SSL:50m; - ssl_session_timeout 5m; - ssl_stapling on; - ssl_stapling_verify on; + {proxy_config} - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; + {app_config} - ssl_prefer_server_ciphers on; + ######################## + # OpenVidu Locations # + ######################## + {common_api_pro} - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Proto https; - proxy_headers_hash_bucket_size 512; - proxy_redirect off; + {deprecated_api_pro} - # Websockets - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; + {new_api_pro} - # Welcome - #root /var/www/html; + ################################# + # LetsEncrypt # + ################################# - # Your app - location / { - proxy_pass http://yourapp; # Openvidu call by default - } - - # Openvidu Admin Panel - location /dashboard { - {rules_access_dashboard} - deny all; - rewrite ^/dashboard/(.*)$ /$1 break; - proxy_pass http://openviduserver/; - } - - location /inspector { - {rules_access_dashboard} - deny all; - proxy_pass http://openviduserver; - } - - location /kibana { - {rules_access_dashboard} - deny all; - auth_basic "Openvidu Monitoring"; - auth_basic_user_file /etc/nginx/kibana.htpasswd; - - rewrite ^/kibana/(.*)$ /$1 break; - proxy_pass http://kibana/; - } - - # Openvidu Server - location /layouts/custom { - rewrite ^/layouts/custom/(.*)$ /custom-layout/$1 break; - root /opt/openvidu; - } - - location /recordings { - proxy_pass http://openviduserver; - } - - location /api { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - location ~ /openvidu$ { - proxy_pass http://openviduserver; - } - - location /info { - {rules_access_dashboard} - deny all; - proxy_pass http://openviduserver; - } - - location /config { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - location /accept-certificate { - proxy_pass http://openviduserver; - } - - location /cdr { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - # Openvidu Server Pro - location /pro { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - location /api-login { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - location /elasticsearch { - {rules_acess_api} - deny all; - proxy_pass http://openviduserver; - } - - # letsencrypt location /.well-known/acme-challenge { root /var/www/certbot; try_files $uri $uri/ =404; diff --git a/openvidu-server/docker/openvidu-proxy/entrypoint.sh b/openvidu-server/docker/openvidu-proxy/entrypoint.sh index 98c8f2f0..659bc90e 100755 --- a/openvidu-server/docker/openvidu-proxy/entrypoint.sh +++ b/openvidu-server/docker/openvidu-proxy/entrypoint.sh @@ -28,22 +28,25 @@ if [[ "${CERTIFICATE_TYPE}" == "letsencrypt" && \ fi # Global variables -CERTIFICATES_FOLDER=/etc/letsencrypt/live -CERTIFICATES_CONF="${CERTIFICATES_FOLDER}/certificates.conf" +CERTIFICATES_FOLDER=/etc/letsencrypt +CERTIFICATES_LIVE_FOLDER="${CERTIFICATES_FOLDER}/live" +CERTIFICATES_CONF="${CERTIFICATES_LIVE_FOLDER}/certificates.conf" -[ ! -d "${CERTIFICATES_FOLDER}" ] && mkdir -p "${CERTIFICATES_FOLDER}" +[ ! -d "${CERTIFICATES_LIVE_FOLDER}" ] && mkdir -p "${CERTIFICATES_LIVE_FOLDER}" [ ! -f "${CERTIFICATES_CONF}" ] && touch "${CERTIFICATES_CONF}" [ -z "${PROXY_HTTP_PORT}" ] && export PROXY_HTTP_PORT=80 [ -z "${PROXY_HTTPS_PORT}" ] && export PROXY_HTTPS_PORT=443 +[ -z "${PROXY_HTTPS_PROTOCOLS}" ] && export PROXY_HTTPS_PROTOCOLS='TLSv1 TLSv1.1 TLSv1.2 TLSv1.3' +[ -z "${PROXY_HTTPS_CIPHERS}" ] && export PROXY_HTTPS_CIPHERS='ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4' [ -z "${WITH_APP}" ] && export WITH_APP=true +[ -z "${SUPPORT_DEPRECATED_API}" ] && export SUPPORT_DEPRECATED_API=true +[ -z "${REDIRECT_WWW}" ] && export REDIRECT_WWW=false [ -z "${PROXY_MODE}" ] && export PROXY_MODE=CE +[ -z "${WORKER_CONNECTIONS}" ] && export WORKER_CONNECTIONS=10240 +[ -z "${PUBLIC_IP}" ] && export PUBLIC_IP=auto-ipv4 [ -z "${ALLOWED_ACCESS_TO_DASHBOARD}" ] && export ALLOWED_ACCESS_TO_DASHBOARD=all [ -z "${ALLOWED_ACCESS_TO_RESTAPI}" ] && export ALLOWED_ACCESS_TO_RESTAPI=all -# Start with default certbot conf -sed -i "s/{http_port}/${PROXY_HTTP_PORT}/" /etc/nginx/conf.d/default.conf -nginx -g "daemon on;" - # Show input enviroment variables printf "\n =======================================" printf "\n = INPUT VARIABLES =" @@ -53,8 +56,11 @@ printf "\n" printf "\n Config NGINX:" printf "\n - Http Port: %s" "${PROXY_HTTP_PORT}" printf "\n - Https Port: %s" "${PROXY_HTTPS_PORT}" +printf "\n - Worker Connections: %s" "${WORKER_CONNECTIONS}" printf "\n - Allowed Access in Openvidu Dashboard: %s" "${ALLOWED_ACCESS_TO_DASHBOARD}" printf "\n - Allowed Access in Openvidu API: %s" "${ALLOWED_ACCESS_TO_RESTAPI}" +printf "\n - Support deprecated API: %s" "${SUPPORT_DEPRECATED_API}" +printf "\n - Redirect www to non-www: %s" "${REDIRECT_WWW}" printf "\n" printf "\n Config Openvidu Application:" printf "\n - Domain name: %s" "${DOMAIN_OR_PUBLIC_IP}" @@ -69,42 +75,54 @@ printf "\n = CONFIGURATION NGINX =" printf "\n =======================================" printf "\n" -printf "\n Configure %s domain..." "${DOMAIN_OR_PUBLIC_IP}" -CERTIFICATED_OLD_CONFIG=$(grep "${DOMAIN_OR_PUBLIC_IP}" "${CERTIFICATES_CONF}" | cut -f2 -d$'\t') +# Override worker connections +sed -i "s/{worker_connections}/${WORKER_CONNECTIONS}/g" /etc/nginx/nginx.conf -printf "\n - New configuration: %s" "${CERTIFICATE_TYPE}" +printf "\n Configure %s domain..." "${DOMAIN_OR_PUBLIC_IP}" +OLD_DOMAIN_OR_PUBLIC_IP=$(head -n 1 "${CERTIFICATES_CONF}" | cut -f1 -d$'\t') +CERTIFICATED_OLD_CONFIG=$(head -n 1 "${CERTIFICATES_CONF}" | cut -f2 -d$'\t') + +printf "\n - New configuration: %s %s" "${CERTIFICATE_TYPE}" "${DOMAIN_OR_PUBLIC_IP}" if [ -z "${CERTIFICATED_OLD_CONFIG}" ]; then - printf "\n - Old configuration: none" + printf "\n - Old configuration: none\n" else - printf "\n - Old configuration: %s" "${CERTIFICATED_OLD_CONFIG}" + printf "\n - Old configuration: %s %s\n" "${CERTIFICATED_OLD_CONFIG}" "${OLD_DOMAIN_OR_PUBLIC_IP}" + + if [ "${CERTIFICATED_OLD_CONFIG}" != "${CERTIFICATE_TYPE}" ] || \ + [ "${OLD_DOMAIN_OR_PUBLIC_IP}" != "${DOMAIN_OR_PUBLIC_IP}" ]; then - if [ "${CERTIFICATED_OLD_CONFIG}" != "${CERTIFICATE_TYPE}" ]; then printf "\n - Restarting configuration... Removing old certificated..." - - rm -rf "${CERTIFICATES_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/"* + # Remove certificate folder safely + find "${CERTIFICATES_FOLDER:?}" -mindepth 1 -delete + # Recreate certificates folder + mkdir -p "${CERTIFICATES_LIVE_FOLDER}" + touch "${CERTIFICATES_CONF}" fi fi # Save actual conf -sed -i "/${DOMAIN_OR_PUBLIC_IP}/d" "${CERTIFICATES_CONF}" -echo -e "${DOMAIN_OR_PUBLIC_IP}\t${CERTIFICATE_TYPE}" >> "${CERTIFICATES_CONF}" +echo -e "${DOMAIN_OR_PUBLIC_IP}\t${CERTIFICATE_TYPE}" > "${CERTIFICATES_CONF}" + +# Start with default certbot conf +sed -i "s/{http_port}/${PROXY_HTTP_PORT}/" /etc/nginx/conf.d/default.conf +nginx -g "daemon on;" case ${CERTIFICATE_TYPE} in "selfsigned") - if [[ ! -f "${CERTIFICATES_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/privkey.pem" && \ - ! -f "${CERTIFICATES_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/fullchain.pem" ]]; then + if [[ ! -f "${CERTIFICATES_LIVE_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/privkey.pem" && \ + ! -f "${CERTIFICATES_LIVE_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/fullchain.pem" ]]; then printf "\n - Generating selfsigned certificate...\n" # Delete and create certificate folder - rm -rf "${CERTIFICATES_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}" | true - mkdir -p "${CERTIFICATES_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}" + rm -rf "${CERTIFICATES_LIVE_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}" | true + mkdir -p "${CERTIFICATES_LIVE_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}" openssl req -new -nodes -x509 \ -subj "/CN=${DOMAIN_OR_PUBLIC_IP}" -days 365 \ - -keyout "${CERTIFICATES_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/privkey.pem" \ - -out "${CERTIFICATES_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/fullchain.pem" \ + -keyout "${CERTIFICATES_LIVE_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/privkey.pem" \ + -out "${CERTIFICATES_LIVE_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/fullchain.pem" \ -extensions v3_ca else printf "\n - Selfsigned certificate already exists, using them..." @@ -112,18 +130,18 @@ case ${CERTIFICATE_TYPE} in ;; "owncert") - if [[ ! -f "${CERTIFICATES_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/privkey.pem" && \ - ! -f "${CERTIFICATES_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/fullchain.pem" ]]; then - printf "\n - Copying owmcert certificate..." + if [[ ! -f "${CERTIFICATES_LIVE_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/privkey.pem" && \ + ! -f "${CERTIFICATES_LIVE_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/fullchain.pem" ]]; then + printf "\n - Copying owncert certificate..." # Delete and create certificate folder - rm -rf "${CERTIFICATES_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}" | true - mkdir -p "${CERTIFICATES_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}" + rm -rf "${CERTIFICATES_LIVE_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}" | true + mkdir -p "${CERTIFICATES_LIVE_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}" - cp /owncert/certificate.key "${CERTIFICATES_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/privkey.pem" - cp /owncert/certificate.cert "${CERTIFICATES_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/fullchain.pem" + cp /owncert/certificate.key "${CERTIFICATES_LIVE_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/privkey.pem" + cp /owncert/certificate.cert "${CERTIFICATES_LIVE_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/fullchain.pem" else - printf "\n - Owmcert certificate already exists, using them..." + printf "\n - Owncert certificate already exists, using them..." fi ;; @@ -132,16 +150,17 @@ case ${CERTIFICATE_TYPE} in /usr/sbin/crond -f & echo '0 */12 * * * certbot renew --post-hook "nginx -s reload" >> /var/log/cron-letsencrypt.log' | crontab - # Auto renew cert - if [[ ! -f "${CERTIFICATES_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/privkey.pem" && \ - ! -f "${CERTIFICATES_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/fullchain.pem" ]]; then + if [[ ! -f "${CERTIFICATES_LIVE_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/privkey.pem" && \ + ! -f "${CERTIFICATES_LIVE_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}/fullchain.pem" ]]; then printf "\n - Requesting LetsEncrypt certificate..." # Delete certificate folder - rm -rf "${CERTIFICATES_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}" | true + rm -rf "${CERTIFICATES_LIVE_FOLDER:?}/${DOMAIN_OR_PUBLIC_IP}" | true certbot certonly -n --webroot -w /var/www/certbot \ -m "${LETSENCRYPT_EMAIL}" \ - --agree-tos -d "${DOMAIN_OR_PUBLIC_IP}" + --agree-tos -d "${DOMAIN_OR_PUBLIC_IP}" \ + `if [[ "${REDIRECT_WWW}" == "true" ]]; then echo "-d www.${DOMAIN_OR_PUBLIC_IP}" ; fi` else printf "\n - LetsEncrypt certificate already exists, using them..." fi @@ -153,27 +172,14 @@ chmod -R 777 /etc/letsencrypt # Use certificates in folder '/default_nginx_conf' if [ "${PROXY_MODE}" == "CE" ]; then - if [ "${WITH_APP}" == "true" ]; then - mv /default_nginx_conf/ce/default-app.conf /default_nginx_conf/default-app.conf - mv /default_nginx_conf/ce/default.conf /default_nginx_conf/default.conf - else - mv /default_nginx_conf/ce/default-app-without-demos.conf /default_nginx_conf/default-app.conf - mv /default_nginx_conf/ce/default.conf /default_nginx_conf/default.conf - fi - - rm -rf /default_nginx_conf/ce - rm -rf /default_nginx_conf/pro + # Remove previous configuration + [[ -f /default_nginx_conf/default.conf ]] && rm /default_nginx_conf/default.conf + cp /default_nginx_conf/ce/default.conf /default_nginx_conf/default.conf fi if [ "${PROXY_MODE}" == "PRO" ]; then - if [ "${WITH_APP}" == "true" ]; then - mv /default_nginx_conf/pro/default.conf /default_nginx_conf/default.conf - else - mv /default_nginx_conf/pro/default-app-without-demos.conf /default_nginx_conf/default.conf - fi - - rm -rf /default_nginx_conf/ce - rm -rf /default_nginx_conf/pro +[[ -f /default_nginx_conf/default.conf ]] && rm /default_nginx_conf/default.conf + cp /default_nginx_conf/pro/default.conf /default_nginx_conf/default.conf fi # Create index.html @@ -184,10 +190,85 @@ EOF # Load nginx conf files rm /etc/nginx/conf.d/* -cp /default_nginx_conf/* /etc/nginx/conf.d + +# If custom config, don't generate configuration files +if [[ -f /custom-nginx/custom-nginx.conf ]]; then + cp /custom-nginx/custom-nginx.conf /etc/nginx/conf.d/custom-nginx.conf + printf "\n" + printf "\n =======================================" + printf "\n = START OPENVIDU PROXY =" + printf "\n = WITH CUSTOM CONFIG =" + printf "\n =======================================" + printf "\n\n" + nginx -s reload + + # nginx logs + tail -f /var/log/nginx/*.log + exit 0 +fi + +# Replace config files +cp /default_nginx_conf/default* /etc/nginx/conf.d + +sed -e '/{ssl_config}/{r default_nginx_conf/global/ssl_config.conf' -e 'd}' -i /etc/nginx/conf.d/* +sed -e '/{proxy_config}/{r default_nginx_conf/global/proxy_config.conf' -e 'd}' -i /etc/nginx/conf.d/* +sed -e '/{nginx_status}/{r default_nginx_conf/global/nginx_status.conf' -e 'd}' -i /etc/nginx/conf.d/* +sed -e '/{common_api_ce}/{r default_nginx_conf/global/ce/common_api_ce.conf' -e 'd}' -i /etc/nginx/conf.d/* +sed -e '/{new_api_ce}/{r default_nginx_conf/global/ce/new_api_ce.conf' -e 'd}' -i /etc/nginx/conf.d/* +sed -e '/{common_api_pro}/{r default_nginx_conf/global/pro/common_api_pro.conf' -e 'd}' -i /etc/nginx/conf.d/* +sed -e '/{new_api_pro}/{r default_nginx_conf/global/pro/new_api_pro.conf' -e 'd}' -i /etc/nginx/conf.d/* + +if [[ "${WITH_APP}" == "true" ]]; then + sed -e '/{app_upstream}/{r default_nginx_conf/global/app_upstream.conf' -e 'd}' -i /etc/nginx/conf.d/* + sed -e '/{app_config}/{r default_nginx_conf/global/app_config.conf' -e 'd}' -i /etc/nginx/conf.d/* +elif [[ "${WITH_APP}" == "false" ]]; then + sed -i '/{app_upstream}/d' /etc/nginx/conf.d/* + sed -e '/{app_config}/{r default_nginx_conf/global/app_config_default.conf' -e 'd}' -i /etc/nginx/conf.d/* +fi + +if [[ "${SUPPORT_DEPRECATED_API}" == "true" ]]; then + sed -e '/{deprecated_api_ce}/{r default_nginx_conf/global/ce/deprecated_api_ce.conf' -e 'd}' -i /etc/nginx/conf.d/* + sed -e '/{deprecated_api_pro}/{r default_nginx_conf/global/pro/deprecated_api_pro.conf' -e 'd}' -i /etc/nginx/conf.d/* +elif [[ "${SUPPORT_DEPRECATED_API}" == "false" ]]; then + sed -i '/{deprecated_api_ce}/d' /etc/nginx/conf.d/* + sed -i '/{deprecated_api_pro}/d' /etc/nginx/conf.d/* +fi + +if [[ "${REDIRECT_WWW}" == "true" ]]; then + sed -e '/{redirect_www_ssl}/{r default_nginx_conf/global/redirect_www_ssl.conf' -e 'd}' -i /etc/nginx/conf.d/* + if [[ "${PROXY_MODE}" == "CE" ]]; then + sed -e '/{redirect_www}/{r default_nginx_conf/global/ce/redirect_www.conf' -e 'd}' -i /etc/nginx/conf.d/* + fi + + if [ "${PROXY_MODE}" == "PRO" ]; then + sed -e '/{redirect_www}/{r default_nginx_conf/global/pro/redirect_www.conf' -e 'd}' -i /etc/nginx/conf.d/* + fi +elif [[ "${REDIRECT_WWW}" == "false" ]]; then + sed -i '/{redirect_www}/d' /etc/nginx/conf.d/* + sed -i '/{redirect_www_ssl}/d' /etc/nginx/conf.d/* +fi + +# Process main configs +sed -e '/{ssl_config}/{r default_nginx_conf/global/ssl_config.conf' -e 'd}' -i /etc/nginx/conf.d/* +sed -e '/{proxy_config}/{r default_nginx_conf/global/proxy_config.conf' -e 'd}' -i /etc/nginx/conf.d/* sed -i "s/{domain_name}/${DOMAIN_OR_PUBLIC_IP}/g" /etc/nginx/conf.d/* + +# IPv6 listening (RFC 6540) +if [ ! -f /proc/net/if_inet6 ]; then + sed -i '/\[::\]:{http_port}/d' /etc/nginx/conf.d/* + sed -i '/\[::\]:{https_port}/d' /etc/nginx/conf.d/* +fi + sed -i "s/{http_port}/${PROXY_HTTP_PORT}/g" /etc/nginx/conf.d/* sed -i "s/{https_port}/${PROXY_HTTPS_PORT}/g" /etc/nginx/conf.d/* +sed -i "s/{ssl_protocols}/${PROXY_HTTPS_PROTOCOLS}/g" /etc/nginx/conf.d/* +sed -i "s/{ssl_ciphers}/${PROXY_HTTPS_CIPHERS}/g" /etc/nginx/conf.d/* + +if [ -n "${PROXY_HTTPS_HSTS}" ]; then + sed -i "s/{add_header_hsts}/${PROXY_HTTPS_HSTS}/g" /etc/nginx/conf.d/* +else + sed -i '/{add_header_hsts}/d' /etc/nginx/conf.d/* +fi # NGINX access printf "\n" @@ -197,8 +278,6 @@ printf "\n =======================================" printf "\n" printf "\n Adding rules..." -LOCAL_NETWORKS=$(ip route list | grep -Eo '([0-9]*\.){3}[0-9]*/[0-9]*') -PUBLIC_IP=$(/usr/local/bin/discover_my_public_ip.sh) valid_ip_v4() { @@ -222,6 +301,23 @@ valid_ip_v6() fi } +LOCAL_NETWORKS=$(ip route list | grep -Eo '([0-9]*\.){3}[0-9]*/[0-9]*') +if [[ "${PUBLIC_IP}" == "auto-ipv4" ]]; then + PUBLIC_IP=$(/usr/local/bin/discover_my_public_ip.sh) + printf "\n - Public IPv4 for rules: %s" "$PUBLIC_IP" +elif [[ "${PUBLIC_IP}" == "auto-ipv6" ]]; then + PUBLIC_IP=$(/usr/local/bin/discover_my_public_ip.sh --ipv6) + printf "\n - Public IPv6 for rules: %s" "$PUBLIC_IP" +else + if valid_ip_v4 "$PUBLIC_IP"; then + printf "\n - Valid defined public IPv4: %s" "$PUBLIC_IP" + elif valid_ip_v6 "$PUBLIC_IP"; then + printf "\n - Valid defined public IPv6: %s" "$PUBLIC_IP" + else + printf "\n - Not valid defined IP Address: %s" "$PUBLIC_IP" + fi +fi + if [ "${ALLOWED_ACCESS_TO_DASHBOARD}" != "all" ]; then IFS=',' for IP in $(echo "${ALLOWED_ACCESS_TO_DASHBOARD}" | tr -d '[:space:]') @@ -287,7 +383,7 @@ else fi if [ "${RULES_DASHBOARD}" != "allow all;" ]; then - if ! echo "${RULES_DASHBOARD}" | grep -q "$PUBLIC_IP" && valid_ip_v4 "$PUBLIC_IP" || valid_ip_v6 "$IP"; then + if ! echo "${RULES_DASHBOARD}" | grep -q "$PUBLIC_IP" && valid_ip_v4 "$PUBLIC_IP" || valid_ip_v6 "$PUBLIC_IP"; then RULES_DASHBOARD="${RULES_DASHBOARD}{new_line}allow $PUBLIC_IP;" fi @@ -305,7 +401,7 @@ if [ "${RULES_DASHBOARD}" != "allow all;" ]; then fi if [ "${RULES_RESTAPI}" != "allow all;" ]; then - if ! echo "${RULES_RESTAPI}" | grep -q "$PUBLIC_IP" && valid_ip_v4 "$PUBLIC_IP" || valid_ip_v6 "$IP"; then + if ! echo "${RULES_RESTAPI}" | grep -q "$PUBLIC_IP" && valid_ip_v4 "$PUBLIC_IP" || valid_ip_v6 "$PUBLIC_IP"; then RULES_RESTAPI="${RULES_RESTAPI}{new_line}allow $PUBLIC_IP;" fi diff --git a/openvidu-server/docker/openvidu-proxy/nginx.conf b/openvidu-server/docker/openvidu-proxy/nginx.conf new file mode 100644 index 00000000..bd56d495 --- /dev/null +++ b/openvidu-server/docker/openvidu-proxy/nginx.conf @@ -0,0 +1,34 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + + +events { + worker_connections {worker_connections}; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + server_tokens off; + + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/vhost.d/*.conf; +} \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-recording/create_image.sh b/openvidu-server/docker/openvidu-recording/create_image.sh index b85144a8..46f1c62a 100755 --- a/openvidu-server/docker/openvidu-recording/create_image.sh +++ b/openvidu-server/docker/openvidu-recording/create_image.sh @@ -1,4 +1,6 @@ #!/bin/bash -x + +# eg: $ ./create_image.sh ubuntu-20-04 86.0.4240.193-1 2.16.0 OPENVIDU_RECORDING_UBUNTU_VERSION=$1 OPENVIDU_RECORDING_CHROME_VERSION=$2 # https://www.ubuntuupdates.org/package_logs?noppa=&page=1&type=ppas&vals=8# OPENVIDU_RECORDING_DOCKER_TAG=$3 diff --git a/openvidu-server/docker/openvidu-recording/firefox/Dockerfile b/openvidu-server/docker/openvidu-recording/firefox/Dockerfile index 98a6fcb1..46b9c210 100644 --- a/openvidu-server/docker/openvidu-recording/firefox/Dockerfile +++ b/openvidu-server/docker/openvidu-recording/firefox/Dockerfile @@ -1,5 +1,5 @@ FROM ubuntu:20.04 -MAINTAINER openvidu@gmail.com +MAINTAINER info@openvidu.io # Install packages RUN apt-get update && apt-get -y upgrade && apt-get install -y \ diff --git a/openvidu-server/docker/openvidu-recording/scripts/composed.sh b/openvidu-server/docker/openvidu-recording/scripts/composed.sh index fe70c076..226a9584 100644 --- a/openvidu-server/docker/openvidu-recording/scripts/composed.sh +++ b/openvidu-server/docker/openvidu-recording/scripts/composed.sh @@ -41,13 +41,16 @@ fi chmod 777 /recordings/$VIDEO_ID echo $RECORDING_JSON > /recordings/$VIDEO_ID/.recording.$VIDEO_ID - pulseaudio -D + # Cleanup to be "stateless" on startup, otherwise pulseaudio daemon can't start + rm -rf /var/run/pulse /var/lib/pulse /root/.config/pulse + # Run pulseaudio + pulseaudio -D --system --disallow-exit --disallow-module-loading ### Start Chrome in headless mode with xvfb, using the display num previously obtained ### touch xvfb.log chmod 777 xvfb.log - xvfb-run --auto-servernum --server-args="-ac -screen 0 ${RESOLUTION}x24 -noreset" google-chrome --kiosk --start-maximized --test-type --no-sandbox --disable-infobars --disable-gpu --disable-popup-blocking --window-size=$WIDTH,$HEIGHT --window-position=0,0 --no-first-run --ignore-certificate-errors --autoplay-policy=no-user-gesture-required $DEBUG_CHROME_FLAGS $URL &> xvfb.log & + xvfb-run --auto-servernum --server-args="-ac -screen 0 ${RESOLUTION}x24 -noreset" google-chrome --kiosk --start-maximized --test-type --no-sandbox --disable-infobars --disable-gpu --disable-popup-blocking --window-size=$WIDTH,$HEIGHT --window-position=0,0 --no-first-run --ignore-certificate-errors --disable-dev-shm-usage --autoplay-policy=no-user-gesture-required $DEBUG_CHROME_FLAGS $URL &> xvfb.log & touch stop chmod 777 /recordings diff --git a/openvidu-server/docker/openvidu-recording/scripts/composed_quick_start.sh b/openvidu-server/docker/openvidu-recording/scripts/composed_quick_start.sh index 68fd2aab..0e58e72c 100644 --- a/openvidu-server/docker/openvidu-recording/scripts/composed_quick_start.sh +++ b/openvidu-server/docker/openvidu-recording/scripts/composed_quick_start.sh @@ -24,13 +24,16 @@ if [[ -z "${COMPOSED_QUICK_START_ACTION}" ]]; then export HEIGHT="$(cut -d'x' -f2 <<< $RESOLUTION)" export RECORDING_MODE=${RECORDING_MODE} - pulseaudio -D + # Cleanup to be "stateless" on startup, otherwise pulseaudio daemon can't start + rm -rf /var/run/pulse /var/lib/pulse /root/.config/pulse + # Run pulseaudio + pulseaudio -D --system --disallow-exit --disallow-module-loading ### Start Chrome in headless mode with xvfb, using the display num previously obtained ### touch xvfb.log chmod 777 xvfb.log - xvfb-run --auto-servernum --server-args="-ac -screen 0 ${RESOLUTION}x24 -noreset" google-chrome --kiosk --start-maximized --test-type --no-sandbox --disable-infobars --disable-gpu --disable-popup-blocking --window-size=$WIDTH,$HEIGHT --window-position=0,0 --no-first-run --ignore-certificate-errors --autoplay-policy=no-user-gesture-required --enable-logging --v=1 $DEBUG_CHROME_FLAGS $URL &> xvfb.log & + xvfb-run --auto-servernum --server-args="-ac -screen 0 ${RESOLUTION}x24 -noreset" google-chrome --kiosk --start-maximized --test-type --no-sandbox --disable-infobars --disable-gpu --disable-popup-blocking --window-size=$WIDTH,$HEIGHT --window-position=0,0 --no-first-run --ignore-certificate-errors --disable-dev-shm-usage --autoplay-policy=no-user-gesture-required --enable-logging --v=1 $DEBUG_CHROME_FLAGS $URL &> xvfb.log & chmod 777 /recordings until pids=$(pidof Xvfb) @@ -96,6 +99,9 @@ elif [[ "${COMPOSED_QUICK_START_ACTION}" == "--start-recording" ]]; then <./stop ffmpeg -y -f alsa -i pulse -f x11grab -draw_mouse 0 -framerate $FRAMERATE -video_size $RESOLUTION -i :$DISPLAY_NUM -c:a aac -c:v libx264 -preset ultrafast -crf 28 -refs 4 -qmin 4 -pix_fmt yuv420p -filter:v fps=$FRAMERATE "/recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT" fi + # Warn the stop thread about ffmpeg process being completed + echo "ffmpeg-completed" > /tmp/$VIDEO_ID-completed.txt + } 2>&1 | tee -a /tmp/container-start-recording.log elif [[ "${COMPOSED_QUICK_START_ACTION}" == "--stop-recording" ]]; then @@ -109,10 +115,17 @@ elif [[ "${COMPOSED_QUICK_START_ACTION}" == "--stop-recording" ]]; then exit 0 fi - # Stop and wait ffmpeg process to be stopped + # Stop ffmpeg process FFMPEG_PID=$(pgrep ffmpeg) echo 'q' > stop && tail --pid=$FFMPEG_PID -f /dev/null + ## Wait for the ffmpeg process to be finished + until [ -f /tmp/$VIDEO_ID-completed.txt ] + do + # Check 20 times per second + sleep 0.05 + done + rm -f /tmp/$VIDEO_ID-completed.txt ### Generate video report file ### ffprobe -v quiet -print_format json -show_format -show_streams /recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT > /recordings/$VIDEO_ID/$VIDEO_ID.info diff --git a/openvidu-server/docker/openvidu-recording/ubuntu-16-04.Dockerfile b/openvidu-server/docker/openvidu-recording/ubuntu-16-04.Dockerfile index bee21093..efd7487a 100644 --- a/openvidu-server/docker/openvidu-recording/ubuntu-16-04.Dockerfile +++ b/openvidu-server/docker/openvidu-recording/ubuntu-16-04.Dockerfile @@ -1,5 +1,5 @@ FROM ubuntu:16.04 -MAINTAINER openvidu@gmail.com +MAINTAINER info@openvidu.io ARG CHROME_VERSION @@ -19,6 +19,9 @@ RUN apt-get install -y ffmpeg pulseaudio xvfb # Install jq for managing JSON RUN apt-get install -y jq +# Add root user to pulseaudio group +RUN adduser root pulse-access + # Clean RUN apt-get autoclean diff --git a/openvidu-server/docker/openvidu-recording/ubuntu-20-04.Dockerfile b/openvidu-server/docker/openvidu-recording/ubuntu-20-04.Dockerfile index 72f88a42..7f7c7eaf 100644 --- a/openvidu-server/docker/openvidu-recording/ubuntu-20-04.Dockerfile +++ b/openvidu-server/docker/openvidu-recording/ubuntu-20-04.Dockerfile @@ -1,5 +1,5 @@ FROM ubuntu:20.04 -MAINTAINER openvidu@gmail.com +MAINTAINER info@openvidu.io ARG CHROME_VERSION @@ -23,6 +23,9 @@ RUN wget http://dl.google.com/linux/deb/pool/main/g/google-chrome-stable/google- && rm google-chrome-stable_${CHROME_VERSION}_amd64.deb \ && google-chrome --version +# Add root user to pulseaudio group +RUN adduser root pulse-access + # Clean RUN apt-get clean && apt-get autoclean && apt-get autoremove diff --git a/openvidu-server/docker/openvidu-redis/entrypoint.sh b/openvidu-server/docker/openvidu-redis/entrypoint.sh index 3a319247..2cb1dc5e 100644 --- a/openvidu-server/docker/openvidu-redis/entrypoint.sh +++ b/openvidu-server/docker/openvidu-redis/entrypoint.sh @@ -1,6 +1,10 @@ #!/bin/sh -[ -z "${REDIS_BINDING}" ] && REDIS_BINDING="127.0.0.1" +if [ -f /proc/net/if_inet6 ]; then + [ -z "${REDIS_BINDING}" ] && REDIS_BINDING="127.0.0.1 ::1" +else + [ -z "${REDIS_BINDING}" ] && REDIS_BINDING="127.0.0.1" +fi printf "\n" printf "\n =======================================" diff --git a/openvidu-server/docker/openvidu-server-kms/Dockerfile b/openvidu-server/docker/openvidu-server-kms/Dockerfile index a4d2b0cc..58950dad 100644 --- a/openvidu-server/docker/openvidu-server-kms/Dockerfile +++ b/openvidu-server/docker/openvidu-server-kms/Dockerfile @@ -1,18 +1,23 @@ -FROM ubuntu:16.04 -MAINTAINER openvidu@gmail.com +FROM ubuntu:18.04 +MAINTAINER info@openvidu.io + +# Update and install dependencies +RUN apt-get update && apt-get -y upgrade && \ + apt-get install -y gnupg2 && \ + rm -rf /var/lib/apt/lists/* # Install Kurento Media Server (KMS) -RUN echo "deb [arch=amd64] http://ubuntu.openvidu.io/6.14.0 xenial kms6" | tee /etc/apt/sources.list.d/kurento.list \ - && apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 5AFA7A83 \ - && apt-get update \ - && apt-get -y install kurento-media-server \ - && rm -rf /var/lib/apt/lists/* +RUN echo "deb [arch=amd64] http://ubuntu.openvidu.io/6.15.0 bionic kms6" | tee /etc/apt/sources.list.d/kurento.list && \ + apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 5AFA7A83 && \ + apt-get update && \ + apt-get -y install kurento-media-server && \ + rm -rf /var/lib/apt/lists/* # Install Java, supervisor and netstat RUN apt-get update && apt-get install -y \ - openjdk-8-jre \ - supervisor \ -&& rm -rf /var/lib/apt/lists/* + openjdk-11-jre \ + supervisor && \ + rm -rf /var/lib/apt/lists/* # Configure supervisor RUN mkdir -p /var/log/supervisor @@ -24,6 +29,10 @@ COPY openvidu-server.jar openvidu-server.jar # Copy KMS entrypoint COPY kms.sh /kms.sh +# Cleanup +RUN rm -rf /var/lib/apt/lists/* && \ + apt-get autoremove --purge -y && apt-get autoclean + EXPOSE 8888 EXPOSE 9091 EXPOSE 4443 diff --git a/openvidu-server/docker/openvidu-server-kms/create_image.sh b/openvidu-server/docker/openvidu-server-kms/create_image.sh index 2f5c47f5..75610611 100755 --- a/openvidu-server/docker/openvidu-server-kms/create_image.sh +++ b/openvidu-server/docker/openvidu-server-kms/create_image.sh @@ -1,5 +1,8 @@ -cp ../../target/openvidu-server-"$1".jar ./openvidu-server.jar - -docker build -t openvidu/openvidu-server-kms . - -rm ./openvidu-server.jar +VERSION=$1 +if [[ ! -z $VERSION ]]; then + cp ../../target/openvidu-server-*.jar ./openvidu-server.jar + docker build -t openvidu/openvidu-server-kms:$VERSION . + rm ./openvidu-server.jar +else + echo "Error: You need to specify a version as first argument" +fi \ No newline at end of file diff --git a/openvidu-server/docker/openvidu-server-pro/Dockerfile b/openvidu-server/docker/openvidu-server-pro/Dockerfile index 4271b2ea..f85f777f 100644 --- a/openvidu-server/docker/openvidu-server-pro/Dockerfile +++ b/openvidu-server/docker/openvidu-server-pro/Dockerfile @@ -1,15 +1,17 @@ -FROM ubuntu:16.04 -MAINTAINER openvidu@gmail.com +FROM ubuntu:18.04 +MAINTAINER info@openvidu.io # Install main components RUN apt-get update && apt-get install -y \ curl \ wget \ - openjdk-8-jre \ + openjdk-11-jre \ coturn \ redis-tools \ jq \ docker.io \ + ethtool \ + dnsutils \ && rm -rf /var/lib/apt/lists/* RUN mkdir -p /opt/openvidu /usr/local/bin/ diff --git a/openvidu-server/docker/openvidu-server-pro/create_image.sh b/openvidu-server/docker/openvidu-server-pro/create_image.sh index 1a5e6430..82e6097d 100755 --- a/openvidu-server/docker/openvidu-server-pro/create_image.sh +++ b/openvidu-server/docker/openvidu-server-pro/create_image.sh @@ -1,9 +1,7 @@ VERSION=$1 if [[ ! -z $VERSION ]]; then cp ../utils/discover_my_public_ip.sh ./discover_my_public_ip.sh - docker build -t openvidu/openvidu-server-pro:$VERSION . - rm ./discover_my_public_ip.sh else echo "Error: You need to specify a version as first argument" diff --git a/openvidu-server/docker/openvidu-server-pro/entrypoint.sh b/openvidu-server/docker/openvidu-server-pro/entrypoint.sh index b7b8ed9e..253a91c0 100755 --- a/openvidu-server/docker/openvidu-server-pro/entrypoint.sh +++ b/openvidu-server/docker/openvidu-server-pro/entrypoint.sh @@ -8,19 +8,12 @@ if [ ! -z "${WAIT_KIBANA_URL}" ]; then printf "\n =======================================" printf "\n" - while true - do - HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${WAIT_KIBANA_URL}") - + until $(curl --insecure --output /dev/null --silent --head --fail --max-time 10 --connect-timeout 10 ${WAIT_KIBANA_URL}) + do printf "\n Waiting for kibana in '%s' URL..." "${WAIT_KIBANA_URL}" - - if [ "$HTTP_STATUS" == "200" ]; then - printf "\n ==== Kibana is Ready ====" - break - fi - sleep 1 done + printf "\n ==== Kibana is Ready ====" fi # Launch Openvidu Pro @@ -30,6 +23,18 @@ printf "\n = LAUNCH OPENVIDU-SERVER =" printf "\n =======================================" printf "\n" +# Get coturn public ip +[[ -z "${COTURN_IP}" ]] && export COTURN_IP=auto-ipv4 +if [[ "${COTURN_IP}" == "auto-ipv4" ]]; then + COTURN_IP=$(/usr/local/bin/discover_my_public_ip.sh) +elif [[ "${COTURN_IP}" == "auto-ipv6" ]]; then + COTURN_IP=$(/usr/local/bin/discover_my_public_ip.sh --ipv6) +fi + +if [[ "${OV_CE_DEBUG_LEVEL}" == "DEBUG" ]]; then + export LOGGING_LEVEL_IO_OPENVIDU_SERVER=DEBUG +fi + if [ ! -z "${JAVA_OPTIONS}" ]; then printf "\n Using java options: %s" "${JAVA_OPTIONS}" fi diff --git a/openvidu-server/docker/openvidu-server/Dockerfile b/openvidu-server/docker/openvidu-server/Dockerfile index 506a88f7..b6bed77d 100644 --- a/openvidu-server/docker/openvidu-server/Dockerfile +++ b/openvidu-server/docker/openvidu-server/Dockerfile @@ -1,13 +1,14 @@ -FROM ubuntu:16.04 -MAINTAINER openvidu@gmail.com +FROM ubuntu:18.04 +MAINTAINER info@openvidu.io # Install Java, supervisor and netstat RUN apt-get update && apt-get install -y \ curl \ wget \ - openjdk-8-jre \ + openjdk-11-jre \ coturn \ redis-tools \ + dnsutils \ && rm -rf /var/lib/apt/lists/* # Copy OpenVidu Server diff --git a/openvidu-server/docker/openvidu-server/entrypoint.sh b/openvidu-server/docker/openvidu-server/entrypoint.sh index be6b71d0..7a509808 100644 --- a/openvidu-server/docker/openvidu-server/entrypoint.sh +++ b/openvidu-server/docker/openvidu-server/entrypoint.sh @@ -6,6 +6,18 @@ printf "\n = LAUNCH OPENVIDU-SERVER =" printf "\n =======================================" printf "\n" +# Get coturn public ip +[[ -z "${COTURN_IP}" ]] && export COTURN_IP=auto-ipv4 +if [[ "${COTURN_IP}" == "auto-ipv4" ]]; then + COTURN_IP=$(/usr/local/bin/discover_my_public_ip.sh) +elif [[ "${COTURN_IP}" == "auto-ipv6" ]]; then + COTURN_IP=$(/usr/local/bin/discover_my_public_ip.sh --ipv6) +fi + +if [[ "${OV_CE_DEBUG_LEVEL}" == "DEBUG" ]]; then + export LOGGING_LEVEL_IO_OPENVIDU_SERVER=DEBUG +fi + if [ ! -z "${JAVA_OPTIONS}" ]; then printf "\n Using java options: %s" "${JAVA_OPTIONS}" fi diff --git a/openvidu-server/docker/utils/discover_my_public_ip.sh b/openvidu-server/docker/utils/discover_my_public_ip.sh index b42b3a97..e8227993 100755 --- a/openvidu-server/docker/utils/discover_my_public_ip.sh +++ b/openvidu-server/docker/utils/discover_my_public_ip.sh @@ -1,47 +1,116 @@ -#!/bin/bash +#!/usr/bin/env bash -# Check if a txt is a valid ip -function valid_ip() -{ - local ip=$1 - local stat=1 +#/ Use DNS to find out about the external IP of the running system. +#/ +#/ This script is useful when running from a machine that sits behind a NAT. +#/ Due to how NAT works, machines behind it belong to an internal or private +#/ subnet, with a different address space than the external or public side. +#/ +#/ Typically it is possible to make an HTTP request to a number of providers +#/ that offer the external IP in their response body (eg: ifconfig.me). However, +#/ why do a slow and heavy HTTP request, when DNS exists and is much faster? +#/ Well established providers such as OpenDNS or Google offer special hostnames +#/ that, when resolved, will actually return the IP address of the caller. +#/ +#/ https://unix.stackexchange.com/questions/22615/how-can-i-get-my-external-ip-address-in-a-shell-script/81699#81699 +#/ +#/ +#/ Arguments +#/ --------- +#/ +#/ --ipv4 +#/ +#/ Find the external IPv4 address. +#/ Optional. Default: Enabled. +#/ +#/ --ipv6 +#/ +#/ Find the external IPv6 address. +#/ Optional. Default: Disabled. - if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then - OIFS=$IFS - IFS='.' - ip=($ip) - IFS=$OIFS - [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \ - && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]] - stat=$? - fi - return $stat + + +# Shell setup +# =========== + +# Bash options for strict error checking. +set -o errexit -o errtrace -o pipefail -o nounset + +# Trace all commands (to stderr). +#set -o xtrace + +# Trap function for unhandled errors. +function on_error() { + echo "[getmyip] ERROR ($?)" >&2 + exit 1 } +trap on_error ERR -# Services to get public ip -SERVICES=( - "curl --silent -sw :%{http_code} ipv4.icanhazip.com" - "curl --silent -sw :%{http_code} ifconfig.me" - "curl --silent -sw :%{http_code} -4 ifconfig.co" - "curl --silent -sw :%{http_code} ipecho.net/plain" - "curl --silent -sw :%{http_code} ipinfo.io/ip" - "curl --silent -sw :%{http_code} checkip.amazonaws.com" - "curl --silent -sw :%{http_code} v4.ident.me" -) -# Get public ip -for service in "${SERVICES[@]}"; do - RUN_COMMAND=$($service | tr -d '[:space:]') - IP=$(echo "$RUN_COMMAND" | cut -d':' -f1) - HTTP_CODE=$(echo "$RUN_COMMAND" | cut -d':' -f2) - if [ "$HTTP_CODE" == "200" ]; then - if valid_ip "$IP"; then - printf "%s" "$IP" - exit 0 - fi +# Parse call arguments +# ==================== + +CFG_IPV4="true" +CFG_IPV6="false" + +while [[ $# -gt 0 ]]; do + case "${1-}" in + --ipv4) + CFG_IPV4="true" + CFG_IPV6="false" + ;; + --ipv6) + CFG_IPV4="false" + CFG_IPV6="true" + ;; + *) + echo "Invalid argument: '${1-}'" >&2 + exit 1 + ;; + esac + shift +done + + + +# Obtain the external IP address +# ============================== + +if [[ "$CFG_IPV4" == "true" ]]; then + COMMANDS=( + 'dig @resolver1.opendns.com myip.opendns.com A -4 +short' + 'dig @ns1.google.com o-o.myaddr.l.google.com TXT -4 +short | tr -d \"' + 'dig @1.1.1.1 whoami.cloudflare TXT CH -4 +short | tr -d \"' + 'dig @ns1-1.akamaitech.net whoami.akamai.net A -4 +short' + ) + + function is_valid_ip() { + # Check if the input looks like an IPv4 address. + # Doesn't check if the actual values are valid; assumes they are. + echo "$1" | grep --perl-regexp --quiet '^(\d{1,3}\.){3}\d{1,3}$' + } +elif [[ "$CFG_IPV6" == "true" ]]; then + COMMANDS=( + 'dig @resolver1.opendns.com myip.opendns.com AAAA -6 +short' + 'dig @ns1.google.com o-o.myaddr.l.google.com TXT -6 +short | tr -d \"' + 'dig @2606:4700:4700::1111 whoami.cloudflare TXT CH -6 +short | tr -d \"' + ) + + function is_valid_ip() { + # Check if the input looks like an IPv6 address. + # It's almost impossible to check the IPv6 representation because it + # varies wildly, so just check that there are at least 2 colons. + [[ "$(echo "$1" | awk -F':' '{print NF-1}')" -ge 2 ]] + } +fi + +for COMMAND in "${COMMANDS[@]}"; do + if IP="$(eval "$COMMAND")" && is_valid_ip "$IP"; then + echo "$IP" + exit 0 fi done -printf "error" -exit 0 \ No newline at end of file +echo "[getmyip] All providers failed" >&2 +exit 1 \ No newline at end of file diff --git a/openvidu-server/pom.xml b/openvidu-server/pom.xml index a2282893..bd2fbf0d 100644 --- a/openvidu-server/pom.xml +++ b/openvidu-server/pom.xml @@ -12,7 +12,7 @@ jar OpenVidu Server - 2.15.0 + 2.16.0 OpenVidu Server https://openvidu.io @@ -187,6 +187,10 @@ org.springframework.boot spring-boot-starter-web + + org.springframework + spring-websocket + org.slf4j slf4j-api @@ -258,6 +262,11 @@ spring-boot-starter-logging ${version.spring-boot} + + org.springframework.boot + spring-boot-starter-websocket + ${version.spring-boot} + com.github.docker-java docker-java diff --git a/openvidu-server/src/dashboard/angular.json b/openvidu-server/src/dashboard/angular.json index 9403c727..c94c23b1 100644 --- a/openvidu-server/src/dashboard/angular.json +++ b/openvidu-server/src/dashboard/angular.json @@ -38,7 +38,6 @@ "optimization": true, "outputHashing": "all", "sourceMap": false, - "extractCss": true, "namedChunks": false, "aot": true, "extractLicenses": true, @@ -127,7 +126,7 @@ "schematics": { "@schematics/angular:component": { "prefix": "app", - "styleext": "css" + "style": "css" }, "@schematics/angular:directive": { "prefix": "app" diff --git a/openvidu-server/src/dashboard/package-lock.json b/openvidu-server/src/dashboard/package-lock.json index 346a0f16..9af76751 100644 --- a/openvidu-server/src/dashboard/package-lock.json +++ b/openvidu-server/src/dashboard/package-lock.json @@ -5,211 +5,354 @@ "requires": true, "dependencies": { "@angular-devkit/architect": { - "version": "0.901.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.901.1.tgz", - "integrity": "sha512-foWDAurMfBDYLAJxHpTFkJBub1c2A8+eWHbBjgqIHmT8xadnE7t8nSA9XDl+k/kIoWw/qFU+6IttPirudYc/vw==", + "version": "0.1100.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1100.1.tgz", + "integrity": "sha512-DIAvTRRY+k7T2xHf4RVV06P16D0V7wSf1MSpGSDWVpfWcA3HNOSGfsk1F+COMlbFehXuKztwieXarv5rXbBCng==", "dev": true, "requires": { - "@angular-devkit/core": "9.1.1", - "rxjs": "6.5.4" + "@angular-devkit/core": "11.0.1", + "rxjs": "6.6.3" }, "dependencies": { "rxjs": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", - "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", "dev": true, "requires": { "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } } } }, "@angular-devkit/build-angular": { - "version": "0.901.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.901.1.tgz", - "integrity": "sha512-6uEvo5htsJoxQHBVwHOGmM6YWq5q6m9UWMv/ughlek0RtSLFfOt9TZQ/yQHgtGQsCQvscD/jBzVoD0zD5Ax/SQ==", + "version": "0.1100.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.1100.1.tgz", + "integrity": "sha512-w8NcoXuruOHio0D/JbX47iDl9FVH8X9k/OlZ/rSNVQ3uEpV6uxIaTm3fZ1ZSrKffi+97rKEwpHOf2N0DXl4XGQ==", "dev": true, "requires": { - "@angular-devkit/architect": "0.901.1", - "@angular-devkit/build-optimizer": "0.901.1", - "@angular-devkit/build-webpack": "0.901.1", - "@angular-devkit/core": "9.1.1", - "@babel/core": "7.9.0", - "@babel/generator": "7.9.3", - "@babel/preset-env": "7.9.0", - "@babel/template": "7.8.6", - "@jsdevtools/coverage-istanbul-loader": "3.0.3", - "@ngtools/webpack": "9.1.1", - "ajv": "6.12.0", - "autoprefixer": "9.7.4", - "babel-loader": "8.0.6", + "@angular-devkit/architect": "0.1100.1", + "@angular-devkit/build-optimizer": "0.1100.1", + "@angular-devkit/build-webpack": "0.1100.1", + "@angular-devkit/core": "11.0.1", + "@babel/core": "7.12.3", + "@babel/generator": "7.12.1", + "@babel/plugin-transform-runtime": "7.12.1", + "@babel/preset-env": "7.12.1", + "@babel/runtime": "7.12.1", + "@babel/template": "7.10.4", + "@jsdevtools/coverage-istanbul-loader": "3.0.5", + "@ngtools/webpack": "11.0.1", + "ansi-colors": "4.1.1", + "autoprefixer": "9.8.6", + "babel-loader": "8.1.0", "browserslist": "^4.9.1", - "cacache": "15.0.0", + "cacache": "15.0.5", "caniuse-lite": "^1.0.30001032", "circular-dependency-plugin": "5.2.0", - "copy-webpack-plugin": "5.1.1", - "core-js": "3.6.4", + "copy-webpack-plugin": "6.2.1", + "core-js": "3.6.5", + "css-loader": "5.0.0", "cssnano": "4.1.10", - "file-loader": "6.0.0", + "file-loader": "6.1.1", "find-cache-dir": "3.3.1", "glob": "7.1.6", - "jest-worker": "25.1.0", + "inquirer": "7.3.3", + "jest-worker": "26.5.0", "karma-source-map-support": "1.4.0", - "less": "3.11.1", - "less-loader": "5.0.0", - "license-webpack-plugin": "2.1.4", + "less": "3.12.2", + "less-loader": "7.0.2", + "license-webpack-plugin": "2.3.1", "loader-utils": "2.0.0", - "mini-css-extract-plugin": "0.9.0", + "mini-css-extract-plugin": "1.2.1", "minimatch": "3.0.4", - "open": "7.0.3", - "parse5": "4.0.0", - "postcss": "7.0.27", + "open": "7.3.0", + "ora": "5.1.0", + "parse5-html-rewriting-stream": "6.0.1", + "pnp-webpack-plugin": "1.6.4", + "postcss": "7.0.32", "postcss-import": "12.0.1", - "postcss-loader": "3.0.0", - "raw-loader": "4.0.0", - "regenerator-runtime": "0.13.5", + "postcss-loader": "4.0.4", + "raw-loader": "4.0.2", + "regenerator-runtime": "0.13.7", + "resolve-url-loader": "3.1.2", "rimraf": "3.0.2", - "rollup": "2.1.0", - "rxjs": "6.5.4", - "sass": "1.26.3", - "sass-loader": "8.0.2", - "semver": "7.1.3", + "rollup": "2.32.1", + "rxjs": "6.6.3", + "sass": "1.27.0", + "sass-loader": "10.0.5", + "semver": "7.3.2", "source-map": "0.7.3", - "source-map-loader": "0.2.4", - "source-map-support": "0.5.16", - "speed-measure-webpack-plugin": "1.3.1", - "style-loader": "1.1.3", - "stylus": "0.54.7", - "stylus-loader": "3.0.2", - "terser": "4.6.10", - "terser-webpack-plugin": "2.3.5", + "source-map-loader": "1.1.2", + "source-map-support": "0.5.19", + "speed-measure-webpack-plugin": "1.3.3", + "style-loader": "2.0.0", + "stylus": "0.54.8", + "stylus-loader": "4.1.1", + "terser": "5.3.7", + "terser-webpack-plugin": "4.2.3", + "text-table": "0.2.0", "tree-kill": "1.2.2", - "webpack": "4.42.0", + "webpack": "4.44.2", "webpack-dev-middleware": "3.7.2", - "webpack-dev-server": "3.10.3", - "webpack-merge": "4.2.2", - "webpack-sources": "1.4.3", - "webpack-subresource-integrity": "1.4.0", - "worker-plugin": "4.0.2" + "webpack-dev-server": "3.11.0", + "webpack-merge": "5.2.0", + "webpack-sources": "2.0.1", + "webpack-subresource-integrity": "1.5.1", + "worker-plugin": "5.0.0" }, "dependencies": { - "core-js": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz", - "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==", + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/generator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.1.tgz", + "integrity": "sha512-DB+6rafIdc9o72Yc3/Ph5h+6hUjeOp66pF0naQBgUFFuPqzQwIlPTm3xZR7YNvduIMtkDIj2t21LSQwnbCrXvg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.1", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, - "parse5": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", - "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", + "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "dev": true + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "rxjs": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", - "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", "dev": true, "requires": { "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } } } }, "@angular-devkit/build-optimizer": { - "version": "0.901.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.901.1.tgz", - "integrity": "sha512-o0A9CcyDQSUnC5CQIKf92VH8amIYRYrMgLf2kdhSMcy0QV+rEJyN81dSvwX/Yxgnr9NbWEAQg7jnyKk06vfhOw==", + "version": "0.1100.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.1100.1.tgz", + "integrity": "sha512-PpqBmDd+/cmaMj9MURe5pSSudo+Qz6BrGdzvYB16ekSp8bSDYLUriv5NvE/bm+ODKwo3jHgFrwWLiwK65vQcxQ==", "dev": true, "requires": { "loader-utils": "2.0.0", "source-map": "0.7.3", - "tslib": "1.11.1", - "typescript": "3.6.5", - "webpack-sources": "1.4.3" + "tslib": "2.0.3", + "typescript": "4.0.5", + "webpack-sources": "2.0.1" }, "dependencies": { - "typescript": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.5.tgz", - "integrity": "sha512-BEjlc0Z06ORZKbtcxGrIvvwYs5hAnuo6TKdNFL55frVDlB+na3z5bsLhFaIxmT+dPWgBIjMo6aNnTOgHHmHgiQ==", + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", "dev": true } } }, "@angular-devkit/build-webpack": { - "version": "0.901.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.901.1.tgz", - "integrity": "sha512-9oNI+wPSk8yECy+f0EebfMx4PH3uDJRrifYZAxcr84IpzEbpfpRuYhE3ecwqd7k0zu2Kdjw1uUrGxBuH4/sbGg==", + "version": "0.1100.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1100.1.tgz", + "integrity": "sha512-nfgsUfP6WyZ35rxgjqDYydB552Si/JdYLMtwy/kAFybW/6yTpw0sBOgCQoppyQ4mvVwyX9X0ZTQsMNhPOzy3sA==", "dev": true, "requires": { - "@angular-devkit/architect": "0.901.1", - "@angular-devkit/core": "9.1.1", - "rxjs": "6.5.4" + "@angular-devkit/architect": "0.1100.1", + "@angular-devkit/core": "11.0.1", + "rxjs": "6.6.3" }, "dependencies": { "rxjs": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", - "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", "dev": true, "requires": { "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } } } }, "@angular-devkit/core": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-9.1.1.tgz", - "integrity": "sha512-57MNew2u1QwVb69jxZyhXgdW9kqcGyWyRy2ui/hWCkWLg7RumWtyypmdTs89FNExB4HqtXlQ2eO3JZxfs7QR3w==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-11.0.1.tgz", + "integrity": "sha512-ui3g7w/0SpU9oq8uwN9upR8Y1eOXZ+P2p3NyDydBrR7ZEfEkRLS1mhozN/ib8farrwK5N3kIIJxMb5t3187Hng==", "dev": true, "requires": { - "ajv": "6.12.0", + "ajv": "6.12.6", "fast-json-stable-stringify": "2.1.0", "magic-string": "0.25.7", - "rxjs": "6.5.4", + "rxjs": "6.6.3", "source-map": "0.7.3" }, "dependencies": { "rxjs": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", - "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", "dev": true, "requires": { "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } } } }, "@angular-devkit/schematics": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-9.1.1.tgz", - "integrity": "sha512-6wx2HcvafHvEjEa1tjDzW2hXrOiSE8ALqJUArb3+NoO1BDM42aGcqyPo0ODzKtDk12CgSsFXdNKRpQ5AmpSPtw==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-11.0.1.tgz", + "integrity": "sha512-rAOnAndcybEH398xf5wzmcUPCoCi0dKiOo/+1dkKU5aTxynw1OUnANt5K6A+ZZTGnJmfjtP0ovkZGYun9IUDxQ==", "dev": true, "requires": { - "@angular-devkit/core": "9.1.1", - "ora": "4.0.3", - "rxjs": "6.5.4" + "@angular-devkit/core": "11.0.1", + "ora": "5.1.0", + "rxjs": "6.6.3" }, "dependencies": { "rxjs": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", - "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", "dev": true, "requires": { "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } } } }, "@angular/animations": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-9.1.2.tgz", - "integrity": "sha512-5UJ8SzCtFj4vZChVsni4K9oa4qE9tQ67bwnP6DKxkLEJKQWWyasYp+2siAi/7zD2ro2XA0qRMYhgQz5Vj6eBoQ==" + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-11.0.0.tgz", + "integrity": "sha512-RGaAnZOI73bPnNWrJq/p8sc+hpUBhScq139M6r4qQjQPsPahazL6v6hHAgRhZNemqw164d1oE4K/22O/i0E3Tw==", + "requires": { + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } }, "@angular/cdk": { "version": "9.2.1", @@ -220,63 +363,98 @@ } }, "@angular/cli": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-9.1.1.tgz", - "integrity": "sha512-sjRAV4UF8M5v+2gw+EwCYSgciBZDc05AbNxQt+uUdxdfR1QU9hifWq8WDxfOR6jdDP5YqMtQsNaFNwrUyjJJoQ==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-11.0.1.tgz", + "integrity": "sha512-zB20jTLQxLpkJjhbYelhRyMcgGsjwbD8pSYYAO6QX656Tx1tCtJ2UskEtf4ePQvgzu0Ds2dnAEfco+tekIMRTA==", "dev": true, "requires": { - "@angular-devkit/architect": "0.901.1", - "@angular-devkit/core": "9.1.1", - "@angular-devkit/schematics": "9.1.1", - "@schematics/angular": "9.1.1", - "@schematics/update": "0.901.1", + "@angular-devkit/architect": "0.1100.1", + "@angular-devkit/core": "11.0.1", + "@angular-devkit/schematics": "11.0.1", + "@schematics/angular": "11.0.1", + "@schematics/update": "0.1100.1", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.1", - "debug": "4.1.1", + "debug": "4.2.0", "ini": "1.3.5", - "inquirer": "7.1.0", - "npm-package-arg": "8.0.1", - "npm-pick-manifest": "6.0.0", - "open": "7.0.3", + "inquirer": "7.3.3", + "npm-package-arg": "8.1.0", + "npm-pick-manifest": "6.1.0", + "open": "7.3.0", "pacote": "9.5.12", - "read-package-tree": "5.3.1", + "resolve": "1.18.1", "rimraf": "3.0.2", - "semver": "7.1.3", - "symbol-observable": "1.2.0", - "universal-analytics": "0.4.20", - "uuid": "7.0.2" + "semver": "7.3.2", + "symbol-observable": "2.0.3", + "universal-analytics": "0.4.23", + "uuid": "8.3.1" }, "dependencies": { - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "resolve": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz", + "integrity": "sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==", + "dev": true, + "requires": { + "is-core-module": "^2.0.0", + "path-parse": "^1.0.6" + } }, "uuid": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.2.tgz", - "integrity": "sha512-vy9V/+pKG+5ZTYKf+VcphF5Oc6EFiu3W8Nv3P3zIh0EqVI80ZxOzuPfe9EHjkFNvf8+xuTHVeei4Drydlx4zjw==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", "dev": true } } }, "@angular/common": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-9.1.2.tgz", - "integrity": "sha512-MAQW0DGq2NhvJSETLTwuImdzwI+wboG+5Uzgc0L8C/hX7SrEE65Hmz4nIjmGi2CPGLpodfWEcCPlV0R0dHun4A==" + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-11.0.0.tgz", + "integrity": "sha512-chlbtxR7jpPs3Rc1ymdp3UfUzqEr57OFIxVMG6hROODclPQQk/7oOHdQB4hpUObaF9y4ZTLeKHKWiR/twi21Pg==", + "requires": { + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } }, "@angular/compiler": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-9.1.2.tgz", - "integrity": "sha512-82DitvlZA5DoE++qiiJdgx6HE71h96sfLHCjjvYJHApoOcnX+zBZFBrflpbXK9W4YdvydU4Lc1Ql1eleQPeT/g==" + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-11.0.0.tgz", + "integrity": "sha512-I7wVhdqvhtBTQTtW61z0lwPb1LiQQ0NOwjsbfN5sAc7/uwxw7em+Kyb/XJgBwgaTKtAL8bZEzdoQGLdsSKQF2g==", + "requires": { + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } }, "@angular/compiler-cli": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-9.1.2.tgz", - "integrity": "sha512-zC/oIuT68vtuiYrgXqWsDNi/0DQ1Ti6J6gMTrXVvZXlEDikEExTAJKrrBV5jo6B0bpUofe/dDcJaKR3Ys3cM3Q==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-11.0.0.tgz", + "integrity": "sha512-zrd/cU9syZ8XuQ3ItfIGaKDn1ZBCWyiqdLVRH9VDmyNqQFiCc/VWQ9Th9z8qpLptgdpzE9+lKFgeZJTDtbcveQ==", "dev": true, "requires": { + "@babel/core": "^7.8.6", + "@babel/types": "^7.8.6", "canonical-path": "1.0.0", "chokidar": "^3.0.0", "convert-source-map": "^1.5.1", @@ -288,25 +466,25 @@ "semver": "^6.3.0", "source-map": "^0.6.1", "sourcemap-codec": "^1.4.8", + "tslib": "^2.0.0", "yargs": "15.3.0" }, "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -343,18 +521,6 @@ "path-exists": "^4.0.0" } }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -364,15 +530,6 @@ "p-locate": "^4.1.0" } }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, "p-locate": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", @@ -382,24 +539,12 @@ "p-limit": "^2.2.0" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -412,25 +557,11 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "dev": true }, "wrap-ansi": { "version": "6.2.0", @@ -463,9 +594,9 @@ } }, "yargs-parser": { - "version": "18.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.2.tgz", - "integrity": "sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -475,9 +606,19 @@ } }, "@angular/core": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-9.1.2.tgz", - "integrity": "sha512-nAqRl+5drAnZlBT7AgSo2JJubmPNrGCvhZ83dosPEUqJoLr69/lYipnF/iqKzYn5xo4MM1O27WIhSNDtDjBmzQ==" + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-11.0.0.tgz", + "integrity": "sha512-FNewyMwYy+kGdw1xWfrtaPD2cSQs3kDVFbl8mNMSzp933W5yMsHDvjXb0+nPFqEb8ywEIdm3MsBMK0y3iBWZQw==", + "requires": { + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } }, "@angular/flex-layout": { "version": "9.0.0-beta.29", @@ -485,14 +626,24 @@ "integrity": "sha512-93sxR+kYfYMOdnlWL0Q77FZ428gg8XnBu0YZm6GsCdkw/vLggIT/G1ZAqHlCPIODt6pxmCJ5KXh4ShvniIYDsA==" }, "@angular/forms": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-9.1.2.tgz", - "integrity": "sha512-/f2WhMiKDo1/RoisTI71Dy4Z4+sAsAuzgIxJEXvgDGSYvzLl9G8erFx4N6be8Cy/xXwErmp3JOwXIAXjzxF8tA==" + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-11.0.0.tgz", + "integrity": "sha512-hP6GF1ZkxKQp7Y+EVbEe9PPDQPrUQNdfVxphCWQYwu3tm8+tn1r91KVXkp2MA3M4Fh6Xo2HQEU2d+VXv4w0iNQ==", + "requires": { + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } }, "@angular/language-service": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-9.1.2.tgz", - "integrity": "sha512-0BnDIFbIAtFKS/2HDI0efcnW3DSkAAFhUWluBnssvwGzuMHikOKGeamuWM27ki3IzPQKGJ7fGS2fz8A7sMeIYQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-11.0.0.tgz", + "integrity": "sha512-lwUVlaiIASNbKQ/EtCK5KOVIlpiyVvysN6idAD0rJHr6BRtrlqwiayNYbV5as5IJyPYLf2E8au3an9j0E/PFDw==", "dev": true }, "@angular/material": { @@ -501,19 +652,49 @@ "integrity": "sha512-nqn/0Eg04DxwnkRGSM1xnmGgtfHYOBcEPbFeTu8c1qAbjFEozd6tpw4y6dQrCCL/JLNIRQPsxsUsVnKeWDF/4Q==" }, "@angular/platform-browser": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-9.1.2.tgz", - "integrity": "sha512-/3L4DdvnebvaJUurusaq8RJBFfr/SHWG6DMmV1VVpADxe8kjREyN0LdNDSkZgVf/QcUSwNEA6153iwcF92Otew==" + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-11.0.0.tgz", + "integrity": "sha512-p8sF6JfaBI+YyLpp5OSg6UcCqjtLKRR+Otq1P/tro5SuxrsrBNRVU8j0tl/crkScsMwAvgmJ1joRyUKdI2mUGQ==", + "requires": { + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } }, "@angular/platform-browser-dynamic": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-9.1.2.tgz", - "integrity": "sha512-BWHxy8S71z+NmUQmtR+/Dkohlj3LQIXltOqeVdCSpjV9cultBNN3bE1w0Rjp3BmCRGCIDH7qFlr4U5woHa7ldw==" + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-11.0.0.tgz", + "integrity": "sha512-NAmKGhHK+tl7dr/Hcqxvr/813Opec3Mv0IRwIgmKdlpZd7qAwT/mw4RnO4YPSEoDOM6hqGt7GdlWrSDX802duQ==", + "requires": { + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } }, "@angular/router": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-9.1.2.tgz", - "integrity": "sha512-csxE4HkuhVR1X932Q3kSDqBoF7Awuq5dsjv0nFk78raiHgG3CNnfMLHt8xD9XtOmR7ZT+D4yh/YmIK6W7J5hbQ==" + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-11.0.0.tgz", + "integrity": "sha512-10ZeobfK3HqVeWS6zjdKU16ccxFtdCHkxT11bnFg3Jwq9vKt+LI5KitAkCI5rYTY3DRfVzasRkqBzZfZMkbftw==", + "requires": { + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } }, "@babel/code-frame": { "version": "7.8.3", @@ -525,48 +706,152 @@ } }, "@babel/compat-data": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.9.0.tgz", - "integrity": "sha512-zeFQrr+284Ekvd9e7KAX954LkapWiOmQtsfHirhxqfdlX6MEC32iRE+pqUGlYIBchdevaCwvzxWGSy/YBNI85g==", - "dev": true, - "requires": { - "browserslist": "^4.9.1", - "invariant": "^2.2.4", - "semver": "^5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.5.tgz", + "integrity": "sha512-DTsS7cxrsH3by8nqQSpFSyjSfSYl57D6Cf4q8dW3LK83tBKBDCkfcay1nYkXq1nIHXnpX8WMMb/O25HOy3h1zg==", + "dev": true }, "@babel/core": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", - "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", + "version": "7.12.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.3.tgz", + "integrity": "sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g==", "dev": true, "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.0", - "@babel/helper-module-transforms": "^7.9.0", - "@babel/helpers": "^7.9.0", - "@babel/parser": "^7.9.0", - "@babel/template": "^7.8.6", - "@babel/traverse": "^7.9.0", - "@babel/types": "^7.9.0", + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.1", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helpers": "^7.12.1", + "@babel/parser": "^7.12.3", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.1", + "@babel/types": "^7.12.1", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.1", "json5": "^2.1.2", - "lodash": "^4.17.13", + "lodash": "^4.17.19", "resolve": "^1.3.2", "semver": "^5.4.1", "source-map": "^0.5.0" }, "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/generator": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", + "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", + "dev": true, + "requires": { + "@babel/types": "^7.12.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", + "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "dev": true + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/traverse": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.5.tgz", + "integrity": "sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.5", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.12.5", + "@babel/types": "^7.12.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -581,55 +866,84 @@ } } }, - "@babel/generator": { - "version": "7.9.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.3.tgz", - "integrity": "sha512-RpxM252EYsz9qLUIq6F7YJyK1sv0wWDBFuztfDGWaQKzHjqDHysxSiRUpA/X9jmfqo+WzkAVKFaUily5h+gDCQ==", + "@babel/helper-annotate-as-pure": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.9.0", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" + "@babel/types": "^7.10.4" }, "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true } } }, - "@babel/helper-annotate-as-pure": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz", - "integrity": "sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" - } - }, "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.8.3.tgz", - "integrity": "sha512-5eFOm2SyFPK4Rh3XMMRDjN7lBH0orh3ss0g3rTYZnBQ+r6YPj7lgDyCvPphynHvUrobJmeMignBr6Acw9mAPlw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz", + "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==", "dev": true, "requires": { - "@babel/helper-explode-assignable-expression": "^7.8.3", - "@babel/types": "^7.8.3" + "@babel/helper-explode-assignable-expression": "^7.10.4", + "@babel/types": "^7.10.4" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + } } }, "@babel/helper-compilation-targets": { - "version": "7.8.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.7.tgz", - "integrity": "sha512-4mWm8DCK2LugIS+p1yArqvG1Pf162upsIsjE7cNBjez+NjliQpVhj20obE520nao0o14DaTnFJv+Fw5a0JpoUw==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz", + "integrity": "sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw==", "dev": true, "requires": { - "@babel/compat-data": "^7.8.6", - "browserslist": "^4.9.1", - "invariant": "^2.2.4", - "levenary": "^1.1.1", + "@babel/compat-data": "^7.12.5", + "@babel/helper-validator-option": "^7.12.1", + "browserslist": "^4.14.5", "semver": "^5.5.0" }, "dependencies": { @@ -641,166 +955,788 @@ } } }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.8.8", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz", - "integrity": "sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg==", + "@babel/helper-create-class-features-plugin": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz", + "integrity": "sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.8.3", - "@babel/helper-regex": "^7.8.3", - "regexpu-core": "^4.7.0" + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-member-expression-to-functions": "^7.12.1", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.10.4" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", + "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "dev": true + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + } + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.1.tgz", + "integrity": "sha512-rsZ4LGvFTZnzdNZR5HZdmJVuXK8834R5QkF3WvcnBhrlVtF0HSIUC6zbreL9MgjTywhKokn8RIYRiq99+DLAxA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-regex": "^7.10.4", + "regexpu-core": "^4.7.1" } }, "@babel/helper-define-map": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz", - "integrity": "sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", + "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.8.3", - "@babel/types": "^7.8.3", - "lodash": "^4.17.13" + "@babel/helper-function-name": "^7.10.4", + "@babel/types": "^7.10.5", + "lodash": "^4.17.19" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", + "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "dev": true + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + } } }, "@babel/helper-explode-assignable-expression": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.8.3.tgz", - "integrity": "sha512-N+8eW86/Kj147bO9G2uclsg5pwfs/fqqY5rwgIL7eTBklgXjcOJ3btzS5iM6AitJcftnY7pm2lGsrJVYLGjzIw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.1.tgz", + "integrity": "sha512-dmUwH8XmlrUpVqgtZ737tK88v07l840z9j3OEhCLwKTkjlvKpfqXVIZ0wpK3aeOxspwGrf/5AP5qLx4rO3w5rA==", "dev": true, "requires": { - "@babel/traverse": "^7.8.3", - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-function-name": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", - "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.9.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.12.1" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + } } }, "@babel/helper-hoist-variables": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.8.3.tgz", - "integrity": "sha512-ky1JLOjcDUtSc+xkt0xhYff7Z6ILTAHKmZLHPxAhOP0Nd77O+3nCsd6uSVYur6nJnCI029CrNbYlc0LoPfAPQg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", + "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", "dev": true, "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.10.4" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + } } }, "@babel/helper-member-expression-to-functions": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz", - "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz", + "integrity": "sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ==", "dev": true, "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.12.1" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + } } }, "@babel/helper-module-imports": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", - "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", + "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", "dev": true, "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.12.5" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + } } }, "@babel/helper-module-transforms": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz", - "integrity": "sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz", + "integrity": "sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.8.3", - "@babel/helper-replace-supers": "^7.8.6", - "@babel/helper-simple-access": "^7.8.3", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/template": "^7.8.6", - "@babel/types": "^7.9.0", - "lodash": "^4.17.13" + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-simple-access": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/helper-validator-identifier": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.1", + "@babel/types": "^7.12.1", + "lodash": "^4.17.19" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/generator": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", + "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", + "dev": true, + "requires": { + "@babel/types": "^7.12.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", + "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "dev": true + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/traverse": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.5.tgz", + "integrity": "sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.5", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.12.5", + "@babel/types": "^7.12.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } } }, "@babel/helper-optimise-call-expression": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz", - "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", + "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", "dev": true, "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.10.4" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + } } }, "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", "dev": true }, "@babel/helper-regex": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.8.3.tgz", - "integrity": "sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.5.tgz", + "integrity": "sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==", "dev": true, "requires": { - "lodash": "^4.17.13" + "lodash": "^4.17.19" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + } } }, "@babel/helper-remap-async-to-generator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.8.3.tgz", - "integrity": "sha512-kgwDmw4fCg7AVgS4DukQR/roGp+jP+XluJE5hsRZwxCYGg+Rv9wSGErDWhlI90FODdYfd4xG4AQRiMDjjN0GzA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz", + "integrity": "sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.8.3", - "@babel/helper-wrap-function": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.8.3", - "@babel/types": "^7.8.3" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-wrap-function": "^7.10.4", + "@babel/types": "^7.12.1" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + } } }, "@babel/helper-replace-supers": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz", - "integrity": "sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz", + "integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==", "dev": true, "requires": { - "@babel/helper-member-expression-to-functions": "^7.8.3", - "@babel/helper-optimise-call-expression": "^7.8.3", - "@babel/traverse": "^7.8.6", - "@babel/types": "^7.8.6" + "@babel/helper-member-expression-to-functions": "^7.12.1", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/traverse": "^7.12.5", + "@babel/types": "^7.12.5" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/generator": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", + "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", + "dev": true, + "requires": { + "@babel/types": "^7.12.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", + "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "dev": true + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/traverse": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.5.tgz", + "integrity": "sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.5", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.12.5", + "@babel/types": "^7.12.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } } }, "@babel/helper-simple-access": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz", - "integrity": "sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz", + "integrity": "sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==", "dev": true, "requires": { - "@babel/template": "^7.8.3", - "@babel/types": "^7.8.3" + "@babel/types": "^7.12.1" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + } } }, - "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz", + "integrity": "sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==", "dev": true, "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.12.1" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + } } }, "@babel/helper-validator-identifier": { @@ -809,27 +1745,283 @@ "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==", "dev": true }, + "@babel/helper-validator-option": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.1.tgz", + "integrity": "sha512-YpJabsXlJVWP0USHjnC/AQDTLlZERbON577YUVO/wLpqyj6HAtVYnWaQaN0iUN+1/tWn3c+uKKXjRut5115Y2A==", + "dev": true + }, "@babel/helper-wrap-function": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz", - "integrity": "sha512-LACJrbUET9cQDzb6kG7EeD7+7doC3JNvUgTEQOx2qaO1fKlzE/Bf05qs9w1oXQMmXlPO65lC3Tq9S6gZpTErEQ==", + "version": "7.12.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.12.3.tgz", + "integrity": "sha512-Cvb8IuJDln3rs6tzjW3Y8UeelAOdnpB8xtQ4sme2MSZ9wOxrbThporC0y/EtE16VAtoyEfLM404Xr1e0OOp+ow==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.8.3", - "@babel/types": "^7.8.3" + "@babel/helper-function-name": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/generator": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", + "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", + "dev": true, + "requires": { + "@babel/types": "^7.12.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", + "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "dev": true + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/traverse": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.5.tgz", + "integrity": "sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.5", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.12.5", + "@babel/types": "^7.12.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } } }, "@babel/helpers": { - "version": "7.9.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.2.tgz", - "integrity": "sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz", + "integrity": "sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==", "dev": true, "requires": { - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.9.0", - "@babel/types": "^7.9.0" + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.5", + "@babel/types": "^7.12.5" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/generator": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", + "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", + "dev": true, + "requires": { + "@babel/types": "^7.12.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", + "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "dev": true + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/traverse": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.5.tgz", + "integrity": "sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.5", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.12.5", + "@babel/types": "^7.12.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } } }, "@babel/highlight": { @@ -843,102 +2035,137 @@ "js-tokens": "^4.0.0" } }, - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==", - "dev": true - }, "@babel/plugin-proposal-async-generator-functions": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz", - "integrity": "sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.1.tgz", + "integrity": "sha512-d+/o30tJxFxrA1lhzJqiUcEJdI6jKlNregCv5bASeGf2Q4MXmnwH7viDo7nhx1/ohf09oaH8j1GVYG/e3Yqk6A==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-remap-async-to-generator": "^7.8.3", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.12.1", "@babel/plugin-syntax-async-generators": "^7.8.0" } }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz", - "integrity": "sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w==", + "@babel/plugin-proposal-class-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz", + "integrity": "sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz", + "integrity": "sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-dynamic-import": "^7.8.0" } }, - "@babel/plugin-proposal-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz", - "integrity": "sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q==", + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz", + "integrity": "sha512-6CThGf0irEkzujYS5LQcjBx8j/4aQGiVv7J9+2f7pGfxqyKh3WnmVJYW3hdrQjyksErMGBPQrCnHfOtna+WLbw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz", + "integrity": "sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.0" } }, - "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw==", + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz", + "integrity": "sha512-k8ZmVv0JU+4gcUGeCDZOGd0lCIamU/sMtIiX3UWnUc5yzgq6YUGyEolNYD+MLYKfSzgECPcqetVcJP9Afe/aCA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz", + "integrity": "sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" } }, "@babel/plugin-proposal-numeric-separator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.8.3.tgz", - "integrity": "sha512-jWioO1s6R/R+wEHizfaScNsAx+xKgwTLNXSh7tTC4Usj3ItsPEhYkEpU4h+lpnBwq7NBVOJXfO6cRFYcX69JUQ==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.5.tgz", + "integrity": "sha512-UiAnkKuOrCyjZ3sYNHlRlfuZJbBHknMQ9VMwVeX97Ofwx7RpD6gS2HfqTCh8KNUQgcOm8IKt103oR4KIjh7Q8g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" } }, "@babel/plugin-proposal-object-rest-spread": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.5.tgz", - "integrity": "sha512-VP2oXvAf7KCYTthbUHwBlewbl1Iq059f6seJGsxMizaCdgHIeczOr7FBqELhSqfkIl04Fi8okzWzl63UKbQmmg==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz", + "integrity": "sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.0", - "@babel/plugin-transform-parameters": "^7.9.5" + "@babel/plugin-transform-parameters": "^7.12.1" } }, "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz", + "integrity": "sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" } }, "@babel/plugin-proposal-optional-chaining": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz", - "integrity": "sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.1.tgz", + "integrity": "sha512-c2uRpY6WzaVDzynVY9liyykS+kVU+WRZPMPYpkelXH8KBt1oXoI89kPbZKKG/jDT5UK92FTW2fZkZaJhdiBabw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", "@babel/plugin-syntax-optional-chaining": "^7.8.0" } }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.8.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz", - "integrity": "sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A==", + "@babel/plugin-proposal-private-methods": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz", + "integrity": "sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.8.8", - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz", + "integrity": "sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-async-generators": { @@ -950,6 +2177,15 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz", + "integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, "@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", @@ -959,6 +2195,15 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, "@babel/plugin-syntax-json-strings": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", @@ -968,6 +2213,15 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, "@babel/plugin-syntax-nullish-coalescing-operator": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", @@ -978,12 +2232,12 @@ } }, "@babel/plugin-syntax-numeric-separator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz", - "integrity": "sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-object-rest-spread": { @@ -1014,386 +2268,453 @@ } }, "@babel/plugin-syntax-top-level-await": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.8.3.tgz", - "integrity": "sha512-kwj1j9lL/6Wd0hROD3b/OZZ7MSrZLqqn9RAZ5+cYYsflQ9HZBIKCUkr3+uL1MEJ1NePiUbf98jjiMQSv0NMR9g==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz", + "integrity": "sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-arrow-functions": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz", - "integrity": "sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz", + "integrity": "sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-async-to-generator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz", - "integrity": "sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz", + "integrity": "sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-remap-async-to-generator": "^7.8.3" + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.12.1" } }, "@babel/plugin-transform-block-scoped-functions": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz", - "integrity": "sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz", + "integrity": "sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-block-scoping": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz", - "integrity": "sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.1.tgz", + "integrity": "sha512-zJyAC9sZdE60r1nVQHblcfCj29Dh2Y0DOvlMkcqSo0ckqjiCwNiUezUKw+RjOCwGfpLRwnAeQ2XlLpsnGkvv9w==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "lodash": "^4.17.13" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-classes": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.9.5.tgz", - "integrity": "sha512-x2kZoIuLC//O5iA7PEvecB105o7TLzZo8ofBVhP79N+DO3jaX+KYfww9TQcfBEZD0nikNyYcGB1IKtRq36rdmg==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz", + "integrity": "sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.8.3", - "@babel/helper-define-map": "^7.8.3", - "@babel/helper-function-name": "^7.9.5", - "@babel/helper-optimise-call-expression": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-replace-supers": "^7.8.6", - "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-define-map": "^7.10.4", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.10.4", "globals": "^11.1.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", + "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "dev": true + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + } } }, "@babel/plugin-transform-computed-properties": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz", - "integrity": "sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz", + "integrity": "sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-destructuring": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.9.5.tgz", - "integrity": "sha512-j3OEsGel8nHL/iusv/mRd5fYZ3DrOxWC82x0ogmdN/vHfAP4MYw+AFKYanzWlktNwikKvlzUV//afBW5FTp17Q==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz", + "integrity": "sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-dotall-regex": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz", - "integrity": "sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz", + "integrity": "sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-create-regexp-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-duplicate-keys": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz", - "integrity": "sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz", + "integrity": "sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-exponentiation-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz", - "integrity": "sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz", + "integrity": "sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug==", "dev": true, "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-for-of": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.9.0.tgz", - "integrity": "sha512-lTAnWOpMwOXpyDx06N+ywmF3jNbafZEqZ96CGYabxHrxNX8l5ny7dt4bK/rGwAh9utyP2b2Hv7PlZh1AAS54FQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz", + "integrity": "sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-function-name": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz", - "integrity": "sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz", + "integrity": "sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", + "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "dev": true + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + } } }, "@babel/plugin-transform-literals": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz", - "integrity": "sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz", + "integrity": "sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-member-expression-literals": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.8.3.tgz", - "integrity": "sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz", + "integrity": "sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-modules-amd": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.0.tgz", - "integrity": "sha512-vZgDDF003B14O8zJy0XXLnPH4sg+9X5hFBBGN1V+B2rgrB+J2xIypSN6Rk9imB2hSTHQi5OHLrFWsZab1GMk+Q==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz", + "integrity": "sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.9.0", - "@babel/helper-plugin-utils": "^7.8.3", - "babel-plugin-dynamic-import-node": "^2.3.0" + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.0.tgz", - "integrity": "sha512-qzlCrLnKqio4SlgJ6FMMLBe4bySNis8DFn1VkGmOcxG9gqEyPIOzeQrA//u0HAKrWpJlpZbZMPB1n/OPa4+n8g==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz", + "integrity": "sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.9.0", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-simple-access": "^7.8.3", - "babel-plugin-dynamic-import-node": "^2.3.0" + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-simple-access": "^7.12.1", + "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.0.tgz", - "integrity": "sha512-FsiAv/nao/ud2ZWy4wFacoLOm5uxl0ExSQ7ErvP7jpoihLR6Cq90ilOFyX9UXct3rbtKsAiZ9kFt5XGfPe/5SQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz", + "integrity": "sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q==", "dev": true, "requires": { - "@babel/helper-hoist-variables": "^7.8.3", - "@babel/helper-module-transforms": "^7.9.0", - "@babel/helper-plugin-utils": "^7.8.3", - "babel-plugin-dynamic-import-node": "^2.3.0" + "@babel/helper-hoist-variables": "^7.10.4", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-validator-identifier": "^7.10.4", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + } } }, "@babel/plugin-transform-modules-umd": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.9.0.tgz", - "integrity": "sha512-uTWkXkIVtg/JGRSIABdBoMsoIeoHQHPTL0Y2E7xf5Oj7sLqwVsNXOkNk0VJc7vF0IMBsPeikHxFjGe+qmwPtTQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz", + "integrity": "sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.9.0", - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz", - "integrity": "sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz", + "integrity": "sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.8.3" + "@babel/helper-create-regexp-features-plugin": "^7.12.1" } }, "@babel/plugin-transform-new-target": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.8.3.tgz", - "integrity": "sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz", + "integrity": "sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-object-super": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz", - "integrity": "sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz", + "integrity": "sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-replace-supers": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1" } }, "@babel/plugin-transform-parameters": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.9.5.tgz", - "integrity": "sha512-0+1FhHnMfj6lIIhVvS4KGQJeuhe1GI//h5uptK4PvLt+BGBxsoUJbd3/IW002yk//6sZPlFgsG1hY6OHLcy6kA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz", + "integrity": "sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-property-literals": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz", - "integrity": "sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz", + "integrity": "sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-regenerator": { - "version": "7.8.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.7.tgz", - "integrity": "sha512-TIg+gAl4Z0a3WmD3mbYSk+J9ZUH6n/Yc57rtKRnlA/7rcCvpekHXe0CMZHP1gYp7/KLe9GHTuIba0vXmls6drA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz", + "integrity": "sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng==", "dev": true, "requires": { "regenerator-transform": "^0.14.2" } }, "@babel/plugin-transform-reserved-words": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.8.3.tgz", - "integrity": "sha512-mwMxcycN3omKFDjDQUl+8zyMsBfjRFr0Zn/64I41pmjv4NJuqcYlEtezwYtw9TFd9WR1vN5kiM+O0gMZzO6L0A==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz", + "integrity": "sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz", - "integrity": "sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w==", + "@babel/plugin-transform-runtime": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.12.1.tgz", + "integrity": "sha512-Ac/H6G9FEIkS2tXsZjL4RAdS3L3WHxci0usAnz7laPWUmFiGtj7tIASChqKZMHTSQTQY6xDbOq+V1/vIq3QrWg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz", - "integrity": "sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz", - "integrity": "sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-regex": "^7.8.3" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz", - "integrity": "sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz", - "integrity": "sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz", - "integrity": "sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/preset-env": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.9.0.tgz", - "integrity": "sha512-712DeRXT6dyKAM/FMbQTV/FvRCms2hPCx+3weRjZ8iQVQWZejWWk1wwG6ViWMyqb/ouBbGOl5b6aCk0+j1NmsQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.9.0", - "@babel/helper-compilation-targets": "^7.8.7", - "@babel/helper-module-imports": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-proposal-async-generator-functions": "^7.8.3", - "@babel/plugin-proposal-dynamic-import": "^7.8.3", - "@babel/plugin-proposal-json-strings": "^7.8.3", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-proposal-numeric-separator": "^7.8.3", - "@babel/plugin-proposal-object-rest-spread": "^7.9.0", - "@babel/plugin-proposal-optional-catch-binding": "^7.8.3", - "@babel/plugin-proposal-optional-chaining": "^7.9.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.8.3", - "@babel/plugin-syntax-async-generators": "^7.8.0", - "@babel/plugin-syntax-dynamic-import": "^7.8.0", - "@babel/plugin-syntax-json-strings": "^7.8.0", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", - "@babel/plugin-syntax-numeric-separator": "^7.8.0", - "@babel/plugin-syntax-object-rest-spread": "^7.8.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.0", - "@babel/plugin-syntax-top-level-await": "^7.8.3", - "@babel/plugin-transform-arrow-functions": "^7.8.3", - "@babel/plugin-transform-async-to-generator": "^7.8.3", - "@babel/plugin-transform-block-scoped-functions": "^7.8.3", - "@babel/plugin-transform-block-scoping": "^7.8.3", - "@babel/plugin-transform-classes": "^7.9.0", - "@babel/plugin-transform-computed-properties": "^7.8.3", - "@babel/plugin-transform-destructuring": "^7.8.3", - "@babel/plugin-transform-dotall-regex": "^7.8.3", - "@babel/plugin-transform-duplicate-keys": "^7.8.3", - "@babel/plugin-transform-exponentiation-operator": "^7.8.3", - "@babel/plugin-transform-for-of": "^7.9.0", - "@babel/plugin-transform-function-name": "^7.8.3", - "@babel/plugin-transform-literals": "^7.8.3", - "@babel/plugin-transform-member-expression-literals": "^7.8.3", - "@babel/plugin-transform-modules-amd": "^7.9.0", - "@babel/plugin-transform-modules-commonjs": "^7.9.0", - "@babel/plugin-transform-modules-systemjs": "^7.9.0", - "@babel/plugin-transform-modules-umd": "^7.9.0", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.8.3", - "@babel/plugin-transform-new-target": "^7.8.3", - "@babel/plugin-transform-object-super": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.8.7", - "@babel/plugin-transform-property-literals": "^7.8.3", - "@babel/plugin-transform-regenerator": "^7.8.7", - "@babel/plugin-transform-reserved-words": "^7.8.3", - "@babel/plugin-transform-shorthand-properties": "^7.8.3", - "@babel/plugin-transform-spread": "^7.8.3", - "@babel/plugin-transform-sticky-regex": "^7.8.3", - "@babel/plugin-transform-template-literals": "^7.8.3", - "@babel/plugin-transform-typeof-symbol": "^7.8.4", - "@babel/plugin-transform-unicode-regex": "^7.8.3", - "@babel/preset-modules": "^0.1.3", - "@babel/types": "^7.9.0", - "browserslist": "^4.9.1", - "core-js-compat": "^3.6.2", - "invariant": "^2.2.2", - "levenary": "^1.1.1", - "semver": "^5.5.0" + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "resolve": "^1.8.1", + "semver": "^5.5.1" }, "dependencies": { "semver": { @@ -1404,10 +2725,181 @@ } } }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz", + "integrity": "sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz", + "integrity": "sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.1.tgz", + "integrity": "sha512-CiUgKQ3AGVk7kveIaPEET1jNDhZZEl1RPMWdTBE1799bdz++SwqDHStmxfCtDfBhQgCl38YRiSnrMuUMZIWSUQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-regex": "^7.10.4" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz", + "integrity": "sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.1.tgz", + "integrity": "sha512-EPGgpGy+O5Kg5pJFNDKuxt9RdmTgj5sgrus2XVeMp/ZIbOESadgILUbm50SNpghOh3/6yrbsH+NB5+WJTmsA7Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz", + "integrity": "sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz", + "integrity": "sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/preset-env": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.1.tgz", + "integrity": "sha512-H8kxXmtPaAGT7TyBvSSkoSTUK6RHh61So05SyEbpmr0MCZrsNYn7mGMzzeYoOUCdHzww61k8XBft2TaES+xPLg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.12.1", + "@babel/helper-compilation-targets": "^7.12.1", + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-validator-option": "^7.12.1", + "@babel/plugin-proposal-async-generator-functions": "^7.12.1", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-proposal-dynamic-import": "^7.12.1", + "@babel/plugin-proposal-export-namespace-from": "^7.12.1", + "@babel/plugin-proposal-json-strings": "^7.12.1", + "@babel/plugin-proposal-logical-assignment-operators": "^7.12.1", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", + "@babel/plugin-proposal-numeric-separator": "^7.12.1", + "@babel/plugin-proposal-object-rest-spread": "^7.12.1", + "@babel/plugin-proposal-optional-catch-binding": "^7.12.1", + "@babel/plugin-proposal-optional-chaining": "^7.12.1", + "@babel/plugin-proposal-private-methods": "^7.12.1", + "@babel/plugin-proposal-unicode-property-regex": "^7.12.1", + "@babel/plugin-syntax-async-generators": "^7.8.0", + "@babel/plugin-syntax-class-properties": "^7.12.1", + "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.0", + "@babel/plugin-syntax-top-level-await": "^7.12.1", + "@babel/plugin-transform-arrow-functions": "^7.12.1", + "@babel/plugin-transform-async-to-generator": "^7.12.1", + "@babel/plugin-transform-block-scoped-functions": "^7.12.1", + "@babel/plugin-transform-block-scoping": "^7.12.1", + "@babel/plugin-transform-classes": "^7.12.1", + "@babel/plugin-transform-computed-properties": "^7.12.1", + "@babel/plugin-transform-destructuring": "^7.12.1", + "@babel/plugin-transform-dotall-regex": "^7.12.1", + "@babel/plugin-transform-duplicate-keys": "^7.12.1", + "@babel/plugin-transform-exponentiation-operator": "^7.12.1", + "@babel/plugin-transform-for-of": "^7.12.1", + "@babel/plugin-transform-function-name": "^7.12.1", + "@babel/plugin-transform-literals": "^7.12.1", + "@babel/plugin-transform-member-expression-literals": "^7.12.1", + "@babel/plugin-transform-modules-amd": "^7.12.1", + "@babel/plugin-transform-modules-commonjs": "^7.12.1", + "@babel/plugin-transform-modules-systemjs": "^7.12.1", + "@babel/plugin-transform-modules-umd": "^7.12.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.12.1", + "@babel/plugin-transform-new-target": "^7.12.1", + "@babel/plugin-transform-object-super": "^7.12.1", + "@babel/plugin-transform-parameters": "^7.12.1", + "@babel/plugin-transform-property-literals": "^7.12.1", + "@babel/plugin-transform-regenerator": "^7.12.1", + "@babel/plugin-transform-reserved-words": "^7.12.1", + "@babel/plugin-transform-shorthand-properties": "^7.12.1", + "@babel/plugin-transform-spread": "^7.12.1", + "@babel/plugin-transform-sticky-regex": "^7.12.1", + "@babel/plugin-transform-template-literals": "^7.12.1", + "@babel/plugin-transform-typeof-symbol": "^7.12.1", + "@babel/plugin-transform-unicode-escapes": "^7.12.1", + "@babel/plugin-transform-unicode-regex": "^7.12.1", + "@babel/preset-modules": "^0.1.3", + "@babel/types": "^7.12.1", + "core-js-compat": "^3.6.2", + "semver": "^5.5.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.12.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", + "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, "@babel/preset-modules": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.3.tgz", - "integrity": "sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", + "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", @@ -1418,62 +2910,14 @@ } }, "@babel/runtime": { - "version": "7.9.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz", - "integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.1.tgz", + "integrity": "sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA==", "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/traverse": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.5.tgz", - "integrity": "sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.5", - "@babel/helper-function-name": "^7.9.5", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.9.0", - "@babel/types": "^7.9.5", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - }, - "dependencies": { - "@babel/generator": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.5.tgz", - "integrity": "sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ==", - "dev": true, - "requires": { - "@babel/types": "^7.9.5", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, "@babel/types": { "version": "7.9.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", @@ -1492,128 +2936,119 @@ "dev": true }, "@jsdevtools/coverage-istanbul-loader": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.3.tgz", - "integrity": "sha512-TAdNkeGB5Fe4Og+ZkAr1Kvn9by2sfL44IAHFtxlh1BA1XJ5cLpO9iSNki5opWESv3l3vSHsZ9BNKuqFKbEbFaA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz", + "integrity": "sha512-EUCPEkaRPvmHjWAAZkWMT7JDzpw7FKB00WTISaiXsbNOd5hCHg77XLA8sLYLFDo1zepYLo2w7GstN8YBqRXZfA==", "dev": true, "requires": { "convert-source-map": "^1.7.0", - "istanbul-lib-instrument": "^4.0.1", - "loader-utils": "^1.4.0", + "istanbul-lib-instrument": "^4.0.3", + "loader-utils": "^2.0.0", "merge-source-map": "^1.1.0", - "schema-utils": "^2.6.4" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - } + "schema-utils": "^2.7.0" } }, "@ngtools/webpack": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-9.1.1.tgz", - "integrity": "sha512-4RPlk6aIlYhk9isTvXbMaA2G0LhxOzcZ+2iG7zV9Yj/Vm8+lrRexpQ/kC/Dh0GI/oCtKIkVpUzx5LTozYeTLdQ==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-11.0.1.tgz", + "integrity": "sha512-z62qQ4J5LhDxW68HjYYCRo+sDK/5yHwX4fCCY2iXngyTtA5cQbGI5WXr3+9B4foX64ft5WvV0WJkx8mjE/VR6w==", "dev": true, "requires": { - "@angular-devkit/core": "9.1.1", - "enhanced-resolve": "4.1.1", - "rxjs": "6.5.4", - "webpack-sources": "1.4.3" + "@angular-devkit/core": "11.0.1", + "enhanced-resolve": "5.3.1", + "webpack-sources": "2.0.1" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", + "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.3", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", + "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.3", + "fastq": "^1.6.0" + } + }, + "@npmcli/move-file": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz", + "integrity": "sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==", + "dev": true, + "requires": { + "mkdirp": "^1.0.4" }, "dependencies": { - "rxjs": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", - "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true } } }, "@schematics/angular": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-9.1.1.tgz", - "integrity": "sha512-V0DcDNgHQ2YR+PGZI6+pf/mUNNxt5SusShkZ1PbwIMk/HUQpzEGkLjm3v1Jw9eIZKiuDx615GNU1xDzQ/KyNRQ==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-11.0.1.tgz", + "integrity": "sha512-cYq3NhFn4DLSXXtbYYU2w0sginkMfN1w7pXjZLT/+etXXbtANQAXSPrPrDQql004ZNMbuDKuC0aoXjv8hgXOfw==", "dev": true, "requires": { - "@angular-devkit/core": "9.1.1", - "@angular-devkit/schematics": "9.1.1" + "@angular-devkit/core": "11.0.1", + "@angular-devkit/schematics": "11.0.1", + "jsonc-parser": "2.3.1" } }, "@schematics/update": { - "version": "0.901.1", - "resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.901.1.tgz", - "integrity": "sha512-g5B8hjXKCbUNLKCkWQxc5r2D9lOQXRTLZJNIqva+l/ck0AML5MpelxkqQId9ZGLfQqfFk/XjdSHRWgcmUs1WnA==", + "version": "0.1100.1", + "resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.1100.1.tgz", + "integrity": "sha512-EVcqdM/d5rC5L1UYnwhFMk/TjHlNgL5LGfroE13C38A+WpKKJquAjgOQLj4nPvJ5csdEZqn3Sui9yeEWc3hklQ==", "dev": true, "requires": { - "@angular-devkit/core": "9.1.1", - "@angular-devkit/schematics": "9.1.1", + "@angular-devkit/core": "11.0.1", + "@angular-devkit/schematics": "11.0.1", "@yarnpkg/lockfile": "1.1.0", "ini": "1.3.5", "npm-package-arg": "^8.0.0", "pacote": "9.5.12", - "rxjs": "6.5.4", - "semver": "7.1.3", + "semver": "7.3.2", "semver-intersect": "1.4.0" - }, - "dependencies": { - "rxjs": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", - "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - } } }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", - "dev": true - }, "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", "dev": true, "requires": { - "@types/events": "*", "@types/minimatch": "*", "@types/node": "*" } }, "@types/jasmine": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.10.tgz", - "integrity": "sha512-3F8qpwBAiVc5+HPJeXJpbrl+XjawGmciN5LgiO7Gv1pl1RHtjoMNqZpqEksaPJW05ViKe8snYInRs6xB25Xdew==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.1.tgz", + "integrity": "sha512-eeSCVhBsgwHNS1FmaMu4zrLxfykCTWJMLFZv7lmyrZQjw7foUUXoPu4GukSN9v7JvUw7X+/aDH3kCaymirBSTg==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", + "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", "dev": true }, "@types/minimatch": { @@ -1628,10 +3063,21 @@ "integrity": "sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g==", "dev": true }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "@types/platform": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/platform/-/platform-1.3.3.tgz", + "integrity": "sha512-1fuOulBHWIxAPLBtLms+UtbeRDt6rL7gP5R+Yugfzdg+poCLxXqXTE8i+FpYeiytGRLUEtnFkjsY/j+usbQBqw==" + }, "@types/q": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", - "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz", + "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==", "dev": true }, "@types/selenium-webdriver": { @@ -1647,9 +3093,9 @@ "dev": true }, "@types/webpack-sources": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.7.tgz", - "integrity": "sha512-XyaHrJILjK1VHVC4aVlKsdNN5KBTwufMb43cQs+flGxtPAf/1Qwl8+Q0tp5BwEGaI8D6XT1L+9bSWXckgkjTLw==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.8.tgz", + "integrity": "sha512-JHB2/xZlXOjzjBB6fMOpH1eQAfsrpqVVIbneE0Rok16WXwFaznaI5vfg75U5WgGJm7V9W1c4xeRQDjX/zwvghA==", "dev": true, "requires": { "@types/node": "*", @@ -1666,178 +3112,177 @@ } }, "@webassemblyjs/ast": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", - "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", + "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", "dev": true, "requires": { - "@webassemblyjs/helper-module-context": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/wast-parser": "1.8.5" + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0" } }, "@webassemblyjs/floating-point-hex-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", - "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", + "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", "dev": true }, "@webassemblyjs/helper-api-error": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", - "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", + "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", "dev": true }, "@webassemblyjs/helper-buffer": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", - "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", + "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", "dev": true }, "@webassemblyjs/helper-code-frame": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", - "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", + "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", "dev": true, "requires": { - "@webassemblyjs/wast-printer": "1.8.5" + "@webassemblyjs/wast-printer": "1.9.0" } }, "@webassemblyjs/helper-fsm": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", - "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", + "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", "dev": true }, "@webassemblyjs/helper-module-context": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", - "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", + "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "mamacro": "^0.0.3" + "@webassemblyjs/ast": "1.9.0" } }, "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", - "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", + "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", "dev": true }, "@webassemblyjs/helper-wasm-section": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", - "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", + "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0" } }, "@webassemblyjs/ieee754": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", - "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", + "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", "dev": true, "requires": { "@xtuc/ieee754": "^1.2.0" } }, "@webassemblyjs/leb128": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", - "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", + "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", "dev": true, "requires": { "@xtuc/long": "4.2.2" } }, "@webassemblyjs/utf8": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", - "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", + "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", "dev": true }, "@webassemblyjs/wasm-edit": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", - "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", + "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/helper-wasm-section": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5", - "@webassemblyjs/wasm-opt": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5", - "@webassemblyjs/wast-printer": "1.8.5" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/helper-wasm-section": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-opt": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/wast-printer": "1.9.0" } }, "@webassemblyjs/wasm-gen": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", - "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", + "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/ieee754": "1.8.5", - "@webassemblyjs/leb128": "1.8.5", - "@webassemblyjs/utf8": "1.8.5" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" } }, "@webassemblyjs/wasm-opt": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", - "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", + "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0" } }, "@webassemblyjs/wasm-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", - "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", + "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-api-error": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/ieee754": "1.8.5", - "@webassemblyjs/leb128": "1.8.5", - "@webassemblyjs/utf8": "1.8.5" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" } }, "@webassemblyjs/wast-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", - "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", + "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/floating-point-hex-parser": "1.8.5", - "@webassemblyjs/helper-api-error": "1.8.5", - "@webassemblyjs/helper-code-frame": "1.8.5", - "@webassemblyjs/helper-fsm": "1.8.5", + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/floating-point-hex-parser": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-code-frame": "1.9.0", + "@webassemblyjs/helper-fsm": "1.9.0", "@xtuc/long": "4.2.2" } }, "@webassemblyjs/wast-printer": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", - "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", + "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/wast-parser": "1.8.5", + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0", "@xtuc/long": "4.2.2" } }, @@ -1869,6 +3314,12 @@ "through": ">=2.2.7 <3" } }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "dev": true + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -1880,15 +3331,25 @@ } }, "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", "dev": true }, + "adjust-sourcemap-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-3.0.0.tgz", + "integrity": "sha512-YBrGyT2/uVQ/c6Rr+t6ZJXniY03YtHGMJQYal368burRGYKqhx9qGTWqcBU5s1CwYY9E/ri63RYyG1IacMZtqw==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + } + }, "adm-zip": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.14.tgz", - "integrity": "sha512-/9aQCnQHF+0IiCl0qhXoK7qs//SwYE7zX8lsr/DNk1BRAHYxeLZPL4pguwK29gUEqasYQjqPtEpDRSWEkdHn9g==", + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", + "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==", "dev": true }, "after": { @@ -1916,9 +3377,9 @@ } }, "aggregate-error": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", - "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, "requires": { "clean-stack": "^2.0.0", @@ -1926,9 +3387,9 @@ } }, "ajv": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", - "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -1944,9 +3405,9 @@ "dev": true }, "ajv-keywords": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", - "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true }, "alphanum-sort": { @@ -1956,9 +3417,9 @@ "dev": true }, "ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true }, "ansi-escapes": { @@ -1977,9 +3438,9 @@ "dev": true }, "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, "ansi-styles": { @@ -2002,20 +3463,11 @@ } }, "app-root-path": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz", - "integrity": "sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz", + "integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==", "dev": true }, - "append-transform": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", - "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", - "dev": true, - "requires": { - "default-require-extensions": "^2.0.0" - } - }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -2047,6 +3499,12 @@ "commander": "^2.11.0" } }, + "arity-n": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arity-n/-/arity-n-1.0.4.tgz", + "integrity": "sha1-2edrEXM+CFacCEeuezmyhgswt0U=", + "dev": true + }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -2072,13 +3530,10 @@ "dev": true }, "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true }, "array-uniq": { "version": "1.0.3", @@ -2104,12 +3559,6 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", - "dev": true - }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -2120,14 +3569,23 @@ } }, "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", "dev": true, "requires": { "bn.js": "^4.0.0", "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } } }, "assert": { @@ -2209,18 +3667,18 @@ "dev": true }, "autoprefixer": { - "version": "9.7.4", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.4.tgz", - "integrity": "sha512-g0Ya30YrMBAEZk60lp+qfX5YQllG+S5W3GYCFvyHTvhOki0AEQJLPEcIuGRsqVwLi8FvXPVtwTGhfr38hVpm0g==", + "version": "9.8.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", + "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==", "dev": true, "requires": { - "browserslist": "^4.8.3", - "caniuse-lite": "^1.0.30001020", - "chalk": "^2.4.2", + "browserslist": "^4.12.0", + "caniuse-lite": "^1.0.30001109", + "colorette": "^1.2.1", "normalize-range": "^0.1.2", "num2fraction": "^1.2.2", - "postcss": "^7.0.26", - "postcss-value-parser": "^4.0.2" + "postcss": "^7.0.32", + "postcss-value-parser": "^4.1.0" } }, "aws-sign2": { @@ -2230,9 +3688,9 @@ "dev": true }, "aws4": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", - "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, "axobject-query": { @@ -2245,15 +3703,16 @@ } }, "babel-loader": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.6.tgz", - "integrity": "sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz", + "integrity": "sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==", "dev": true, "requires": { - "find-cache-dir": "^2.0.0", - "loader-utils": "^1.0.2", - "mkdirp": "^0.5.1", - "pify": "^4.0.1" + "find-cache-dir": "^2.1.0", + "loader-utils": "^1.4.0", + "mkdirp": "^0.5.3", + "pify": "^4.0.1", + "schema-utils": "^2.6.5" }, "dependencies": { "find-cache-dir": { @@ -2290,9 +3749,9 @@ } }, "babel-plugin-dynamic-import-node": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", - "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", "dev": true, "requires": { "object.assign": "^4.1.0" @@ -2362,25 +3821,31 @@ "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, "base64-arraybuffer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=", "dev": true }, "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true }, "base64id": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", - "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", "dev": true }, "batch": { @@ -2414,9 +3879,9 @@ "dev": true }, "binary-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", - "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "dev": true }, "blob": { @@ -2441,9 +3906,9 @@ "dev": true }, "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", + "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==", "dev": true }, "body-parser": { @@ -2484,12 +3949,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true } } }, @@ -2576,28 +4035,49 @@ } }, "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", "dev": true, "requires": { - "bn.js": "^4.1.0", + "bn.js": "^5.0.0", "randombytes": "^2.0.1" } }, "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", "dev": true, "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "browserify-zlib": { @@ -2610,15 +4090,16 @@ } }, "browserslist": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.11.1.tgz", - "integrity": "sha512-DCTr3kDrKEYNw6Jb9HFxVLQNaue8z+0ZfRBRjmCunKDEXEBajKDj2Y+Uelg+Pi29OnvaSGwjOsnRyNEkXzHg5g==", + "version": "4.14.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz", + "integrity": "sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001038", - "electron-to-chromium": "^1.3.390", - "node-releases": "^1.1.53", - "pkg-up": "^2.0.0" + "caniuse-lite": "^1.0.30001157", + "colorette": "^1.2.1", + "electron-to-chromium": "^1.3.591", + "escalade": "^3.1.1", + "node-releases": "^1.1.66" } }, "browserstack": { @@ -2684,27 +4165,27 @@ "dev": true }, "cacache": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.0.tgz", - "integrity": "sha512-L0JpXHhplbJSiDGzyJJnJCTL7er7NzbBgxzVqLswEb4bO91Zbv17OUMuUeu/q0ZwKn3V+1HM4wb9tO4eVE/K8g==", + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.5.tgz", + "integrity": "sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A==", "dev": true, "requires": { - "chownr": "^1.1.2", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "glob": "^7.1.4", "infer-owner": "^1.0.4", - "lru-cache": "^5.1.1", + "lru-cache": "^6.0.0", "minipass": "^3.1.1", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.2", "mkdirp": "^1.0.3", - "move-concurrently": "^1.0.1", - "p-map": "^3.0.0", + "p-map": "^4.0.0", "promise-inflight": "^1.0.1", - "rimraf": "^2.7.1", + "rimraf": "^3.0.2", "ssri": "^8.0.0", - "tar": "^6.0.1", + "tar": "^6.0.2", "unique-filename": "^1.1.1" }, "dependencies": { @@ -2713,15 +4194,6 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } } } }, @@ -2740,6 +4212,24 @@ "to-object-path": "^0.3.0", "union-value": "^1.0.0", "unset-value": "^1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "call-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", + "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.0" } }, "caller-callsite": { @@ -2773,9 +4263,9 @@ "dev": true }, "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", "dev": true }, "caniuse-api": { @@ -2791,9 +4281,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001042", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001042.tgz", - "integrity": "sha512-igMQ4dlqnf4tWv0xjaaE02op9AJ2oQzXKjWf4EuAHFN694Uo9/EfPVIPJcmn2WkU9RqozCxx5e2KPcVClHDbDw==", + "version": "1.0.30001158", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001158.tgz", + "integrity": "sha512-s5loVYY+yKpuVA3HyW8BarzrtJvwHReuzugQXlv1iR3LKSReoFXRm86mT6hT7PEF5RxW+XQZg+6nYjlywYzQ+g==", "dev": true }, "canonical-path": { @@ -2826,9 +4316,9 @@ "dev": true }, "chokidar": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz", - "integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", + "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", "dev": true, "requires": { "anymatch": "~3.1.1", @@ -2838,24 +4328,13 @@ "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.3.0" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } + "readdirp": "~3.5.0" } }, "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "dev": true }, "chrome-trace-event": { @@ -2865,6 +4344,14 @@ "dev": true, "requires": { "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "cipher-base": { @@ -2903,6 +4390,12 @@ "requires": { "is-descriptor": "^0.1.0" } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -2922,49 +4415,72 @@ } }, "cli-spinners": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.3.0.tgz", - "integrity": "sha512-Xs2Hf2nzrvJMFKimOR7YR0QwZ8fc0u98kdtwN1eNAZzNQgH3vK2pXzff6GJtKh7S5hoJ87ECiAiZFS2fb5Ii2w==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.5.0.tgz", + "integrity": "sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ==", "dev": true }, "cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "dev": true }, "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "dev": true, "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" } } } }, "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", "dev": true }, "clone-deep": { @@ -2989,29 +4505,40 @@ "q": "^1.1.2" } }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, "codelyzer": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-5.2.2.tgz", - "integrity": "sha512-jB4FZ1Sx7kZhvZVdf+N2BaKTdrrNZOL0Bj10RRfrhHrb3zEvXjJvvq298JPMJAiyiCS/v4zs1QlGU0ip7xGqeA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-6.0.1.tgz", + "integrity": "sha512-cOyGQgMdhnRYtW2xrJUNrNYDjEgwQ+BrE2y93Bwz3h4DJ6vJRLfupemU5N3pbYsUlBHJf0u1j1UGk+NLW4d97g==", "dev": true, "requires": { - "app-root-path": "^2.2.1", + "@angular/compiler": "9.0.0", + "@angular/core": "9.0.0", + "app-root-path": "^3.0.0", "aria-query": "^3.0.0", "axobject-query": "2.0.2", "css-selector-tokenizer": "^0.7.1", "cssauron": "^1.4.0", "damerau-levenshtein": "^1.0.4", + "rxjs": "^6.5.3", "semver-dsl": "^1.0.1", "source-map": "^0.5.7", - "sprintf-js": "^1.1.2" + "sprintf-js": "^1.1.2", + "tslib": "^1.10.0", + "zone.js": "~0.10.3" }, "dependencies": { + "@angular/compiler": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-9.0.0.tgz", + "integrity": "sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ==", + "dev": true + }, + "@angular/core": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-9.0.0.tgz", + "integrity": "sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w==", + "dev": true + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -3023,6 +4550,12 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true } } }, @@ -3037,13 +4570,13 @@ } }, "color": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz", - "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz", + "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==", "dev": true, "requires": { "color-convert": "^1.9.1", - "color-string": "^1.5.2" + "color-string": "^1.5.4" } }, "color-convert": { @@ -3062,15 +4595,21 @@ "dev": true }, "color-string": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", - "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz", + "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==", "dev": true, "requires": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, + "colorette": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", + "dev": true + }, "colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", @@ -3098,12 +4637,6 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, - "compare-versions": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", - "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", - "dev": true - }, "component-bind": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", @@ -3122,6 +4655,15 @@ "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", "dev": true }, + "compose-function": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz", + "integrity": "sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8=", + "dev": true, + "requires": { + "arity-n": "^1.0.4" + } + }, "compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -3296,121 +4838,58 @@ "dev": true }, "copy-webpack-plugin": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.1.1.tgz", - "integrity": "sha512-P15M5ZC8dyCjQHWwd4Ia/dm0SgVvZJMYeykVIVYXbGyqO4dWB5oyPHp9i7wjwo5LhtlhKbiBCdS2NvM07Wlybg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-6.2.1.tgz", + "integrity": "sha512-VH2ZTMIBsx4p++Lmpg77adZ0KUyM5gFR/9cuTrbneNnJlcQXUFvsNariPqq2dq2kV3F2skHiDGPQCyKWy1+U0Q==", "dev": true, "requires": { - "cacache": "^12.0.3", - "find-cache-dir": "^2.1.0", - "glob-parent": "^3.1.0", - "globby": "^7.1.1", - "is-glob": "^4.0.1", - "loader-utils": "^1.2.3", - "minimatch": "^3.0.4", + "cacache": "^15.0.5", + "fast-glob": "^3.2.4", + "find-cache-dir": "^3.3.1", + "glob-parent": "^5.1.1", + "globby": "^11.0.1", + "loader-utils": "^2.0.0", "normalize-path": "^3.0.0", - "p-limit": "^2.2.1", - "schema-utils": "^1.0.0", - "serialize-javascript": "^2.1.2", - "webpack-log": "^2.0.0" + "p-limit": "^3.0.2", + "schema-utils": "^3.0.0", + "serialize-javascript": "^5.0.1", + "webpack-sources": "^1.4.3" }, "dependencies": { - "cacache": { - "version": "12.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", - "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", - "dev": true, - "requires": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } - }, - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - }, "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", "dev": true, "requires": { "p-try": "^2.0.0" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", "dev": true, "requires": { - "glob": "^7.1.3" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1" + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" } } } @@ -3421,12 +4900,12 @@ "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==" }, "core-js-compat": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz", - "integrity": "sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.7.0.tgz", + "integrity": "sha512-V8yBI3+ZLDVomoWICO6kq/CD28Y4r1M7CWeO4AGpMdMfseu8bkSubBmUPySMGKRTS+su4XQ07zUkAsiu9FCWTg==", "dev": true, "requires": { - "browserslist": "^4.8.5", + "browserslist": "^4.14.6", "semver": "7.0.0" }, "dependencies": { @@ -3457,13 +4936,21 @@ } }, "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", "dev": true, "requires": { "bn.js": "^4.1.0", - "elliptic": "^6.0.0" + "elliptic": "^6.5.3" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } } }, "create-hash": { @@ -3569,6 +5056,57 @@ "timsort": "^0.3.0" } }, + "css-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.0.0.tgz", + "integrity": "sha512-9g35eXRBgjvswyJWoqq/seWp+BOxvUl8IinVNTsUBFFxtwfEYvlmEn6ciyn0liXGbGh5HyJjPGCuobDSfqMIVg==", + "dev": true, + "requires": { + "camelcase": "^6.1.0", + "cssesc": "^3.0.0", + "icss-utils": "^5.0.0", + "loader-utils": "^2.0.0", + "postcss": "^8.1.1", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^3.0.0", + "semver": "^7.3.2" + }, + "dependencies": { + "postcss": { + "version": "8.1.7", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.1.7.tgz", + "integrity": "sha512-llCQW1Pz4MOPwbZLmOddGM9eIJ8Bh7SZ2Oj5sxZva77uVaotYDsYTch1WBTNu7fUY0fpWp0fdt7uW40D4sRiiQ==", + "dev": true, + "requires": { + "colorette": "^1.2.1", + "line-column": "^1.0.2", + "nanoid": "^3.1.16", + "source-map": "^0.6.1" + } + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "css-parse": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz", @@ -3597,14 +5135,13 @@ "dev": true }, "css-selector-tokenizer": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.2.tgz", - "integrity": "sha512-yj856NGuAymN6r8bn8/Jl46pR+OC3eEvAhfGYDUe7YPtTPAYrSSw4oAniZ9Y8T5B92hjhwTBLUen0/vKPxf6pw==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz", + "integrity": "sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg==", "dev": true, "requires": { "cssesc": "^3.0.0", - "fastparse": "^1.1.2", - "regexpu-core": "^4.6.0" + "fastparse": "^1.1.2" } }, "css-tree": { @@ -3626,9 +5163,9 @@ } }, "css-what": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz", - "integrity": "sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", "dev": true }, "cssauron": { @@ -3724,28 +5261,28 @@ "dev": true }, "csso": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.0.3.tgz", - "integrity": "sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.1.1.tgz", + "integrity": "sha512-Rvq+e1e0TFB8E8X+8MQjHSY6vtol45s5gxtLI/018UsAn2IBMmwNEZRM/h+HVnAJRHjasLIKKUO3uvoMM28LvA==", "dev": true, "requires": { - "css-tree": "1.0.0-alpha.39" + "css-tree": "^1.0.0" }, "dependencies": { "css-tree": { - "version": "1.0.0-alpha.39", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.39.tgz", - "integrity": "sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.1.tgz", + "integrity": "sha512-WroX+2MvsYcRGP8QA0p+rxzOniT/zpAoQ/DTKDSJzh5T3IQKUkFHeIIfgIapm2uaP178GWY3Mime1qbk8GO/tA==", "dev": true, "requires": { - "mdn-data": "2.0.6", + "mdn-data": "2.0.12", "source-map": "^0.6.1" } }, "mdn-data": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz", - "integrity": "sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.12.tgz", + "integrity": "sha512-ULbAlgzVb8IqZ0Hsxm6hHSlQl3Jckst2YEQS7fODu9ilNWy2LvcoSY7TRFIktABP2mdppBioc66va90T+NUs8Q==", "dev": true }, "source-map": { @@ -3768,6 +5305,16 @@ "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", "dev": true }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dev": true, + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, "damerau-levenshtein": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz", @@ -3784,9 +5331,9 @@ } }, "date-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", - "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-3.0.0.tgz", + "integrity": "sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==", "dev": true }, "debug": { @@ -3798,12 +5345,6 @@ "ms": "^2.1.1" } }, - "debuglog": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", - "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=", - "dev": true - }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -3840,15 +5381,6 @@ "ip-regex": "^2.1.0" } }, - "default-require-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", - "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", - "dev": true, - "requires": { - "strip-bom": "^3.0.0" - } - }, "defaults": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", @@ -3856,14 +5388,6 @@ "dev": true, "requires": { "clone": "^1.0.2" - }, - "dependencies": { - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "dev": true - } } }, "define-properties": { @@ -3913,6 +5437,12 @@ "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -3931,6 +5461,15 @@ "rimraf": "^2.6.3" }, "dependencies": { + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, "globby": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", @@ -4009,16 +5548,6 @@ "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", "dev": true }, - "dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", - "dev": true, - "requires": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, "di": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", @@ -4040,15 +5569,23 @@ "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } } }, "dir-glob": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", - "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, "requires": { - "path-type": "^3.0.0" + "path-type": "^4.0.0" } }, "dns-equal": { @@ -4099,9 +5636,9 @@ }, "dependencies": { "domelementtype": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", - "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz", + "integrity": "sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA==", "dev": true } } @@ -4129,9 +5666,9 @@ } }, "dot-prop": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", "dev": true, "requires": { "is-obj": "^2.0.0" @@ -4166,15 +5703,15 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.409", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.409.tgz", - "integrity": "sha512-CB2HUXiMsaVYY5VvcpELhDShiTRhI2FfN7CuacEZ5mDmMFuSG/ZVm8HoSya0+S61RvUd3TjIjFSKywqHZpRPzQ==", + "version": "1.3.596", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.596.tgz", + "integrity": "sha512-nLO2Wd2yU42eSoNJVQKNf89CcEGqeFZd++QsnN2XIgje1s/19AgctfjLIbPORlvcCO8sYjLwX4iUgDdusOY8Sg==", "dev": true }, "elliptic": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", - "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", "dev": true, "requires": { "bn.js": "^4.4.0", @@ -4184,6 +5721,14 @@ "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } } }, "emoji-regex": { @@ -4205,12 +5750,23 @@ "dev": true }, "encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "dev": true, "requires": { - "iconv-lite": "~0.4.13" + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", + "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } } }, "end-of-stream": { @@ -4223,17 +5779,17 @@ } }, "engine.io": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz", - "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz", + "integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==", "dev": true, "requires": { "accepts": "~1.3.4", - "base64id": "1.0.0", + "base64id": "2.0.0", "cookie": "0.3.1", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.0", - "ws": "~3.3.1" + "debug": "~4.1.0", + "engine.io-parser": "~2.2.0", + "ws": "^7.1.2" }, "dependencies": { "cookie": { @@ -4242,59 +5798,33 @@ "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", "dev": true }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, "ws": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0", - "ultron": "~1.1.0" - } + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz", + "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==", + "dev": true } } }, "engine.io-client": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", - "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.4.tgz", + "integrity": "sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==", "dev": true, "requires": { - "component-emitter": "1.2.1", + "component-emitter": "~1.3.0", "component-inherit": "0.0.3", "debug": "~3.1.0", - "engine.io-parser": "~2.1.1", + "engine.io-parser": "~2.2.0", "has-cors": "1.1.0", "indexof": "0.0.1", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "ws": "~3.3.1", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~6.1.0", "xmlhttprequest-ssl": "~1.5.4", "yeast": "0.1.2" }, "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", @@ -4310,41 +5840,50 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==", + "dev": true + }, + "parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==", + "dev": true + }, "ws": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", + "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", "dev": true, "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0", - "ultron": "~1.1.0" + "async-limiter": "~1.0.0" } } } }, "engine.io-parser": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", - "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", + "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", "dev": true, "requires": { "after": "0.8.2", "arraybuffer.slice": "~0.0.7", - "base64-arraybuffer": "0.1.5", + "base64-arraybuffer": "0.1.4", "blob": "0.0.5", "has-binary2": "~1.0.2" } }, "enhanced-resolve": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz", - "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.3.1.tgz", + "integrity": "sha512-G1XD3MRGrGfNcf6Hg0LVZG7GIKcYkbfHa5QMxt1HDUTdYoXH0JR1xXyg+MaKLF73E9A27uWNVxvFivNRYeUB6w==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" + "graceful-fs": "^4.2.4", + "tapable": "^2.0.0" } }, "ent": { @@ -4354,9 +5893,9 @@ "dev": true }, "entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", - "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", "dev": true }, "err-code": { @@ -4384,22 +5923,22 @@ } }, "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "has": "^1.0.3", "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" } }, "es-to-primitive": { @@ -4413,6 +5952,28 @@ "is-symbol": "^1.0.2" } }, + "es5-ext": { + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "dev": true, + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, "es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -4428,6 +5989,22 @@ "es6-promise": "^4.0.3" } }, + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dev": true, + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -4457,12 +6034,20 @@ "dev": true }, "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "requires": { - "estraverse": "^4.1.0" + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } } }, "estraverse": { @@ -4484,15 +6069,15 @@ "dev": true }, "eventemitter3": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", - "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "dev": true }, "events": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", - "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", "dev": true }, "eventsource": { @@ -4643,11 +6228,22 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + } + } + }, + "ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "dev": true, + "requires": { + "type": "^2.0.0" + }, + "dependencies": { + "type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", + "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==", "dev": true } } @@ -4762,11 +6358,25 @@ "dev": true }, "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-glob": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", + "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4779,6 +6389,15 @@ "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", "dev": true }, + "fastq": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.9.0.tgz", + "integrity": "sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "faye-websocket": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", @@ -4804,23 +6423,26 @@ } }, "file-loader": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.0.0.tgz", - "integrity": "sha512-/aMOAYEFXDdjG0wytpTL5YQLfZnnTmLNjn+AIrJ/6HVnTfDqLsVKUUwkDf4I4kgex36BvjuXEn/TX9B/1ESyqQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.1.1.tgz", + "integrity": "sha512-Klt8C4BjWSXYQAfhpYYkG4qHNTna4toMHEbWrI5IuVoxbU6uiDKeKAP99R8mmbJi3lvewn/jQBOgU4+NS3tDQw==", "dev": true, "requires": { "loader-utils": "^2.0.0", - "schema-utils": "^2.6.5" - } - }, - "fileset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", - "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", - "dev": true, - "requires": { - "glob": "^7.0.3", - "minimatch": "^3.0.3" + "schema-utils": "^3.0.0" + }, + "dependencies": { + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } } }, "fill-range": { @@ -4895,23 +6517,14 @@ } }, "make-dir": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", - "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "requires": { "semver": "^6.0.0" } }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, "p-locate": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", @@ -4921,12 +6534,6 @@ "p-limit": "^2.2.0" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4951,12 +6558,12 @@ } }, "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "requires": { - "locate-path": "^2.0.0" + "locate-path": "^3.0.0" } }, "flatted": { @@ -4976,24 +6583,10 @@ } }, "follow-redirects": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.11.0.tgz", - "integrity": "sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA==", - "dev": true, - "requires": { - "debug": "^3.0.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", + "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==", + "dev": true }, "for-in": { "version": "1.0.2", @@ -5033,6 +6626,14 @@ "map-cache": "^0.2.2" } }, + "freeice": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/freeice/-/freeice-2.2.2.tgz", + "integrity": "sha512-XNoIxDHufqPIBSLpp4IrFPnoc+hv/0RwdOGhIoggIDC2ZKf5r6OoixbeoFJSmZOAq2aYiEUArhuQ8zVVrM5C4w==", + "requires": { + "normalice": "^1.0.0" + } + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -5088,9 +6689,9 @@ "dev": true }, "fsevents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", - "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", "dev": true, "optional": true }, @@ -5107,17 +6708,28 @@ "dev": true }, "gensync": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", - "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-intrinsic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", + "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -5157,24 +6769,12 @@ } }, "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "dev": true, "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } + "is-glob": "^4.0.1" } }, "globals": { @@ -5184,31 +6784,23 @@ "dev": true }, "globby": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", - "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", + "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", "dev": true, "requires": { - "array-union": "^1.0.1", - "dir-glob": "^2.0.0", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" } }, "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", "dev": true }, "handle-thing": { @@ -5224,15 +6816,23 @@ "dev": true }, "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "dev": true, "requires": { - "ajv": "^6.5.5", + "ajv": "^6.12.3", "har-schema": "^2.0.0" } }, + "hark": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/hark/-/hark-1.2.3.tgz", + "integrity": "sha512-u68vz9SCa38ESiFJSDjqK8XbXqWzyot7Cj6Y2b6jk2NJ+II3MY2dIrLMg/kjtIAun4Y1DHF/20hfx4rq1G5GMg==", + "requires": { + "wildemitter": "^1.2.0" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -5249,6 +6849,14 @@ "dev": true, "requires": { "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + } } }, "has-binary2": { @@ -5295,6 +6903,14 @@ "get-value": "^2.0.6", "has-values": "^1.0.0", "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } } }, "has-values": { @@ -5339,13 +6955,33 @@ } }, "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", "dev": true, "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "hash.js": { @@ -5376,12 +7012,12 @@ } }, "hosted-git-info": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.4.tgz", - "integrity": "sha512-4oT62d2jwSDBbLLFLZE+1vPuQ1h8p9wjrJ8Mqx5TjsyWmBMV5B13eJqn8pvluqubLf3cJPTfiYCIwNwDNmzScQ==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.7.tgz", + "integrity": "sha512-fWqc0IcuXs+BmE9orLDyVykAG9GJtGLGuZAAqgcckPgv5xad4AcXGIv8galtQvlwutxSlaMcdw7BUtq2EIvqCQ==", "dev": true, "requires": { - "lru-cache": "^5.1.1" + "lru-cache": "^6.0.0" } }, "hpack.js": { @@ -5459,16 +7095,10 @@ } } }, - "http-parser-js": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", - "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=", - "dev": true - }, "http-proxy": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz", - "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, "requires": { "eventemitter3": "^4.0.0", @@ -5513,6 +7143,117 @@ "is-glob": "^4.0.0", "lodash": "^4.17.11", "micromatch": "^3.1.10" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } } }, "http-signature": { @@ -5571,10 +7312,16 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "icss-utils": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.0.0.tgz", + "integrity": "sha512-aF2Cf/CkEZrI/vsu5WI/I+akFgdbwQHVE9YRZxATrhH4PVIe6a3BIjwjEcW+z+jP/hNh+YvM3lAAn1wJQ6opSg==", + "dev": true + }, "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true }, "iferr": { @@ -5584,9 +7331,9 @@ "dev": true }, "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true }, "ignore-walk": { @@ -5611,15 +7358,6 @@ "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", "dev": true }, - "import-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", - "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", - "dev": true, - "requires": { - "import-from": "^2.1.0" - } - }, "import-fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", @@ -5630,15 +7368,6 @@ "resolve-from": "^3.0.0" } }, - "import-from": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", - "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - } - }, "import-local": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", @@ -5702,46 +7431,39 @@ "dev": true }, "inquirer": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", - "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", + "chalk": "^4.1.0", "cli-cursor": "^3.1.0", - "cli-width": "^2.0.0", + "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", - "lodash": "^4.17.15", + "lodash": "^4.17.19", "mute-stream": "0.0.8", "run-async": "^2.4.0", - "rxjs": "^6.5.3", + "rxjs": "^6.6.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6" }, "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -5769,36 +7491,33 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "rxjs": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", "dev": true, "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" + "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -5816,21 +7535,6 @@ "ipaddr.js": "^1.9.0" } }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -5903,9 +7607,9 @@ "dev": true }, "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", "dev": true }, "is-color-stop": { @@ -5922,6 +7626,15 @@ "rgba-regex": "^1.0.0" } }, + "is-core-module": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz", + "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -5974,9 +7687,9 @@ "dev": true }, "is-docker": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz", - "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", "dev": true }, "is-extendable": { @@ -5992,9 +7705,9 @@ "dev": true }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "is-glob": { @@ -6012,6 +7725,12 @@ "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "dev": true }, + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", + "dev": true + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -6048,12 +7767,6 @@ "path-is-inside": "^1.0.2" } }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true - }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -6061,21 +7774,23 @@ "dev": true, "requires": { "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } } }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true - }, "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", "dev": true, "requires": { - "has": "^1.0.3" + "has-symbols": "^1.0.1" } }, "is-resolvable": { @@ -6121,10 +7836,13 @@ "dev": true }, "is-wsl": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.1.1.tgz", - "integrity": "sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==", - "dev": true + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } }, "isarray": { "version": "1.0.0", @@ -6145,10 +7863,13 @@ "dev": true }, "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } }, "isstream": { "version": "0.1.2", @@ -6156,81 +7877,19 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "dev": true }, - "istanbul-api": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.1.6.tgz", - "integrity": "sha512-x0Eicp6KsShG1k1rMgBAi/1GgY7kFGEBwQpw3PXGEmu+rBcBNhqU8g2DgY9mlepAsLPzrzrbqSgCGANnki4POA==", - "dev": true, - "requires": { - "async": "^2.6.2", - "compare-versions": "^3.4.0", - "fileset": "^2.0.3", - "istanbul-lib-coverage": "^2.0.5", - "istanbul-lib-hook": "^2.0.7", - "istanbul-lib-instrument": "^3.3.0", - "istanbul-lib-report": "^2.0.8", - "istanbul-lib-source-maps": "^3.0.6", - "istanbul-reports": "^2.2.4", - "js-yaml": "^3.13.1", - "make-dir": "^2.1.0", - "minimatch": "^3.0.4", - "once": "^1.4.0" - }, - "dependencies": { - "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", - "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", - "dev": true, - "requires": { - "@babel/generator": "^7.4.0", - "@babel/parser": "^7.4.3", - "@babel/template": "^7.4.0", - "@babel/traverse": "^7.4.3", - "@babel/types": "^7.4.0", - "istanbul-lib-coverage": "^2.0.5", - "semver": "^6.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, "istanbul-lib-coverage": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", "dev": true }, - "istanbul-lib-hook": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", - "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", - "dev": true, - "requires": { - "append-transform": "^1.0.0" - } - }, "istanbul-lib-instrument": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz", - "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, "requires": { "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.0.0", "semver": "^6.3.0" @@ -6245,29 +7904,44 @@ } }, "istanbul-lib-report": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", - "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", "dev": true, "requires": { - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "supports-color": "^6.1.0" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" }, "dependencies": { - "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" } } } @@ -6309,12 +7983,13 @@ } }, "istanbul-reports": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", - "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", "dev": true, "requires": { - "html-escaper": "^2.0.0" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" } }, "jasmine": { @@ -6337,9 +8012,9 @@ } }, "jasmine-core": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", - "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.6.0.tgz", + "integrity": "sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==", "dev": true }, "jasmine-spec-reporter": { @@ -6358,11 +8033,12 @@ "dev": true }, "jest-worker": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.1.0.tgz", - "integrity": "sha512-ZHhHtlxOWSxCoNOKHGbiLzXnl42ga9CxDr27H36Qn+15pQZd3R/F24jrmjDelw9j/iHUIWMWs08/u2QN50HHOg==", + "version": "26.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.5.0.tgz", + "integrity": "sha512-kTw66Dn4ZX7WpjZ7T/SUDgRhapFRKWmisVAF0Rv4Fu8SLFD7eLbqpLvbxVqYhSgaWa7I+bW7pHnbyfNsH6stug==", "dev": true, "requires": { + "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^7.0.0" }, @@ -6374,9 +8050,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -6423,6 +8099,12 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -6456,6 +8138,12 @@ "minimist": "^1.2.5" } }, + "jsonc-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", + "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==", + "dev": true + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -6484,9 +8172,9 @@ } }, "jszip": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.3.0.tgz", - "integrity": "sha512-EJ9k766htB1ZWnsV5ZMDkKLgA+201r/ouFF8R2OigVjVdcm2rurcBrrdXaeqBJbqnUVMko512PYmlncBKE1Huw==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz", + "integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==", "dev": true, "requires": { "lie": "~3.3.0", @@ -6496,53 +8184,52 @@ } }, "karma": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/karma/-/karma-5.0.1.tgz", - "integrity": "sha512-xrDGtZ0mykEQjx1BUHOP1ITi39MDsCGocmSvLJWHxUQpxuKwxk3ZUrC6HI2VWh1plLC6+7cA3B19m12yzO/FRw==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/karma/-/karma-5.0.9.tgz", + "integrity": "sha512-dUA5z7Lo7G4FRSe1ZAXqOINEEWxmCjDBbfRBmU/wYlSMwxUQJP/tEEP90yJt3Uqo03s9rCgVnxtlfq+uDhxSPg==", "dev": true, "requires": { - "body-parser": "^1.16.1", + "body-parser": "^1.19.0", "braces": "^3.0.2", "chokidar": "^3.0.0", - "colors": "^1.1.0", - "connect": "^3.6.0", + "colors": "^1.4.0", + "connect": "^3.7.0", "di": "^0.0.1", - "dom-serialize": "^2.2.0", - "flatted": "^2.0.0", - "glob": "^7.1.1", - "graceful-fs": "^4.1.2", - "http-proxy": "^1.13.0", - "isbinaryfile": "^4.0.2", - "lodash": "^4.17.14", - "log4js": "^4.0.0", - "mime": "^2.3.1", - "minimatch": "^3.0.2", - "qjobs": "^1.1.4", - "range-parser": "^1.2.0", - "rimraf": "^2.6.0", - "socket.io": "2.1.1", + "dom-serialize": "^2.2.1", + "flatted": "^2.0.2", + "glob": "^7.1.6", + "graceful-fs": "^4.2.4", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.6", + "lodash": "^4.17.15", + "log4js": "^6.2.1", + "mime": "^2.4.5", + "minimatch": "^3.0.4", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^2.3.0", "source-map": "^0.6.1", - "tmp": "0.0.33", + "tmp": "0.2.1", "ua-parser-js": "0.7.21", "yargs": "^15.3.1" }, "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -6579,18 +8266,6 @@ "path-exists": "^4.0.0" } }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -6601,20 +8276,11 @@ } }, "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", "dev": true }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, "p-locate": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", @@ -6624,57 +8290,25 @@ "p-limit": "^2.2.0" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", "dev": true, "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" + "rimraf": "^3.0.0" } }, "wrap-ansi": { @@ -6689,9 +8323,9 @@ } }, "yargs": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", - "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "dev": true, "requires": { "cliui": "^6.0.0", @@ -6704,13 +8338,13 @@ "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^18.1.1" + "yargs-parser": "^18.1.2" } }, "yargs-parser": { - "version": "18.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.2.tgz", - "integrity": "sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -6729,22 +8363,33 @@ } }, "karma-coverage-istanbul-reporter": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.1.1.tgz", - "integrity": "sha512-CH8lTi8+kKXGvrhy94+EkEMldLCiUA0xMOiL31vvli9qK0T+qcXJAwWBRVJWnVWxYkTmyWar8lPz63dxX6/z1A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz", + "integrity": "sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw==", "dev": true, "requires": { - "istanbul-api": "^2.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^3.0.2", "minimatch": "^3.0.4" } }, "karma-jasmine": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-3.1.1.tgz", - "integrity": "sha512-pxBmv5K7IkBRLsFSTOpgiK/HzicQT3mfFF+oHAC7nxMfYKhaYFgxOa5qjnHW4sL5rUnmdkSajoudOnnOdPyW4Q==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.1.tgz", + "integrity": "sha512-h8XDAhTiZjJKzfkoO1laMH+zfNlra+dEQHUAjpn5JV1zCPtOIVWGQjLBrqhnzQa/hrU2XrZwSyBa6XjEBzfXzw==", "dev": true, "requires": { - "jasmine-core": "^3.5.0" + "jasmine-core": "^3.6.0" + }, + "dependencies": { + "jasmine-core": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.6.0.tgz", + "integrity": "sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==", + "dev": true + } } }, "karma-jasmine-html-reporter": { @@ -6774,29 +8419,24 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } + "klona": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", + "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==", + "dev": true }, "less": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/less/-/less-3.11.1.tgz", - "integrity": "sha512-tlWX341RECuTOvoDIvtFqXsKj072hm3+9ymRBe76/mD6O5ZZecnlAOVDlWAleF2+aohFrxNidXhv2773f6kY7g==", + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/less/-/less-3.12.2.tgz", + "integrity": "sha512-+1V2PCMFkL+OIj2/HrtrvZw0BC0sYLMICJfbQjuj/K8CEnlrFX6R5cKKgzzttsZDHyxQNL1jqMREjKN3ja/E3Q==", "dev": true, "requires": { - "clone": "^2.1.2", "errno": "^0.1.1", "graceful-fs": "^4.1.2", "image-size": "~0.5.0", + "make-dir": "^2.1.0", "mime": "^1.4.1", - "mkdirp": "^0.5.0", - "promise": "^7.1.1", - "request": "^2.83.0", + "native-request": "^1.0.5", "source-map": "~0.6.0", "tslib": "^1.10.0" }, @@ -6807,65 +8447,65 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "optional": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true } } }, "less-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-5.0.0.tgz", - "integrity": "sha512-bquCU89mO/yWLaUq0Clk7qCsKhsF/TZpJUzETRvJa9KSVEL9SO3ovCvdEHISBhrC81OwC8QSVX7E0bzElZj9cg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-7.0.2.tgz", + "integrity": "sha512-7MKlgjnkCf63E3Lv6w2FvAEgLMx3d/tNBExITcanAq7ys5U8VPWT3F6xcRjYmdNfkoQ9udoVFb1r2azSiTnD6w==", "dev": true, "requires": { - "clone": "^2.1.1", - "loader-utils": "^1.1.0", - "pify": "^4.0.1" + "klona": "^2.0.4", + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" }, "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", "dev": true, "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" } } } }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true - }, - "levenary": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/levenary/-/levenary-1.1.1.tgz", - "integrity": "sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==", - "dev": true, - "requires": { - "leven": "^3.1.0" - } - }, "license-webpack-plugin": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-2.1.4.tgz", - "integrity": "sha512-1Xq72fmPbTg5KofXs+yI5L4QqPFjQ6mZxoeI6D7gfiEDOtaEIk6PGrdLaej90bpDqKNHNxlQ/MW4tMAL6xMPJQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-2.3.1.tgz", + "integrity": "sha512-yhqTmlYIEpZWA122lf6E0G8+rkn0AzoQ1OpzUKKs/lXUqG1plmGnwmkuuPlfggzJR5y6DLOdot/Tv00CC51CeQ==", "dev": true, "requires": { "@types/webpack-sources": "^0.1.5", "webpack-sources": "^1.2.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + } } }, "lie": { @@ -6877,6 +8517,22 @@ "immediate": "~3.0.5" } }, + "line-column": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz", + "integrity": "sha1-0lryk2tvSEkXKzEuR5LR2Ye8NKI=", + "dev": true, + "requires": { + "isarray": "^1.0.0", + "isobject": "^2.0.0" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, "loader-runner": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", @@ -6895,12 +8551,12 @@ } }, "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, "requires": { - "p-locate": "^2.0.0", + "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, @@ -6910,12 +8566,6 @@ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -6929,57 +8579,91 @@ "dev": true }, "log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", "dev": true, "requires": { - "chalk": "^2.4.2" + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "log4js": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.5.1.tgz", - "integrity": "sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.3.0.tgz", + "integrity": "sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw==", "dev": true, "requires": { - "date-format": "^2.0.0", + "date-format": "^3.0.0", "debug": "^4.1.1", - "flatted": "^2.0.0", + "flatted": "^2.0.1", "rfdc": "^1.1.4", - "streamroller": "^1.0.6" + "streamroller": "^2.2.4" } }, "loglevel": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz", - "integrity": "sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.0.tgz", + "integrity": "sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ==", "dev": true }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "requires": { - "yallist": "^3.0.2" - }, - "dependencies": { - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } + "yallist": "^4.0.0" } }, "magic-string": { @@ -7057,6 +8741,21 @@ "y18n": "^4.0.0" } }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -7074,24 +8773,15 @@ "requires": { "figgy-pudding": "^3.5.1" } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true } } }, - "mamacro": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", - "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", - "dev": true - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -7130,21 +8820,10 @@ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, "memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", "dev": true, "requires": { "errno": "^0.1.3", @@ -7180,6 +8859,12 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -7187,108 +8872,13 @@ "dev": true }, "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", "dev": true, "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "dependencies": { - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } + "braces": "^3.0.1", + "picomatch": "^2.0.5" } }, "miller-rabin": { @@ -7299,6 +8889,14 @@ "requires": { "bn.js": "^4.0.0", "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } } }, "mime": { @@ -7308,18 +8906,18 @@ "dev": true }, "mime-db": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", - "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", "dev": true }, "mime-types": { - "version": "2.1.26", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", - "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", "dev": true, "requires": { - "mime-db": "1.43.0" + "mime-db": "1.44.0" } }, "mimic-fn": { @@ -7329,58 +8927,41 @@ "dev": true }, "mini-css-extract-plugin": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz", - "integrity": "sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.2.1.tgz", + "integrity": "sha512-G3yw7/TQaPfkuiR73MDcyiqhyP8SnbmLhUbpC76H+wtQxA6wfKhMCQOCb6wnPK0dQbjORAeOILQqEesg4/wF7A==", "dev": true, "requires": { - "loader-utils": "^1.1.0", - "normalize-url": "1.9.1", - "schema-utils": "^1.0.0", + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0", "webpack-sources": "^1.1.0" }, "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - }, - "normalize-url": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", - "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", - "dev": true, - "requires": { - "object-assign": "^4.0.1", - "prepend-http": "^1.0.0", - "query-string": "^4.1.0", - "sort-keys": "^1.0.0" - } - }, "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", "dev": true, "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" } } } @@ -7413,9 +8994,9 @@ "dev": true }, "minipass": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz", - "integrity": "sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", "dev": true, "requires": { "yallist": "^4.0.0" @@ -7440,18 +9021,18 @@ } }, "minipass-pipeline": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.2.tgz", - "integrity": "sha512-3JS5A2DKhD2g0Gg8x3yamO0pj7YeKGwVlDS90pF++kxptwx/F+B//roxf9SqYil5tQo65bijy+dAuAFZmYOouA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "dev": true, "requires": { "minipass": "^3.0.0" } }, "minizlib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.0.tgz", - "integrity": "sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "dev": true, "requires": { "minipass": "^3.0.0", @@ -7559,6 +9140,12 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nanoid": { + "version": "3.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.16.tgz", + "integrity": "sha512-+AK8MN0WHji40lj8AEuwLOvLSbWYApQpre/aFJZD71r43wVRLrOYS4FmJOPQYon1TqB462RzrrxlfA74XRES8w==", + "dev": true + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -7578,6 +9165,13 @@ "to-regex": "^3.0.1" } }, + "native-request": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/native-request/-/native-request-1.0.8.tgz", + "integrity": "sha512-vU2JojJVelUGp6jRcLwToPoWGxSx23z/0iX+I77J3Ht17rf2INGjrhOoQnjVo60nQd8wVsgzKkPfRXBiVdD2ag==", + "dev": true, + "optional": true + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -7585,9 +9179,15 @@ "dev": true }, "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, "nice-try": { @@ -7608,9 +9208,9 @@ } }, "node-forge": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", - "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", "dev": true }, "node-libs-browser": { @@ -7653,11 +9253,16 @@ } }, "node-releases": { - "version": "1.1.53", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.53.tgz", - "integrity": "sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ==", + "version": "1.1.66", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz", + "integrity": "sha512-JHEQ1iWPGK+38VLB2H9ef2otU4l8s3yAMt9Xf934r6+ojCYDMHPMqvCc9TnzfeFSP1QEOeU6YZEd3+De0LTCgg==", "dev": true }, + "normalice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/normalice/-/normalice-1.0.1.tgz", + "integrity": "sha1-A0NcLuzVYxprygLaOTDsPjRagPc=" + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -7727,12 +9332,12 @@ "dev": true }, "npm-package-arg": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.0.1.tgz", - "integrity": "sha512-/h5Fm6a/exByzFSTm7jAyHbgOqErl9qSNJDQF32Si/ZzgwT2TERVxRxn3Jurw1wflgyVVAxnFR4fRHPM7y1ClQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.0.tgz", + "integrity": "sha512-/ep6QDxBkm9HvOhOg0heitSd7JHA1U7y1qhhlRlteYYAi9Pdb/ZV7FW5aHpkrpM8+P+4p/jjR8zCyKPBMBjSig==", "dev": true, "requires": { - "hosted-git-info": "^3.0.2", + "hosted-git-info": "^3.0.6", "semver": "^7.0.0", "validate-npm-package-name": "^3.0.0" } @@ -7749,9 +9354,9 @@ } }, "npm-pick-manifest": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.0.0.tgz", - "integrity": "sha512-PdJpXMvjqt4nftNEDpCgjBUF8yI3Q3MyuAmVB9nemnnCg32F4BPL/JFBfdj8DubgHCYUFQhtLWmBPvdsFtjWMg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz", + "integrity": "sha512-ygs4k6f54ZxJXrzT0x34NybRlLeZ4+6nECAIbr2i0foTnijtS1TJiyzpqtuUAJOps/hO0tNDr8fRV5g+BtRlTw==", "dev": true, "requires": { "npm-install-checks": "^4.0.0", @@ -7760,9 +9365,9 @@ } }, "npm-registry-fetch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.3.tgz", - "integrity": "sha512-WGvUx0lkKFhu9MbiGFuT9nG2NpfQ+4dCJwRwwtK2HK5izJEvwDxMeUyqbuMS7N/OkpVCqDorV6rO5E4V9F8lJw==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.7.tgz", + "integrity": "sha512-cny9v0+Mq6Tjz+e0erFAB+RYJ/AVGzkjnISiobqP8OWj9c9FLoZZu8/SPSKJWE17F1tk4018wfjV+ZbIbqC7fQ==", "dev": true, "requires": { "JSONStream": "^1.3.4", @@ -7780,6 +9385,15 @@ "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", "dev": true }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, "npm-package-arg": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz", @@ -7793,9 +9407,9 @@ } }, "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true }, "semver": { @@ -7803,6 +9417,12 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true } } }, @@ -7830,12 +9450,6 @@ "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", "dev": true }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -7886,19 +9500,41 @@ } }, "object-inspect": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", "dev": true }, "object-is": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz", - "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz", + "integrity": "sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "object-keys": { @@ -7914,18 +9550,26 @@ "dev": true, "requires": { "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } } }, "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" } }, "object.getownpropertydescriptors": { @@ -7945,6 +9589,14 @@ "dev": true, "requires": { "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } } }, "object.values": { @@ -7990,18 +9642,18 @@ } }, "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "requires": { "mimic-fn": "^2.1.0" } }, "open": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/open/-/open-7.0.3.tgz", - "integrity": "sha512-sP2ru2v0P290WFfv49Ap8MF6PkzGNnGlAwHweB4WR4mr5d2d0woiCluUeJ218w7/+PmoBy9JmYgD5A4mLcWOFA==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/open/-/open-7.3.0.tgz", + "integrity": "sha512-mgLwQIx2F/ye9SmbrUkurZCnkoXyXyu9EbHtJZrICjVAJfyMArdHp3KkixGdZx1ZHFPNIwl0DDM1dFFqXbTLZw==", "dev": true, "requires": { "is-docker": "^2.0.0", @@ -8009,4387 +9661,28 @@ } }, "openvidu-browser": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/openvidu-browser/-/openvidu-browser-2.15.0.tgz", - "integrity": "sha512-agnyeYIf1ze5ynGqNw32zFedlov9JZzjoFQHNMwuAoFYc2/24Aajs9cyw3j0m7v8xmMkqWSOYvsu7kGc8z1mZg==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/openvidu-browser/-/openvidu-browser-2.16.0.tgz", + "integrity": "sha512-o7TWCKhEKPYYuDpYhL1u2OMvZVhzj6Kef8iD64QOTO3q3Wq6jpDGpM6J9ZxfqjE9owyJWQkDL6jdzq4tEiiQvw==", "requires": { - "@types/node": "13.13.2", - "@types/platform": "1.3.2", + "@types/node": "14.14.7", + "@types/platform": "1.3.3", "freeice": "2.2.2", "hark": "1.2.3", - "platform": "1.3.5", - "uuid": "7.0.3", + "platform": "1.3.6", + "uuid": "8.3.1", "wolfy87-eventemitter": "5.2.9" }, "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, "@types/node": { - "version": "13.13.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.2.tgz", - "integrity": "sha512-LB2R1Oyhpg8gu4SON/mfforE525+Hi/M1ineICEDftqNVTyFg1aRIeGuTvXAoWHc4nbrFncWtJgMmoyRvuGh7A==" - }, - "@types/platform": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/platform/-/platform-1.3.2.tgz", - "integrity": "sha512-Tn6OuJDAG7bJbyi4R7HqcxXp1w2lmIxVXqyNhPt1Bm0FO2EWIi3CI87JVzF7ncqK0ZMPuUycS3wTMIk85EeF1Q==" - }, - "JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==" - }, - "acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "requires": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "acorn-walk": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz", - "integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==" - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - }, - "dependencies": { - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - } - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" - }, - "array-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=" - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" - }, - "array-slice": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", - "requires": { - "object-assign": "^4.1.1", - "util": "0.10.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "requires": { - "inherits": "2.0.1" - } - } - } - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" - }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==" - }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" - }, - "body": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", - "integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=", - "requires": { - "continuable-cache": "^0.3.1", - "error": "^7.0.0", - "raw-body": "~1.1.0", - "safe-json-parse": "~1.0.1" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "browser-pack": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", - "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", - "requires": { - "JSONStream": "^1.0.3", - "combine-source-map": "~0.8.0", - "defined": "^1.0.0", - "safe-buffer": "^5.1.1", - "through2": "^2.0.0", - "umd": "^3.0.0" - } - }, - "browser-resolve": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", - "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" - } - } - }, - "browserify": { - "version": "16.5.1", - "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.5.1.tgz", - "integrity": "sha512-EQX0h59Pp+0GtSRb5rL6OTfrttlzv+uyaUVlK6GX3w11SQ0jKPKyjC/54RhPR2ib2KmfcELM06e8FxcI5XNU2A==", - "requires": { - "JSONStream": "^1.0.3", - "assert": "^1.4.0", - "browser-pack": "^6.0.1", - "browser-resolve": "^1.11.0", - "browserify-zlib": "~0.2.0", - "buffer": "~5.2.1", - "cached-path-relative": "^1.0.0", - "concat-stream": "^1.6.0", - "console-browserify": "^1.1.0", - "constants-browserify": "~1.0.0", - "crypto-browserify": "^3.0.0", - "defined": "^1.0.0", - "deps-sort": "^2.0.0", - "domain-browser": "^1.2.0", - "duplexer2": "~0.1.2", - "events": "^2.0.0", - "glob": "^7.1.0", - "has": "^1.0.0", - "htmlescape": "^1.1.0", - "https-browserify": "^1.0.0", - "inherits": "~2.0.1", - "insert-module-globals": "^7.0.0", - "labeled-stream-splicer": "^2.0.0", - "mkdirp-classic": "^0.5.2", - "module-deps": "^6.0.0", - "os-browserify": "~0.3.0", - "parents": "^1.0.1", - "path-browserify": "~0.0.0", - "process": "~0.11.0", - "punycode": "^1.3.2", - "querystring-es3": "~0.2.0", - "read-only-stream": "^2.0.0", - "readable-stream": "^2.0.2", - "resolve": "^1.1.4", - "shasum": "^1.0.0", - "shell-quote": "^1.6.1", - "stream-browserify": "^2.0.0", - "stream-http": "^3.0.0", - "string_decoder": "^1.1.1", - "subarg": "^1.0.0", - "syntax-error": "^1.1.1", - "through2": "^2.0.0", - "timers-browserify": "^1.0.1", - "tty-browserify": "0.0.1", - "url": "~0.11.0", - "util": "~0.10.1", - "vm-browserify": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "requires": { - "bn.js": "^4.1.0", - "randombytes": "^2.0.1" - } - }, - "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "requires": { - "pako": "~1.0.5" - } - }, - "buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", - "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" - }, - "bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", - "integrity": "sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g=" - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "cached-path-relative": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz", - "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==" - }, - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, - "dependencies": { - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "requires": { - "is-extglob": "^2.1.1" - } - } - } - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "coffeescript": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.10.0.tgz", - "integrity": "sha1-56qDAZF+9iGzXYo580jc3R234z4=" - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=" - }, - "combine-source-map": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", - "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", - "requires": { - "convert-source-map": "~1.1.0", - "inline-source-map": "~0.6.0", - "lodash.memoize": "~3.0.3", - "source-map": "~0.5.3" - } - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" - }, - "continuable-cache": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz", - "integrity": "sha1-vXJ6f67XfnH/OYWskzUakSczrQ8=" - }, - "convert-source-map": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", - "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=" - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.0.0" - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "cross-spawn": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-0.2.9.tgz", - "integrity": "sha1-vWf5bAfvtjA7f+lMHpefiEeOCjk=", - "requires": { - "lru-cache": "^2.5.0" - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "csproj2ts": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/csproj2ts/-/csproj2ts-1.1.0.tgz", - "integrity": "sha512-sk0RTT51t4lUNQ7UfZrqjQx7q4g0m3iwNA6mvyh7gLsgQYvwKzfdyoAgicC9GqJvkoIkU0UmndV9c7VZ8pJ45Q==", - "requires": { - "es6-promise": "^4.1.1", - "lodash": "^4.17.4", - "semver": "^5.4.1", - "xml2js": "^0.4.19" - }, - "dependencies": { - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - } - } - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "requires": { - "array-find-index": "^1.0.1" - } - }, - "dargs": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", - "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "dash-ast": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", - "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==" - }, - "dateformat": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", - "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", - "requires": { - "get-stdin": "^4.0.1", - "meow": "^3.3.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" - }, - "deps-sort": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.1.tgz", - "integrity": "sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw==", - "requires": { - "JSONStream": "^1.0.3", - "shasum-object": "^1.0.0", - "subarg": "^1.0.0", - "through2": "^2.0.0" - } - }, - "des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=" - }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "requires": { - "repeating": "^2.0.0" - } - }, - "detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=" - }, - "detective": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", - "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", - "requires": { - "acorn-node": "^1.6.1", - "defined": "^1.0.0", - "minimist": "^1.1.1" - } - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" - }, - "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" - }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "requires": { - "readable-stream": "^2.0.2" - } - }, - "elliptic": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", - "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", - "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" - } - }, - "error": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/error/-/error-7.2.1.tgz", - "integrity": "sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==", - "requires": { - "string-template": "~0.2.1" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es6-promise": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-0.1.2.tgz", - "integrity": "sha1-8RLCn+paCZhTn8tqL9IUQ9KPBfc=" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "eventemitter2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=" - }, - "events": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", - "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==" - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "fast-safe-stringify": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", - "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" - }, - "faye-websocket": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", - "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "file-sync-cmp": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz", - "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=" - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "optional": true - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "findup-sync": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", - "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", - "requires": { - "glob": "~5.0.0" - }, - "dependencies": { - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", - "requires": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" - } - }, - "flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==" - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" - }, - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "requires": { - "for-in": "^1.0.1" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "^0.2.2" - } - }, - "freeice": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/freeice/-/freeice-2.2.2.tgz", - "integrity": "sha512-XNoIxDHufqPIBSLpp4IrFPnoc+hv/0RwdOGhIoggIDC2ZKf5r6OoixbeoFJSmZOAq2aYiEUArhuQ8zVVrM5C4w==", - "requires": { - "normalice": "^1.0.0" - } - }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.12.tgz", - "integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==", - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1", - "node-pre-gyp": "*" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.4", - "bundled": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "debug": { - "version": "3.2.6", - "bundled": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.7", - "bundled": true, - "optional": true, - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.6", - "bundled": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.3", - "bundled": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "bundled": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "bundled": true, - "optional": true - }, - "minipass": { - "version": "2.9.0", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "bundled": true, - "optional": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.3", - "bundled": true, - "optional": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.2", - "bundled": true, - "optional": true - }, - "needle": { - "version": "2.3.3", - "bundled": true, - "optional": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.14.0", - "bundled": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - } - }, - "nopt": { - "version": "4.0.3", - "bundled": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.1.1", - "bundled": true, - "optional": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.8", - "bundled": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.7", - "bundled": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.7.1", - "bundled": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "optional": true - }, - "semver": { - "version": "5.7.1", - "bundled": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "tar": { - "version": "4.4.13", - "bundled": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "yallist": { - "version": "3.1.1", - "bundled": true, - "optional": true - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "requires": { - "globule": "^1.0.0" - } - }, - "get-assigned-identifiers": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", - "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==" - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" - }, - "getobject": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", - "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=" - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - } - }, - "global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", - "requires": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - } - }, - "globule": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.1.tgz", - "integrity": "sha512-OVyWOHgw29yosRHCHo7NncwR1hW5ew0W/UrvtwvjefVJeQ26q4/8r8FmPsSF1hJ93IgWkyv16pCTz6WblMzm/g==", - "requires": { - "glob": "~7.1.1", - "lodash": "~4.17.12", - "minimatch": "~3.0.2" - } - }, - "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" - }, - "grunt": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.1.0.tgz", - "integrity": "sha512-+NGod0grmviZ7Nzdi9am7vuRS/h76PcWDsV635mEXF0PEQMUV6Kb+OjTdsVxbi0PZmfQOjCMKb3w8CVZcqsn1g==", - "requires": { - "coffeescript": "~1.10.0", - "dateformat": "~1.0.12", - "eventemitter2": "~0.4.13", - "exit": "~0.1.1", - "findup-sync": "~0.3.0", - "glob": "~7.0.0", - "grunt-cli": "~1.2.0", - "grunt-known-options": "~1.1.0", - "grunt-legacy-log": "~2.0.0", - "grunt-legacy-util": "~1.1.1", - "iconv-lite": "~0.4.13", - "js-yaml": "~3.13.1", - "minimatch": "~3.0.2", - "mkdirp": "~1.0.3", - "nopt": "~3.0.6", - "path-is-absolute": "~1.0.0", - "rimraf": "~2.6.2" - }, - "dependencies": { - "glob": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", - "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.2", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "grunt-cli": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz", - "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=", - "requires": { - "findup-sync": "~0.3.0", - "grunt-known-options": "~1.1.0", - "nopt": "~3.0.6", - "resolve": "~1.1.0" - } - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" - } - } - }, - "grunt-cli": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.3.2.tgz", - "integrity": "sha512-8OHDiZZkcptxVXtMfDxJvmN7MVJNE8L/yIcPb4HB7TlyFD1kDvjHrb62uhySsU14wJx9ORMnTuhRMQ40lH/orQ==", - "requires": { - "grunt-known-options": "~1.1.0", - "interpret": "~1.1.0", - "liftoff": "~2.5.0", - "nopt": "~4.0.1", - "v8flags": "~3.1.1" - }, - "dependencies": { - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - } - } - }, - "grunt-contrib-copy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz", - "integrity": "sha1-cGDGWB6QS4qw0A8HbgqPbj58NXM=", - "requires": { - "chalk": "^1.1.1", - "file-sync-cmp": "^0.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "grunt-contrib-sass": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-sass/-/grunt-contrib-sass-1.0.0.tgz", - "integrity": "sha1-gGg4JRy8DhqU1k1RXN00z2dNcBs=", - "requires": { - "async": "^0.9.0", - "chalk": "^1.0.0", - "cross-spawn": "^0.2.3", - "dargs": "^4.0.0", - "which": "^1.0.5" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "grunt-contrib-uglify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-4.0.1.tgz", - "integrity": "sha512-dwf8/+4uW1+7pH72WButOEnzErPGmtUvc8p08B0eQS/6ON0WdeQu0+WFeafaPTbbY1GqtS25lsHWaDeiTQNWPg==", - "requires": { - "chalk": "^2.4.1", - "maxmin": "^2.1.0", - "uglify-js": "^3.5.0", - "uri-path": "^1.0.0" - } - }, - "grunt-contrib-watch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.1.0.tgz", - "integrity": "sha512-yGweN+0DW5yM+oo58fRu/XIRrPcn3r4tQx+nL7eMRwjpvk+rQY6R8o94BPK0i2UhTg9FN21hS+m8vR8v9vXfeg==", - "requires": { - "async": "^2.6.0", - "gaze": "^1.1.0", - "lodash": "^4.17.10", - "tiny-lr": "^1.1.1" - }, - "dependencies": { - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "requires": { - "lodash": "^4.17.14" - } - } - } - }, - "grunt-known-options": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.1.tgz", - "integrity": "sha512-cHwsLqoighpu7TuYj5RonnEuxGVFnztcUqTqp5rXFGYL4OuPFofwC4Ycg7n9fYwvK6F5WbYgeVOwph9Crs2fsQ==" - }, - "grunt-legacy-log": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-2.0.0.tgz", - "integrity": "sha512-1m3+5QvDYfR1ltr8hjiaiNjddxGdQWcH0rw1iKKiQnF0+xtgTazirSTGu68RchPyh1OBng1bBUjLmX8q9NpoCw==", - "requires": { - "colors": "~1.1.2", - "grunt-legacy-log-utils": "~2.0.0", - "hooker": "~0.2.3", - "lodash": "~4.17.5" - } - }, - "grunt-legacy-log-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.0.1.tgz", - "integrity": "sha512-o7uHyO/J+i2tXG8r2bZNlVk20vlIFJ9IEYyHMCQGfWYru8Jv3wTqKZzvV30YW9rWEjq0eP3cflQ1qWojIe9VFA==", - "requires": { - "chalk": "~2.4.1", - "lodash": "~4.17.10" - } - }, - "grunt-legacy-util": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-1.1.1.tgz", - "integrity": "sha512-9zyA29w/fBe6BIfjGENndwoe1Uy31BIXxTH3s8mga0Z5Bz2Sp4UCjkeyv2tI449ymkx3x26B+46FV4fXEddl5A==", - "requires": { - "async": "~1.5.2", - "exit": "~0.1.1", - "getobject": "~0.1.0", - "hooker": "~0.2.3", - "lodash": "~4.17.10", - "underscore.string": "~3.3.4", - "which": "~1.3.0" - } - }, - "grunt-postcss": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/grunt-postcss/-/grunt-postcss-0.9.0.tgz", - "integrity": "sha512-lglLcVaoOIqH0sFv7RqwUKkEFGQwnlqyAKbatxZderwZGV1nDyKHN7gZS9LUiTx1t5GOvRBx0BEalHMyVwFAIA==", - "requires": { - "chalk": "^2.1.0", - "diff": "^3.0.0", - "postcss": "^6.0.11" - } - }, - "grunt-string-replace": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/grunt-string-replace/-/grunt-string-replace-1.3.1.tgz", - "integrity": "sha1-YzoDvHhIKg4OH5339kWBH8H7sWI=", - "requires": { - "async": "^2.0.0", - "chalk": "^1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "requires": { - "lodash": "^4.17.14" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "grunt-ts": { - "version": "6.0.0-beta.22", - "resolved": "https://registry.npmjs.org/grunt-ts/-/grunt-ts-6.0.0-beta.22.tgz", - "integrity": "sha512-g9e+ZImQ7W38dfpwhp0+GUltXWidy3YGPfIA/IyGL5HMv6wmVmMMoSgscI5swhs2HSPf8yAvXAAJbwrouijoRg==", - "requires": { - "chokidar": "^2.0.4", - "csproj2ts": "^1.1.0", - "detect-indent": "^4.0.0", - "detect-newline": "^2.1.0", - "es6-promise": "~0.1.1", - "jsmin2": "^1.2.1", - "lodash": "~4.17.10", - "ncp": "0.5.1", - "rimraf": "2.2.6", - "semver": "^5.3.0", - "strip-bom": "^2.0.0" - }, - "dependencies": { - "rimraf": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz", - "integrity": "sha1-xZWXVpsU2VatKcrMQr3d9fDqT0w=" - } - } - }, - "gzip-size": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz", - "integrity": "sha1-VGGI6b3DN/Zzdy+BZgRks4nc5SA=", - "requires": { - "duplexer": "^0.1.1" - } - }, - "handlebars": { - "version": "4.7.6", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", - "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", - "requires": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "hark": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/hark/-/hark-1.2.3.tgz", - "integrity": "sha512-u68vz9SCa38ESiFJSDjqK8XbXqWzyot7Cj6Y2b6jk2NJ+II3MY2dIrLMg/kjtIAun4Y1DHF/20hfx4rq1G5GMg==", - "requires": { - "wildemitter": "^1.2.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "highlight.js": { - "version": "9.18.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.1.tgz", - "integrity": "sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg==" - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "requires": { - "parse-passwd": "^1.0.0" - } - }, - "hooker": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=" - }, - "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" - }, - "htmlescape": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", - "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=" - }, - "http-parser-js": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", - "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=" - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "requires": { - "repeating": "^2.0.0" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "inline-source-map": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", - "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", - "requires": { - "source-map": "~0.5.3" - } - }, - "insert-module-globals": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.0.tgz", - "integrity": "sha512-VE6NlW+WGn2/AeOMd496AHFYmE7eLKkUY6Ty31k4og5vmA3Fjuwe9v6ifH6Xx/Hz27QvdoMoviw1/pqWRB09Sw==", - "requires": { - "JSONStream": "^1.0.3", - "acorn-node": "^1.5.2", - "combine-source-map": "^0.8.0", - "concat-stream": "^1.6.1", - "is-buffer": "^1.1.0", - "path-is-absolute": "^1.0.1", - "process": "~0.11.0", - "through2": "^2.0.0", - "undeclared-identifiers": "^1.1.2", - "xtend": "^4.0.0" - } - }, - "interpret": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=" - }, - "is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "requires": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==" - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "requires": { - "is-extglob": "^2.1.0" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" - } - }, - "is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "requires": { - "is-unc-path": "^1.0.0" - } - }, - "is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "requires": { - "unc-path-regex": "^0.1.2" - } - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsmin2": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/jsmin2/-/jsmin2-1.2.1.tgz", - "integrity": "sha1-iPvi+/dfCpH2YCD9mBzWk/S/5X4=" - }, - "json-stable-stringify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", - "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", - "requires": { - "jsonify": "~0.0.0" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" - }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" - }, - "labeled-stream-splicer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", - "integrity": "sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==", - "requires": { - "inherits": "^2.0.1", - "stream-splicer": "^2.0.0" - } - }, - "liftoff": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz", - "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=", - "requires": { - "extend": "^3.0.0", - "findup-sync": "^2.0.0", - "fined": "^1.0.1", - "flagged-respawn": "^1.0.0", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.0", - "rechoir": "^0.6.2", - "resolve": "^1.1.7" - }, - "dependencies": { - "findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^3.1.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - } - } - } - }, - "livereload-js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz", - "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==" - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" - }, - "lodash.memoize": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", - "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=" - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "lru-cache": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" - }, - "lunr": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.8.tgz", - "integrity": "sha512-oxMeX/Y35PNFuZoHp+jUj5OSEmLCaIH4KTFJh7a93cHBoFmpw2IoPs22VIz7vyO2YUnx2Tn9dzIwO2P/4quIRg==" - }, - "make-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", - "requires": { - "kind-of": "^6.0.2" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "requires": { - "object-visit": "^1.0.0" - } - }, - "marked": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.8.2.tgz", - "integrity": "sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw==" - }, - "maxmin": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-2.1.0.tgz", - "integrity": "sha1-TTsiCQPZXu5+t6x/qGTnLcCaMWY=", - "requires": { - "chalk": "^1.0.0", - "figures": "^1.0.1", - "gzip-size": "^3.0.0", - "pretty-bytes": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, - "mkdirp-classic": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz", - "integrity": "sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g==" - }, - "module-deps": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.2.tgz", - "integrity": "sha512-a9y6yDv5u5I4A+IPHTnqFxcaKr4p50/zxTjcQJaX2ws9tN/W6J6YXnEKhqRyPhl494dkcxx951onSKVezmI+3w==", - "requires": { - "JSONStream": "^1.0.3", - "browser-resolve": "^1.7.0", - "cached-path-relative": "^1.0.2", - "concat-stream": "~1.6.0", - "defined": "^1.0.0", - "detective": "^5.2.0", - "duplexer2": "^0.1.2", - "inherits": "^2.0.1", - "parents": "^1.0.0", - "readable-stream": "^2.0.2", - "resolve": "^1.4.0", - "stream-combiner2": "^1.1.1", - "subarg": "^1.0.0", - "through2": "^2.0.0", - "xtend": "^4.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "nan": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", - "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "ncp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz", - "integrity": "sha1-dDmFMW49tFkoG1hxaehFc1oFQ58=" - }, - "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "requires": { - "abbrev": "1" - } - }, - "normalice": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/normalice/-/normalice-1.0.1.tgz", - "integrity": "sha1-A0NcLuzVYxprygLaOTDsPjRagPc=" - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "requires": { - "isobject": "^3.0.0" - } - }, - "object.defaults": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", - "requires": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "object.map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", - "requires": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "requires": { - "isobject": "^3.0.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "parents": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", - "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", - "requires": { - "path-platform": "~0.11.15" - } - }, - "parse-asn1": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", - "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", - "requires": { - "asn1.js": "^4.0.0", - "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", - "requires": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "requires": { - "error-ex": "^1.2.0" - } - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" - }, - "path-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==" - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "path-platform": { - "version": "0.11.15", - "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", - "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=" - }, - "path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", - "requires": { - "path-root-regex": "^0.1.0" - } - }, - "path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=" - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "^2.0.0" - } - }, - "platform": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", - "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==" - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" - }, - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "pretty-bytes": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz", - "integrity": "sha1-J9AAjXeAY6C0gRuzXHnxvV1fvM8=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" - }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==" - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "raw-body": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", - "integrity": "sha1-HQJ8K/oRasxmI7yo8AAWVyqH1CU=", - "requires": { - "bytes": "1", - "string_decoder": "0.10" - }, - "dependencies": { - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "read-only-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", - "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", - "requires": { - "readable-stream": "^2.0.2" - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "requires": { - "resolve": "^1.1.6" - } - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "requires": { - "is-finite": "^1.0.0" - } - }, - "resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", - "requires": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - } - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "requires": { - "glob": "^7.1.3" - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" - }, - "safe-json-parse": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", - "integrity": "sha1-PnZyPjjf3aE8mx0poeB//uSzC1c=" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shasum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", - "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", - "requires": { - "json-stable-stringify": "~0.0.0", - "sha.js": "~2.4.4" - } - }, - "shasum-object": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz", - "integrity": "sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg==", - "requires": { - "fast-safe-stringify": "^2.0.7" - } - }, - "shell-quote": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", - "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==" - }, - "shelljs": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", - "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", - "requires": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - } - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" - }, - "simple-concat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", - "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.18.tgz", - "integrity": "sha512-9luZr/BZ2QeU6tO2uG8N2aZpVSli4TSAOAqFOyTO51AJcD9P99c0K1h6dD6r6qo5dyT44BR5exweOaLLeldTkQ==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" - }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==" - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "stream-combiner2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", - "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", - "requires": { - "duplexer2": "~0.1.0", - "readable-stream": "^2.0.2" - } - }, - "stream-http": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.1.0.tgz", - "integrity": "sha512-cuB6RgO7BqC4FBYzmnvhob5Do3wIdIsXAgGycHJnW+981gHqoYcYz9lqjJrk8WXRddbwPuqPYRl+bag6mYv4lw==", - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^3.0.6", - "xtend": "^4.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "stream-splicer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.1.tgz", - "integrity": "sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg==", - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.2" - } - }, - "string-template": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", - "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=" - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "requires": { - "is-utf8": "^0.2.0" - } - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "requires": { - "get-stdin": "^4.0.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "subarg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", - "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", - "requires": { - "minimist": "^1.1.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "syntax-error": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", - "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", - "requires": { - "acorn-node": "^1.2.0" - } - }, - "terser": { - "version": "4.6.11", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.11.tgz", - "integrity": "sha512-76Ynm7OXUG5xhOpblhytE7X58oeNSmC8xnNhjWVo8CksHit0U0kO4hfNbPrrYwowLWFgM2n9L176VNx2QaHmtA==", - "requires": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "timers-browserify": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", - "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", - "requires": { - "process": "~0.11.0" - } - }, - "tiny-lr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", - "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==", - "requires": { - "body": "^5.1.0", - "debug": "^3.1.0", - "faye-websocket": "~0.10.0", - "livereload-js": "^2.3.0", - "object-assign": "^4.1.0", - "qs": "^6.4.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" - }, - "tsconfig": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-5.0.3.tgz", - "integrity": "sha1-X0J45wGACWeo/Dg/0ZZIh48qbjo=", - "requires": { - "any-promise": "^1.3.0", - "parse-json": "^2.2.0", - "strip-bom": "^2.0.0", - "strip-json-comments": "^2.0.0" - } - }, - "tsify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/tsify/-/tsify-4.0.1.tgz", - "integrity": "sha512-ClznEI+pmwY5wmD0J7HCSVERwkD+l71ch3Dqyod2JuQLEsFaiNDI+vPjaGadsuVFVvmzgoI7HghrBtWsSmCDHQ==", - "requires": { - "convert-source-map": "^1.1.0", - "fs.realpath": "^1.0.0", - "object-assign": "^4.1.0", - "semver": "^5.6.0", - "through2": "^2.0.0", - "tsconfig": "^5.0.3" - } - }, - "tslib": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" - }, - "tslint": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.1.tgz", - "integrity": "sha512-kd6AQ/IgPRpLn6g5TozqzPdGNZ0q0jtXW4//hRcj10qLYBaa3mTUU2y2MCG+RXZm8Zx+KZi0eA+YCrMyNlF4UA==", - "requires": { - "@babel/code-frame": "^7.0.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^4.0.1", - "glob": "^7.1.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.3", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.10.0", - "tsutils": "^2.29.0" - }, - "dependencies": { - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - } - } - }, - "tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", - "requires": { - "tslib": "^1.8.1" - } - }, - "tty-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", - "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==" - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "typedoc": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.17.4.tgz", - "integrity": "sha512-4Lotef1l6lNU5Fulpux809WPlF9CkmcXfv5QFyanrjYlxMFxSdARRdsy8Jv1OU3z0vjR4JsvUQT0YpiPqztcOA==", - "requires": { - "fs-extra": "^8.1.0", - "handlebars": "^4.7.6", - "highlight.js": "^9.18.1", - "lodash": "^4.17.15", - "lunr": "^2.3.8", - "marked": "0.8.2", - "minimatch": "^3.0.0", - "progress": "^2.0.3", - "shelljs": "^0.8.3", - "typedoc-default-themes": "^0.10.0" - } - }, - "typedoc-default-themes": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.10.1.tgz", - "integrity": "sha512-SuqAQI0CkwhqSJ2kaVTgl37cWs733uy9UGUqwtcds8pkFK8oRF4rZmCq+FXTGIb9hIUOu40rf5Kojg0Ha6akeg==", - "requires": { - "lunr": "^2.3.8" - } - }, - "typescript": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", - "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==" - }, - "uglify-js": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.9.1.tgz", - "integrity": "sha512-JUPoL1jHsc9fOjVFHdQIhqEEJsQvfKDjlubcCilu8U26uZ73qOg8VsN8O1jbuei44ZPlwL7kmbAdM4tzaUvqnA==", - "requires": { - "commander": "~2.20.3" - } - }, - "umd": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", - "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==" - }, - "unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" - }, - "undeclared-identifiers": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz", - "integrity": "sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw==", - "requires": { - "acorn-node": "^1.3.0", - "dash-ast": "^1.0.0", - "get-assigned-identifiers": "^1.2.0", - "simple-concat": "^1.0.0", - "xtend": "^4.0.1" - } - }, - "underscore.string": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz", - "integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==", - "requires": { - "sprintf-js": "^1.0.3", - "util-deprecate": "^1.0.2" - } - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" - } - } - }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==" - }, - "uri-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/uri-path/-/uri-path-1.0.0.tgz", - "integrity": "sha1-l0fwGDWJM8Md4PzP2C0TjmcmLjI=" - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" - } - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" - }, - "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "requires": { - "inherits": "2.0.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "version": "14.14.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.7.tgz", + "integrity": "sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg==" }, "uuid": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", - "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" - }, - "v8flags": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", - "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" - }, - "websocket-driver": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", - "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", - "requires": { - "http-parser-js": ">=0.4.0 <0.4.11", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - } - }, - "websocket-extensions": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==" - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - }, - "wildemitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/wildemitter/-/wildemitter-1.2.1.tgz", - "integrity": "sha512-UMmSUoIQSir+XbBpTxOTS53uJ8s/lVhADCkEbhfRjUGFDPme/XGOb0sBWLx5sTz7Wx/2+TlAw1eK9O5lw5PiEw==" - }, - "wolfy87-eventemitter": { - "version": "5.2.9", - "resolved": "https://registry.npmjs.org/wolfy87-eventemitter/-/wolfy87-eventemitter-5.2.9.tgz", - "integrity": "sha512-P+6vtWyuDw+MB01X7UeF8TaHBvbCovf4HPEMF/SV7BdDc1SMTiBy13SRD71lQh4ExFTG1d/WNzDGDCyOKSMblw==" - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } - }, - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" } } }, @@ -12410,60 +9703,35 @@ } } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - } - } - }, "ora": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/ora/-/ora-4.0.3.tgz", - "integrity": "sha512-fnDebVFyz309A73cqCipVL1fBZewq4vwgSHfxh43vVy31mbyoQ8sCH3Oeaog/owYOs/lLlGVPCISQonTneg6Pg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.1.0.tgz", + "integrity": "sha512-9tXIMPvjZ7hPTbk8DFq1f7Kow/HU/pQYB60JbNq+QnGwcyhWVZaQ4hM9zQDEsPxw/muLpgiHSaumUZxCAmod/w==", "dev": true, "requires": { - "chalk": "^3.0.0", + "chalk": "^4.1.0", "cli-cursor": "^3.1.0", - "cli-spinners": "^2.2.0", + "cli-spinners": "^2.4.0", "is-interactive": "^1.0.0", - "log-symbols": "^3.0.0", + "log-symbols": "^4.0.0", "mute-stream": "0.0.8", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" }, "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -12491,19 +9759,10 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -12532,17 +9791,6 @@ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -12559,46 +9807,34 @@ "os-tmpdir": "^1.0.0" } }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "dev": true }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "requires": { - "p-try": "^1.0.0" + "p-try": "^2.0.0" } }, "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, "requires": { - "p-limit": "^1.1.0" + "p-limit": "^2.0.0" } }, "p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, "requires": { "aggregate-error": "^3.0.0" @@ -12614,9 +9850,9 @@ } }, "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, "pacote": { @@ -12680,6 +9916,12 @@ "y18n": "^4.0.0" } }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, "fs-minipass": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", @@ -12695,6 +9937,15 @@ "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", "dev": true }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, "minipass": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", @@ -12801,15 +10052,31 @@ "readable-stream": "^2.1.5" } }, - "parse-asn1": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", - "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "requires": { - "asn1.js": "^4.0.0", + "callsites": "^3.0.0" + }, + "dependencies": { + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + } + } + }, + "parse-asn1": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "dev": true, + "requires": { + "asn1.js": "^5.2.0", "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", "pbkdf2": "^3.0.3", "safe-buffer": "^5.1.1" @@ -12831,6 +10098,41 @@ "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", "optional": true }, + "parse5-html-rewriting-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-6.0.1.tgz", + "integrity": "sha512-vwLQzynJVEfUlURxgnf51yAJDQTtVpNyGD8tKi2Za7m+akukNHxCcUQMAa/mUGLhCeicFdpy7Tlvj8ZNKadprg==", + "dev": true, + "requires": { + "parse5": "^6.0.1", + "parse5-sax-parser": "^6.0.1" + }, + "dependencies": { + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + } + } + }, + "parse5-sax-parser": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-6.0.1.tgz", + "integrity": "sha512-kXX+5S81lgESA0LsDuGjAlBybImAChYRMT+/uKCEXFBFOeEhS52qUCydGhU3qLRD8D9DVjaUo821WK7DM4iCeg==", + "dev": true, + "requires": { + "parse5": "^6.0.1" + }, + "dependencies": { + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + } + } + }, "parseqs": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", @@ -12910,26 +10212,15 @@ "dev": true }, "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true }, "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", + "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", "dev": true, "requires": { "create-hash": "^1.1.2", @@ -12979,71 +10270,31 @@ "dev": true, "requires": { "find-up": "^3.0.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - } } }, - "pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", - "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", + "platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==" + }, + "pnp-webpack-plugin": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", + "integrity": "sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==", "dev": true, "requires": { - "find-up": "^2.1.0" + "ts-pnp": "^1.1.6" } }, "portfinder": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz", - "integrity": "sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==", + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", "dev": true, "requires": { "async": "^2.6.2", "debug": "^3.1.1", - "mkdirp": "^0.5.1" + "mkdirp": "^0.5.5" }, "dependencies": { "debug": { @@ -13064,9 +10315,9 @@ "dev": true }, "postcss": { - "version": "7.0.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.27.tgz", - "integrity": "sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==", + "version": "7.0.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz", + "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -13092,9 +10343,9 @@ } }, "postcss-calc": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.2.tgz", - "integrity": "sha512-rofZFHUg6ZIrvRwPeFktv06GdbDYLcGqh9EwiMutZg+a0oePCCw1zHOEiji6LCpyRcjTREtPASuUqeAvYlEVvQ==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz", + "integrity": "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==", "dev": true, "requires": { "postcss": "^7.0.27", @@ -13197,57 +10448,69 @@ } } }, - "postcss-load-config": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.0.tgz", - "integrity": "sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q==", - "dev": true, - "requires": { - "cosmiconfig": "^5.0.0", - "import-cwd": "^2.0.0" - } - }, "postcss-loader": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", - "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-4.0.4.tgz", + "integrity": "sha512-pntA9zIR14drQo84yGTjQJg1m7T0DkXR4vXYHBngiRZdJtEeCrojL6lOpqUanMzG375lIJbT4Yug85zC/AJWGw==", "dev": true, "requires": { - "loader-utils": "^1.1.0", - "postcss": "^7.0.0", - "postcss-load-config": "^2.0.0", - "schema-utils": "^1.0.0" + "cosmiconfig": "^7.0.0", + "klona": "^2.0.4", + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0", + "semver": "^7.3.2" }, "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", "dev": true, "requires": { - "minimist": "^1.2.0" + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" } }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "import-fresh": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz", + "integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==", "dev": true, "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" } }, + "parse-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", "dev": true, "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" } } } @@ -13384,6 +10647,41 @@ } } }, + "postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true + }, + "postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0" + } + }, "postcss-normalize-charset": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", @@ -13599,14 +10897,15 @@ } }, "postcss-selector-parser": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", - "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", + "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", "dev": true, "requires": { "cssesc": "^3.0.0", "indexes-of": "^1.0.1", - "uniq": "^1.0.1" + "uniq": "^1.0.1", + "util-deprecate": "^1.0.2" } }, "postcss-svgo": { @@ -13641,21 +10940,9 @@ } }, "postcss-value-parser": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz", - "integrity": "sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg==", - "dev": true - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", - "dev": true - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", "dev": true }, "process": { @@ -13670,16 +10957,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dev": true, - "optional": true, - "requires": { - "asap": "~2.0.3" - } - }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -13714,9 +10991,9 @@ } }, "protractor": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/protractor/-/protractor-5.4.3.tgz", - "integrity": "sha512-7pMAolv8Ah1yJIqaorDTzACtn3gk7BamVKPTeO5lqIGOrfosjPgXFx/z1dqSI+m5EeZc2GMJHPr5DYlodujDNA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/protractor/-/protractor-7.0.0.tgz", + "integrity": "sha512-UqkFjivi4GcvUQYzqGYNe0mLzfn5jiLmO8w9nMhQoJRLhy2grJonpga2IWhI6yJO30LibWXJJtA4MOIZD2GgZw==", "dev": true, "requires": { "@types/q": "^0.0.32", @@ -13727,13 +11004,13 @@ "glob": "^7.0.3", "jasmine": "2.8.0", "jasminewd2": "^2.1.0", - "optimist": "~0.6.0", "q": "1.4.1", "saucelabs": "^1.5.0", "selenium-webdriver": "3.6.0", "source-map-support": "~0.4.0", "webdriver-js-extender": "2.1.0", - "webdriver-manager": "^12.0.6" + "webdriver-manager": "^12.1.7", + "yargs": "^15.3.1" }, "dependencies": { "@types/q": { @@ -13742,12 +11019,33 @@ "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=", "dev": true }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", @@ -13761,6 +11059,49 @@ "supports-color": "^2.0.0" } }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "del": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", @@ -13776,6 +11117,16 @@ "rimraf": "^2.2.8" } }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, "globby": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", @@ -13814,6 +11165,30 @@ "path-is-inside": "^1.0.1" } }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -13856,6 +11231,15 @@ "source-map": "^0.5.6" } }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -13880,6 +11264,72 @@ "semver": "^5.3.0", "xml2js": "^0.4.17" } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } } } }, @@ -13917,6 +11367,14 @@ "parse-asn1": "^5.0.0", "randombytes": "^2.0.1", "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } } }, "pump": { @@ -13971,21 +11429,11 @@ "dev": true }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", "dev": true }, - "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", - "dev": true, - "requires": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } - }, "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", @@ -13999,9 +11447,9 @@ "dev": true }, "querystringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", - "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true }, "randombytes": { @@ -14050,33 +11498,24 @@ } }, "raw-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.0.tgz", - "integrity": "sha512-iINUOYvl1cGEmfoaLjnZXt4bKfT2LJnZZib5N/LLyAphC+Dd11vNP9CNVb38j+SAJpFI1uo8j9frmih53ASy7Q==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", + "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", "dev": true, "requires": { - "loader-utils": "^1.2.3", - "schema-utils": "^2.5.0" + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" }, "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", "dev": true, "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" } } } @@ -14098,30 +11537,6 @@ } } }, - "read-package-json": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.1.tgz", - "integrity": "sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A==", - "dev": true, - "requires": { - "glob": "^7.1.1", - "graceful-fs": "^4.1.2", - "json-parse-better-errors": "^1.0.1", - "normalize-package-data": "^2.0.0", - "npm-normalize-package-bin": "^1.0.0" - } - }, - "read-package-tree": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz", - "integrity": "sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==", - "dev": true, - "requires": { - "read-package-json": "^2.0.0", - "readdir-scoped-modules": "^1.0.0", - "util-promisify": "^2.1.0" - } - }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -14137,25 +11552,13 @@ "util-deprecate": "~1.0.1" } }, - "readdir-scoped-modules": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", - "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", - "dev": true, - "requires": { - "debuglog": "^1.0.1", - "dezalgo": "^1.0.0", - "graceful-fs": "^4.1.2", - "once": "^1.3.0" - } - }, "readdirp": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz", - "integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", "dev": true, "requires": { - "picomatch": "^2.0.7" + "picomatch": "^2.2.1" } }, "reflect-metadata": { @@ -14165,9 +11568,9 @@ "dev": true }, "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", "dev": true }, "regenerate-unicode-properties": { @@ -14180,19 +11583,18 @@ } }, "regenerator-runtime": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", "dev": true }, "regenerator-transform": { - "version": "0.14.4", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.4.tgz", - "integrity": "sha512-EaJaKPBI9GvKpvUz2mz4fhx7WPgvwRLY9v3hlNHWmAuJHI13T4nwKnNvm5RWJzEdnI5g5UwtOww+S8IdoUC2bw==", + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", "dev": true, "requires": { - "@babel/runtime": "^7.8.4", - "private": "^0.1.8" + "@babel/runtime": "^7.8.4" } }, "regex-not": { @@ -14205,6 +11607,12 @@ "safe-regex": "^1.1.0" } }, + "regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "dev": true + }, "regexp.prototype.flags": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", @@ -14216,9 +11624,9 @@ } }, "regexpu-core": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz", - "integrity": "sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", + "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", "dev": true, "requires": { "regenerate": "^1.4.0", @@ -14230,9 +11638,9 @@ } }, "regjsgen": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", - "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", + "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", "dev": true }, "regjsparser": { @@ -14296,6 +11704,14 @@ "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + } } }, "require-directory": { @@ -14305,9 +11721,9 @@ "dev": true }, "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, "requires-port": { @@ -14346,6 +11762,84 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, + "resolve-url-loader": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.2.tgz", + "integrity": "sha512-QEb4A76c8Mi7I3xNKXlRKQSlLBwjUV/ULFMP+G7n3/7tJZ8MG5wsZ3ucxP1Jz8Vevn6fnJsxDx9cIls+utGzPQ==", + "dev": true, + "requires": { + "adjust-sourcemap-loader": "3.0.0", + "camelcase": "5.3.1", + "compose-function": "3.0.3", + "convert-source-map": "1.7.0", + "es6-iterator": "2.0.3", + "loader-utils": "1.2.3", + "postcss": "7.0.21", + "rework": "1.0.1", + "rework-visit": "1.0.0", + "source-map": "0.6.1" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "postcss": { + "version": "7.0.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.21.tgz", + "integrity": "sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -14368,6 +11862,36 @@ "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", "dev": true }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rework": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rework/-/rework-1.0.1.tgz", + "integrity": "sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=", + "dev": true, + "requires": { + "convert-source-map": "^0.3.3", + "css": "^2.0.0" + }, + "dependencies": { + "convert-source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz", + "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=", + "dev": true + } + } + }, + "rework-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rework-visit/-/rework-visit-1.0.0.tgz", + "integrity": "sha1-mUWygD8hni96ygCtuLyfZA+ELJo=", + "dev": true + }, "rfdc": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz", @@ -14406,22 +11930,25 @@ } }, "rollup": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.1.0.tgz", - "integrity": "sha512-gfE1455AEazVVTJoeQtcOq/U6GSxwoj4XPSWVsuWmgIxj7sBQNLDOSA82PbdMe+cP8ql8fR1jogPFe8Wg8g4SQ==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.32.1.tgz", + "integrity": "sha512-Op2vWTpvK7t6/Qnm1TTh7VjEZZkN8RWgf0DHbkKzQBwNf748YhXbozHVefqpPp/Fuyk/PQPAnYsBxAEtlMvpUw==", "dev": true, "requires": { "fsevents": "~2.1.2" } }, "run-async": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz", - "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==", - "dev": true, - "requires": { - "is-promise": "^2.1.0" - } + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "run-parallel": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz", + "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==", + "dev": true }, "run-queue": { "version": "1.0.3", @@ -14438,6 +11965,13 @@ "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", "requires": { "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } } }, "safe-buffer": { @@ -14462,52 +11996,37 @@ "dev": true }, "sass": { - "version": "1.26.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.26.3.tgz", - "integrity": "sha512-5NMHI1+YFYw4sN3yfKjpLuV9B5l7MqQ6FlkTcC4FT+oHbBRUZoSjHrrt/mE0nFXJyY2kQtU9ou9HxvFVjLFuuw==", + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.27.0.tgz", + "integrity": "sha512-0gcrER56OkzotK/GGwgg4fPrKuiFlPNitO7eUJ18Bs+/NBlofJfMxmxqpqJxjae9vu0Wq8TZzrSyxZal00WDig==", "dev": true, "requires": { "chokidar": ">=2.0.0 <4.0.0" } }, "sass-loader": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.2.tgz", - "integrity": "sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ==", + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.0.5.tgz", + "integrity": "sha512-2LqoNPtKkZq/XbXNQ4C64GFEleSEHKv6NPSI+bMC/l+jpEXGJhiRYkAQToO24MR7NU4JRY2RpLpJ/gjo2Uf13w==", "dev": true, "requires": { - "clone-deep": "^4.0.1", - "loader-utils": "^1.2.3", - "neo-async": "^2.6.1", - "schema-utils": "^2.6.1", - "semver": "^6.3.0" + "klona": "^2.0.4", + "loader-utils": "^2.0.0", + "neo-async": "^2.6.2", + "schema-utils": "^3.0.0", + "semver": "^7.3.2" }, "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", "dev": true, "requires": { - "minimist": "^1.2.0" + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" } - }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true } } }, @@ -14527,13 +12046,14 @@ "dev": true }, "schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", "dev": true, "requires": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" } }, "select-hose": { @@ -14575,18 +12095,18 @@ } }, "selfsigned": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz", - "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==", + "version": "1.10.8", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz", + "integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==", "dev": true, "requires": { - "node-forge": "0.9.0" + "node-forge": "^0.10.0" } }, "semver": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz", - "integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", "dev": true }, "semver-dsl": { @@ -14670,10 +12190,13 @@ } }, "serialize-javascript": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", - "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", - "dev": true + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } }, "serve-index": { "version": "1.9.1", @@ -14848,9 +12371,9 @@ } }, "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, "smart-buffer": { @@ -14964,6 +12487,12 @@ "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -14988,34 +12517,17 @@ } }, "socket.io": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz", - "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", + "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", "dev": true, "requires": { - "debug": "~3.1.0", - "engine.io": "~3.2.0", + "debug": "~4.1.0", + "engine.io": "~3.4.0", "has-binary2": "~1.0.2", "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.1.1", - "socket.io-parser": "~3.2.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } + "socket.io-client": "2.3.0", + "socket.io-parser": "~3.4.0" } }, "socket.io-adapter": { @@ -15025,76 +12537,39 @@ "dev": true }, "socket.io-client": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz", - "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", + "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", "dev": true, "requires": { "backo2": "1.0.2", "base64-arraybuffer": "0.1.5", "component-bind": "1.0.0", "component-emitter": "1.2.1", - "debug": "~3.1.0", - "engine.io-client": "~3.2.0", + "debug": "~4.1.0", + "engine.io-client": "~3.4.0", "has-binary2": "~1.0.2", "has-cors": "1.1.0", "indexof": "0.0.1", "object-component": "0.0.3", "parseqs": "0.0.5", "parseuri": "0.0.5", - "socket.io-parser": "~3.2.0", + "socket.io-parser": "~3.3.0", "to-array": "0.1.4" }, "dependencies": { + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", + "dev": true + }, "component-emitter": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", "dev": true }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "socket.io-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", - "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", - "dev": true, - "requires": { - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "isarray": "2.0.1" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, "isarray": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", @@ -15106,17 +12581,71 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true + }, + "socket.io-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz", + "integrity": "sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ==", + "dev": true, + "requires": { + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + } + } + }, + "socket.io-parser": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz", + "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", + "dev": true } } }, "sockjs": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", - "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz", + "integrity": "sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==", "dev": true, "requires": { "faye-websocket": "^0.10.0", - "uuid": "^3.0.1" + "uuid": "^3.4.0", + "websocket-driver": "0.6.5" } }, "sockjs-client": { @@ -15184,15 +12713,6 @@ } } }, - "sort-keys": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", - "dev": true, - "requires": { - "is-plain-obj": "^1.0.0" - } - }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -15206,34 +12726,44 @@ "dev": true }, "source-map-loader": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.2.4.tgz", - "integrity": "sha512-OU6UJUty+i2JDpTItnizPrlpOIBLmQbWMuBg9q5bVtnHACqw1tn9nNwqJLbv0/00JjnJb/Ee5g5WS5vrRv7zIQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.1.2.tgz", + "integrity": "sha512-bjf6eSENOYBX4JZDfl9vVLNsGAQ6Uz90fLmOazcmMcyDYOBFsGxPNn83jXezWLY9bJsVAo1ObztxPcV8HAbjVA==", "dev": true, "requires": { - "async": "^2.5.0", - "loader-utils": "^1.1.0" + "abab": "^2.0.5", + "iconv-lite": "^0.6.2", + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0", + "source-map": "^0.6.1", + "whatwg-mimetype": "^2.3.0" }, "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "iconv-lite": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", + "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", "dev": true, "requires": { - "minimist": "^1.2.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" } }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", "dev": true, "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true } } }, @@ -15281,9 +12811,9 @@ "dev": true }, "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", @@ -15291,15 +12821,15 @@ } }, "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", "dev": true }, "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "requires": { "spdx-exceptions": "^2.1.0", @@ -15307,9 +12837,9 @@ } }, "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz", + "integrity": "sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==", "dev": true }, "spdy": { @@ -15353,9 +12883,9 @@ } }, "speed-measure-webpack-plugin": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.3.1.tgz", - "integrity": "sha512-qVIkJvbtS9j/UeZumbdfz0vg+QfG/zxonAjzefZrqzkr7xOncLVXkeGbTpzd1gjCBM4PmVNkWlkeTVhgskAGSQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.3.3.tgz", + "integrity": "sha512-2ljD4Ch/rz2zG3HsLsnPfp23osuPBS0qPuz9sGpkNXTN1Ic4M+W9xB8l8rS8ob2cO4b1L+WTJw/0AJwWYVgcxQ==", "dev": true, "requires": { "chalk": "^2.0.1" @@ -15475,113 +13005,108 @@ "dev": true }, "streamroller": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.6.tgz", - "integrity": "sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.4.tgz", + "integrity": "sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ==", "dev": true, "requires": { - "async": "^2.6.2", - "date-format": "^2.0.0", - "debug": "^3.2.6", - "fs-extra": "^7.0.1", - "lodash": "^4.17.14" + "date-format": "^2.1.0", + "debug": "^4.1.1", + "fs-extra": "^8.1.0" }, "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } + "date-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", + "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==", + "dev": true }, "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", + "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } } } }, - "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", - "dev": true - }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "dev": true, "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "string.prototype.trimend": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", + "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" }, "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" } } } }, - "string.prototype.trimend": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", - "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "string.prototype.trimleft": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", - "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5", - "string.prototype.trimstart": "^1.0.0" - } - }, - "string.prototype.trimright": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", - "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5", - "string.prototype.trimend": "^1.0.0" - } - }, "string.prototype.trimstart": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", - "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", + "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "string_decoder": { @@ -15594,20 +13119,14 @@ } }, "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^2.0.0" + "ansi-regex": "^5.0.0" } }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -15615,33 +13134,24 @@ "dev": true }, "style-loader": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.1.3.tgz", - "integrity": "sha512-rlkH7X/22yuwFYK357fMN/BxYOorfnfq0eD7+vqlemSK4wEcejFF1dg4zxP0euBW8NrYx2WZzZ8PPFevr7D+Kw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz", + "integrity": "sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==", "dev": true, "requires": { - "loader-utils": "^1.2.3", - "schema-utils": "^2.6.4" + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" }, "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", "dev": true, "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" } } } @@ -15671,18 +13181,18 @@ } }, "stylus": { - "version": "0.54.7", - "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.7.tgz", - "integrity": "sha512-Yw3WMTzVwevT6ZTrLCYNHAFmanMxdylelL3hkWNgPMeTCpMwpV3nXjpOHuBXtFv7aiO2xRuQS6OoAdgkNcSNug==", + "version": "0.54.8", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz", + "integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==", "dev": true, "requires": { "css-parse": "~2.0.0", "debug": "~3.1.0", - "glob": "^7.1.3", - "mkdirp": "~0.5.x", + "glob": "^7.1.6", + "mkdirp": "~1.0.4", "safer-buffer": "^2.1.2", "sax": "~1.2.4", - "semver": "^6.0.0", + "semver": "^6.3.0", "source-map": "^0.7.3" }, "dependencies": { @@ -15695,6 +13205,12 @@ "ms": "2.0.0" } }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -15710,34 +13226,27 @@ } }, "stylus-loader": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-3.0.2.tgz", - "integrity": "sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-4.1.1.tgz", + "integrity": "sha512-Vnm7J/nIs/P6swIrdwJW/dflhsCOiFmb1U3PeQ6phRtg1soPLN4uKnnL7AtGIJDe173elbtYIXVzmCyF493CfA==", "dev": true, "requires": { - "loader-utils": "^1.0.2", - "lodash.clonedeep": "^4.5.0", - "when": "~3.6.x" + "fast-glob": "^3.2.4", + "klona": "^2.0.4", + "loader-utils": "^2.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^3.0.0" }, "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", "dev": true, "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" } } } @@ -15773,27 +13282,27 @@ } }, "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz", + "integrity": "sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==", "dev": true }, "tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.1.1.tgz", + "integrity": "sha512-Wib1S8m2wdpLbmQz0RBEVosIyvb/ykfKXf3ZIDqvWoMg/zTNm6G/tDSuUM61J1kNCDXWJrLHGSFeMhAG+gAGpQ==", "dev": true }, "tar": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.1.tgz", - "integrity": "sha512-bKhKrrz2FJJj5s7wynxy/fyxpE0CmCjmOQ1KV4KkgXFWOgoIT/NbTMnB1n+LFNrNk0SSBVGGxcK5AGsyC+pW5Q==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz", + "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==", "dev": true, "requires": { - "chownr": "^1.1.3", + "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^3.0.0", - "minizlib": "^2.1.0", + "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" }, @@ -15807,89 +13316,71 @@ } }, "terser": { - "version": "4.6.10", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.10.tgz", - "integrity": "sha512-qbF/3UOo11Hggsbsqm2hPa6+L4w7bkr+09FNseEe8xrcVD3APGLFqE+Oz1ZKAxjYnFsj80rLOfgAtJ0LNJjtTA==", + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.3.7.tgz", + "integrity": "sha512-lJbKdfxWvjpV330U4PBZStCT9h3N9A4zZVA5Y4k9sCWXknrpdyxi1oMsRKLmQ/YDMDxSBKIh88v0SkdhdqX06w==", "dev": true, "requires": { "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" + "source-map": "~0.7.2", + "source-map-support": "~0.5.19" }, "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } } } }, "terser-webpack-plugin": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.5.tgz", - "integrity": "sha512-WlWksUoq+E4+JlJ+h+U+QUzXpcsMSSNXkDy9lBVkSqDn1w23Gg29L/ary9GeJVYCGiNJJX7LnVc4bwL1N3/g1w==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-4.2.3.tgz", + "integrity": "sha512-jTgXh40RnvOrLQNgIkwEKnQ8rmHjHK4u+6UBEi+W+FPmvb+uo+chJXntKe7/3lW5mNysgSWD60KyesnhW8D6MQ==", "dev": true, "requires": { - "cacache": "^13.0.1", - "find-cache-dir": "^3.2.0", - "jest-worker": "^25.1.0", - "p-limit": "^2.2.2", - "schema-utils": "^2.6.4", - "serialize-javascript": "^2.1.2", + "cacache": "^15.0.5", + "find-cache-dir": "^3.3.1", + "jest-worker": "^26.5.0", + "p-limit": "^3.0.2", + "schema-utils": "^3.0.0", + "serialize-javascript": "^5.0.1", "source-map": "^0.6.1", - "terser": "^4.4.3", + "terser": "^5.3.4", "webpack-sources": "^1.4.3" }, "dependencies": { - "cacache": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz", - "integrity": "sha512-5ZvAxd05HDDU+y9BVvcqYu2LLXmPnQ0hW62h32g4xBTgL/MppR4/04NHfj/ycM2y6lmTnbw6HVi+1eN0Psba6w==", - "dev": true, - "requires": { - "chownr": "^1.1.2", - "figgy-pudding": "^3.5.1", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.2", - "infer-owner": "^1.0.4", - "lru-cache": "^5.1.1", - "minipass": "^3.0.0", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "p-map": "^3.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^2.7.1", - "ssri": "^7.0.0", - "unique-filename": "^1.1.1" - } - }, "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", "dev": true, "requires": { "p-try": "^2.0.0" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", "dev": true, "requires": { - "glob": "^7.1.3" + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" } }, "source-map": { @@ -15898,18 +13389,24 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, - "ssri": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.0.tgz", - "integrity": "sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g==", + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", "dev": true, "requires": { - "figgy-pudding": "^3.5.1", - "minipass": "^3.1.1" + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" } } } }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -15933,9 +13430,9 @@ "dev": true }, "timers-browserify": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", - "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", "dev": true, "requires": { "setimmediate": "^1.0.4" @@ -16050,10 +13547,16 @@ "yn": "3.1.1" } }, + "ts-pnp": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz", + "integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==", + "dev": true + }, "tslib": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" }, "tslint": { "version": "6.1.1", @@ -16081,6 +13584,12 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true } } }, @@ -16091,6 +13600,14 @@ "dev": true, "requires": { "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "tty-browserify": { @@ -16114,6 +13631,12 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "dev": true }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "dev": true + }, "type-fest": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", @@ -16137,9 +13660,9 @@ "dev": true }, "typescript": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", - "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz", + "integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==", "dev": true }, "ua-parser-js": { @@ -16148,12 +13671,6 @@ "integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==", "dev": true }, - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", - "dev": true - }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", @@ -16225,25 +13742,14 @@ } }, "universal-analytics": { - "version": "0.4.20", - "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.4.20.tgz", - "integrity": "sha512-gE91dtMvNkjO+kWsPstHRtSwHXz0l2axqptGYp5ceg4MsuurloM0PU3pdOfpb5zBXUvyjT4PwhWK2m39uczZuw==", + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.4.23.tgz", + "integrity": "sha512-lgMIH7XBI6OgYn1woDEmxhGdj8yDefMKg7GkWdeATAlQZFrMrNyxSkpDzY57iY0/6fdlzTbBV03OawvvzG+q7A==", "dev": true, "requires": { - "debug": "^3.0.0", - "request": "^2.88.0", + "debug": "^4.1.1", + "request": "^2.88.2", "uuid": "^3.0.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } } }, "universalify": { @@ -16301,6 +13807,12 @@ "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -16311,9 +13823,9 @@ "dev": true }, "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", "dev": true, "requires": { "punycode": "^2.1.0" @@ -16382,15 +13894,6 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, - "util-promisify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/util-promisify/-/util-promisify-2.1.0.tgz", - "integrity": "sha1-PCI2R2xNMsX/PEcAKt18E7moKlM=", - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3" - } - }, "util.promisify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", @@ -16470,14 +13973,25 @@ "dev": true }, "watchpack": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz", - "integrity": "sha512-+IF9hfUFOrYOOaKyfaI7h7dquUIOgyEMoQMLA7OP5FxegKA2+XdXThAZ9TU2kucfhDH7rfMHs1oPYziVGWRnZA==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", + "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", "dev": true, "requires": { - "chokidar": "^2.1.8", + "chokidar": "^3.4.1", "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" + "neo-async": "^2.5.0", + "watchpack-chokidar2": "^2.0.1" + } + }, + "watchpack-chokidar2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", + "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", + "dev": true, + "optional": true, + "requires": { + "chokidar": "^2.1.8" }, "dependencies": { "anymatch": { @@ -16485,6 +13999,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "dev": true, + "optional": true, "requires": { "micromatch": "^3.1.4", "normalize-path": "^2.1.1" @@ -16495,6 +14010,7 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "dev": true, + "optional": true, "requires": { "remove-trailing-separator": "^1.0.1" } @@ -16505,13 +14021,15 @@ "version": "1.13.1", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true + "dev": true, + "optional": true }, "braces": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, + "optional": true, "requires": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", @@ -16523,6 +14041,18 @@ "snapdragon-node": "^2.0.1", "split-string": "^3.0.2", "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } } }, "chokidar": { @@ -16530,6 +14060,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "dev": true, + "optional": true, "requires": { "anymatch": "^2.0.0", "async-each": "^1.0.1", @@ -16545,573 +14076,58 @@ "upath": "^1.1.1" } }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dev": true, + "optional": true, "requires": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", "repeat-string": "^1.6.1", "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } } }, "fsevents": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.12.tgz", - "integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", "dev": true, "optional": true, "requires": { - "node-pre-gyp": "*" + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" }, "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dev": true, "optional": true, "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" + "is-extglob": "^2.1.0" } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "3.2.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.9.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.14.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - } - }, - "nopt": { - "version": "4.0.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.7.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.1", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.13", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.1.1", - "bundled": true, - "dev": true, - "optional": true } } }, @@ -17120,6 +14136,7 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", "dev": true, + "optional": true, "requires": { "binary-extensions": "^1.0.0" } @@ -17129,17 +14146,50 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, + "optional": true, "requires": { "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } } }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true, + "optional": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "optional": true, "requires": { - "is-buffer": "^1.1.5" + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" } }, "readdirp": { @@ -17147,6 +14197,7 @@ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", "dev": true, + "optional": true, "requires": { "graceful-fs": "^4.1.11", "micromatch": "^3.1.10", @@ -17158,6 +14209,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "dev": true, + "optional": true, "requires": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" @@ -17194,36 +14246,65 @@ } }, "webpack": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.42.0.tgz", - "integrity": "sha512-EzJRHvwQyBiYrYqhyjW9AqM90dE4+s1/XtCfn7uWg6cS72zH+2VPFAlsnW0+W0cDi0XRjNKUMoJtpSi50+Ph6w==", + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.2.tgz", + "integrity": "sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-module-context": "1.8.5", - "@webassemblyjs/wasm-edit": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5", - "acorn": "^6.2.1", + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/wasm-edit": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "acorn": "^6.4.1", "ajv": "^6.10.2", "ajv-keywords": "^3.4.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.1.0", + "enhanced-resolve": "^4.3.0", "eslint-scope": "^4.0.3", "json-parse-better-errors": "^1.0.2", "loader-runner": "^2.4.0", "loader-utils": "^1.2.3", "memory-fs": "^0.4.1", "micromatch": "^3.1.10", - "mkdirp": "^0.5.1", + "mkdirp": "^0.5.3", "neo-async": "^2.6.1", "node-libs-browser": "^2.2.1", "schema-utils": "^1.0.0", "tapable": "^1.1.3", "terser-webpack-plugin": "^1.4.3", - "watchpack": "^1.6.0", + "watchpack": "^1.7.4", "webpack-sources": "^1.4.1" }, "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "cacache": { "version": "12.0.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", @@ -17247,6 +14328,58 @@ "y18n": "^4.0.0" } }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "enhanced-resolve": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", + "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "dependencies": { + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "find-cache-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", @@ -17258,12 +14391,38 @@ "pkg-dir": "^3.0.0" } }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, "is-wsl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", "dev": true }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, "json5": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", @@ -17284,14 +14443,34 @@ "json5": "^1.0.1" } }, - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" + "yallist": "^3.0.2" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" } }, "rimraf": { @@ -17314,6 +14493,15 @@ "ajv-keywords": "^3.1.0" } }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -17329,22 +14517,65 @@ "figgy-pudding": "^3.5.1" } }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + } + }, "terser-webpack-plugin": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", - "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", + "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", "dev": true, "requires": { "cacache": "^12.0.2", "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", "schema-utils": "^1.0.0", - "serialize-javascript": "^2.1.2", + "serialize-javascript": "^4.0.0", "source-map": "^0.6.1", "terser": "^4.1.2", "webpack-sources": "^1.4.0", "worker-farm": "^1.7.0" } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true } } }, @@ -17361,28 +14592,18 @@ "webpack-log": "^2.0.0" }, "dependencies": { - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", "dev": true } } }, "webpack-dev-server": { - "version": "3.10.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.10.3.tgz", - "integrity": "sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz", + "integrity": "sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==", "dev": true, "requires": { "ansi-html": "0.0.7", @@ -17393,33 +14614,39 @@ "debug": "^4.1.1", "del": "^4.1.1", "express": "^4.17.1", - "html-entities": "^1.2.1", + "html-entities": "^1.3.1", "http-proxy-middleware": "0.19.1", "import-local": "^2.0.0", "internal-ip": "^4.3.0", "ip": "^1.1.5", "is-absolute-url": "^3.0.3", "killable": "^1.0.1", - "loglevel": "^1.6.6", + "loglevel": "^1.6.8", "opn": "^5.5.0", "p-retry": "^3.0.1", - "portfinder": "^1.0.25", + "portfinder": "^1.0.26", "schema-utils": "^1.0.0", "selfsigned": "^1.10.7", "semver": "^6.3.0", "serve-index": "^1.9.1", - "sockjs": "0.3.19", + "sockjs": "0.3.20", "sockjs-client": "1.4.0", - "spdy": "^4.0.1", + "spdy": "^4.0.2", "strip-ansi": "^3.0.1", "supports-color": "^6.1.0", "url": "^0.11.0", "webpack-dev-middleware": "^3.7.2", "webpack-log": "^2.0.0", "ws": "^6.2.1", - "yargs": "12.0.5" + "yargs": "^13.3.2" }, "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", @@ -17463,6 +14690,17 @@ "snapdragon-node": "^2.0.1", "split-string": "^3.0.2", "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } } }, "chokidar": { @@ -17485,15 +14723,6 @@ "upath": "^1.1.1" } }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -17504,554 +14733,44 @@ "is-number": "^3.0.0", "repeat-string": "^1.6.1", "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } } }, "fsevents": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.12.tgz", - "integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", "dev": true, - "optional": true, "requires": { - "node-pre-gyp": "*" + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" }, "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" + "is-extglob": "^2.1.0" } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "3.2.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.9.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.14.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - } - }, - "nopt": { - "version": "4.0.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.7.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.1", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.13", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.1.1", - "bundled": true, - "dev": true, - "optional": true } } }, @@ -18077,15 +14796,44 @@ "dev": true, "requires": { "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } } }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, "requires": { - "is-buffer": "^1.1.5" + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" } }, "readdirp": { @@ -18116,6 +14864,15 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "supports-color": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", @@ -18145,25 +14902,34 @@ "requires": { "ansi-colors": "^3.0.0", "uuid": "^3.3.2" + }, + "dependencies": { + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + } } }, "webpack-merge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", - "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.2.0.tgz", + "integrity": "sha512-QBglJBg5+lItm3/Lopv8KDDK01+hjdg2azEwi/4vKJ8ZmGPdtJsTpjtNNOW3a4WiqzXdCATtTudOZJngE7RKkA==", "dev": true, "requires": { - "lodash": "^4.17.15" + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" } }, "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.0.1.tgz", + "integrity": "sha512-A9oYz7ANQBK5EN19rUXbvNgfdfZf5U2gP0769OXsj9CvYkCR6OHOsd6OKyEy4H38GGxpsQPKIL83NC64QY6Xmw==", "dev": true, "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" }, "dependencies": { "source-map": { @@ -18175,35 +14941,51 @@ } }, "webpack-subresource-integrity": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.4.0.tgz", - "integrity": "sha512-GB1kB/LwAWC3CxwcedGhMkxGpNZxSheCe1q+KJP1bakuieAdX/rGHEcf5zsEzhKXpqsGqokgsDoD9dIkr61VDQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.5.1.tgz", + "integrity": "sha512-uekbQ93PZ9e7BFB8Hl9cFIVYQyQqiXp2ExKk9Zv+qZfH/zHXHrCFAfw1VW0+NqWbTWrs/HnuDrto3+tiPXh//Q==", "dev": true, "requires": { "webpack-sources": "^1.3.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + } } }, "websocket-driver": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", - "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", + "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", "dev": true, "requires": { - "http-parser-js": ">=0.4.0 <0.4.11", - "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, "websocket-extensions": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true }, - "when": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/when/-/when-3.6.4.tgz", - "integrity": "sha1-RztRfsFZ4rhQBUl6E5g/CVQS404=", + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", "dev": true }, "which": { @@ -18221,12 +15003,22 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", "dev": true }, + "wildemitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/wildemitter/-/wildemitter-1.2.1.tgz", + "integrity": "sha512-UMmSUoIQSir+XbBpTxOTS53uJ8s/lVhADCkEbhfRjUGFDPme/XGOb0sBWLx5sTz7Wx/2+TlAw1eK9O5lw5PiEw==" + }, + "wolfy87-eventemitter": { + "version": "5.2.9", + "resolved": "https://registry.npmjs.org/wolfy87-eventemitter/-/wolfy87-eventemitter-5.2.9.tgz", + "integrity": "sha512-P+6vtWyuDw+MB01X7UeF8TaHBvbCovf4HPEMF/SV7BdDc1SMTiBy13SRD71lQh4ExFTG1d/WNzDGDCyOKSMblw==" + }, "worker-farm": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", @@ -18237,9 +15029,9 @@ } }, "worker-plugin": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/worker-plugin/-/worker-plugin-4.0.2.tgz", - "integrity": "sha512-V+1zSZMOOKk+uBzKyNIODLQLsx59zSIOaI75J1EMS0iR1qy+KQR3y/pQ3T0vIhvPfDFapGRMsoMvQNEL3okqSA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/worker-plugin/-/worker-plugin-5.0.0.tgz", + "integrity": "sha512-AXMUstURCxDD6yGam2r4E34aJg6kW85IiaeX72hi+I1cxyaMUtrvVY6sbfpGKAj5e7f68Acl62BjQF5aOOx2IQ==", "dev": true, "requires": { "loader-utils": "^1.1.0" @@ -18268,33 +15060,52 @@ } }, "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", "dev": true, "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" }, "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true }, "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "dev": true, "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" } } } @@ -18354,79 +15165,86 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==", + "dev": true + }, "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "dev": true, "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", + "cliui": "^5.0.0", "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", + "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", + "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^2.0.0", + "string-width": "^3.0.0", "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" }, "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } } } }, "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } } }, "yeast": { diff --git a/openvidu-server/src/dashboard/package.json b/openvidu-server/src/dashboard/package.json index 0591bb1c..0b7a2bea 100644 --- a/openvidu-server/src/dashboard/package.json +++ b/openvidu-server/src/dashboard/package.json @@ -1,55 +1,55 @@ { - "dependencies": { - "@angular/animations": "9.1.2", - "@angular/cdk": "9.2.1", - "@angular/common": "9.1.2", - "@angular/compiler": "9.1.2", - "@angular/core": "9.1.2", - "@angular/flex-layout": "9.0.0-beta.29", - "@angular/forms": "9.1.2", - "@angular/material": "9.2.1", - "@angular/platform-browser": "9.1.2", - "@angular/platform-browser-dynamic": "9.1.2", - "@angular/router": "9.1.2", - "core-js": "3.6.5", - "jquery": "3.5.0", - "openvidu-browser": "2.15.0", - "rxjs": "6.5.5", - "tslib": "1.11.1", - "zone.js": "0.10.3" - }, - "devDependencies": { - "@angular-devkit/build-angular": "0.901.1", - "@angular/cli": "9.1.1", - "@angular/compiler-cli": "9.1.2", - "@angular/language-service": "9.1.2", - "@types/jasmine": "3.5.10", - "@types/node": "13.11.1", - "codelyzer": "5.2.2", - "jasmine-core": "3.5.0", - "jasmine-spec-reporter": "5.0.1", - "karma": "5.0.1", - "karma-chrome-launcher": "3.1.0", - "karma-coverage-istanbul-reporter": "2.1.1", - "karma-jasmine": "3.1.1", - "karma-jasmine-html-reporter": "1.5.3", - "protractor": "5.4.3", - "ts-node": "8.8.2", - "tslint": "6.1.1", - "typescript": "3.8.3" - }, - "license": "Apache-2.0", - "name": "frontend", - "private": true, - "scripts": { - "build": "./node_modules/@angular/cli/bin/ng build --base-href /dashboard/ --output-path ../main/resources/static/dashboard", - "build-prod": "./node_modules/@angular/cli/bin/ng build --prod --base-href /dashboard/ --output-path ../main/resources/static/dashboard", - "e2e": "ng e2e", - "lint": "ng lint", - "ng": "ng", - "postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points", - "start": "ng serve", - "test": "ng test" - }, - "version": "0.0.0" + "dependencies": { + "@angular/animations": "11.0.0", + "@angular/cdk": "11.0.0", + "@angular/common": "11.0.0", + "@angular/compiler": "11.0.0", + "@angular/core": "11.0.0", + "@angular/flex-layout": "11.0.0-beta.33", + "@angular/forms": "11.0.0", + "@angular/material": "11.0.0", + "@angular/platform-browser": "11.0.0", + "@angular/platform-browser-dynamic": "11.0.0", + "@angular/router": "11.0.0", + "core-js": "3.7.0", + "jquery": "3.5.1", + "openvidu-browser": "2.16.0", + "rxjs": "6.6.3", + "tslib": "2.0.3", + "zone.js": "0.11.3" + }, + "devDependencies": { + "@angular-devkit/build-angular": "0.1100.1", + "@angular/cli": "11.0.1", + "@angular/compiler-cli": "11.0.0", + "@angular/language-service": "11.0.0", + "@types/jasmine": "3.6.1", + "@types/node": "14.14.7", + "codelyzer": "6.0.1", + "jasmine-core": "3.6.0", + "jasmine-spec-reporter": "6.0.0", + "karma": "5.2.3", + "karma-chrome-launcher": "3.1.0", + "karma-coverage-istanbul-reporter": "3.0.3", + "karma-jasmine": "4.0.1", + "karma-jasmine-html-reporter": "1.5.4", + "protractor": "7.0.0", + "ts-node": "9.0.0", + "tslint": "6.1.3", + "typescript": "4.0.5" + }, + "license": "Apache-2.0", + "name": "frontend", + "private": true, + "scripts": { + "build": "./node_modules/@angular/cli/bin/ng build --base-href /dashboard/ --output-path ../main/resources/static/dashboard", + "build-prod": "./node_modules/@angular/cli/bin/ng build --prod --base-href /dashboard/ --output-path ../main/resources/static/dashboard", + "e2e": "ng e2e", + "lint": "ng lint", + "ng": "ng", + "postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points", + "start": "ng serve", + "test": "ng test" + }, + "version": "0.0.0" } diff --git a/openvidu-server/src/dashboard/src/app/app.component.spec.ts b/openvidu-server/src/dashboard/src/app/app.component.spec.ts index c740bcd7..ce614fb2 100644 --- a/openvidu-server/src/dashboard/src/app/app.component.spec.ts +++ b/openvidu-server/src/dashboard/src/app/app.component.spec.ts @@ -1,9 +1,9 @@ -import { TestBed, async } from '@angular/core/testing'; +import { TestBed, waitForAsync } from '@angular/core/testing'; import { AppComponent } from './app.component'; describe('AppComponent', () => { - beforeEach(async(() => { + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ AppComponent @@ -11,19 +11,19 @@ describe('AppComponent', () => { }).compileComponents(); })); - it('should create the app', async(() => { + it('should create the app', waitForAsync(() => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.debugElement.componentInstance; expect(app).toBeTruthy(); })); - it(`should have as title 'app works!'`, async(() => { + it(`should have as title 'app works!'`, waitForAsync(() => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.debugElement.componentInstance; expect(app.title).toEqual('app works!'); })); - it('should render title in a h1 tag', async(() => { + it('should render title in a h1 tag', waitForAsync(() => { const fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); const compiled = fixture.debugElement.nativeElement; diff --git a/openvidu-server/src/dashboard/src/app/app.routing.ts b/openvidu-server/src/dashboard/src/app/app.routing.ts index 4b0f7cb1..e508eab1 100644 --- a/openvidu-server/src/dashboard/src/app/app.routing.ts +++ b/openvidu-server/src/dashboard/src/app/app.routing.ts @@ -41,4 +41,4 @@ const appRoutes: Routes = [ } ]; -export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes, { useHash: true }); +export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes, { useHash: true, relativeLinkResolution: 'legacy' }); diff --git a/openvidu-server/src/dashboard/src/app/components/dashboard/dashboard.component.spec.ts b/openvidu-server/src/dashboard/src/app/components/dashboard/dashboard.component.spec.ts index 9c996c37..eae86971 100644 --- a/openvidu-server/src/dashboard/src/app/components/dashboard/dashboard.component.spec.ts +++ b/openvidu-server/src/dashboard/src/app/components/dashboard/dashboard.component.spec.ts @@ -1,4 +1,4 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { DashboardComponent } from './dashboard.component'; @@ -6,7 +6,7 @@ describe('DashboardComponent', () => { let component: DashboardComponent; let fixture: ComponentFixture; - beforeEach(async(() => { + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ DashboardComponent ] }) diff --git a/openvidu-server/src/dashboard/src/app/components/dashboard/dashboard.component.ts b/openvidu-server/src/dashboard/src/app/components/dashboard/dashboard.component.ts index 4e896042..fe0b5787 100644 --- a/openvidu-server/src/dashboard/src/app/components/dashboard/dashboard.component.ts +++ b/openvidu-server/src/dashboard/src/app/components/dashboard/dashboard.component.ts @@ -47,7 +47,7 @@ export class DashboardComponent implements OnInit, OnDestroy { const protocol = location.protocol.includes('https') ? 'wss://' : 'ws://'; const port = (location.port) ? (':' + location.port) : ''; - this.websocket = new WebSocket(protocol + location.hostname + port + '/info'); + this.websocket = new WebSocket(protocol + location.hostname + port + '/openvidu/info'); this.websocket.onopen = (event) => { console.log('Info websocket connected'); diff --git a/openvidu-server/src/dashboard/src/app/components/layouts/layout-base/layout-base.component.spec.ts b/openvidu-server/src/dashboard/src/app/components/layouts/layout-base/layout-base.component.spec.ts index 0aafa469..c8be0254 100644 --- a/openvidu-server/src/dashboard/src/app/components/layouts/layout-base/layout-base.component.spec.ts +++ b/openvidu-server/src/dashboard/src/app/components/layouts/layout-base/layout-base.component.spec.ts @@ -1,4 +1,4 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { LayoutBaseComponent } from './layout-base.component'; @@ -6,7 +6,7 @@ describe('LayoutBaseComponent', () => { let component: LayoutBaseComponent; let fixture: ComponentFixture; - beforeEach(async(() => { + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ LayoutBaseComponent ] }) diff --git a/openvidu-server/src/dashboard/src/app/components/session-details/session-details.component.spec.ts b/openvidu-server/src/dashboard/src/app/components/session-details/session-details.component.spec.ts index 1f1d2fb7..5afebbb3 100644 --- a/openvidu-server/src/dashboard/src/app/components/session-details/session-details.component.spec.ts +++ b/openvidu-server/src/dashboard/src/app/components/session-details/session-details.component.spec.ts @@ -1,4 +1,4 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { SessionDetailsComponent } from './session-details.component'; @@ -6,7 +6,7 @@ describe('SessionDetailsComponent', () => { let component: SessionDetailsComponent; let fixture: ComponentFixture; - beforeEach(async(() => { + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ SessionDetailsComponent ] }) diff --git a/openvidu-server/src/dashboard/src/app/services/rest.service.ts b/openvidu-server/src/dashboard/src/app/services/rest.service.ts index 0e2425d6..0abfea57 100644 --- a/openvidu-server/src/dashboard/src/app/services/rest.service.ts +++ b/openvidu-server/src/dashboard/src/app/services/rest.service.ts @@ -7,6 +7,7 @@ import { throwError } from 'rxjs'; export class RestService { private openviduPublicUrl: string; + private API_PATH: string = 'openvidu/api'; constructor(private httpClient: HttpClient) { } @@ -16,7 +17,7 @@ export class RestService { resolve(this.openviduPublicUrl); } else { this.httpClient.get(location.protocol + '//' + location.hostname + ((!!location.port) ? (':' + location.port) : '') + - '/config/openvidu-publicurl', { responseType: 'text' }).pipe( + '/' + this.API_PATH + '/config/openvidu-publicurl', { responseType: 'text' }).pipe( catchError(error => { reject(error); return throwError(error); @@ -44,7 +45,7 @@ export class RestService { 'Content-Type': 'application/json' }) }; - this.httpClient.post(this.openviduPublicUrl + 'api/sessions', body, options) + this.httpClient.post(this.openviduPublicUrl + this.API_PATH + '/sessions', body, options) .pipe( catchError(error => { reject(error); @@ -66,7 +67,7 @@ export class RestService { 'Content-Type': 'application/json' }) }; - this.httpClient.post(this.openviduPublicUrl + 'api/tokens', body, options) + this.httpClient.post(this.openviduPublicUrl + this.API_PATH + '/tokens', body, options) .pipe( catchError(error => { reject(error); diff --git a/openvidu-server/src/dashboard/tsconfig.json b/openvidu-server/src/dashboard/tsconfig.json index a94b3484..83d411fb 100644 --- a/openvidu-server/src/dashboard/tsconfig.json +++ b/openvidu-server/src/dashboard/tsconfig.json @@ -1,7 +1,7 @@ { "compileOnSave": false, "compilerOptions": { - "module": "esnext", + "module": "es2020", "outDir": "./dist/out-tsc", "baseUrl": "src", "sourceMap": true, @@ -9,7 +9,7 @@ "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, - "target": "es5", + "target": "es2018", "typeRoots": [ "node_modules/@types" ], diff --git a/openvidu-server/src/dashboard/tslint.json b/openvidu-server/src/dashboard/tslint.json index bb84fcf3..eb983623 100644 --- a/openvidu-server/src/dashboard/tslint.json +++ b/openvidu-server/src/dashboard/tslint.json @@ -12,6 +12,9 @@ "curly": true, "eofline": true, "forin": true, + "deprecation": { + "severity": "warning" + }, "import-blacklist": [true], "import-spacing": true, "indent": [ @@ -53,7 +56,6 @@ "no-switch-case-fall-through": true, "no-trailing-whitespace": true, "no-unused-expression": true, - "no-use-before-declare": true, "no-var-keyword": true, "object-literal-sort-keys": false, "one-line": [ diff --git a/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java b/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java index a81943f1..ee044ff1 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java +++ b/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java @@ -34,7 +34,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.DependsOn; @@ -50,7 +52,6 @@ import io.openvidu.server.config.OpenviduConfig.Error; import io.openvidu.server.core.SessionEventsHandler; import io.openvidu.server.core.SessionManager; import io.openvidu.server.core.TokenGenerator; -import io.openvidu.server.core.TokenGeneratorDefault; import io.openvidu.server.coturn.CoturnCredentialsService; import io.openvidu.server.coturn.CoturnCredentialsServiceFactory; import io.openvidu.server.kurento.core.KurentoParticipantEndpointConfig; @@ -61,13 +62,21 @@ import io.openvidu.server.kurento.kms.FixedOneKmsManager; import io.openvidu.server.kurento.kms.KmsManager; import io.openvidu.server.kurento.kms.LoadManager; import io.openvidu.server.recording.DummyRecordingDownloader; +import io.openvidu.server.recording.DummyRecordingUploader; import io.openvidu.server.recording.RecordingDownloader; +import io.openvidu.server.recording.RecordingUploader; import io.openvidu.server.recording.service.RecordingManager; +import io.openvidu.server.recording.service.RecordingManagerUtils; +import io.openvidu.server.recording.service.RecordingManagerUtilsLocalStorage; +import io.openvidu.server.rest.ApiRestPathRewriteFilter; +import io.openvidu.server.rest.RequestMappings; import io.openvidu.server.rpc.RpcHandler; import io.openvidu.server.rpc.RpcNotificationService; import io.openvidu.server.utils.CommandExecutor; import io.openvidu.server.utils.GeoLocationByIp; import io.openvidu.server.utils.GeoLocationByIpDummy; +import io.openvidu.server.utils.LocalCustomFileManager; +import io.openvidu.server.utils.LocalDockerManager; import io.openvidu.server.utils.MediaNodeStatusManager; import io.openvidu.server.utils.MediaNodeStatusManagerDummy; import io.openvidu.server.utils.QuarantineKiller; @@ -86,25 +95,12 @@ public class OpenViduServer implements JsonRpcConfigurer { private static final Logger log = LoggerFactory.getLogger(OpenViduServer.class); - public static final String WS_PATH = "/openvidu"; public static String wsUrl; public static String httpUrl; @Autowired OpenviduConfig config; - @Bean - @ConditionalOnMissingBean - @DependsOn("openviduConfig") - public KmsManager kmsManager(OpenviduConfig openviduConfig) { - if (openviduConfig.getKmsUris().isEmpty()) { - throw new IllegalArgumentException("'KMS_URIS' should contain at least one KMS url"); - } - String firstKmsWsUri = openviduConfig.getKmsUris().get(0); - log.info("OpenVidu Server using one KMS: {}", firstKmsWsUri); - return new FixedOneKmsManager(); - } - @Bean @ConditionalOnMissingBean @DependsOn("openviduConfig") @@ -118,7 +114,8 @@ public class OpenViduServer implements JsonRpcConfigurer { } if (openviduConfig.isWebhookEnabled()) { log.info("OpenVidu Webhook service is enabled"); - loggers.add(new CDRLoggerWebhook(openviduConfig)); + loggers.add(new CDRLoggerWebhook(openviduConfig.getOpenViduWebhookEndpoint(), + openviduConfig.getOpenViduWebhookHeaders(), openviduConfig.getOpenViduWebhookEvents())); } else { log.info("OpenVidu Webhook service is disabled (may be enabled with 'OPENVIDU_WEBHOOK=true')"); } @@ -139,6 +136,18 @@ public class OpenViduServer implements JsonRpcConfigurer { return new KurentoSessionManager(); } + @Bean + @ConditionalOnMissingBean + @DependsOn({ "openviduConfig", "sessionManager", "mediaNodeStatusManager" }) + public KmsManager kmsManager(OpenviduConfig openviduConfig, SessionManager sessionManager) { + if (openviduConfig.getKmsUris().isEmpty()) { + throw new IllegalArgumentException("'KMS_URIS' should contain at least one KMS url"); + } + String firstKmsWsUri = openviduConfig.getKmsUris().get(0); + log.info("OpenVidu Server using one KMS: {}", firstKmsWsUri); + return new FixedOneKmsManager(sessionManager); + } + @Bean @ConditionalOnMissingBean @DependsOn("openviduConfig") @@ -157,14 +166,14 @@ public class OpenViduServer implements JsonRpcConfigurer { @ConditionalOnMissingBean @DependsOn("openviduConfig") public TokenGenerator tokenGenerator() { - return new TokenGeneratorDefault(); + return new TokenGenerator(); } @Bean @ConditionalOnMissingBean @DependsOn("openviduConfig") public RecordingManager recordingManager() { - return new RecordingManager(); + return new RecordingManager(new LocalDockerManager(false), new LocalCustomFileManager()); } @Bean @@ -185,6 +194,20 @@ public class OpenViduServer implements JsonRpcConfigurer { return new KurentoParticipantEndpointConfig(); } + @Bean + @ConditionalOnMissingBean + @DependsOn({ "openviduConfig", "recordingManager" }) + public RecordingManagerUtils recordingManagerUtils(OpenviduConfig openviduConfig, + RecordingManager recordingManager) { + return new RecordingManagerUtilsLocalStorage(openviduConfig, recordingManager); + } + + @Bean + @ConditionalOnMissingBean + public RecordingUploader recordingUpload() { + return new DummyRecordingUploader(); + } + @Bean @ConditionalOnMissingBean public RecordingDownloader recordingDownload() { @@ -203,6 +226,12 @@ public class OpenViduServer implements JsonRpcConfigurer { return new SDPMunging(); } + @Bean + @ConditionalOnMissingBean + public SDPMunging sdpMunging() { + return new SDPMunging(); + } + @Bean @ConditionalOnMissingBean public QuarantineKiller quarantineKiller() { @@ -215,10 +244,20 @@ public class OpenViduServer implements JsonRpcConfigurer { return new MediaNodeStatusManagerDummy(); } + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "SUPPORT_DEPRECATED_API", havingValue = "true") + public FilterRegistrationBean filterRegistrationBean() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean(); + ApiRestPathRewriteFilter apiRestPathRewriteFilter = new ApiRestPathRewriteFilter(); + registrationBean.setFilter(apiRestPathRewriteFilter); + return registrationBean; + } + @Override public void registerJsonRpcHandlers(JsonRpcHandlerRegistry registry) { registry.addHandler(rpcHandler().withPingWatchdog(true).withInterceptors(new HttpHandshakeInterceptor()), - WS_PATH); + RequestMappings.WS_RPC); } public static String getContainerIp() throws IOException, InterruptedException { @@ -324,11 +363,11 @@ public class OpenViduServer implements JsonRpcConfigurer { @EventListener(ApplicationReadyEvent.class) public void whenReady() { - String dashboardUrl = httpUrl + "dashboard/"; + String dashboardUrl = httpUrl + config.getOpenViduFrontendDefaultPath().replaceAll("^/", ""); // @formatter:off String msg = "\n\n----------------------------------------------------\n" + "\n" + " OpenVidu is ready!\n" - + " ---------------------------\n" + "\n" + " * OpenVidu Server: " + httpUrl + "\n" + "\n" + + " ---------------------------\n" + "\n" + " * OpenVidu Server URL: " + httpUrl + "\n" + "\n" + " * OpenVidu Dashboard: " + dashboardUrl + "\n" + "\n" + "----------------------------------------------------\n"; // @formatter:on diff --git a/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventMediaServerCrashed.java b/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventMediaServerCrashed.java new file mode 100644 index 00000000..fec4a96a --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventMediaServerCrashed.java @@ -0,0 +1,31 @@ +package io.openvidu.server.cdr; + +import com.google.gson.JsonObject; + +import io.openvidu.server.kurento.kms.Kms; + +public class CDREventMediaServerCrashed extends CDREvent { + + private Kms kms; + private String environmentId; + + public CDREventMediaServerCrashed(CDREventName eventName, String sessionId, Long timeStamp, Kms kms, + String environmentId) { + super(eventName, sessionId, timeStamp); + this.kms = kms; + this.environmentId = environmentId; + } + + @Override + public JsonObject toJson() { + JsonObject json = super.toJson(); + json.addProperty("id", this.kms.getId()); + if (this.environmentId != null) { + json.addProperty("environmentId", this.environmentId); + } + json.addProperty("ip", this.kms.getIp()); + json.addProperty("uri", this.kms.getUri()); + return json; + } + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventName.java b/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventName.java index 2144b588..6020e3e1 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventName.java +++ b/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventName.java @@ -21,6 +21,6 @@ public enum CDREventName { sessionCreated, sessionDestroyed, participantJoined, participantLeft, webrtcConnectionCreated, webrtcConnectionDestroyed, recordingStarted, recordingStopped, recordingStatusChanged, filterEventDispatched, - mediaNodeStatusChanged, autoscaling + signalSent, mediaNodeStatusChanged, autoscaling, mediaServerCrashed } diff --git a/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventParticipant.java b/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventParticipant.java index 8c4fc5b5..6081aab1 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventParticipant.java +++ b/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventParticipant.java @@ -28,7 +28,7 @@ public class CDREventParticipant extends CDREventEnd { // participantJoined public CDREventParticipant(Participant participant) { - super(CDREventName.participantJoined, participant.getSessionId(), participant.getCreatedAt()); + super(CDREventName.participantJoined, participant.getSessionId(), participant.getActiveAt()); this.participant = participant; } diff --git a/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventSignal.java b/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventSignal.java new file mode 100644 index 00000000..9f3d232d --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventSignal.java @@ -0,0 +1,52 @@ +/* + * (C) Copyright 2017-2020 OpenVidu (https://openvidu.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.openvidu.server.cdr; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +public class CDREventSignal extends CDREvent { + + private String from; + private String[] to; + private String type; + private String data; + + public CDREventSignal(String sessionId, String from, String[] to, String type, String data) { + super(CDREventName.signalSent, sessionId, System.currentTimeMillis()); + this.from = from; + this.to = to; + this.type = type; + this.data = data; + } + + @Override + public JsonObject toJson() { + JsonObject json = super.toJson(); + json.addProperty("from", this.from); + JsonArray toArray = new JsonArray(); + for (String id : this.to) { + toArray.add(id); + } + json.add("to", toArray); + json.addProperty("type", this.type); + json.addProperty("data", this.data); + return json; + } + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventWebrtcConnection.java b/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventWebrtcConnection.java index 4536c9a2..eaa8e9a5 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventWebrtcConnection.java +++ b/openvidu-server/src/main/java/io/openvidu/server/cdr/CDREventWebrtcConnection.java @@ -69,6 +69,7 @@ public class CDREventWebrtcConnection extends CDREventEnd implements Comparable< if (kMediaOptions.rtspUri != null) { json.addProperty("rtspUri", kMediaOptions.rtspUri); json.addProperty("adaptativeBitrate", kMediaOptions.adaptativeBitrate); + json.addProperty("networkCache", kMediaOptions.networkCache); json.addProperty("onlyPlayWithSubscribers", kMediaOptions.onlyPlayWithSubscribers); } } diff --git a/openvidu-server/src/main/java/io/openvidu/server/cdr/CallDetailRecord.java b/openvidu-server/src/main/java/io/openvidu/server/cdr/CallDetailRecord.java index 9228d8fd..990fa4f9 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/cdr/CallDetailRecord.java +++ b/openvidu-server/src/main/java/io/openvidu/server/cdr/CallDetailRecord.java @@ -36,58 +36,15 @@ import io.openvidu.server.core.Participant; import io.openvidu.server.core.Session; import io.openvidu.server.core.SessionManager; import io.openvidu.server.kurento.endpoint.KmsEvent; +import io.openvidu.server.kurento.kms.Kms; import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.service.RecordingManager; import io.openvidu.server.summary.SessionSummary; import io.openvidu.server.webhook.CDRLoggerWebhook; /** - * CDR logger to register all information of a Session. - * Enabled by property 'OPENVIDU_CDR=true' - * - * - 'sessionCreated': {sessionId, timestamp} - * - 'sessionDestroyed': {sessionId, timestamp, startTime, duration, reason} - * - 'participantJoined': {sessionId, timestamp, participantId, location, platform} - * - 'participantLeft': {sessionId, timestamp, participantId, startTime, duration, reason} - * - 'webrtcConnectionCreated' {sessionId, timestamp, participantId, connection, [receivingFrom], audioEnabled, videoEnabled, [videoSource], [videoFramerate]} - * - 'webrtcConnectionDestroyed' {sessionId, timestamp, participantId, startTime, duration, connection, [receivingFrom], audioEnabled, videoEnabled, [videoSource], [videoFramerate], reason} - * - 'recordingStarted' {sessionId, timestamp, id, name, hasAudio, hasVideo, resolution, recordingLayout, size} - * - 'recordingStopped' {sessionId, timestamp, id, name, hasAudio, hasVideo, resolution, recordingLayout, size} - * - 'recordingStatusChanged' {sessionId, timestamp, id, name, hasAudio, hasVideo, resolution, recordingLayout, size, status} - * - 'filterEventDispatched' {sessionId, timestamp, participantId, streamId, filterType, eventType, data} - * - * PROPERTIES VALUES: - * - * - sessionId: string - * - timestamp: number - * - startTime: number - * - duration: number - * - participantId: string - * - connection: "INBOUND", "OUTBOUND" - * - receivingFrom: string - * - audioEnabled: boolean - * - videoEnabled: boolean - * - videoSource: "CAMERA", "SCREEN", "CUSTOM", "IPCAM" - * - videoFramerate: number - * - videoDimensions: string - * - id: string - * - name: string - * - hasAudio: boolean - * - hasVideo: boolean - * - resolution string - * - recordingLayout: string - * - size: number - * - status: string - * - webrtcConnectionDestroyed.reason: "unsubscribe", "unpublish", "disconnect", "networkDisconnect", "mediaServerDisconnect", "openviduServerStopped" - * - participantLeft.reason: "unsubscribe", "unpublish", "disconnect", "networkDisconnect", "mediaServerDisconnect", "openviduServerStopped" - * - sessionDestroyed.reason: "lastParticipantLeft", "mediaServerDisconnect", "openviduServerStopped" - * - recordingStopped.reason: "recordingStoppedByServer", "lastParticipantLeft", "sessionClosedByServer", "automaticStop", "mediaServerDisconnect", "openviduServerStopped" - * - * [OPTIONAL_PROPERTIES]: - * - receivingFrom: only if connection = "INBOUND" - * - videoSource: only if videoEnabled = true - * - videoFramerate: only if videoEnabled = true - * - videoDimensions: only if videoEnabled = true + * CDR logger to register all information of a Session. Enabled by property + * 'OPENVIDU_CDR=true' * * @author Pablo Fuente (pablofuenteperez@gmail.com) */ @@ -228,6 +185,13 @@ public class CallDetailRecord { this.log(new CDREventFilterEvent(sessionId, participantId, streamId, filterType, event)); } + public void recordSignalSent(String sessionId, String from, String[] to, String type, String data) { + if (from != null) { + type = type.replaceFirst("^signal:", ""); + } + this.log(new CDREventSignal(sessionId, from, to, type, data)); + } + protected void log(CDREvent event) { this.loggers.forEach(logger -> { @@ -253,4 +217,10 @@ public class CallDetailRecord { }); } + public void recordMediaServerCrashed(Kms kms, String environmentId, long timeOfKurentoDisconnection) { + CDREvent e = new CDREventMediaServerCrashed(CDREventName.mediaServerCrashed, null, timeOfKurentoDisconnection, + kms, environmentId); + this.log(e); + } + } diff --git a/openvidu-server/src/main/java/io/openvidu/server/config/InfoSocketConfig.java b/openvidu-server/src/main/java/io/openvidu/server/config/InfoSocketConfig.java index 6e39357b..6620a3b5 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/config/InfoSocketConfig.java +++ b/openvidu-server/src/main/java/io/openvidu/server/config/InfoSocketConfig.java @@ -24,13 +24,15 @@ import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; +import io.openvidu.server.rest.RequestMappings; + @Configuration @EnableWebSocket public class InfoSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(infoHandler(), "/info").setAllowedOrigins("*"); + registry.addHandler(infoHandler(), RequestMappings.WS_INFO).setAllowedOrigins("*"); } @Bean diff --git a/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java b/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java index e2b27fe0..40b89bab 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java +++ b/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java @@ -34,14 +34,18 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import javax.annotation.PostConstruct; import com.google.gson.Gson; import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.http.Header; import org.apache.http.message.BasicHeader; import org.kurento.jsonrpc.JsonUtils; @@ -58,6 +62,7 @@ import io.openvidu.server.OpenViduServer; import io.openvidu.server.cdr.CDREventName; import io.openvidu.server.config.Dotenv.DotenvFormatException; import io.openvidu.server.recording.RecordingNotification; +import io.openvidu.server.rest.RequestMappings; @Component public class OpenviduConfig { @@ -103,7 +108,7 @@ public class OpenviduConfig { private List userConfigProps; - private Map propertiesSource; + protected Map propertiesSource; @Autowired protected Environment env; @@ -131,6 +136,8 @@ public class OpenviduConfig { private String openviduRecordingCustomLayout; + private boolean openviduRecordingComposedBasicauth; + private String openviduRecordingVersion; private Integer openviduStreamsVideoMaxRecvBandwidth; @@ -243,6 +250,10 @@ public class OpenviduConfig { return openViduRecordingDebug; } + public boolean isRecordingComposedExternal() { + return false; + } + public String getOpenViduRecordingPath() { return this.openviduRecordingPath; } @@ -259,6 +270,10 @@ public class OpenviduConfig { return this.openviduRecordingCustomLayout; } + public boolean isOpenviduRecordingComposedBasicauth() { + return this.openviduRecordingComposedBasicauth; + } + public String getOpenViduRecordingVersion() { return this.openviduRecordingVersion; } @@ -319,6 +334,14 @@ public class OpenviduConfig { return openviduSessionsGarbageThreshold; } + public VideoCodec getOpenviduForcedCodec() { + return openviduForcedCodec; + } + + public boolean isOpenviduAllowingTranscoding() { + return openviduAllowTranscoding; + } + public String getDotenvPath() { return dotenvPath; } @@ -380,7 +403,7 @@ public class OpenviduConfig { } public String getOpenViduFrontendDefaultPath() { - return "dashboard"; + return RequestMappings.FRONTEND_CE; } // Properties management methods @@ -488,7 +511,8 @@ public class OpenviduConfig { coturnRedisConnectTimeout = getValue("COTURN_REDIS_CONNECT_TIMEOUT"); - openviduSecret = asNonEmptyString("OPENVIDU_SECRET"); + openviduSecret = asNonEmptyAlphanumericString("OPENVIDU_SECRET", + "Cannot be empty and must contain only alphanumeric characters [a-zA-Z0-9], hypens (\"-\") and underscores (\"_\")"); openviduCdr = asBoolean("OPENVIDU_CDR"); openviduCdrPath = openviduCdr ? asWritableFileSystemPath("OPENVIDU_CDR_PATH") @@ -501,6 +525,7 @@ public class OpenviduConfig { openviduRecordingPublicAccess = asBoolean("OPENVIDU_RECORDING_PUBLIC_ACCESS"); openviduRecordingAutostopTimeout = asNonNegativeInteger("OPENVIDU_RECORDING_AUTOSTOP_TIMEOUT"); openviduRecordingCustomLayout = asFileSystemPath("OPENVIDU_RECORDING_CUSTOM_LAYOUT"); + openviduRecordingComposedBasicauth = asBoolean("OPENVIDU_RECORDING_COMPOSED_BASICAUTH"); openviduRecordingVersion = asNonEmptyString("OPENVIDU_RECORDING_VERSION"); openviduRecordingComposedUrl = asOptionalURL("OPENVIDU_RECORDING_COMPOSED_URL"); checkOpenviduRecordingNotification(); @@ -513,9 +538,9 @@ public class OpenviduConfig { openviduSessionsGarbageInterval = asNonNegativeInteger("OPENVIDU_SESSIONS_GARBAGE_INTERVAL"); openviduSessionsGarbageThreshold = asNonNegativeInteger("OPENVIDU_SESSIONS_GARBAGE_THRESHOLD"); - openviduForcedCodec = asEnumValue("OPENVIDU_FORCED_CODEC", VideoCodec.class); - openviduAllowTranscoding = asBoolean("OPENVIDU_ALLOW_TRANSCODING"); - + openviduForcedCodec = asEnumValue("OPENVIDU_STREAMS_FORCED_VIDEO_CODEC", VideoCodec.class); + openviduAllowTranscoding = asBoolean("OPENVIDU_STREAMS_ALLOW_TRANSCODING"); + kmsUrisList = checkKmsUris(); checkCoturnIp(); @@ -748,6 +773,17 @@ public class OpenviduConfig { } } + protected String asNonEmptyAlphanumericString(String property, String errorMessage) { + final String REGEX = "^[a-zA-Z0-9_-]+$"; + String stringValue = getValue(property); + if (stringValue != null && !stringValue.isEmpty() && stringValue.matches(REGEX)) { + return stringValue; + } else { + addError(property, errorMessage); + return null; + } + } + protected String asOptionalString(String property) { return getValue(property); } @@ -881,9 +917,28 @@ public class OpenviduConfig { } } + protected Map asOptionalStringMap(String property) { + Map map = new HashMap<>(); + String str = getValue(property); + if (str != null && !str.isEmpty()) { + try { + Gson gson = new Gson(); + JsonObject jsonObject = gson.fromJson(str, JsonObject.class); + for (Entry entry : jsonObject.entrySet()) { + map.put(entry.getKey(), entry.getValue().getAsString()); + } + return map; + } catch (JsonSyntaxException e) { + addError(property, "Is not a valid map of strings. " + e.getMessage()); + return map; + } + } + return map; + } + public URI checkWebsocketUri(String uri) throws Exception { try { - if (!uri.startsWith("ws://") || uri.startsWith("wss://")) { + if (!StringUtils.startsWithAny(uri, "ws://", "wss://")) { throw new Exception("WebSocket protocol not found"); } String parsedUri = uri.replaceAll("^ws://", "http://").replaceAll("^wss://", "https://"); @@ -953,4 +1008,4 @@ public class OpenviduConfig { return null; } -} \ No newline at end of file +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/config/SecurityConfig.java b/openvidu-server/src/main/java/io/openvidu/server/config/SecurityConfig.java index 74042b51..c06b1a6e 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/config/SecurityConfig.java +++ b/openvidu-server/src/main/java/io/openvidu/server/config/SecurityConfig.java @@ -20,8 +20,12 @@ package io.openvidu.server.config; import java.util.Arrays; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -31,41 +35,45 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; -@Configuration +import io.openvidu.server.rest.ApiRestPathRewriteFilter; +import io.openvidu.server.rest.RequestMappings; + +@Configuration() +@ConditionalOnMissingBean(name = "securityConfigPro") +@Order(Ordered.LOWEST_PRECEDENCE) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired - OpenviduConfig openviduConf; + protected OpenviduConfig openviduConf; + + @Autowired + protected Environment environment; @Override protected void configure(HttpSecurity http) throws Exception { - // Security for API REST ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry conf = http.cors().and() .csrf().disable().authorizeRequests() - // /api - .antMatchers("/api/**").authenticated() - // /config - .antMatchers(HttpMethod.GET, "/config/openvidu-publicurl").permitAll() - .antMatchers(HttpMethod.GET, "/config/**").authenticated() - // /cdr - .antMatchers(HttpMethod.GET, "/cdr/**").authenticated() - // /accept-certificate - .antMatchers(HttpMethod.GET, "/accept-certificate").permitAll() - // Dashboard - .antMatchers(HttpMethod.GET, "/dashboard/**").authenticated(); + .antMatchers(HttpMethod.GET, RequestMappings.API + "/config/openvidu-publicurl").permitAll() + .antMatchers(HttpMethod.GET, RequestMappings.ACCEPT_CERTIFICATE).permitAll() + .antMatchers(RequestMappings.API + "/**").authenticated() + .antMatchers(HttpMethod.GET, RequestMappings.CDR + "/**").authenticated() + .antMatchers(HttpMethod.GET, RequestMappings.FRONTEND_CE + "/**").authenticated() + .antMatchers(HttpMethod.GET, RequestMappings.CUSTOM_LAYOUTS + "/**").authenticated(); - // Security for recording layouts - conf.antMatchers("/layouts/**").authenticated(); - - // Security for recorded video files + // Secure recordings depending on OPENVIDU_RECORDING_PUBLIC_ACCESS if (openviduConf.getOpenViduRecordingPublicAccess()) { - conf = conf.antMatchers("/recordings/**").permitAll(); + conf = conf.antMatchers(HttpMethod.GET, RequestMappings.RECORDINGS + "/**").permitAll(); } else { - conf = conf.antMatchers("/recordings/**").authenticated(); + conf = conf.antMatchers(HttpMethod.GET, RequestMappings.RECORDINGS + "/**").authenticated(); } conf.and().httpBasic(); + + // TODO: remove this when deprecating SUPPORT_DEPRECATED_API + if (Boolean.valueOf(environment.getProperty("SUPPORT_DEPRECATED_API"))) { + ApiRestPathRewriteFilter.protectOldPathsCe(conf, openviduConf); + } } @Bean diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/EndReason.java b/openvidu-server/src/main/java/io/openvidu/server/core/EndReason.java index 34300a83..220a789a 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/EndReason.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/EndReason.java @@ -20,7 +20,7 @@ package io.openvidu.server.core; public enum EndReason { unsubscribe, unpublish, disconnect, forceUnpublishByUser, forceUnpublishByServer, forceDisconnectByUser, - forceDisconnectByServer, lastParticipantLeft, networkDisconnect, mediaServerDisconnect, openviduServerStopped, - recordingStoppedByServer, automaticStop, sessionClosedByServer + forceDisconnectByServer, lastParticipantLeft, networkDisconnect, mediaServerDisconnect, mediaServerCrashed, + openviduServerStopped, recordingStoppedByServer, automaticStop, sessionClosedByServer } diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/Participant.java b/openvidu-server/src/main/java/io/openvidu/server/core/Participant.java index 4c17cebe..8869815b 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/Participant.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/Participant.java @@ -27,18 +27,43 @@ import io.openvidu.server.utils.GeoLocation; public class Participant { + enum ParticipantStatus { + + /** + * The participant has not called Session.publish in the client side yet. The + * internal token is available. + */ + pending, + + /** + * The participant has called Session.publish in the client side and a WebSocket + * connection is established. The internal token has been consumed and cannot be + * used again. + */ + active + } + protected String finalUserId; // ID to match this connection with a final user (HttpSession id) protected String participantPrivatetId; // ID to identify the user on server (org.kurento.jsonrpc.Session.id) protected String participantPublicId; // ID to identify the user on clients - private String sessionId; // ID of the session to which the participant belongs - protected Long createdAt; // Timestamp when this connection was established + protected String sessionId; // ID of the session to which the participant belongs + protected ParticipantStatus status; // Status of the connection + protected Long activeAt; // Timestamp when this connection entered status "active" protected String clientMetadata = ""; // Metadata provided on client side - protected String serverMetadata = ""; // Metadata provided on server side protected Token token; // Token associated to this participant protected GeoLocation location; // Location of the participant protected String platform; // Platform used by the participant to connect to the session protected EndpointType endpointType; // Type of participant (web participant, IP cam participant...) + // TODO + // Unify with "PublisherEndpoint.MediaOptions" + // Also unify "streamPropertyChanged" and "videoData" RPCs when possible + protected Integer videoWidth = 0; + protected Integer videoHeight = 0; + protected Boolean videoActive = false; + protected Boolean audioActive = false; + protected Long publishedAt = null; // Timestamp when this participant was published + protected boolean streaming = false; protected volatile boolean closed = false; @@ -52,23 +77,21 @@ public class Participant { public Participant(String finalUserId, String participantPrivatetId, String participantPublicId, String sessionId, Token token, String clientMetadata, GeoLocation location, String platform, EndpointType endpointType, - Long createdAt) { + Long activeAt) { this.finalUserId = finalUserId; this.participantPrivatetId = participantPrivatetId; this.participantPublicId = participantPublicId; this.sessionId = sessionId; - if (createdAt != null) { - this.createdAt = createdAt; - } else { - this.createdAt = System.currentTimeMillis(); - } + this.status = ParticipantStatus.active; this.token = token; + if (activeAt != null) { + this.activeAt = activeAt; + } else { + this.activeAt = System.currentTimeMillis(); + } if (clientMetadata != null) { this.clientMetadata = clientMetadata; } - if (!token.getServerMetadata().isEmpty()) { - this.serverMetadata = token.getServerMetadata(); - } this.location = location; this.platform = platform; this.endpointType = endpointType; @@ -98,8 +121,8 @@ public class Participant { return sessionId; } - public Long getCreatedAt() { - return this.createdAt; + public Long getActiveAt() { + return this.activeAt; } public String getClientMetadata() { @@ -111,21 +134,13 @@ public class Participant { } public String getServerMetadata() { - return serverMetadata; - } - - public void setServerMetadata(String serverMetadata) { - this.serverMetadata = serverMetadata; + return this.token.getServerMetadata(); } public Token getToken() { return this.token; } - public void setToken(Token token) { - this.token = token; - } - public GeoLocation getLocation() { return this.location; } @@ -146,6 +161,46 @@ public class Participant { return this.endpointType; } + public Integer getVideoWidth() { + return videoWidth; + } + + public void setVideoWidth(Integer videoWidth) { + this.videoWidth = videoWidth; + } + + public Integer getVideoHeight() { + return videoHeight; + } + + public void setVideoHeight(Integer videoHeight) { + this.videoHeight = videoHeight; + } + + public Boolean isVideoActive() { + return videoActive; + } + + public void setVideoActive(Boolean videoActive) { + this.videoActive = videoActive; + } + + public Boolean isAudioActive() { + return audioActive; + } + + public void setPublishedAt(Long publishedAt) { + this.publishedAt = publishedAt; + } + + public Long getPublishedAt() { + return publishedAt; + } + + public void setAudioActive(Boolean audioActive) { + this.audioActive = audioActive; + } + public boolean isStreaming() { return streaming; } @@ -155,7 +210,7 @@ public class Participant { } public boolean isIpcam() { - return this.platform.equals("IPCAM") && this.participantPrivatetId.startsWith(IdentifierPrefixes.IPCAM_ID); + return this.platform != null && this.platform.equals("IPCAM") && this.participantPrivatetId.startsWith(IdentifierPrefixes.IPCAM_ID); } public String getPublisherStreamId() { @@ -163,17 +218,26 @@ public class Participant { } public String getFullMetadata() { - String fullMetadata; - if ((!this.clientMetadata.isEmpty()) && (!this.serverMetadata.isEmpty())) { - fullMetadata = this.clientMetadata + METADATA_SEPARATOR + this.serverMetadata; - } else { - fullMetadata = this.clientMetadata + this.serverMetadata; + String fullMetadata = ""; + if (this.clientMetadata != null && !this.clientMetadata.isEmpty()) { + // Client data defined + fullMetadata += this.clientMetadata; + } + if (this.token.getServerMetadata() != null && !this.token.getServerMetadata().isEmpty()) { + // Server data defined + if (fullMetadata.isEmpty()) { + // Only server data + fullMetadata += this.token.getServerMetadata(); + } else { + // Both client data and server data + fullMetadata += METADATA_SEPARATOR + this.token.getServerMetadata(); + } } return fullMetadata; } public void deleteIpcamProperties() { - this.clientMetadata = ""; + this.clientMetadata = null; this.token.setToken(null); } @@ -235,17 +299,32 @@ public class Participant { public JsonObject toJson() { JsonObject json = new JsonObject(); - json.addProperty("connectionId", this.participantPublicId); - json.addProperty("createdAt", this.createdAt); + // COMMON + json.addProperty("id", this.participantPublicId); + json.addProperty("object", "connection"); + json.addProperty("status", this.status.name()); + json.addProperty("connectionId", this.participantPublicId); // TODO: deprecated. Better use only "id" + json.addProperty("sessionId", this.sessionId); + json.addProperty("createdAt", this.token.getCreatedAt()); + json.addProperty("activeAt", this.activeAt); json.addProperty("location", this.location != null ? this.location.toString() : "unknown"); json.addProperty("platform", this.platform); if (this.token.getToken() != null) { json.addProperty("token", this.token.getToken()); + } else { + json.add("token", null); } - json.addProperty("role", this.token.getRole().name()); - json.addProperty("serverData", this.serverMetadata); + // Add all ConnectionProperties + JsonObject connectionPropertiesJson = this.token.getConnectionPropertiesWithFinalJsonFormat(); + connectionPropertiesJson.entrySet().forEach(entry -> { + json.add(entry.getKey(), entry.getValue()); + }); json.addProperty("clientData", this.clientMetadata); return json; } + public JsonObject withStatsToJson() { + return null; + } + } diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/Session.java b/openvidu-server/src/main/java/io/openvidu/server/core/Session.java index bf0c0807..5436be4d 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/Session.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/Session.java @@ -18,6 +18,8 @@ package io.openvidu.server.core; import java.util.HashSet; +import java.util.Iterator; +import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -27,7 +29,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.function.Function; +import java.util.stream.Collectors; import com.google.gson.JsonArray; import com.google.gson.JsonObject; @@ -41,7 +43,6 @@ import io.openvidu.client.internal.ProtocolElements; import io.openvidu.java.client.RecordingLayout; import io.openvidu.java.client.SessionProperties; import io.openvidu.server.config.OpenviduConfig; -import io.openvidu.server.kurento.core.KurentoParticipant; import io.openvidu.server.recording.service.RecordingManager; import io.openvidu.server.utils.RecordingUtils; @@ -60,6 +61,7 @@ public class Session implements SessionInterface { protected volatile boolean closed = false; protected AtomicInteger activePublishers = new AtomicInteger(0); + protected AtomicInteger activeIndividualRecordedPublishers = new AtomicInteger(0); /** * This lock protects the following operations with read lock: [REST API](POST @@ -145,28 +147,50 @@ public class Session implements SessionInterface { return activePublishers.get(); } - public void registerPublisher() { - this.activePublishers.incrementAndGet(); + public int getActiveIndividualRecordedPublishers() { + return activeIndividualRecordedPublishers.get(); } - public void deregisterPublisher() { + public void registerPublisher(Participant participant) { + this.activePublishers.incrementAndGet(); + if (participant.getToken().record()) { + activeIndividualRecordedPublishers.incrementAndGet(); + } + } + + public void deregisterPublisher(Participant participant) { this.activePublishers.decrementAndGet(); + if (participant.getToken().record()) { + activeIndividualRecordedPublishers.decrementAndGet(); + } } public void storeToken(Token token) { this.tokens.put(token.getToken(), token); } - public boolean isTokenValid(String token) { - return this.tokens.containsKey(token); + public boolean deleteTokenFromConnectionId(String connectionId) { + boolean deleted = false; + Iterator> iterator = this.tokens.entrySet().iterator(); + while (iterator.hasNext() && !deleted) { + Entry entry = iterator.next(); + if (connectionId.equals(entry.getValue().getConnectionId())) { + iterator.remove(); + deleted = true; + } + } + return deleted; } public Token consumeToken(String token) { Token tokenObj = this.tokens.remove(token); - showTokens("Token consumed"); return tokenObj; } + public Iterator> getTokenIterator() { + return this.tokens.entrySet().iterator(); + } + public void showTokens(String preMessage) { log.info("{} { Session: {} | Tokens: {} }", preMessage, this.sessionId, this.tokens.keySet().toString()); } @@ -185,17 +209,34 @@ public class Session implements SessionInterface { } } - public JsonObject toJson() { - return this.sharedJson(KurentoParticipant::toJson); + public JsonArray getSnapshotOfConnectionsAsJsonArray(boolean withPendingConnections, boolean withWebrtcStats) { + + Set snapshotOfActiveConnections = this.getParticipants().stream().collect(Collectors.toSet()); + JsonArray jsonArray = new JsonArray(); + snapshotOfActiveConnections.forEach(participant -> { + // Filter recorder participant + if (!ProtocolElements.RECORDER_PARTICIPANT_PUBLICID.equals(participant.getParticipantPublicId())) { + jsonArray.add(withWebrtcStats ? participant.withStatsToJson() : participant.toJson()); + } + }); + + if (withPendingConnections) { + Set snapshotOfPendingConnections = this.tokens.values().stream().collect(Collectors.toSet()); + // Eliminate duplicates in case some concurrent situation took place + Set activeConnectionIds = snapshotOfActiveConnections.stream() + .map(participant -> participant.getParticipantPublicId()).collect(Collectors.toSet()); + snapshotOfPendingConnections.removeIf(token -> activeConnectionIds.contains(token.getConnectionId())); + snapshotOfPendingConnections.forEach(token -> jsonArray.add(token.toJsonAsParticipant())); + } + + return jsonArray; } - public JsonObject withStatsToJson() { - return this.sharedJson(KurentoParticipant::withStatsToJson); - } - - private JsonObject sharedJson(Function toJsonFunction) { + public JsonObject toJson(boolean withPendingConnections, boolean withWebrtcStats) { JsonObject json = new JsonObject(); - json.addProperty("sessionId", this.sessionId); + json.addProperty("id", this.sessionId); + json.addProperty("object", "session"); + json.addProperty("sessionId", this.sessionId); // TODO: deprecated. Better use only "id" json.addProperty("createdAt", this.startTime); json.addProperty("mediaMode", this.sessionProperties.mediaMode().name()); json.addProperty("recordingMode", this.sessionProperties.recordingMode().name()); @@ -212,16 +253,15 @@ public class Session implements SessionInterface { json.addProperty("customSessionId", this.sessionProperties.customSessionId()); } JsonObject connections = new JsonObject(); - JsonArray participants = new JsonArray(); - this.participants.values().forEach(p -> { - if (!ProtocolElements.RECORDER_PARTICIPANT_PUBLICID.equals(p.getParticipantPublicId())) { - participants.add(toJsonFunction.apply((KurentoParticipant) p)); - } - }); + JsonArray participants = this.getSnapshotOfConnectionsAsJsonArray(withPendingConnections, withWebrtcStats); connections.addProperty("numberOfElements", participants.size()); connections.add("content", participants); json.add("connections", connections); json.addProperty("recording", this.recordingManager.sessionIsBeingRecorded(this.sessionId)); + if (this.sessionProperties.forcedVideoCodec() != null) { + json.addProperty("forcedVideoCodec", this.sessionProperties.forcedVideoCodec().name()); + } + json.addProperty("allowTranscoding", this.sessionProperties.isTranscodingAllowed()); return json; } diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/SessionEventsHandler.java b/openvidu-server/src/main/java/io/openvidu/server/core/SessionEventsHandler.java index 48c6b555..453033ab 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/SessionEventsHandler.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/SessionEventsHandler.java @@ -22,7 +22,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import org.kurento.client.GenericMediaEvent; @@ -39,11 +38,11 @@ import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.internal.ProtocolElements; import io.openvidu.java.client.OpenViduRole; import io.openvidu.server.cdr.CallDetailRecord; -import io.openvidu.server.config.InfoHandler; import io.openvidu.server.config.OpenviduBuildInfo; import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.kurento.core.KurentoParticipant; import io.openvidu.server.kurento.endpoint.KurentoFilter; +import io.openvidu.server.kurento.kms.Kms; import io.openvidu.server.recording.Recording; import io.openvidu.server.rpc.RpcNotificationService; @@ -54,9 +53,6 @@ public class SessionEventsHandler { @Autowired protected RpcNotificationService rpcNotificationService; - @Autowired - protected InfoHandler infoHandler; - @Autowired protected CallDetailRecord CDR; @@ -66,9 +62,7 @@ public class SessionEventsHandler { @Autowired protected OpenviduBuildInfo openviduBuildConfig; - Map recordingsStarted = new ConcurrentHashMap<>(); - - ReentrantLock lock = new ReentrantLock(); + protected Map recordingsToSendClientEvents = new ConcurrentHashMap<>(); public void onSessionCreated(Session session) { CDR.recordSessionCreated(session); @@ -93,7 +87,7 @@ public class SessionEventsHandler { participantJson.addProperty(ProtocolElements.JOINROOM_PEERID_PARAM, existingParticipant.getParticipantPublicId()); participantJson.addProperty(ProtocolElements.JOINROOM_PEERCREATEDAT_PARAM, - existingParticipant.getCreatedAt()); + existingParticipant.getActiveAt()); // Metadata associated to each existing participant participantJson.addProperty(ProtocolElements.JOINROOM_METADATA_PARAM, @@ -149,7 +143,7 @@ public class SessionEventsHandler { // Metadata associated to new participant notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_USER_PARAM, participant.getParticipantPublicId()); - notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_CREATEDAT_PARAM, participant.getCreatedAt()); + notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_CREATEDAT_PARAM, participant.getActiveAt()); notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_METADATA_PARAM, participant.getFullMetadata()); @@ -158,11 +152,27 @@ public class SessionEventsHandler { } } result.addProperty(ProtocolElements.PARTICIPANTJOINED_USER_PARAM, participant.getParticipantPublicId()); - result.addProperty(ProtocolElements.PARTICIPANTJOINED_CREATEDAT_PARAM, participant.getCreatedAt()); + result.addProperty(ProtocolElements.PARTICIPANTJOINED_CREATEDAT_PARAM, participant.getActiveAt()); result.addProperty(ProtocolElements.PARTICIPANTJOINED_METADATA_PARAM, participant.getFullMetadata()); - result.addProperty(ProtocolElements.JOINROOM_OPENVIDUSERVERVERSION_PARAM, + result.add(ProtocolElements.PARTICIPANTJOINED_VALUE_PARAM, resultArray); + + result.addProperty(ProtocolElements.PARTICIPANTJOINED_SESSION_PARAM, participant.getSessionId()); + result.addProperty(ProtocolElements.PARTICIPANTJOINED_VERSION_PARAM, openviduBuildConfig.getOpenViduServerVersion()); - result.add("value", resultArray); + if (participant.getToken() != null) { + result.addProperty(ProtocolElements.PARTICIPANTJOINED_RECORD_PARAM, participant.getToken().record()); + if (participant.getToken().getRole() != null) { + result.addProperty(ProtocolElements.PARTICIPANTJOINED_ROLE_PARAM, + participant.getToken().getRole().name()); + } + result.addProperty(ProtocolElements.PARTICIPANTJOINED_COTURNIP_PARAM, openviduConfig.getCoturnIp()); + if (participant.getToken().getTurnCredentials() != null) { + result.addProperty(ProtocolElements.PARTICIPANTJOINED_TURNUSERNAME_PARAM, + participant.getToken().getTurnCredentials().getUsername()); + result.addProperty(ProtocolElements.PARTICIPANTJOINED_TURNCREDENTIAL_PARAM, + participant.getToken().getTurnCredentials().getCredential()); + } + } rpcNotificationService.sendResponse(participant.getParticipantPrivateId(), transactionId, result); } @@ -306,16 +316,10 @@ public class SessionEventsHandler { rpcNotificationService.sendResponse(participant.getParticipantPrivateId(), transactionId, result); if (ProtocolElements.RECORDER_PARTICIPANT_PUBLICID.equals(participant.getParticipantPublicId())) { - lock.lock(); - try { - Recording recording = this.recordingsStarted.remove(session.getSessionId()); - if (recording != null) { - // RECORDER participant is now receiving video from the first publisher - this.sendRecordingStartedNotification(session, recording); - } - } finally { - lock.unlock(); - } + recordingsToSendClientEvents.computeIfPresent(session.getSessionId(), (key, value) -> { + sendRecordingStartedNotification(session, value); + return null; + }); } } @@ -328,7 +332,7 @@ public class SessionEventsHandler { } public void onSendMessage(Participant participant, JsonObject message, Set participants, - Integer transactionId, OpenViduException error) { + String sessionId, Integer transactionId, OpenViduException error) { boolean isRpcCall = transactionId != null; if (isRpcCall) { @@ -339,16 +343,22 @@ public class SessionEventsHandler { } } + String from = null; + String type = null; + String data = null; + JsonObject params = new JsonObject(); if (message.has("data")) { - params.addProperty(ProtocolElements.PARTICIPANTSENDMESSAGE_DATA_PARAM, message.get("data").getAsString()); + data = message.get("data").getAsString(); + params.addProperty(ProtocolElements.PARTICIPANTSENDMESSAGE_DATA_PARAM, data); } if (message.has("type")) { - params.addProperty(ProtocolElements.PARTICIPANTSENDMESSAGE_TYPE_PARAM, message.get("type").getAsString()); + type = message.get("type").getAsString(); + params.addProperty(ProtocolElements.PARTICIPANTSENDMESSAGE_TYPE_PARAM, type); } if (participant != null) { - params.addProperty(ProtocolElements.PARTICIPANTSENDMESSAGE_FROM_PARAM, - participant.getParticipantPublicId()); + from = participant.getParticipantPublicId(); + params.addProperty(ProtocolElements.PARTICIPANTSENDMESSAGE_FROM_PARAM, from); } Set toSet = new HashSet(); @@ -367,6 +377,7 @@ public class SessionEventsHandler { if (toSet.isEmpty()) { for (Participant p : participants) { + toSet.add(p.getParticipantPublicId()); rpcNotificationService.sendNotification(p.getParticipantPrivateId(), ProtocolElements.PARTICIPANTSENDMESSAGE_METHOD, params); } @@ -389,6 +400,8 @@ public class SessionEventsHandler { if (isRpcCall) { rpcNotificationService.sendResponse(participant.getParticipantPrivateId(), transactionId, new JsonObject()); } + + CDR.recordSignalSent(sessionId, from, toSet.toArray(new String[toSet.size()]), type, data); } public void onStreamPropertyChanged(Participant participant, Integer transactionId, Set participants, @@ -472,7 +485,7 @@ public class SessionEventsHandler { public void sendRecordingStoppedNotification(Session session, Recording recording, EndReason reason) { // Be sure to clean this map (this should return null) - this.recordingsStarted.remove(session.getSessionId()); + recordingsToSendClientEvents.remove(session.getSessionId()); // Filter participants by roles according to "OPENVIDU_RECORDING_NOTIFICATION" Set existingParticipants; @@ -578,15 +591,37 @@ public class SessionEventsHandler { } } + public void onVideoData(Participant participant, Integer transactionId, Integer height, Integer width, + Boolean videoActive, Boolean audioActive) { + participant.setVideoHeight(height); + participant.setVideoWidth(width); + participant.setVideoActive(videoActive); + participant.setAudioActive(audioActive); + log.info( + "Video data of participant {} was initialized. height:{}, width:{}, isVideoActive: {}, isAudioActive: {}", + participant.getParticipantPublicId(), height, width, videoActive, audioActive); + rpcNotificationService.sendResponse(participant.getParticipantPrivateId(), transactionId, new JsonObject()); + } + public void closeRpcSession(String participantPrivateId) { this.rpcNotificationService.closeRpcSession(participantPrivateId); } - public void setRecordingStarted(String sessionId, Recording recording) { - this.recordingsStarted.put(sessionId, recording); + public void storeRecordingToSendClientEvent(Recording recording) { + recordingsToSendClientEvents.put(recording.getSessionId(), recording); } - private Set filterParticipantsByRole(OpenViduRole[] roles, Set participants) { + public void onNetworkQualityLevelChanged(Session session, JsonObject params) { + } + + public void onConnectionPropertyChanged(Participant participant, String property, Object newValue) { + } + + public void onMediaServerCrashed(Kms kms, long timeOfKurentoDisconnection) { + CDR.recordMediaServerCrashed(kms, null, timeOfKurentoDisconnection); + } + + protected Set filterParticipantsByRole(OpenViduRole[] roles, Set participants) { return participants.stream().filter(part -> { if (ProtocolElements.RECORDER_PARTICIPANT_PUBLICID.equals(part.getParticipantPublicId())) { return false; diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/SessionInterface.java b/openvidu-server/src/main/java/io/openvidu/server/core/SessionInterface.java index fa398f22..08d1d805 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/SessionInterface.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/SessionInterface.java @@ -47,9 +47,7 @@ public interface SessionInterface { String getMediaNodeId(); - JsonObject toJson(); - - JsonObject withStatsToJson(); + JsonObject toJson(boolean withPendingConnections, boolean withWebrtcStats); Long getStartTime(); diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java b/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java index a72f1166..9a795886 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java @@ -27,6 +27,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.PostConstruct; @@ -37,7 +38,6 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; -import org.apache.commons.lang3.RandomStringUtils; import org.kurento.jsonrpc.message.Request; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,13 +46,14 @@ import org.springframework.beans.factory.annotation.Autowired; import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.internal.ProtocolElements; +import io.openvidu.java.client.ConnectionProperties; +import io.openvidu.java.client.KurentoOptions; import io.openvidu.java.client.OpenViduRole; import io.openvidu.java.client.Recording; import io.openvidu.java.client.SessionProperties; import io.openvidu.server.cdr.CDREventRecording; import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.coturn.CoturnCredentialsService; -import io.openvidu.server.kurento.core.KurentoTokenOptions; import io.openvidu.server.kurento.endpoint.EndpointType; import io.openvidu.server.recording.service.RecordingManager; import io.openvidu.server.utils.FormatChecker; @@ -88,6 +89,8 @@ public abstract class SessionManager { public FormatChecker formatChecker = new FormatChecker(); + private UpdatableTimerTask sessionGarbageCollectorTimer; + final protected ConcurrentMap sessions = new ConcurrentHashMap<>(); final protected ConcurrentMap sessionsNotActive = new ConcurrentHashMap<>(); protected ConcurrentMap> sessionidParticipantpublicidParticipant = new ConcurrentHashMap<>(); @@ -115,7 +118,7 @@ public abstract class SessionManager { public void sendMessage(String message, String sessionId) { try { JsonObject messageJson = JsonParser.parseString(message).getAsJsonObject(); - sessionEventsHandler.onSendMessage(null, messageJson, getParticipants(sessionId), null, null); + sessionEventsHandler.onSendMessage(null, messageJson, getParticipants(sessionId), sessionId, null, null); } catch (JsonSyntaxException | IllegalStateException e) { throw new OpenViduException(Code.SIGNAL_FORMAT_INVALID_ERROR_CODE, "Provided signal object '" + message + "' has not a valid JSON format"); @@ -126,7 +129,7 @@ public abstract class SessionManager { try { JsonObject messageJson = JsonParser.parseString(message).getAsJsonObject(); sessionEventsHandler.onSendMessage(participant, messageJson, getParticipants(participant.getSessionId()), - transactionId, null); + participant.getSessionId(), transactionId, null); } catch (JsonSyntaxException | IllegalStateException e) { throw new OpenViduException(Code.SIGNAL_FORMAT_INVALID_ERROR_CODE, "Provided signal object '" + message + "' has not a valid JSON format"); @@ -160,8 +163,8 @@ public abstract class SessionManager { public abstract void removeFilterEventListener(Session session, Participant subscriber, String streamId, String eventType); - public abstract Participant publishIpcam(Session session, MediaOptions mediaOptions, String serverMetadata) - throws Exception; + public abstract Participant publishIpcam(Session session, MediaOptions mediaOptions, + ConnectionProperties connectionProperties) throws Exception; public abstract void reconnectStream(Participant participant, String streamId, String sdpOffer, Integer transactionId); @@ -169,6 +172,9 @@ public abstract class SessionManager { public abstract String getParticipantPrivateIdFromStreamId(String sessionId, String streamId) throws OpenViduException; + public abstract void onVideoData(Participant participant, Integer transactionId, Integer height, Integer width, + Boolean videoActive, Boolean audioActive); + /** * Returns a Session given its id * @@ -297,22 +303,23 @@ public abstract class SessionManager { return sessionNotActive; } - public String newToken(Session session, OpenViduRole role, String serverMetadata, - KurentoTokenOptions kurentoTokenOptions) throws Exception { + public Token newToken(Session session, OpenViduRole role, String serverMetadata, boolean record, + KurentoOptions kurentoOptions) throws Exception { if (!formatChecker.isServerMetadataFormatCorrect(serverMetadata)) { log.error("Data invalid format"); throw new OpenViduException(Code.GENERIC_ERROR_CODE, "Data invalid format"); } - Token tokenObj = tokenGenerator.generateToken(session.getSessionId(), role, serverMetadata, - kurentoTokenOptions); + Token tokenObj = tokenGenerator.generateToken(session.getSessionId(), serverMetadata, record, role, + kurentoOptions); session.storeToken(tokenObj); session.showTokens("Token created"); - return tokenObj.getToken(); + return tokenObj; } - public Token newTokenForInsecureUser(Session session, String token, String serverMetadata) throws Exception { - Token tokenObj = new Token(token, OpenViduRole.PUBLISHER, serverMetadata != null ? serverMetadata : "", - this.openviduConfig.isTurnadminAvailable() ? this.coturnCredentialsService.createUser() : null, null); + public Token newTokenForInsecureUser(Session session, String token, ConnectionProperties connectionProperties) + throws Exception { + Token tokenObj = new Token(token, session.getSessionId(), connectionProperties, + this.openviduConfig.isTurnadminAvailable() ? this.coturnCredentialsService.createUser() : null); session.storeToken(tokenObj); session.showTokens("Token created for insecure user"); return tokenObj; @@ -357,17 +364,13 @@ public abstract class SessionManager { public Participant newParticipant(String sessionId, String participantPrivatetId, Token token, String clientMetadata, GeoLocation location, String platform, String finalUserId) { + if (this.sessionidParticipantpublicidParticipant.get(sessionId) != null) { - String participantPublicId = IdentifierPrefixes.PARTICIPANT_PUBLIC_ID - + RandomStringUtils.randomAlphabetic(1).toUpperCase() + RandomStringUtils.randomAlphanumeric(9); - Participant p = new Participant(finalUserId, participantPrivatetId, participantPublicId, sessionId, token, - clientMetadata, location, platform, EndpointType.WEBRTC_ENDPOINT, null); - while (this.sessionidParticipantpublicidParticipant.get(sessionId).putIfAbsent(participantPublicId, - p) != null) { - participantPublicId = IdentifierPrefixes.PARTICIPANT_PUBLIC_ID - + RandomStringUtils.randomAlphabetic(1).toUpperCase() + RandomStringUtils.randomAlphanumeric(9); - p.setParticipantPublicId(participantPublicId); - } + + Participant p = new Participant(finalUserId, participantPrivatetId, token.getConnectionId(), sessionId, + token, clientMetadata, location, platform, EndpointType.WEBRTC_ENDPOINT, null); + + this.sessionidParticipantpublicidParticipant.get(sessionId).put(p.getParticipantPublicId(), p); this.sessionidFinalUsers.get(sessionId).computeIfAbsent(finalUserId, k -> { log.info("Participant {} of session {} is a final user connecting to this session for the first time", @@ -376,6 +379,7 @@ public abstract class SessionManager { }).addConnectionIfAbsent(p); return p; + } else { throw new OpenViduException(Code.ROOM_NOT_FOUND_ERROR_CODE, sessionId); } @@ -425,6 +429,9 @@ public abstract class SessionManager { log.warn("Error closing session '{}': {}", sessionId, e.getMessage()); } } + if (this.sessionGarbageCollectorTimer != null) { + this.sessionGarbageCollectorTimer.cancelTimer(); + } } @PostConstruct @@ -434,44 +441,18 @@ public abstract class SessionManager { "Garbage collector for non active sessions is disabled (property 'OPENVIDU_SESSIONS_GARBAGE_INTERVAL' is 0)"); return; } - new UpdatableTimerTask(() -> { + + this.sessionGarbageCollectorTimer = new UpdatableTimerTask(() -> { // Remove all non active sessions created more than the specified time log.info("Running non active sessions garbage collector..."); final long currentMillis = System.currentTimeMillis(); - // Loop through all non active sessions. Safely remove them and clean all of - // their data if their threshold has elapsed - for (Iterator> iter = sessionsNotActive.entrySet().iterator(); iter.hasNext();) { - final Session sessionNotActive = iter.next().getValue(); - final String sessionId = sessionNotActive.getSessionId(); - long sessionExistsSince = currentMillis - sessionNotActive.getStartTime(); - if (sessionExistsSince > (openviduConfig.getSessionGarbageThreshold() * 1000)) { - try { - if (sessionNotActive.closingLock.writeLock().tryLock(15, TimeUnit.SECONDS)) { - try { - if (sessions.containsKey(sessionId)) { - // The session passed to active during lock wait - continue; - } - iter.remove(); - cleanCollections(sessionId); - log.info("Non active session {} cleaned up by garbage collector", sessionId); - } finally { - sessionNotActive.closingLock.writeLock().unlock(); - } - } else { - log.error( - "Timeout waiting for Session closing lock to be available for garbage collector to clean session {}", - sessionId); - } - } catch (InterruptedException e) { - log.error( - "InterruptedException while waiting for Session closing lock to be available for garbage collector to clean session {}", - sessionId); - } - } - } + this.closeNonActiveSessions(sessionNotActive -> { + // Remove non active session if threshold has elapsed + return (currentMillis - sessionNotActive.getStartTime()) > (openviduConfig.getSessionGarbageThreshold() + * 1000); + }); // Warn about possible ghost sessions for (Iterator> iter = sessions.entrySet().iterator(); iter.hasNext();) { @@ -480,7 +461,9 @@ public abstract class SessionManager { log.warn("Possible ghost session {}", sessionActive.getSessionId()); } } - }, () -> new Long(openviduConfig.getSessionGarbageInterval() * 1000)).updateTimer(); + }, () -> Long.valueOf(openviduConfig.getSessionGarbageInterval() * 1000)); + + this.sessionGarbageCollectorTimer.updateTimer(); log.info( "Garbage collector for non active sessions initialized. Running every {} seconds and cleaning up non active Sessions more than {} seconds old", @@ -543,6 +526,40 @@ public abstract class SessionManager { } } + public void closeNonActiveSessions(Function conditionToRemove) { + // Loop through all non active sessions. Safely remove and clean all of + // the data for each non active session meeting the condition + for (Iterator> iter = sessionsNotActive.entrySet().iterator(); iter.hasNext();) { + final Session sessionNotActive = iter.next().getValue(); + final String sessionId = sessionNotActive.getSessionId(); + if (conditionToRemove.apply(sessionNotActive)) { + try { + if (sessionNotActive.closingLock.writeLock().tryLock(15, TimeUnit.SECONDS)) { + try { + if (sessions.containsKey(sessionId)) { + // The session passed to active during lock wait + continue; + } + iter.remove(); + cleanCollections(sessionId); + log.info("Non active session {} cleaned up", sessionId); + } finally { + sessionNotActive.closingLock.writeLock().unlock(); + } + } else { + log.error( + "Timeout waiting for Session closing lock to be available to clean up non active session {}", + sessionId); + } + } catch (InterruptedException e) { + log.error( + "InterruptedException while waiting for non active Session closing lock to be available to clean up non active session session {}", + sessionId); + } + } + } + } + public void closeSessionAndEmptyCollections(Session session, EndReason reason, boolean stopRecording) { if (openviduConfig.isRecordingModuleEnabled()) { diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/Token.java b/openvidu-server/src/main/java/io/openvidu/server/core/Token.java index feeb99af..a44b9d6a 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/Token.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/Token.java @@ -17,60 +17,193 @@ package io.openvidu.server.core; +import org.apache.commons.lang3.RandomStringUtils; + +import com.google.gson.JsonObject; + +import io.openvidu.java.client.ConnectionProperties; +import io.openvidu.java.client.ConnectionType; +import io.openvidu.java.client.KurentoOptions; import io.openvidu.java.client.OpenViduRole; +import io.openvidu.server.core.Participant.ParticipantStatus; import io.openvidu.server.coturn.TurnCredentials; -import io.openvidu.server.kurento.core.KurentoTokenOptions; public class Token { private String token; - private OpenViduRole role; - private String serverMetadata = ""; + private String sessionId; + private Long createdAt; + private ConnectionProperties connectionProperties; private TurnCredentials turnCredentials; - private KurentoTokenOptions kurentoTokenOptions; + private final String connectionId = IdentifierPrefixes.PARTICIPANT_PUBLIC_ID + + RandomStringUtils.randomAlphabetic(1).toUpperCase() + RandomStringUtils.randomAlphanumeric(9); - public Token(String token) { + public Token(String token, String sessionId, ConnectionProperties connectionProperties, + TurnCredentials turnCredentials) { this.token = token; + this.sessionId = sessionId; + this.createdAt = System.currentTimeMillis(); + this.connectionProperties = connectionProperties; + this.turnCredentials = turnCredentials; } - public Token(String token, OpenViduRole role, String serverMetadata, TurnCredentials turnCredentials, - KurentoTokenOptions kurentoTokenOptions) { - this.token = token; - this.role = role; - this.serverMetadata = serverMetadata; - this.turnCredentials = turnCredentials; - this.kurentoTokenOptions = kurentoTokenOptions; + public ConnectionType getType() { + return this.connectionProperties.getType(); } public String getToken() { return token; } - public void setToken(String token) { - this.token = token; + public void setToken(String newToken) { + this.token = newToken; } - - public OpenViduRole getRole() { - return role; + + public Long getCreatedAt() { + return this.createdAt; } public String getServerMetadata() { - return serverMetadata; + return this.connectionProperties.getData(); + } + + public Boolean record() { + return this.connectionProperties.record(); + } + + public void setRecord(boolean newRecord) { + this.updateConnectionProperties(connectionProperties.getType(), connectionProperties.getData(), newRecord, + connectionProperties.getRole(), connectionProperties.getKurentoOptions(), + connectionProperties.getRtspUri(), connectionProperties.adaptativeBitrate(), + connectionProperties.onlyPlayWithSubscribers(), connectionProperties.getNetworkCache()); + } + + public OpenViduRole getRole() { + return this.connectionProperties.getRole(); + } + + public void setRole(OpenViduRole newRole) { + this.updateConnectionProperties(connectionProperties.getType(), connectionProperties.getData(), + connectionProperties.record(), newRole, connectionProperties.getKurentoOptions(), + connectionProperties.getRtspUri(), connectionProperties.adaptativeBitrate(), + connectionProperties.onlyPlayWithSubscribers(), connectionProperties.getNetworkCache()); + } + + public KurentoOptions getKurentoOptions() { + return this.connectionProperties.getKurentoOptions(); + } + + public String getRtspUri() { + return this.connectionProperties.getRtspUri(); + } + + public Boolean adaptativeBitrate() { + return this.connectionProperties.adaptativeBitrate(); + } + + public Boolean onlyPlayWithSubscribers() { + return this.connectionProperties.onlyPlayWithSubscribers(); + } + + public Integer getNetworkCache() { + return this.connectionProperties.getNetworkCache(); } public TurnCredentials getTurnCredentials() { return turnCredentials; } - public KurentoTokenOptions getKurentoTokenOptions() { - return kurentoTokenOptions; + public String getConnectionId() { + return connectionId; + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("id", this.getToken()); + json.addProperty("token", this.getToken()); + json.addProperty("createdAt", this.getCreatedAt()); + json.addProperty("connectionId", this.getConnectionId()); + json.addProperty("session", this.sessionId); + json.addProperty("data", this.getServerMetadata()); + json.addProperty("role", this.getRole().toString()); + if (this.getKurentoOptions() != null) { + json.add("kurentoOptions", this.getKurentoOptions().toJson()); + } + return json; + } + + public JsonObject toJsonAsParticipant() { + JsonObject json = new JsonObject(); + json.addProperty("id", this.getConnectionId()); + json.addProperty("object", "connection"); + json.addProperty("status", ParticipantStatus.pending.name()); + json.addProperty("connectionId", this.getConnectionId()); // DEPRECATED: better use id + json.addProperty("sessionId", this.sessionId); + json.addProperty("createdAt", this.createdAt); + + // Add all ConnectionProperties + JsonObject connectionPropertiesJson = this.getConnectionPropertiesWithFinalJsonFormat(); + connectionPropertiesJson.entrySet().forEach(entry -> { + json.add(entry.getKey(), entry.getValue()); + }); + + json.addProperty("token", this.getToken()); + json.add("activeAt", null); + json.add("location", null); + json.add("platform", null); + json.add("clientData", null); + json.add("publishers", null); + json.add("subscribers", null); + return json; + } + + protected JsonObject getConnectionPropertiesWithFinalJsonFormat() { + JsonObject json = this.connectionProperties.toJson(this.sessionId); + json.remove("session"); + json.add("serverData", json.get("data")); + json.remove("data"); + return json; + } + + private void updateConnectionProperties(ConnectionType type, String data, Boolean record, OpenViduRole role, + KurentoOptions kurentoOptions, String rtspUri, Boolean adaptativeBitrate, Boolean onlyPlayWithSubscribers, + Integer networkCache) { + ConnectionProperties.Builder builder = new ConnectionProperties.Builder(); + if (type != null) { + builder.type(type); + } + if (data != null) { + builder.data(data); + } + if (record != null) { + builder.record(record); + } + if (role != null) { + builder.role(role); + } + if (kurentoOptions != null) { + builder.kurentoOptions(kurentoOptions); + } + if (rtspUri != null) { + builder.rtspUri(rtspUri); + } + if (adaptativeBitrate != null) { + builder.adaptativeBitrate(adaptativeBitrate); + } + if (onlyPlayWithSubscribers != null) { + builder.onlyPlayWithSubscribers(onlyPlayWithSubscribers); + } + if (networkCache != null) { + builder.networkCache(networkCache); + } + this.connectionProperties = builder.build(); } @Override public String toString() { - if (this.role != null) - return this.role.name(); + if (this.connectionProperties.getRole() != null) + return this.connectionProperties.getRole().name(); else return this.token; } diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/TokenGenerator.java b/openvidu-server/src/main/java/io/openvidu/server/core/TokenGenerator.java index 472f4ee8..35192b52 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/TokenGenerator.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/TokenGenerator.java @@ -17,12 +17,43 @@ package io.openvidu.server.core; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; + +import io.openvidu.java.client.ConnectionProperties; +import io.openvidu.java.client.ConnectionType; +import io.openvidu.java.client.KurentoOptions; import io.openvidu.java.client.OpenViduRole; -import io.openvidu.server.kurento.core.KurentoTokenOptions; +import io.openvidu.server.OpenViduServer; +import io.openvidu.server.config.OpenviduBuildInfo; +import io.openvidu.server.config.OpenviduConfig; +import io.openvidu.server.coturn.CoturnCredentialsService; +import io.openvidu.server.coturn.TurnCredentials; -public interface TokenGenerator { +public class TokenGenerator { - public Token generateToken(String sessionId, OpenViduRole role, String serverMetadata, - KurentoTokenOptions kurentoTokenOptions) throws Exception; + @Autowired + private CoturnCredentialsService coturnCredentialsService; + + @Autowired + protected OpenviduConfig openviduConfig; + + @Autowired + protected OpenviduBuildInfo openviduBuildConfig; + + public Token generateToken(String sessionId, String serverMetadata, boolean record, OpenViduRole role, + KurentoOptions kurentoOptions) throws Exception { + String token = OpenViduServer.wsUrl; + token += "?sessionId=" + sessionId; + token += "&token=" + IdentifierPrefixes.TOKEN_ID + RandomStringUtils.randomAlphabetic(1).toUpperCase() + + RandomStringUtils.randomAlphanumeric(15); + TurnCredentials turnCredentials = null; + if (this.openviduConfig.isTurnadminAvailable()) { + turnCredentials = coturnCredentialsService.createUser(); + } + ConnectionProperties connectionProperties = new ConnectionProperties.Builder().type(ConnectionType.WEBRTC) + .data(serverMetadata).record(record).role(role).kurentoOptions(kurentoOptions).build(); + return new Token(token, sessionId, connectionProperties, turnCredentials); + } } diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/TokenGeneratorDefault.java b/openvidu-server/src/main/java/io/openvidu/server/core/TokenGeneratorDefault.java deleted file mode 100644 index eeb46563..00000000 --- a/openvidu-server/src/main/java/io/openvidu/server/core/TokenGeneratorDefault.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * (C) Copyright 2017-2020 OpenVidu (https://openvidu.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package io.openvidu.server.core; - -import org.apache.commons.lang3.RandomStringUtils; -import org.springframework.beans.factory.annotation.Autowired; - -import io.openvidu.java.client.OpenViduRole; -import io.openvidu.server.OpenViduServer; -import io.openvidu.server.config.OpenviduBuildInfo; -import io.openvidu.server.config.OpenviduConfig; -import io.openvidu.server.coturn.CoturnCredentialsService; -import io.openvidu.server.coturn.TurnCredentials; -import io.openvidu.server.kurento.core.KurentoTokenOptions; - -public class TokenGeneratorDefault implements TokenGenerator { - - @Autowired - private CoturnCredentialsService coturnCredentialsService; - - @Autowired - protected OpenviduConfig openviduConfig; - - @Autowired - protected OpenviduBuildInfo openviduBuildConfig; - - @Override - public Token generateToken(String sessionId, OpenViduRole role, String serverMetadata, - KurentoTokenOptions kurentoTokenOptions) throws Exception { - String token = OpenViduServer.wsUrl; - token += "?sessionId=" + sessionId; - token += "&token=" + IdentifierPrefixes.TOKEN_ID + RandomStringUtils.randomAlphabetic(1).toUpperCase() - + RandomStringUtils.randomAlphanumeric(15); - token += "&role=" + role.name(); - token += "&version=" + openviduBuildConfig.getOpenViduServerVersion(); - TurnCredentials turnCredentials = null; - if (this.openviduConfig.isTurnadminAvailable()) { - turnCredentials = coturnCredentialsService.createUser(); - if (turnCredentials != null) { - token += "&coturnIp=" + openviduConfig.getCoturnIp(); - token += "&turnUsername=" + turnCredentials.getUsername(); - token += "&turnCredential=" + turnCredentials.getCredential(); - } - } - return new Token(token, role, serverMetadata, turnCredentials, kurentoTokenOptions); - } -} diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoMediaOptions.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoMediaOptions.java index 7684b821..1e477356 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoMediaOptions.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoMediaOptions.java @@ -32,6 +32,7 @@ public class KurentoMediaOptions extends MediaOptions { public String rtspUri; public Boolean adaptativeBitrate; public Boolean onlyPlayWithSubscribers; + public Integer networkCache; public KurentoMediaOptions(boolean isOffer, String sdpOffer, Boolean hasAudio, Boolean hasVideo, Boolean audioActive, Boolean videoActive, String typeOfVideo, Integer frameRate, String videoDimensions, @@ -45,7 +46,7 @@ public class KurentoMediaOptions extends MediaOptions { public KurentoMediaOptions(boolean isOffer, String sdpOffer, Boolean hasAudio, Boolean hasVideo, Boolean audioActive, Boolean videoActive, String typeOfVideo, Integer frameRate, String videoDimensions, KurentoFilter filter, boolean doLoopback, String rtspUri, Boolean adaptativeBitrate, - Boolean onlyPlayWithSubscribers) { + Boolean onlyPlayWithSubscribers, Integer networkCache) { super(hasAudio, hasVideo, audioActive, videoActive, typeOfVideo, frameRate, videoDimensions, filter); this.isOffer = isOffer; this.sdpOffer = sdpOffer; @@ -53,6 +54,7 @@ public class KurentoMediaOptions extends MediaOptions { this.rtspUri = rtspUri; this.adaptativeBitrate = adaptativeBitrate; this.onlyPlayWithSubscribers = onlyPlayWithSubscribers; + this.networkCache = networkCache; } public KurentoMediaOptions(Boolean hasAudio, Boolean hasVideo, Boolean audioActive, Boolean videoActive, @@ -65,6 +67,7 @@ public class KurentoMediaOptions extends MediaOptions { this.rtspUri = streamProperties.rtspUri; this.adaptativeBitrate = streamProperties.adaptativeBitrate; this.onlyPlayWithSubscribers = streamProperties.onlyPlayWithSubscribers; + this.networkCache = streamProperties.networkCache; } @Override @@ -76,6 +79,9 @@ public class KurentoMediaOptions extends MediaOptions { if (onlyPlayWithSubscribers != null) { json.addProperty("onlyPlayWithSubscribers", onlyPlayWithSubscribers); } + if (networkCache != null) { + json.addProperty("networkCache", networkCache); + } return json; } diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoParticipant.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoParticipant.java index 45017756..720bd9de 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoParticipant.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoParticipant.java @@ -37,6 +37,7 @@ import org.kurento.client.MediaElement; import org.kurento.client.MediaPipeline; import org.kurento.client.PassThrough; import org.kurento.client.internal.server.KurentoServerException; +import org.kurento.jsonrpc.JsonRpcException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -79,7 +80,7 @@ public class KurentoParticipant extends Participant { super(participant.getFinalUserId(), participant.getParticipantPrivateId(), participant.getParticipantPublicId(), kurentoSession.getSessionId(), participant.getToken(), participant.getClientMetadata(), participant.getLocation(), participant.getPlatform(), participant.getEndpointType(), - participant.getCreatedAt()); + participant.getActiveAt()); this.endpointConfig = endpointConfig; this.openviduConfig = openviduConfig; this.recordingManager = recordingManager; @@ -87,11 +88,15 @@ public class KurentoParticipant extends Participant { if (!OpenViduRole.SUBSCRIBER.equals(participant.getToken().getRole())) { // Initialize a PublisherEndpoint - this.publisher = new PublisherEndpoint(endpointType, this, participant.getParticipantPublicId(), - this.session.getPipeline(), this.openviduConfig, null); + initPublisherEndpoint(); } } + public void initPublisherEndpoint() { + this.publisher = new PublisherEndpoint(endpointType, this, this.participantPublicId, this.session.getPipeline(), + this.openviduConfig, null); + } + public void createPublishingEndpoint(MediaOptions mediaOptions, String streamId) { String type = mediaOptions.hasVideo() ? mediaOptions.getTypeOfVideo() : "MICRO"; if (streamId == null) { @@ -135,6 +140,10 @@ public class KurentoParticipant extends Participant { } } + public boolean isPublisherEndpointDefined() { + return this.publisher != null; + } + public PublisherEndpoint getPublisher() { try { if (!publisherLatch.await(KurentoSession.ASYNC_LATCH_TIMEOUT, TimeUnit.SECONDS)) { @@ -180,9 +189,9 @@ public class KurentoParticipant extends Participant { log.info("PARTICIPANT {}: Is now publishing video in room {}", this.getParticipantPublicId(), this.session.getSessionId()); - if (this.openviduConfig.isRecordingModuleEnabled() + if (this.openviduConfig.isRecordingModuleEnabled() && this.token.record() && this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) { - this.recordingManager.startOneIndividualStreamRecording(session, null, null, this); + this.recordingManager.startOneIndividualStreamRecording(session, this); } if (!silent) { @@ -193,7 +202,7 @@ public class KurentoParticipant extends Participant { return sdpAnswer; } - public void unpublishMedia(EndReason reason, long kmsDisconnectionTime) { + public void unpublishMedia(EndReason reason, Long kmsDisconnectionTime) { log.info("PARTICIPANT {}: unpublishing media stream from room {}", this.getParticipantPublicId(), this.session.getSessionId()); final MediaOptions mediaOptions = this.getPublisher().getMediaOptions(); @@ -390,7 +399,7 @@ public class KurentoParticipant extends Participant { } } - public void close(EndReason reason, boolean definitelyClosed, long kmsDisconnectionTime) { + public void close(EndReason reason, boolean definitelyClosed, Long kmsDisconnectionTime) { log.debug("PARTICIPANT {}: Closing user", this.getParticipantPublicId()); if (isClosed()) { log.warn("PARTICIPANT {}: Already closed", this.getParticipantPublicId()); @@ -405,11 +414,16 @@ public class KurentoParticipant extends Participant { it.remove(); if (subscriber != null && subscriber.getEndpoint() != null) { - releaseSubscriberEndpoint(remoteParticipantName, - (KurentoParticipant) this.session.getParticipantByPublicId(remoteParticipantName), subscriber, - reason, false); - log.debug("PARTICIPANT {}: Released subscriber endpoint to {}", this.getParticipantPublicId(), - remoteParticipantName); + try { + releaseSubscriberEndpoint(remoteParticipantName, + (KurentoParticipant) this.session.getParticipantByPublicId(remoteParticipantName), + subscriber, reason, false); + log.debug("PARTICIPANT {}: Released subscriber endpoint to {}", this.getParticipantPublicId(), + remoteParticipantName); + } catch (JsonRpcException e) { + log.error("Error releasing subscriber endpoint of participant {}: {}", this.participantPublicId, + e.getMessage()); + } } else { log.warn( "PARTICIPANT {}: Trying to close subscriber endpoint to {}. " @@ -463,7 +477,7 @@ public class KurentoParticipant extends Participant { session.sendMediaError(this.getParticipantPrivateId(), desc); } - private void releasePublisherEndpoint(EndReason reason, long kmsDisconnectionTime) { + private void releasePublisherEndpoint(EndReason reason, Long kmsDisconnectionTime) { if (publisher != null && publisher.getEndpoint() != null) { final ReadWriteLock closingLock = publisher.closingLock; try { @@ -486,26 +500,34 @@ public class KurentoParticipant extends Participant { } } - private void releasePublisherEndpointAux(EndReason reason, long kmsDisconnectionTime) { - // Remove streamId from publisher's map - this.session.publishedStreamIds.remove(this.getPublisherStreamId()); + private void releasePublisherEndpointAux(EndReason reason, Long kmsDisconnectionTime) { + try { + // Remove streamId from publisher's map + this.session.publishedStreamIds.remove(this.getPublisherStreamId()); - if (this.openviduConfig.isRecordingModuleEnabled() - && this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) { - this.recordingManager.stopOneIndividualStreamRecording(session, this.getPublisherStreamId(), - kmsDisconnectionTime); + this.streaming = false; + this.session.deregisterPublisher(this); + + if (this.openviduConfig.isRecordingModuleEnabled() && this.token.record() + && this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) { + this.recordingManager.stopOneIndividualStreamRecording(session, this.getPublisherStreamId(), + kmsDisconnectionTime); + } + + publisher.cancelStatsLoop.set(true); + + // These operations are all remote + publisher.unregisterErrorListeners(); + for (MediaElement el : publisher.getMediaElements()) { + releaseElement(getParticipantPublicId(), el); + } + releaseElement(getParticipantPublicId(), publisher.getEndpoint()); + + } catch (JsonRpcException e) { + log.error("Error releasing publisher endpoint of participant {}: {}", this.participantPublicId, + e.getMessage()); } - publisher.unregisterErrorListeners(); - publisher.cancelStatsLoop.set(true); - - for (MediaElement el : publisher.getMediaElements()) { - releaseElement(getParticipantPublicId(), el); - } - releaseElement(getParticipantPublicId(), publisher.getEndpoint()); - this.streaming = false; - this.session.deregisterPublisher(); - endpointConfig.getCdr().stopPublisher(this.getParticipantPublicId(), publisher.getStreamId(), reason); publisher = null; } @@ -603,6 +625,7 @@ public class KurentoParticipant extends Participant { return this.sharedJson(MediaEndpoint::toJson); } + @Override public JsonObject withStatsToJson() { return this.sharedJson(MediaEndpoint::withStatsToJson); } diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSession.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSession.java index e0ad8cb4..e1771c7f 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSession.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSession.java @@ -88,7 +88,7 @@ public class KurentoSession extends Session { } public void newPublisher(Participant participant) { - registerPublisher(); + registerPublisher(participant); log.debug("SESSION {}: Virtually subscribed other participants {} to new publisher {}", sessionId, participants.values(), participant.getParticipantPublicId()); } @@ -122,7 +122,7 @@ public class KurentoSession extends Session { log.info("PARTICIPANT {}: Leaving session {}", participant.getParticipantPublicId(), this.sessionId); this.removeParticipant(participant, reason); - participant.close(reason, true, 0); + participant.close(reason, true, null); } @Override @@ -133,7 +133,7 @@ public class KurentoSession extends Session { for (Participant participant : participants.values()) { ((KurentoParticipant) participant).releaseAllFilters(); - ((KurentoParticipant) participant).close(reason, true, 0); + ((KurentoParticipant) participant).close(reason, true, null); } participants.clear(); @@ -280,7 +280,7 @@ public class KurentoSession extends Session { return this.publishedStreamIds.get(streamId); } - public void restartStatusInKurento(long kmsDisconnectionTime) { + public void restartStatusInKurento(Long kmsDisconnectionTime) { log.info("Resetting process: resetting remote media objects for active session {}", this.sessionId); diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionEventsHandler.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionEventsHandler.java index a47f18ce..ebf4aa0d 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionEventsHandler.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionEventsHandler.java @@ -29,9 +29,6 @@ import io.openvidu.server.core.SessionEventsHandler; public class KurentoSessionEventsHandler extends SessionEventsHandler { - public KurentoSessionEventsHandler() { - } - public void onIceCandidate(String roomName, String participantPrivateId, String senderPublicId, String endpointName, IceCandidate candidate) { JsonObject params = new JsonObject(); @@ -59,11 +56,4 @@ public class KurentoSessionEventsHandler extends SessionEventsHandler { rpcNotificationService.sendNotification(participantId, ProtocolElements.MEDIAERROR_METHOD, notifParams); } - public void updateFilter(String roomName, Participant participant, String filterId, String state) { - } - - public String getNextFilterState(String filterId, String state) { - return null; - } - } diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java index 5859ffeb..32085990 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; +import java.sql.Timestamp; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -47,6 +48,8 @@ import org.springframework.beans.factory.annotation.Autowired; import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.internal.ProtocolElements; +import io.openvidu.java.client.ConnectionProperties; +import io.openvidu.java.client.KurentoOptions; import io.openvidu.java.client.MediaMode; import io.openvidu.java.client.Recording; import io.openvidu.java.client.RecordingLayout; @@ -76,14 +79,14 @@ public class KurentoSessionManager extends SessionManager { private static final Logger log = LoggerFactory.getLogger(KurentoSessionManager.class); @Autowired - private KmsManager kmsManager; + protected KmsManager kmsManager; @Autowired - private KurentoSessionEventsHandler kurentoSessionEventsHandler; + protected KurentoSessionEventsHandler kurentoSessionEventsHandler; @Autowired - private KurentoParticipantEndpointConfig kurentoEndpointConfig; - + protected KurentoParticipantEndpointConfig kurentoEndpointConfig; + @Autowired private SDPMunging sdpMunging; @@ -111,28 +114,14 @@ public class KurentoSessionManager extends SessionManager { if (KmsManager.selectAndRemoveKmsLock.tryLock(KmsManager.MAX_SECONDS_LOCK_WAIT, TimeUnit.SECONDS)) { try { kSession = (KurentoSession) sessions.get(sessionId); - if (kSession == null) { // Session still null. It was not created by other thread while waiting for lock - Kms lessLoadedKms = null; - try { - lessLoadedKms = this.kmsManager.getLessLoadedConnectedAndRunningKms(); - } catch (NoSuchElementException e) { - // Restore session not active - this.cleanCollections(sessionId); - this.storeSessionNotActive(sessionNotActive); - throw new OpenViduException(Code.ROOM_CANNOT_BE_CREATED_ERROR_CODE, - "There is no available Media Node where to initialize session '" + sessionId - + "'"); - } - log.info("KMS less loaded is {} with a load of {}", lessLoadedKms.getUri(), - lessLoadedKms.getLoad()); - kSession = createSession(sessionNotActive, lessLoadedKms); + Kms selectedMediaNode = this.selectMediaNode(sessionNotActive); + kSession = createSession(sessionNotActive, selectedMediaNode); } } finally { KmsManager.selectAndRemoveKmsLock.unlock(); } - } else { String error = "Timeout of " + KmsManager.MAX_SECONDS_LOCK_WAIT + " seconds waiting to acquire lock"; @@ -151,7 +140,8 @@ public class KurentoSessionManager extends SessionManager { // If Recording default layout is COMPOSED_QUICK_START Recording.OutputMode defaultOutputMode = kSession.getSessionProperties().defaultOutputMode(); - if (openviduConfig.isRecordingModuleEnabled() && defaultOutputMode.equals(Recording.OutputMode.COMPOSED_QUICK_START)) { + if (openviduConfig.isRecordingModuleEnabled() + && defaultOutputMode.equals(Recording.OutputMode.COMPOSED_QUICK_START)) { recordingManager.startComposedQuickStartContainer(kSession); } @@ -309,10 +299,12 @@ public class KurentoSessionManager extends SessionManager { } else if (remainingParticipants.size() == 1 && openviduConfig.isRecordingModuleEnabled() && MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode()) - && session.getSessionProperties().defaultOutputMode().equals(Recording.OutputMode.COMPOSED_QUICK_START) + && session.getSessionProperties().defaultOutputMode() + .equals(Recording.OutputMode.COMPOSED_QUICK_START) && ProtocolElements.RECORDER_PARTICIPANT_PUBLICID - .equals(remainingParticipants.iterator().next().getParticipantPublicId())) { - // If no recordings are active in COMPOSED_QUICK_START output mode, stop container + .equals(remainingParticipants.iterator().next().getParticipantPublicId())) { + // If no recordings are active in COMPOSED_QUICK_START output mode, stop + // container recordingManager.stopComposedQuickStartContainer(session, reason); } } @@ -371,30 +363,15 @@ public class KurentoSessionManager extends SessionManager { KurentoMediaOptions kurentoOptions = (KurentoMediaOptions) mediaOptions; KurentoParticipant kParticipant = (KurentoParticipant) participant; KurentoSession kSession = kParticipant.getSession(); + SdpType sdpType = kurentoOptions.isOffer ? SdpType.OFFER : SdpType.ANSWER; boolean isTranscodingAllowed = kSession.getSessionProperties().isTranscodingAllowed(); VideoCodec forcedVideoCodec = kSession.getSessionProperties().forcedVideoCodec(); - log.debug("PARTICIPANT '{}' in Session '{}' publishing SDP before munging: \n {}", - participant.getParticipantPublicId(), kSession.getSessionId(), kurentoOptions.sdpOffer); - // Modify sdp if forced codec is defined and this is not an IP camera + // Modify sdp if forced codec is defined if (forcedVideoCodec != VideoCodec.NONE && !participant.isIpcam()) { - String sdpOffer = kurentoOptions.sdpOffer; - try { - kurentoOptions.sdpOffer = this.sdpMunging.setCodecPreference(forcedVideoCodec, sdpOffer, true); - log.debug("PARTICIPANT '{}' in Session '{}' publishing SDP Offer after munging: \n {}", - participant.getParticipantPublicId(), kSession.getSessionId(), kurentoOptions.sdpOffer); - } catch (OpenViduException e) { - String errorMessage = "Error forcing codec: '" + forcedVideoCodec + "', for PARTICIPANT" - + participant.getParticipantPublicId() + "' publishing in Session: '" - + kSession.getSessionId() + "'\nException: " + e.getMessage() + "\nSDP:\n" + sdpAnswer; - if(!isTranscodingAllowed) { - throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, errorMessage); - } - log.info("Codec: '" + forcedVideoCodec + "' is not supported for PARTICIPANT: '" + participant.getParticipantPublicId() - + " publishing in Session: '" + kSession.getSessionId() + "'. Transcoding will be allowed"); - } + kurentoOptions.sdpOffer = sdpMunging.forceCodec(participant, kurentoOptions.sdpOffer, + kurentoOptions.isOffer, kSession, true, false, isTranscodingAllowed, forcedVideoCodec); } - log.debug( "Request [PUBLISH_MEDIA] isOffer={} sdp={} " + "loopbackAltSrc={} lpbkConnType={} doLoopback={} rtspUri={} ({})", @@ -408,7 +385,7 @@ public class KurentoSessionManager extends SessionManager { * kurentoParticipant.getPublisher().apply(elem); } */ - KurentoTokenOptions kurentoTokenOptions = participant.getToken().getKurentoTokenOptions(); + KurentoOptions kurentoTokenOptions = participant.getToken().getKurentoOptions(); if (kurentoOptions.getFilter() != null && kurentoTokenOptions != null) { if (kurentoTokenOptions.isFilterAllowed(kurentoOptions.getFilter().getType())) { this.applyFilterInPublisher(kParticipant, kurentoOptions.getFilter()); @@ -424,9 +401,9 @@ public class KurentoSessionManager extends SessionManager { throw e; } } - + sdpAnswer = kParticipant.publishToRoom(kurentoOptions.sdpOffer, kurentoOptions.doLoopback, false); - + if (sdpAnswer == null) { OpenViduException e = new OpenViduException(Code.MEDIA_SDP_ERROR_CODE, "Error generating SDP response for publishing user " + participant.getParticipantPublicId()); @@ -454,7 +431,10 @@ public class KurentoSessionManager extends SessionManager { recordingManager.startRecording(kSession, new RecordingProperties.Builder().name("") .outputMode(kSession.getSessionProperties().defaultOutputMode()) .recordingLayout(kSession.getSessionProperties().defaultRecordingLayout()) - .customLayout(kSession.getSessionProperties().defaultCustomLayout()).build()); + .customLayout(kSession.getSessionProperties().defaultCustomLayout()) + .resolution(/* + * kSession.getSessionProperties().defaultRecordingResolution() + */"1920x1080").mediaNode(kSession.getMediaNodeId()).build()); }).start(); } else if (recordingManager.sessionIsBeingRecorded(kSession.getSessionId())) { // Abort automatic recording stop thread for any recorded session in which a @@ -489,11 +469,13 @@ public class KurentoSessionManager extends SessionManager { } + participant.setPublishedAt(new Timestamp(System.currentTimeMillis()).getTime()); kSession.newPublisher(participant); participants = kParticipant.getSession().getParticipants(); if (sdpAnswer != null) { + log.debug("SDP Answer for publishing PARTICIPANT {}: {}", participant.getParticipantPublicId(), sdpAnswer); sessionEventsHandler.onPublishMedia(participant, participant.getPublisherStreamId(), kParticipant.getPublisher().createdAt(), kSession.getSessionId(), mediaOptions, sdpAnswer, participants, transactionId, null); @@ -517,7 +499,7 @@ public class KurentoSessionManager extends SessionManager { throw new OpenViduException(Code.USER_NOT_STREAMING_ERROR_CODE, "Participant '" + participant.getParticipantPublicId() + "' is not streaming media"); } - kParticipant.unpublishMedia(reason, 0); + kParticipant.unpublishMedia(reason, null); session.cancelPublisher(participant, reason); Set participants = session.getParticipants(); @@ -566,7 +548,7 @@ public class KurentoSessionManager extends SessionManager { sdpOffer = kParticipant.prepareReceiveMediaFrom(senderParticipant); VideoCodec forcedVideoCodec = session.getSessionProperties().forcedVideoCodec(); boolean isTranscodingAllowed = session.getSessionProperties().isTranscodingAllowed(); - + if (forcedVideoCodec != VideoCodec.NONE) { try { log.debug("PARTICIPANT '{}' in Session '{}' SDP Offer before munging: \n {}", @@ -576,20 +558,20 @@ public class KurentoSessionManager extends SessionManager { sdpOffer = this.sdpMunging.setfmtpH264(sdpOffer); } } catch (OpenViduException e) { - String errorMessage = "Error forcing codec: '" + forcedVideoCodec + "', for PARTICIPANT: '" - + participant.getParticipantPublicId() + "' subscribing in Session: '" + String errorMessage = "Error forcing codec: '" + forcedVideoCodec + "', for PARTICIPANT: '" + + participant.getParticipantPublicId() + "' subscribing in Session: '" + session.getSessionId() + "'\nException: " + e.getMessage() + "\nSDP:\n" + sdpOffer; - + if(!isTranscodingAllowed) { throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, errorMessage); } - log.info("Codec: '" + forcedVideoCodec + "' is not supported for PARTICIPANT: '" + participant.getParticipantPublicId() + log.info("Codec: '" + forcedVideoCodec + "' is not supported for PARTICIPANT: '" + participant.getParticipantPublicId() + " subscribing in Session: '" + session.getSessionId() + "'. Transcoding will be allowed"); } } log.debug("Request [SUBSCRIBE] remoteParticipant={} sdpOffer={} ({})", senderPublicId, sdpOffer, participant.getParticipantPublicId()); - + if (sdpOffer == null) { throw new OpenViduException(Code.MEDIA_SDP_ERROR_CODE, "Unable to generate SDP offer when subscribing '" + participant.getParticipantPublicId() + "' to '" + senderPublicId + "'"); @@ -616,28 +598,12 @@ public class KurentoSessionManager extends SessionManager { Participant senderParticipant = session.getParticipantByPublicId(senderName); boolean isTranscodingAllowed = session.getSessionProperties().isTranscodingAllowed(); VideoCodec forcedVideoCodec = session.getSessionProperties().forcedVideoCodec(); - - log.debug("PARTICIPANT '{}' subscribing in Session '{}' SDP Answer before munging: \n {}", - participant.getParticipantPublicId(), session.getSessionId(), sdpAnswer); + // Modify sdp if forced codec is defined - if (forcedVideoCodec != VideoCodec.NONE) { - try { - sdpAnswer = this.sdpMunging.setCodecPreference(forcedVideoCodec, sdpAnswer, true); - log.debug("PARTICIPANT '{}' subscribing in Session '{}' SDP Answer after munging: \n {}", - participant.getParticipantPublicId(), session.getSessionId(), sdpAnswer); - } catch (OpenViduException e) { - String errorMessage = "Error forcing codec: '" + forcedVideoCodec + "', for PARTICIPANT: '" - + participant.getParticipantPublicId() + "' subscribing in Session: '" - + session.getSessionId() + "'\nException: " + e.getMessage() + "\nSDP:\n" + sdpAnswer; - - if(!isTranscodingAllowed) { - throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, errorMessage); - } - log.info("Codec: '" + forcedVideoCodec + "' is not supported for PARTICIPANT: '" + participant.getParticipantPublicId() - + " subscribing in Session: '" + session.getSessionId() + "'. Transcoding will be allowed"); - } + if (forcedVideoCodec != VideoCodec.NONE && !participant.isIpcam()) { + sdpOffer = sdpMunging.forceCodec(participant, sdpOffer, true, session, false, false, + isTranscodingAllowed, forcedVideoCodec); } - if (senderParticipant == null) { log.warn( @@ -660,7 +626,11 @@ public class KurentoSessionManager extends SessionManager { sessionEventsHandler.onSubscribe(participant, session, transactionId, null); } catch (OpenViduException e) { log.error("PARTICIPANT {}: Error subscribing to {}", participant.getParticipantPublicId(), senderName, e); - sessionEventsHandler.onSubscribe(participant, session, transactionId, e); + sessionEventsHandler.onSubscribe(participant, session, null, transactionId, e); + } + if (sdpAnswer != null) { + log.debug("SDP Answer for subscribing PARTICIPANT {}: {}", participant.getParticipantPublicId(), sdpAnswer); + sessionEventsHandler.onSubscribe(participant, session, sdpAnswer, transactionId, null); } } @@ -740,7 +710,7 @@ public class KurentoSessionManager extends SessionManager { /** * Creates a session with the already existing not-active session in the * indicated KMS, if it doesn't already exist - * + * * @throws OpenViduException in case of error while creating the session */ public KurentoSession createSession(Session sessionNotActive, Kms kms) throws OpenViduException { @@ -1099,8 +1069,8 @@ public class KurentoSessionManager extends SessionManager { @Override /* Protected by Session.closingLock.readLock */ - public Participant publishIpcam(Session session, MediaOptions mediaOptions, String serverMetadata) - throws Exception { + public Participant publishIpcam(Session session, MediaOptions mediaOptions, + ConnectionProperties connectionProperties) throws Exception { final String sessionId = session.getSessionId(); final KurentoMediaOptions kMediaOptions = (KurentoMediaOptions) mediaOptions; @@ -1133,7 +1103,8 @@ public class KurentoSessionManager extends SessionManager { } String rtspConnectionId = kMediaOptions.getTypeOfVideo() + "_" + protocol + "_" - + RandomStringUtils.randomAlphanumeric(4).toUpperCase() + "_" + url.getAuthority() + url.getPath(); + + RandomStringUtils.randomAlphanumeric(4).toUpperCase() + "_" + url.getHost() + + (url.getPort() != -1 ? (":" + url.getPort()) : "") + url.getPath(); rtspConnectionId = rtspConnectionId.replace("/", "_").replace("-", "").replace(".", "_").replace(":", "_"); rtspConnectionId = IdentifierPrefixes.IPCAM_ID + rtspConnectionId; @@ -1141,7 +1112,8 @@ public class KurentoSessionManager extends SessionManager { this.newInsecureParticipant(rtspConnectionId); String token = IdentifierPrefixes.TOKEN_ID + RandomStringUtils.randomAlphabetic(1).toUpperCase() + RandomStringUtils.randomAlphanumeric(15); - this.newTokenForInsecureUser(session, token, serverMetadata); + + this.newTokenForInsecureUser(session, token, connectionProperties); final Token tokenObj = session.consumeToken(token); Participant ipcamParticipant = this.newIpcamParticipant(sessionId, rtspConnectionId, tokenObj, location, @@ -1172,26 +1144,11 @@ public class KurentoSessionManager extends SessionManager { boolean isPublisher = streamId.equals(participant.getPublisherStreamId()); boolean isTranscodingAllowed = kSession.getSessionProperties().isTranscodingAllowed(); VideoCodec forcedVideoCodec = kSession.getSessionProperties().forcedVideoCodec(); - - log.debug("PARTICIPANT '{}' in Session '{}' reconnecting SDP before munging: \n {}", - participant.getParticipantPublicId(), kSession.getSessionId(), sdpString); + // Modify sdp if forced codec is defined - if (forcedVideoCodec != VideoCodec.NONE) { - try { - sdpString = sdpMunging.setCodecPreference(forcedVideoCodec, sdpString, true); - log.debug("PARTICIPANT '{}' in Session '{}' reconnecting SDP after munging: \n {}", - participant.getParticipantPublicId(), kSession.getSessionId(), sdpString); - } catch (OpenViduException e) { - String errorMessage = "Error in reconnect and forcing codec: '" + forcedVideoCodec + "', for PARTICIPANT: '" - + participant.getParticipantPublicId() + "' " + (isPublisher ? "publishing" : "subscribing") - + " in Session: '" + kSession.getSessionId() + "'\nException: " - + e.getMessage() + "\nSDP:\n" + sdpString; - if(!isTranscodingAllowed) { - throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, errorMessage); - } - log.info("Codec: '" + forcedVideoCodec + "' is not supported for PARTICIPANT: '" + participant.getParticipantPublicId() - + "' " + (isPublisher ? "publishing" : "subscribing") + " in Session: '" + kSession.getSessionId() + "'. Transcoding will be allowed"); - } + if (forcedVideoCodec != VideoCodec.NONE && !participant.isIpcam()) { + sdpOffer = sdpMunging.forceCodec(participant, sdpOffer, true, kSession, isPublisher, true, + isTranscodingAllowed, forcedVideoCodec); } if (isPublisher) { @@ -1211,8 +1168,10 @@ public class KurentoSessionManager extends SessionManager { // 3) Create a new PublisherEndpoint connecting it to the previous PassThrough kParticipant.resetPublisherEndpoint(kurentoOptions, passThru); kParticipant.createPublishingEndpoint(kurentoOptions, streamId); - String sdpAnswer = kParticipant.publishToRoom(sdpString, kurentoOptions.doLoopback, true); - + SdpType sdpType = kurentoOptions.isOffer ? SdpType.OFFER : SdpType.ANSWER; + String sdpAnswer = kParticipant.publishToRoom(sdpType, sdpOffer, kurentoOptions.doLoopback, true); + log.debug("SDP Answer for publishing reconnection PARTICIPANT {}: {}", participant.getParticipantPublicId(), + sdpAnswer); sessionEventsHandler.onPublishMedia(participant, participant.getPublisherStreamId(), kParticipant.getPublisher().createdAt(), kSession.getSessionId(), kurentoOptions, sdpAnswer, new HashSet(), transactionId, null); @@ -1223,8 +1182,16 @@ public class KurentoSessionManager extends SessionManager { String senderPrivateId = kSession.getParticipantPrivateIdFromStreamId(streamId); if (senderPrivateId != null) { KurentoParticipant sender = (KurentoParticipant) kSession.getParticipantByPrivateId(senderPrivateId); - kParticipant.receiveMediaFrom(sender, sdpString, true); - sessionEventsHandler.onSubscribe(participant, kSession, transactionId, null); + kParticipant.cancelReceivingMedia(sender, null, true); + String sdpAnswer = kParticipant.receiveMediaFrom(sender, sdpOffer, true); + if (sdpAnswer == null) { + throw new OpenViduException(Code.MEDIA_SDP_ERROR_CODE, + "Unable to generate SDP answer when reconnecting subscriber to '" + streamId + "'"); + } + + log.debug("SDP Answer for subscribing reconnection PARTICIPANT {}: {}", + participant.getParticipantPublicId(), sdpAnswer); + sessionEventsHandler.onSubscribe(participant, kSession, sdpAnswer, transactionId, null); } else { throw new OpenViduException(Code.USER_NOT_STREAMING_ERROR_CODE, "Stream '" + streamId + "' does not exist in Session '" + kSession.getSessionId() + "'"); @@ -1238,7 +1205,13 @@ public class KurentoSessionManager extends SessionManager { return ((KurentoSession) session).getParticipantPrivateIdFromStreamId(streamId); } - private void applyFilterInPublisher(KurentoParticipant kParticipant, KurentoFilter filter) + @Override + public void onVideoData(Participant participant, Integer transactionId, Integer height, Integer width, + Boolean videoActive, Boolean audioActive) { + sessionEventsHandler.onVideoData(participant, transactionId, height, width, videoActive, audioActive); + } + + protected void applyFilterInPublisher(KurentoParticipant kParticipant, KurentoFilter filter) throws OpenViduException { GenericMediaElement.Builder builder = new GenericMediaElement.Builder(kParticipant.getPipeline(), filter.getType()); @@ -1250,13 +1223,13 @@ public class KurentoSessionManager extends SessionManager { kParticipant.getPublisher().getMediaOptions().setFilter(filter); } - private void removeFilterInPublisher(KurentoParticipant kParticipant) { + protected void removeFilterInPublisher(KurentoParticipant kParticipant) { kParticipant.getPublisher().cleanAllFilterListeners(); kParticipant.getPublisher().revert(kParticipant.getPublisher().getFilter()); kParticipant.getPublisher().getMediaOptions().setFilter(null); } - private KurentoFilter execFilterMethodInPublisher(KurentoParticipant kParticipant, String method, + protected KurentoFilter execFilterMethodInPublisher(KurentoParticipant kParticipant, String method, JsonObject params) { kParticipant.getPublisher().execMethod(method, params); KurentoFilter filter = kParticipant.getPublisher().getMediaOptions().getFilter(); @@ -1265,7 +1238,7 @@ public class KurentoSessionManager extends SessionManager { return updatedFilter; } - private void addFilterEventListenerInPublisher(KurentoParticipant kParticipant, String eventType) + protected void addFilterEventListenerInPublisher(KurentoParticipant kParticipant, String eventType) throws OpenViduException { PublisherEndpoint pub = kParticipant.getPublisher(); if (!pub.isListenerAddedToFilterEvent(eventType)) { @@ -1289,11 +1262,27 @@ public class KurentoSessionManager extends SessionManager { } } - private void removeFilterEventListenerInPublisher(KurentoParticipant kParticipant, String eventType) { + protected void removeFilterEventListenerInPublisher(KurentoParticipant kParticipant, String eventType) { PublisherEndpoint pub = kParticipant.getPublisher(); if (pub.isListenerAddedToFilterEvent(eventType)) { GenericMediaElement filter = kParticipant.getPublisher().getFilter(); filter.removeEventListener(pub.removeListener(eventType)); } } + + protected Kms selectMediaNode(Session session) throws OpenViduException { + Kms lessLoadedKms = null; + try { + lessLoadedKms = this.kmsManager.getLessLoadedConnectedAndRunningKms(); + } catch (NoSuchElementException e) { + // Restore session not active + this.cleanCollections(session.getSessionId()); + this.storeSessionNotActive(session); + throw new OpenViduException(Code.ROOM_CANNOT_BE_CREATED_ERROR_CODE, + "There is no available Media Node where to initialize session '" + session.getSessionId() + "'"); + } + log.info("KMS less loaded is {} with a load of {}", lessLoadedKms.getUri(), lessLoadedKms.getLoad()); + return lessLoadedKms; + } + } diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoTokenOptions.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoTokenOptions.java deleted file mode 100644 index cc785116..00000000 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoTokenOptions.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * (C) Copyright 2017-2020 OpenVidu (https://openvidu.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package io.openvidu.server.kurento.core; - -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; - -public class KurentoTokenOptions { - - private Integer videoMaxRecvBandwidth; - private Integer videoMinRecvBandwidth; - private Integer videoMaxSendBandwidth; - private Integer videoMinSendBandwidth; - private Map allowedFilters = new ConcurrentHashMap<>(); - - public KurentoTokenOptions(JsonObject options) { - if (options.has("videoMaxRecvBandwidth")) { - this.videoMaxRecvBandwidth = options.get("videoMaxRecvBandwidth").getAsInt(); - } - if (options.has("videoMinRecvBandwidth")) { - this.videoMinRecvBandwidth = options.get("videoMinRecvBandwidth").getAsInt(); - } - if (options.has("videoMaxSendBandwidth")) { - this.videoMaxSendBandwidth = options.get("videoMaxSendBandwidth").getAsInt(); - } - if (options.has("videoMinSendBandwidth")) { - this.videoMinSendBandwidth = options.get("videoMinSendBandwidth").getAsInt(); - } - if (options.has("allowedFilters")) { - JsonArray filters = options.get("allowedFilters").getAsJsonArray(); - Iterator it = filters.iterator(); - while (it.hasNext()) { - this.allowedFilters.put(it.next().getAsString(), true); - } - } - } - - public Integer getVideoMaxRecvBandwidth() { - return videoMaxRecvBandwidth; - } - - public Integer getVideoMinRecvBandwidth() { - return videoMinRecvBandwidth; - } - - public Integer getVideoMaxSendBandwidth() { - return videoMaxSendBandwidth; - } - - public Integer getVideoMinSendBandwidth() { - return videoMinSendBandwidth; - } - - public String[] getAllowedFilters() { - return allowedFilters.keySet().stream().toArray(String[]::new); - } - - public boolean isFilterAllowed(String filterType) { - return this.allowedFilters.containsKey(filterType); - } - -} diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/MediaEndpoint.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/MediaEndpoint.java index ff6dfe36..19bc9899 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/MediaEndpoint.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/endpoint/MediaEndpoint.java @@ -54,11 +54,11 @@ import com.google.gson.JsonObject; import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; +import io.openvidu.java.client.KurentoOptions; import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.core.Participant; import io.openvidu.server.kurento.core.KurentoMediaOptions; import io.openvidu.server.kurento.core.KurentoParticipant; -import io.openvidu.server.kurento.core.KurentoTokenOptions; /** * {@link Endpoint} wrapper. Can be based on WebRtcEndpoint (that supports @@ -129,7 +129,7 @@ public abstract class MediaEndpoint { this.openviduConfig = openviduConfig; - KurentoTokenOptions kurentoTokenOptions = this.owner.getToken().getKurentoTokenOptions(); + KurentoOptions kurentoTokenOptions = this.owner.getToken().getKurentoOptions(); if (kurentoTokenOptions != null) { this.maxRecvKbps = kurentoTokenOptions.getVideoMaxRecvBandwidth() != null ? kurentoTokenOptions.getVideoMaxRecvBandwidth() @@ -365,6 +365,9 @@ public abstract class MediaEndpoint { if (!mediaOptions.adaptativeBitrate) { playerBuilder = playerBuilder.useEncodedMedia(); } + if (mediaOptions.networkCache != null) { + playerBuilder = playerBuilder.withNetworkCache(mediaOptions.networkCache); + } playerBuilder.buildAsync(new Continuation() { diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/kms/FixedOneKmsManager.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/kms/FixedOneKmsManager.java index 192859a1..fd4e23c6 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/kms/FixedOneKmsManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/kms/FixedOneKmsManager.java @@ -20,24 +20,37 @@ package io.openvidu.server.kurento.kms; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.NoSuchElementException; import javax.annotation.PostConstruct; -import org.apache.commons.lang3.RandomStringUtils; import org.kurento.client.KurentoClient; import org.kurento.commons.exception.KurentoException; +import org.kurento.jsonrpc.client.JsonRpcClientNettyWebSocket; +import org.kurento.jsonrpc.client.JsonRpcWSConnectionListener; -import io.openvidu.server.core.IdentifierPrefixes; +import io.openvidu.java.client.RecordingProperties; +import io.openvidu.server.core.Session; +import io.openvidu.server.core.SessionManager; public class FixedOneKmsManager extends KmsManager { + public FixedOneKmsManager(SessionManager sessionManager) { + super(sessionManager); + } + @Override - public List initializeKurentoClients(List kmsProperties, boolean disconnectUponFailure) throws Exception { + public List initializeKurentoClients(List kmsProperties, boolean disconnectUponFailure) + throws Exception { KmsProperties firstProps = kmsProperties.get(0); KurentoClient kClient = null; - Kms kms = new Kms(firstProps, loadManager); + Kms kms = new Kms(firstProps, loadManager, quarantineKiller); try { - kClient = KurentoClient.create(firstProps.getUri(), this.generateKurentoConnectionListener(kms.getId())); + JsonRpcWSConnectionListener listener = this.generateKurentoConnectionListener(kms.getId()); + JsonRpcClientNettyWebSocket client = new JsonRpcClientNettyWebSocket(firstProps.getUri(), listener); + client.setTryReconnectingMaxTime(0); + client.setTryReconnectingForever(false); + kClient = KurentoClient.createFromJsonRpcClient(client); this.addKms(kms); kms.setKurentoClient(kClient); @@ -55,6 +68,25 @@ public class FixedOneKmsManager extends KmsManager { return Arrays.asList(kms); } + @Override + public void incrementActiveRecordings(RecordingProperties properties, String recordingId, Session session) { + try { + this.getKmss().iterator().next().incrementActiveRecordings(recordingId, session.getSessionId()); + } catch (NoSuchElementException e) { + log.error("There is no KMS available when incrementing active recordings"); + } + } + + @Override + public void decrementActiveRecordings(RecordingProperties recordingProperties, String recordingId, + Session session) { + try { + this.getKmss().iterator().next().decrementActiveRecordings(recordingId); + } catch (NoSuchElementException e) { + log.error("There is no KMS available when decrementing active recordings"); + } + } + @Override @PostConstruct protected void postConstructInitKurentoClients() { diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/kms/Kms.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/kms/Kms.java index 72b3cca4..bcdd10f9 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/kms/Kms.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/kms/Kms.java @@ -21,9 +21,10 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.kurento.client.KurentoClient; @@ -36,6 +37,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import io.openvidu.server.kurento.core.KurentoSession; +import io.openvidu.server.utils.QuarantineKiller; /** * Abstraction of a KMS instance: an object of this class corresponds to a KMS @@ -57,15 +59,16 @@ public class Kms { private String ip; private KurentoClient client; private LoadManager loadManager; + private QuarantineKiller quarantineKiller; private AtomicBoolean isKurentoClientConnected = new AtomicBoolean(false); private AtomicLong timeOfKurentoClientConnection = new AtomicLong(0); private AtomicLong timeOfKurentoClientDisconnection = new AtomicLong(0); private Map kurentoSessions = new ConcurrentHashMap<>(); - private AtomicInteger activeRecordings = new AtomicInteger(0); + private Map activeRecordings = new ConcurrentHashMap<>(); - public Kms(KmsProperties props, LoadManager loadManager) { + public Kms(KmsProperties props, LoadManager loadManager, QuarantineKiller quarantineKiller) { this.id = props.getId(); this.uri = props.getUri(); @@ -79,6 +82,7 @@ public class Kms { this.ip = url.getHost(); this.loadManager = loadManager; + this.quarantineKiller = quarantineKiller; } public void setKurentoClient(KurentoClient client) { @@ -145,16 +149,25 @@ public class Kms { this.kurentoSessions.remove(sessionId); } - public AtomicInteger getActiveRecordings() { - return this.activeRecordings; + public synchronized Set> getActiveRecordings() { + return this.activeRecordings.entrySet(); + } + + public synchronized void incrementActiveRecordings(String recordingId, String sessionId) { + this.activeRecordings.put(recordingId, sessionId); + } + + public synchronized void decrementActiveRecordings(String recordingId) { + this.activeRecordings.remove(recordingId); + this.quarantineKiller.dropMediaNode(this.id); } public JsonObject toJson() { JsonObject json = new JsonObject(); json.addProperty("id", this.id); + json.addProperty("object", "mediaNode"); json.addProperty("ip", this.ip); json.addProperty("uri", this.uri); - final boolean connected = this.isKurentoClientConnected(); json.addProperty("connected", connected); json.addProperty("connectionTime", this.getTimeOfKurentoClientConnection()); @@ -164,18 +177,26 @@ public class Kms { return json; } - public JsonObject toJsonExtended(boolean withSessions, boolean withExtraInfo) { + public JsonObject toJsonExtended(boolean withSessions, boolean withRecordings, boolean withExtraInfo) { JsonObject json = this.toJson(); if (withSessions) { JsonArray sessions = new JsonArray(); for (KurentoSession session : this.kurentoSessions.values()) { - sessions.add(session.toJson()); + sessions.add(session.toJson(false, false)); } json.add("sessions", sessions); } + if (withRecordings) { + JsonArray activeRecordingsJson = new JsonArray(); + for (String recordingId : this.activeRecordings.keySet()) { + activeRecordingsJson.add(recordingId); + } + json.add("recordingIds", activeRecordingsJson); + } + if (withExtraInfo) { if (json.get("connected").getAsBoolean()) { diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/kms/KmsManager.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/kms/KmsManager.java index f93112c3..b1fcab94 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/kms/KmsManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/kms/KmsManager.java @@ -23,8 +23,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -34,17 +34,23 @@ import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import org.apache.commons.lang3.RandomStringUtils; -import org.kurento.client.KurentoConnectionListener; +import org.kurento.jsonrpc.client.JsonRpcWSConnectionListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import com.google.gson.JsonObject; +import io.openvidu.java.client.RecordingProperties; import io.openvidu.server.config.OpenviduConfig; +import io.openvidu.server.core.EndReason; import io.openvidu.server.core.IdentifierPrefixes; +import io.openvidu.server.core.Session; +import io.openvidu.server.core.SessionEventsHandler; +import io.openvidu.server.core.SessionManager; import io.openvidu.server.kurento.core.KurentoSession; import io.openvidu.server.utils.MediaNodeStatusManager; +import io.openvidu.server.utils.QuarantineKiller; import io.openvidu.server.utils.UpdatableTimerTask; public abstract class KmsManager { @@ -54,7 +60,7 @@ public abstract class KmsManager { public static final Lock selectAndRemoveKmsLock = new ReentrantLock(true); public static final int MAX_SECONDS_LOCK_WAIT = 15; - private Map kmsReconnectionLocks = new ConcurrentHashMap<>(); + private UpdatableTimerTask kurentoReconnectTimer; public class KmsLoad implements Comparable { @@ -85,8 +91,8 @@ public abstract class KmsManager { return json; } - public JsonObject toJsonExtended(boolean withSessions, boolean withExtraInfo) { - JsonObject json = this.kms.toJsonExtended(withSessions, withExtraInfo); + public JsonObject toJsonExtended(boolean withSessions, boolean withRecordings, boolean withExtraInfo) { + JsonObject json = this.kms.toJsonExtended(withSessions, withRecordings, withExtraInfo); json.addProperty("load", this.load); return json; } @@ -98,11 +104,23 @@ public abstract class KmsManager { @Autowired protected LoadManager loadManager; + @Autowired + protected QuarantineKiller quarantineKiller; + @Autowired protected MediaNodeStatusManager mediaNodeStatusManager; + @Autowired + protected SessionEventsHandler sessionEventsHandler; + final protected Map kmss = new ConcurrentHashMap<>(); + private SessionManager sessionManager; + + public KmsManager(SessionManager sessionManager) { + this.sessionManager = sessionManager; + } + public synchronized void addKms(Kms kms) { this.kmss.put(kms.getId(), kms); } @@ -121,6 +139,13 @@ public abstract class KmsManager { } } + public synchronized boolean atLeastOneConnectedAndRunningKms() { + Optional optional = this.kmss.values().stream() + .filter(kms -> kms.isKurentoClientConnected() && mediaNodeStatusManager.isRunning(kms.getId())) + .findFirst(); + return optional.isPresent(); + } + public synchronized List getKmssSortedByLoad() { List kmsLoads = getKmsLoads(); Collections.sort(kmsLoads); @@ -131,11 +156,6 @@ public abstract class KmsManager { return this.kmss.get(kmsId); } - public KmsLoad getKmsLoad(String kmsId) { - Kms kms = this.kmss.get(kmsId); - return new KmsLoad(kms, kms.getLoad()); - } - public Collection getKmss() { return this.kmss.values(); } @@ -154,55 +174,14 @@ public abstract class KmsManager { return kmsLoads; } - protected KurentoConnectionListener generateKurentoConnectionListener(final String kmsId) { - return new KurentoConnectionListener() { + protected JsonRpcWSConnectionListener generateKurentoConnectionListener(final String kmsId) { + return new JsonRpcWSConnectionListener() { @Override public void reconnected(boolean sameServer) { - final Kms kms = kmss.get(kmsId); - log.info("Kurento Client \"reconnected\" event for KMS {} (sameServer: {}) [{}]", kms.getUri(), sameServer, kms.getKurentoClient().toString()); - - kmsReconnectionLocks.putIfAbsent(kms.getId(), new ReentrantLock()); - boolean lockAcquired = false; - try { - if (kmsReconnectionLocks.get(kms.getId()).tryLock(5, TimeUnit.SECONDS)) { - lockAcquired = true; - if (kms.isKurentoClientConnected()) { - // Timer task of disconnected event successfully executed - log.warn( - "Timer task already executed for reconnected Kurento Client [{}] to KMS with uri {}. Skipping event listener execution", - kms.getKurentoClient().toString(), kms.getUri()); - return; - } - kms.setKurentoClientConnected(true); - kms.setTimeOfKurentoClientConnection(System.currentTimeMillis()); - if (!sameServer) { - // Different KMS. Reset sessions status (no Publisher or SUbscriber endpoints) - log.warn("Kurento Client reconnected to a different KMS instance, with uri {}", - kms.getUri()); - log.warn("Updating all webrtc endpoints for active sessions"); - final long timeOfKurentoDisconnection = kms.getTimeOfKurentoClientDisconnection(); - kms.getKurentoSessions().forEach(kSession -> { - kSession.restartStatusInKurento(timeOfKurentoDisconnection); - }); - } else { - // Same KMS. We may infer that openvidu-server/KMS connection has been lost, but - // not the clients/KMS connections - log.warn("Kurento Client reconnected to same KMS {} with uri {}", kmsId, kms.getUri()); - } - kms.setTimeOfKurentoClientDisconnection(0); - } - } catch (InterruptedException e) { - log.error("InterruptedException when waiting for lock on reconnected event of KMS with uri {}", - kms.getUri()); - } finally { - if (lockAcquired) { - kmsReconnectionLocks.get(kms.getId()).unlock(); - } - } } @Override @@ -221,74 +200,67 @@ public abstract class KmsManager { kms.getUri(), kms.getKurentoClient().toString()); } - // TODO: this is a fix for the lack of reconnected event - kmsReconnectionLocks.putIfAbsent(kms.getId(), new ReentrantLock()); - final UpdatableTimerTask[] TIMER = new UpdatableTimerTask[1]; - final AtomicInteger ITERATION = new AtomicInteger(0); + final int loops = 6; + final AtomicInteger iteration = new AtomicInteger(loops); + final long intervalWaitMs = 500L; - TIMER[0] = new UpdatableTimerTask(() -> { - boolean lockAcquired = false; - try { - if (kmsReconnectionLocks.get(kms.getId()).tryLock(5, TimeUnit.SECONDS)) { - lockAcquired = true; + kurentoReconnectTimer = new UpdatableTimerTask(() -> { + if (iteration.decrementAndGet() < 0) { - if (kms.isKurentoClientConnected()) { - // reconnected listener already executed - log.info( - "Timer of KMS with uri {} and KurentoClient [{}] cancelled (reconnected event received during interval wait)", - kms.getUri(), kms.getKurentoClient().toString()); - TIMER[0].cancelTimer(); - return; - } + log.error("KurentoClient [{}] could not reconnect to KMS with uri {} in {} seconds", + kms.getKurentoClient().toString(), kms.getUri(), (intervalWaitMs * 6 / 1000)); + kurentoReconnectTimer.cancelTimer(); + log.warn("Closing {} sessions hosted by KMS with uri {}: {}", kms.getKurentoSessions().size(), + kms.getUri(), kms.getKurentoSessions().stream().map(s -> s.getSessionId()) + .collect(Collectors.joining(",", "[", "]"))); - if (kms.getKurentoClient().isClosed()) { - log.info( - "Timer of KMS with uri {} and KurentoClient [{}] has been closed. Cancelling Timer", - kms.getUri(), kms.getKurentoClient().toString()); - TIMER[0].cancelTimer(); - return; - } + final long timeOfKurentoDisconnection = kms.getTimeOfKurentoClientDisconnection(); + sessionEventsHandler.onMediaServerCrashed(kms, timeOfKurentoDisconnection); + kms.getKurentoSessions().forEach(kSession -> { + sessionManager.closeSession(kSession.getSessionId(), EndReason.mediaServerCrashed); + }); + + } else { + + try { kms.getKurentoClient().getServerManager().getInfo(); - log.info("According to Timer KMS with uri {} and KurentoClient [{}] is now reconnected", - kms.getUri(), kms.getKurentoClient().toString()); - TIMER[0].cancelTimer(); - kms.setKurentoClientConnected(true); - kms.setTimeOfKurentoClientConnection(System.currentTimeMillis()); + } catch (Exception e) { + log.error( + "According to Timer KMS with uri {} and KurentoClient [{}] is not reconnected yet. Exception {}", + kms.getUri(), kms.getKurentoClient().toString(), e.getClass().getName()); + return; + } - final long timeOfKurentoDisconnection = kms.getTimeOfKurentoClientDisconnection(); + log.info("According to Timer KMS with uri {} and KurentoClient [{}] is now reconnected", + kms.getUri(), kms.getKurentoClient().toString()); + kurentoReconnectTimer.cancelTimer(); + kms.setKurentoClientConnected(true); + kms.setTimeOfKurentoClientConnection(System.currentTimeMillis()); - if (kms.getKurentoSessions().isEmpty()) { - log.info("There were no sessions in the KMS with uri {}. Nothing must be done", - kms.getUri()); + final long timeOfKurentoDisconnection = kms.getTimeOfKurentoClientDisconnection(); + + if (kms.getKurentoSessions().isEmpty()) { + log.info("There were no sessions in the KMS with uri {}. Nothing must be done", + kms.getUri()); + } else { + if (isNewKms(kms)) { + log.warn("KMS with URI {} is a new KMS process. Resetting {} sessions: {}", + kms.getUri(), kms.getKurentoSessions().size(), kms.getKurentoSessions().stream() + .map(s -> s.getSessionId()).collect(Collectors.joining(",", "[", "]"))); + kms.getKurentoSessions().forEach(kSession -> { + kSession.restartStatusInKurento(timeOfKurentoDisconnection); + }); } else { - if (isNewKms(kms)) { - log.warn("KMS with URI {} is a new KMS process. Resetting {} sessions: {}", - kms.getUri(), kms.getKurentoSessions().size(), - kms.getKurentoSessions().stream().map(s -> s.getSessionId()) - .collect(Collectors.joining(",", "[", "]"))); - kms.getKurentoSessions().forEach(kSession -> { - kSession.restartStatusInKurento(timeOfKurentoDisconnection); - }); - } else { - log.info("KMS with URI {} is the same process. Nothing must be done", kms.getUri()); - } + log.info("KMS with URI {} is the same process. Nothing must be done", kms.getUri()); } + } - kms.setTimeOfKurentoClientDisconnection(0); - } - } catch (Exception e) { - log.error( - "According to Timer KMS with uri {} and KurentoClient [{}] is not reconnected yet. Exception {}", - kms.getUri(), kms.getKurentoClient().toString(), e.getClass().getName()); - } finally { - if (lockAcquired) { - kmsReconnectionLocks.get(kms.getId()).unlock(); - } + kms.setTimeOfKurentoClientDisconnection(0); } - }, () -> new Long(dynamicReconnectLoopSeconds(ITERATION.getAndIncrement()) * 1000)); + }, () -> intervalWaitMs); // Try 2 times per seconds - TIMER[0].updateTimer(); + kurentoReconnectTimer.updateTimer(); } @Override @@ -308,6 +280,11 @@ public abstract class KmsManager { // kms.setKurentoClientConnected(true); // kms.setTimeOfKurentoClientConnection(System.currentTimeMillis()); } + + @Override + public void reconnecting() { + } + }; } @@ -323,36 +300,23 @@ public abstract class KmsManager { } } - private int dynamicReconnectLoopSeconds(int iteration) { - // First 10 loops every second, next 20 loops ever 3s, the rest every 10s - final int[][] intervals = { new int[] { 1, 10 }, new int[] { 3, 20 }, new int[] { 10, Integer.MAX_VALUE } }; - - int accumulatedIntervals = 0; - for (int i = 0; i < intervals.length - 1; i++) { - if ((accumulatedIntervals + intervals[i][1]) > iteration) { - // Interval found for current iteration - return intervals[i][0]; - } else { - // This iteration has already been surpassed - accumulatedIntervals += intervals[i][1]; - } - } - // Return last interval - return intervals[intervals.length - 1][0]; - } - public abstract List initializeKurentoClients(List kmsProperties, boolean disconnectUponFailure) throws Exception; - public LoadManager getLoadManager() { - return this.loadManager; - } + public abstract void incrementActiveRecordings(RecordingProperties recordingProperties, String recordingId, + Session session); + + public abstract void decrementActiveRecordings(RecordingProperties recordingProperties, String recordingId, + Session session); @PostConstruct protected abstract void postConstructInitKurentoClients(); @PreDestroy public void close() { + if (kurentoReconnectTimer != null) { + kurentoReconnectTimer.cancelTimer(); + } log.info("Closing all KurentoClients"); this.kmss.values().forEach(kms -> { kms.getKurentoClient().destroy(); diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/CompositeWrapper.java b/openvidu-server/src/main/java/io/openvidu/server/recording/CompositeWrapper.java index d8946d86..1d66e9af 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/CompositeWrapper.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/CompositeWrapper.java @@ -86,8 +86,8 @@ public class CompositeWrapper { this.recorderEndpoint.record(); } - public synchronized void stopCompositeRecording(CountDownLatch stopLatch, Long timeOfKmsDisconnection) { - if (timeOfKmsDisconnection == 0) { + public synchronized void stopCompositeRecording(CountDownLatch stopLatch, Long kmsDisconnectionTime) { + if (kmsDisconnectionTime == null) { this.recorderEndpoint.addStoppedListener(new EventListener() { @Override public void onEvent(StoppedEvent event) { @@ -101,7 +101,7 @@ public class CompositeWrapper { }); this.recorderEndpoint.stop(); } else { - endTime = timeOfKmsDisconnection; + endTime = kmsDisconnectionTime; stopLatch.countDown(); log.warn("Forcing composed audio-only recording stop after KMS restart in session {}", this.session.getSessionId()); diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/DummyRecordingDownloader.java b/openvidu-server/src/main/java/io/openvidu/server/recording/DummyRecordingDownloader.java index d62cb0ee..20ddbd0a 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/DummyRecordingDownloader.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/DummyRecordingDownloader.java @@ -23,7 +23,7 @@ import java.util.Collection; public class DummyRecordingDownloader implements RecordingDownloader { @Override - public void downloadRecording(Recording recording, Collection streamIds, Runnable callback) + public void downloadRecording(Recording recording, Collection wrappers, Runnable callback) throws IOException { // Just immediately run callback function callback.run(); diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/DummyRecordingUploader.java b/openvidu-server/src/main/java/io/openvidu/server/recording/DummyRecordingUploader.java new file mode 100644 index 00000000..828e10c1 --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/DummyRecordingUploader.java @@ -0,0 +1,20 @@ +package io.openvidu.server.recording; + +public class DummyRecordingUploader implements RecordingUploader { + + @Override + public void uploadRecording(Recording recording, Runnable successCallback, Runnable errorCallback) { + // Just immediately run success callback function + successCallback.run(); + } + + @Override + public void storeAsUploadingRecording(String recording) { + } + + @Override + public boolean isBeingUploaded(String recordingId) { + return false; + } + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/RecorderEndpointWrapper.java b/openvidu-server/src/main/java/io/openvidu/server/recording/RecorderEndpointWrapper.java index 51577e2c..f6b7dc29 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/RecorderEndpointWrapper.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/RecorderEndpointWrapper.java @@ -17,13 +17,19 @@ package io.openvidu.server.recording; +import org.apache.commons.lang3.StringUtils; import org.kurento.client.RecorderEndpoint; import com.google.gson.JsonObject; +import io.openvidu.server.kurento.core.KurentoParticipant; +import io.openvidu.server.recording.service.RecordingService; + public class RecorderEndpointWrapper { private RecorderEndpoint recorder; + private KurentoParticipant kParticipant; + private String name; private String connectionId; private String recordingId; private String streamId; @@ -37,23 +43,57 @@ public class RecorderEndpointWrapper { private long endTime; private long size; - public RecorderEndpointWrapper(RecorderEndpoint recorder, String connectionId, String recordingId, String streamId, - String clientData, String serverData, boolean hasAudio, boolean hasVideo, String typeOfVideo) { + public RecorderEndpointWrapper(RecorderEndpoint recorder, KurentoParticipant kParticipant, String recordingId, + String name) { + this.name = name; this.recorder = recorder; - this.connectionId = connectionId; + this.kParticipant = kParticipant; this.recordingId = recordingId; - this.streamId = streamId; - this.clientData = clientData; - this.serverData = serverData; - this.hasAudio = hasAudio; - this.hasVideo = hasVideo; - this.typeOfVideo = typeOfVideo; + this.connectionId = kParticipant.getParticipantPublicId(); + this.streamId = kParticipant.getPublisherStreamId(); + this.clientData = kParticipant.getClientMetadata(); + this.serverData = kParticipant.getServerMetadata(); + this.hasAudio = kParticipant.getPublisher().getMediaOptions().hasAudio(); + this.hasVideo = kParticipant.getPublisher().getMediaOptions().hasVideo(); + this.typeOfVideo = kParticipant.getPublisher().getMediaOptions().getTypeOfVideo(); + } + + public RecorderEndpointWrapper(JsonObject json) { + String nameAux = json.get("name").getAsString(); + // If the name includes the extension, remove it + this.name = StringUtils.removeEnd(nameAux, RecordingService.INDIVIDUAL_RECORDING_EXTENSION); + this.connectionId = json.get("connectionId").getAsString(); + this.streamId = json.get("streamId").getAsString(); + this.clientData = (json.has("clientData") && !json.get("clientData").isJsonNull()) + ? json.get("clientData").getAsString() + : null; + this.serverData = json.get("serverData").getAsString(); + this.startTime = json.get("startTime").getAsLong(); + this.endTime = json.get("endTime").getAsLong(); + this.size = json.get("size").getAsLong(); + this.hasAudio = json.get("hasAudio").getAsBoolean(); + this.hasVideo = json.get("hasVideo").getAsBoolean(); + if (this.hasVideo) { + this.typeOfVideo = json.get("typeOfVideo").getAsString(); + } } public RecorderEndpoint getRecorder() { return recorder; } + public KurentoParticipant getParticipant() { + return this.kParticipant; + } + + public String getName() { + return this.name; + } + + public String getNameWithExtension() { + return this.name + RecordingService.INDIVIDUAL_RECORDING_EXTENSION; + } + public String getConnectionId() { return connectionId; } @@ -112,18 +152,19 @@ public class RecorderEndpointWrapper { public JsonObject toJson() { JsonObject json = new JsonObject(); - json.addProperty("connectionId", this.connectionId); - json.addProperty("streamId", this.streamId); - json.addProperty("clientData", this.clientData); - json.addProperty("serverData", this.serverData); - json.addProperty("startTime", this.startTime); - json.addProperty("endTime", this.endTime); - json.addProperty("duration", this.endTime - this.startTime); - json.addProperty("size", this.size); - json.addProperty("hasAudio", this.hasAudio); - json.addProperty("hasVideo", this.hasVideo); - if (this.hasVideo) { - json.addProperty("typeOfVideo", this.typeOfVideo); + json.addProperty("name", this.getNameWithExtension()); + json.addProperty("connectionId", this.getConnectionId()); + json.addProperty("streamId", this.getStreamId()); + json.addProperty("clientData", this.getClientData()); + json.addProperty("serverData", this.getServerData()); + json.addProperty("startTime", this.getStartTime()); + json.addProperty("endTime", this.getEndTime()); + json.addProperty("duration", this.getEndTime() - this.getStartTime()); + json.addProperty("size", this.getSize()); + json.addProperty("hasAudio", this.hasAudio()); + json.addProperty("hasVideo", this.hasVideo()); + if (this.hasVideo()) { + json.addProperty("typeOfVideo", this.getTypeOfVideo()); } return json; } diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/Recording.java b/openvidu-server/src/main/java/io/openvidu/server/recording/Recording.java index bcaf216a..e35805ee 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/Recording.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/Recording.java @@ -61,7 +61,7 @@ public class Recording { try { this.duration = json.get("duration").getAsDouble(); } catch (Exception e) { - this.duration = new Long((long) json.get("duration").getAsLong()).doubleValue(); + this.duration = Long.valueOf((long) json.get("duration").getAsLong()).doubleValue(); } if (json.get("url").isJsonNull()) { this.url = null; @@ -191,6 +191,7 @@ public class Recording { public JsonObject toJson() { JsonObject json = new JsonObject(); json.addProperty("id", this.id); + json.addProperty("object", "recording"); json.addProperty("name", this.recordingProperties.name()); json.addProperty("outputMode", this.getOutputMode().name()); if (RecordingUtils.IS_COMPOSED(this.recordingProperties.outputMode()) && this.hasVideo) { diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingDownloader.java b/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingDownloader.java index 65c750b5..71d392c5 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingDownloader.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingDownloader.java @@ -22,7 +22,7 @@ import java.util.Collection; public interface RecordingDownloader { - public void downloadRecording(Recording recording, Collection streamIds, Runnable callback) + public void downloadRecording(Recording recording, Collection wrappers, Runnable callback) throws IOException; public void cancelDownload(String recordingId); diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingInfoUtils.java b/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingInfoUtils.java index f4e2cddd..ac303add 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingInfoUtils.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingInfoUtils.java @@ -50,7 +50,7 @@ public class RecordingInfoUtils { throw new OpenViduException(Code.RECORDING_FILE_EMPTY_ERROR, "The recording file is corrupted"); } if (this.json.size() == 0) { - // Recording metadata from ffprobe is an emtpy JSON + // Recording metadata from ffprobe is an empty JSON throw new OpenViduException(Code.RECORDING_FILE_EMPTY_ERROR, "The recording file is empty"); } diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingUploader.java b/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingUploader.java new file mode 100644 index 00000000..e88b8719 --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingUploader.java @@ -0,0 +1,11 @@ +package io.openvidu.server.recording; + +public interface RecordingUploader { + + void uploadRecording(Recording recording, Runnable successCallback, Runnable errorCallback); + + void storeAsUploadingRecording(String recording); + + boolean isBeingUploaded(String recordingId); + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedQuickStartRecordingService.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedQuickStartRecordingService.java index 3f529772..c9dc6e6f 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedQuickStartRecordingService.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedQuickStartRecordingService.java @@ -1,216 +1,309 @@ package io.openvidu.server.recording.service; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Volume; + import io.openvidu.client.OpenViduException; +import io.openvidu.client.OpenViduException.Code; import io.openvidu.java.client.RecordingProperties; import io.openvidu.server.cdr.CallDetailRecord; import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.core.EndReason; import io.openvidu.server.core.Session; +import io.openvidu.server.kurento.kms.KmsManager; import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.RecordingDownloader; -import io.openvidu.server.utils.QuarantineKiller; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; +import io.openvidu.server.recording.RecordingUploader; +import io.openvidu.server.utils.CustomFileManager; +import io.openvidu.server.utils.DockerManager; public class ComposedQuickStartRecordingService extends ComposedRecordingService { - private static final Logger log = LoggerFactory.getLogger(ComposedRecordingService.class); + private static final Logger log = LoggerFactory.getLogger(ComposedRecordingService.class); - public ComposedQuickStartRecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader, OpenviduConfig openviduConfig, CallDetailRecord cdr, QuarantineKiller quarantineKiller) { - super(recordingManager, recordingDownloader, openviduConfig, cdr, quarantineKiller); - } + public ComposedQuickStartRecordingService(RecordingManager recordingManager, + RecordingDownloader recordingDownloader, RecordingUploader recordingUploader, KmsManager kmsManager, + CustomFileManager fileManager, OpenviduConfig openviduConfig, CallDetailRecord cdr, + DockerManager dockerManager) { + super(recordingManager, recordingDownloader, recordingUploader, kmsManager, fileManager, openviduConfig, cdr, + dockerManager); + } - public void stopRecordingContainer(Session session, EndReason reason) { - log.info("Stopping COMPOSED_QUICK_START of session {}. Reason: {}", - session.getSessionId(), RecordingManager.finalReason(reason)); + public void stopRecordingContainer(Session session, EndReason reason) { + log.info("Stopping COMPOSED_QUICK_START of session {}. Reason: {}", session.getSessionId(), + RecordingManager.finalReason(reason)); - String containerId = this.sessionsContainers.get(session.getSessionId()); + String containerId = this.sessionsContainers.get(session.getSessionId()); - if (containerId != null) { - try { - dockerManager.removeDockerContainer(containerId, true); - } catch (Exception e) { - log.error("Can't remove COMPOSED_QUICK_START recording container from session {}", session.getSessionId()); - } + if (containerId != null) { + try { + dockerManager.removeContainer(session.getMediaNodeId(), containerId, true); + } catch (Exception e) { + log.error("Can't remove COMPOSED_QUICK_START recording container from session {}", + session.getSessionId()); + } - containers.remove(containerId); - sessionsContainers.remove(session.getSessionId()); - } + containers.remove(containerId); + sessionsContainers.remove(session.getSessionId()); + } - } + } - @Override - protected Recording startRecordingWithVideo(Session session, Recording recording, RecordingProperties properties) - throws OpenViduException { + @Override + protected Recording startRecordingWithVideo(Session session, Recording recording, RecordingProperties properties) + throws OpenViduException { - log.info("Starting COMPOSED_QUICK_START ({}) recording {} of session {}", - properties.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId()); + log.info("Starting COMPOSED_QUICK_START ({}) recording {} of session {}", + properties.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId()); - List envs = new ArrayList<>(); + List envs = new ArrayList<>(); - envs.add("DEBUG_MODE=" + openviduConfig.isOpenViduRecordingDebug()); - envs.add("RESOLUTION=" + properties.resolution()); - envs.add("ONLY_VIDEO=" + !properties.hasAudio()); - envs.add("FRAMERATE=30"); - envs.add("VIDEO_ID=" + recording.getId()); - envs.add("VIDEO_NAME=" + properties.name()); - envs.add("VIDEO_FORMAT=mp4"); - envs.add("RECORDING_JSON='" + recording.toJson().toString() + "'"); + envs.add("DEBUG_MODE=" + openviduConfig.isOpenViduRecordingDebug()); + envs.add("RESOLUTION=" + properties.resolution()); + envs.add("ONLY_VIDEO=" + !properties.hasAudio()); + envs.add("FRAMERATE=30"); + envs.add("VIDEO_ID=" + recording.getId()); + envs.add("VIDEO_NAME=" + properties.name()); + envs.add("VIDEO_FORMAT=mp4"); + envs.add("RECORDING_JSON='" + recording.toJson().toString() + "'"); - String containerId = this.sessionsContainers.get(session.getSessionId()); - try { - String recordExecCommand = ""; - for(int i = 0; i < envs.size(); i++) { - if (i > 0) { - recordExecCommand += "&& "; - } - recordExecCommand += "export " + envs.get(i) + " "; - } - recordExecCommand += "&& ./composed_quick_start.sh --start-recording > /var/log/ffmpeg.log 2>&1 &"; - dockerManager.runCommandInContainer(containerId, recordExecCommand, 0); - } catch (Exception e) { - this.cleanRecordingMaps(recording); - throw this.failStartRecording(session, recording, - "Couldn't initialize recording container. Error: " + e.getMessage()); - } + String containerId = this.sessionsContainers.get(session.getSessionId()); + try { + String recordExecCommand = ""; + for (int i = 0; i < envs.size(); i++) { + if (i > 0) { + recordExecCommand += "&& "; + } + recordExecCommand += "export " + envs.get(i) + " "; + } + recordExecCommand += "&& ./composed_quick_start.sh --start-recording > /var/log/ffmpeg.log 2>&1 &"; + dockerManager.runCommandInContainerAsync(recording.getRecordingProperties().mediaNode(), containerId, + recordExecCommand); + } catch (Exception e) { + this.cleanRecordingMaps(recording); + throw this.failStartRecording(session, recording, + "Couldn't initialize recording container. Error: " + e.getMessage()); + } - this.sessionsContainers.put(session.getSessionId(), containerId); + this.sessionsContainers.put(session.getSessionId(), containerId); - try { - this.waitForVideoFileNotEmpty(recording); - } catch (OpenViduException e) { - this.cleanRecordingMaps(recording); - throw this.failStartRecording(session, recording, - "Couldn't initialize recording container. Error: " + e.getMessage()); - } + try { + this.waitForVideoFileNotEmpty(recording); + } catch (Exception e) { + this.cleanRecordingMaps(recording); + throw this.failStartRecording(session, recording, + "Couldn't initialize recording container. Error: " + e.getMessage()); + } - return recording; - } + if (this.openviduConfig.isRecordingComposedExternal()) { + this.generateRecordingMetadataFile(recording); + } - @Override - protected Recording stopRecordingWithVideo(Session session, Recording recording, EndReason reason) { - log.info("Stopping COMPOSED_QUICK_START ({}) recording {} of session {}. Reason: {}", - recording.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId(), - RecordingManager.finalReason(reason)); - log.info("Container for session {} still being ready for new recordings", session.getSessionId()); + return recording; + } - String containerId = this.sessionsContainers.get(recording.getSessionId()); + @Override + protected Recording stopRecordingWithVideo(Session session, Recording recording, EndReason reason) { + log.info("Stopping COMPOSED_QUICK_START ({}) recording {} of session {}. Reason: {}", + recording.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId(), + RecordingManager.finalReason(reason)); + log.info("Container for session {} still ready for new recordings", recording.getSessionId()); - if (session == null) { - log.warn( - "Existing recording {} does not have an active session associated. This usually means a custom recording" - + " layout did not join a recorded participant or the recording has been automatically" - + " stopped after last user left and timeout passed", - recording.getId()); - } + String containerId = this.sessionsContainers.get(recording.getSessionId()); - try { - dockerManager.runCommandInContainer(containerId, "./composed_quick_start.sh --stop-recording", 10); - } catch (InterruptedException e1) { - cleanRecordingMaps(recording); - log.error("Error stopping recording for session id: {}", session.getSessionId()); - e1.printStackTrace(); - } + if (session == null) { + log.warn( + "Existing recording {} does not have an active session associated. This usually means a custom recording" + + " layout did not join a recorded participant or the recording has been automatically" + + " stopped after last user left and timeout passed", + recording.getId()); + } - recording = updateRecordingAttributes(recording); + try { + dockerManager.runCommandInContainerSync(recording.getRecordingProperties().mediaNode(), containerId, + "./composed_quick_start.sh --stop-recording", 10); + } catch (IOException e1) { + log.error("Error stopping COMPOSED_QUICK_START recording {}: {}", recording.getId(), e1.getMessage()); + failRecordingCompletion(recording, containerId, true, + new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE, e1.getMessage())); + } - final String folderPath = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/"; - final String metadataFilePath = folderPath + RecordingManager.RECORDING_ENTITY_FILE + recording.getId(); - this.sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(), - metadataFilePath); - cleanRecordingMaps(recording); + if (this.openviduConfig.isRecordingComposedExternal()) { + try { + waitForComposedQuickStartFiles(recording); + } catch (Exception e) { + failRecordingCompletion(recording, containerId, false, + new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE, e.getMessage())); + } + } - final long timestamp = System.currentTimeMillis(); - this.cdr.recordRecordingStatusChanged(recording, reason, timestamp, recording.getStatus()); + if (session != null && reason != null) { + this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason); + } - if (session != null && reason != null) { - this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason); - } + downloadComposedRecording(session, recording, reason); - // Decrement active recordings - // ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet(); + return recording; + } - return recording; - } + public void runComposedQuickStartContainer(Session session) { + // Start recording container if output mode=COMPOSED_QUICK_START + Session recorderSession = session; + io.openvidu.java.client.Recording.OutputMode defaultOutputMode = recorderSession.getSessionProperties() + .defaultOutputMode(); + if (io.openvidu.java.client.Recording.OutputMode.COMPOSED_QUICK_START.equals(defaultOutputMode) + && sessionsContainers.get(recorderSession.getSessionId()) == null) { + // Retry to run if container is launched for the same session quickly after + // close it + int secondsToRetry = 10; + int secondsBetweenRetries = 1; + int seconds = 0; + boolean launched = false; + while (!launched && seconds < secondsToRetry) { + try { + log.info("Launching COMPOSED_QUICK_START recording container for session: {}", + recorderSession.getSessionId()); + runContainer(recorderSession, new RecordingProperties.Builder().name("") + .outputMode(recorderSession.getSessionProperties().defaultOutputMode()) + .recordingLayout(recorderSession.getSessionProperties().defaultRecordingLayout()) + .customLayout(recorderSession.getSessionProperties().defaultCustomLayout()) + .resolution( + /* recorderSession.getSessionProperties().defaultRecordingResolution() */"1920x1080") + .mediaNode(recorderSession.getMediaNodeId()).build()); + log.info("COMPOSED_QUICK_START recording container launched for session: {}", + recorderSession.getSessionId()); + launched = true; + } catch (Exception e) { + log.warn( + "Failed to launch COMPOSED_QUICK_START recording container for session {}. Trying again in {} seconds", + recorderSession.getSessionId(), secondsBetweenRetries); + try { + Thread.sleep(secondsBetweenRetries * 1000); + } catch (InterruptedException e2) { + } + seconds++; + } finally { + if (seconds == secondsToRetry && !launched) { + log.error("Error launchaing COMPOSED_QUICK_ªSTART recording container for session {}", + recorderSession.getSessionId()); + } + } + } + } + } - public void runComposedQuickStartContainer(Session session) { - // Start recording container if output mode=COMPOSED_QUICK_START - Session recorderSession = session; - io.openvidu.java.client.Recording.OutputMode defaultOutputMode = recorderSession.getSessionProperties().defaultOutputMode(); - if (io.openvidu.java.client.Recording.OutputMode.COMPOSED_QUICK_START.equals(defaultOutputMode) - && sessionsContainers.get(recorderSession.getSessionId()) == null) { - // Retry to run if container is launched for the same session quickly after close it - int secondsToRetry = 10; - int secondsBetweenRetries = 1; - int seconds = 0; - boolean launched = false; - while (!launched && seconds < secondsToRetry) { - try { - log.info("Launching COMPOSED_QUICK_START recording container for session: {}", recorderSession.getSessionId()); - runContainer(recorderSession, new RecordingProperties.Builder().name("") - .outputMode(recorderSession.getSessionProperties().defaultOutputMode()) - .recordingLayout(recorderSession.getSessionProperties().defaultRecordingLayout()) - .customLayout(recorderSession.getSessionProperties().defaultCustomLayout()).build()); - log.info("COMPOSED_QUICK_START recording container launched for session: {}", recorderSession.getSessionId()); - launched = true; - } catch (Exception e) { - log.warn("Failed to launch COMPOSED_QUICK_START recording container for session {}. Trying again in {} seconds", recorderSession.getSessionId(), secondsBetweenRetries); - try { - Thread.sleep(secondsBetweenRetries * 1000); - } catch (InterruptedException e2) {} - seconds++; - } finally { - if (seconds == secondsToRetry && !launched) { - log.error("Error launchaing COMPOSED_QUICK_ªSTART recording container for session {}", recorderSession.getSessionId()); - } - } - } - } - } + private String runContainer(Session session, RecordingProperties properties) throws Exception { + log.info("Starting COMPOSED_QUICK_START container for session id: {}", session.getSessionId()); - private String runContainer(Session session, RecordingProperties properties) throws Exception { - log.info("Starting COMPOSED_QUICK_START container for session id: {}", session.getSessionId()); + Recording recording = new Recording(session.getSessionId(), null, properties); + String layoutUrl = this.getLayoutUrl(recording); - Recording recording = new Recording(session.getSessionId(), null, properties); - String layoutUrl = this.getLayoutUrl(recording); + List envs = new ArrayList<>(); + envs.add("DEBUG_MODE=" + openviduConfig.isOpenViduRecordingDebug()); + envs.add("RECORDING_TYPE=COMPOSED_QUICK_START"); + envs.add("RESOLUTION=" + properties.resolution()); + envs.add("URL=" + layoutUrl); - List envs = new ArrayList<>(); - envs.add("DEBUG_MODE=" + openviduConfig.isOpenViduRecordingDebug()); - envs.add("RECORDING_TYPE=COMPOSED_QUICK_START"); - envs.add("RESOLUTION=" + properties.resolution()); - envs.add("URL=" + layoutUrl); + log.info("Recorder connecting to url {}", layoutUrl); - log.info("Recorder connecting to url {}", layoutUrl); + String containerId = null; + try { + final String container = RecordingManager.IMAGE_NAME + ":" + openviduConfig.getOpenViduRecordingVersion(); + final String containerName = "recording_" + session.getSessionId(); + Volume volume1 = new Volume("/recordings"); + List volumes = new ArrayList<>(); + volumes.add(volume1); + Bind bind1 = new Bind(openviduConfig.getOpenViduRecordingPath(), volume1); + List binds = new ArrayList<>(); + binds.add(bind1); + containerId = dockerManager.runContainer(properties.mediaNode(), container, containerName, null, volumes, + binds, "host", envs, null, properties.shmSize(), false, null); + containers.put(containerId, containerName); + this.sessionsContainers.put(session.getSessionId(), containerId); + } catch (Exception e) { + if (containerId != null) { + dockerManager.removeContainer(properties.mediaNode(), containerId, true); + containers.remove(containerId); + sessionsContainers.remove(session.getSessionId()); + } + log.error("Error while launching container for COMPOSED_QUICK_START: ({})", e.getMessage()); + throw e; + } + return containerId; + } - String containerId = null; - try { - final String container = RecordingManager.IMAGE_NAME + ":" + RecordingManager.IMAGE_TAG; - final String containerName = "recording_" + session.getSessionId(); - Volume volume1 = new Volume("/recordings"); - List volumes = new ArrayList<>(); - volumes.add(volume1); - Bind bind1 = new Bind(openviduConfig.getOpenViduRecordingPath(), volume1); - List binds = new ArrayList<>(); - binds.add(bind1); - containerId = dockerManager.runContainer(container, containerName, null, volumes, binds, "host", envs, null, - properties.shmSize(), false, null); - containers.put(containerId, containerName); - this.sessionsContainers.put(session.getSessionId(), containerId); - } catch (Exception e) { - if (containerId != null) { - dockerManager.removeDockerContainer(containerId, true); - containers.remove(containerId); - sessionsContainers.remove(session.getSessionId()); - } - log.error("Error while launchig container for COMPOSED_QUICK_START: ({})", e.getMessage()); - throw e; - } - return containerId; - } + private void waitForComposedQuickStartFiles(Recording recording) throws Exception { + + final int SECONDS_MAX_WAIT = fileManager.maxSecondsWaitForFile(); + final String PATH = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/"; + + // Waiting for the files generated at the end of the stopping process: the + // ffprobe info and the thumbnail + final String INFO_FILE = PATH + recording.getId() + RecordingService.COMPOSED_INFO_FILE_EXTENSION; + final String THUMBNAIL_FILE = PATH + recording.getId() + RecordingService.COMPOSED_THUMBNAIL_EXTENSION; + + Set filesToWaitFor = Stream.of(INFO_FILE, THUMBNAIL_FILE).collect(Collectors.toSet()); + + Collection waitForFileThreads = new HashSet<>(); + CountDownLatch latch = new CountDownLatch(filesToWaitFor.size()); + + for (final String file : filesToWaitFor) { + Thread downloadThread = new Thread() { + @Override + public void run() { + try { + fileManager.waitForFileToExistAndNotEmpty(recording.getRecordingProperties().mediaNode(), file); + } catch (Exception e) { + log.error(e.getMessage()); + recording.setStatus(io.openvidu.java.client.Recording.Status.failed); + } finally { + latch.countDown(); + } + } + }; + waitForFileThreads.add(downloadThread); + } + waitForFileThreads.forEach(t -> t.start()); + + try { + if (!latch.await(SECONDS_MAX_WAIT, TimeUnit.SECONDS)) { + recording.setStatus(io.openvidu.java.client.Recording.Status.failed); + String msg = "The wait for files of COMPOSED_QUICK_START recording " + recording.getId() + + " didn't complete in " + fileManager.maxSecondsWaitForFile() + " seconds"; + log.error(msg); + throw new Exception(msg); + } else { + if (io.openvidu.java.client.Recording.Status.failed.equals(recording.getStatus())) { + String msg = "Error waiting for some COMPOSED_QUICK_START recording file in recording " + + recording.getId(); + log.error(msg); + throw new Exception(msg); + } else { + log.info("All files of COMPOSED_QUICK_START recording {} are available to download", + recording.getId()); + } + } + } catch (InterruptedException e) { + recording.setStatus(io.openvidu.java.client.Recording.Status.failed); + String msg = "Error waiting for COMPOSED_QUICK_START recording files of recording " + recording.getId() + + ": " + e.getMessage(); + log.error(msg); + throw new Exception(msg); + } + } } diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedRecordingService.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedRecordingService.java index a645e6d2..27922ea4 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedRecordingService.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedRecordingService.java @@ -52,12 +52,15 @@ import io.openvidu.server.core.Participant; import io.openvidu.server.core.Session; import io.openvidu.server.kurento.core.KurentoParticipant; import io.openvidu.server.kurento.core.KurentoSession; +import io.openvidu.server.kurento.kms.KmsManager; import io.openvidu.server.recording.CompositeWrapper; import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.RecordingDownloader; import io.openvidu.server.recording.RecordingInfoUtils; +import io.openvidu.server.recording.RecordingUploader; +import io.openvidu.server.rest.RequestMappings; +import io.openvidu.server.utils.CustomFileManager; import io.openvidu.server.utils.DockerManager; -import io.openvidu.server.utils.QuarantineKiller; public class ComposedRecordingService extends RecordingService { @@ -70,18 +73,15 @@ public class ComposedRecordingService extends RecordingService { protected DockerManager dockerManager; public ComposedRecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader, - OpenviduConfig openviduConfig, CallDetailRecord cdr, QuarantineKiller quarantineKiller) { - super(recordingManager, recordingDownloader, openviduConfig, cdr, quarantineKiller); - this.dockerManager = new DockerManager(); + RecordingUploader recordingUploader, KmsManager kmsManager, CustomFileManager fileManager, + OpenviduConfig openviduConfig, CallDetailRecord cdr, DockerManager dockerManager) { + super(recordingManager, recordingDownloader, recordingUploader, kmsManager, fileManager, openviduConfig, cdr); + this.dockerManager = dockerManager; } @Override - public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException { - - PropertiesRecordingId updatePropertiesAndRecordingId = this.setFinalRecordingNameAndGetFreeRecordingId(session, - properties); - properties = updatePropertiesAndRecordingId.properties; - String recordingId = updatePropertiesAndRecordingId.recordingId; + public Recording startRecording(Session session, String recordingId, RecordingProperties properties) + throws OpenViduException { // Instantiate and store recording object Recording recording = new Recording(session.getSessionId(), recordingId, properties); @@ -95,23 +95,19 @@ public class ComposedRecordingService extends RecordingService { recording = this.startRecordingAudioOnly(session, recording, properties); } - // Increment active recordings - // ((KurentoSession) session).getKms().getActiveRecordings().incrementAndGet(); - return recording; } @Override public Recording stopRecording(Session session, Recording recording, EndReason reason) { - recording = this.sealRecordingMetadataFileAsStopped(recording); if (recording.hasVideo()) { return this.stopRecordingWithVideo(session, recording, reason); } else { - return this.stopRecordingAudioOnly(session, recording, reason, 0); + return this.stopRecordingAudioOnly(session, recording, reason, null); } } - public Recording stopRecording(Session session, Recording recording, EndReason reason, long kmsDisconnectionTime) { + public Recording stopRecording(Session session, Recording recording, EndReason reason, Long kmsDisconnectionTime) { if (recording.hasVideo()) { return this.stopRecordingWithVideo(session, recording, reason); } else { @@ -146,7 +142,7 @@ public class ComposedRecordingService extends RecordingService { throws OpenViduException { log.info("Starting composed ({}) recording {} of session {}", - properties.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId()); + properties.hasAudio() ? "video + audio" : "video-only", recording.getId(), recording.getSessionId()); List envs = new ArrayList<>(); @@ -167,7 +163,7 @@ public class ComposedRecordingService extends RecordingService { String containerId; try { - final String container = RecordingManager.IMAGE_NAME + ":" + RecordingManager.IMAGE_TAG; + final String container = RecordingManager.IMAGE_NAME + ":" + openviduConfig.getOpenViduRecordingVersion(); final String containerName = "recording_" + recording.getId(); Volume volume1 = new Volume("/recordings"); List volumes = new ArrayList<>(); @@ -175,8 +171,8 @@ public class ComposedRecordingService extends RecordingService { Bind bind1 = new Bind(openviduConfig.getOpenViduRecordingPath(), volume1); List binds = new ArrayList<>(); binds.add(bind1); - containerId = dockerManager.runContainer(container, containerName, null, volumes, binds, "host", envs, null, - properties.shmSize(), false, null); + containerId = dockerManager.runContainer(properties.mediaNode(), container, containerName, null, volumes, + binds, "host", envs, null, properties.shmSize(), false, null); containers.put(containerId, containerName); } catch (Exception e) { this.cleanRecordingMaps(recording); @@ -188,12 +184,16 @@ public class ComposedRecordingService extends RecordingService { try { this.waitForVideoFileNotEmpty(recording); - } catch (OpenViduException e) { + } catch (Exception e) { this.cleanRecordingMaps(recording); throw this.failStartRecording(session, recording, "Couldn't initialize recording container. Error: " + e.getMessage()); } + if (this.openviduConfig.isRecordingComposedExternal()) { + this.generateRecordingMetadataFile(recording); + } + return recording; } @@ -222,16 +222,13 @@ public class ComposedRecordingService extends RecordingService { this.generateRecordingMetadataFile(recording); - // Increment active recordings - ((KurentoSession) session).getKms().getActiveRecordings().incrementAndGet(); - return recording; } protected Recording stopRecordingWithVideo(Session session, Recording recording, EndReason reason) { log.info("Stopping composed ({}) recording {} of session {}. Reason: {}", - recording.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId(), + recording.hasAudio() ? "video + audio" : "video-only", recording.getId(), recording.getSessionId(), RecordingManager.finalReason(reason)); String containerId = this.sessionsContainers.remove(recording.getSessionId()); @@ -271,7 +268,8 @@ public class ComposedRecordingService extends RecordingService { } else { log.warn("Removing container {} for closed session {}...", containerIdAux, session.getSessionId()); - dockerManager.removeDockerContainer(containerIdAux, true); + dockerManager.removeContainer(recordingAux.getRecordingProperties().mediaNode(), + containerIdAux, true); containers.remove(containerId); containerClosed = true; log.warn("Container {} for closed session {} succesfully stopped and removed", @@ -279,46 +277,39 @@ public class ComposedRecordingService extends RecordingService { log.warn("Deleting unusable files for recording {}", recordingId); if (HttpStatus.NO_CONTENT .equals(this.recordingManager.deleteRecordingFromHost(recordingId, true))) { - log.warn("Files properly deleted"); + log.warn("Files properly deleted for recording {}", recordingId); + } else { + log.warn("No files found for recording {}", recordingId); } } } cleanRecordingMaps(recordingAux); + + // Decrement active recordings + kmsManager.decrementActiveRecordings(recordingAux.getRecordingProperties(), recordingId, session); + if (i == timeout) { log.error("Container did not launched in {} seconds", timeout / 2); return; } - // Decrement active recordings - // ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet(); }).start(); } } else { stopAndRemoveRecordingContainer(recording, containerId, 30); - recording = updateRecordingAttributes(recording); - - final String folderPath = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/"; - final String metadataFilePath = folderPath + RecordingManager.RECORDING_ENTITY_FILE + recording.getId(); - this.sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(), - metadataFilePath); - cleanRecordingMaps(recording); - - final long timestamp = System.currentTimeMillis(); - this.cdr.recordRecordingStatusChanged(recording, reason, timestamp, recording.getStatus()); if (session != null && reason != null) { this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason); } - // Decrement active recordings - // ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet(); + downloadComposedRecording(session, recording, reason); } return recording; } private Recording stopRecordingAudioOnly(Session session, Recording recording, EndReason reason, - long kmsDisconnectionTime) { + Long kmsDisconnectionTime) { log.info("Stopping composed (audio-only) recording {} of session {}. Reason: {}", recording.getId(), recording.getSessionId(), reason); @@ -357,6 +348,7 @@ public class ComposedRecordingService extends RecordingService { finalRecordingArray[0] = recording; try { this.recordingDownloader.downloadRecording(finalRecordingArray[0], null, () -> { + String filesPath = this.openviduConfig.getOpenViduRecordingPath() + finalRecordingArray[0].getId() + "/"; File videoFile = new File(filesPath + finalRecordingArray[0].getName() + ".webm"); @@ -365,17 +357,16 @@ public class ComposedRecordingService extends RecordingService { this.updateFilePermissions(filesPath); finalRecordingArray[0] = this.sealRecordingMetadataFileAsReady(finalRecordingArray[0], finalSize, finalDuration, - filesPath + RecordingManager.RECORDING_ENTITY_FILE + finalRecordingArray[0].getId()); + filesPath + RecordingService.RECORDING_ENTITY_FILE + finalRecordingArray[0].getId()); - final long timestamp = System.currentTimeMillis(); - cdr.recordRecordingStatusChanged(finalRecordingArray[0], reason, timestamp, - finalRecordingArray[0].getStatus()); + // Decrement active recordings once it is downloaded. This method will also drop + // the Media Node if no more sessions or recordings and status is + // waiting-idle-to-terminate + kmsManager.decrementActiveRecordings(finalRecordingArray[0].getRecordingProperties(), + finalRecordingArray[0].getId(), session); - // Decrement active recordings once it is downloaded - ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet(); - - // Now we can drop Media Node if waiting-idle-to-terminate - this.quarantineKiller.dropMediaNode(session.getMediaNodeId()); + // Upload if necessary + this.uploadRecording(finalRecordingArray[0], reason); }); } catch (IOException e) { @@ -390,32 +381,35 @@ public class ComposedRecordingService extends RecordingService { return finalRecordingArray[0]; } - protected void stopAndRemoveRecordingContainer(Recording recording, String containerId, int secondsOfWait) { + private void stopAndRemoveRecordingContainer(Recording recording, String containerId, int secondsOfWait) { // Gracefully stop ffmpeg process try { - dockerManager.runCommandInContainer(containerId, "echo 'q' > stop", 0); - } catch (InterruptedException e1) { + dockerManager.runCommandInContainerAsync(recording.getRecordingProperties().mediaNode(), containerId, + "echo 'q' > stop"); + } catch (IOException e1) { e1.printStackTrace(); } // Wait for the container to be gracefully self-stopped final int timeOfWait = 30; try { - dockerManager.waitForContainerStopped(containerId, timeOfWait); + dockerManager.waitForContainerStopped(recording.getRecordingProperties().mediaNode(), containerId, + timeOfWait); } catch (Exception e) { - failRecordingCompletion(recording, containerId, new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE, - "The recording completion process couldn't finish in " + timeOfWait + " seconds")); + failRecordingCompletion(recording, containerId, true, + new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE, + "The recording completion process couldn't finish in " + timeOfWait + " seconds")); } // Remove container - dockerManager.removeDockerContainer(containerId, false); + dockerManager.removeContainer(recording.getRecordingProperties().mediaNode(), containerId, false); containers.remove(containerId); } - protected Recording updateRecordingAttributes(Recording recording) { + protected void updateRecordingAttributes(Recording recording) { try { RecordingInfoUtils infoUtils = new RecordingInfoUtils(this.openviduConfig.getOpenViduRecordingPath() - + recording.getId() + "/" + recording.getId() + ".info"); + + recording.getId() + "/" + recording.getId() + RecordingService.COMPOSED_INFO_FILE_EXTENSION); if (!infoUtils.hasVideo()) { log.error("COMPOSED recording {} with hasVideo=true has not video track", recording.getId()); @@ -429,7 +423,6 @@ public class ComposedRecordingService extends RecordingService { recording.setHasVideo(infoUtils.hasVideo()); } infoUtils.deleteFilePath(); - return recording; } catch (IOException e) { recording.setStatus(io.openvidu.java.client.Recording.Status.failed); throw new OpenViduException(Code.RECORDING_REPORT_ERROR_CODE, @@ -437,34 +430,22 @@ public class ComposedRecordingService extends RecordingService { } } - protected void waitForVideoFileNotEmpty(Recording recording) throws OpenViduException { - boolean isPresent = false; - int i = 1; - int timeout = 150; // Wait for 150*150 = 22500 = 22.5 seconds - while (!isPresent && timeout <= 150) { - try { - Thread.sleep(150); - timeout++; - File f = new File(this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/" - + recording.getName() + ".mp4"); - isPresent = ((f.isFile()) && (f.length() > 0)); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - if (i == timeout) { - log.error("Recorder container failed generating video file (is empty) for session {}", - recording.getSessionId()); - throw new OpenViduException(Code.RECORDING_START_ERROR_CODE, - "Recorder container failed generating video file (is empty)"); - } + protected void waitForVideoFileNotEmpty(Recording recording) throws Exception { + final String VIDEO_FILE = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/" + + recording.getName() + RecordingService.COMPOSED_RECORDING_EXTENSION; + this.fileManager.waitForFileToExistAndNotEmpty(recording.getRecordingProperties().mediaNode(), VIDEO_FILE); } - protected void failRecordingCompletion(Recording recording, String containerId, OpenViduException e) - throws OpenViduException { + protected void failRecordingCompletion(Recording recording, String containerId, boolean removeContainer, + OpenViduException e) throws OpenViduException { recording.setStatus(io.openvidu.java.client.Recording.Status.failed); - dockerManager.removeDockerContainer(containerId, true); - containers.remove(containerId); + if (removeContainer) { + dockerManager.removeContainer(recording.getRecordingProperties().mediaNode(), containerId, true); + containers.remove(containerId); + } + sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(), + getMetadataFilePath(recording)); + cleanRecordingMaps(recording); throw e; } @@ -519,6 +500,8 @@ public class ComposedRecordingService extends RecordingService { } String layout, finalUrl; + final String basicauth = openviduConfig.isOpenviduRecordingComposedBasicauth() ? ("OPENVIDUAPP:" + secret + "@") + : ""; if (RecordingLayout.CUSTOM.equals(recording.getRecordingLayout())) { layout = recording.getCustomLayout(); if (!layout.isEmpty()) { @@ -526,8 +509,9 @@ public class ComposedRecordingService extends RecordingService { layout = layout.endsWith("/") ? layout.substring(0, layout.length() - 1) : layout; } layout += "/index.html"; - finalUrl = (startsWithHttp ? "http" : "https") + "://OPENVIDUAPP:" + secret + "@" + recordingUrl - + "/layouts/custom" + layout + "?sessionId=" + recording.getSessionId() + "&secret=" + secret; + finalUrl = (startsWithHttp ? "http" : "https") + "://" + basicauth + recordingUrl + + RequestMappings.CUSTOM_LAYOUTS + layout + "?sessionId=" + recording.getSessionId() + "&secret=" + + secret; } else { layout = recording.getRecordingLayout().name().toLowerCase().replaceAll("_", "-"); int port = startsWithHttp ? 80 : 443; @@ -537,8 +521,8 @@ public class ComposedRecordingService extends RecordingService { log.error(e.getMessage()); } String defaultPathForDefaultLayout = recordingComposedUrlDefined ? "" - : ("/" + openviduConfig.getOpenViduFrontendDefaultPath()); - finalUrl = (startsWithHttp ? "http" : "https") + "://OPENVIDUAPP:" + secret + "@" + recordingUrl + : (openviduConfig.getOpenViduFrontendDefaultPath()); + finalUrl = (startsWithHttp ? "http" : "https") + "://" + basicauth + recordingUrl + defaultPathForDefaultLayout + "/#/layout-" + layout + "/" + recording.getSessionId() + "/" + secret + "/" + port + "/" + !recording.hasAudio(); } @@ -595,4 +579,27 @@ public class ComposedRecordingService extends RecordingService { return finalUrl; } + protected void downloadComposedRecording(final Session session, final Recording recording, final EndReason reason) { + try { + this.recordingDownloader.downloadRecording(recording, null, () -> { + + updateRecordingAttributes(recording); + + this.sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(), + getMetadataFilePath(recording)); + cleanRecordingMaps(recording); + + // Decrement active recordings once it is downloaded. This method will also drop + // the Media Node if no more sessions or recordings and status is + // waiting-idle-to-terminate + kmsManager.decrementActiveRecordings(recording.getRecordingProperties(), recording.getId(), session); + + // Upload if necessary + this.uploadRecording(recording, reason); + }); + } catch (IOException e) { + log.error("Error while downloading recording {}: {}", recording.getName(), e.getMessage()); + } + } + } \ No newline at end of file diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java index 41b901f7..20faad49 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java @@ -67,15 +67,17 @@ import io.openvidu.server.core.Participant; import io.openvidu.server.core.Session; import io.openvidu.server.core.SessionEventsHandler; import io.openvidu.server.core.SessionManager; -import io.openvidu.server.kurento.core.KurentoSession; import io.openvidu.server.kurento.kms.Kms; import io.openvidu.server.kurento.kms.KmsManager; import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.RecordingDownloader; +import io.openvidu.server.recording.RecordingUploader; +import io.openvidu.server.recording.service.RecordingService.PropertiesRecordingId; import io.openvidu.server.utils.CustomFileManager; import io.openvidu.server.utils.DockerManager; import io.openvidu.server.utils.JsonUtils; -import io.openvidu.server.utils.QuarantineKiller; +import io.openvidu.server.utils.LocalCustomFileManager; +import io.openvidu.server.utils.LocalDockerManager; import io.openvidu.server.utils.RecordingUtils; public class RecordingManager { @@ -86,6 +88,7 @@ public class RecordingManager { private ComposedQuickStartRecordingService composedQuickStartRecordingService; private SingleStreamRecordingService singleStreamRecordingService; private DockerManager dockerManager; + private CustomFileManager fileManager; @Autowired protected SessionEventsHandler sessionHandler; @@ -93,18 +96,21 @@ public class RecordingManager { @Autowired private SessionManager sessionManager; + @Autowired + protected RecordingManagerUtils recordingManagerUtils; + @Autowired private RecordingDownloader recordingDownloader; + @Autowired + private RecordingUploader recordingUploader; + @Autowired protected OpenviduConfig openviduConfig; @Autowired private KmsManager kmsManager; - @Autowired - protected QuarantineKiller quarantineKiller; - @Autowired private CallDetailRecord cdr; @@ -119,14 +125,17 @@ public class RecordingManager { private ScheduledThreadPoolExecutor automaticRecordingStopExecutor = new ScheduledThreadPoolExecutor( Runtime.getRuntime().availableProcessors()); - static final String RECORDING_ENTITY_FILE = ".recording."; public static final String IMAGE_NAME = "openvidu/openvidu-recording"; - static String IMAGE_TAG; private static final List LAST_PARTICIPANT_LEFT_REASONS = Arrays .asList(new EndReason[] { EndReason.disconnect, EndReason.forceDisconnectByUser, EndReason.forceDisconnectByServer, EndReason.networkDisconnect }); + public RecordingManager(DockerManager dockerManager, CustomFileManager fileManager) { + this.dockerManager = dockerManager; + this.fileManager = fileManager; + } + @PostConstruct public void init() { if (this.openviduConfig.isRecordingModuleEnabled()) { @@ -156,63 +165,33 @@ public class RecordingManager { public void initializeRecordingManager() throws OpenViduException { - RecordingManager.IMAGE_TAG = openviduConfig.getOpenViduRecordingVersion(); + this.dockerManager.init(); - this.dockerManager = new DockerManager(); - this.composedRecordingService = new ComposedRecordingService(this, recordingDownloader, openviduConfig, cdr, - quarantineKiller); + this.composedRecordingService = new ComposedRecordingService(this, recordingDownloader, recordingUploader, + kmsManager, fileManager, openviduConfig, cdr, this.dockerManager); this.composedQuickStartRecordingService = new ComposedQuickStartRecordingService(this, recordingDownloader, - openviduConfig, cdr, quarantineKiller); - this.singleStreamRecordingService = new SingleStreamRecordingService(this, recordingDownloader, openviduConfig, - cdr, quarantineKiller); - - log.info("Recording module required: Downloading openvidu/openvidu-recording:" - + openviduConfig.getOpenViduRecordingVersion() + " Docker image (350MB aprox)"); + recordingUploader, kmsManager, fileManager, openviduConfig, cdr, this.dockerManager); + this.singleStreamRecordingService = new SingleStreamRecordingService(this, recordingDownloader, + recordingUploader, kmsManager, fileManager, openviduConfig, cdr); this.checkRecordingRequirements(this.openviduConfig.getOpenViduRecordingPath(), this.openviduConfig.getOpenviduRecordingCustomLayout()); - if (dockerManager.dockerImageExistsLocally(IMAGE_NAME + ":" + IMAGE_TAG)) { - log.info("Docker image already exists locally"); - } else { - Thread t = new Thread(() -> { - boolean keep = true; - log.info("Downloading "); - while (keep) { - System.out.print("."); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - keep = false; - log.info("\nDownload complete"); - } - } - }); - t.start(); - try { - dockerManager.downloadDockerImage(IMAGE_NAME + ":" + IMAGE_TAG, 600); - } catch (Exception e) { - log.error("Error downloading docker image {}:{}", IMAGE_NAME, IMAGE_TAG); - } - t.interrupt(); - try { - t.join(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - log.info("Docker image available"); + LocalDockerManager dockMng = new LocalDockerManager(true); + + if (!openviduConfig.isRecordingComposedExternal()) { + downloadRecordingImageToLocal(dockMng); } // Clean any stranded openvidu/openvidu-recording container on startup - dockerManager.cleanStrandedContainers(RecordingManager.IMAGE_NAME); + dockMng.cleanStrandedContainers(RecordingManager.IMAGE_NAME); } public void checkRecordingRequirements(String openviduRecordingPath, String openviduRecordingCustomLayout) throws OpenViduException { - if (dockerManager == null) { - this.dockerManager = new DockerManager(); - } + LocalDockerManager dockerManager = null; try { + dockerManager = new LocalDockerManager(true); dockerManager.checkDockerEnabled(); } catch (OpenViduException e) { String message = e.getMessage(); @@ -231,10 +210,49 @@ public class RecordingManager { } log.error(message); throw e; + } finally { + dockerManager.close(); } this.checkRecordingPaths(openviduRecordingPath, openviduRecordingCustomLayout); } + private void downloadRecordingImageToLocal(LocalDockerManager dockMng) { + log.info("Recording module required: Downloading openvidu/openvidu-recording:" + + openviduConfig.getOpenViduRecordingVersion() + " Docker image (350MB aprox)"); + + if (dockMng.dockerImageExistsLocally(IMAGE_NAME + ":" + openviduConfig.getOpenViduRecordingVersion())) { + log.info("Docker image already exists locally"); + } else { + Thread t = new Thread(() -> { + boolean keep = true; + log.info("Downloading "); + while (keep) { + System.out.print("."); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + keep = false; + log.info("\nDownload complete"); + } + } + }); + t.start(); + try { + dockMng.downloadDockerImage(IMAGE_NAME + ":" + openviduConfig.getOpenViduRecordingVersion(), 600); + } catch (Exception e) { + log.error("Error downloading docker image {}:{}", IMAGE_NAME, + openviduConfig.getOpenViduRecordingVersion()); + } + t.interrupt(); + try { + t.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + log.info("Docker image available"); + } + } + public void startComposedQuickStartContainer(Session session) { this.composedQuickStartRecordingService.runComposedQuickStartContainer(session); } @@ -244,61 +262,80 @@ public class RecordingManager { } public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException { + + String recordingId = null; + try { - if (session.recordingLock.tryLock(15, TimeUnit.SECONDS)) { - try { - if (sessionIsBeingRecorded(session.getSessionId())) { - throw new OpenViduException(Code.RECORDING_START_ERROR_CODE, - "Concurrent start of recording for session " + session.getSessionId()); - } else { - Recording recording = null; - try { + PropertiesRecordingId updatePropertiesAndRecordingId = ((RecordingService) this.composedRecordingService) + .setFinalRecordingNameAndGetFreeRecordingId(session, properties); + properties = updatePropertiesAndRecordingId.properties; + recordingId = updatePropertiesAndRecordingId.recordingId; + + // INCREMENT ACTIVE RECORDINGS OF MEDIA NODE HERE. IF MEDIA NODE IS NOT + // AVAILABLE FOR STARTING NEW RECORDINGS THIS METHOD THROWS AN EXCEPTION + kmsManager.incrementActiveRecordings(properties, recordingId, session); + + try { + if (session.recordingLock.tryLock(15, TimeUnit.SECONDS)) { + try { + if (sessionIsBeingRecorded(session.getSessionId())) { + throw new OpenViduException(Code.RECORDING_START_ERROR_CODE, + "Concurrent start of recording for session " + session.getSessionId()); + } else { + Recording recording = null; switch (properties.outputMode()) { case COMPOSED: - recording = this.composedRecordingService.startRecording(session, properties); + recording = this.composedRecordingService.startRecording(session, recordingId, + properties); break; case COMPOSED_QUICK_START: - recording = this.composedQuickStartRecordingService.startRecording(session, properties); + recording = this.composedQuickStartRecordingService.startRecording(session, recordingId, + properties); break; case INDIVIDUAL: - recording = this.singleStreamRecordingService.startRecording(session, properties); + recording = this.singleStreamRecordingService.startRecording(session, recordingId, + properties); break; } - } catch (Exception e) { - throw e; - } - this.recordingFromStartingToStarted(recording); + this.recordingFromStartingToStarted(recording); - this.cdr.recordRecordingStarted(recording); - this.cdr.recordRecordingStatusChanged(recording, null, recording.getCreatedAt(), - Status.started); + this.cdr.recordRecordingStarted(recording); + this.cdr.recordRecordingStatusChanged(recording, null, recording.getCreatedAt(), + Status.started); - if (!(OutputMode.COMPOSED.equals(properties.outputMode()) && properties.hasVideo())) { - // Directly send recording started notification for all cases except for - // COMPOSED recordings with video (will be sent on first RECORDER subscriber) - this.sessionHandler.sendRecordingStartedNotification(session, recording); + if (!(OutputMode.COMPOSED.equals(properties.outputMode()) && properties.hasVideo())) { + // Directly send recording started notification for all cases except for + // COMPOSED recordings with video (will be sent on first RECORDER subscriber) + // Both INDIVIDUAL and COMPOSED_QUICK_START should notify immediately + this.sessionHandler.sendRecordingStartedNotification(session, recording); + } + if (session.getActivePublishers() == 0) { + // Init automatic recording stop if no publishers when starting the recording + log.info( + "No publisher in session {}. Starting {} seconds countdown for stopping recording", + session.getSessionId(), + this.openviduConfig.getOpenviduRecordingAutostopTimeout()); + this.initAutomaticRecordingStopThread(session); + } + return recording; } - if (session.getActivePublishers() == 0) { - // Init automatic recording stop if there are now publishers when starting - // recording - log.info("No publisher in session {}. Starting {} seconds countdown for stopping recording", - session.getSessionId(), this.openviduConfig.getOpenviduRecordingAutostopTimeout()); - this.initAutomaticRecordingStopThread(session); - } - return recording; + } finally { + session.recordingLock.unlock(); } - } finally { - session.recordingLock.unlock(); + } else { + throw new OpenViduException(Code.RECORDING_START_ERROR_CODE, + "Timeout waiting for recording Session lock to be available for session " + + session.getSessionId()); } - } else { + } catch (InterruptedException e) { throw new OpenViduException(Code.RECORDING_START_ERROR_CODE, - "Timeout waiting for recording Session lock to be available for session " + "InterruptedException waiting for recording Session lock to be available for session " + session.getSessionId()); } - } catch (InterruptedException e) { - throw new OpenViduException(Code.RECORDING_START_ERROR_CODE, - "InterruptedException waiting for recording Session lock to be available for session " - + session.getSessionId()); + } catch (Exception e) { + // DECREMENT ACTIVE RECORDINGS OF MEDIA NODE AND TRY REMOVE MEDIA NODE HERE + kmsManager.decrementActiveRecordings(properties, recordingId, session); + throw e; } } @@ -310,6 +347,20 @@ public class RecordingManager { recording = this.sessionsRecordings.get(session.getSessionId()); } + if (recording == null) { + recording = this.sessionsRecordingsStarting.get(session.getSessionId()); + if (recording == null) { + log.error("Cannot stop recording. Session {} is not being recorded", recordingId, + session.getSessionId()); + return null; + } else { + // Recording is still starting + log.warn("Recording {} is still in \"starting\" status", recording.getId()); + } + } + + ((RecordingService) singleStreamRecordingService).sealRecordingMetadataFileAsStopped(recording); + final long timestamp = System.currentTimeMillis(); this.cdr.recordRecordingStatusChanged(recording, reason, timestamp, Status.stopped); cdr.recordRecordingStopped(recording, reason, timestamp); @@ -329,7 +380,7 @@ public class RecordingManager { return recording; } - public Recording forceStopRecording(Session session, EndReason reason, long kmsDisconnectionTime) { + public Recording forceStopRecording(Session session, EndReason reason, Long kmsDisconnectionTime) { Recording recording; recording = this.sessionsRecordings.get(session.getSessionId()); switch (recording.getOutputMode()) { @@ -361,8 +412,7 @@ public class RecordingManager { return recording; } - public void startOneIndividualStreamRecording(Session session, String recordingId, MediaProfileSpecType profile, - Participant participant) { + public void startOneIndividualStreamRecording(Session session, Participant participant) { Recording recording = this.sessionsRecordings.get(session.getSessionId()); if (recording == null) { recording = this.sessionsRecordingsStarting.get(session.getSessionId()); @@ -377,28 +427,46 @@ public class RecordingManager { log.info("Starting new RecorderEndpoint in session {} for new stream of participant {}", session.getSessionId(), participant.getParticipantPublicId()); final CountDownLatch startedCountDown = new CountDownLatch(1); - this.singleStreamRecordingService.startRecorderEndpointForPublisherEndpoint(session, recordingId, profile, + + MediaProfileSpecType profile = null; + try { + profile = this.singleStreamRecordingService.generateMediaProfile(recording.getRecordingProperties(), + participant); + } catch (OpenViduException e) { + log.error("Cannot start single stream recorder for stream {} in session {}: {}", + participant.getPublisherStreamId(), session.getSessionId(), e.getMessage()); + return; + } + + this.singleStreamRecordingService.startRecorderEndpointForPublisherEndpoint(recording.getId(), profile, participant, startedCountDown); } else if (RecordingUtils.IS_COMPOSED(recording.getOutputMode()) && !recording.hasVideo()) { // Connect this stream to existing Composite recorder log.info("Joining PublisherEndpoint to existing Composite in session {} for new stream of participant {}", session.getSessionId(), participant.getParticipantPublicId()); - this.composedRecordingService.joinPublisherEndpointToComposite(session, recordingId, participant); + this.composedRecordingService.joinPublisherEndpointToComposite(session, recording.getId(), participant); } } - public void stopOneIndividualStreamRecording(KurentoSession session, String streamId, long kmsDisconnectionTime) { + public void stopOneIndividualStreamRecording(Session session, String streamId, Long kmsDisconnectionTime) { Recording recording = this.sessionsRecordings.get(session.getSessionId()); if (recording == null) { - log.error("Cannot stop recording of existing stream {}. Session {} is not being recorded", streamId, - session.getSessionId()); + recording = this.sessionsRecordingsStarting.get(session.getSessionId()); + if (recording == null) { + log.error("Cannot stop recording of existing stream {}. Session {} is not being recorded", streamId, + session.getSessionId()); + return; + } else { + // Recording is still starting + log.warn("Recording {} is still in \"starting\" status", recording.getId()); + } } if (OutputMode.INDIVIDUAL.equals(recording.getOutputMode())) { // Stop specific RecorderEndpoint for this stream log.info("Stopping RecorderEndpoint in session {} for stream of participant {}", session.getSessionId(), streamId); final CountDownLatch stoppedCountDown = new CountDownLatch(1); - this.singleStreamRecordingService.stopRecorderEndpointOfPublisherEndpoint(session.getSessionId(), streamId, + this.singleStreamRecordingService.stopRecorderEndpointOfPublisherEndpoint(recording.getId(), streamId, stoppedCountDown, kmsDisconnectionTime); try { if (!stoppedCountDown.await(5, TimeUnit.SECONDS)) { @@ -430,68 +498,110 @@ public class RecordingManager { } public Collection getFinishedRecordings() { - return this.getAllRecordingsFromHost().stream().filter(recording -> recording.getStatus().equals(Status.ready)) - .collect(Collectors.toSet()); + return recordingManagerUtils.getAllRecordingsFromStorage().stream() + .filter(recording -> recording.getStatus().equals(Status.ready)).collect(Collectors.toSet()); } public Recording getRecording(String recordingId) { - return this.getRecordingFromHost(recordingId); + return recordingManagerUtils.getRecordingFromStorage(recordingId); } public Collection getAllRecordings() { - return this.getAllRecordingsFromHost(); + return recordingManagerUtils.getAllRecordingsFromStorage(); } public String getFreeRecordingId(String sessionId) { - Set recordingIds = this.getRecordingIdsFromHost(); - String recordingId = sessionId; - boolean isPresent = recordingIds.contains(recordingId); - int i = 1; - while (isPresent) { - recordingId = sessionId + "-" + i; - i++; - isPresent = recordingIds.contains(recordingId); - } - return recordingId; + return recordingManagerUtils.getFreeRecordingId(sessionId); } public HttpStatus deleteRecordingFromHost(String recordingId, boolean force) { - if (!force && (this.startedRecordings.containsKey(recordingId) - || this.startingRecordings.containsKey(recordingId))) { - // Cannot delete an active recording - return HttpStatus.CONFLICT; + if (this.startedRecordings.containsKey(recordingId) || this.startingRecordings.containsKey(recordingId)) { + if (!force) { + // Cannot delete an active recording + return HttpStatus.CONFLICT; + } } - Recording recording = getRecordingFromHost(recordingId); + Recording recording = recordingManagerUtils.getRecordingFromStorage(recordingId); if (recording == null) { return HttpStatus.NOT_FOUND; } if (Status.stopped.equals(recording.getStatus())) { - // Recording is being downloaded from remote host - log.warn("Cancelling ongoing download process of recording {}", recording.getId()); + // Recording is being downloaded from remote host or being uploaded + log.warn("Recording {} status is \"stopped\". Cancelling possible ongoing download process", + recording.getId()); this.recordingDownloader.cancelDownload(recording.getId()); } - File folder = new File(this.openviduConfig.getOpenViduRecordingPath()); + return recordingManagerUtils.deleteRecordingFromStorage(recordingId); + } + + public Set getAllRecordingIdsFromLocalStorage() { + File folder = new File(openviduConfig.getOpenViduRecordingPath()); + File[] files = folder.listFiles(); + + Set fileNamesNoExtension = new HashSet<>(); + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + File[] innerFiles = files[i].listFiles(); + for (int j = 0; j < innerFiles.length; j++) { + if (innerFiles[j].isFile() + && innerFiles[j].getName().startsWith(RecordingService.RECORDING_ENTITY_FILE)) { + fileNamesNoExtension + .add(innerFiles[j].getName().replaceFirst(RecordingService.RECORDING_ENTITY_FILE, "")); + break; + } + } + } + } + return fileNamesNoExtension; + } + + public HttpStatus deleteRecordingFromLocalStorage(String recordingId) { + File folder = new File(openviduConfig.getOpenViduRecordingPath()); File[] files = folder.listFiles(); for (int i = 0; i < files.length; i++) { if (files[i].isDirectory() && files[i].getName().equals(recordingId)) { // Correct folder. Delete it try { FileUtils.deleteDirectory(files[i]); + return HttpStatus.NO_CONTENT; } catch (IOException e) { log.error("Couldn't delete folder {}", files[i].getAbsolutePath()); + return HttpStatus.INTERNAL_SERVER_ERROR; } - break; } } + return HttpStatus.NOT_FOUND; + } - return HttpStatus.NO_CONTENT; + public File getRecordingEntityFileFromLocalStorage(String recordingId) { + String metadataFilePath = openviduConfig.getOpenViduRecordingPath() + recordingId + "/" + + RecordingService.RECORDING_ENTITY_FILE + recordingId; + return new File(metadataFilePath); + } + + public Set getAllRecordingsFromLocalStorage() { + File folder = new File(openviduConfig.getOpenViduRecordingPath()); + File[] files = folder.listFiles(); + Set recordingEntities = new HashSet<>(); + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + File[] innerFiles = files[i].listFiles(); + for (int j = 0; j < innerFiles.length; j++) { + Recording recording = getRecordingFromEntityFile(innerFiles[j]); + if (recording != null) { + recordingEntities.add(recording); + } + } + } + } + return recordingEntities; } public Recording getRecordingFromEntityFile(File file) { - if (file.isFile() && file.getName().startsWith(RecordingManager.RECORDING_ENTITY_FILE)) { + if (file.isFile() && file.getName().startsWith(RecordingService.RECORDING_ENTITY_FILE)) { JsonObject json; try { json = jsonUtils.fromFileToJsonObject(file.getAbsolutePath()); @@ -499,28 +609,26 @@ public class RecordingManager { log.error("Error reading recording entity file {}: {}", file.getAbsolutePath(), (e.getMessage())); return null; } - Recording recording = new Recording(json); - if (Status.ready.equals(recording.getStatus()) || Status.failed.equals(recording.getStatus())) { - recording.setUrl(getRecordingUrl(recording)); - } - return recording; + return getRecordingFromJson(json); } return null; } - public String getRecordingUrl(Recording recording) { - return openviduConfig.getFinalUrl() + "recordings/" + recording.getId() + "/" + recording.getName() + "." - + this.getExtensionFromRecording(recording); + public Recording getRecordingFromJson(JsonObject json) { + Recording recording = new Recording(json); + if (Status.ready.equals(recording.getStatus()) + && composedQuickStartRecordingService.isBeingUploaded(recording)) { + // Recording has finished but is being uploaded + recording.setStatus(Status.stopped); + } else if (Status.ready.equals(recording.getStatus()) || Status.failed.equals(recording.getStatus())) { + // Recording has been completely processed and must include URL + recording.setUrl(recordingManagerUtils.getRecordingUrl(recording)); + } + return recording; } - private String getExtensionFromRecording(Recording recording) { - if (OutputMode.INDIVIDUAL.equals(recording.getOutputMode())) { - return "zip"; - } else if (recording.hasVideo()) { - return "mp4"; - } else { - return "webm"; - } + public String getRecordingUrl(Recording recording) { + return recordingManagerUtils.getRecordingUrl(recording); } public void initAutomaticRecordingStopThread(final Session session) { @@ -621,56 +729,6 @@ public class RecordingManager { } } - private Recording getRecordingFromHost(String recordingId) { - log.info(this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/" - + RecordingManager.RECORDING_ENTITY_FILE + recordingId); - File file = new File(this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/" - + RecordingManager.RECORDING_ENTITY_FILE + recordingId); - log.info("File exists: " + file.exists()); - Recording recording = this.getRecordingFromEntityFile(file); - return recording; - } - - private Set getAllRecordingsFromHost() { - File folder = new File(this.openviduConfig.getOpenViduRecordingPath()); - File[] files = folder.listFiles(); - - Set recordingEntities = new HashSet<>(); - for (int i = 0; i < files.length; i++) { - if (files[i].isDirectory()) { - File[] innerFiles = files[i].listFiles(); - for (int j = 0; j < innerFiles.length; j++) { - Recording recording = this.getRecordingFromEntityFile(innerFiles[j]); - if (recording != null) { - recordingEntities.add(recording); - } - } - } - } - return recordingEntities; - } - - private Set getRecordingIdsFromHost() { - File folder = new File(this.openviduConfig.getOpenViduRecordingPath()); - File[] files = folder.listFiles(); - - Set fileNamesNoExtension = new HashSet<>(); - for (int i = 0; i < files.length; i++) { - if (files[i].isDirectory()) { - File[] innerFiles = files[i].listFiles(); - for (int j = 0; j < innerFiles.length; j++) { - if (innerFiles[j].isFile() - && innerFiles[j].getName().startsWith(RecordingManager.RECORDING_ENTITY_FILE)) { - fileNamesNoExtension - .add(innerFiles[j].getName().replaceFirst(RecordingManager.RECORDING_ENTITY_FILE, "")); - break; - } - } - } - } - return fileNamesNoExtension; - } - private void checkRecordingPaths(String openviduRecordingPath, String openviduRecordingCustomLayout) throws OpenViduException { log.info("Initializing recording paths"); @@ -698,7 +756,8 @@ public class RecordingManager { } final String testFolderPath = openviduRecordingPath + "/TEST_RECORDING_PATH_" + System.currentTimeMillis(); - final String testFilePath = testFolderPath + "/TEST_RECORDING_PATH.webm"; + final String testFilePath = testFolderPath + "/TEST_RECORDING_PATH" + + RecordingService.INDIVIDUAL_RECORDING_EXTENSION; // Check Kurento Media Server write permissions in recording path if (this.kmsManager.getKmss().isEmpty()) { @@ -752,7 +811,7 @@ public class RecordingManager { log.info("Kurento Media Server has write permissions on recording path: {}", openviduRecordingPath); try { - new CustomFileManager().deleteFolder(testFolderPath); + new LocalCustomFileManager().deleteFolder(testFolderPath); log.info("OpenVidu Server has write permissions over files created by Kurento Media Server"); } catch (IOException e) { String errorMessage = "The recording path \"" + openviduRecordingPath @@ -826,6 +885,8 @@ public class RecordingManager { || (sessionsRecordingsStarting.putIfAbsent(recording.getSessionId(), recording) != null)) { log.error("Concurrent session recording initialization. Aborting this thread"); throw new RuntimeException("Concurrent initialization of recording " + recording.getId()); + } else { + this.sessionHandler.storeRecordingToSendClientEvent(recording); } } @@ -834,7 +895,6 @@ public class RecordingManager { * collection */ private void recordingFromStartingToStarted(Recording recording) { - this.sessionHandler.setRecordingStarted(recording.getSessionId(), recording); this.sessionsRecordings.put(recording.getSessionId(), recording); this.startingRecordings.remove(recording.getId()); this.sessionsRecordingsStarting.remove(recording.getSessionId()); diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManagerUtils.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManagerUtils.java new file mode 100644 index 00000000..b2eaa7cb --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManagerUtils.java @@ -0,0 +1,61 @@ +package io.openvidu.server.recording.service; + +import java.util.Set; + +import org.springframework.http.HttpStatus; + +import io.openvidu.java.client.Recording.OutputMode; +import io.openvidu.server.config.OpenviduConfig; +import io.openvidu.server.recording.Recording; +import io.openvidu.server.utils.JsonUtils; + +public abstract class RecordingManagerUtils { + + protected OpenviduConfig openviduConfig; + protected RecordingManager recordingManager; + + protected JsonUtils jsonUtils = new JsonUtils(); + + public RecordingManagerUtils(OpenviduConfig openviduConfig, RecordingManager recordingManager) { + this.openviduConfig = openviduConfig; + this.recordingManager = recordingManager; + } + + public abstract Recording getRecordingFromStorage(String recordingId); + + public abstract Set getAllRecordingsFromStorage(); + + public abstract HttpStatus deleteRecordingFromStorage(String recordingId); + + protected abstract String getRecordingUrl(Recording recording); + + protected abstract Set getAllRecordingIdsFromStorage(); + + protected String getExtensionFromRecording(Recording recording) { + if (OutputMode.INDIVIDUAL.equals(recording.getOutputMode())) { + return "zip"; + } else if (recording.hasVideo()) { + return "mp4"; + } else { + return "webm"; + } + } + + public String getFreeRecordingId(String sessionId) { + Set recordingIds = getAllRecordingIdsFromStorage(); + return getNextAvailableRecordingId(sessionId, recordingIds); + } + + private String getNextAvailableRecordingId(String baseRecordingId, Set existingRecordingIds) { + String recordingId = baseRecordingId; + boolean isPresent = existingRecordingIds.contains(recordingId); + int i = 1; + while (isPresent) { + recordingId = baseRecordingId + "-" + i; + i++; + isPresent = existingRecordingIds.contains(recordingId); + } + return recordingId; + } + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManagerUtilsLocalStorage.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManagerUtilsLocalStorage.java new file mode 100644 index 00000000..66130a87 --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManagerUtilsLocalStorage.java @@ -0,0 +1,64 @@ +package io.openvidu.server.recording.service; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.http.HttpStatus; + +import io.openvidu.server.config.OpenviduConfig; +import io.openvidu.server.recording.Recording; +import io.openvidu.server.rest.RequestMappings; + +public class RecordingManagerUtilsLocalStorage extends RecordingManagerUtils { + + public RecordingManagerUtilsLocalStorage(OpenviduConfig openviduConfig, RecordingManager recordingManager) { + super(openviduConfig, recordingManager); + } + + @Override + public Recording getRecordingFromStorage(String recordingId) { + File file = recordingManager.getRecordingEntityFileFromLocalStorage(recordingId); + return recordingManager.getRecordingFromEntityFile(file); + } + + @Override + public Set getAllRecordingsFromStorage() { + return recordingManager.getAllRecordingsFromLocalStorage(); + } + + @Override + public HttpStatus deleteRecordingFromStorage(String recordingId) { + return recordingManager.deleteRecordingFromLocalStorage(recordingId); + } + + @Override + public String getRecordingUrl(Recording recording) { + String basePath = RequestMappings.RECORDINGS.replaceFirst("^/", "") + "/"; + return openviduConfig.getFinalUrl() + basePath + recording.getId() + "/" + recording.getName() + "." + + this.getExtensionFromRecording(recording); + } + + @Override + protected Set getAllRecordingIdsFromStorage() { + File folder = new File(openviduConfig.getOpenViduRecordingPath()); + File[] files = folder.listFiles(); + + Set fileNamesNoExtension = new HashSet<>(); + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + File[] innerFiles = files[i].listFiles(); + for (int j = 0; j < innerFiles.length; j++) { + if (innerFiles[j].isFile() + && innerFiles[j].getName().startsWith(RecordingService.RECORDING_ENTITY_FILE)) { + fileNamesNoExtension + .add(innerFiles[j].getName().replaceFirst(RecordingService.RECORDING_ENTITY_FILE, "")); + break; + } + } + } + } + return fileNamesNoExtension; + } + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingService.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingService.java index 15317a07..4076532c 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingService.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingService.java @@ -24,17 +24,19 @@ import org.slf4j.LoggerFactory; import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; +import io.openvidu.java.client.Recording.Status; import io.openvidu.java.client.RecordingLayout; import io.openvidu.java.client.RecordingProperties; import io.openvidu.server.cdr.CallDetailRecord; import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.core.EndReason; import io.openvidu.server.core.Session; +import io.openvidu.server.kurento.kms.KmsManager; import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.RecordingDownloader; +import io.openvidu.server.recording.RecordingUploader; import io.openvidu.server.utils.CommandExecutor; import io.openvidu.server.utils.CustomFileManager; -import io.openvidu.server.utils.QuarantineKiller; import io.openvidu.server.utils.RecordingUtils; public abstract class RecordingService { @@ -44,20 +46,33 @@ public abstract class RecordingService { protected OpenviduConfig openviduConfig; protected RecordingManager recordingManager; protected RecordingDownloader recordingDownloader; + protected RecordingUploader recordingUploader; + protected KmsManager kmsManager; + protected CustomFileManager fileManager; protected CallDetailRecord cdr; - protected QuarantineKiller quarantineKiller; - protected CustomFileManager fileWriter = new CustomFileManager(); - RecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader, - OpenviduConfig openviduConfig, CallDetailRecord cdr, QuarantineKiller quarantineKiller) { + public final static String RECORDING_ENTITY_FILE = ".recording."; + public final static String COMPOSED_RECORDING_EXTENSION = ".mp4"; + public final static String COMPOSED_THUMBNAIL_EXTENSION = ".jpg"; + public final static String COMPOSED_INFO_FILE_EXTENSION = ".info"; + public final static String INDIVIDUAL_RECORDING_EXTENSION = ".webm"; + public final static String INDIVIDUAL_STREAM_METADATA_FILE = ".stream."; + public final static String INDIVIDUAL_RECORDING_COMPRESSED_EXTENSION = ".zip"; + + public RecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader, + RecordingUploader recordingUploader, KmsManager kmsManager, CustomFileManager fileManager, + OpenviduConfig openviduConfig, CallDetailRecord cdr) { this.recordingManager = recordingManager; this.recordingDownloader = recordingDownloader; + this.recordingUploader = recordingUploader; + this.kmsManager = kmsManager; + this.fileManager = fileManager; this.openviduConfig = openviduConfig; this.cdr = cdr; - this.quarantineKiller = quarantineKiller; } - public abstract Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException; + public abstract Recording startRecording(Session session, String recordingId, RecordingProperties properties) + throws OpenViduException; public abstract Recording stopRecording(Session session, Recording recording, EndReason reason); @@ -67,20 +82,16 @@ public abstract class RecordingService { */ protected void generateRecordingMetadataFile(Recording recording) { String folder = this.openviduConfig.getOpenViduRecordingPath() + recording.getId(); - boolean newFolderCreated = this.fileWriter.createFolderIfNotExists(folder); + boolean newFolderCreated = this.fileManager.createFolderIfNotExists(folder); if (newFolderCreated) { - log.warn( - "New folder {} created. This means A) Cluster mode is enabled B) The recording started for a session with no publishers or C) No media type compatible publishers", - folder); - } else { - log.info("Folder {} already existed. Some publisher is already being recorded", folder); + log.info("New folder {} created for recording {}", folder, recording.getId()); } String filePath = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/" - + RecordingManager.RECORDING_ENTITY_FILE + recording.getId(); + + RecordingService.RECORDING_ENTITY_FILE + recording.getId(); String text = recording.toJson().toString(); - this.fileWriter.createAndWriteFile(filePath, text); + this.fileManager.createAndWriteFile(filePath, text); log.info("Generated recording metadata file at {}", filePath); } @@ -92,7 +103,7 @@ public abstract class RecordingService { */ protected Recording sealRecordingMetadataFileAsStopped(Recording recording) { final String entityFile = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/" - + RecordingManager.RECORDING_ENTITY_FILE + recording.getId(); + + RecordingService.RECORDING_ENTITY_FILE + recording.getId(); return this.sealRecordingMetadataFile(recording, 0, 0, io.openvidu.java.client.Recording.Status.stopped, entityFile); } @@ -105,15 +116,20 @@ public abstract class RecordingService { */ protected Recording sealRecordingMetadataFileAsReady(Recording recording, long size, double duration, String metadataFilePath) { - io.openvidu.java.client.Recording.Status status = io.openvidu.java.client.Recording.Status.failed - .equals(recording.getStatus()) ? io.openvidu.java.client.Recording.Status.failed - : io.openvidu.java.client.Recording.Status.ready; + Status status = Status.failed.equals(recording.getStatus()) ? Status.failed : Status.ready; + + if (Status.ready.equals(status)) { + // Prevent uploading recordings from being retrieved from REST API with "ready" + // status. This will force their status back to "stopped" on GET until upload + // process has finished + storeAsUploadingRecording(recording); + } // Status is now failed or ready. Url property must be defined recording.setUrl(recordingManager.getRecordingUrl(recording)); final String entityFile = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/" - + RecordingManager.RECORDING_ENTITY_FILE + recording.getId(); + + RecordingService.RECORDING_ENTITY_FILE + recording.getId(); return this.sealRecordingMetadataFile(recording, size, duration, status, entityFile); } @@ -123,7 +139,7 @@ public abstract class RecordingService { recording.setSize(size); // Size in bytes recording.setDuration(duration > 0 ? duration : 0); // Duration in seconds - if (this.fileWriter.overwriteFile(metadataFilePath, recording.toJson().toString())) { + if (this.fileManager.overwriteFile(metadataFilePath, recording.toJson().toString())) { log.info("Sealed recording metadata file at {} with status [{}]", metadataFilePath, status.name()); } @@ -141,8 +157,8 @@ public abstract class RecordingService { if (properties.name() == null || properties.name().isEmpty()) { // No name provided for the recording file. Use recordingId RecordingProperties.Builder builder = new RecordingProperties.Builder().name(recordingId) - .outputMode(properties.outputMode()).hasAudio(properties.hasAudio()) - .hasVideo(properties.hasVideo()); + .outputMode(properties.outputMode()).hasAudio(properties.hasAudio()).hasVideo(properties.hasVideo()) + .mediaNode(properties.mediaNode()); if (RecordingUtils.IS_COMPOSED(properties.outputMode()) && properties.hasVideo()) { builder.resolution(properties.resolution()); builder.recordingLayout(properties.recordingLayout()); @@ -175,6 +191,10 @@ public abstract class RecordingService { protected OpenViduException failStartRecording(Session session, Recording recording, String errorMessage) { log.error("Recording start failed for session {}: {}", session.getSessionId(), errorMessage); recording.setStatus(io.openvidu.java.client.Recording.Status.failed); + + sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(), + getMetadataFilePath(recording)); + this.recordingManager.startingRecordings.remove(recording.getId()); this.recordingManager.sessionsRecordingsStarting.remove(session.getSessionId()); this.stopRecording(session, recording, null); @@ -186,6 +206,30 @@ public abstract class RecordingService { this.recordingManager.startedRecordings.remove(recording.getId()); } + protected String getMetadataFilePath(Recording recording) { + final String folderPath = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/"; + return folderPath + RecordingService.RECORDING_ENTITY_FILE + recording.getId(); + } + + protected void uploadRecording(final Recording recording, EndReason reason) { + recordingUploader.uploadRecording(recording, () -> { + final long timestamp = System.currentTimeMillis(); + cdr.recordRecordingStatusChanged(recording, reason, timestamp, recording.getStatus()); + }, () -> { + final long timestamp = System.currentTimeMillis(); + cdr.recordRecordingStatusChanged(recording, reason, timestamp, + io.openvidu.java.client.Recording.Status.failed); + }); + } + + protected void storeAsUploadingRecording(Recording recording) { + recordingUploader.storeAsUploadingRecording(recording.getId()); + } + + protected boolean isBeingUploaded(Recording recording) { + return recordingUploader.isBeingUploaded(recording.getId()); + } + /** * Simple wrapper for returning update RecordingProperties and a free * recordingId when starting a new recording diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/SingleStreamRecordingService.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/SingleStreamRecordingService.java index 1691d833..9bea2201 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/SingleStreamRecordingService.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/SingleStreamRecordingService.java @@ -24,7 +24,9 @@ import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.Reader; -import java.util.HashMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; @@ -45,10 +47,10 @@ import org.kurento.client.StoppedEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; @@ -59,34 +61,32 @@ import io.openvidu.server.core.EndReason; import io.openvidu.server.core.Participant; import io.openvidu.server.core.Session; import io.openvidu.server.kurento.core.KurentoParticipant; -import io.openvidu.server.kurento.core.KurentoSession; import io.openvidu.server.kurento.endpoint.PublisherEndpoint; +import io.openvidu.server.kurento.kms.KmsManager; import io.openvidu.server.recording.RecorderEndpointWrapper; import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.RecordingDownloader; -import io.openvidu.server.utils.QuarantineKiller; +import io.openvidu.server.recording.RecordingUploader; +import io.openvidu.server.utils.CustomFileManager; public class SingleStreamRecordingService extends RecordingService { private static final Logger log = LoggerFactory.getLogger(SingleStreamRecordingService.class); + // One recorder endpoint active at a time per stream private Map> activeRecorders = new ConcurrentHashMap<>(); - private Map> storedRecorders = new ConcurrentHashMap<>(); - - private final String INDIVIDUAL_STREAM_METADATA_FILE = ".stream."; + // Multiple recorder endpoints per stream during a recording + private Map>> storedRecorders = new ConcurrentHashMap<>(); public SingleStreamRecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader, - OpenviduConfig openviduConfig, CallDetailRecord cdr, QuarantineKiller quarantineKiller) { - super(recordingManager, recordingDownloader, openviduConfig, cdr, quarantineKiller); + RecordingUploader recordingUploader, KmsManager kmsManager, CustomFileManager fileManager, + OpenviduConfig openviduConfig, CallDetailRecord cdr) { + super(recordingManager, recordingDownloader, recordingUploader, kmsManager, fileManager, openviduConfig, cdr); } @Override - public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException { - - PropertiesRecordingId updatePropertiesAndRecordingId = this.setFinalRecordingNameAndGetFreeRecordingId(session, - properties); - properties = updatePropertiesAndRecordingId.properties; - String recordingId = updatePropertiesAndRecordingId.recordingId; + public Recording startRecording(Session session, String recordingId, RecordingProperties properties) + throws OpenViduException { log.info("Starting individual ({}) recording {} of session {}", properties.hasVideo() ? (properties.hasAudio() ? "video+audio" : "video-only") : "audioOnly", @@ -95,14 +95,14 @@ public class SingleStreamRecordingService extends RecordingService { Recording recording = new Recording(session.getSessionId(), recordingId, properties); this.recordingManager.recordingToStarting(recording); - activeRecorders.put(session.getSessionId(), new ConcurrentHashMap()); - storedRecorders.put(session.getSessionId(), new ConcurrentHashMap()); + activeRecorders.put(recording.getId(), new ConcurrentHashMap<>()); + storedRecorders.put(recording.getId(), new ConcurrentHashMap<>()); - final int activePublishers = session.getActivePublishers(); - final CountDownLatch recordingStartedCountdown = new CountDownLatch(activePublishers); + int activePublishersToRecord = session.getActiveIndividualRecordedPublishers(); + final CountDownLatch recordingStartedCountdown = new CountDownLatch(activePublishersToRecord); for (Participant p : session.getParticipants()) { - if (p.isStreaming()) { + if (p.isStreaming() && p.getToken().record()) { MediaProfileSpecType profile = null; try { @@ -114,7 +114,7 @@ public class SingleStreamRecordingService extends RecordingService { recordingStartedCountdown.countDown(); continue; } - this.startRecorderEndpointForPublisherEndpoint(session, recordingId, profile, p, + this.startRecorderEndpointForPublisherEndpoint(recording.getId(), profile, p, recordingStartedCountdown); } } @@ -131,30 +131,28 @@ public class SingleStreamRecordingService extends RecordingService { this.generateRecordingMetadataFile(recording); - // Increment active recordings - ((KurentoSession) session).getKms().getActiveRecordings().incrementAndGet(); - return recording; } @Override public Recording stopRecording(Session session, Recording recording, EndReason reason) { - recording = this.sealRecordingMetadataFileAsStopped(recording); - return this.stopRecording(session, recording, reason, 0); + return this.stopRecording(session, recording, reason, null); } - public Recording stopRecording(Session session, Recording recording, EndReason reason, long kmsDisconnectionTime) { + public Recording stopRecording(Session session, Recording recording, EndReason reason, Long kmsDisconnectionTime) { log.info("Stopping individual ({}) recording {} of session {}. Reason: {}", recording.hasVideo() ? (recording.hasAudio() ? "video+audio" : "video-only") : "audioOnly", recording.getId(), recording.getSessionId(), reason); - final HashMap wrappers = new HashMap<>( - storedRecorders.get(recording.getSessionId())); + final List wrappers = new ArrayList<>(); + storedRecorders.get(recording.getId()).values().forEach(list -> { + wrappers.addAll(list); + }); final CountDownLatch stoppedCountDown = new CountDownLatch(wrappers.size()); - for (RecorderEndpointWrapper wrapper : wrappers.values()) { - this.stopRecorderEndpointOfPublisherEndpoint(recording.getSessionId(), wrapper.getStreamId(), - stoppedCountDown, kmsDisconnectionTime); + for (RecorderEndpointWrapper wrapper : wrappers) { + this.stopRecorderEndpointOfPublisherEndpoint(recording.getId(), wrapper.getStreamId(), stoppedCountDown, + kmsDisconnectionTime); } try { if (!stoppedCountDown.await(5, TimeUnit.SECONDS)) { @@ -171,31 +169,30 @@ public class SingleStreamRecordingService extends RecordingService { final Recording[] finalRecordingArray = new Recording[1]; finalRecordingArray[0] = recording; try { - this.recordingDownloader.downloadRecording(finalRecordingArray[0], wrappers.keySet(), () -> { + this.recordingDownloader.downloadRecording(finalRecordingArray[0], wrappers, () -> { // Update recording entity files with final file size - for (RecorderEndpointWrapper wrapper : wrappers.values()) { + for (RecorderEndpointWrapper wrapper : wrappers) { if (wrapper.getSize() == 0) { updateIndividualMetadataFile(wrapper); } } finalRecordingArray[0] = this.sealMetadataFiles(finalRecordingArray[0]); - final long timestamp = System.currentTimeMillis(); - cdr.recordRecordingStatusChanged(finalRecordingArray[0], reason, timestamp, - finalRecordingArray[0].getStatus()); + cleanRecordingWrappers(finalRecordingArray[0]); - cleanRecordingWrappers(finalRecordingArray[0].getSessionId()); + // Decrement active recordings once it is downloaded. This method will also drop + // the Media Node if no more sessions or recordings and status is + // waiting-idle-to-terminate + kmsManager.decrementActiveRecordings(finalRecordingArray[0].getRecordingProperties(), + finalRecordingArray[0].getId(), session); - // Decrement active recordings once it is downloaded - ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet(); - - // Now we can drop Media Node if waiting-idle-to-terminate - this.quarantineKiller.dropMediaNode(session.getMediaNodeId()); + // Upload if necessary + this.uploadRecording(finalRecordingArray[0], reason); }); } catch (IOException e) { log.error("Error while downloading recording {}", finalRecordingArray[0].getName()); - cleanRecordingWrappers(finalRecordingArray[0].getSessionId()); + cleanRecordingWrappers(finalRecordingArray[0]); } if (reason != null && session != null) { @@ -206,60 +203,42 @@ public class SingleStreamRecordingService extends RecordingService { return finalRecordingArray[0]; } - public void startRecorderEndpointForPublisherEndpoint(final Session session, String recordingId, - MediaProfileSpecType profile, final Participant participant, CountDownLatch globalStartLatch) { + public void startRecorderEndpointForPublisherEndpoint(final String recordingId, MediaProfileSpecType profile, + final Participant participant, CountDownLatch globalStartLatch) { log.info("Starting single stream recorder for stream {} in session {}", participant.getPublisherStreamId(), - session.getSessionId()); + participant.getSessionId()); + + final String streamId = participant.getPublisherStreamId(); try { if (participant.singleRecordingLock.tryLock(15, TimeUnit.SECONDS)) { try { - if (this.activeRecorders.get(session.getSessionId()) - .containsKey(participant.getPublisherStreamId())) { + if (this.activeRecorders.get(recordingId).containsKey(streamId)) { log.warn("Concurrent initialization of RecorderEndpoint for stream {} of session {}. Returning", - participant.getPublisherStreamId(), session.getSessionId()); + streamId, participant.getSessionId()); return; } - if (recordingId == null) { - // Stream is being recorded because is a new publisher in an ongoing recorded - // session. If recordingId is defined is because Stream is being recorded from - // "startRecording" method - Recording recording = this.recordingManager.sessionsRecordings.get(session.getSessionId()); - if (recording == null) { - recording = this.recordingManager.sessionsRecordingsStarting.get(session.getSessionId()); - if (recording == null) { - log.error( - "Cannot start single stream recorder for stream {} in session {}. The recording {} cannot be found", - participant.getPublisherStreamId(), session.getSessionId(), recordingId); - return; - } - } - recordingId = recording.getId(); - - try { - profile = generateMediaProfile(recording.getRecordingProperties(), participant); - } catch (OpenViduException e) { - log.error("Cannot start single stream recorder for stream {} in session {}: {}", - participant.getPublisherStreamId(), session.getSessionId(), e.getMessage()); - return; - } - } + // Update stream recording counter + final List wrapperList = storedRecorders.get(recordingId).get(streamId); + final int streamCounter = wrapperList != null ? wrapperList.size() : 0; + String fileName = streamCounter == 0 ? streamId : (streamId + "-" + streamCounter); KurentoParticipant kurentoParticipant = (KurentoParticipant) participant; MediaPipeline pipeline = kurentoParticipant.getPublisher().getPipeline(); RecorderEndpoint recorder = new RecorderEndpoint.Builder(pipeline, - "file://" + openviduConfig.getOpenViduRemoteRecordingPath() + recordingId + "/" - + participant.getPublisherStreamId() + ".webm").withMediaProfile(profile).build(); + "file://" + openviduConfig.getOpenViduRemoteRecordingPath() + recordingId + "/" + fileName + + RecordingService.INDIVIDUAL_RECORDING_EXTENSION).withMediaProfile(profile) + .build(); recorder.addRecordingListener(new EventListener() { @Override public void onEvent(RecordingEvent event) { - activeRecorders.get(session.getSessionId()).get(participant.getPublisherStreamId()) + activeRecorders.get(recordingId).get(streamId) .setStartTime(Long.parseLong(event.getTimestampMillis())); - log.info("Recording started event for stream {}", participant.getPublisherStreamId()); + log.info("Recording started event for stream {}", streamId); globalStartLatch.countDown(); } }); @@ -271,15 +250,14 @@ public class SingleStreamRecordingService extends RecordingService { } }); - RecorderEndpointWrapper wrapper = new RecorderEndpointWrapper(recorder, - participant.getParticipantPublicId(), recordingId, participant.getPublisherStreamId(), - participant.getClientMetadata(), participant.getServerMetadata(), - kurentoParticipant.getPublisher().getMediaOptions().hasAudio(), - kurentoParticipant.getPublisher().getMediaOptions().hasVideo(), - kurentoParticipant.getPublisher().getMediaOptions().getTypeOfVideo()); - - activeRecorders.get(session.getSessionId()).put(participant.getPublisherStreamId(), wrapper); - storedRecorders.get(session.getSessionId()).put(participant.getPublisherStreamId(), wrapper); + RecorderEndpointWrapper wrapper = new RecorderEndpointWrapper(recorder, kurentoParticipant, + recordingId, fileName); + activeRecorders.get(recordingId).put(streamId, wrapper); + if (wrapperList != null) { + wrapperList.add(wrapper); + } else { + storedRecorders.get(recordingId).put(streamId, new ArrayList<>(Arrays.asList(wrapper))); + } connectAccordingToProfile(kurentoParticipant.getPublisher(), recorder, profile); wrapper.getRecorder().record(); @@ -289,51 +267,73 @@ public class SingleStreamRecordingService extends RecordingService { } } else { log.error( - "Timeout waiting for individual recording lock to be available for participant {} of session {}", - participant.getParticipantPublicId(), session.getSessionId()); + "Timeout waiting for individual recording lock to be available to start stream recording for participant {} of session {}", + participant.getParticipantPublicId(), participant.getSessionId()); } } catch (InterruptedException e) { log.error( - "InterruptedException waiting for individual recording lock to be available for participant {} of session {}", - participant.getParticipantPublicId(), session.getSessionId()); + "InterruptedException waiting for individual recording lock to be available to start stream recording for participant {} of session {}", + participant.getParticipantPublicId(), participant.getSessionId()); } } - public void stopRecorderEndpointOfPublisherEndpoint(String sessionId, String streamId, + public void stopRecorderEndpointOfPublisherEndpoint(String recordingId, String streamId, CountDownLatch globalStopLatch, Long kmsDisconnectionTime) { - log.info("Stopping single stream recorder for stream {} in session {}", streamId, sessionId); - final RecorderEndpointWrapper finalWrapper = activeRecorders.get(sessionId).remove(streamId); - if (finalWrapper != null && kmsDisconnectionTime == 0) { - finalWrapper.getRecorder().addStoppedListener(new EventListener() { - @Override - public void onEvent(StoppedEvent event) { - finalWrapper.setEndTime(Long.parseLong(event.getTimestampMillis())); - generateIndividualMetadataFile(finalWrapper); - log.info("Recording stopped event for stream {}", streamId); - finalWrapper.getRecorder().release(); - globalStopLatch.countDown(); - } - }); - finalWrapper.getRecorder().stop(); - } else { - if (kmsDisconnectionTime != 0) { - // Stopping recorder endpoint because of a KMS disconnection - finalWrapper.setEndTime(kmsDisconnectionTime); - generateIndividualMetadataFile(finalWrapper); - log.warn("Forcing individual recording stop after KMS restart for stream {} in session {}", streamId, - sessionId); - } else { - if (storedRecorders.get(sessionId).containsKey(streamId)) { - log.info("Stream {} recording of session {} was already stopped", streamId, sessionId); + + log.info("Stopping single stream recorder for stream {} in recording {}", streamId, recordingId); + + final RecorderEndpointWrapper finalWrapper = activeRecorders.get(recordingId).remove(streamId); + if (finalWrapper != null) { + KurentoParticipant kParticipant = finalWrapper.getParticipant(); + try { + if (kParticipant.singleRecordingLock.tryLock(15, TimeUnit.SECONDS)) { + try { + if (kmsDisconnectionTime == null) { + finalWrapper.getRecorder().addStoppedListener(new EventListener() { + @Override + public void onEvent(StoppedEvent event) { + finalWrapper.setEndTime(Long.parseLong(event.getTimestampMillis())); + generateIndividualMetadataFile(finalWrapper); + log.info("Recording stopped event for stream {}", streamId); + finalWrapper.getRecorder().release(); + globalStopLatch.countDown(); + } + }); + finalWrapper.getRecorder().stop(); + } else { + // Stopping recorder endpoint because of a KMS disconnection + finalWrapper.setEndTime(kmsDisconnectionTime); + generateIndividualMetadataFile(finalWrapper); + globalStopLatch.countDown(); + log.warn( + "Forcing individual recording stop after KMS restart for stream {} in recording {}", + streamId, recordingId); + } + } finally { + kParticipant.singleRecordingLock.unlock(); + } } else { - log.error("Stream {} wasn't being recorded in session {}", streamId, sessionId); + log.error( + "Timeout waiting for individual recording lock to be available to stop stream recording for participant {} of session {}", + kParticipant.getParticipantPublicId(), kParticipant.getSessionId()); } + } catch (InterruptedException e) { + log.error( + "InterruptedException waiting for individual recording lock to be available to stop stream recording for participant {} of session {}", + kParticipant.getParticipantPublicId(), kParticipant.getSessionId()); + } + } else { + // The streamId has no associated RecorderEndpoint + if (storedRecorders.get(recordingId).containsKey(streamId)) { + log.info("Stream {} recording of recording {} was already stopped", streamId, recordingId); + } else { + log.info("Stream {} wasn't being recorded in recording {}", streamId, recordingId); } globalStopLatch.countDown(); } } - private MediaProfileSpecType generateMediaProfile(RecordingProperties properties, Participant participant) + MediaProfileSpecType generateMediaProfile(RecordingProperties properties, Participant participant) throws OpenViduException { KurentoParticipant kParticipant = (KurentoParticipant) participant; @@ -406,19 +406,19 @@ public class SingleStreamRecordingService extends RecordingService { } private void generateIndividualMetadataFile(RecorderEndpointWrapper wrapper) { - this.commonWriteIndividualMetadataFile(wrapper, this.fileWriter::createAndWriteFile); + this.commonWriteIndividualMetadataFile(wrapper, this.fileManager::createAndWriteFile); } private void updateIndividualMetadataFile(RecorderEndpointWrapper wrapper) { - this.commonWriteIndividualMetadataFile(wrapper, this.fileWriter::overwriteFile); + this.commonWriteIndividualMetadataFile(wrapper, this.fileManager::overwriteFile); } private void commonWriteIndividualMetadataFile(RecorderEndpointWrapper wrapper, BiFunction writeFunction) { String filesPath = this.openviduConfig.getOpenViduRecordingPath() + wrapper.getRecordingId() + "/"; - File videoFile = new File(filesPath + wrapper.getStreamId() + ".webm"); + File videoFile = new File(filesPath + wrapper.getNameWithExtension()); wrapper.setSize(videoFile.length()); - String metadataFilePath = filesPath + INDIVIDUAL_STREAM_METADATA_FILE + wrapper.getStreamId(); + String metadataFilePath = filesPath + INDIVIDUAL_STREAM_METADATA_FILE + wrapper.getName(); String metadataFileContent = wrapper.toJson().toString(); writeFunction.apply(metadataFilePath, metadataFileContent); } @@ -428,7 +428,7 @@ public class SingleStreamRecordingService extends RecordingService { // individual recordings) and "size" (sum of all individual recordings size) String folderPath = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/"; - String metadataFilePath = folderPath + RecordingManager.RECORDING_ENTITY_FILE + recording.getId(); + String metadataFilePath = folderPath + RecordingService.RECORDING_ENTITY_FILE + recording.getId(); String syncFilePath = folderPath + recording.getName() + ".json"; recording = this.recordingManager.getRecordingFromEntityFile(new File(metadataFilePath)); @@ -441,7 +441,6 @@ public class SingleStreamRecordingService extends RecordingService { File[] files = folder.listFiles(); Reader reader = null; - Gson gson = new Gson(); // Sync metadata json object to store in "RECORDING_NAME.json" JsonObject json = new JsonObject(); @@ -458,12 +457,14 @@ public class SingleStreamRecordingService extends RecordingService { } catch (FileNotFoundException e) { log.error("Error reading file {}. Error: {}", files[i].getAbsolutePath(), e.getMessage()); } - RecorderEndpointWrapper wr = gson.fromJson(reader, RecorderEndpointWrapper.class); + RecorderEndpointWrapper wr = new RecorderEndpointWrapper( + JsonParser.parseReader(reader).getAsJsonObject()); minStartTime = Math.min(minStartTime, wr.getStartTime()); maxEndTime = Math.max(maxEndTime, wr.getEndTime()); accumulatedSize += wr.getSize(); JsonObject jsonFile = new JsonObject(); + jsonFile.addProperty("name", wr.getNameWithExtension()); jsonFile.addProperty("connectionId", wr.getConnectionId()); jsonFile.addProperty("streamId", wr.getStreamId()); jsonFile.addProperty("size", wr.getSize()); @@ -482,8 +483,9 @@ public class SingleStreamRecordingService extends RecordingService { } json.add("files", jsonArrayFiles); - this.fileWriter.createAndWriteFile(syncFilePath, new GsonBuilder().setPrettyPrinting().create().toJson(json)); - this.generateZipFileAndCleanFolder(folderPath, recording.getName() + ".zip"); + this.fileManager.createAndWriteFile(syncFilePath, new GsonBuilder().setPrettyPrinting().create().toJson(json)); + this.generateZipFileAndCleanFolder(folderPath, + recording.getName() + RecordingService.INDIVIDUAL_RECORDING_COMPRESSED_EXTENSION); double duration = (double) (maxEndTime - minStartTime) / 1000; duration = duration > 0 ? duration : 0; @@ -506,7 +508,8 @@ public class SingleStreamRecordingService extends RecordingService { for (int i = 0; i < files.length; i++) { String fileExtension = FilenameUtils.getExtension(files[i].getName()); - if (files[i].isFile() && (fileExtension.equals("json") || fileExtension.equals("webm"))) { + if (files[i].isFile() && (fileExtension.equals("json") + || RecordingService.INDIVIDUAL_RECORDING_EXTENSION.equals("." + fileExtension))) { // Zip video files and json sync metadata file FileInputStream fis = new FileInputStream(files[i]); @@ -520,7 +523,7 @@ public class SingleStreamRecordingService extends RecordingService { fis.close(); } - if (!files[i].getName().startsWith(RecordingManager.RECORDING_ENTITY_FILE)) { + if (!files[i].getName().startsWith(RecordingService.RECORDING_ENTITY_FILE)) { // Clean inspected file if it is not files[i].delete(); } @@ -539,9 +542,9 @@ public class SingleStreamRecordingService extends RecordingService { } } - private void cleanRecordingWrappers(String sessionId) { - this.storedRecorders.remove(sessionId); - this.activeRecorders.remove(sessionId); + private void cleanRecordingWrappers(Recording recording) { + this.storedRecorders.remove(recording.getId()); + this.activeRecorders.remove(recording.getId()); } } diff --git a/openvidu-server/src/main/java/io/openvidu/server/resources/CDRHttpHandler.java b/openvidu-server/src/main/java/io/openvidu/server/resources/CDRHttpHandler.java index 151e24ee..a2f94978 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/resources/CDRHttpHandler.java +++ b/openvidu-server/src/main/java/io/openvidu/server/resources/CDRHttpHandler.java @@ -23,6 +23,7 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import io.openvidu.server.config.OpenviduConfig; +import io.openvidu.server.rest.RequestMappings; @Configuration public class CDRHttpHandler implements WebMvcConfigurer { @@ -33,7 +34,7 @@ public class CDRHttpHandler implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String cdrPath = openviduConfig.getOpenviduCdrPath(); - registry.addResourceHandler("/cdr/**.log").addResourceLocations("file:" + cdrPath); + registry.addResourceHandler(RequestMappings.CDR + "/**.log").addResourceLocations("file:" + cdrPath); } } diff --git a/openvidu-server/src/main/java/io/openvidu/server/resources/FrontendResourceHandler.java b/openvidu-server/src/main/java/io/openvidu/server/resources/FrontendResourceHandler.java index 076b4c86..4add4b05 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/resources/FrontendResourceHandler.java +++ b/openvidu-server/src/main/java/io/openvidu/server/resources/FrontendResourceHandler.java @@ -29,7 +29,7 @@ import io.openvidu.server.config.OpenviduConfig; * /NEW_FRONTEND_PATH. Entrypoint file index.html must have tag * * - * By default in OpenVidu CE this path is /dashbaord and in OpenVidu PRO is + * By default in OpenVidu CE this path is /dashboard and in OpenVidu PRO is * /inspector * * @author Pablo Fuente (pablofuenteperez@gmail.com) @@ -42,10 +42,10 @@ public class FrontendResourceHandler extends WebMvcConfigurerAdapter { @Override public void addViewControllers(ViewControllerRegistry registry) { - registry.addViewController("/" + openviduConfig.getOpenViduFrontendDefaultPath()) - .setViewName("redirect:/" + openviduConfig.getOpenViduFrontendDefaultPath() + "/"); - registry.addViewController("/" + openviduConfig.getOpenViduFrontendDefaultPath() + "/") - .setViewName("forward:/" + openviduConfig.getOpenViduFrontendDefaultPath() + "/index.html"); + registry.addViewController(openviduConfig.getOpenViduFrontendDefaultPath()) + .setViewName("redirect:" + openviduConfig.getOpenViduFrontendDefaultPath() + "/"); + registry.addViewController(openviduConfig.getOpenViduFrontendDefaultPath() + "/") + .setViewName("forward:" + openviduConfig.getOpenViduFrontendDefaultPath() + "/index.html"); super.addViewControllers(registry); } diff --git a/openvidu-server/src/main/java/io/openvidu/server/resources/RecordingCustomLayoutsResourceHandler.java b/openvidu-server/src/main/java/io/openvidu/server/resources/RecordingCustomLayoutsResourceHandler.java index dab3e05f..29311660 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/resources/RecordingCustomLayoutsResourceHandler.java +++ b/openvidu-server/src/main/java/io/openvidu/server/resources/RecordingCustomLayoutsResourceHandler.java @@ -24,6 +24,7 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import io.openvidu.server.config.OpenviduConfig; +import io.openvidu.server.rest.RequestMappings; /** * This class serves custom recording layouts from host folder indicated in @@ -41,7 +42,8 @@ public class RecordingCustomLayoutsResourceHandler implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String customLayoutsPath = openviduConfig.getOpenviduRecordingCustomLayout(); - registry.addResourceHandler("/layouts/custom/**").addResourceLocations("file:" + customLayoutsPath); + registry.addResourceHandler(RequestMappings.CUSTOM_LAYOUTS + "/**") + .addResourceLocations("file:" + customLayoutsPath); } } \ No newline at end of file diff --git a/openvidu-server/src/main/java/io/openvidu/server/resources/RecordingsResourceHandler.java b/openvidu-server/src/main/java/io/openvidu/server/resources/RecordingsResourceHandler.java index f1433fa6..9bb6474f 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/resources/RecordingsResourceHandler.java +++ b/openvidu-server/src/main/java/io/openvidu/server/resources/RecordingsResourceHandler.java @@ -23,6 +23,7 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import io.openvidu.server.config.OpenviduConfig; +import io.openvidu.server.rest.RequestMappings; /** * This class serves recording files from host folder indicated in configuration @@ -39,7 +40,7 @@ public class RecordingsResourceHandler implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String recordingsPath = openviduConfig.getOpenViduRecordingPath(); - registry.addResourceHandler("/recordings/**").addResourceLocations("file:" + recordingsPath); + registry.addResourceHandler(RequestMappings.RECORDINGS + "/**").addResourceLocations("file:" + recordingsPath); } } diff --git a/openvidu-server/src/main/java/io/openvidu/server/rest/ApiRestPathRewriteFilter.java b/openvidu-server/src/main/java/io/openvidu/server/rest/ApiRestPathRewriteFilter.java new file mode 100644 index 00000000..2abe443b --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/rest/ApiRestPathRewriteFilter.java @@ -0,0 +1,145 @@ +package io.openvidu.server.rest; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.web.util.WebUtils; + +import io.openvidu.server.config.OpenviduConfig; + +public class ApiRestPathRewriteFilter implements Filter { + + protected static final Logger log = LoggerFactory.getLogger(ApiRestPathRewriteFilter.class); + + protected Map PATH_REDIRECTIONS_MAP = new HashMap() { + { + // WS + put("/info", RequestMappings.WS_INFO); + // APIs + put("/api/", RequestMappings.API + "/"); + put("/config", RequestMappings.API + "/config"); + put("/config/", RequestMappings.API + "/config/"); + put("/cdr", RequestMappings.CDR); + // Static resources + put("/cdr/", RequestMappings.CDR + "/"); + put("/recordings/", RequestMappings.RECORDINGS + "/"); + put("/layouts/custom/", RequestMappings.CUSTOM_LAYOUTS + "/"); + + put("/accept-certificate", RequestMappings.ACCEPT_CERTIFICATE); // ?? + } + }; + protected String[] PATH_REDIRECTIONS_ARRAY; + + public ApiRestPathRewriteFilter() { + PATH_REDIRECTIONS_ARRAY = PATH_REDIRECTIONS_MAP.keySet().toArray(new String[PATH_REDIRECTIONS_MAP.size()]); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + log.warn("Support for deprecated REST API paths enabled. Update your REST API clients to use the new paths"); + log.warn( + "Deprecated path support will be removed in a future release. You can disable old path support to test compatibility with property SUPPORT_DEPRECATED_API=false"); + } + + @Override + public void destroy() { + // Nothing to free up... + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse res = (HttpServletResponse) response; + String requestPath = req.getRequestURI(); + + String oldBasePath = null; + String newBasePath = null; + + for (final String path : PATH_REDIRECTIONS_ARRAY) { + if (requestPath.startsWith(path)) { + oldBasePath = path; + break; + } + } + + if (oldBasePath != null) { + + newBasePath = PATH_REDIRECTIONS_MAP.get(oldBasePath); + + String redirectURI = newBasePath + requestPath.substring(oldBasePath.length()); + StringBuffer redirectURL = new StringBuffer( + ((HttpServletRequest) request).getRequestURL().toString().replaceFirst(oldBasePath, newBasePath)); + + String logPathEnding = oldBasePath.endsWith("/") ? "**" : "/**"; + log.warn("Path {} is deprecated. Use path {} instead. Deprecated path will be removed in a future release", + oldBasePath + logPathEnding, newBasePath + logPathEnding); + + chain.doFilter(new HttpServletRequestWrapper((HttpServletRequest) request) { + @Override + public String getRequestURI() { + return redirectURI; + } + + @Override + public StringBuffer getRequestURL() { + return redirectURL; + } + + @Override + public Object getAttribute(String name) { + if (WebUtils.INCLUDE_SERVLET_PATH_ATTRIBUTE.equals(name)) + return redirectURI; + return super.getAttribute(name); + } + }, response); + + } else { + chain.doFilter(req, res); + } + } + + public static void protectOldPathsCe( + ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry conf, + OpenviduConfig openviduConf) throws Exception { + + conf.antMatchers("/api/**").authenticated() + // /config + .antMatchers(HttpMethod.GET, "/config/openvidu-publicurl").permitAll() + .antMatchers(HttpMethod.GET, "/config/**").authenticated() + // /cdr + .antMatchers(HttpMethod.GET, "/cdr/**").authenticated() + // /accept-certificate + .antMatchers(HttpMethod.GET, "/accept-certificate").permitAll() + // Dashboard + .antMatchers(HttpMethod.GET, "/dashboard/**").authenticated(); + + // Security for recording layouts + conf.antMatchers("/layouts/**").authenticated(); + + // Security for recorded video files + if (openviduConf.getOpenViduRecordingPublicAccess()) { + conf = conf.antMatchers("/recordings/**").permitAll(); + } else { + conf = conf.antMatchers("/recordings/**").authenticated(); + } + } + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/rest/CDRRestController.java b/openvidu-server/src/main/java/io/openvidu/server/rest/CDRRestController.java index 0ef92fa3..47d207d2 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/rest/CDRRestController.java +++ b/openvidu-server/src/main/java/io/openvidu/server/rest/CDRRestController.java @@ -47,7 +47,7 @@ import io.openvidu.server.config.OpenviduConfig; */ @RestController @CrossOrigin -@RequestMapping("/cdr") +@RequestMapping(RequestMappings.CDR) public class CDRRestController { private static final Logger log = LoggerFactory.getLogger(CDRRestController.class); @@ -58,7 +58,7 @@ public class CDRRestController { @RequestMapping(method = RequestMethod.GET) public ResponseEntity listCdrFiles() { - log.info("REST API: GET /cdr"); + log.info("REST API: GET {}", RequestMappings.CDR); String cdrPath = openviduConfig.getOpenviduCdrPath(); JsonArray cdrFiles = new JsonArray(); diff --git a/openvidu-server/src/main/java/io/openvidu/server/rest/CertificateRestController.java b/openvidu-server/src/main/java/io/openvidu/server/rest/CertificateRestController.java index c5058d9d..542249fa 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/rest/CertificateRestController.java +++ b/openvidu-server/src/main/java/io/openvidu/server/rest/CertificateRestController.java @@ -24,7 +24,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController @CrossOrigin -@RequestMapping("/accept-certificate") +@RequestMapping(RequestMappings.ACCEPT_CERTIFICATE) public class CertificateRestController { @RequestMapping(method = RequestMethod.GET) diff --git a/openvidu-server/src/main/java/io/openvidu/server/rest/ConfigRestController.java b/openvidu-server/src/main/java/io/openvidu/server/rest/ConfigRestController.java index c8f4d847..9c1d1e2f 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/rest/ConfigRestController.java +++ b/openvidu-server/src/main/java/io/openvidu/server/rest/ConfigRestController.java @@ -21,9 +21,8 @@ import org.apache.http.Header; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; @@ -36,6 +35,7 @@ import com.google.gson.JsonObject; import io.openvidu.server.cdr.CDREventName; import io.openvidu.server.config.OpenviduBuildInfo; import io.openvidu.server.config.OpenviduConfig; +import io.openvidu.server.utils.RestUtils; /** * @@ -43,7 +43,8 @@ import io.openvidu.server.config.OpenviduConfig; */ @RestController @CrossOrigin -@RequestMapping("/config") +@ConditionalOnMissingBean(name = "configRestControllerPro") +@RequestMapping(RequestMappings.API + "/config") public class ConfigRestController { private static final Logger log = LoggerFactory.getLogger(ConfigRestController.class); @@ -54,10 +55,18 @@ public class ConfigRestController { @Autowired private OpenviduBuildInfo openviduBuildInfo; + @RequestMapping(method = RequestMethod.GET) + public ResponseEntity getOpenViduConfiguration() { + + log.info("REST API: GET {}", RequestMappings.API + "/config"); + + return this.getConfig(); + } + @RequestMapping(value = "/openvidu-version", method = RequestMethod.GET) public String getOpenViduServerVersion() { - log.info("REST API: GET /config/openvidu-version"); + log.info("REST API: GET {}/openvidu-version", RequestMappings.API + "/config"); return openviduBuildInfo.getOpenViduServerVersion(); } @@ -65,7 +74,7 @@ public class ConfigRestController { @RequestMapping(value = "/openvidu-publicurl", method = RequestMethod.GET) public String getOpenViduPublicUrl() { - log.info("REST API: GET /config/openvidu-publicurl"); + log.info("REST API: GET {}/openvidu-publicurl", RequestMappings.API + "/config"); return openviduConfig.getFinalUrl(); } @@ -73,7 +82,7 @@ public class ConfigRestController { @RequestMapping(value = "/openvidu-recording", method = RequestMethod.GET) public Boolean getOpenViduRecordingEnabled() { - log.info("REST API: GET /config/openvidu-recording"); + log.info("REST API: GET {}/openvidu-recording", RequestMappings.API + "/config"); return openviduConfig.isRecordingModuleEnabled(); } @@ -81,7 +90,7 @@ public class ConfigRestController { @RequestMapping(value = "/openvidu-recording-path", method = RequestMethod.GET) public String getOpenViduRecordingPath() { - log.info("REST API: GET /config/openvidu-recording-path"); + log.info("REST API: GET {}/openvidu-recording-path", RequestMappings.API + "/config"); return openviduConfig.getOpenViduRecordingPath(); } @@ -89,16 +98,12 @@ public class ConfigRestController { @RequestMapping(value = "/openvidu-cdr", method = RequestMethod.GET) public Boolean getOpenViduCdrEnabled() { - log.info("REST API: GET /config/openvidu-cdr"); + log.info("REST API: GET {}/openvidu-cdr", RequestMappings.API + "/config"); return openviduConfig.isCdrEnabled(); } - @RequestMapping(method = RequestMethod.GET) - public ResponseEntity getOpenViduConfiguration() { - - log.info("REST API: GET /config"); - + protected ResponseEntity getConfig() { JsonObject json = new JsonObject(); json.addProperty("VERSION", openviduBuildInfo.getVersion()); json.addProperty("DOMAIN_OR_PUBLIC_IP", openviduConfig.getDomainOrPublicIp()); @@ -109,6 +114,8 @@ public class ConfigRestController { json.addProperty("OPENVIDU_STREAMS_VIDEO_MIN_RECV_BANDWIDTH", openviduConfig.getVideoMinRecvBandwidth()); json.addProperty("OPENVIDU_STREAMS_VIDEO_MAX_SEND_BANDWIDTH", openviduConfig.getVideoMaxSendBandwidth()); json.addProperty("OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH", openviduConfig.getVideoMinSendBandwidth()); + json.addProperty("OPENVIDU_STREAMS_FORCED_VIDEO_CODEC", openviduConfig.getOpenviduForcedCodec().name()); + json.addProperty("OPENVIDU_STREAMS_ALLOW_TRANSCODING", openviduConfig.isOpenviduAllowingTranscoding()); json.addProperty("OPENVIDU_SESSIONS_GARBAGE_INTERVAL", openviduConfig.getSessionGarbageInterval()); json.addProperty("OPENVIDU_SESSIONS_GARBAGE_THRESHOLD", openviduConfig.getSessionGarbageThreshold()); json.addProperty("OPENVIDU_RECORDING", openviduConfig.isRecordingModuleEnabled()); @@ -141,13 +148,7 @@ public class ConfigRestController { json.add("OPENVIDU_WEBHOOK_EVENTS", webhookEvents); } - return new ResponseEntity<>(json.toString(), getResponseHeaders(), HttpStatus.OK); - } - - protected HttpHeaders getResponseHeaders() { - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.setContentType(MediaType.APPLICATION_JSON); - return responseHeaders; + return new ResponseEntity<>(json.toString(), RestUtils.getResponseHeaders(), HttpStatus.OK); } } diff --git a/openvidu-server/src/main/java/io/openvidu/server/rest/RequestMappings.java b/openvidu-server/src/main/java/io/openvidu/server/rest/RequestMappings.java new file mode 100644 index 00000000..f842ee85 --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/rest/RequestMappings.java @@ -0,0 +1,21 @@ +package io.openvidu.server.rest; + +public class RequestMappings { + + // WebSocket + final public static String WS_RPC = "/openvidu"; + final public static String WS_INFO = "/openvidu/info"; + // REST API + final public static String API = "/openvidu/api"; + final public static String CDR = "/openvidu/cdr"; + final public static String API_ELK = "/openvidu/elk"; + final public static String API_INSPECTOR = "/openvidu/inspector-api"; + // Static resources + final public static String RECORDINGS = "/openvidu/recordings"; + final public static String CUSTOM_LAYOUTS = "/openvidu/layouts"; + final public static String FRONTEND_CE = "/dashboard"; + final public static String FRONTEND_PRO = "/inspector"; + + final public static String ACCEPT_CERTIFICATE = "/openvidu/accept-certificate"; // ???? + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java b/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java index 606efcb1..4a5b56ea 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java +++ b/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java @@ -20,7 +20,9 @@ package io.openvidu.server.rest; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; import java.util.Map; +import java.util.Map.Entry; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -34,9 +36,8 @@ import org.apache.commons.lang3.RandomStringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.PathVariable; @@ -46,8 +47,19 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + import io.openvidu.client.OpenViduException; +import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.internal.ProtocolElements; +import io.openvidu.java.client.ConnectionProperties; +import io.openvidu.java.client.ConnectionType; +import io.openvidu.java.client.KurentoOptions; import io.openvidu.java.client.MediaMode; import io.openvidu.java.client.OpenViduRole; import io.openvidu.java.client.Recording.OutputMode; @@ -62,11 +74,12 @@ import io.openvidu.server.core.IdentifierPrefixes; import io.openvidu.server.core.Participant; import io.openvidu.server.core.Session; import io.openvidu.server.core.SessionManager; +import io.openvidu.server.core.Token; import io.openvidu.server.kurento.core.KurentoMediaOptions; -import io.openvidu.server.kurento.core.KurentoTokenOptions; import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.service.RecordingManager; import io.openvidu.server.utils.RecordingUtils; +import io.openvidu.server.utils.RestUtils; /** * @@ -74,114 +87,39 @@ import io.openvidu.server.utils.RecordingUtils; */ @RestController @CrossOrigin -@RequestMapping("/api") +@ConditionalOnMissingBean(name = "sessionRestControllerPro") +@RequestMapping(RequestMappings.API) public class SessionRestController { private static final Logger log = LoggerFactory.getLogger(SessionRestController.class); @Autowired - private SessionManager sessionManager; + protected SessionManager sessionManager; @Autowired - private RecordingManager recordingManager; + protected RecordingManager recordingManager; @Autowired - private OpenviduConfig openviduConfig; + protected OpenviduConfig openviduConfig; @RequestMapping(value = "/sessions", method = RequestMethod.POST) - public ResponseEntity getSessionId(@RequestBody(required = false) Map params) { + public ResponseEntity initializeSession(@RequestBody(required = false) Map params) { - log.info("REST API: POST /api/sessions {}", params != null ? params.toString() : "{}"); + log.info("REST API: POST {}/sessions {}", RequestMappings.API, params != null ? params.toString() : "{}"); - SessionProperties.Builder builder = new SessionProperties.Builder(); - String customSessionId = null; - - if (params != null) { - - String mediaModeString; - String recordingModeString; - String defaultOutputModeString; - String defaultRecordingLayoutString; - String defaultCustomLayout; - String forcedVideoCodec; - Boolean allowTranscoding; - try { - mediaModeString = (String) params.get("mediaMode"); - recordingModeString = (String) params.get("recordingMode"); - defaultOutputModeString = (String) params.get("defaultOutputMode"); - defaultRecordingLayoutString = (String) params.get("defaultRecordingLayout"); - defaultCustomLayout = (String) params.get("defaultCustomLayout"); - customSessionId = (String) params.get("customSessionId"); - forcedVideoCodec = (String) params.get("forcedVideoCodec"); - allowTranscoding = (Boolean) params.get("allowTranscoding"); - } catch (ClassCastException e) { - return this.generateErrorResponse("Type error in some parameter", "/api/sessions", - HttpStatus.BAD_REQUEST); - } - - try { - - // Safe parameter retrieval. Default values if not defined - if (recordingModeString != null) { - RecordingMode recordingMode = RecordingMode.valueOf(recordingModeString); - builder = builder.recordingMode(recordingMode); - } else { - builder = builder.recordingMode(RecordingMode.MANUAL); - } - if (defaultOutputModeString != null) { - OutputMode defaultOutputMode = OutputMode.valueOf(defaultOutputModeString); - builder = builder.defaultOutputMode(defaultOutputMode); - } else { - builder.defaultOutputMode(OutputMode.COMPOSED); - } - if (defaultRecordingLayoutString != null) { - RecordingLayout defaultRecordingLayout = RecordingLayout.valueOf(defaultRecordingLayoutString); - builder = builder.defaultRecordingLayout(defaultRecordingLayout); - } else { - builder.defaultRecordingLayout(RecordingLayout.BEST_FIT); - } - if (mediaModeString != null) { - MediaMode mediaMode = MediaMode.valueOf(mediaModeString); - builder = builder.mediaMode(mediaMode); - } else { - builder = builder.mediaMode(MediaMode.ROUTED); - } - if (customSessionId != null && !customSessionId.isEmpty()) { - if (!sessionManager.formatChecker.isValidCustomSessionId(customSessionId)) { - return this.generateErrorResponse( - "Parameter \"customSessionId\" is wrong. Must be an alphanumeric string", - "/api/sessions", HttpStatus.BAD_REQUEST); - } - builder = builder.customSessionId(customSessionId); - } - builder = builder.defaultCustomLayout((defaultCustomLayout != null) ? defaultCustomLayout : ""); - if (forcedVideoCodec != null) { - builder = builder.forcedVideoCodec(VideoCodec.valueOf(forcedVideoCodec)); - } else { - builder = builder.forcedVideoCodec(openviduConfig.getOpenviduForcedCodec()); - } - if (allowTranscoding != null) { - builder = builder.allowTranscoding(allowTranscoding); - } else { - builder = builder.allowTranscoding(openviduConfig.isOpenviduAllowingTranscoding()); - } - - } catch (IllegalArgumentException e) { - return this.generateErrorResponse("RecordingMode " + params.get("recordingMode") + " | " - + "Default OutputMode " + params.get("defaultOutputMode") + " | " + "Default RecordingLayout " - + params.get("defaultRecordingLayout") + " | " + "MediaMode " + params.get("mediaMode") - + ". Some parameter is not defined", "/api/sessions", HttpStatus.BAD_REQUEST); - } + SessionProperties sessionProperties; + try { + sessionProperties = getSessionPropertiesFromParams(params).build(); + } catch (Exception e) { + return this.generateErrorResponse(e.getMessage(), "/sessions", HttpStatus.BAD_REQUEST); } - SessionProperties sessionProperties = builder.build(); - String sessionId; - if (customSessionId != null && !customSessionId.isEmpty()) { - if (sessionManager.getSessionWithNotActive(customSessionId) != null) { + if (sessionProperties.customSessionId() != null && !sessionProperties.customSessionId().isEmpty()) { + if (sessionManager.getSessionWithNotActive(sessionProperties.customSessionId()) != null) { return new ResponseEntity<>(HttpStatus.CONFLICT); } - sessionId = customSessionId; + sessionId = sessionProperties.customSessionId(); } else { sessionId = IdentifierPrefixes.SESSION_ID + RandomStringUtils.randomAlphabetic(1).toUpperCase() + RandomStringUtils.randomAlphanumeric(9); @@ -190,28 +128,27 @@ public class SessionRestController { Session sessionNotActive = sessionManager.storeSessionNotActive(sessionId, sessionProperties); log.info("New session {} initialized {}", sessionId, this.sessionManager.getSessionsWithNotActive().stream() .map(Session::getSessionId).collect(Collectors.toList()).toString()); - JsonObject responseJson = sessionNotActive.toJson(); - responseJson.remove("sessionId"); - responseJson.addProperty("id", sessionNotActive.getSessionId()); - return new ResponseEntity<>(responseJson.toString(), getResponseHeaders(), HttpStatus.OK); + + return new ResponseEntity<>(sessionNotActive.toJson(false, false).toString(), RestUtils.getResponseHeaders(), + HttpStatus.OK); } @RequestMapping(value = "/sessions/{sessionId}", method = RequestMethod.GET) public ResponseEntity getSession(@PathVariable("sessionId") String sessionId, + @RequestParam(value = "pendingConnections", defaultValue = "false", required = false) boolean pendingConnections, @RequestParam(value = "webRtcStats", defaultValue = "false", required = false) boolean webRtcStats) { - log.info("REST API: GET /api/sessions/{}", sessionId); + log.info("REST API: GET {}/sessions/{}", RequestMappings.API, sessionId); Session session = this.sessionManager.getSession(sessionId); if (session != null) { - JsonObject response = (webRtcStats == true) ? session.withStatsToJson() : session.toJson(); - return new ResponseEntity<>(response.toString(), getResponseHeaders(), HttpStatus.OK); + JsonObject response = session.toJson(pendingConnections, webRtcStats); + return new ResponseEntity<>(response.toString(), RestUtils.getResponseHeaders(), HttpStatus.OK); } else { Session sessionNotActive = this.sessionManager.getSessionNotActive(sessionId); if (sessionNotActive != null) { - JsonObject response = (webRtcStats == true) ? sessionNotActive.withStatsToJson() - : sessionNotActive.toJson(); - return new ResponseEntity<>(response.toString(), getResponseHeaders(), HttpStatus.OK); + JsonObject response = sessionNotActive.toJson(pendingConnections, webRtcStats); + return new ResponseEntity<>(response.toString(), RestUtils.getResponseHeaders(), HttpStatus.OK); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } @@ -220,26 +157,27 @@ public class SessionRestController { @RequestMapping(value = "/sessions", method = RequestMethod.GET) public ResponseEntity listSessions( + @RequestParam(value = "pendingConnections", defaultValue = "false", required = false) boolean pendingConnections, @RequestParam(value = "webRtcStats", defaultValue = "false", required = false) boolean webRtcStats) { - log.info("REST API: GET /api/sessions?webRtcStats={}", webRtcStats); + log.info("REST API: GET {}/sessions", RequestMappings.API); Collection sessions = this.sessionManager.getSessionsWithNotActive(); JsonObject json = new JsonObject(); JsonArray jsonArray = new JsonArray(); - sessions.forEach(s -> { - JsonObject sessionJson = (webRtcStats == true) ? s.withStatsToJson() : s.toJson(); + sessions.forEach(session -> { + JsonObject sessionJson = session.toJson(pendingConnections, webRtcStats); jsonArray.add(sessionJson); }); json.addProperty("numberOfElements", sessions.size()); json.add("content", jsonArray); - return new ResponseEntity<>(json.toString(), getResponseHeaders(), HttpStatus.OK); + return new ResponseEntity<>(json.toString(), RestUtils.getResponseHeaders(), HttpStatus.OK); } @RequestMapping(value = "/sessions/{sessionId}", method = RequestMethod.DELETE) public ResponseEntity closeSession(@PathVariable("sessionId") String sessionId) { - log.info("REST API: DELETE /api/sessions/{}", sessionId); + log.info("REST API: DELETE {}/sessions/{}", RequestMappings.API, sessionId); Session session = this.sessionManager.getSession(sessionId); if (session != null) { @@ -263,26 +201,102 @@ public class SessionRestController { } } else { String errorMsg = "Timeout waiting for Session " + sessionId - + " closing lock to be available for closing from DELETE /api/sessions"; + + " closing lock to be available for closing from DELETE " + RequestMappings.API + + "/sessions"; log.error(errorMsg); - return this.generateErrorResponse(errorMsg, "/api/sessions", HttpStatus.BAD_REQUEST); + return this.generateErrorResponse(errorMsg, "/sessions", HttpStatus.BAD_REQUEST); } } catch (InterruptedException e) { String errorMsg = "InterruptedException while waiting for Session " + sessionId - + " closing lock to be available for closing from DELETE /api/sessions"; + + " closing lock to be available for closing from DELETE " + RequestMappings.API + "/sessions"; log.error(errorMsg); - return this.generateErrorResponse(errorMsg, "/api/sessions", HttpStatus.BAD_REQUEST); + return this.generateErrorResponse(errorMsg, "/sessions", HttpStatus.BAD_REQUEST); } } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } } + @RequestMapping(value = "/sessions/{sessionId}/connection", method = RequestMethod.POST) + public ResponseEntity initializeConnection(@PathVariable("sessionId") String sessionId, + @RequestBody Map params) { + + log.info("REST API: POST {} {}", RequestMappings.API + "/sessions/" + sessionId + "/connection", + params.toString()); + + Session session = this.sessionManager.getSessionWithNotActive(sessionId); + if (session == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + ConnectionProperties connectionProperties; + try { + connectionProperties = getConnectionPropertiesFromParams(params).build(); + } catch (Exception e) { + return this.generateErrorResponse(e.getMessage(), "/sessions/" + sessionId + "/connection", + HttpStatus.BAD_REQUEST); + } + switch (connectionProperties.getType()) { + case WEBRTC: + return this.newWebrtcConnection(session, connectionProperties); + case IPCAM: + return this.newIpcamConnection(session, connectionProperties); + default: + return this.generateErrorResponse("Wrong type parameter", "/sessions/" + sessionId + "/connection", + HttpStatus.BAD_REQUEST); + } + } + + @RequestMapping(value = "/sessions/{sessionId}/connection/{connectionId}", method = RequestMethod.GET) + public ResponseEntity getConnection(@PathVariable("sessionId") String sessionId, + @PathVariable("connectionId") String connectionId) { + + log.info("REST API: GET {}/sessions/{}/connection/{}", RequestMappings.API, sessionId, connectionId); + + Session session = this.sessionManager.getSessionWithNotActive(sessionId); + if (session != null) { + Participant p = session.getParticipantByPublicId(connectionId); + if (p != null) { + return new ResponseEntity<>(p.toJson().toString(), RestUtils.getResponseHeaders(), HttpStatus.OK); + } else { + Token t = getTokenFromConnectionId(connectionId, session.getTokenIterator()); + if (t != null) { + return new ResponseEntity<>(t.toJsonAsParticipant().toString(), RestUtils.getResponseHeaders(), + HttpStatus.OK); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + } else { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + } + + @RequestMapping(value = "/sessions/{sessionId}/connection", method = RequestMethod.GET) + public ResponseEntity listConnections(@PathVariable("sessionId") String sessionId, + @RequestParam(value = "pendingConnections", defaultValue = "true", required = false) boolean pendingConnections, + @RequestParam(value = "webRtcStats", defaultValue = "false", required = false) boolean webRtcStats) { + + log.info("REST API: GET {}/sessions/{}/connection", RequestMappings.API, sessionId); + + Session session = this.sessionManager.getSessionWithNotActive(sessionId); + + if (session != null) { + JsonObject json = new JsonObject(); + JsonArray jsonArray = session.getSnapshotOfConnectionsAsJsonArray(pendingConnections, webRtcStats); + json.addProperty("numberOfElements", jsonArray.size()); + json.add("content", jsonArray); + return new ResponseEntity<>(json.toString(), RestUtils.getResponseHeaders(), HttpStatus.OK); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + @RequestMapping(value = "/sessions/{sessionId}/connection/{connectionId}", method = RequestMethod.DELETE) - public ResponseEntity disconnectParticipant(@PathVariable("sessionId") String sessionId, + public ResponseEntity closeConnection(@PathVariable("sessionId") String sessionId, @PathVariable("connectionId") String participantPublicId) { - log.info("REST API: DELETE /api/sessions/{}/connection/{}", sessionId, participantPublicId); + log.info("REST API: DELETE {}/sessions/{}/connection/{}", RequestMappings.API, sessionId, participantPublicId); Session session = this.sessionManager.getSessionWithNotActive(sessionId); if (session == null) { @@ -294,173 +308,24 @@ public class SessionRestController { this.sessionManager.evictParticipant(participant, null, null, EndReason.forceDisconnectByServer); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } else { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - } - - @RequestMapping(value = "/sessions/{sessionId}/stream/{streamId}", method = RequestMethod.DELETE) - public ResponseEntity unpublishStream(@PathVariable("sessionId") String sessionId, - @PathVariable("streamId") String streamId) { - - log.info("REST API: DELETE /api/sessions/{}/stream/{}", sessionId, streamId); - - Session session = this.sessionManager.getSessionWithNotActive(sessionId); - if (session == null) { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); - } - - session = this.sessionManager.getSession(sessionId); - if (session != null) { - - final String participantPrivateId = this.sessionManager.getParticipantPrivateIdFromStreamId(sessionId, - streamId); - - if (participantPrivateId == null) { + // Try to delete unused token + if (session.deleteTokenFromConnectionId(participantPublicId)) { + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } - - Participant participant = this.sessionManager.getParticipant(participantPrivateId); - if (participant.isIpcam()) { - return new ResponseEntity<>(HttpStatus.METHOD_NOT_ALLOWED); - } - - this.sessionManager.unpublishStream(session, streamId, null, null, EndReason.forceUnpublishByServer); - return new ResponseEntity<>(HttpStatus.NO_CONTENT); - } else { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - } - - @RequestMapping(value = "/tokens", method = RequestMethod.POST) - public ResponseEntity newToken(@RequestBody Map params) { - - if (params == null) { - return this.generateErrorResponse("Error in body parameters. Cannot be empty", "/api/tokens", - HttpStatus.BAD_REQUEST); - } - - log.info("REST API: POST /api/tokens {}", params.toString()); - - String sessionId; - String roleString; - String metadata; - try { - sessionId = (String) params.get("session"); - roleString = (String) params.get("role"); - metadata = (String) params.get("data"); - } catch (ClassCastException e) { - return this.generateErrorResponse("Type error in some parameter", "/api/tokens", HttpStatus.BAD_REQUEST); - } - - if (sessionId == null) { - return this.generateErrorResponse("\"session\" parameter is mandatory", "/api/tokens", - HttpStatus.BAD_REQUEST); - } - - final Session session = this.sessionManager.getSessionWithNotActive(sessionId); - if (session == null) { - return this.generateErrorResponse("Session " + sessionId + " not found", "/api/tokens", - HttpStatus.NOT_FOUND); - } - - JsonObject kurentoOptions = null; - - if (params.get("kurentoOptions") != null) { - try { - kurentoOptions = JsonParser.parseString(params.get("kurentoOptions").toString()).getAsJsonObject(); - } catch (Exception e) { - return this.generateErrorResponse("Error in parameter 'kurentoOptions'. It is not a valid JSON object", - "/api/tokens", HttpStatus.BAD_REQUEST); - } - } - - OpenViduRole role; - try { - if (roleString != null) { - role = OpenViduRole.valueOf(roleString); - } else { - role = OpenViduRole.PUBLISHER; - } - } catch (IllegalArgumentException e) { - return this.generateErrorResponse("Parameter role " + params.get("role") + " is not defined", "/api/tokens", - HttpStatus.BAD_REQUEST); - } - - KurentoTokenOptions kurentoTokenOptions = null; - if (kurentoOptions != null) { - try { - kurentoTokenOptions = new KurentoTokenOptions(kurentoOptions); - } catch (Exception e) { - return this.generateErrorResponse("Type error in some parameter of 'kurentoOptions'", "/api/tokens", - HttpStatus.BAD_REQUEST); - } - } - - metadata = (metadata != null) ? metadata : ""; - - // While closing a session tokens can't be generated - if (session.closingLock.readLock().tryLock()) { - try { - String token = sessionManager.newToken(session, role, metadata, kurentoTokenOptions); - - JsonObject responseJson = new JsonObject(); - responseJson.addProperty("id", token); - responseJson.addProperty("session", sessionId); - responseJson.addProperty("role", role.toString()); - responseJson.addProperty("data", metadata); - responseJson.addProperty("token", token); - - if (kurentoOptions != null) { - JsonObject kurentoOptsResponse = new JsonObject(); - if (kurentoTokenOptions.getVideoMaxRecvBandwidth() != null) { - kurentoOptsResponse.addProperty("videoMaxRecvBandwidth", - kurentoTokenOptions.getVideoMaxRecvBandwidth()); - } - if (kurentoTokenOptions.getVideoMinRecvBandwidth() != null) { - kurentoOptsResponse.addProperty("videoMinRecvBandwidth", - kurentoTokenOptions.getVideoMinRecvBandwidth()); - } - if (kurentoTokenOptions.getVideoMaxSendBandwidth() != null) { - kurentoOptsResponse.addProperty("videoMaxSendBandwidth", - kurentoTokenOptions.getVideoMaxSendBandwidth()); - } - if (kurentoTokenOptions.getVideoMinSendBandwidth() != null) { - kurentoOptsResponse.addProperty("videoMinSendBandwidth", - kurentoTokenOptions.getVideoMinSendBandwidth()); - } - if (kurentoTokenOptions.getAllowedFilters().length > 0) { - JsonArray filters = new JsonArray(); - for (String filter : kurentoTokenOptions.getAllowedFilters()) { - filters.add(filter); - } - kurentoOptsResponse.add("allowedFilters", filters); - } - responseJson.add("kurentoOptions", kurentoOptsResponse); - } - return new ResponseEntity<>(responseJson.toString(), getResponseHeaders(), HttpStatus.OK); - } catch (Exception e) { - return this.generateErrorResponse( - "Error generating token for session " + sessionId + ": " + e.getMessage(), "/api/tokens", - HttpStatus.INTERNAL_SERVER_ERROR); - } finally { - session.closingLock.readLock().unlock(); - } - } else { - log.error("Session {} is in the process of closing. Token couldn't be generated", sessionId); - return this.generateErrorResponse("Session " + sessionId + " not found", "/api/tokens", - HttpStatus.NOT_FOUND); } } @RequestMapping(value = "/recordings/start", method = RequestMethod.POST) - public ResponseEntity startRecordingSession(@RequestBody Map params) { + public ResponseEntity startRecording(@RequestBody Map params) { if (params == null) { - return this.generateErrorResponse("Error in body parameters. Cannot be empty", "/api/recordings/start", + return this.generateErrorResponse("Error in body parameters. Cannot be empty", "/recordings/start", HttpStatus.BAD_REQUEST); } - log.info("REST API: POST /api/recordings/start {}", params.toString()); + log.info("REST API: POST {}/recordings/start {}", RequestMappings.API, params.toString()); if (!this.openviduConfig.isRecordingModuleEnabled()) { // OpenVidu Server configuration property "OPENVIDU_RECORDING" is set to false @@ -468,69 +333,19 @@ public class SessionRestController { } String sessionId; - String name; - String outputModeString; - String resolution; - Boolean hasAudio; - Boolean hasVideo; - String recordingLayoutString; - String customLayout; - Long shmSize = null; try { sessionId = (String) params.get("session"); - name = (String) params.get("name"); - outputModeString = (String) params.get("outputMode"); - resolution = (String) params.get("resolution"); - hasAudio = (Boolean) params.get("hasAudio"); - hasVideo = (Boolean) params.get("hasVideo"); - recordingLayoutString = (String) params.get("recordingLayout"); - customLayout = (String) params.get("customLayout"); - if (params.get("shmSize") != null) { - shmSize = new Long(params.get("shmSize").toString()); - } - } catch (ClassCastException | NumberFormatException e) { - return this.generateErrorResponse("Type error in some parameter", "/api/recordings/start", + } catch (Exception e) { + return this.generateErrorResponse("Type error in parameter \"session\"", "/recordings/start", HttpStatus.BAD_REQUEST); } if (sessionId == null) { // "session" parameter not found - return this.generateErrorResponse("\"session\" parameter is mandatory", "/api/recordings/start", + return this.generateErrorResponse("\"session\" parameter is mandatory", "/recordings/start", HttpStatus.BAD_REQUEST); } - OutputMode finalOutputMode = OutputMode.COMPOSED; - RecordingLayout recordingLayout = null; - if (outputModeString != null && !outputModeString.isEmpty()) { - try { - finalOutputMode = OutputMode.valueOf(outputModeString); - } catch (Exception e) { - return this.generateErrorResponse("Type error in some parameter", "/api/recordings/start", - HttpStatus.BAD_REQUEST); - } - } - if (RecordingUtils.IS_COMPOSED(finalOutputMode)) { - if (resolution != null && !sessionManager.formatChecker.isAcceptableRecordingResolution(resolution)) { - return this.generateErrorResponse( - "Wrong \"resolution\" parameter. Acceptable values from 100 to 1999 for both width and height", - "/api/recordings/start", HttpStatus.UNPROCESSABLE_ENTITY); - } - if (recordingLayoutString != null && !recordingLayoutString.isEmpty()) { - try { - recordingLayout = RecordingLayout.valueOf(recordingLayoutString); - } catch (Exception e) { - return this.generateErrorResponse("Type error in some parameter", "/api/recordings/start", - HttpStatus.BAD_REQUEST); - } - } - } - if ((hasAudio != null && hasVideo != null) && !hasAudio && !hasVideo) { - // Cannot start a recording with both "hasAudio" and "hasVideo" to false - return this.generateErrorResponse( - "Cannot start a recording with both \"hasAudio\" and \"hasVideo\" set to false", - "/api/recordings/start", HttpStatus.UNPROCESSABLE_ENTITY); - } - Session session = sessionManager.getSession(sessionId); if (session == null) { session = sessionManager.getSessionNotActive(sessionId); @@ -557,52 +372,37 @@ public class SessionRestController { return new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE); } - // If outputMode is COMPOSED when defaultOutputMode is COMPOSED_QUICK_START, - // change outputMode to COMPOSED_QUICK_START (and vice versa) - OutputMode defaultOutputMode = session.getSessionProperties().defaultOutputMode(); - if (OutputMode.COMPOSED_QUICK_START.equals(defaultOutputMode) && OutputMode.COMPOSED.equals(finalOutputMode)) { - finalOutputMode = OutputMode.COMPOSED_QUICK_START; - } else if (OutputMode.COMPOSED.equals(defaultOutputMode) - && OutputMode.COMPOSED_QUICK_START.equals(finalOutputMode)) { - finalOutputMode = OutputMode.COMPOSED; + RecordingProperties recordingProperties; + try { + recordingProperties = getRecordingPropertiesFromParams(params, session).build(); + } catch (RuntimeException e) { + return this.generateErrorResponse(e.getMessage(), "/sessions", HttpStatus.UNPROCESSABLE_ENTITY); + } catch (Exception e) { + return this.generateErrorResponse(e.getMessage(), "/sessions", HttpStatus.BAD_REQUEST); } - RecordingProperties.Builder builder = new RecordingProperties.Builder(); - builder.outputMode( - finalOutputMode == null ? session.getSessionProperties().defaultOutputMode() : finalOutputMode); - if (RecordingUtils.IS_COMPOSED(finalOutputMode)) { - if (resolution != null) { - builder.resolution(resolution); - } - builder.recordingLayout(recordingLayout == null ? session.getSessionProperties().defaultRecordingLayout() - : recordingLayout); - if (RecordingLayout.CUSTOM.equals(recordingLayout)) { - builder.customLayout( - customLayout == null ? session.getSessionProperties().defaultCustomLayout() : customLayout); - } - if (shmSize != null) { - if (shmSize < 134217728L) { - return this.generateErrorResponse("Wrong \"shmSize\" parameter. Must be 134217728 (128 MB) minimum", - "/api/recordings/start", HttpStatus.UNPROCESSABLE_ENTITY); - } - builder.shmSize(shmSize); - } - } - builder.name(name).hasAudio(hasAudio != null ? hasAudio : true).hasVideo(hasVideo != null ? hasVideo : true); - try { - Recording startedRecording = this.recordingManager.startRecording(session, builder.build()); - return new ResponseEntity<>(startedRecording.toJson().toString(), getResponseHeaders(), HttpStatus.OK); + Recording startedRecording = this.recordingManager.startRecording(session, recordingProperties); + return new ResponseEntity<>(startedRecording.toJson().toString(), RestUtils.getResponseHeaders(), + HttpStatus.OK); } catch (OpenViduException e) { - return new ResponseEntity<>("Error starting recording: " + e.getMessage(), getResponseHeaders(), - HttpStatus.INTERNAL_SERVER_ERROR); + HttpStatus status = e.getCodeValue() == Code.MEDIA_NODE_STATUS_WRONG.getValue() + ? HttpStatus.SERVICE_UNAVAILABLE + : HttpStatus.INTERNAL_SERVER_ERROR; + return new ResponseEntity<>("Error starting recording: " + e.getMessage(), RestUtils.getResponseHeaders(), + status); } } @RequestMapping(value = "/recordings/stop/{recordingId}", method = RequestMethod.POST) - public ResponseEntity stopRecordingSession(@PathVariable("recordingId") String recordingId) { + public ResponseEntity stopRecording(@PathVariable("recordingId") String recordingId) { - log.info("REST API: POST /api/recordings/stop/{}", recordingId); + log.info("REST API: POST {}/recordings/stop/{}", RequestMappings.API, recordingId); + + if (!this.openviduConfig.isRecordingModuleEnabled()) { + // OpenVidu Server configuration property "OPENVIDU_RECORDING" is set to false + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } Recording recording = recordingManager.getStartedRecording(recordingId); @@ -621,8 +421,13 @@ public class SessionRestController { Session session = sessionManager.getSession(recording.getSessionId()); - Recording stoppedRecording = this.recordingManager.stopRecording(session, recording.getId(), - EndReason.recordingStoppedByServer); + Recording stoppedRecording; + try { + stoppedRecording = this.recordingManager.stopRecording(session, recording.getId(), + EndReason.recordingStoppedByServer); + } catch (Exception e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } session.recordingManuallyStopped.set(true); @@ -632,13 +437,19 @@ public class SessionRestController { session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID), null, null, null); } - return new ResponseEntity<>(stoppedRecording.toJson().toString(), getResponseHeaders(), HttpStatus.OK); + return new ResponseEntity<>(stoppedRecording.toJson().toString(), RestUtils.getResponseHeaders(), + HttpStatus.OK); } @RequestMapping(value = "/recordings/{recordingId}", method = RequestMethod.GET) public ResponseEntity getRecording(@PathVariable("recordingId") String recordingId) { - log.info("REST API: GET /api/recordings/{}", recordingId); + log.info("REST API: GET {}/recordings/{}", RequestMappings.API, recordingId); + + if (!this.openviduConfig.isRecordingModuleEnabled()) { + // OpenVidu Server configuration property "OPENVIDU_RECORDING" is set to false + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } try { Recording recording = this.recordingManager.getRecording(recordingId); @@ -646,16 +457,21 @@ public class SessionRestController { && recordingManager.getStartingRecording(recording.getId()) != null) { recording.setStatus(io.openvidu.java.client.Recording.Status.starting); } - return new ResponseEntity<>(recording.toJson().toString(), getResponseHeaders(), HttpStatus.OK); + return new ResponseEntity<>(recording.toJson().toString(), RestUtils.getResponseHeaders(), HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } } @RequestMapping(value = "/recordings", method = RequestMethod.GET) - public ResponseEntity getAllRecordings() { + public ResponseEntity listRecordings() { - log.info("REST API: GET /api/recordings"); + log.info("REST API: GET {}/recordings", RequestMappings.API); + + if (!this.openviduConfig.isRecordingModuleEnabled()) { + // OpenVidu Server configuration property "OPENVIDU_RECORDING" is set to false + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } Collection recordings = this.recordingManager.getAllRecordings(); JsonObject json = new JsonObject(); @@ -669,26 +485,114 @@ public class SessionRestController { }); json.addProperty("count", recordings.size()); json.add("items", jsonArray); - return new ResponseEntity<>(json.toString(), getResponseHeaders(), HttpStatus.OK); + return new ResponseEntity<>(json.toString(), RestUtils.getResponseHeaders(), HttpStatus.OK); } @RequestMapping(value = "/recordings/{recordingId}", method = RequestMethod.DELETE) public ResponseEntity deleteRecording(@PathVariable("recordingId") String recordingId) { - log.info("REST API: DELETE /api/recordings/{}", recordingId); + log.info("REST API: DELETE {}/recordings/{}", RequestMappings.API, recordingId); + + if (!this.openviduConfig.isRecordingModuleEnabled()) { + // OpenVidu Server configuration property "OPENVIDU_RECORDING" is set to false + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } return new ResponseEntity<>(this.recordingManager.deleteRecordingFromHost(recordingId, false)); } + @RequestMapping(value = "/tokens", method = RequestMethod.POST) + public ResponseEntity newToken(@RequestBody Map params) { + + if (params == null) { + return this.generateErrorResponse("Error in body parameters. Cannot be empty", "/tokens", + HttpStatus.BAD_REQUEST); + } + + log.info("REST API: POST {}/tokens {}", RequestMappings.API, params.toString()); + + String sessionId; + try { + sessionId = (String) params.get("session"); + } catch (ClassCastException e) { + return this.generateErrorResponse("Type error in some parameter", "/tokens", HttpStatus.BAD_REQUEST); + } + + if (sessionId == null) { + return this.generateErrorResponse("\"session\" parameter is mandatory", "/tokens", HttpStatus.BAD_REQUEST); + } + + log.warn("Token API is deprecated. Use Connection API instead (POST {}/sessions/{}/connection)", + RequestMappings.API, sessionId); + + final Session session = this.sessionManager.getSessionWithNotActive(sessionId); + if (session == null) { + return this.generateErrorResponse("Session " + sessionId + " not found", "/tokens", HttpStatus.NOT_FOUND); + } + + ConnectionProperties connectionProperties; + params.remove("record"); + try { + connectionProperties = getConnectionPropertiesFromParams(params).build(); + } catch (Exception e) { + return this.generateErrorResponse(e.getMessage(), "/sessions/" + sessionId + "/connection", + HttpStatus.BAD_REQUEST); + } + ResponseEntity entity = this.newWebrtcConnection(session, connectionProperties); + JsonObject jsonResponse = JsonParser.parseString(entity.getBody().toString()).getAsJsonObject(); + + if (jsonResponse.has("error")) { + return this.generateErrorResponse(jsonResponse.get("message").getAsString(), "/tokens", + HttpStatus.valueOf(jsonResponse.get("status").getAsInt())); + } else { + String connectionId = jsonResponse.get("id").getAsString(); + Token token = getTokenFromConnectionId(connectionId, session.getTokenIterator()); + return new ResponseEntity<>(token.toJson().toString(), RestUtils.getResponseHeaders(), HttpStatus.OK); + } + } + + @RequestMapping(value = "/sessions/{sessionId}/stream/{streamId}", method = RequestMethod.DELETE) + public ResponseEntity unpublishStream(@PathVariable("sessionId") String sessionId, + @PathVariable("streamId") String streamId) { + + log.info("REST API: DELETE {}/sessions/{}/stream/{}", RequestMappings.API, sessionId, streamId); + + Session session = this.sessionManager.getSessionWithNotActive(sessionId); + if (session == null) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + session = this.sessionManager.getSession(sessionId); + if (session != null) { + + final String participantPrivateId = this.sessionManager.getParticipantPrivateIdFromStreamId(sessionId, + streamId); + + if (participantPrivateId == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + Participant participant = this.sessionManager.getParticipant(participantPrivateId); + if (participant.isIpcam()) { + return new ResponseEntity<>(HttpStatus.METHOD_NOT_ALLOWED); + } + + this.sessionManager.unpublishStream(session, streamId, null, null, EndReason.forceUnpublishByServer); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + @RequestMapping(value = "/signal", method = RequestMethod.POST) public ResponseEntity signal(@RequestBody Map params) { if (params == null) { - return this.generateErrorResponse("Error in body parameters. Cannot be empty", "/api/signal", + return this.generateErrorResponse("Error in body parameters. Cannot be empty", "/signal", HttpStatus.BAD_REQUEST); } - log.info("REST API: POST /api/signal {}", params.toString()); + log.info("REST API: POST {}/signal {}", RequestMappings.API, params.toString()); String sessionId; String type; @@ -700,15 +604,14 @@ public class SessionRestController { type = (String) params.get("type"); data = (String) params.get("data"); } catch (ClassCastException e) { - return this.generateErrorResponse("Type error in some parameter", "/api/signal", HttpStatus.BAD_REQUEST); + return this.generateErrorResponse("Type error in some parameter", "/signal", HttpStatus.BAD_REQUEST); } JsonObject completeMessage = new JsonObject(); if (sessionId == null) { // "session" parameter not found - return this.generateErrorResponse("\"session\" parameter is mandatory", "/api/signal", - HttpStatus.BAD_REQUEST); + return this.generateErrorResponse("\"session\" parameter is mandatory", "/signal", HttpStatus.BAD_REQUEST); } Session session = sessionManager.getSession(sessionId); if (session == null) { @@ -735,7 +638,7 @@ public class SessionRestController { JsonArray toArray = gson.toJsonTree(to).getAsJsonArray(); completeMessage.add("to", toArray); } catch (IllegalStateException exception) { - return this.generateErrorResponse("\"to\" parameter is not a valid JSON array", "/api/signal", + return this.generateErrorResponse("\"to\" parameter is not a valid JSON array", "/signal", HttpStatus.BAD_REQUEST); } } @@ -743,63 +646,55 @@ public class SessionRestController { try { sessionManager.sendMessage(completeMessage.toString(), sessionId); } catch (OpenViduException e) { - return this.generateErrorResponse("\"to\" array has no valid connection identifiers", "/api/signal", + return this.generateErrorResponse("\"to\" array has no valid connection identifiers", "/signal", HttpStatus.NOT_ACCEPTABLE); } return new ResponseEntity<>(HttpStatus.OK); } - @RequestMapping(value = "/sessions/{sessionId}/connection", method = RequestMethod.POST) - public ResponseEntity publishIpcam(@PathVariable("sessionId") String sessionId, @RequestBody Map params) { + protected ResponseEntity newWebrtcConnection(Session session, ConnectionProperties connectionProperties) { - if (params == null) { - return this.generateErrorResponse("Error in body parameters. Cannot be empty", - "/api/sessions/" + sessionId + "/connection", HttpStatus.BAD_REQUEST); + final String REQUEST_PATH = "/sessions/" + session.getSessionId() + "/connection"; + + // While closing a session tokens can't be generated + if (session.closingLock.readLock().tryLock()) { + try { + Token token = sessionManager.newToken(session, connectionProperties.getRole(), + connectionProperties.getData(), connectionProperties.record(), + connectionProperties.getKurentoOptions()); + return new ResponseEntity<>(token.toJsonAsParticipant().toString(), RestUtils.getResponseHeaders(), + HttpStatus.OK); + } catch (Exception e) { + return this.generateErrorResponse( + "Error creating Connection for session " + session.getSessionId() + ": " + e.getMessage(), + REQUEST_PATH, HttpStatus.INTERNAL_SERVER_ERROR); + } finally { + session.closingLock.readLock().unlock(); + } + } else { + log.error("Session {} is in the process of closing. Connection couldn't be created", + session.getSessionId()); + return this.generateErrorResponse("Session " + session.getSessionId() + " not found", REQUEST_PATH, + HttpStatus.NOT_FOUND); } + } - log.info("REST API: POST /api/sessions/{}/connection {}", sessionId, params.toString()); + protected ResponseEntity newIpcamConnection(Session session, ConnectionProperties connectionProperties) { - Session session = this.sessionManager.getSessionWithNotActive(sessionId); - if (session == null) { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - - String type; - String rtspUri; - Boolean adaptativeBitrate; - Boolean onlyPlayWithSubscribers; - String data; - try { - type = (String) params.get("type"); - rtspUri = (String) params.get("rtspUri"); - adaptativeBitrate = (Boolean) params.get("adaptativeBitrate"); - onlyPlayWithSubscribers = (Boolean) params.get("onlyPlayWithSubscribers"); - data = (String) params.get("data"); - } catch (ClassCastException e) { - return this.generateErrorResponse("Type error in some parameter", - "/api/sessions/" + sessionId + "/connection", HttpStatus.BAD_REQUEST); - } - if (rtspUri == null) { - return this.generateErrorResponse("\"rtspUri\" parameter is mandatory", - "/api/sessions/" + sessionId + "/connection", HttpStatus.BAD_REQUEST); - } - - type = "IPCAM"; // Other possible values in the future - adaptativeBitrate = adaptativeBitrate != null ? adaptativeBitrate : true; - onlyPlayWithSubscribers = onlyPlayWithSubscribers != null ? onlyPlayWithSubscribers : true; - data = data != null ? data : ""; + final String REQUEST_PATH = "/sessions/" + session.getSessionId() + "/connection"; boolean hasAudio = true; boolean hasVideo = true; boolean audioActive = true; boolean videoActive = true; - String typeOfVideo = type; + String typeOfVideo = ConnectionType.IPCAM.name(); Integer frameRate = null; String videoDimensions = null; KurentoMediaOptions mediaOptions = new KurentoMediaOptions(true, null, hasAudio, hasVideo, audioActive, - videoActive, typeOfVideo, frameRate, videoDimensions, null, false, rtspUri, adaptativeBitrate, - onlyPlayWithSubscribers); + videoActive, typeOfVideo, frameRate, videoDimensions, null, false, connectionProperties.getRtspUri(), + connectionProperties.adaptativeBitrate(), connectionProperties.onlyPlayWithSubscribers(), + connectionProperties.getNetworkCache()); // While closing a session IP cameras can't be published if (session.closingLock.readLock().tryLock()) { @@ -807,14 +702,15 @@ public class SessionRestController { if (session.isClosed()) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } - Participant ipcamParticipant = this.sessionManager.publishIpcam(session, mediaOptions, data); - return new ResponseEntity<>(ipcamParticipant.toJson().toString(), getResponseHeaders(), HttpStatus.OK); + Participant ipcamParticipant = this.sessionManager.publishIpcam(session, mediaOptions, + connectionProperties); + return new ResponseEntity<>(ipcamParticipant.toJson().toString(), RestUtils.getResponseHeaders(), + HttpStatus.OK); } catch (MalformedURLException e) { - return this.generateErrorResponse("\"rtspUri\" parameter is not a valid rtsp uri", - "/api/sessions/" + sessionId + "/connection", HttpStatus.BAD_REQUEST); + return this.generateErrorResponse("\"rtspUri\" parameter is not a valid rtsp uri", REQUEST_PATH, + HttpStatus.BAD_REQUEST); } catch (Exception e) { - return this.generateErrorResponse(e.getMessage(), "/api/sessions/" + sessionId + "/connection", - HttpStatus.INTERNAL_SERVER_ERROR); + return this.generateErrorResponse(e.getMessage(), REQUEST_PATH, HttpStatus.INTERNAL_SERVER_ERROR); } finally { session.closingLock.readLock().unlock(); } @@ -823,19 +719,330 @@ public class SessionRestController { } } - private ResponseEntity generateErrorResponse(String errorMessage, String path, HttpStatus status) { + protected SessionProperties.Builder getSessionPropertiesFromParams(Map params) throws Exception { + + SessionProperties.Builder builder = new SessionProperties.Builder(); + String customSessionId = null; + + if (params != null) { + + String mediaModeString; + String recordingModeString; + String defaultOutputModeString; + String defaultRecordingLayoutString; + String defaultCustomLayout; + String forcedVideoCodec; + Boolean allowTranscoding; + try { + mediaModeString = (String) params.get("mediaMode"); + recordingModeString = (String) params.get("recordingMode"); + defaultOutputModeString = (String) params.get("defaultOutputMode"); + defaultRecordingLayoutString = (String) params.get("defaultRecordingLayout"); + defaultCustomLayout = (String) params.get("defaultCustomLayout"); + customSessionId = (String) params.get("customSessionId"); + forcedVideoCodec = (String) params.get("forcedVideoCodec"); + allowTranscoding = (Boolean) params.get("allowTranscoding"); + } catch (ClassCastException e) { + throw new Exception("Type error in some parameter: " + e.getMessage()); + } + + try { + // Safe parameter retrieval. Default values if not defined + if (recordingModeString != null) { + RecordingMode recordingMode = RecordingMode.valueOf(recordingModeString); + builder = builder.recordingMode(recordingMode); + } else { + builder = builder.recordingMode(RecordingMode.MANUAL); + } + if (defaultOutputModeString != null) { + OutputMode defaultOutputMode = OutputMode.valueOf(defaultOutputModeString); + builder = builder.defaultOutputMode(defaultOutputMode); + } else { + builder.defaultOutputMode(OutputMode.COMPOSED); + } + if (defaultRecordingLayoutString != null) { + RecordingLayout defaultRecordingLayout = RecordingLayout.valueOf(defaultRecordingLayoutString); + builder = builder.defaultRecordingLayout(defaultRecordingLayout); + } else { + builder.defaultRecordingLayout(RecordingLayout.BEST_FIT); + } + if (defaultCustomLayout != null) { + builder.defaultCustomLayout(defaultCustomLayout); + } else { + builder.defaultCustomLayout(""); + } + if (mediaModeString != null) { + MediaMode mediaMode = MediaMode.valueOf(mediaModeString); + builder = builder.mediaMode(mediaMode); + } else { + builder = builder.mediaMode(MediaMode.ROUTED); + } + if (customSessionId != null && !customSessionId.isEmpty()) { + if (!sessionManager.formatChecker.isValidCustomSessionId(customSessionId)) { + throw new Exception( + "Parameter 'customSessionId' is wrong. Must be an alphanumeric string [a-zA-Z0-9_-]"); + } + builder = builder.customSessionId(customSessionId); + } + if (forcedVideoCodec != null) { + builder = builder.forcedVideoCodec(VideoCodec.valueOf(forcedVideoCodec)); + } else { + builder = builder.forcedVideoCodec(openviduConfig.getOpenviduForcedCodec()); + } + if (allowTranscoding != null) { + builder = builder.allowTranscoding(allowTranscoding); + } else { + builder = builder.allowTranscoding(openviduConfig.isOpenviduAllowingTranscoding()); + } + + } catch (IllegalArgumentException e) { + throw new Exception("RecordingMode " + params.get("recordingMode") + " | " + "Default OutputMode " + + params.get("defaultOutputMode") + " | " + "Default RecordingLayout " + + params.get("defaultRecordingLayout") + " | " + "MediaMode " + params.get("mediaMode") + + ". Some parameter is not defined"); + } + } + return builder; + } + + protected ConnectionProperties.Builder getConnectionPropertiesFromParams(Map params) throws Exception { + + ConnectionProperties.Builder builder = new ConnectionProperties.Builder(); + + String typeString; + String data; + try { + typeString = (String) params.get("type"); + data = (String) params.get("data"); + } catch (ClassCastException e) { + throw new Exception("Type error in some parameter: " + e.getMessage()); + } + + ConnectionType type; + try { + if (typeString != null) { + type = ConnectionType.valueOf(typeString); + } else { + type = ConnectionType.WEBRTC; + } + } catch (IllegalArgumentException e) { + throw new Exception("Parameter 'type' " + typeString + " is not defined"); + } + data = data != null ? data : ""; + + // Build COMMON options + builder.type(type).data(data).record(true); + + OpenViduRole role = null; + KurentoOptions kurentoOptions = null; + + if (ConnectionType.WEBRTC.equals(type)) { + String roleString; + try { + roleString = (String) params.get("role"); + } catch (ClassCastException e) { + throw new Exception("Type error in parameter 'role': " + e.getMessage()); + } + try { + if (roleString != null) { + role = OpenViduRole.valueOf(roleString); + } else { + role = OpenViduRole.PUBLISHER; + } + } catch (IllegalArgumentException e) { + throw new Exception("Parameter role " + params.get("role") + " is not defined"); + } + JsonObject kurentoOptionsJson = null; + if (params.get("kurentoOptions") != null) { + try { + kurentoOptionsJson = JsonParser.parseString(params.get("kurentoOptions").toString()) + .getAsJsonObject(); + } catch (Exception e) { + throw new Exception("Error in parameter 'kurentoOptions'. It is not a valid JSON object"); + } + } + if (kurentoOptionsJson != null) { + try { + KurentoOptions.Builder builder2 = new KurentoOptions.Builder(); + if (kurentoOptionsJson.has("videoMaxRecvBandwidth")) { + builder2.videoMaxRecvBandwidth(kurentoOptionsJson.get("videoMaxRecvBandwidth").getAsInt()); + } + if (kurentoOptionsJson.has("videoMinRecvBandwidth")) { + builder2.videoMinRecvBandwidth(kurentoOptionsJson.get("videoMinRecvBandwidth").getAsInt()); + } + if (kurentoOptionsJson.has("videoMaxSendBandwidth")) { + builder2.videoMaxSendBandwidth(kurentoOptionsJson.get("videoMaxSendBandwidth").getAsInt()); + } + if (kurentoOptionsJson.has("videoMinSendBandwidth")) { + builder2.videoMinSendBandwidth(kurentoOptionsJson.get("videoMinSendBandwidth").getAsInt()); + } + if (kurentoOptionsJson.has("allowedFilters")) { + JsonArray filters = kurentoOptionsJson.get("allowedFilters").getAsJsonArray(); + String[] arrayOfFilters = new String[filters.size()]; + Iterator it = filters.iterator(); + int index = 0; + while (it.hasNext()) { + arrayOfFilters[index] = it.next().getAsString(); + index++; + } + builder2.allowedFilters(arrayOfFilters); + } + kurentoOptions = builder2.build(); + } catch (Exception e) { + throw new Exception("Type error in some parameter of 'kurentoOptions': " + e.getMessage()); + } + } + + // Build WEBRTC options + builder.role(role).kurentoOptions(kurentoOptions); + + } else if (ConnectionType.IPCAM.equals(type)) { + String rtspUri; + Boolean adaptativeBitrate; + Boolean onlyPlayWithSubscribers; + Integer networkCache; + try { + rtspUri = (String) params.get("rtspUri"); + adaptativeBitrate = (Boolean) params.get("adaptativeBitrate"); + onlyPlayWithSubscribers = (Boolean) params.get("onlyPlayWithSubscribers"); + networkCache = (Integer) params.get("networkCache"); + } catch (ClassCastException e) { + throw new Exception("Type error in some parameter: " + e.getMessage()); + } + adaptativeBitrate = adaptativeBitrate != null ? adaptativeBitrate : true; + onlyPlayWithSubscribers = onlyPlayWithSubscribers != null ? onlyPlayWithSubscribers : true; + networkCache = networkCache != null ? networkCache : 2000; + + // Build IPCAM options + builder.rtspUri(rtspUri).adaptativeBitrate(adaptativeBitrate) + .onlyPlayWithSubscribers(onlyPlayWithSubscribers).networkCache(networkCache).build(); + } + + return builder; + } + + protected RecordingProperties.Builder getRecordingPropertiesFromParams(Map params, Session session) + throws Exception { + + RecordingProperties.Builder builder = new RecordingProperties.Builder(); + + String sessionId; + String name; + String outputModeString; + String resolution; + Boolean hasAudio; + Boolean hasVideo; + String recordingLayoutString; + String customLayout; + Long shmSize = null; + try { + sessionId = (String) params.get("session"); + name = (String) params.get("name"); + outputModeString = (String) params.get("outputMode"); + resolution = (String) params.get("resolution"); + hasAudio = (Boolean) params.get("hasAudio"); + hasVideo = (Boolean) params.get("hasVideo"); + recordingLayoutString = (String) params.get("recordingLayout"); + customLayout = (String) params.get("customLayout"); + if (params.get("shmSize") != null) { + shmSize = Long.parseLong(params.get("shmSize").toString()); + } + } catch (ClassCastException | NumberFormatException e) { + throw new Exception("Type error in some parameter: " + e.getMessage()); + } + + if (sessionId == null) { + // "session" parameter not found + throw new Exception("\"session\" parameter is mandatory"); + } + + if (name != null && !name.isEmpty()) { + if (!sessionManager.formatChecker.isValidRecordingName(name)) { + throw new Exception("Parameter 'name' is wrong. Must be an alphanumeric string [a-zA-Z0-9_-]"); + } + } + + OutputMode finalOutputMode = OutputMode.COMPOSED; + RecordingLayout recordingLayout = null; + if (outputModeString != null && !outputModeString.isEmpty()) { + try { + finalOutputMode = OutputMode.valueOf(outputModeString); + } catch (Exception e) { + throw new Exception("Type error in parameter 'outputMode'"); + } + } + if (RecordingUtils.IS_COMPOSED(finalOutputMode)) { + if (resolution != null && !sessionManager.formatChecker.isAcceptableRecordingResolution(resolution)) { + throw new RuntimeException( + "Wrong 'resolution' parameter. Acceptable values from 100 to 1999 for both width and height"); + } + if (recordingLayoutString != null && !recordingLayoutString.isEmpty()) { + try { + recordingLayout = RecordingLayout.valueOf(recordingLayoutString); + } catch (Exception e) { + throw new Exception("Type error in parameter 'recordingLayout'"); + } + } + } + if ((hasAudio != null && hasVideo != null) && !hasAudio && !hasVideo) { + // Cannot start a recording with both "hasAudio" and "hasVideo" to false + throw new RuntimeException("Cannot start a recording with both \"hasAudio\" and \"hasVideo\" set to false"); + } + + // If outputMode is COMPOSED when defaultOutputMode is COMPOSED_QUICK_START, + // change outputMode to COMPOSED_QUICK_START (and vice versa) + OutputMode defaultOutputMode = session.getSessionProperties().defaultOutputMode(); + if (OutputMode.COMPOSED_QUICK_START.equals(defaultOutputMode) && OutputMode.COMPOSED.equals(finalOutputMode)) { + finalOutputMode = OutputMode.COMPOSED_QUICK_START; + } else if (OutputMode.COMPOSED.equals(defaultOutputMode) + && OutputMode.COMPOSED_QUICK_START.equals(finalOutputMode)) { + finalOutputMode = OutputMode.COMPOSED; + } + + builder.outputMode( + finalOutputMode == null ? session.getSessionProperties().defaultOutputMode() : finalOutputMode); + if (RecordingUtils.IS_COMPOSED(finalOutputMode)) { + builder.resolution(resolution != null ? resolution : "1920x1080"); // resolution == null ? + // sessionProperties.defaultRecordingResolution) + // : resolution)); + builder.recordingLayout(recordingLayout == null ? session.getSessionProperties().defaultRecordingLayout() + : recordingLayout); + if (RecordingLayout.CUSTOM.equals(recordingLayout)) { + builder.customLayout( + customLayout == null ? session.getSessionProperties().defaultCustomLayout() : customLayout); + } + if (shmSize != null) { + if (shmSize < 134217728L) { + throw new RuntimeException("Wrong \"shmSize\" parameter. Must be 134217728 (128 MB) minimum"); + } + builder.shmSize(shmSize); + } + } + builder.name(name).hasAudio(hasAudio != null ? hasAudio : true).hasVideo(hasVideo != null ? hasVideo : true); + return builder; + } + + protected Token getTokenFromConnectionId(String connectionId, Iterator> iterator) { + boolean found = false; + Token token = null; + while (iterator.hasNext() && !found) { + Token tAux = iterator.next().getValue(); + found = tAux.getConnectionId().equals(connectionId); + if (found) { + token = tAux; + } + } + return token; + } + + protected ResponseEntity generateErrorResponse(String errorMessage, String path, HttpStatus status) { JsonObject responseJson = new JsonObject(); responseJson.addProperty("timestamp", System.currentTimeMillis()); responseJson.addProperty("status", status.value()); responseJson.addProperty("error", status.getReasonPhrase()); responseJson.addProperty("message", errorMessage); - responseJson.addProperty("path", path); - return new ResponseEntity<>(responseJson.toString(), getResponseHeaders(), status); + responseJson.addProperty("path", RequestMappings.API + path); + return new ResponseEntity<>(responseJson.toString(), RestUtils.getResponseHeaders(), status); } - private HttpHeaders getResponseHeaders() { - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.setContentType(MediaType.APPLICATION_JSON); - return responseHeaders; - } } diff --git a/openvidu-server/src/main/java/io/openvidu/server/rpc/RpcHandler.java b/openvidu-server/src/main/java/io/openvidu/server/rpc/RpcHandler.java index b38a7d66..d4694043 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/rpc/RpcHandler.java +++ b/openvidu-server/src/main/java/io/openvidu/server/rpc/RpcHandler.java @@ -45,6 +45,7 @@ import org.springframework.http.HttpHeaders; import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.internal.ProtocolElements; +import io.openvidu.java.client.ConnectionProperties; import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.core.EndReason; import io.openvidu.server.core.IdentifierPrefixes; @@ -168,6 +169,9 @@ public class RpcHandler extends DefaultJsonRpcHandler { case ProtocolElements.RECONNECTSTREAM_METHOD: reconnectStream(rpcConnection, request); break; + case ProtocolElements.VIDEODATA_METHOD: + updateVideoData(rpcConnection, request); + break; default: log.error("Unrecognized request {}", request); break; @@ -248,7 +252,7 @@ public class RpcHandler extends DefaultJsonRpcHandler { token = IdentifierPrefixes.TOKEN_ID + RandomStringUtils.randomAlphabetic(1).toUpperCase() + RandomStringUtils.randomAlphanumeric(15); try { - sessionManager.newTokenForInsecureUser(session, token, null); + sessionManager.newTokenForInsecureUser(session, token, new ConnectionProperties.Builder().build()); } catch (Exception e) { throw new OpenViduException(Code.TOKEN_CANNOT_BE_CREATED_ERROR_CODE, "Unable to create token for session " + sessionId + ": " + e.getMessage()); @@ -260,6 +264,9 @@ public class RpcHandler extends DefaultJsonRpcHandler { Token tokenObj = session.consumeToken(token); if (tokenObj != null) { + + session.showTokens("Token consumed"); + String clientMetadata = getStringParam(request, ProtocolElements.JOINROOM_METADATA_PARAM); if (sessionManager.formatChecker.isServerMetadataFormatCorrect(clientMetadata)) { @@ -493,7 +500,7 @@ public class RpcHandler extends DefaultJsonRpcHandler { // user's stream) or if the user is the owner of the stream and has a token // configured with this specific filter if (isModerator || (this.userIsStreamOwner(rpcConnection.getSessionId(), participant, streamId) - && participant.getToken().getKurentoTokenOptions().isFilterAllowed(filterType))) { + && participant.getToken().getKurentoOptions().isFilterAllowed(filterType))) { JsonObject filterOptions; try { filterOptions = JsonParser.parseString(getStringParam(request, ProtocolElements.FILTER_OPTIONS_PARAM)) @@ -638,6 +645,20 @@ public class RpcHandler extends DefaultJsonRpcHandler { } } + private void updateVideoData(RpcConnection rpcConnection, Request request) { + Participant participant; + try { + participant = sanityCheckOfSession(rpcConnection, "videoData"); + int height = getIntParam(request, "height"); + int width = getIntParam(request, "width"); + boolean videoActive = getBooleanParam(request, "videoActive"); + boolean audioActive = getBooleanParam(request, "audioActive"); + sessionManager.onVideoData(participant, request.getId(), height, width, videoActive, audioActive); + } catch (OpenViduException e) { + log.error("Error getting video data: {}", e.toString()); + } + } + public void leaveRoomAfterConnClosed(String participantPrivateId, EndReason reason) { try { sessionManager.evictParticipant(this.sessionManager.getParticipant(participantPrivateId), null, null, diff --git a/openvidu-server/src/main/java/io/openvidu/server/utils/CommandExecutor.java b/openvidu-server/src/main/java/io/openvidu/server/utils/CommandExecutor.java index 9d933ed0..608a1b84 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/utils/CommandExecutor.java +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/CommandExecutor.java @@ -21,7 +21,9 @@ import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; @@ -37,13 +39,20 @@ public class CommandExecutor { public static String execCommand(long msTimeout, String... command) throws IOException, InterruptedException { ProcessBuilder processBuilder = new ProcessBuilder(command); processBuilder.redirectErrorStream(true); - return commonExecCommand(msTimeout, processBuilder); + return commonExecCommand(msTimeout, processBuilder, true).get(0); + } + + public static List execCommandReturnList(long msTimeout, String... command) + throws IOException, InterruptedException { + ProcessBuilder processBuilder = new ProcessBuilder(command); + processBuilder.redirectErrorStream(true); + return commonExecCommand(msTimeout, processBuilder, false); } public static String execCommandRedirectError(long msTimeout, File errorOutputFile, String... command) throws IOException, InterruptedException { ProcessBuilder processBuilder = new ProcessBuilder(command).redirectError(errorOutputFile); - return commonExecCommand(msTimeout, processBuilder); + return commonExecCommand(msTimeout, processBuilder, true).get(0); } public static void execCommandRedirectStandardOutputAndError(long msTimeout, File standardOutputFile, @@ -59,11 +68,11 @@ public class CommandExecutor { } } - private static String commonExecCommand(long msTimeout, ProcessBuilder processBuilder) + private static List commonExecCommand(long msTimeout, ProcessBuilder processBuilder, boolean singleString) throws IOException, InterruptedException { Process process = processBuilder.start(); StringBuilder processOutput = new StringBuilder(); - String output; + List outputList = new ArrayList<>(); InputStreamReader inputStreamReader = null; BufferedReader processOutputReader = null; try { @@ -71,9 +80,15 @@ public class CommandExecutor { processOutputReader = new BufferedReader(inputStreamReader); String readLine; while ((readLine = processOutputReader.readLine()) != null) { - processOutput.append(readLine + System.lineSeparator()); + if (singleString) { + processOutput.append(readLine + System.lineSeparator()); + } else { + outputList.add(readLine); + } + } + if (singleString) { + outputList = Arrays.asList(processOutput.toString().trim()); } - if (!process.waitFor(msTimeout, TimeUnit.MILLISECONDS)) { log.error("Command {} did not receive a response in {} ms", Arrays.toString(processBuilder.command().toArray()), msTimeout); @@ -81,7 +96,6 @@ public class CommandExecutor { log.error(errorMsg); throw new IOException(errorMsg); } - output = processOutput.toString().trim(); } finally { if (inputStreamReader != null) { inputStreamReader.close(); @@ -90,7 +104,7 @@ public class CommandExecutor { processOutputReader.close(); } } - return output; + return outputList; } public static String gatherLinuxHostInformation() throws IOException, InterruptedException { diff --git a/openvidu-server/src/main/java/io/openvidu/server/utils/CustomFileManager.java b/openvidu-server/src/main/java/io/openvidu/server/utils/CustomFileManager.java index 3c0e0546..4cf84766 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/utils/CustomFileManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/CustomFileManager.java @@ -28,7 +28,7 @@ import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class CustomFileManager { +public abstract class CustomFileManager { private static final Logger log = LoggerFactory.getLogger(CustomFileManager.class); @@ -121,4 +121,8 @@ public class CustomFileManager { } } + public abstract void waitForFileToExistAndNotEmpty(String mediaNodeId, String absolutePathToFile) throws Exception; + + public abstract int maxSecondsWaitForFile(); + } diff --git a/openvidu-server/src/main/java/io/openvidu/server/utils/DockerManager.java b/openvidu-server/src/main/java/io/openvidu/server/utils/DockerManager.java index bb4a87d0..74857a43 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/utils/DockerManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/DockerManager.java @@ -1,245 +1,27 @@ -/* - * (C) Copyright 2017-2020 OpenVidu (https://openvidu.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - package io.openvidu.server.utils; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import javax.ws.rs.ProcessingException; +import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.Volume; -import com.github.dockerjava.api.DockerClient; -import com.github.dockerjava.api.async.ResultCallback; -import com.github.dockerjava.api.command.*; -import com.github.dockerjava.api.exception.ConflictException; -import com.github.dockerjava.api.exception.DockerClientException; -import com.github.dockerjava.api.exception.InternalServerErrorException; -import com.github.dockerjava.api.exception.NotFoundException; -import com.github.dockerjava.api.model.*; -import com.github.dockerjava.core.DefaultDockerClientConfig; -import com.github.dockerjava.core.DockerClientBuilder; -import com.github.dockerjava.core.DockerClientConfig; -import com.github.dockerjava.core.command.ExecStartResultCallback; -import com.github.dockerjava.core.command.PullImageResultCallback; +public interface DockerManager { -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; + public DockerManager init(); -import io.openvidu.client.OpenViduException; -import io.openvidu.client.OpenViduException.Code; -import io.openvidu.server.recording.service.WaitForContainerStoppedCallback; + public String runContainer(String mediaNodeId, String image, String containerName, String user, + List volumes, List binds, String networkMode, List envs, List command, + Long shmSize, boolean privileged, Map labels) throws Exception; -public class DockerManager { + public void removeContainer(String mediaNodeId, String containerId, boolean force); - private static final Logger log = LoggerFactory.getLogger(DockerManager.class); + public void runCommandInContainerSync(String mediaNodeId, String containerId, String command, int secondsOfWait) + throws IOException; - DockerClient dockerClient; + public void runCommandInContainerAsync(String mediaNodeId, String containerId, String command) throws IOException; - public DockerManager() { - DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); - this.dockerClient = DockerClientBuilder.getInstance(config).build(); - } - - public void downloadDockerImage(String image, int secondsOfWait) throws Exception { - try { - // Pull image - this.dockerClient.pullImageCmd(image).exec(new PullImageResultCallback()).awaitCompletion(secondsOfWait, - TimeUnit.SECONDS); - - } catch (NotFoundException | InternalServerErrorException e) { - if (dockerImageExistsLocally(image)) { - log.info("Docker image '{}' exists locally", image); - } else { - throw e; - } - } catch (DockerClientException e) { - log.info("Error on Pulling '{}' image. Probably because the user has stopped the execution", image); - throw e; - } catch (InterruptedException e) { - log.info("Error on Pulling '{}' image. Thread was interrupted: {}", image, e.getMessage()); - throw e; - } - } - - public boolean dockerImageExistsLocally(String image) throws ProcessingException { - boolean imageExists = false; - try { - this.dockerClient.inspectImageCmd(image).exec(); - imageExists = true; - } catch (NotFoundException nfe) { - imageExists = false; - } catch (ProcessingException e) { - throw e; - } - return imageExists; - } - - public void checkDockerEnabled() throws OpenViduException { - try { - this.dockerImageExistsLocally("hello-world"); - log.info("Docker is installed and enabled"); - } catch (ProcessingException exception) { - throw new OpenViduException(Code.DOCKER_NOT_FOUND, "Exception connecting to Docker daemon"); - } - } - - public String runContainer(String container, String containerName, String user, List volumes, - List binds, String networkMode, List envs, List command, Long shmSize, boolean privileged, - Map labels) - throws Exception { - - CreateContainerCmd cmd = dockerClient.createContainerCmd(container).withEnv(envs); - if (containerName != null) { - cmd.withName(containerName); - } - - if (user != null) { - cmd.withUser(user); - } - - HostConfig hostConfig = new HostConfig().withNetworkMode(networkMode).withPrivileged(privileged); - if (shmSize != null) { - hostConfig.withShmSize(shmSize); - } - if (volumes != null) { - cmd.withVolumes(volumes); - } - if (binds != null) { - hostConfig.withBinds(binds); - } - - if (labels != null) { - cmd.withLabels(labels); - } - - if (command != null) { - cmd.withCmd(command); - } - - cmd.withHostConfig(hostConfig); - - CreateContainerResponse response = null; - try { - response = cmd.exec(); - dockerClient.startContainerCmd(response.getId()).exec(); - log.info("Container ID: {}", response.getId()); - return response.getId(); - } catch (ConflictException e) { - log.error( - "The container name {} is already in use. Probably caused by a session with unique publisher re-publishing a stream", - containerName); - throw e; - } catch (NotFoundException e) { - log.error("Docker image {} couldn't be found in docker host", container); - throw e; - } - } - - public void removeDockerContainer(String containerId, boolean force) { - dockerClient.removeContainerCmd(containerId).withForce(force).exec(); - } - - public void cleanStrandedContainers(String imageName) { - List existingContainers = this.dockerClient.listContainersCmd().withShowAll(true).exec(); - for (Container container : existingContainers) { - if (container.getImage().startsWith(imageName)) { - log.info("Stranded {} Docker container ({}) removed on startup", imageName, container.getId()); - this.dockerClient.removeContainerCmd(container.getId()).withForce(true).exec(); - } - } - } - - public String runCommandInContainer(String containerId, String command, int secondsOfWait) - throws InterruptedException { - ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId).withAttachStdout(true) - .withAttachStderr(true).withCmd("bash", "-c", command).exec(); - CountDownLatch latch = new CountDownLatch(1); - final String[] stringResponse = new String[1]; - dockerClient.execStartCmd(execCreateCmdResponse.getId()).exec(new ExecStartResultCallback() { - @Override - public void onNext(Frame item) { - stringResponse[0] = new String(item.getPayload()); - latch.countDown(); - } - }); - latch.await(secondsOfWait, TimeUnit.SECONDS); - return stringResponse[0]; - } - - public void waitForContainerStopped(String containerId, int secondsOfWait) throws Exception { - CountDownLatch latch = new CountDownLatch(1); - WaitForContainerStoppedCallback callback = new WaitForContainerStoppedCallback(latch); - dockerClient.waitContainerCmd(containerId).exec(callback); - boolean stopped = false; - try { - stopped = latch.await(secondsOfWait, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw e; - } - if (!stopped) { - throw new Exception(); - } - } - - public String getContainerIp(String containerId) { - try { - return CommandExecutor.execCommand(5000, "/bin/sh", "-c", - "docker inspect -f \"{{ .NetworkSettings.IPAddress }}\" " + containerId); - } catch (IOException | InterruptedException e) { - log.error(e.getMessage()); - return null; - } - } - - public List getRunningContainers(String fullImageName) { - List containerIds = new ArrayList<>(); - List existingContainers = this.dockerClient.listContainersCmd().exec(); - for (Container container : existingContainers) { - if (container.getImage().startsWith(fullImageName)) { - containerIds.add(container.getId()); - } else if (container.getImageId().contains(fullImageName)) { - containerIds.add(container.getId()); - } - } - return containerIds; - } - - public String getImageId(String fullImageName) { - InspectImageResponse imageResponse = this.dockerClient.inspectImageCmd(fullImageName).exec(); - return imageResponse.getId(); - } - - public Map getLabels(String containerId) { - InspectContainerResponse containerInfo = dockerClient.inspectContainerCmd(containerId).exec(); - return containerInfo.getConfig().getLabels(); - } - - static public String getDockerGatewayIp() { - try { - return CommandExecutor.execCommand(5000, "/bin/sh", "-c", - "docker network inspect bridge --format='{{(index .IPAM.Config 0).Gateway}}'"); - } catch (IOException | InterruptedException e) { - log.error(e.getMessage()); - return null; - } - } + public void waitForContainerStopped(String mediaNodeId, String containerId, int secondsOfWait) throws Exception; } diff --git a/openvidu-server/src/main/java/io/openvidu/server/utils/FormatChecker.java b/openvidu-server/src/main/java/io/openvidu/server/utils/FormatChecker.java index 05748411..267d8f9f 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/utils/FormatChecker.java +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/FormatChecker.java @@ -30,8 +30,15 @@ public class FormatChecker { } public boolean isValidCustomSessionId(String customSessionId) { - // Alphanumeric string - return customSessionId.matches("[a-zA-Z0-9_-]+"); + return isValidAlphanumeric(customSessionId); + } + + public boolean isValidRecordingName(String recodingName) { + return isValidAlphanumeric(recodingName); + } + + private boolean isValidAlphanumeric(String str) { + return str.matches("[a-zA-Z0-9_-]+"); } } diff --git a/openvidu-server/src/main/java/io/openvidu/server/utils/JsonUtils.java b/openvidu-server/src/main/java/io/openvidu/server/utils/JsonUtils.java index 7e0468a3..24242d3b 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/utils/JsonUtils.java +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/JsonUtils.java @@ -20,6 +20,7 @@ package io.openvidu.server.utils; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; +import java.io.Reader; import java.util.Map.Entry; import org.kurento.jsonrpc.Props; @@ -56,13 +57,15 @@ public class JsonUtils { public JsonElement fromFileToJsonElement(String filePath) throws IOException, FileNotFoundException, JsonParseException, IllegalStateException { + return fromReaderToJsonElement(new FileReader(filePath)); + } + + public JsonObject fromReaderToJsonObject(Reader reader) throws IOException { + return this.fromReaderToJsonElement(reader).getAsJsonObject(); + } + + public JsonElement fromReaderToJsonElement(Reader reader) throws IOException { JsonElement json = null; - FileReader reader = null; - try { - reader = new FileReader(filePath); - } catch (FileNotFoundException e) { - throw e; - } try { json = JsonParser.parseReader(reader); } catch (JsonParseException | IllegalStateException exception) { diff --git a/openvidu-server/src/main/java/io/openvidu/server/utils/LocalCustomFileManager.java b/openvidu-server/src/main/java/io/openvidu/server/utils/LocalCustomFileManager.java new file mode 100644 index 00000000..5e45fb50 --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/LocalCustomFileManager.java @@ -0,0 +1,41 @@ +package io.openvidu.server.utils; + +import java.io.File; + +public class LocalCustomFileManager extends CustomFileManager { + + @Override + public void waitForFileToExistAndNotEmpty(String mediaNodeId, String absolutePathToFile) throws Exception { + + // Check 10 times per seconds + int MAX_SECONDS_WAIT = this.maxSecondsWaitForFile(); + int MILLISECONDS_INTERVAL_WAIT = 100; + int LIMIT = MAX_SECONDS_WAIT * 1000 / MILLISECONDS_INTERVAL_WAIT; + int i = 0; + + boolean arePresent = fileExistsAndHasBytes(absolutePathToFile); + while (!arePresent && i < LIMIT) { + try { + Thread.sleep(MILLISECONDS_INTERVAL_WAIT); + arePresent = fileExistsAndHasBytes(absolutePathToFile); + i++; + } catch (InterruptedException e) { + throw new Exception("Interrupted exception while waiting for file " + absolutePathToFile + " to exist"); + } + } + if (!arePresent) { + throw new Exception("File " + absolutePathToFile + " does not exist and hasn't been created in " + + MAX_SECONDS_WAIT + " seconds"); + } + } + + private boolean fileExistsAndHasBytes(String fileName) { + File f = new File(fileName); + return (f.exists() && f.isFile() && f.length() > 0); + } + + public int maxSecondsWaitForFile() { + return 30; + } + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/utils/LocalDockerManager.java b/openvidu-server/src/main/java/io/openvidu/server/utils/LocalDockerManager.java new file mode 100644 index 00000000..0b697b51 --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/LocalDockerManager.java @@ -0,0 +1,280 @@ +/* + * (C) Copyright 2017-2020 OpenVidu (https://openvidu.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.openvidu.server.utils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.ws.rs.ProcessingException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.CreateContainerCmd; +import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.api.command.ExecCreateCmdResponse; +import com.github.dockerjava.api.command.InspectContainerResponse; +import com.github.dockerjava.api.command.InspectImageResponse; +import com.github.dockerjava.api.exception.ConflictException; +import com.github.dockerjava.api.exception.DockerClientException; +import com.github.dockerjava.api.exception.InternalServerErrorException; +import com.github.dockerjava.api.exception.NotFoundException; +import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.Container; +import com.github.dockerjava.api.model.HostConfig; +import com.github.dockerjava.api.model.Volume; +import com.github.dockerjava.core.DefaultDockerClientConfig; +import com.github.dockerjava.core.DockerClientBuilder; +import com.github.dockerjava.core.DockerClientConfig; +import com.github.dockerjava.core.command.ExecStartResultCallback; +import com.github.dockerjava.core.command.PullImageResultCallback; + +import io.openvidu.client.OpenViduException; +import io.openvidu.client.OpenViduException.Code; +import io.openvidu.server.recording.service.WaitForContainerStoppedCallback; + +public class LocalDockerManager implements DockerManager { + + private static final Logger log = LoggerFactory.getLogger(DockerManager.class); + + private DockerClient dockerClient; + + public LocalDockerManager(boolean init) { + if (init) { + this.init(); + } + } + + @Override + public DockerManager init() { + DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); + this.dockerClient = DockerClientBuilder.getInstance(config).build(); + return this; + } + + public void downloadDockerImage(String image, int secondsOfWait) throws Exception { + try { + // Pull image + this.dockerClient.pullImageCmd(image).exec(new PullImageResultCallback()).awaitCompletion(secondsOfWait, + TimeUnit.SECONDS); + + } catch (NotFoundException | InternalServerErrorException e) { + if (dockerImageExistsLocally(image)) { + log.info("Docker image '{}' exists locally", image); + } else { + throw e; + } + } catch (DockerClientException e) { + log.info("Error on Pulling '{}' image. Probably because the user has stopped the execution", image); + throw e; + } catch (InterruptedException e) { + log.info("Error on Pulling '{}' image. Thread was interrupted: {}", image, e.getMessage()); + throw e; + } + } + + public boolean dockerImageExistsLocally(String image) throws ProcessingException { + boolean imageExists = false; + try { + this.dockerClient.inspectImageCmd(image).exec(); + imageExists = true; + } catch (NotFoundException nfe) { + imageExists = false; + } catch (ProcessingException e) { + throw e; + } + return imageExists; + } + + public void checkDockerEnabled() throws OpenViduException { + try { + this.dockerImageExistsLocally("hello-world"); + log.info("Docker is installed and enabled"); + } catch (ProcessingException exception) { + throw new OpenViduException(Code.DOCKER_NOT_FOUND, "Exception connecting to Docker daemon"); + } + } + + @Override + public String runContainer(String mediaNodeId, String image, String containerName, String user, + List volumes, List binds, String networkMode, List envs, List command, + Long shmSize, boolean privileged, Map labels) throws Exception { + + CreateContainerCmd cmd = dockerClient.createContainerCmd(image).withEnv(envs); + if (containerName != null) { + cmd.withName(containerName); + } + + if (user != null) { + cmd.withUser(user); + } + + HostConfig hostConfig = new HostConfig().withNetworkMode(networkMode).withPrivileged(privileged); + if (shmSize != null) { + hostConfig.withShmSize(shmSize); + } + if (volumes != null) { + cmd.withVolumes(volumes); + } + if (binds != null) { + hostConfig.withBinds(binds); + } + + if (labels != null) { + cmd.withLabels(labels); + } + + if (command != null) { + cmd.withCmd(command); + } + + cmd.withHostConfig(hostConfig); + + CreateContainerResponse response = null; + try { + response = cmd.exec(); + dockerClient.startContainerCmd(response.getId()).exec(); + log.info("Container ID: {}", response.getId()); + return response.getId(); + } catch (ConflictException e) { + log.error( + "The container name {} is already in use. Probably caused by a session with unique publisher re-publishing a stream", + containerName); + throw e; + } catch (NotFoundException e) { + log.error("Docker image {} couldn't be found in docker host", image); + throw e; + } + } + + @Override + public void removeContainer(String mediaNodeId, String containerId, boolean force) { + dockerClient.removeContainerCmd(containerId).withForce(force).exec(); + } + + public void cleanStrandedContainers(String imageName) { + List existingContainers = this.dockerClient.listContainersCmd().withShowAll(true).exec(); + for (Container container : existingContainers) { + if (container.getImage().startsWith(imageName)) { + log.info("Stranded {} Docker container ({}) removed on startup", imageName, container.getId()); + this.dockerClient.removeContainerCmd(container.getId()).withForce(true).exec(); + } + } + } + + @Override + public void runCommandInContainerSync(String mediaNodeId, String containerId, String command, int secondsOfWait) + throws IOException { + ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId).withAttachStdout(true) + .withAttachStderr(true).withCmd("bash", "-c", command).exec(); + CountDownLatch latch = new CountDownLatch(1); + dockerClient.execStartCmd(execCreateCmdResponse.getId()).exec(new ExecStartResultCallback() { + @Override + public void onComplete() { + latch.countDown(); + } + }); + try { + latch.await(secondsOfWait, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new IOException("Container " + containerId + " did not return from executing command \"" + command + + "\" in " + secondsOfWait + " seconds"); + } + } + + @Override + public void runCommandInContainerAsync(String mediaNodeId, String containerId, String command) throws IOException { + ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId).withAttachStdout(true) + .withAttachStderr(true).withCmd("bash", "-c", command).exec(); + dockerClient.execStartCmd(execCreateCmdResponse.getId()).exec(new ExecStartResultCallback() { + }); + } + + @Override + public void waitForContainerStopped(String mediaNodeId, String containerId, int secondsOfWait) throws Exception { + CountDownLatch latch = new CountDownLatch(1); + WaitForContainerStoppedCallback callback = new WaitForContainerStoppedCallback(latch); + dockerClient.waitContainerCmd(containerId).exec(callback); + boolean stopped = false; + try { + stopped = latch.await(secondsOfWait, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw e; + } + if (!stopped) { + throw new Exception(); + } + } + + public String getContainerIp(String containerId) { + try { + return CommandExecutor.execCommand(5000, "/bin/sh", "-c", + "docker inspect -f \"{{ .NetworkSettings.IPAddress }}\" " + containerId); + } catch (IOException | InterruptedException e) { + log.error(e.getMessage()); + return null; + } + } + + public List getRunningContainers(String fullImageName) { + List containerIds = new ArrayList<>(); + List existingContainers = this.dockerClient.listContainersCmd().exec(); + for (Container container : existingContainers) { + if (container.getImage().startsWith(fullImageName)) { + containerIds.add(container.getId()); + } else if (container.getImageId().contains(fullImageName)) { + containerIds.add(container.getId()); + } + } + return containerIds; + } + + public String getImageId(String fullImageName) { + InspectImageResponse imageResponse = this.dockerClient.inspectImageCmd(fullImageName).exec(); + return imageResponse.getId(); + } + + public Map getLabels(String containerId) { + InspectContainerResponse containerInfo = dockerClient.inspectContainerCmd(containerId).exec(); + return containerInfo.getConfig().getLabels(); + } + + public void close() { + try { + this.dockerClient.close(); + } catch (IOException e) { + log.error("Error closing DockerClient: {}", e.getMessage()); + } + } + + static public String getDockerGatewayIp() { + try { + return CommandExecutor.execCommand(5000, "/bin/sh", "-c", + "docker network inspect bridge --format='{{(index .IPAM.Config 0).Gateway}}'"); + } catch (IOException | InterruptedException e) { + log.error(e.getMessage()); + return null; + } + } + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/utils/RestUtils.java b/openvidu-server/src/main/java/io/openvidu/server/utils/RestUtils.java new file mode 100644 index 00000000..66512440 --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/RestUtils.java @@ -0,0 +1,29 @@ +package io.openvidu.server.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; + +public class RestUtils { + + private static final Logger log = LoggerFactory.getLogger(RestUtils.class); + + public static HttpHeaders getResponseHeaders() { + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setContentType(MediaType.APPLICATION_JSON); + return responseHeaders; + } + + public static ResponseEntity getErrorResponse(String message, HttpStatus status) { + if (!status.is2xxSuccessful()) { + log.error(message); + } + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setContentType(MediaType.TEXT_PLAIN); + return new ResponseEntity<>(message, responseHeaders, status); + } + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/utils/SDPMunging.java b/openvidu-server/src/main/java/io/openvidu/server/utils/SDPMunging.java index bee5cbd6..6544f764 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/utils/SDPMunging.java +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/SDPMunging.java @@ -1,48 +1,67 @@ +/* + * (C) Copyright 2017-2020 OpenVidu (https://openvidu.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ package io.openvidu.server.utils; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - +import io.openvidu.client.OpenViduException; +import io.openvidu.java.client.VideoCodec; +import io.openvidu.client.OpenViduException.Code; +import io.openvidu.server.core.Participant; +import io.openvidu.server.core.Session; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.openvidu.client.OpenViduException; -import io.openvidu.client.OpenViduException.Code; -import io.openvidu.java.client.VideoCodec; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class SDPMunging { - + private static final Logger log = LoggerFactory.getLogger(SDPMunging.class); + private Set supportedVideoCodecs = new HashSet<>(Arrays.asList( + VideoCodec.VP8, + VideoCodec.H264 + )); + /** - * `codec` is a uppercase SDP-style codec name: "VP8", "H264". - * - * This looks for all video m-sections (lines starting with "m=video"), - * then searches all of its related PayloadTypes trying to find those which - * correspond to the preferred codec. If any is found, they are moved to the - * front of the PayloadTypes list in the m= line, without removing the other - * codecs that might be present. - * - * If our preferred codec is not found, the m= line is left without changes. - * - * This works based on the basis that RFC 3264 "Offer/Answer Model SDP" section - * 6.1 "Unicast Streams" allows the answerer to list media formats in a - * different order of preference from what it got in the offer: - * - * > Although the answerer MAY list the formats in their desired order of - * > preference, it is RECOMMENDED that unless there is a specific reason, - * > the answerer list formats in the same relative order they were - * > present in the offer. - * - * Here we have a specific reason, thus we use this allowance to change the - * ordering of formats. Browsers (tested with Chrome 84) honor this change and - * use the first codec provided in the answer, so this operation actually works. - */ - public String setCodecPreference(VideoCodec codec, String sdp, boolean removeCodecs) throws OpenViduException { + * `codec` is a uppercase SDP-style codec name: "VP8", "H264". + * + * This looks for all video m-sections (lines starting with "m=video"), + * then searches all of its related PayloadTypes trying to find those which + * correspond to the preferred codec. If any is found, they are moved to the + * front of the PayloadTypes list in the m= line, without removing the other + * codecs that might be present. + * + * If our preferred codec is not found, the m= line is left without changes. + * + * This works based on the basis that RFC 3264 "Offer/Answer Model SDP" section + * 6.1 "Unicast Streams" allows the answerer to list media formats in a + * different order of preference from what it got in the offer: + * + * > Although the answerer MAY list the formats in their desired order of + * > preference, it is RECOMMENDED that unless there is a specific reason, + * > the answerer list formats in the same relative order they were + * > present in the offer. + * + * Here we have a specific reason, thus we use this allowance to change the + * ordering of formats. Browsers (tested with Chrome 84) honor this change and + * use the first codec provided in the answer, so this operation actually works. + */ + public String setCodecPreference(VideoCodec codec, String sdp) throws OpenViduException { String codecStr = codec.name(); log.info("[setCodecPreference] codec: {}", codecStr); @@ -122,82 +141,57 @@ public class SDPMunging { } // Replace the original m= line with the one we just built. - if (!removeCodecs) { - // Add the rest of PayloadTypes. - newLine.append(String.join(" ", lineParts)); - } lines[sl] = newLine.toString().trim(); } return String.join("\r\n", lines) + "\r\n"; } - - /** - * Possible Kurento's bug - * Some browsers can't use H264 as a video codec if in the offerer SDP - * the parameter "a=fmtp: <...> profile-level-id=42e01f" is not defined. - * This munging is only used when the forced codec needs to be H264 - * References: - * https://stackoverflow.com/questions/38432137/cannot-establish-webrtc-connection-different-codecs-and-payload-type-in-sdp + + /** + * Return a SDP modified to force a specific codec */ - public String setfmtpH264(String sdp) { - String codecStr = VideoCodec.H264.name(); - - // Get all lines - List lines = new LinkedList(Arrays.asList(sdp.split("\\R+"))); - - // Index to reference the line with "m=video" - int mVideoLineIndex = -1; - List validCodecsPayload = new ArrayList<>(); - - for(int i = 0; i < lines.size(); i++) { - String sdpLine = lines.get(i); - - // Check that we are in "m=video" - if (sdpLine.startsWith("m=video")) { - mVideoLineIndex = i; - - // Search for payload-type for the specified codec - for(int j = i+1; j < lines.size(); j++) { - String auxLine = lines.get(j); - - // Check that the line we're going to analizae is not anoter "m=" - if(auxLine.startsWith("m=")) { - break; - } - - if (auxLine.startsWith("a=rtpmap")) { - String[] rtpmapInfo = auxLine.split(":")[1].split(" "); - String possiblePayload = rtpmapInfo[0]; - String possibleCodec = rtpmapInfo[1]; - if (possibleCodec.contains(codecStr)) { - validCodecsPayload.add(possiblePayload); - } - } - } - // If a payload is not found, then the codec is not in the SDP - if (validCodecsPayload.size() == 0) { - throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, String.format("Codec %s not found", codecStr)); - } - continue; + public String forceCodec(Participant participant, String sdp, boolean isOffer, Session session, boolean isPublisher, + boolean isReconnecting, boolean isTranscodingAllowed, VideoCodec forcedVideoCodec) throws OpenViduException { + try { + if (supportedVideoCodecs.contains(forcedVideoCodec)) { + String mungedSdpOffer; + + log.debug("PARTICIPANT '{}' in Session '{}'. Is Publisher: '{}'. Is Subscriber: '{}'." + + " Is Offer SDP: '{}'. Is Answer SDP: '{}'. Is Reconnecting '{}'." + + " SDP before munging: \n {}", participant.getParticipantPublicId(), + session.getSessionId(), isPublisher, !isPublisher, isOffer, !isOffer, isReconnecting, sdp); + + mungedSdpOffer = this.setCodecPreference(forcedVideoCodec, sdp); + + log.debug("PARTICIPANT '{}' in Session '{}'. Is Publisher: '{}'. Is Subscriber: '{}'." + + " Is Offer SDP: '{}'. Is Answer SDP: '{}'. Is Reconnecting '{}'." + + " SDP after munging: \n {}", participant.getParticipantPublicId(), + session.getSessionId(), isPublisher, !isPublisher, isOffer, !isOffer, isReconnecting, mungedSdpOffer); + + return mungedSdpOffer; + } else { + throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, "Codec not supported by Media Server"); } - + + } catch (OpenViduException e) { + + String errorMessage = "Error forcing codec: '" + forcedVideoCodec + "', for PARTICIPANT: '" + + participant.getParticipantPublicId() + "' in Session: '" + session.getSessionId() + + "'. Is publishing: '" + isPublisher + "'. Is Subscriber: '" + !isPublisher + + "'. Is Offer: '" + isOffer + "'. Is Answer: '" + !isOffer + "'. Is Reconnecting: '" + + isReconnecting + "'.\nException: " + e.getMessage() + "\nSDP:\n" + sdp; + + if(!isTranscodingAllowed) { + throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, errorMessage); + } + + log.info("Codec: '{}' is not supported for PARTICIPANT: '{}' in Session: '{}'. Is publishing: '{}'. " + + "Is Subscriber: '{}' Is Offer SDP: '{}'. Is Answer SDP: '{}'. Is Reconnecting: '{}'." + + " Transcoding will be allowed", forcedVideoCodec, participant.getParticipantPublicId(), + session.getSessionId(), isPublisher, !isPublisher, isOffer, !isOffer, isReconnecting); + + return sdp; } - if (mVideoLineIndex == -1) { - throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, "This SDP does not offer video"); - } else { - for (String codecPayload: validCodecsPayload) { - if (!sdp.contains(String.format("a=fmtp:%s", codecPayload))) { - String newfmtpLine = String.format("a=fmtp:%s level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", codecPayload); - lines.add(mVideoLineIndex + 1, newfmtpLine); - } - } - } - - // Return munging sdp!! - String[] munguedSdpLines = lines.toArray(new String[lines.size()]); - return String.join("\r\n", munguedSdpLines) + "\r\n"; - } -} \ No newline at end of file +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/utils/UpdatableTimerTask.java b/openvidu-server/src/main/java/io/openvidu/server/utils/UpdatableTimerTask.java index d94122e5..6a59836b 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/utils/UpdatableTimerTask.java +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/UpdatableTimerTask.java @@ -67,7 +67,7 @@ public class UpdatableTimerTask extends TimerTask { try { task.run(); } catch (Exception e) { - log.error("Exception running UpdatableTimerTask: {}", e.getMessage()); + log.error("Exception running UpdatableTimerTask: {} - {}", e.getMessage(), e.getStackTrace()); } updateTimer(); } diff --git a/openvidu-server/src/main/java/io/openvidu/server/webhook/CDRLoggerWebhook.java b/openvidu-server/src/main/java/io/openvidu/server/webhook/CDRLoggerWebhook.java index 3aa670e5..acca20ae 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/webhook/CDRLoggerWebhook.java +++ b/openvidu-server/src/main/java/io/openvidu/server/webhook/CDRLoggerWebhook.java @@ -17,9 +17,13 @@ package io.openvidu.server.webhook; +import java.util.List; + +import org.apache.http.Header; + import io.openvidu.server.cdr.CDREvent; +import io.openvidu.server.cdr.CDREventName; import io.openvidu.server.cdr.CDRLogger; -import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.kurento.endpoint.KmsEvent; import io.openvidu.server.summary.SessionSummary; @@ -27,9 +31,8 @@ public class CDRLoggerWebhook implements CDRLogger { private HttpWebhookSender webhookSender; - public CDRLoggerWebhook(OpenviduConfig openviduConfig) { - this.webhookSender = new HttpWebhookSender(openviduConfig.getOpenViduWebhookEndpoint(), - openviduConfig.getOpenViduWebhookHeaders(), openviduConfig.getOpenViduWebhookEvents()); + public CDRLoggerWebhook(String webhookEndpoint, List
    webhookHeaders, List webhookEvents) { + this.webhookSender = new HttpWebhookSender(webhookEndpoint, webhookHeaders, webhookEvents); } @Override diff --git a/openvidu-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/openvidu-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 9448757d..04a0f0f6 100644 --- a/openvidu-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/openvidu-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -93,6 +93,12 @@ "description": "URL the composed-video recording dockerized Chrome will use to connect to the recording layouts inside OpenVidu Server host. This will affect all video recording layouts (default one BEST_FIT, all CUSTOM layouts). This allows changing the default URL, which is the public URL 'https://DOMAIN_OR_PUBLIC_IP:HTTPS_PORT/', for those cases where OpenVidu Server host does not allow back and forth connections using the public url from inside the host", "defaultValue": "" }, + { + "name": "OPENVIDU_RECORDING_COMPOSED_BASICAUTH", + "type": "java.lang.Boolean", + "description": "'true' to automatically add Basic Auth credentials to the URL passed to the recording container, false otherwise", + "defaultValue": true + }, { "name": "OPENVIDU_WEBHOOK", "type": "java.lang.Boolean", @@ -141,6 +147,18 @@ "description": "Minimum video bandwidth sent from OpenVidu Server to clients, in kbps. 0 means unconstrained", "defaultValue": 300 }, + { + "name": "OPENVIDU_STREAMS_FORCED_VIDEO_CODEC", + "type": "java.lang.String", + "description": "Defines which video codec is being forced to be used in the browser/client", + "defaultValue": "VP8" + }, + { + "name": "OPENVIDU_STREAMS_ALLOW_TRANSCODING", + "type": "java.lang.Boolean", + "description": "Defines if transcoding is allowed or not when OPENVIDU_STREAMS_FORCED_VIDEO_CODEC is not a compatible codec with the browser/client.", + "defaultValue": false + }, { "name": "OPENVIDU_SESSIONS_GARBAGE_INTERVAL", "type": "java.lang.Integer", diff --git a/openvidu-server/src/main/resources/OpenVidu.postman_collection.json b/openvidu-server/src/main/resources/OpenVidu.postman_collection.json new file mode 100644 index 00000000..86399bcd --- /dev/null +++ b/openvidu-server/src/main/resources/OpenVidu.postman_collection.json @@ -0,0 +1,1120 @@ +{ + "info": { + "_postman_id": "b2fbd114-3827-4117-8281-a16afaf81dcc", + "name": "OpenVidu REST API", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "POST Session", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"mediaMode\": \"ROUTED\",\n \"recordingMode\": \"MANUAL\",\n \"customSessionId\": \"TestSession\",\n \"defaultOutputMode\": \"COMPOSED\",\n \"defaultRecordingLayout\": \"BEST_FIT\",\n \"defaultCustomLayout\": \"CUSTOM_LAYOUT\"\n}" + }, + "url": { + "raw": "https://localhost:4443/openvidu/api/sessions", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "sessions" + ] + } + }, + "response": [] + }, + { + "name": "GET Session/ID", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:4443/openvidu/api/sessions/ID?webRtcStats=false&pendingConnections=true", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "sessions", + "ID" + ], + "query": [ + { + "key": "webRtcStats", + "value": "false" + }, + { + "key": "pendingConnections", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "GET Sessions", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:4443/openvidu/api/sessions?webRtcStats=false&pendingConnections=true", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "sessions" + ], + "query": [ + { + "key": "webRtcStats", + "value": "false" + }, + { + "key": "pendingConnections", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "DELETE Session/ID", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "https://localhost:4443/openvidu/api/sessions/ID", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "sessions", + "ID" + ] + } + }, + "response": [] + }, + { + "name": "POST Connection", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"type\": \"asdf\",\n \"role\": \"PUBLISHER\",\n \"data\": \"SERVER_DATA\",\n \"record\": true,\n \"kurentoOptions\": {\n \"videoMaxRecvBandwidth\": 1000,\n \"videoMinRecvBandwidth\": 300,\n \"videoMaxSendBandwidth\": 1000,\n \"videoMinSendBandwidth\": 300,\n \"allowedFilters\": [\n \"GStreamerFilter\",\n \"ZBarFilter\"\n ]\n },\n \"rtspUri\": \"rtsp://your.camera.ip.sdp\",\n \"adaptativeBitrate\": true,\n \"onlyPlayWithSubscribers\": true,\n \"networkCache\": 2000\n}" + }, + "url": { + "raw": "https://localhost:4443/openvidu/api/sessions/TestSession/connection", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "sessions", + "TestSession", + "connection" + ] + } + }, + "response": [] + }, + { + "name": "GET Connection/ID", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:4443/openvidu/api/sessions/ID/connection/ID", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "sessions", + "ID", + "connection", + "ID" + ] + } + }, + "response": [] + }, + { + "name": "GET Connections", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:4443/openvidu/api/sessions/ID/connection", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "sessions", + "ID", + "connection" + ] + } + }, + "response": [] + }, + { + "name": "PATCH Connection", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"role\": \"MODERATOR\",\n \"record\": false\n}" + }, + "url": { + "raw": "https://localhost:4443/openvidu/api/sessions/TestSession/connection/con_SKoAiag7OO", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "sessions", + "TestSession", + "connection", + "con_SKoAiag7OO" + ] + } + }, + "response": [] + }, + { + "name": "DELETE Connection/ID", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "https://localhost:4443/openvidu/api/sessions/ID/connection/ID", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "sessions", + "ID", + "connection", + "ID" + ] + } + }, + "response": [] + }, + { + "name": "POST Recording Start", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"session\":\"TestSession\",\n \"name\":\"MyRecording\",\n \"outputMode\":\"COMPOSED\",\n \"hasAudio\": true,\n \"hasVideo\": true,\n \"recordingLayout\":\"BEST_FIT\",\n \"customLayout\":\"\",\n \"resolution\": \"1280x720\"\n}" + }, + "url": { + "raw": "https://localhost:4443/openvidu/api/recordings/start", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "recordings", + "start" + ] + } + }, + "response": [] + }, + { + "name": "POST Recording Stop", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "url": { + "raw": "https://localhost:4443/openvidu/api/recordings/stop/ID", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "recordings", + "stop", + "ID" + ] + } + }, + "response": [] + }, + { + "name": "GET Recording/ID", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:4443/openvidu/api/recordings/ID", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "recordings", + "ID" + ] + } + }, + "response": [] + }, + { + "name": "GET Recordings", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:4443/openvidu/api/recordings", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "recordings" + ] + } + }, + "response": [] + }, + { + "name": "DELETE Recording", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "https://localhost:4443/openvidu/api/recordings/ID", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "recordings", + "ID" + ] + } + }, + "response": [] + }, + { + "name": "GET MediaNode/ID", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:4443/openvidu/api/media-nodes/ID?sessions=true&extra-info=false", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "media-nodes", + "ID" + ], + "query": [ + { + "key": "sessions", + "value": "true" + }, + { + "key": "extra-info", + "value": "false" + } + ] + } + }, + "response": [] + }, + { + "name": "GET MediaNodes", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:4443/openvidu/api/media-nodes?sessions=true&extra-info=false", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "media-nodes" + ], + "query": [ + { + "key": "sessions", + "value": "true" + }, + { + "key": "extra-info", + "value": "false" + } + ] + } + }, + "response": [] + }, + { + "name": "POST Media Node", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "url": { + "raw": "https://localhost:4443/openvidu/api/media-nodes?wait=true", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "media-nodes" + ], + "query": [ + { + "key": "wait", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "DELETE Media Node", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "https://localhost:4443/openvidu/api/media-nodes/ID?wait=true&deletion-strategy=when-no-sessions", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "media-nodes", + "ID" + ], + "query": [ + { + "key": "wait", + "value": "true" + }, + { + "key": "deletion-strategy", + "value": "when-no-sessions" + } + ] + } + }, + "response": [] + }, + { + "name": "PATCH Media Node", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"status\": \"terminating\"\n}" + }, + "url": { + "raw": "https://localhost:4443/openvidu/api/media-nodes/kms_LmVJiQpm", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "media-nodes", + "kms_LmVJiQpm" + ] + } + }, + "response": [] + }, + { + "name": "PUT Media Nodes", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "url": { + "raw": "https://localhost:4443/openvidu/api/media-nodes", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "media-nodes" + ] + } + }, + "response": [] + }, + { + "name": "POST Token", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"session\": \"TestSession\",\n \"role\": \"PUBLISHER\",\n \"data\": \"SERVER_DATA\",\n \"record\": true,\n \"kurentoOptions\": {\n \"videoMaxRecvBandwidth\": 1000,\n \"videoMinRecvBandwidth\": 300,\n \"videoMaxSendBandwidth\": 1000,\n \"videoMinSendBandwidth\": 300,\n \"allowedFilters\": [ \"GStreamerFilter\", \"ZBarFilter\" ]\n }\n}" + }, + "url": { + "raw": "https://localhost:4443/openvidu/api/tokens", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "tokens" + ] + } + }, + "response": [] + }, + { + "name": "DELETE Stream/ID", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "https://localhost:4443/openvidu/api/sessions/ID/stream/ID", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "sessions", + "ID", + "stream", + "ID" + ] + } + }, + "response": [] + }, + { + "name": "POST Signal", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"session\":\"TestSession\",\n \"to\": [],\n \"type\":\"MY_TYPE\",\n \"data\":\"This is my signal data\"\n}" + }, + "url": { + "raw": "https://localhost:4443/openvidu/api/signal", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "signal" + ] + } + }, + "response": [] + }, + { + "name": "GET Config", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:4443/openvidu/api/config", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "config" + ] + } + }, + "response": [] + }, + { + "name": "GET Health", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:4443/openvidu/api/health", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "health" + ] + } + }, + "response": [] + }, + { + "name": "POST Restart", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "MY_SECRET", + "type": "string" + }, + { + "key": "username", + "value": "OPENVIDUAPP", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"OPENVIDU_SECRET\":\"MY_SECRET\",\n \"OPENVIDU_CDR\":true,\n \"OPENVIDU_RECORDING\":true,\n \"OPENVIDU_RECORDING_PUBLIC_ACCESS\":true,\n \"OPENVIDU_RECORDING_NOTIFICATION\":\"publisher_moderator\",\n \"OPENVIDU_RECORDING_PATH\":\"/opt/openvidu/recordings\",\n \"OPENVIDU_RECORDING_CUSTOM_LAYOUT\":\"/opt/openvidu/custom-layout\",\n \"OPENVIDU_RECORDING_AUTOSTOP_TIMEOUT\":120,\n \"OPENVIDU_WEBHOOK\":false,\n \"OPENVIDU_WEBHOOK_ENDPOINT\":\"http://localhost:7777/webhook/\",\n \"OPENVIDU_WEBHOOK_HEADERS\":[\n \"Authorization: Basic T1BFTlZJRFVBUFA6TVlfU0VDUkVU\"\n ],\n \"OPENVIDU_WEBHOOK_EVENTS\":[\n \"recordingStatusChanged\"\n ],\n \"OPENVIDU_STREAMS_VIDEO_MAX_RECV_BANDWIDTH\":1000,\n \"OPENVIDU_STREAMS_VIDEO_MIN_RECV_BANDWIDTH\":300,\n \"OPENVIDU_STREAMS_VIDEO_MAX_SEND_BANDWIDTH\":1000,\n \"OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH\":300,\n \"OPENVIDU_SESSIONS_GARBAGE_INTERVAL\":900,\n \"OPENVIDU_SESSIONS_GARBAGE_THRESHOLD\":3600,\n \"OPENVIDU_PRO_STATS_MONITORING_INTERVAL\":30,\n \"OPENVIDU_PRO_STATS_WEBRTC_INTERVAL\":20\n}" + }, + "url": { + "raw": "https://localhost:4443/openvidu/api/restart", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "4443", + "path": [ + "openvidu", + "api", + "restart" + ] + } + }, + "response": [] + } + ], + "protocolProfileBehavior": {} +} \ No newline at end of file diff --git a/openvidu-server/src/main/resources/application.properties b/openvidu-server/src/main/resources/application.properties index 2ae45ad0..560e41fb 100644 --- a/openvidu-server/src/main/resources/application.properties +++ b/openvidu-server/src/main/resources/application.properties @@ -8,6 +8,8 @@ server.ssl.key-alias=openvidu-selfsigned logging.level.root=info spring.main.allow-bean-definition-overriding=true +SUPPORT_DEPRECATED_API=true + DOTENV_PATH=. DOMAIN_OR_PUBLIC_IP= @@ -22,22 +24,25 @@ OPENVIDU_CDR_PATH=/opt/openvidu/cdr OPENVIDU_WEBHOOK=false OPENVIDU_WEBHOOK_ENDPOINT= OPENVIDU_WEBHOOK_HEADERS=[] -OPENVIDU_WEBHOOK_EVENTS=["sessionCreated","sessionDestroyed","participantJoined","participantLeft","webrtcConnectionCreated","webrtcConnectionDestroyed","recordingStatusChanged","filterEventDispatched","mediaNodeStatusChanged"] +OPENVIDU_WEBHOOK_EVENTS=["sessionCreated","sessionDestroyed","participantJoined","participantLeft","webrtcConnectionCreated","webrtcConnectionDestroyed","recordingStatusChanged","filterEventDispatched","signalSent","mediaNodeStatusChanged","autoscaling","mediaServerCrashed"] OPENVIDU_RECORDING=false OPENVIDU_RECORDING_DEBUG=false -OPENVIDU_RECORDING_VERSION=2.15.0 +OPENVIDU_RECORDING_VERSION=2.16.0 OPENVIDU_RECORDING_PATH=/opt/openvidu/recordings OPENVIDU_RECORDING_PUBLIC_ACCESS=false OPENVIDU_RECORDING_NOTIFICATION=publisher_moderator OPENVIDU_RECORDING_CUSTOM_LAYOUT=/opt/openvidu/custom-layout OPENVIDU_RECORDING_AUTOSTOP_TIMEOUT=120 OPENVIDU_RECORDING_COMPOSED_URL= +OPENVIDU_RECORDING_COMPOSED_BASICAUTH=true OPENVIDU_STREAMS_VIDEO_MAX_RECV_BANDWIDTH=1000 OPENVIDU_STREAMS_VIDEO_MIN_RECV_BANDWIDTH=300 OPENVIDU_STREAMS_VIDEO_MAX_SEND_BANDWIDTH=1000 OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH=300 +OPENVIDU_STREAMS_FORCED_VIDEO_CODEC=VP8 +OPENVIDU_STREAMS_ALLOW_TRANSCODING=false OPENVIDU_SESSIONS_GARBAGE_INTERVAL=900 OPENVIDU_SESSIONS_GARBAGE_THRESHOLD=3600 diff --git a/openvidu-server/src/test/java/io/openvidu/server/test/integration/SessionGarbageCollectorIntegrationTest.java b/openvidu-server/src/test/java/io/openvidu/server/test/integration/SessionGarbageCollectorIntegrationTest.java index 6b1527c8..8b49bdc6 100644 --- a/openvidu-server/src/test/java/io/openvidu/server/test/integration/SessionGarbageCollectorIntegrationTest.java +++ b/openvidu-server/src/test/java/io/openvidu/server/test/integration/SessionGarbageCollectorIntegrationTest.java @@ -18,7 +18,6 @@ package io.openvidu.server.test.integration; import java.util.HashMap; -import java.util.Map; import java.util.UUID; import org.junit.Assert; @@ -36,7 +35,7 @@ import org.springframework.test.context.web.WebAppConfiguration; import com.google.gson.Gson; import com.google.gson.JsonObject; -import io.openvidu.java.client.OpenViduRole; +import io.openvidu.java.client.ConnectionProperties; import io.openvidu.server.core.Participant; import io.openvidu.server.core.SessionManager; import io.openvidu.server.core.Token; @@ -98,24 +97,24 @@ public class SessionGarbageCollectorIntegrationTest { } private String getSessionId() { - String stringResponse = (String) sessionRestController.getSessionId(new HashMap<>()).getBody(); + String stringResponse = (String) sessionRestController.initializeSession(new HashMap<>()).getBody(); return new Gson().fromJson(stringResponse, JsonObject.class).get("id").getAsString(); } private String getToken(String sessionId) { - Map map = new HashMap<>(); - map.put("session", sessionId); - String stringResponse = (String) sessionRestController.newToken(map).getBody(); + String stringResponse = (String) sessionRestController.initializeConnection(sessionId, new HashMap<>()) + .getBody(); return new Gson().fromJson(stringResponse, JsonObject.class).get("token").getAsString(); } private JsonObject listSessions() { - String stringResponse = (String) sessionRestController.listSessions(false).getBody(); + String stringResponse = (String) sessionRestController.listSessions(false, false).getBody(); return new Gson().fromJson(stringResponse, JsonObject.class); } private void joinParticipant(String sessionId, String token) { - Token t = new Token(token, OpenViduRole.PUBLISHER, "SERVER_METADATA", null, null); + ConnectionProperties connectionProperties = new ConnectionProperties.Builder().data("SERVER_METADATA").build(); + Token t = new Token(token, sessionId, connectionProperties, null); String uuid = UUID.randomUUID().toString(); String participantPrivateId = "PARTICIPANT_PRIVATE_ID_" + uuid; String finalUserId = "FINAL_USER_ID_" + uuid; diff --git a/openvidu-server/src/test/java/io/openvidu/server/test/integration/config/IntegrationTestConfiguration.java b/openvidu-server/src/test/java/io/openvidu/server/test/integration/config/IntegrationTestConfiguration.java index 02dc465d..c55fa4d0 100644 --- a/openvidu-server/src/test/java/io/openvidu/server/test/integration/config/IntegrationTestConfiguration.java +++ b/openvidu-server/src/test/java/io/openvidu/server/test/integration/config/IntegrationTestConfiguration.java @@ -14,9 +14,11 @@ import org.kurento.client.KurentoClient; import org.kurento.client.MediaPipeline; import org.kurento.client.ServerManager; import org.mockito.Mockito; +import org.powermock.reflect.Whitebox; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import io.openvidu.server.kurento.core.KurentoSessionManager; import io.openvidu.server.kurento.kms.FixedOneKmsManager; import io.openvidu.server.kurento.kms.Kms; import io.openvidu.server.kurento.kms.KmsManager; @@ -32,12 +34,13 @@ public class IntegrationTestConfiguration { @Bean public KmsManager kmsManager() throws Exception { - final KmsManager spy = Mockito.spy(new FixedOneKmsManager()); + final KmsManager spy = Mockito.spy(new FixedOneKmsManager(new KurentoSessionManager())); doAnswer(invocation -> { List successfullyConnectedKmss = new ArrayList<>(); List kmsProperties = invocation.getArgument(0); for (KmsProperties kmsProp : kmsProperties) { - Kms kms = new Kms(kmsProp, spy.getLoadManager()); + Kms kms = new Kms(kmsProp, Whitebox.getInternalState(spy, "loadManager"), + Whitebox.getInternalState(spy, "quarantineKiller")); KurentoClient kClient = mock(KurentoClient.class); doAnswer(i -> { diff --git a/openvidu-server/src/test/java/io/openvidu/server/test/unit/SDPMungingTest.java b/openvidu-server/src/test/java/io/openvidu/server/test/unit/SDPMungingTest.java index c0beff51..4b3d4ac3 100644 --- a/openvidu-server/src/test/java/io/openvidu/server/test/unit/SDPMungingTest.java +++ b/openvidu-server/src/test/java/io/openvidu/server/test/unit/SDPMungingTest.java @@ -22,61 +22,39 @@ import io.openvidu.server.utils.SDPMunging; public class SDPMungingTest { private SDPMunging sdpMungin = new SDPMunging(); - + private String oldSdp; - + private String newSdp; List h264codecPayloads; - + List forceCodecPayloads; - + String validSDPH264Files[] = new String[]{ - "sdp_kurento_h264_with_profile_id.txt", - "sdp_kurento_h264_without_profile_id_1.txt", - "sdp_chrome84.txt", - "sdp_firefox79.txt", - "sdp_safari13-1.txt" + "sdp_kurento_h264.txt", + "sdp_chrome84.txt", + "sdp_firefox79.txt", + "sdp_safari13-1.txt" }; - + String validSDPVP8Files[] = new String[]{ - "sdp_kurento_h264_with_profile_id.txt", - "sdp_kurento_h264_without_profile_id_1.txt", - "sdp_chrome84.txt", - "sdp_firefox79.txt", - "sdp_safari13-1.txt" + "sdp_kurento_h264.txt", + "sdp_chrome84.txt", + "sdp_firefox79.txt", + "sdp_safari13-1.txt" }; - + String validSDPVP9Files[] = new String[] { - "sdp_chrome84.txt", - "sdp_firefox79.txt" + "sdp_chrome84.txt", + "sdp_firefox79.txt" }; - + String notValidVP9Files[] = new String[] { - "sdp_kurento_h264_with_profile_id.txt", - "sdp_kurento_h264_without_profile_id_1.txt", - "sdp_safari13-1.txt" + "sdp_kurento_h264.txt", + "sdp_safari13-1.txt" }; - - @Test - @DisplayName("[setfmtpH264] Compare 'profile-level-id' ocurrences with Payload-Type H264 ocurrences") - public void checkSDPOcurrenciesOfProfileIdForH264() throws IOException { - for(String sdpFileName: validSDPH264Files) { - initTestsSetfmtp(sdpFileName); - checkOcurrences(); - } - } - - @Test - @DisplayName("[setfmtpH264] In the generated SDP for each H264 Payload-Type, exists one" + - "'a=fmtp:' with a 'profile-level-id' defined") - public void checkOneProfileIdForEachH264Payload() throws IOException { - for(String sdpFileName: validSDPH264Files) { - initTestsSetfmtp(sdpFileName); - checkOneProfileIdForEachH264(); - } - } - + @Test @DisplayName("[setCodecPreference] Force VP8 Codec prevalence in 'm=video' line") public void checkPreferenceCodecVP8() throws IOException { @@ -85,7 +63,6 @@ public class SDPMungingTest { checkPrevalenceCodecInML(); } } - @Test @DisplayName("[setCodecPreference] Force VP8 Codec prevalence in 'm=video' line") public void checkPreferenceCodecVP9() throws IOException { @@ -94,7 +71,6 @@ public class SDPMungingTest { checkPrevalenceCodecInML(); } } - @Test @DisplayName("[setCodecPreference] Force H264 Codec prevalence in 'm=video' line") public void checkPreferenceCodecH264() throws IOException { @@ -103,7 +79,6 @@ public class SDPMungingTest { checkPrevalenceCodecInML(); } } - @Test @DisplayName("[setCodecPreference] Exception when codec does not exists on SDP") public void checkPreferenceCodecException() throws IOException { @@ -113,45 +88,21 @@ public class SDPMungingTest { }); String expectedMessage = "The specified forced codec VP9 is not present in the SDP"; assertTrue(exception.getMessage().contains(expectedMessage)); - } + } } - + private String getSdpFile(String sdpNameFile) throws IOException { - Path sdpFile = Files.createTempFile("sdp-test", ".tmp"); + Path sdpFile = Files.createTempFile("sdp-test", ".tmp"); Files.copy(getClass().getResourceAsStream("/sdp/" + sdpNameFile), sdpFile, StandardCopyOption.REPLACE_EXISTING); String sdpUnformatted = new String(Files.readAllBytes(sdpFile)); return String.join("\r\n", sdpUnformatted.split("\\R+")) + "\r\n"; } - - private void checkOcurrences() { - // Check that new sdp actually contains a line with a=fmtp for - // each h264 defined payload - int allOcurrences = 0; - for(String h264pt: this.h264codecPayloads) { - allOcurrences += ( newSdp.split("m=video")[1].split("a=fmtp:" + h264pt).length ) - 1; - } - assertEquals(allOcurrences, h264codecPayloads.size()); - } - - private void initTestsSetfmtp(String sdpNameFile) throws IOException { - this.oldSdp = getSdpFile(sdpNameFile); - this.newSdp = this.sdpMungin.setfmtpH264(oldSdp); - this.h264codecPayloads = new ArrayList<>(); - - // Get all Payload-Type for h264 - for(String oldSdpLine: oldSdp.split("\\R+")) { - if(oldSdpLine.startsWith("a=rtpmap") && oldSdpLine.endsWith("H264/90000")) { - String pt = oldSdpLine.split(":")[1].split(" ")[0]; - this.h264codecPayloads.add(pt); - } - } - } - + private void initTestsSetCodecPrevalence(VideoCodec codec, String sdpNameFile) throws IOException { this.oldSdp = getSdpFile(sdpNameFile); - this.newSdp = this.sdpMungin.setCodecPreference(codec, oldSdp, true); + this.newSdp = this.sdpMungin.setCodecPreference(codec, oldSdp); this.forceCodecPayloads = new ArrayList<>(); - + // Get all Payload-Type for video Codec for(String oldSdpLine: oldSdp.split("\\R+")) { if(oldSdpLine.startsWith("a=rtpmap") && oldSdpLine.endsWith(codec.name() + "/90000")) { @@ -159,7 +110,6 @@ public class SDPMungingTest { this.forceCodecPayloads.add(pt); } } - // Get all Payload-Types rtx related with codec // Not the best way to do it, but enough to check if the sdp // generated is correct @@ -177,44 +127,14 @@ public class SDPMungingTest { } } } - } + } } } this.forceCodecPayloads.addAll(rtxForcedCodecs); } - - private void checkOneProfileIdForEachH264() throws IOException { - - // Check one profile-id for each h264 Payload-Type - boolean inVideoBlock = false; - int numFoundProfileIds = 0; - for(String newSdpLine: newSdp.split("\\R+")) { - if (!inVideoBlock && newSdpLine.startsWith("m=video")) { - inVideoBlock = true; - continue; - } - - if (inVideoBlock && newSdpLine.startsWith("m=")) { - break; - } - - if (inVideoBlock && newSdpLine.startsWith("a=fmtp:")) { - boolean foundProfileId = false; - for(String h264pt: this.h264codecPayloads) { - foundProfileId = newSdpLine.contains("a=fmtp:") - && newSdpLine.contains(h264pt) && newSdpLine.contains("profile-level-id"); - - if (foundProfileId) { - numFoundProfileIds++; - } - } - } - } - assertEquals(numFoundProfileIds, this.h264codecPayloads.size()); - } - + private void checkPrevalenceCodecInML() { - + String newml = null; String[] newSdpLines = this.newSdp.split("\\R+"); for(String newSdpLine: newSdpLines) { @@ -223,11 +143,11 @@ public class SDPMungingTest { break; } } - + if (newml == null) { fail("'m=video' line not found in SDP"); } - + List newMlCodecPrevalenceList = new ArrayList<>(); String[] lmParams = newml.split(" "); int numOfCodecsWithPrevalence = this.forceCodecPayloads.size(); @@ -236,7 +156,6 @@ public class SDPMungingTest { for(int i = indexStartCodecs; i < indexEndPreferencedCodecs; i++) { newMlCodecPrevalenceList.add(lmParams[i]); } - for(int j = 0; j < numOfCodecsWithPrevalence; j++) { String codecToCheck = newMlCodecPrevalenceList.get(j); boolean codecFoundInPrevalenceList = false; diff --git a/openvidu-server/src/test/resources/integration-test.properties b/openvidu-server/src/test/resources/integration-test.properties index 227addf0..9c9b57e6 100644 --- a/openvidu-server/src/test/resources/integration-test.properties +++ b/openvidu-server/src/test/resources/integration-test.properties @@ -24,8 +24,7 @@ OPENVIDU_WEBHOOK_HEADERS=[] OPENVIDU_WEBHOOK_EVENTS=["sessionCreated","sessionDestroyed","participantJoined","participantLeft","webrtcConnectionCreated","webrtcConnectionDestroyed","recordingStatusChanged","filterEventDispatched","mediaNodeStatusChanged"] OPENVIDU_RECORDING=false -OPENVIDU_RECORDING_DEBUG=false -OPENVIDU_RECORDING_VERSION=2.15.0 +OPENVIDU_RECORDING_VERSION=2.16.0 OPENVIDU_RECORDING_PATH=/opt/openvidu/recordings OPENVIDU_RECORDING_PUBLIC_ACCESS=false OPENVIDU_RECORDING_NOTIFICATION=publisher_moderator @@ -37,6 +36,8 @@ OPENVIDU_STREAMS_VIDEO_MAX_RECV_BANDWIDTH=1000 OPENVIDU_STREAMS_VIDEO_MIN_RECV_BANDWIDTH=300 OPENVIDU_STREAMS_VIDEO_MAX_SEND_BANDWIDTH=1000 OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH=300 +OPENVIDU_STREAMS_FORCED_VIDEO_CODEC=VP8 +OPENVIDU_STREAMS_ALLOW_TRANSCODING=false OPENVIDU_SESSIONS_GARBAGE_INTERVAL=900 OPENVIDU_SESSIONS_GARBAGE_THRESHOLD=3600 diff --git a/openvidu-server/src/test/resources/sdp/sdp_kurento_h264.txt b/openvidu-server/src/test/resources/sdp/sdp_kurento_h264.txt new file mode 100644 index 00000000..18d81183 --- /dev/null +++ b/openvidu-server/src/test/resources/sdp/sdp_kurento_h264.txt @@ -0,0 +1,61 @@ +v=0 +o=- 3808465464 3808465464 IN IP4 0.0.0.0 +s=Kurento Media Server +c=IN IP4 0.0.0.0 +t=0 0 +a=msid-semantic: WMS m0W2gMak7LgkgzgJeDQhxBX0ivcsejWjQ0jD +a=group:BUNDLE 0 1 +m=audio 1 UDP/TLS/RTP/SAVPF 111 0 +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=recvonly +a=mid:0 +a=rtcp:9 IN IP4 0.0.0.0 +a=rtpmap:111 opus/48000/2 +a=rtpmap:0 PCMU/8000 +a=setup:active +a=rtcp-mux +a=fmtp:111 minptime=10;useinbandfec=1 +a=ssrc:1929271881 cname:user4129876135@host-ed881df6 +a=ice-ufrag:cXmf +a=ice-pwd:9giZcfpsuoHRuxCgbnCLRy +a=fingerprint:sha-256 C8:D4:B5:56:A7:89:E5:E1:C8:28:0A:47:2B:49:F6:7A:E2:2E:B3:0A:40:10:AD:79:82:E7:FD:A0:ED:6C:F6:51 +m=video 1 UDP/TLS/RTP/SAVPF 96 102 127 125 108 +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=recvonly +a=mid:1 +a=rtcp:9 IN IP4 0.0.0.0 +a=rtpmap:96 VP8/90000 +a=rtpmap:102 H264/90000 +a=rtpmap:127 H264/90000 +a=rtpmap:125 H264/90000 +a=rtpmap:108 H264/90000 +a=rtcp-fb:96 goog-remb +a=rtcp-fb:96 ccm fir +a=rtcp-fb:96 nack +a=rtcp-fb:96 nack pli +a=rtcp-fb:102 goog-remb +a=rtcp-fb:102 ccm fir +a=rtcp-fb:102 nack +a=rtcp-fb:102 nack pli +a=rtcp-fb:127 goog-remb +a=rtcp-fb:127 ccm fir +a=rtcp-fb:127 nack +a=rtcp-fb:127 nack pli +a=rtcp-fb:125 goog-remb +a=rtcp-fb:125 ccm fir +a=rtcp-fb:125 nack +a=rtcp-fb:125 nack pli +a=rtcp-fb:108 goog-remb +a=rtcp-fb:108 ccm fir +a=rtcp-fb:108 nack +a=rtcp-fb:108 nack pli +a=setup:active +a=rtcp-mux +a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f +a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f +a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f +a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f +a=ssrc:3762875210 cname:user4129876135@host-ed881df6 +a=ice-ufrag:cXmf +a=ice-pwd:9giZcfpsuoHRuxCgbnCLRy +a=fingerprint:sha-256 C8:D4:B5:56:A7:89:E5:E1:C8:28:0A:47:2B:49:F6:7A:E2:2E:B3:0A:40:10:AD:79:82:E7:FD:A0:ED:6C:F6:51 \ No newline at end of file diff --git a/openvidu-test-browsers/pom.xml b/openvidu-test-browsers/pom.xml index 25519eba..77e2d039 100644 --- a/openvidu-test-browsers/pom.xml +++ b/openvidu-test-browsers/pom.xml @@ -47,8 +47,8 @@ - - 1.8 + + 11 ${java.version} ${java.version} @@ -77,16 +77,6 @@ selenium-java ${version.selenium} - - org.seleniumhq.selenium - selenium-chrome-driver - ${version.selenium} - - - org.seleniumhq.selenium - selenium-firefox-driver - ${version.selenium} - com.google.code.gson gson @@ -97,6 +87,21 @@ unirest-java ${version.unirest} + + org.jcodec + jcodec-javase + ${version.jcodec} + + + io.openvidu + openvidu-java-client + ${version.openvidu.java.client} + + + junit + junit + ${version.junit} + diff --git a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/BrowserUser.java b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/BrowserUser.java index bfe491b8..8b38e509 100644 --- a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/BrowserUser.java +++ b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/BrowserUser.java @@ -17,7 +17,11 @@ package io.openvidu.test.browsers; +import java.time.Duration; +import java.time.temporal.ChronoUnit; + import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.ui.ExpectedCondition; import org.openqa.selenium.support.ui.WebDriverWait; import org.slf4j.LoggerFactory; @@ -64,4 +68,10 @@ public class BrowserUser { this.driver.quit(); } + public void waitWithNewTime(int newWaitTime, ExpectedCondition condition) { + this.waiter.withTimeout(Duration.of(newWaitTime, ChronoUnit.SECONDS)); + this.waiter.until(condition); + this.waiter.withTimeout(Duration.of(this.timeOfWaitInSeconds, ChronoUnit.SECONDS)); + } + } \ No newline at end of file diff --git a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/ChromeUser.java b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/ChromeUser.java index b6fe64e1..3ffca920 100644 --- a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/ChromeUser.java +++ b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/ChromeUser.java @@ -51,7 +51,7 @@ public class ChromeUser extends BrowserUser { options.setUnhandledPromptBehaviour(UnexpectedAlertBehaviour.IGNORE); options.addArguments("--disable-infobars"); - options.setExperimentalOption("excludeSwitches", new String[]{"enable-automation"}); + options.setExperimentalOption("excludeSwitches", new String[] { "enable-automation" }); Map prefs = new HashMap(); prefs.put("profile.default_content_setting_values.media_stream_mic", 1); @@ -84,6 +84,9 @@ public class ChromeUser extends BrowserUser { // This flag selects the entire screen as video source when screen sharing options.addArguments("--auto-select-desktop-capture-source=Entire screen"); + // Background Chrome + // options.addArguments("--headless"); + if (runningAsRoot) { options.addArguments("--no-sandbox"); } diff --git a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/FirefoxUser.java b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/FirefoxUser.java index 6c3b39b2..76c2d216 100644 --- a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/FirefoxUser.java +++ b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/FirefoxUser.java @@ -22,6 +22,7 @@ import java.net.URL; import org.openqa.selenium.UnexpectedAlertBehaviour; import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.firefox.FirefoxOptions; import org.openqa.selenium.firefox.FirefoxProfile; import org.openqa.selenium.remote.CapabilityType; import org.openqa.selenium.remote.DesiredCapabilities; @@ -29,7 +30,7 @@ import org.openqa.selenium.remote.RemoteWebDriver; public class FirefoxUser extends BrowserUser { - public FirefoxUser(String userName, int timeOfWaitInSeconds) { + public FirefoxUser(String userName, int timeOfWaitInSeconds, boolean disableOpenH264) { super(userName, timeOfWaitInSeconds); DesiredCapabilities capabilities = DesiredCapabilities.firefox(); @@ -42,6 +43,10 @@ public class FirefoxUser extends BrowserUser { // This flag force to use fake user media (synthetic video of multiple color) profile.setPreference("media.navigator.streams.fake", true); + if (disableOpenH264) { + profile.setPreference("media.gmp-gmpopenh264.enabled", false); + } + capabilities.setCapability(FirefoxDriver.PROFILE, profile); String REMOTE_URL = System.getProperty("REMOTE_URL_FIREFOX"); @@ -54,7 +59,7 @@ public class FirefoxUser extends BrowserUser { } } else { log.info("Using local web driver"); - this.driver = new FirefoxDriver(capabilities); + this.driver = new FirefoxDriver(new FirefoxOptions(capabilities)); } this.configureDriver(); diff --git a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/OperaUser.java b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/OperaUser.java index 0de43097..eb56e400 100644 --- a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/OperaUser.java +++ b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/OperaUser.java @@ -4,10 +4,8 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.concurrent.TimeUnit; -import org.openqa.selenium.UnexpectedAlertBehaviour; import org.openqa.selenium.opera.OperaDriver; import org.openqa.selenium.opera.OperaOptions; -import org.openqa.selenium.remote.CapabilityType; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; @@ -16,27 +14,28 @@ public class OperaUser extends BrowserUser { public OperaUser(String userName, int timeOfWaitInSeconds) { super(userName, timeOfWaitInSeconds); - OperaOptions options = new OperaOptions(); - options.setBinary("/usr/bin/opera"); DesiredCapabilities capabilities = DesiredCapabilities.operaBlink(); capabilities.setAcceptInsecureCerts(true); - capabilities.setCapability(CapabilityType.UNEXPECTED_ALERT_BEHAVIOUR, UnexpectedAlertBehaviour.IGNORE); - + OperaOptions options = new OperaOptions(); + // This flag avoids to grant the user media options.addArguments("--use-fake-ui-for-media-stream"); + // This flag fakes user media with synthetic video options.addArguments("--use-fake-device-for-media-stream"); - capabilities.setCapability(OperaOptions.CAPABILITY, options); + // This flag selects the entire screen as video source when screen sharing + options.addArguments("--auto-select-desktop-capture-source=Entire screen"); + options.merge(capabilities); String REMOTE_URL = System.getProperty("REMOTE_URL_OPERA"); if (REMOTE_URL != null) { log.info("Using URL {} to connect to remote web driver", REMOTE_URL); try { - this.driver = new RemoteWebDriver(new URL(REMOTE_URL), capabilities); + this.driver = new RemoteWebDriver(new URL(REMOTE_URL), options); } catch (MalformedURLException e) { e.printStackTrace(); } } else { log.info("Using local web driver"); - this.driver = new OperaDriver(capabilities); + this.driver = new OperaDriver(options); } this.driver.manage().timeouts().setScriptTimeout(this.timeOfWaitInSeconds, TimeUnit.SECONDS); diff --git a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/CustomHttpClient.java b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/CustomHttpClient.java index 1228b190..6daa0339 100644 --- a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/CustomHttpClient.java +++ b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/CustomHttpClient.java @@ -24,7 +24,7 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Base64; -import java.util.Map; +import java.util.Map.Entry; import javax.net.ssl.SSLContext; @@ -33,14 +33,14 @@ import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.apache.http.impl.client.HttpClients; import org.apache.http.ssl.SSLContextBuilder; -import org.json.JSONArray; -import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; import com.google.gson.JsonSyntaxException; import com.mashape.unirest.http.HttpMethod; import com.mashape.unirest.http.HttpResponse; @@ -89,97 +89,144 @@ public class CustomHttpClient { return this.commonRest(method, path, body, status); } - public JsonObject rest(HttpMethod method, String path, String body, int status, boolean exactReturnedFields, - String jsonReturnedValue) throws Exception { - JsonObject json = this.commonRest(method, path, body, status); - JsonObject jsonObjExpected = null; - jsonReturnedValue.replaceAll("'", "\""); + /** + * "matchKeys" to true for the returned JSON to have the exact same keys as the + * expected JSON (same number, position and name). If false, then any key + * existing in the expected JSON must exist in the returned JSON, but the + * returned JSON may have extra keys not available in expected JSON. + * + * "matchValues" to true for the returned JSON to have the exact same value in + * all the key-value pairs declared in the expected JSON. If the returned JSON + * does not have any of the keys of the expected JSON, an Error is thrown. The + * value comparison applies to NULL, JSON arrays, JSON objects or primitive + * values, at all nested levels. + * + * "matchArrays" to true for the returned JSON to have the exact same JSON array + * as value that any array property of the expected JSON. That includes value + * and order. To false to check only that JSON array is the type of the returned + * value, but not to check its content. If "matchValues" is false, then this + * property will not have effect and shall be considered false. + */ + public JsonObject rest(HttpMethod method, String path, String body, int status, boolean matchKeys, + boolean matchValues, boolean matchArrays, String jsonReturnedValue) throws Exception { + JsonObject jsonExpected = null; + jsonReturnedValue = jsonReturnedValue.replaceAll("'", "\""); try { - jsonObjExpected = JsonParser.parseString(jsonReturnedValue).getAsJsonObject(); + jsonExpected = JsonParser.parseString(jsonReturnedValue).getAsJsonObject(); } catch (JsonSyntaxException e1) { - throw new Exception("Expected json element is a string without a JSON format: " + jsonReturnedValue); + throw new Exception("Expected JSON element is a string without a JSON format: " + jsonReturnedValue); } + JsonObject jsonActual = this.commonRest(method, path, body, status); + check(jsonExpected, jsonActual, matchKeys, matchValues, matchArrays); + return jsonActual; - if (exactReturnedFields) { - if (jsonObjExpected.size() != json.size()) { - throw new Exception( - "Error in number of keys in JSON response to POST (" + json.toString() + ")" + path); - } - } - for (String key : jsonObjExpected.keySet()) { - Class c1 = jsonObjExpected.get(key).getClass(); - Class c2 = json.get(key).getClass(); - - c1 = unifyNumberType(c1); - c2 = unifyNumberType(c2); - - if (!c1.equals(c2)) { - throw new Exception("Wrong class of property " + key); - } - } - return json; } - public JsonObject rest(HttpMethod method, String path, String body, int status, boolean exactReturnedFields, - Map jsonResponse) throws Exception { - JsonObject json = this.commonRest(method, path, body, status); - - if (exactReturnedFields) { - if (jsonResponse.size() != json.size()) - throw new Exception("Error in number of keys in JSON response to POST " + path); - } - - for (Map.Entry entry : jsonResponse.entrySet()) { - Object value = entry.getValue(); - - if (value instanceof String) { - try { - JsonObject jsonObjExpected = JsonParser.parseString((String) value).getAsJsonObject(); - JsonObject jsonObjActual = json.get(entry.getKey()).getAsJsonObject(); - // COMPARE - - } catch (JsonSyntaxException e1) { - try { - JsonArray jsonArrayExpected = JsonParser.parseString((String) value).getAsJsonArray(); - JsonArray jsonArrayActual = json.get(entry.getKey()).getAsJsonArray(); - // COMPARE - - } catch (JsonSyntaxException e2) { - if (((String) value) != json.get(entry.getKey()).getAsString()) { - throw new Exception("JSON field " + entry.getKey() + " has not expected value. Expected: " - + value + ". Actual: " + json.get(entry.getKey()).getAsString()); - } - } + public static void check(JsonObject jsonExpected, JsonObject jsonActual, boolean matchKeys, boolean matchValues, + boolean matchArrays) throws Exception { + if (matchKeys) { + checkSameKeys(jsonExpected, jsonActual, null, matchValues, matchArrays); + } else { + for (String key : jsonExpected.keySet()) { + JsonElement elExpected = jsonExpected.get(key); + JsonElement elActual = jsonActual.get(key); + if (elActual == null) { + throw new Exception( + "Expected property \"" + key + "\" did not exist in actual JSON " + jsonActual.toString()); + } + checkSameType(elExpected, elActual, key, matchValues); + } + } + } + + public static void checkSameKeys(JsonElement expected, JsonElement actual, String parent, boolean matchValues, + boolean matchArrays) throws Exception { + if (!expected.isJsonObject()) { + if (expected.isJsonArray()) { + JsonArray expectedArray = expected.getAsJsonArray(); + JsonArray actualArray = actual.getAsJsonArray(); + if (matchArrays) { + checkSameType(expectedArray, actualArray, parent, matchValues); + } + } else { + checkSameType(expected, actual, parent, matchValues); + } + } else { + JsonObject exp = expected.getAsJsonObject(); + JsonObject act = actual.getAsJsonObject(); + if (exp.size() != act.size()) { + throw new Exception("Error in number of keys in JSON object. Expected " + exp.size() + ". Actual: " + + act.size() + ". Actual object: " + act.toString()); + } + for (Entry entry : exp.entrySet()) { + String key = entry.getKey(); + if (!act.has(key)) { + throw new Exception("Property \"" + key + "\" is missing in actual object " + act.toString()); + } + checkSameKeys(entry.getValue(), act.get(key), key, matchValues, matchArrays); + } + } + } + + public static void checkSameType(JsonElement expected, JsonElement actual, String key, boolean checkAlsoSameValue) + throws Exception { + if (!expected.getClass().equals(actual.getClass())) { + throw new Exception("Expected JSON element \"" + key + + "\" has not the same class as the actual JSON element. Expected: " + + expected.getClass().getSimpleName() + ". Actual: " + actual.getClass().getSimpleName()); + } + if (expected.isJsonNull()) { + if (!actual.isJsonNull()) { + throw new Exception("Actual JSON element should be null"); + } + } + if (expected.isJsonArray()) { + if (!actual.isJsonArray()) { + throw new Exception("Actual JSON element should be a JSON array"); + } + JsonArray arrayExpected = expected.getAsJsonArray(); + JsonArray arrayActual = actual.getAsJsonArray(); + if (checkAlsoSameValue && !arrayExpected.equals(arrayActual)) { + throw new Exception("Property \"" + key + "\" expected an array " + arrayExpected.toString() + + " but found " + arrayActual.toString()); + } + } + if (expected.isJsonObject()) { + if (!actual.isJsonObject()) { + throw new Exception("Actual JSON element should be a JSON object"); + } + JsonObject objectExpected = expected.getAsJsonObject(); + JsonObject objectActual = actual.getAsJsonObject(); + if (checkAlsoSameValue && !objectExpected.equals(objectActual)) { + throw new Exception("Property \"" + key + "\" expected a JSON object " + objectExpected.toString() + + " but found " + objectActual.toString()); + } + } + if (expected.isJsonPrimitive()) { + JsonPrimitive primitive1 = expected.getAsJsonPrimitive(); + JsonPrimitive primitive2 = actual.getAsJsonPrimitive(); + if (primitive1.isString()) { + String string1 = primitive1.getAsString(); + String string2 = primitive2.getAsString(); + if (checkAlsoSameValue && !string1.equals(string2)) { + throw new Exception("Property \"" + key + "\" expected " + string1 + " but was " + string2); + } + } + if (primitive1.isBoolean()) { + boolean boolean1 = primitive1.getAsBoolean(); + boolean boolean2 = primitive2.getAsBoolean(); + if (checkAlsoSameValue && !boolean1 == boolean2) { + throw new Exception("Property \"" + key + "\" expected " + boolean1 + " but was " + boolean2); + } + } + if (primitive1.isNumber()) { + Number number1 = primitive1.getAsNumber(); + Number number2 = primitive2.getAsNumber(); + if (checkAlsoSameValue && !number1.equals(number2)) { + throw new Exception("Property \"" + key + "\" expected " + number1 + " but was " + number2); } - } else if (value instanceof Integer) { - if (((int) value) != json.get(entry.getKey()).getAsInt()) { - throw new Exception("JSON field " + entry.getKey() + " has not expected value. Expected: " + value - + ". Actual: " + json.get(entry.getKey()).getAsInt()); - } - } else if (value instanceof Long) { - if (((long) value) != json.get(entry.getKey()).getAsLong()) { - throw new Exception("JSON field " + entry.getKey() + " has not expected value. Expected: " + value - + ". Actual: " + json.get(entry.getKey()).getAsLong()); - } - } else if (value instanceof Double) { - if (((double) value) != json.get(entry.getKey()).getAsDouble()) { - throw new Exception("JSON field " + entry.getKey() + " has not expected value. Expected: " + value - + ". Actual: " + json.get(entry.getKey()).getAsDouble()); - } - } else if (value instanceof Boolean) { - if (((boolean) value) != json.get(entry.getKey()).getAsBoolean()) { - throw new Exception("JSON field " + entry.getKey() + " has not expected value. Expected: " + value - + ". Actual: " + json.get(entry.getKey()).getAsBoolean()); - } - } else if (value instanceof JSONArray || value instanceof JsonArray) { - JsonParser.parseString(entry.getValue().toString()).getAsJsonArray(); - } else if (value instanceof JSONObject || value instanceof JsonObject) { - JsonParser.parseString(entry.getValue().toString()).getAsJsonObject(); - } else { - throw new Exception("JSON response field cannot be parsed: " + entry.toString()); } } - return json; } public void shutdown() throws IOException { @@ -262,11 +309,4 @@ public class CustomHttpClient { return json; } - private Class unifyNumberType(Class myClass) { - if (Number.class.isAssignableFrom(myClass)) { - return Number.class; - } - return myClass; - } - } diff --git a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/RecordingUtils.java b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/RecordingUtils.java new file mode 100644 index 00000000..0a5e9971 --- /dev/null +++ b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/RecordingUtils.java @@ -0,0 +1,264 @@ +package io.openvidu.test.browsers.utils; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import javax.imageio.ImageIO; + +import org.jcodec.api.FrameGrab; +import org.jcodec.api.JCodecException; +import org.jcodec.common.model.Picture; +import org.jcodec.scale.AWTUtil; +import org.junit.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.stream.JsonReader; + +import io.openvidu.java.client.Recording; + +public class RecordingUtils { + + protected static final Logger log = LoggerFactory.getLogger(RecordingUtils.class); + + public boolean recordedGreenFileFine(File file, Recording recording) throws IOException { + return this.recordedFileFine(file, recording, RecordingUtils::checkVideoAverageRgbGreen); + } + + public boolean recordedRedFileFine(File file, Recording recording) throws IOException { + return this.recordedFileFine(file, recording, RecordingUtils::checkVideoAverageRgbRed); + } + + private boolean recordedFileFine(File file, Recording recording, + Function, Boolean> colorCheckFunction) throws IOException { + this.checkMultimediaFile(file, recording.hasAudio(), recording.hasVideo(), recording.getDuration(), + recording.getResolution(), "aac", "h264", true); + + boolean isFine = false; + Picture frame; + try { + // Get a frame at 75% duration and check that it has the expected color + frame = FrameGrab.getFrameAtSec(file, (double) (recording.getDuration() * 0.75)); + BufferedImage image = AWTUtil.toBufferedImage(frame); + Map colorMap = this.averageColor(image); + + String realResolution = image.getWidth() + "x" + image.getHeight(); + Assert.assertEquals( + "Resolution (" + recording.getResolution() + + ") of recording entity is not equal to real video resolution (" + realResolution + ")", + recording.getResolution(), realResolution); + + log.info("Recording map color: {}", colorMap.toString()); + log.info("Recording frame below"); + System.out.println(bufferedImageToBase64PngString(image)); + isFine = colorCheckFunction.apply(colorMap); + } catch (IOException | JCodecException e) { + log.warn("Error getting frame from video recording: {}", e.getMessage()); + isFine = false; + } + return isFine; + } + + public static boolean checkVideoAverageRgbGreen(Map rgb) { + // GREEN color: {r < 15, g > 130, b <15} + return (rgb.get("r") < 15) && (rgb.get("g") > 130) && (rgb.get("b") < 15); + } + + public static boolean checkVideoAverageRgbGray(Map rgb) { + // GRAY color: {r < 50, g < 50, b < 50} and the absolute difference between them + // not greater than 2 + return (rgb.get("r") < 50) && (rgb.get("g") < 50) && (rgb.get("b") < 50) + && (Math.abs(rgb.get("r") - rgb.get("g")) <= 2) && (Math.abs(rgb.get("r") - rgb.get("b")) <= 2) + && (Math.abs(rgb.get("b") - rgb.get("g")) <= 2); + } + + public static boolean checkVideoAverageRgbRed(Map rgb) { + // RED color: {r > 240, g < 15, b <15} + return (rgb.get("r") > 240) && (rgb.get("g") < 15) && (rgb.get("b") < 15); + } + + private String bufferedImageToBase64PngString(BufferedImage image) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + String imageString = null; + try { + ImageIO.write(image, "png", bos); + byte[] imageBytes = bos.toByteArray(); + imageString = "data:image/png;base64," + Base64.getEncoder().encodeToString(imageBytes); + bos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return imageString; + } + + public void checkIndividualRecording(String recPath, Recording recording, int numberOfVideoFiles, + String audioDecoder, String videoDecoder, boolean checkAudio) throws IOException { + + // Should be only 2 files: zip and metadata + File folder = new File(recPath); + Assert.assertEquals("There are more than 2 files (ZIP and metadata) inside individual recording folder " + + recPath + ": " + Arrays.toString(folder.listFiles()), 2, folder.listFiles().length); + + File file1 = new File(recPath + recording.getName() + ".zip"); + File file2 = new File(recPath + ".recording." + recording.getId()); + + Assert.assertTrue("File " + file1.getAbsolutePath() + " does not exist or is empty", + file1.exists() && file1.length() > 0); + Assert.assertTrue("File " + file2.getAbsolutePath() + " does not exist or is empty", + file2.exists() && file2.length() > 0); + + List unzippedWebmFiles = new Unzipper().unzipFile(recPath, recording.getName() + ".zip"); + + Assert.assertEquals("Expecting " + numberOfVideoFiles + " videos inside ZIP file but " + + unzippedWebmFiles.size() + " found: " + unzippedWebmFiles.toString(), numberOfVideoFiles, + unzippedWebmFiles.size()); + + File jsonSyncFile = new File(recPath + recording.getName() + ".json"); + Assert.assertTrue("JSON sync file " + jsonSyncFile.getAbsolutePath() + "does not exist or is empty", + jsonSyncFile.exists() && jsonSyncFile.length() > 0); + + JsonObject jsonSyncMetadata; + try { + Gson gson = new Gson(); + JsonReader reader = new JsonReader(new FileReader(jsonSyncFile)); + jsonSyncMetadata = gson.fromJson(reader, JsonObject.class); + } catch (Exception e) { + log.error("Cannot read JSON sync metadata file from {}. Error: {}", jsonSyncFile.getAbsolutePath(), + e.getMessage()); + Assert.fail("Cannot read JSON sync metadata file from " + jsonSyncFile.getAbsolutePath()); + return; + } + + long totalFileSize = 0; + JsonArray syncArray = jsonSyncMetadata.get("files").getAsJsonArray(); + for (File webmFile : unzippedWebmFiles) { + totalFileSize += webmFile.length(); + + Assert.assertTrue("WEBM file " + webmFile.getAbsolutePath() + " does not exist or is empty", + webmFile.exists() && webmFile.length() > 0); + + double durationInSeconds = 0; + boolean found = false; + for (int i = 0; i < syncArray.size(); i++) { + JsonObject j = syncArray.get(i).getAsJsonObject(); + if (webmFile.getName().contains(j.get("streamId").getAsString())) { + durationInSeconds = (double) (j.get("endTimeOffset").getAsDouble() + - j.get("startTimeOffset").getAsDouble()) / 1000; + found = true; + break; + } + } + + Assert.assertTrue("Couldn't find in JSON sync object information for webm file " + webmFile.getName(), + found); + + log.info("Duration of {} according to sync metadata json file: {} s", webmFile.getName(), + durationInSeconds); + this.checkMultimediaFile(webmFile, recording.hasAudio(), recording.hasVideo(), durationInSeconds, + recording.getResolution(), audioDecoder, videoDecoder, checkAudio); + webmFile.delete(); + } + + Assert.assertEquals("Size of recording entity (" + recording.getSessionId() + + ") is not equal to real file size (" + totalFileSize + ")", recording.getSize(), totalFileSize); + + jsonSyncFile.delete(); + } + + public void checkMultimediaFile(File file, boolean hasAudio, boolean hasVideo, double duration, String resolution, + String audioDecoder, String videoDecoder, boolean checkAudio) throws IOException { + // Check tracks, duration, resolution, framerate and decoders + MultimediaFileMetadata metadata = new MultimediaFileMetadata(file.getAbsolutePath()); + + if (hasVideo) { + if (checkAudio) { + if (hasAudio) { + Assert.assertTrue("Media file " + file.getAbsolutePath() + " should have audio", + metadata.hasAudio() && metadata.hasVideo()); + Assert.assertTrue(metadata.getAudioDecoder().toLowerCase().contains(audioDecoder)); + } else { + Assert.assertTrue("Media file " + file.getAbsolutePath() + " should have video", + metadata.hasVideo()); + Assert.assertFalse(metadata.hasAudio()); + } + } + if (resolution != null) { + Assert.assertEquals(resolution, metadata.getVideoWidth() + "x" + metadata.getVideoHeight()); + } + Assert.assertTrue(metadata.getVideoDecoder().toLowerCase().contains(videoDecoder)); + } else if (hasAudio && checkAudio) { + Assert.assertTrue(metadata.hasAudio()); + Assert.assertFalse(metadata.hasVideo()); + Assert.assertTrue(metadata.getAudioDecoder().toLowerCase().contains(audioDecoder)); + } else { + Assert.fail("Cannot check a file witho no audio and no video"); + } + // Check duration with 1 decimal precision + DecimalFormat df = new DecimalFormat("#0.0"); + df.setRoundingMode(RoundingMode.UP); + log.info("Duration of {} according to ffmpeg: {} s", file.getName(), metadata.getDuration()); + log.info("Duration of {} according to 'duration' property: {} s", file.getName(), duration); + log.info("Difference in s duration: {}", Math.abs(metadata.getDuration() - duration)); + final double difference = 10; + Assert.assertTrue( + "Difference between recording entity duration (" + duration + ") and real video duration (" + + metadata.getDuration() + ") is greater than " + difference + " in file " + file.getName(), + Math.abs((metadata.getDuration() - duration)) < difference); + } + + public boolean thumbnailIsFine(File file, Function, Boolean> colorCheckFunction) { + boolean isFine = false; + BufferedImage image = null; + try { + image = ImageIO.read(file); + } catch (IOException e) { + log.error(e.getMessage()); + return false; + } + log.info("Recording thumbnail dimensions: {}x{}", image.getWidth(), image.getHeight()); + Map colorMap = this.averageColor(image); + log.info("Thumbnail map color: {}", colorMap.toString()); + isFine = colorCheckFunction.apply(colorMap); + return isFine; + } + + private Map averageColor(BufferedImage bi) { + int x0 = 0; + int y0 = 0; + int w = bi.getWidth(); + int h = bi.getHeight(); + int x1 = x0 + w; + int y1 = y0 + h; + long sumr = 0, sumg = 0, sumb = 0; + for (int x = x0; x < x1; x++) { + for (int y = y0; y < y1; y++) { + Color pixel = new Color(bi.getRGB(x, y)); + sumr += pixel.getRed(); + sumg += pixel.getGreen(); + sumb += pixel.getBlue(); + } + } + int num = w * h; + Map colorMap = new HashMap<>(); + colorMap.put("r", (long) (sumr / num)); + colorMap.put("g", (long) (sumg / num)); + colorMap.put("b", (long) (sumb / num)); + return colorMap; + } + +} diff --git a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/layout/CustomLayoutHandler.java b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/layout/CustomLayoutHandler.java new file mode 100644 index 00000000..a1ca17b1 --- /dev/null +++ b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/layout/CustomLayoutHandler.java @@ -0,0 +1,41 @@ +package io.openvidu.test.browsers.utils.layout; + +import java.util.concurrent.CountDownLatch; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.EventListener; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@SpringBootApplication +public class CustomLayoutHandler extends WebSecurityConfigurerAdapter implements WebMvcConfigurer { + + private static ConfigurableApplicationContext context; + public static CountDownLatch initLatch; + + public static void main(String[] args, CountDownLatch initLatch) { + CustomLayoutHandler.initLatch = initLatch; + CustomLayoutHandler.context = new SpringApplicationBuilder(CustomLayoutHandler.class) + .properties("spring.config.location:classpath:aplication-pro-layout-handler.properties").build() + .run(args); + } + + @Override + protected void configure(HttpSecurity security) throws Exception { + security.httpBasic().disable(); + } + + @EventListener(ApplicationReadyEvent.class) + public void afterStartup() { + CustomLayoutHandler.initLatch.countDown(); + } + + public static void shutDown() { + CustomLayoutHandler.context.close(); + } + +} diff --git a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/CustomWebhook.java b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/webhook/CustomWebhook.java similarity index 97% rename from openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/CustomWebhook.java rename to openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/webhook/CustomWebhook.java index 2737785f..06d05e74 100644 --- a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/CustomWebhook.java +++ b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/webhook/CustomWebhook.java @@ -15,7 +15,7 @@ * */ -package io.openvidu.test.browsers.utils; +package io.openvidu.test.browsers.utils.webhook; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; @@ -59,6 +59,10 @@ public class CustomWebhook { CustomWebhook.context.close(); } + public static void clean() { + CustomWebhook.events.clear(); + } + public synchronized static JsonObject waitForEvent(String eventName, int maxSecondsWait) throws Exception { if (events.get(eventName) == null) { events.put(eventName, new LinkedBlockingDeque<>()); diff --git a/openvidu-test-browsers/src/main/resources/aplication-pro-layout-handler.properties b/openvidu-test-browsers/src/main/resources/aplication-pro-layout-handler.properties new file mode 100644 index 00000000..10fc87dd --- /dev/null +++ b/openvidu-test-browsers/src/main/resources/aplication-pro-layout-handler.properties @@ -0,0 +1,3 @@ +server.port=5555 +server.ssl.enabled=false +security.basic.enabled=false \ No newline at end of file diff --git a/openvidu-test-browsers/src/main/resources/static/index.html b/openvidu-test-browsers/src/main/resources/static/index.html new file mode 100644 index 00000000..ca45b3f7 --- /dev/null +++ b/openvidu-test-browsers/src/main/resources/static/index.html @@ -0,0 +1,55 @@ + + + + + + + + + +
    + + + + + \ No newline at end of file diff --git a/openvidu-test-browsers/src/test/java/io/openvidu/test/browsers/CustomHttpClientTest.java b/openvidu-test-browsers/src/test/java/io/openvidu/test/browsers/CustomHttpClientTest.java new file mode 100644 index 00000000..3fd27cbc --- /dev/null +++ b/openvidu-test-browsers/src/test/java/io/openvidu/test/browsers/CustomHttpClientTest.java @@ -0,0 +1,124 @@ +package io.openvidu.test.browsers; + +import org.junit.Assert; +import org.junit.Test; + +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + +import io.openvidu.test.browsers.utils.CustomHttpClient; + +public class CustomHttpClientTest { + + @Test + public void testOneLevel() throws Exception { + String expected = "{}"; + String actual = "{}"; + executeCheck(expected, actual, true, true, true); + expected = "{'prop1':'val1'}"; + actual = "{'prop1':'val1'}"; + executeCheck(expected, actual, true, true, true); + expected = "{'prop1':'val1','prop2':'val2'}"; + actual = "{'prop1':'val1','prop2':'val2'}"; + executeCheck(expected, actual, true, true, true); + expected = "{'prop1':'val1'}"; + actual = "{'prop1':'val1','prop2':'val2'}"; + executeCheck(expected, actual, false, true, true); + expected = "{'prop1':'val1','prop2':'val2'}"; + actual = "{'prop1':'WRONG','prop2':'WRONG'}"; + executeCheck(expected, actual, true, false, true); + expected = "{'prop1':'val1','prop2':[{},{}]}"; + actual = "{'prop1':'WRONG','prop2':[{}]}"; + executeCheck(expected, actual, true, false, true); + } + + @Test + public void testMultipleLevels() throws Exception { + String expected = "{'prop1':{'prop2':'val2'}}"; + String actual = "{'prop1':{'prop2':'val2'}}"; + executeCheck(expected, actual, true, true, true); + expected = "{'prop1':'val1','prop2':{'prop3':'val3'}}"; + actual = "{'prop1':'val1','prop2':{'prop3':'val3'}}"; + executeCheck(expected, actual, true, true, true); + expected = "{'prop1':'val1','prop2':{'prop3':'val3'}}"; + actual = "{'prop1':'WRONG','prop2':{'prop3':'WRONG'}}"; + executeCheck(expected, actual, true, false, true); + Assert.assertThrows(IllegalStateException.class, () -> { + String expected2 = "{'prop1':'val1','prop2':{'prop3':'val3'}}"; + String actual2 = "{'prop1':'WRONG','prop2':'WRONG'}"; + executeCheck(expected2, actual2, true, false, true); + }); + Assert.assertThrows(IllegalStateException.class, () -> { + String expected2 = "{'prop1':'val1','prop2':{'prop3':'val3'}}"; + String actual2 = "{'prop1':'WRONG','prop2':[12,34]}"; + executeCheck(expected2, actual2, true, false, true); + }); + expected = "{'prop1':'val1','prop1':{'prop3':'val3'}}"; + actual = "{'prop1':'val1','prop1':{'prop3':'val3'},'WRONG':'val1'}"; + executeCheck(expected, actual, false, true, true); + Assert.assertThrows(Exception.class, () -> { + String expected2 = "{'prop1':'val1','prop2':[12,34]}"; + String actual2 = "{'prop1':'val1','prop2':[12,35]}"; + executeCheck(expected2, actual2, false, true, true); + }); + Assert.assertThrows(IllegalStateException.class, () -> { + String expected2 = "{'prop1':'val1','prop2':[12,34]}"; + String actual2 = "{'prop1':'val1','prop2':{'WRONG':true}}"; + executeCheck(expected2, actual2, true, false, true); + }); + Assert.assertThrows(Exception.class, () -> { + String expected2 = "{'prop1':'val1','prop1':{'prop3':null}}"; + String actual2 = "{'prop1':'val1','prop1':{'prop3':12.4},'WRONG':'val1'}"; + executeCheck(expected2, actual2, false, true, true); + }); + expected = "{'prop1':'val1','prop2':{'prop3':null}}"; + actual = "{'prop1':'val1','prop2':{'prop3':null},'WRONG':'val1'}"; + executeCheck(expected, actual, false, true, true); + expected = "{'prop1':'val1','prop2':{'prop3':12}}"; + actual = "{'prop1':'val1','prop2':{'prop3':12}}"; + executeCheck(expected, actual, true, true, true); + expected = "{'prop1':'val1','prop2':[true,false]}"; + actual = "{'prop1':'val1','prop2':[true,false]}"; + executeCheck(expected, actual, true, true, true); + Assert.assertThrows(Exception.class, () -> { + String expected2 = "{'prop1':'val1','prop2':[false,true]}"; + String actual2 = "{'prop1':'val1','prop2':[true,false]}"; + executeCheck(expected2, actual2, true, true, true); + }); + Assert.assertThrows(Exception.class, () -> { + String expected2 = "{'prop1':'val1','prop2':[false,true]}"; + String actual2 = "{'prop1':'val1','prop2':[true,false]}"; + executeCheck(expected2, actual2, true, true, true); + }); + expected = "{'prop1':'val1','prop2':[false,true]}"; + actual = "{'prop1':'val1','prop2':[]}"; + executeCheck(expected, actual, true, false, true); + Assert.assertThrows(Exception.class, () -> { + String expected2 = "{'prop1':'val1','prop2':[false,true]}"; + String actual2 = "{'prop1':'val1','prop2':[],'prop3':false}"; + executeCheck(expected2, actual2, false, true, true); + }); + expected = "{'prop1':1,'prop2':[]}"; + actual = "{'prop1':1,'prop2':[{'prop2':'val2'}]}"; + executeCheck(expected, actual, true, true, false); + Assert.assertThrows(Exception.class, () -> { + String expected2 = "{'prop1':1,'prop2':[]}"; + String actual2 = "{'prop1':0,'prop2':[{'prop2':'val2'}]}"; + executeCheck(expected2, actual2, true, true, false); + }); + Assert.assertThrows(Exception.class, () -> { + String expected2 = "{'prop1':1,'prop2':[]}"; + String actual2 = "{'prop1':1,'prop2':[{'prop2':'val2'}]}"; + executeCheck(expected2, actual2, true, true, true); + }); + } + + private void executeCheck(String expected, String actual, boolean matchKeys, boolean matchValues, + boolean matchArrays) throws JsonSyntaxException, Exception { + expected = expected.replaceAll("'", "\""); + actual = actual.replaceAll("'", "\""); + CustomHttpClient.check(JsonParser.parseString(expected).getAsJsonObject(), + JsonParser.parseString(actual).getAsJsonObject(), matchKeys, matchValues, matchArrays); + } + +} diff --git a/openvidu-test-e2e/docker/bionic/Dockerfile b/openvidu-test-e2e/docker/bionic/Dockerfile index b5b2c24e..7b6ca520 100644 --- a/openvidu-test-e2e/docker/bionic/Dockerfile +++ b/openvidu-test-e2e/docker/bionic/Dockerfile @@ -1,6 +1,6 @@ FROM ubuntu:18.04 -LABEL maintainer="openvidu@gmail.com" +LABEL maintainer="info@openvidu.io" USER root @@ -9,7 +9,7 @@ RUN apt-get update && apt-get -y upgrade RUN apt-get install -y software-properties-common && apt-get install -y --no-install-recommends apt-utils # Install Kurento Media Server (KMS) -RUN echo "deb [arch=amd64] http://ubuntu.openvidu.io/6.14.0 bionic kms6" | tee /etc/apt/sources.list.d/kurento.list \ +RUN echo "deb [arch=amd64] http://ubuntu.openvidu.io/6.15.0 bionic kms6" | tee /etc/apt/sources.list.d/kurento.list \ && apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 5AFA7A83 \ && apt-get update \ && apt-get -y install kurento-media-server @@ -17,10 +17,10 @@ RUN sed -i "s/DAEMON_USER=\"kurento\"/DAEMON_USER=\"root\"/g" /etc/default/kuren # Install Node RUN apt-get update && apt-get install -y curl -RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - && apt-get install -y nodejs +RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - && apt-get install -y nodejs -# Java 8 -RUN apt-get install -y openjdk-8-jdk-headless +# Java 11 +RUN apt-get install -y openjdk-11-jdk-headless # Maven RUN apt-get install -y maven diff --git a/openvidu-test-e2e/docker/my-custom-layout/index.html b/openvidu-test-e2e/docker/my-custom-layout/index.html new file mode 100644 index 00000000..ca45b3f7 --- /dev/null +++ b/openvidu-test-e2e/docker/my-custom-layout/index.html @@ -0,0 +1,55 @@ + + + + + + + + + +
    + + + + + \ No newline at end of file diff --git a/openvidu-test-e2e/docker/xenial/Dockerfile b/openvidu-test-e2e/docker/xenial/Dockerfile index 5ade8ad2..975dacf8 100644 --- a/openvidu-test-e2e/docker/xenial/Dockerfile +++ b/openvidu-test-e2e/docker/xenial/Dockerfile @@ -1,15 +1,15 @@ FROM ubuntu:16.04 -LABEL maintainer="openvidu@gmail.com" +LABEL maintainer="info@openvidu.io" USER root -RUN apt-get update && apt-get -y upgrade +RUN apt-get update && apt-get -y upgrade RUN apt-get install -y software-properties-common && apt-get install -y --no-install-recommends apt-utils # Install Kurento Media Server (KMS) -RUN echo "deb [arch=amd64] http://ubuntu.openvidu.io/6.14.0 xenial kms6" | tee /etc/apt/sources.list.d/kurento.list \ +RUN echo "deb [arch=amd64] http://ubuntu.openvidu.io/6.15.0 xenial kms6" | tee /etc/apt/sources.list.d/kurento.list \ && apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 5AFA7A83 \ && apt-get update \ && apt-get -y install kurento-media-server @@ -17,13 +17,7 @@ RUN sed -i "s/DAEMON_USER=\"kurento\"/DAEMON_USER=\"root\"/g" /etc/default/kuren # Install Node RUN apt-get update && apt-get install -y curl -RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - && apt-get install -y nodejs - -# Java 8 -RUN apt-get install -y openjdk-8-jdk-headless - -# Maven -RUN apt-get install -y maven +RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - && apt-get install -y nodejs # git RUN apt-get install -y git @@ -42,6 +36,21 @@ RUN apt-get install -y ffmpeg # docker RUN apt-get update && apt-get -y install docker.io +# Java 11 +RUN add-apt-repository ppa:openjdk-r/ppa && \ + apt-get update && \ + apt-get install -y openjdk-11-jdk-headless + +# This is a fix: JDK 11 in Ubuntu 16.04 misses Java certs +RUN wget https://download.java.net/openjdk/jdk8u41/ri/openjdk-8u41-b04-linux-x64-14_jan_2020.tar.gz -P /tmp/jdk8 +RUN tar -zxvf /tmp/jdk8/openjdk-*.tar.gz -C /tmp/jdk8 --strip-components=1 && \ + cp /tmp/jdk8/jre/lib/security/cacerts /etc/ssl/certs/java && \ + update-ca-certificates -f && \ + rm -rf /tmp/jdk8 + +# Maven +RUN apt-get install -y maven + # Cleanup RUN rm -rf /var/lib/apt/lists/* RUN apt-get autoremove --purge -y && apt-get autoclean diff --git a/openvidu-test-e2e/jenkins/Jenkinsfile b/openvidu-test-e2e/jenkins/Jenkinsfile index 762d0aa7..e722c6e6 100644 --- a/openvidu-test-e2e/jenkins/Jenkinsfile +++ b/openvidu-test-e2e/jenkins/Jenkinsfile @@ -1,99 +1,178 @@ node('container') { - sh 'docker rm -f chrome firefox e2e || true' + + sh 'docker rm -f e2e chrome firefox opera || true' + sh 'rm -rf /opt/openvidu/* || true' + sh 'wget https://github.com/OpenVidu/openvidu/raw/master/openvidu-test-e2e/docker/barcode.y4m -P /opt/openvidu' + sh 'wget https://github.com/OpenVidu/openvidu/raw/master/openvidu-test-e2e/docker/fakeaudio.wav -P /opt/openvidu' + sh 'wget --directory-prefix=/opt/openvidu/test-layouts/layout1 https://raw.githubusercontent.com/OpenVidu/openvidu/master/openvidu-test-e2e/docker/my-custom-layout/index.html' + + docker.image('openvidu/openvidu-test-e2e:$DISTRO').pull() docker.image('selenium/standalone-chrome:latest').pull() docker.image('selenium/standalone-firefox:latest').pull() - docker.image('selenium/standalone-chrome:latest').withRun('-p 6666:4444 --name chrome --shm-size=1g -v /opt/openvidu:/opt/openvidu') { c -> - sh 'rm -rf /opt/openvidu/barcode.* && wget https://github.com/OpenVidu/openvidu/raw/master/openvidu-test-e2e/docker/barcode.y4m -P /opt/openvidu' - sh 'rm -rf /opt/openvidu/fakeaudio.* && wget https://github.com/OpenVidu/openvidu/raw/master/openvidu-test-e2e/docker/fakeaudio.wav -P /opt/openvidu' - docker.image('selenium/standalone-firefox:latest').withRun('-p 6667:4444 --name firefox --shm-size=1g') { d -> - def mycontainer = docker.image('openvidu/openvidu-test-e2e:$DISTRO') - mycontainer.pull() - mycontainer.inside("--name e2e -p 4200:4200 -p 4443:4443 -u root -e MY_UID=0 -v /var/run/docker.sock:/var/run/docker.sock:rw -v /dev/shm:/dev/shm -v /opt/openvidu:/opt/openvidu --privileged") { - stage('Preparation') { - sh 'rm -rf ~/.m2 || true' - sh 'rm -rf openvidu || true' - sh 'rm -rf kurento-java || true' - sh 'rm -rf /opt/openvidu/recordings/* || true' - sh 'git clone https://github.com/OpenVidu/openvidu.git' - sh 'cd openvidu && git fetch origin && git checkout $OPENVIDU_COMMIT' - sh(script: '''#!/bin/bash - if $KURENTO_JAVA_SNAPSHOT ; then - git clone https://github.com/Kurento/kurento-java.git - cd kurento-java && MVN_VERSION=$(mvn --batch-mode -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) - cd ../openvidu && mvn --batch-mode versions:set-property -Dproperty=version.kurento -DnewVersion=$MVN_VERSION - mvn dependency:get -DrepoUrl=https://maven.openvidu.io/repository/snapshots/ -Dartifact=org.kurento:kurento-client:$MVN_VERSION - mvn dependency:get -DrepoUrl=https://maven.openvidu.io/repository/snapshots/ -Dartifact=org.kurento:kurento-jsonrpc-client-jetty:$MVN_VERSION - mvn dependency:get -DrepoUrl=https://maven.openvidu.io/repository/snapshots/ -Dartifact=org.kurento:kurento-jsonrpc-server:$MVN_VERSION - mvn dependency:get -DrepoUrl=https://maven.openvidu.io/repository/snapshots/ -Dartifact=org.kurento:kurento-test:$MVN_VERSION - fi - '''.stripIndent()) - sh(script: '''#!/bin/bash - if $KURENTO_MEDIA_SERVER_DEV ; then - echo "Upgrading KMS to dev version" - sudo apt-get update && sudo apt-get install -y aptitude - sudo aptitude remove -y kurento-media-server - DISTRO=`lsb_release --codename | cut -f2` - sudo echo "deb [arch=amd64] http://ubuntu.openvidu.io/dev $DISTRO kms6" | sudo tee /etc/apt/sources.list.d/kurento.list - sudo apt-get update && sudo apt-get --yes -o Dpkg::Options::="--force-confnew" install kurento-media-server - fi - '''.stripIndent()) - } - stage('OpenVidu parent build') { - sh 'cd openvidu/openvidu-java-client && mvn --batch-mode versions:set -DnewVersion=1.0.0-TEST' - sh 'cd openvidu && mvn --batch-mode versions:set-property -Dproperty=version.openvidu.java.client -DnewVersion=1.0.0-TEST' - sh 'cd openvidu && mvn --batch-mode -DskipTests=true clean install' - } - stage('OpenVidu Browser build') { - sh 'cd openvidu/openvidu-browser && npm install --unsafe-perm && npm run build && npm link' - } - stage('OpenVidu Node Client build') { - sh 'cd openvidu/openvidu-node-client && npm install --unsafe-perm && npm run build && npm link' - } - stage('OpenVidu TestApp build') { - sh 'cd openvidu/openvidu-testapp && npm install --unsafe-perm && npm link openvidu-browser && npm link openvidu-node-client && export NG_CLI_ANALYTICS=ci && ./node_modules/@angular/cli/bin/ng build --prod' - } - stage('OpenVidu Server unit tests') { - sh 'cd openvidu/openvidu-server && mvn --batch-mode -Dtest=io.openvidu.server.test.unit.*Test test' - } - stage('OpenVidu Server integration tests') { - sh 'cd openvidu/openvidu-server && mvn --batch-mode -Dtest=io.openvidu.server.test.integration.*Test test' - } - stage('OpenVidu Server build') { - sh 'cd openvidu/openvidu-server/src/dashboard && npm install --unsafe-perm && npm link openvidu-browser && export NG_CLI_ANALYTICS=ci && npm run build-prod' - sh 'cd openvidu/openvidu-server && mvn --batch-mode clean compile package' - } - stage ('Environment Launch') { - sh 'openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -subj "/CN=www.mydom.com/O=My Company LTD./C=US" -keyout openvidu/openvidu-testapp/dist/key.pem -out openvidu/openvidu-testapp/dist/cert.pem' - sh 'cd openvidu/openvidu-testapp/dist && http-server -S -p 4200 &> /testapp.log &' - sh '/usr/bin/kurento-media-server &> /kms.log &' - sh 'until $(curl --insecure --output /dev/null --silent http://127.0.0.1:8888/kurento); do echo "Waiting for KMS..."; sleep 1; done' - sh(script: '''#!/bin/bash - if [ "$DOCKER_RECORDING_VERSION" != "default" ]; then - echo "Using custom openvidu-recording tag: $DOCKER_RECORDING_VERSION" - cd openvidu/openvidu-server/target && java -jar -DDOMAIN_OR_PUBLIC_IP=172.17.0.1 -DOPENVIDU_SECRET=MY_SECRET -DHTTPS_PORT=4443 -DOPENVIDU_RECORDING=true -DOPENVIDU_RECORDING_VERSION=$DOCKER_RECORDING_VERSION -DOPENVIDU_WEBHOOK=true -DOPENVIDU_WEBHOOK_ENDPOINT=http://127.0.0.1:7777/webhook openvidu-server-*.jar &> openvidu-server.log & - else - echo "Using default openvidu-recording tag" - cd openvidu/openvidu-server/target && java -jar -DDOMAIN_OR_PUBLIC_IP=172.17.0.1 -DOPENVIDU_SECRET=MY_SECRET -DHTTPS_PORT=4443 -DOPENVIDU_RECORDING=true -DOPENVIDU_WEBHOOK=true -DOPENVIDU_WEBHOOK_ENDPOINT=http://127.0.0.1:7777/webhook openvidu-server-*.jar &> openvidu-server.log & - fi - '''.stripIndent()) - sh 'until $(curl --insecure --output /dev/null --silent --head --fail https://OPENVIDUAPP:MY_SECRET@localhost:4443/); do echo "Waiting for openvidu-server..."; sleep 2; done' - } - stage ('OpenVidu E2E tests') { - try { - sh(script: '''#!/bin/bash - cd openvidu/openvidu-test-e2e && sudo mvn --batch-mode -DAPP_URL=https://172.17.0.1:4200/ -DOPENVIDU_URL=https://172.17.0.1:4443/ -DREMOTE_URL_CHROME=http://172.17.0.1:6666/wd/hub/ -DREMOTE_URL_FIREFOX=http://172.17.0.1:6667/wd/hub/ test - if [[ "$?" -ne 0 ]] ; then - echo "ERROR RUNNING TESTS" - cat openvidu/openvidu-server/target/openvidu-server.log - fi - '''.stripIndent()) - } - finally { - junit 'openvidu/openvidu-test-e2e/**/target/surefire-reports/TEST-*.xml' - archiveArtifacts artifacts: '**/openvidu-server.log' + docker.image('selenium/standalone-opera:latest').pull() + + docker.image('openvidu/openvidu-test-e2e:$DISTRO').inside('--name e2e -p 4200:4200 -p 4443:4443 -p 5555:5555 -u root -e MY_UID=0 -v /var/run/docker.sock:/var/run/docker.sock:rw -v /dev/shm:/dev/shm -v /opt/openvidu:/opt/openvidu --privileged') { + + stage('Preparation') { + sh 'rm -rf ~/.m2 || true' + sh 'rm -rf openvidu || true' + sh 'rm -rf kurento-java || true' + sh 'git clone https://github.com/OpenVidu/openvidu.git' + sh 'cd openvidu && git fetch --all && git checkout $OPENVIDU_COMMIT' + sh(script: '''#!/bin/bash -xe + if [[ $KURENTO_JAVA_COMMIT != "default" ]]; then + git clone https://github.com/Kurento/kurento-java.git + cd kurento-java + git checkout -f $KURENTO_JAVA_COMMIT + mvn clean install + fi + '''.stripIndent()) + sh(script: '''#!/bin/bash -xe + if $KURENTO_MEDIA_SERVER_DEV ; then + echo "Upgrading KMS to dev version" + sudo apt-get update && sudo apt-get install -y aptitude + sudo aptitude remove -y kurento-media-server + DISTRO=`lsb_release --codename | cut -f2` + sudo echo "deb [arch=amd64] http://ubuntu.openvidu.io/dev $DISTRO kms6" | sudo tee /etc/apt/sources.list.d/kurento.list + sudo apt-get update && sudo apt-get --yes -o Dpkg::Options::="--force-confnew" install kurento-media-server + fi + '''.stripIndent()) + } + + stage('OpenVidu Browser build') { + sh(script: '''#!/bin/bash -xe + cd openvidu + if [[ $OPENVIDU_BROWSER_COMMIT != "default" ]]; then + git checkout -f $OPENVIDU_BROWSER_COMMIT + fi + cd openvidu-browser + npm install --unsafe-perm && npm run build && npm pack + cp openvidu-browser-*.tgz /opt/openvidu + cd .. + git checkout -f $OPENVIDU_COMMIT + '''.stripIndent()) + } + + stage('OpenVidu Node Client build') { + sh(script: '''#!/bin/bash -xe + cd openvidu + if [[ $OPENVIDU_NODE_CLIENT_COMMIT != "default" ]]; then + git checkout -f $OPENVIDU_NODE_CLIENT_COMMIT + fi + cd openvidu-node-client + npm install --unsafe-perm && npm run build && npm pack + cp openvidu-node-client-*.tgz /opt/openvidu + cd .. + git checkout -f $OPENVIDU_COMMIT + '''.stripIndent()) + } + + stage('OpenVidu TestApp build') { + sh(script: '''#!/bin/bash -xe + cd openvidu + if [[ $OPENVIDU_TESTAPP_COMMIT != "default" ]]; then + git checkout -f $OPENVIDU_TESTAPP_COMMIT + fi + cd openvidu-testapp + npm install --unsafe-perm + npm install /opt/openvidu/openvidu-browser-*.tgz + npm install /opt/openvidu/openvidu-node-client-*.tgz + export NG_CLI_ANALYTICS="false" && ./node_modules/@angular/cli/bin/ng build --prod --output-path=/opt/openvidu/testapp + cd .. + git checkout -f $OPENVIDU_COMMIT + '''.stripIndent()) + } + + stage('OpenVidu Java Client build') { + sh(script: '''#!/bin/bash -xe + cd openvidu + if [[ $OPENVIDU_JAVA_CLIENT_COMMIT != "default" ]]; then + git checkout -f $OPENVIDU_JAVA_CLIENT_COMMIT + fi + cd openvidu-java-client + mvn --batch-mode versions:set -DnewVersion=TEST + mvn clean compile package + mvn install:install-file -Dfile=target/openvidu-java-client-TEST.jar -DgroupId=io.openvidu -DartifactId=openvidu-java-client -Dversion=TEST -Dpackaging=jar + cd .. + git checkout -f $OPENVIDU_COMMIT + '''.stripIndent()) + } + + stage('OpenVidu parent build') { + sh(script: '''#!/bin/bash -xe + if [[ $KURENTO_JAVA_COMMIT != "default" ]]; then + cd kurento-java && MVN_VERSION=$(mvn --batch-mode -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) + cd ../openvidu && mvn --batch-mode versions:set-property -Dproperty=version.kurento -DnewVersion=$MVN_VERSION + fi + '''.stripIndent()) + sh 'cd openvidu && mvn --batch-mode versions:set-property -Dproperty=version.openvidu.java.client -DnewVersion=TEST' + sh 'cd openvidu && mvn --batch-mode -DskipTests=true clean install' + } + + stage('OpenVidu Server unit tests') { + sh 'cd openvidu/openvidu-server && mvn --batch-mode -Dtest=io.openvidu.server.test.unit.*Test test' + } + + stage('OpenVidu Server integration tests') { + sh 'cd openvidu/openvidu-server && mvn --batch-mode -Dtest=io.openvidu.server.test.integration.*Test test' + } + + stage('OpenVidu Server build') { + sh(script: '''#!/bin/bash -xe + cd openvidu/openvidu-server/src/dashboard && npm install --unsafe-perm && npm install /opt/openvidu/openvidu-browser-*.tgz && export NG_CLI_ANALYTICS="false" && npm run build-prod + cd ../.. + mvn --batch-mode package + cp target/openvidu-server*.jar /opt/openvidu + '''.stripIndent()) + } + + stage ('Environment Launch') { + sh 'openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -subj "/CN=www.mydom.com/O=My Company LTD./C=US" -keyout /opt/openvidu/testapp/key.pem -out /opt/openvidu/testapp/cert.pem' + sh 'cd /opt/openvidu/testapp && http-server -S -p 4200 &> /opt/openvidu/testapp.log &' + sh '/usr/bin/kurento-media-server &> /kms.log &' + sh 'until $(curl --insecure --output /dev/null --silent http://127.0.0.1:8888/kurento); do echo "Waiting for KMS..."; sleep 1; done' + sh(script: '''#!/bin/bash -xe + if [ "$DOCKER_RECORDING_VERSION" != "default" ]; then + echo "Using custom openvidu-recording tag: $DOCKER_RECORDING_VERSION" + java -jar -DDOMAIN_OR_PUBLIC_IP=172.17.0.1 -DOPENVIDU_SECRET=MY_SECRET -DHTTPS_PORT=4443 -DOPENVIDU_RECORDING=true -DOPENVIDU_RECORDING_CUSTOM_LAYOUT=/opt/openvidu/test-layouts -DOPENVIDU_RECORDING_VERSION=$DOCKER_RECORDING_VERSION -DOPENVIDU_WEBHOOK=true -DOPENVIDU_WEBHOOK_ENDPOINT=http://127.0.0.1:7777/webhook /opt/openvidu/openvidu-server-*.jar &> openvidu-server.log & + else + echo "Using default openvidu-recording tag" + java -jar -DDOMAIN_OR_PUBLIC_IP=172.17.0.1 -DOPENVIDU_SECRET=MY_SECRET -DHTTPS_PORT=4443 -DOPENVIDU_RECORDING=true -DOPENVIDU_RECORDING_CUSTOM_LAYOUT=/opt/openvidu/test-layouts -DOPENVIDU_WEBHOOK=true -DOPENVIDU_WEBHOOK_ENDPOINT=http://127.0.0.1:7777/webhook /opt/openvidu/openvidu-server-*.jar &> openvidu-server.log & + fi + '''.stripIndent()) + sh 'until $(curl --insecure --output /dev/null --silent --head --fail https://OPENVIDUAPP:MY_SECRET@localhost:4443/); do echo "Waiting for openvidu-server..."; sleep 2; done' + } + + docker.image('selenium/standalone-chrome:latest').withRun('-p 6666:4444 --name chrome --shm-size=1g -v /opt/openvidu:/opt/openvidu') { a -> + docker.image('selenium/standalone-firefox:latest').withRun('-p 6667:4444 --name firefox --shm-size=1g') { b -> + docker.image('selenium/standalone-opera:latest').withRun('-p 6668:4444 --name opera --shm-size=1g') { c -> + + stage ('OpenVidu E2E tests') { + try { + sh(script: '''#!/bin/bash -xe + cd openvidu + if [[ $OPENVIDU_TESTE2E_COMMIT != "default" ]]; then + git checkout -f $OPENVIDU_TESTE2E_COMMIT + fi + cd openvidu-test-browsers + mvn --batch-mode versions:set -DnewVersion=TEST && mvn clean install + cd .. + mvn --batch-mode versions:set-property -Dproperty=version.openvidu.java.client -DnewVersion=TEST + mvn --batch-mode versions:set-property -Dproperty=version.openvidu.test.browsers -DnewVersion=TEST + cd openvidu-test-e2e + mvn -DskipTests=true clean install + sudo mvn --batch-mode -Dtest=OpenViduTestAppE2eTest -DAPP_URL=https://172.17.0.1:4200/ -DOPENVIDU_URL=https://172.17.0.1:4443/ -DREMOTE_URL_CHROME=http://172.17.0.1:6666/wd/hub/ -DREMOTE_URL_FIREFOX=http://172.17.0.1:6667/wd/hub/ -DREMOTE_URL_OPERA=http://172.17.0.1:6668/wd/hub/ -DEXTERNAL_CUSTOM_LAYOUT_URL=http://172.17.0.1:5555 -DEXTERNAL_CUSTOM_LAYOUT_PARAMS=sessionId,CUSTOM_LAYOUT_SESSION,secret,MY_SECRET test + git checkout -f $OPENVIDU_COMMIT + '''.stripIndent()) + } + finally { + junit 'openvidu/openvidu-test-e2e/**/target/surefire-reports/TEST-*.xml' + archiveArtifacts artifacts: '**/openvidu-server.log' + } } + } } } } -} +} \ No newline at end of file diff --git a/openvidu-test-e2e/pom.xml b/openvidu-test-e2e/pom.xml index f2eabf8c..0613d8a0 100644 --- a/openvidu-test-e2e/pom.xml +++ b/openvidu-test-e2e/pom.xml @@ -47,8 +47,8 @@ - - 1.8 + + 11 ${java.version} ${java.version} @@ -91,18 +91,6 @@ ${version.selenium} test - - org.seleniumhq.selenium - selenium-chrome-driver - ${version.selenium} - test - - - org.seleniumhq.selenium - selenium-firefox-driver - ${version.selenium} - test - org.seleniumhq.selenium selenium-api @@ -120,12 +108,6 @@ gson ${version.gson} - - org.jcodec - jcodec-javase - 0.2.3 - test - com.mashape.unirest unirest-java diff --git a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/AbstractOpenViduTestAppE2eTest.java b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/AbstractOpenViduTestAppE2eTest.java new file mode 100644 index 00000000..5e7edc67 --- /dev/null +++ b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/AbstractOpenViduTestAppE2eTest.java @@ -0,0 +1,341 @@ +package io.openvidu.test.e2e; + +import static org.openqa.selenium.OutputType.BASE64; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.io.FileUtils; +import org.apache.http.HttpStatus; +import org.junit.Assert; +import org.junit.jupiter.api.AfterEach; +import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedCondition; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mashape.unirest.http.HttpMethod; + +import io.github.bonigarcia.wdm.WebDriverManager; +import io.openvidu.java.client.OpenVidu; +import io.openvidu.java.client.OpenViduHttpException; +import io.openvidu.java.client.OpenViduJavaClientException; +import io.openvidu.java.client.VideoCodec; +import io.openvidu.test.browsers.BrowserUser; +import io.openvidu.test.browsers.ChromeUser; +import io.openvidu.test.browsers.FirefoxUser; +import io.openvidu.test.browsers.OperaUser; +import io.openvidu.test.browsers.ChromeAndroidUser; +import io.openvidu.test.browsers.utils.CommandLineExecutor; +import io.openvidu.test.browsers.utils.CustomHttpClient; +import io.openvidu.test.browsers.utils.RecordingUtils; + +public class AbstractOpenViduTestAppE2eTest { + + final protected String DEFAULT_JSON_SESSION = "{'id':'STR','object':'session','sessionId':'STR','createdAt':0,'mediaMode':'STR','recordingMode':'STR','defaultOutputMode':'STR','defaultRecordingLayout':'STR','customSessionId':'STR','connections':{'numberOfElements':0,'content':[]},'recording':false,'forcedVideoCodec':'STR','allowTranscoding':false}"; + final protected String DEFAULT_JSON_PENDING_CONNECTION = "{'id':'STR','object':'connection','type':'WEBRTC','status':'pending','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':null,'location':null,'platform':null,'token':'STR','serverData':'STR','record':true,'role':'STR','kurentoOptions':null,'rtspUri':null,'adaptativeBitrate':null,'onlyPlayWithSubscribers':null,'networkCache':null,'clientData':null,'publishers':null,'subscribers':null}"; + final protected String DEFAULT_JSON_ACTIVE_CONNECTION = "{'id':'STR','object':'connection','type':'WEBRTC','status':'active','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':0,'location':'STR','platform':'STR','token':'STR','serverData':'STR','record':true,'role':'STR','kurentoOptions':null,'rtspUri':null,'adaptativeBitrate':null,'onlyPlayWithSubscribers':null,'networkCache':null,'clientData':'STR','publishers':[],'subscribers':[]}"; + final protected String DEFAULT_JSON_IPCAM_CONNECTION = "{'id':'STR','object':'connection','type':'IPCAM','status':'active','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':0,'location':'STR','platform':'IPCAM','token':null,'serverData':'STR','record':true,'role':null,'kurentoOptions':null,'rtspUri':'STR','adaptativeBitrate':true,'onlyPlayWithSubscribers':true,'networkCache':2000,'clientData':null,'publishers':[],'subscribers':[]}"; + final protected String DEFAULT_JSON_TOKEN = "{'id':'STR','token':'STR','connectionId':'STR','createdAt':0,'session':'STR','role':'STR','data':'STR','kurentoOptions':{}}"; + + protected static String OPENVIDU_SECRET = "MY_SECRET"; + protected static String OPENVIDU_URL = "https://localhost:4443/"; + protected static String APP_URL = "http://localhost:4200/"; + protected static String EXTERNAL_CUSTOM_LAYOUT_URL = "http://localhost:5555"; + protected static String EXTERNAL_CUSTOM_LAYOUT_PARAMS = "sessionId,CUSTOM_LAYOUT_SESSION,secret,MY_SECRET"; + protected static Exception ex = null; + protected final Object lock = new Object(); + + protected static final Logger log = LoggerFactory.getLogger(OpenViduTestAppE2eTest.class); + protected static final CommandLineExecutor commandLine = new CommandLineExecutor(); + protected static final String RECORDING_IMAGE = "openvidu/openvidu-recording"; + + protected MyUser user; + protected Collection otherUsers = new ArrayList<>(); + protected volatile static boolean isRecordingTest; + protected volatile static boolean isKurentoRestartTest; + + protected static VideoCodec defaultForcedVideoCodec; + protected static boolean defaultAllowTranscoding; + + protected static OpenVidu OV; + + protected RecordingUtils recordingUtils = new RecordingUtils(); + + protected static void checkFfmpegInstallation() { + String ffmpegOutput = commandLine.executeCommand("which ffmpeg"); + if (ffmpegOutput == null || ffmpegOutput.isEmpty()) { + log.error("ffmpeg package is not installed in the host machine"); + Assert.fail(); + return; + } else { + log.info("ffmpeg is installed and accesible"); + } + } + + protected static void setupBrowserDrivers() { + WebDriverManager.chromedriver().setup(); + WebDriverManager.firefoxdriver().setup(); + WebDriverManager.operadriver().setup(); + } + + protected static void cleanFoldersAndSetUpOpenViduJavaClient() { + try { + log.info("Cleaning folder /opt/openvidu/recordings"); + FileUtils.cleanDirectory(new File("/opt/openvidu/recordings")); + } catch (IOException e) { + log.error(e.getMessage()); + } + OV = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET); + } + + protected static void loadEnvironmentVariables() { + String appUrl = System.getProperty("APP_URL"); + if (appUrl != null) { + APP_URL = appUrl; + } + log.info("Using URL {} to connect to openvidu-testapp", APP_URL); + + String externalCustomLayoutUrl = System.getProperty("EXTERNAL_CUSTOM_LAYOUT_URL"); + if (externalCustomLayoutUrl != null) { + EXTERNAL_CUSTOM_LAYOUT_URL = externalCustomLayoutUrl; + } + log.info("Using URL {} to connect to external custom layout", EXTERNAL_CUSTOM_LAYOUT_URL); + + String externalCustomLayoutParams = System.getProperty("EXTERNAL_CUSTOM_LAYOUT_PARAMS"); + if (externalCustomLayoutParams != null) { + // Parse external layout parameters and build a URL formatted params string + List params = Stream.of(externalCustomLayoutParams.split(",", -1)).collect(Collectors.toList()); + if (params.size() % 2 != 0) { + log.error( + "Wrong configuration property EXTERNAL_CUSTOM_LAYOUT_PARAMS. Must be a comma separated list with an even number of elements. e.g: EXTERNAL_CUSTOM_LAYOUT_PARAMS=param1,value1,param2,value2"); + Assert.fail(); + return; + } else { + EXTERNAL_CUSTOM_LAYOUT_PARAMS = ""; + for (int i = 0; i < params.size(); i++) { + if (i % 2 == 0) { + // Param name + EXTERNAL_CUSTOM_LAYOUT_PARAMS += params.get(i) + "="; + } else { + // Param value + EXTERNAL_CUSTOM_LAYOUT_PARAMS += params.get(i); + if (i < params.size() - 1) { + EXTERNAL_CUSTOM_LAYOUT_PARAMS += "&"; + } + } + } + } + } + log.info("Using URL {} to connect to external custom layout", EXTERNAL_CUSTOM_LAYOUT_PARAMS); + + String openviduUrl = System.getProperty("OPENVIDU_URL"); + if (openviduUrl != null) { + OPENVIDU_URL = openviduUrl; + } + log.info("Using URL {} to connect to openvidu-server", OPENVIDU_URL); + + String openvidusecret = System.getProperty("OPENVIDU_SECRET"); + if (openvidusecret != null) { + OPENVIDU_SECRET = openvidusecret; + } + log.info("Using secret {} to connect to openvidu-server", OPENVIDU_SECRET); + } + + protected void setupBrowser(String browser) { + + BrowserUser browserUser; + + switch (browser) { + case "chrome": + browserUser = new ChromeUser("TestUser", 50, false); + break; + case "firefox": + browserUser = new FirefoxUser("TestUser", 50, false); + break; + case "firefoxDisabledOpenH264": + browserUser = new FirefoxUser("TestUser", 50, true); + break; + case "opera": + browserUser = new OperaUser("TestUser", 50); + break; + case "chromeAndroid": + browserUser = new ChromeAndroidUser("TestUser", 50); + break; + case "chromeAlternateScreenShare": + browserUser = new ChromeUser("TestUser", 50, "OpenVidu TestApp", false); + break; + case "chromeAsRoot": + browserUser = new ChromeUser("TestUser", 50, true); + break; + default: + browserUser = new ChromeUser("TestUser", 50, false); + } + + this.user = new MyUser(browserUser); + + user.getDriver().get(APP_URL); + + WebElement urlInput = user.getDriver().findElement(By.id("openvidu-url")); + urlInput.clear(); + urlInput.sendKeys(OPENVIDU_URL); + WebElement secretInput = user.getDriver().findElement(By.id("openvidu-secret")); + secretInput.clear(); + secretInput.sendKeys(OPENVIDU_SECRET); + + user.getEventManager().startPolling(); + } + + protected void setupChromeWithFakeVideo(Path videoFileLocation) { + this.user = new MyUser(new ChromeUser("TestUser", 50, videoFileLocation)); + user.getDriver().get(APP_URL); + WebElement urlInput = user.getDriver().findElement(By.id("openvidu-url")); + urlInput.clear(); + urlInput.sendKeys(OPENVIDU_URL); + WebElement secretInput = user.getDriver().findElement(By.id("openvidu-secret")); + secretInput.clear(); + secretInput.sendKeys(OPENVIDU_SECRET); + user.getEventManager().startPolling(); + } + + protected static void getDefaultTranscodingValues() throws Exception { + CustomHttpClient restClient = new CustomHttpClient(OPENVIDU_URL, "OPENVIDUAPP", OPENVIDU_SECRET); + JsonObject ovConfig = restClient.rest(HttpMethod.GET, "/openvidu/api/config", HttpStatus.SC_OK); + defaultForcedVideoCodec = VideoCodec.valueOf(ovConfig.get("OPENVIDU_STREAMS_FORCED_VIDEO_CODEC").getAsString()); + defaultAllowTranscoding = ovConfig.get("OPENVIDU_STREAMS_ALLOW_TRANSCODING").getAsBoolean(); + } + + @AfterEach + protected void dispose() { + if (user != null) { + user.dispose(); + } + Iterator it = otherUsers.iterator(); + while (it.hasNext()) { + MyUser other = it.next(); + other.dispose(); + it.remove(); + } + try { + OV.fetch(); + } catch (OpenViduJavaClientException | OpenViduHttpException e1) { + log.error("Error fetching sessions: {}", e1.getMessage()); + } + OV.getActiveSessions().forEach(session -> { + try { + session.close(); + log.info("Session {} successfully closed", session.getSessionId()); + } catch (OpenViduJavaClientException e) { + log.error("Error closing session: {}", e.getMessage()); + } catch (OpenViduHttpException e) { + log.error("Error closing session: {}", e.getMessage()); + } + }); + if (isRecordingTest) { + removeAllRecordingContiners(); + try { + FileUtils.cleanDirectory(new File("/opt/openvidu/recordings")); + } catch (IOException e) { + log.error(e.getMessage()); + } + isRecordingTest = false; + } + if (isKurentoRestartTest) { + this.restartKms(); + isKurentoRestartTest = false; + } + OV = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET); + } + + protected void listEmptyRecordings() { + // List existing recordings (empty) + user.getDriver().findElement(By.id("list-recording-btn")).click(); + user.getWaiter() + .until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", "Recording list []")); + } + + protected ExpectedCondition waitForVideoDuration(WebElement element, int durationInSeconds) { + return new ExpectedCondition() { + @Override + public Boolean apply(WebDriver input) { + return element.getAttribute("duration").matches( + durationInSeconds - 1 + "\\.[5-9][0-9]{0,5}|" + durationInSeconds + "\\.[0-5][0-9]{0,5}"); + } + }; + } + + protected void gracefullyLeaveParticipants(int numberOfParticipants) throws Exception { + int accumulatedConnectionDestroyed = 0; + for (int j = 1; j <= numberOfParticipants; j++) { + user.getDriver().findElement(By.id("remove-user-btn")).sendKeys(Keys.ENTER); + user.getEventManager().waitUntilEventReaches("sessionDisconnected", j); + accumulatedConnectionDestroyed = (j != numberOfParticipants) + ? (accumulatedConnectionDestroyed + numberOfParticipants - j) + : (accumulatedConnectionDestroyed); + user.getEventManager().waitUntilEventReaches("connectionDestroyed", accumulatedConnectionDestroyed); + } + } + + protected String getBase64Screenshot(MyUser user) throws Exception { + String screenshotBase64 = ((TakesScreenshot) user.getDriver()).getScreenshotAs(BASE64); + return "data:image/png;base64," + screenshotBase64; + } + + protected void startKms() { + log.info("Starting KMS"); + commandLine.executeCommand("/usr/bin/kurento-media-server &>> /kms.log &"); + } + + protected void stopKms() { + log.info("Stopping KMS"); + commandLine.executeCommand("kill -9 $(pidof kurento-media-server)"); + } + + protected void restartKms() { + this.stopKms(); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + this.startKms(); + } + + protected void checkDockerContainerRunning(String imageName, int amount) { + int number = Integer.parseInt(commandLine.executeCommand("docker ps | grep " + imageName + " | wc -l")); + Assert.assertEquals("Wrong number of Docker containers for image " + imageName + " running", amount, number); + } + + protected void removeAllRecordingContiners() { + commandLine.executeCommand("docker ps -a | awk '{ print $1,$2 }' | grep " + RECORDING_IMAGE + + " | awk '{print $1 }' | xargs -I {} docker rm -f {}"); + } + + protected String mergeJson(String json, String newProperties, String[] removeProperties) { + JsonObject jsonObj = JsonParser.parseString(json.replaceAll("'", "\"")).getAsJsonObject(); + JsonObject newJsonObj = JsonParser.parseString(newProperties.replaceAll("'", "\"")).getAsJsonObject(); + newJsonObj.entrySet().forEach(entry -> { + jsonObj.remove(entry.getKey()); + jsonObj.add(entry.getKey(), entry.getValue()); + }); + for (String prop : removeProperties) { + jsonObj.remove(prop); + } + return jsonObj.toString().replaceAll("\"", "'"); + } + +} diff --git a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/MyUser.java b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/MyUser.java index 3a7952b1..8ed6a980 100644 --- a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/MyUser.java +++ b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/MyUser.java @@ -28,7 +28,7 @@ public class MyUser { } public void dispose() { - this.eventManager.stopPolling(true); + this.eventManager.stopPolling(true, true); this.browserUser.dispose(); } diff --git a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduEventManager.java b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduEventManager.java index 1bb933b4..be49e3b1 100644 --- a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduEventManager.java +++ b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduEventManager.java @@ -83,6 +83,7 @@ public class OpenViduEventManager { private Map eventNumbers; private Map eventCountdowns; private AtomicBoolean isInterrupted = new AtomicBoolean(false); + private CountDownLatch pollingLatch = new CountDownLatch(1); private int timeOfWaitInSeconds; public OpenViduEventManager(WebDriver driver, int timeOfWaitInSeconds) { @@ -100,7 +101,7 @@ public class OpenViduEventManager { public void uncaughtException(Thread th, Throwable ex) { if (ex.getClass().getSimpleName().equals("UnhandledAlertException") && ex.getMessage().contains("unexpected alert open")) { - stopPolling(false); + stopPolling(false, false); System.err .println("Alert opened (" + ex.getMessage() + "). Waiting 1 second and restarting polling"); try { @@ -130,20 +131,23 @@ public class OpenViduEventManager { e.printStackTrace(); } } + log.info("Polling thread is now interrupted!"); + this.pollingLatch.countDown(); }); this.pollingThread.setUncaughtExceptionHandler(h); this.pollingThread.start(); } - public void stopPolling(boolean stopThread) { - this.eventCallbacks.clear(); - this.eventCountdowns.clear(); - this.eventNumbers.clear(); - + public void stopPolling(boolean stopThread, boolean cleanExistingEvents) { if (stopThread) { this.isInterrupted.set(true); this.pollingThread.interrupt(); } + if (cleanExistingEvents) { + this.eventCallbacks.clear(); + this.eventCountdowns.clear(); + this.eventNumbers.clear(); + } } public void on(String eventName, Consumer callback) { @@ -186,6 +190,26 @@ public class OpenViduEventManager { this.setCountDown(eventName, new CountDownLatch(0)); } + public synchronized void clearAllCurrentEvents() { + this.eventNumbers.keySet().forEach(eventName -> { + this.clearCurrentEvents(eventName); + }); + } + + public void resetEventThread() throws InterruptedException { + this.stopPolling(true, true); + this.pollingLatch.await(); + this.execService.shutdownNow(); + this.execService.awaitTermination(10, TimeUnit.SECONDS); + this.execService = Executors.newCachedThreadPool(); + this.stopPolling(false, true); + this.clearAllCurrentEvents(); + this.isInterrupted.set(false); + this.pollingLatch = new CountDownLatch(1); + this.eventQueue.clear(); + this.startPolling(); + } + public boolean assertMediaTracks(WebElement videoElement, boolean audioTransmission, boolean videoTransmission, String parentSelector) { return this.assertMediaTracks(Collections.singleton(videoElement), audioTransmission, videoTransmission, @@ -196,6 +220,10 @@ public class OpenViduEventManager { boolean videoTransmission) { boolean success = true; for (WebElement video : videoElements) { + if (!waitUntilSrcObjectDefined(video, "", 5000)) { + System.err.println("srcObject of HTMLVideoElement was not defined!"); + return false; + } success = success && (audioTransmission == this.hasAudioTracks(video, "")) && (videoTransmission == this.hasVideoTracks(video, "")); if (!success) @@ -208,6 +236,10 @@ public class OpenViduEventManager { boolean videoTransmission, String parentSelector) { boolean success = true; for (WebElement video : videoElements) { + if (!waitUntilSrcObjectDefined(video, "", 5000)) { + System.err.println("srcObject of HTMLVideoElement was not defined!"); + return false; + } success = success && (audioTransmission == this.hasAudioTracks(video, parentSelector)) && (videoTransmission == this.hasVideoTracks(video, parentSelector)); if (!success) @@ -251,9 +283,8 @@ public class OpenViduEventManager { } String[] events = rawEvents.replaceFirst("^
    ", "").split("
    "); - JsonParser parser = new JsonParser(); for (String e : events) { - JsonObject event = (JsonObject) parser.parse(e); + JsonObject event = JsonParser.parseString(e).getAsJsonObject(); final String eventType = event.get("type").getAsString(); this.eventQueue.add(event); @@ -314,21 +345,44 @@ public class OpenViduEventManager { } private boolean hasAudioTracks(WebElement videoElement, String parentSelector) { - boolean audioTracks = (boolean) ((JavascriptExecutor) driver).executeScript( - "return ((document.querySelector('" + parentSelector + (parentSelector.isEmpty() ? "" : " ") + "#" - + videoElement.getAttribute("id") + "').srcObject.getAudioTracks().length > 0)" - + "&& (document.querySelector('" + parentSelector + (parentSelector.isEmpty() ? "" : " ") + "#" - + videoElement.getAttribute("id") + "').srcObject.getAudioTracks()[0].enabled))"); + String script = "return ((document.querySelector('" + parentSelector + (parentSelector.isEmpty() ? "" : " ") + + "#" + videoElement.getAttribute("id") + "').srcObject.getAudioTracks().length > 0)" + + " && (document.querySelector('" + parentSelector + (parentSelector.isEmpty() ? "" : " ") + "#" + + videoElement.getAttribute("id") + "').srcObject.getAudioTracks()[0].enabled))"; + boolean audioTracks = (boolean) ((JavascriptExecutor) driver).executeScript(script); return audioTracks; } private boolean hasVideoTracks(WebElement videoElement, String parentSelector) { - boolean videoTracks = (boolean) ((JavascriptExecutor) driver).executeScript( - "return ((document.querySelector('" + parentSelector + (parentSelector.isEmpty() ? "" : " ") + "#" - + videoElement.getAttribute("id") + "').srcObject.getVideoTracks().length > 0)" - + "&& (document.querySelector('" + parentSelector + (parentSelector.isEmpty() ? "" : " ") + "#" - + videoElement.getAttribute("id") + "').srcObject.getVideoTracks()[0].enabled))"); + String script = "return ((document.querySelector('" + parentSelector + (parentSelector.isEmpty() ? "" : " ") + + "#" + videoElement.getAttribute("id") + "').srcObject.getVideoTracks().length > 0)" + + " && (document.querySelector('" + parentSelector + (parentSelector.isEmpty() ? "" : " ") + "#" + + videoElement.getAttribute("id") + "').srcObject.getVideoTracks()[0].enabled))"; + boolean videoTracks = (boolean) ((JavascriptExecutor) driver).executeScript(script); return videoTracks; } + private boolean waitUntilSrcObjectDefined(WebElement videoElement, String parentSelector, int maxMsWait) { + final int sleepInterval = 50; + int maxIterations = maxMsWait / sleepInterval; + int counter = 0; + boolean defined = srcObjectDefined(videoElement, parentSelector); + while (!defined && counter < maxIterations) { + try { + Thread.sleep(sleepInterval); + } catch (InterruptedException e) { + } + defined = srcObjectDefined(videoElement, parentSelector); + counter++; + } + return defined; + } + + private boolean srcObjectDefined(WebElement videoElement, String parentSelector) { + String script = "return (!!(document.querySelector('" + parentSelector + (parentSelector.isEmpty() ? "" : " ") + + "#" + videoElement.getAttribute("id") + "').srcObject))"; + boolean defined = (boolean) ((JavascriptExecutor) driver).executeScript(script); + return defined; + } + } diff --git a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduProTestAppE2eTest.java b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduProTestAppE2eTest.java new file mode 100644 index 00000000..f56be54f --- /dev/null +++ b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduProTestAppE2eTest.java @@ -0,0 +1,632 @@ +package io.openvidu.test.e2e; + +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.FileReader; +import java.util.Iterator; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.http.HttpStatus; +import org.junit.Assert; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.Alert; +import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.stream.JsonReader; +import com.mashape.unirest.http.HttpMethod; + +import io.openvidu.java.client.Connection; +import io.openvidu.java.client.ConnectionProperties; +import io.openvidu.java.client.ConnectionType; +import io.openvidu.java.client.KurentoOptions; +import io.openvidu.java.client.OpenVidu; +import io.openvidu.java.client.OpenViduHttpException; +import io.openvidu.java.client.OpenViduRole; +import io.openvidu.java.client.Recording; +import io.openvidu.java.client.Session; +import io.openvidu.test.browsers.utils.CustomHttpClient; +import io.openvidu.test.browsers.utils.Unzipper; + +public class OpenViduProTestAppE2eTest extends AbstractOpenViduTestAppE2eTest { + + protected volatile static boolean isNetworkQualityTest; + + @BeforeAll() + protected static void setupAll() { + checkFfmpegInstallation(); + loadEnvironmentVariables(); + setupBrowserDrivers(); + cleanFoldersAndSetUpOpenViduJavaClient(); + } + + @Override + @AfterEach + protected void dispose() { + super.dispose(); + if (isNetworkQualityTest) { + // Disable network quality API + try { + CustomHttpClient restClient = new CustomHttpClient(OPENVIDU_URL, "OPENVIDUAPP", OPENVIDU_SECRET); + if (restClient.rest(HttpMethod.GET, "/openvidu/api/config", 200).get("OPENVIDU_PRO_NETWORK_QUALITY") + .getAsBoolean()) { + String body = "{'OPENVIDU_PRO_NETWORK_QUALITY':false}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/restart", body, 200); + waitUntilOpenViduRestarted(30); + } + } catch (Exception e) { + log.error(e.getMessage()); + Assert.fail("Error restarting OpenVidu Server to disable Network quality API"); + } finally { + isNetworkQualityTest = false; + } + } + } + + @Test + @DisplayName("Individual dynamic record") + void individualDynamicRecordTest() throws Exception { + isRecordingTest = true; + + setupBrowser("chrome"); + + log.info("Individual dynamic record"); + + CustomHttpClient restClient = new CustomHttpClient(OpenViduTestAppE2eTest.OPENVIDU_URL, "OPENVIDUAPP", + OpenViduTestAppE2eTest.OPENVIDU_SECRET); + + // Connect 3 users. Record only the first one + for (int i = 0; i < 3; i++) { + user.getDriver().findElement(By.id("add-user-btn")).click(); + if (i > 0) { + user.getDriver().findElement(By.id("session-settings-btn-" + i)).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.id("record-checkbox")).click(); + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(1000); + } + } + + String sessionName = "TestSession"; + + user.getDriver().findElements(By.className("join-btn")).forEach(el -> el.sendKeys(Keys.ENTER)); + user.getEventManager().waitUntilEventReaches("streamPlaying", 9); + + // Start the recording for one of the not recorded users + JsonObject sessionInfo = restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/" + sessionName, + HttpStatus.SC_OK); + JsonArray connections = sessionInfo.get("connections").getAsJsonObject().get("content").getAsJsonArray(); + String connectionId1 = null; + String streamId1 = null; + // Get connectionId and streamId + for (JsonElement connection : connections) { + if (connection.getAsJsonObject().get("record").getAsBoolean()) { + connectionId1 = connection.getAsJsonObject().get("connectionId").getAsString(); + streamId1 = connection.getAsJsonObject().get("publishers").getAsJsonArray().get(0).getAsJsonObject() + .get("streamId").getAsString(); + break; + } + } + + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", + "{'session':'" + sessionName + "','outputMode':'INDIVIDUAL'}", HttpStatus.SC_OK); + user.getEventManager().waitUntilEventReaches("recordingStarted", 3); + Thread.sleep(1000); + + // Start the recording for one of the not recorded users + sessionInfo = restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/" + sessionName, HttpStatus.SC_OK); + connections = sessionInfo.get("connections").getAsJsonObject().get("content").getAsJsonArray(); + String connectionId2 = null; + String streamId2 = null; + // Get connectionId and streamId + for (JsonElement connection : connections) { + if (!connection.getAsJsonObject().get("record").getAsBoolean()) { + connectionId2 = connection.getAsJsonObject().get("connectionId").getAsString(); + streamId2 = connection.getAsJsonObject().get("publishers").getAsJsonArray().get(0).getAsJsonObject() + .get("streamId").getAsString(); + break; + } + } + + // Generate 3 total recordings of 1 second length for this same stream + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/" + sessionName + "/connection/" + connectionId2, + "{'record':true}", HttpStatus.SC_OK); + Thread.sleep(1000); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/" + sessionName + "/connection/" + connectionId2, + "{'record':false}", HttpStatus.SC_OK); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/" + sessionName + "/connection/" + connectionId2, + "{'record':true}", HttpStatus.SC_OK); + Thread.sleep(1000); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/" + sessionName + "/connection/" + connectionId2, + "{'record':false}", HttpStatus.SC_OK); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/" + sessionName + "/connection/" + connectionId2, + "{'record':true}", HttpStatus.SC_OK); + Thread.sleep(1000); + + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/stop/" + sessionName, HttpStatus.SC_OK); + user.getEventManager().waitUntilEventReaches("recordingStopped", 3); + + gracefullyLeaveParticipants(3); + + String recPath = "/opt/openvidu/recordings/" + sessionName + "/"; + Recording recording = new OpenVidu(OpenViduTestAppE2eTest.OPENVIDU_URL, OpenViduTestAppE2eTest.OPENVIDU_SECRET) + .getRecording(sessionName); + this.recordingUtils.checkIndividualRecording(recPath, recording, 4, "opus", "vp8", true); + + // Analyze INDIVIDUAL recording metadata + new Unzipper().unzipFile(recPath, recording.getName() + ".zip"); + File jsonSyncFile = new File(recPath + recording.getName() + ".json"); + JsonReader reader = new JsonReader(new FileReader(jsonSyncFile)); + JsonObject jsonMetadata = new Gson().fromJson(reader, JsonObject.class); + JsonArray syncArray = jsonMetadata.get("files").getAsJsonArray(); + int count1 = 0; + int count2 = 0; + List names = Stream.of(streamId2 + ".webm", streamId2 + "-1.webm", streamId2 + "-2.webm") + .collect(Collectors.toList()); + for (JsonElement fileJson : syncArray) { + JsonObject file = fileJson.getAsJsonObject(); + String fileStreamId = file.get("streamId").getAsString(); + if (fileStreamId.equals(streamId1)) { + // Normal recorded user + Assert.assertEquals("Wrong connectionId file metadata property", connectionId1, + file.get("connectionId").getAsString()); + long msDuration = file.get("endTimeOffset").getAsLong() - file.get("startTimeOffset").getAsLong(); + Assert.assertTrue("Wrong recording duration of individual file. Difference: " + (msDuration - 4000), + msDuration - 4000 < 750); + count1++; + } else if (fileStreamId.equals(streamId2)) { + // Dynamically recorded user + Assert.assertEquals("Wrong connectionId file metadata property", connectionId2, + file.get("connectionId").getAsString()); + long msDuration = file.get("endTimeOffset").getAsLong() - file.get("startTimeOffset").getAsLong(); + Assert.assertTrue( + "Wrong recording duration of individual file. Difference: " + Math.abs(msDuration - 1000), + Math.abs(msDuration - 1000) < 150); + Assert.assertTrue("File name not found among " + names.toString(), + names.remove(file.get("name").getAsString())); + count2++; + } else { + Assert.fail("Metadata file element does not belong to a known stream (" + fileStreamId + ")"); + } + } + Assert.assertEquals("Wrong number of recording files for stream " + streamId1, 1, count1); + Assert.assertEquals("Wrong number of recording files for stream " + streamId2, 3, count2); + Assert.assertTrue("Some expected file name didn't existed: " + names.toString(), names.isEmpty()); + } + + @Test + @DisplayName("REST API PRO test") + void restApiProTest() throws Exception { + + log.info("REST API PRO test"); + + CustomHttpClient restClient = new CustomHttpClient(OPENVIDU_URL, "OPENVIDUAPP", OPENVIDU_SECRET); + + /** + * PATCH /openvidu/api/sessions//connection/ + **/ + String body = "{'customSessionId': 'CUSTOM_SESSION_ID'}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", body, HttpStatus.SC_OK); + body = "{'role':'PUBLISHER','record':false,'data':'MY_SERVER_PRO_DATA'}"; + JsonObject res = restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", body, + HttpStatus.SC_OK); + final String token = res.get("token").getAsString(); + final String connectionId = res.get("connectionId").getAsString(); + final long createdAt = res.get("createdAt").getAsLong(); + + /** UPDATE PENDING CONNECTION **/ + + // Test with REST API + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, + "{'role':false}", HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, + "{'record':123}", HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, + "{'role':'PUBLISHER','record':'WRONG'}", HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/WRONG/connection/" + connectionId, + "{'role':'PUBLISHER','record':'WRONG'}", HttpStatus.SC_NOT_FOUND); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/WRONG", + "{'role':'PUBLISHER','record':true}", HttpStatus.SC_NOT_FOUND); + + // No change should return 200. At this point role=PUBLISHER and record=false + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, "{}", + HttpStatus.SC_OK); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, + "{'role':'PUBLISHER'}", HttpStatus.SC_OK); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, + "{'record':false}", HttpStatus.SC_OK); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, + "{'role':'PUBLISHER','record':false,'data':'OTHER_DATA'}", HttpStatus.SC_OK); + + // Updating only role should let record value untouched + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, + "{'role':'MODERATOR'}", HttpStatus.SC_OK, true, true, true, + mergeJson(DEFAULT_JSON_PENDING_CONNECTION, + "{'id':'" + connectionId + "','connectionId':'" + connectionId + + "','role':'MODERATOR','serverData':'MY_SERVER_PRO_DATA','record':false,'token':'" + + token + "','sessionId':'CUSTOM_SESSION_ID','createdAt':" + createdAt + "}", + new String[0])); + // Updating only record should let role value untouched + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, + "{'record':true}", HttpStatus.SC_OK, true, true, true, + mergeJson(DEFAULT_JSON_PENDING_CONNECTION, + "{'id':'" + connectionId + "','connectionId':'" + connectionId + + "','role':'MODERATOR','serverData':'MY_SERVER_PRO_DATA','token':'" + token + + "','sessionId':'CUSTOM_SESSION_ID','createdAt':" + createdAt + "}", + new String[0])); + + // Test with openvidu-java-client + OpenVidu OV = new OpenVidu(OpenViduTestAppE2eTest.OPENVIDU_URL, OpenViduTestAppE2eTest.OPENVIDU_SECRET); + Assert.assertTrue("OpenVidu object should have changed", OV.fetch()); + Session session = OV.getActiveSessions().get(0); + try { + session.updateConnection("WRONG_CONNECTION_ID", new ConnectionProperties.Builder().build()); + Assert.fail("Expected OpenViduHttpException exception"); + } catch (OpenViduHttpException exception) { + Assert.assertEquals("Wrong HTTP status", HttpStatus.SC_NOT_FOUND, exception.getStatus()); + } + Assert.assertFalse("Session object should not have changed", session.fetch()); + Connection connection = session.updateConnection(connectionId, + new ConnectionProperties.Builder().role(OpenViduRole.SUBSCRIBER).record(false).build()); + Assert.assertEquals("Wrong role Connection property", OpenViduRole.SUBSCRIBER, connection.getRole()); + Assert.assertFalse("Wrong record Connection property", connection.record()); + Assert.assertEquals("Wrong data Connection property", "MY_SERVER_PRO_DATA", connection.getServerData()); + + setupBrowser("chrome"); + + user.getDriver().findElement(By.id("add-user-btn")).click(); + user.getDriver().findElement(By.id("session-settings-btn-0")).click(); + Thread.sleep(1000); + + // Set token + WebElement tokenInput = user.getDriver().findElement(By.cssSelector("#custom-token-div input")); + tokenInput.clear(); + tokenInput.sendKeys(token); + // Force publishing even SUBSCRIBER + user.getDriver().findElement(By.id("force-publishing-checkbox")).click(); + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(1000); + + user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .join-btn")).sendKeys(Keys.ENTER); + + try { + user.getWaiter().until(ExpectedConditions.alertIsPresent()); + Alert alert = user.getDriver().switchTo().alert(); + Assert.assertTrue("Alert does not contain expected text", + alert.getText().equals("OPENVIDU_PERMISSION_DENIED: You don't have permissions to publish")); + alert.accept(); + } catch (Exception e) { + Assert.fail("Alert exception"); + } + Thread.sleep(500); + + user.getEventManager().waitUntilEventReaches("connectionCreated", 1); + user.getEventManager().waitUntilEventReaches("accessAllowed", 1); + + Assert.assertTrue("Session object should have changed", session.fetch()); + connection = session.getActiveConnections().get(0); + final Long activeAt = connection.activeAt(); + Assert.assertTrue("activeAt should be greater than createdAt in Connection object", activeAt > createdAt); + Assert.assertEquals("Wrong role in Connection object", OpenViduRole.SUBSCRIBER, connection.getRole()); + Assert.assertFalse("Wrong record in Connection object", connection.record()); + + /** UPDATE ACTIVE CONNECTION **/ + + // Test with REST API + + // No change should return 200. At this point role=SUBSCRIBER and record=false + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, "{}", + HttpStatus.SC_OK); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, + "{'role':'SUBSCRIBER'}", HttpStatus.SC_OK); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, + "{'record':false}", HttpStatus.SC_OK); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, + "{'role':'SUBSCRIBER','record':false}", HttpStatus.SC_OK); + + // Updating only role should let record value untouched + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, + "{'role':'MODERATOR'}", HttpStatus.SC_OK, false, true, true, + mergeJson(DEFAULT_JSON_ACTIVE_CONNECTION, + "{'id':'" + connectionId + "','connectionId':'" + connectionId + + "','role':'MODERATOR','record':false,'token':'" + token + + "','sessionId':'CUSTOM_SESSION_ID','createdAt':" + createdAt + ",'activeAt':" + + activeAt + ",'serverData':'MY_SERVER_PRO_DATA'}", + new String[] { "location", "platform", "clientData" })); + + user.getEventManager().waitUntilEventReaches("connectionPropertyChanged", 1); + + // Updating only record should let role value untouched + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, + "{'record':true}", HttpStatus.SC_OK, false, true, true, + mergeJson(DEFAULT_JSON_ACTIVE_CONNECTION, + "{'id':'" + connectionId + "','connectionId':'" + connectionId + + "','role':'MODERATOR','record':true,'token':'" + token + + "','sessionId':'CUSTOM_SESSION_ID','createdAt':" + createdAt + ",'activeAt':" + + activeAt + ",'serverData':'MY_SERVER_PRO_DATA'}", + new String[] { "location", "platform", "clientData" })); + + user.getEventManager().waitUntilEventReaches("connectionPropertyChanged", 2); + + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, + "{'role':'SUBSCRIBER','record':true,'data':'OTHER DATA'}", HttpStatus.SC_OK, false, true, true, + mergeJson(DEFAULT_JSON_ACTIVE_CONNECTION, + "{'id':'" + connectionId + "','connectionId':'" + connectionId + + "','role':'SUBSCRIBER','record':true,'token':'" + token + + "','sessionId':'CUSTOM_SESSION_ID','createdAt':" + createdAt + ",'activeAt':" + + activeAt + ",'serverData':'MY_SERVER_PRO_DATA'}", + new String[] { "location", "platform", "clientData" })); + + user.getEventManager().waitUntilEventReaches("connectionPropertyChanged", 3); + + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, + "{'role':'PUBLISHER'}", HttpStatus.SC_OK, false, true, true, + mergeJson(DEFAULT_JSON_ACTIVE_CONNECTION, + "{'id':'" + connectionId + "','connectionId':'" + connectionId + + "','role':'PUBLISHER','record':true,'token':'" + token + + "','sessionId':'CUSTOM_SESSION_ID','createdAt':" + createdAt + ",'activeAt':" + + activeAt + ",'serverData':'MY_SERVER_PRO_DATA'}", + new String[] { "location", "platform", "clientData" })); + + user.getEventManager().waitUntilEventReaches("connectionPropertyChanged", 4); + + // Test with openvidu-node-client + user.getDriver().findElement(By.id("session-api-btn-0")).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.id("connection-id-field")).clear(); + user.getDriver().findElement(By.id("connection-id-field")).sendKeys(connectionId); + user.getDriver().findElement(By.id("update-connection-api-btn")).click(); + user.getWaiter().until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", + "Connection updated: {\"role\":\"PUBLISHER\",\"record\":true,\"data\":\"MY_SERVER_PRO_DATA\"}")); + user.getDriver().findElement(By.id("record-checkbox")).click(); + user.getDriver().findElement(By.id("token-role-select")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("option-SUBSCRIBER")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("update-connection-api-btn")).click(); + user.getWaiter().until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", + "Connection updated: {\"role\":\"SUBSCRIBER\",\"record\":false,\"data\":\"MY_SERVER_PRO_DATA\"}")); + + user.getEventManager().waitUntilEventReaches("connectionPropertyChanged", 6); + + user.getDriver().findElement(By.id("close-dialog-btn")).click(); + Thread.sleep(1000); + + // Test with openvidu-java-client + Assert.assertFalse("Session object should not have changed", session.fetch()); + try { + session.updateConnection("WRONG_CONNECTION_ID", new ConnectionProperties.Builder().build()); + Assert.fail("Expected OpenViduHttpException exception"); + } catch (OpenViduHttpException exception) { + Assert.assertEquals("Wrong HTTP status", HttpStatus.SC_NOT_FOUND, exception.getStatus()); + } + Assert.assertFalse("Session object should not have changed", session.fetch()); + connection = session.updateConnection(connectionId, + new ConnectionProperties.Builder().role(OpenViduRole.PUBLISHER).build()); + + user.getEventManager().waitUntilEventReaches("connectionPropertyChanged", 7); + + Assert.assertFalse("Session object should not have changed", session.fetch()); + Assert.assertEquals("Wrong connectionId in Connection object", connectionId, connection.getConnectionId()); + Assert.assertEquals("Wrong role in Connection object", OpenViduRole.PUBLISHER, connection.getRole()); + Assert.assertFalse("Wrong record in Connection object", connection.record()); + Assert.assertEquals("Wrong status in Connection object", "active", connection.getStatus()); + connection = session.updateConnection(connectionId, + new ConnectionProperties.Builder().role(OpenViduRole.SUBSCRIBER).build()); + + user.getEventManager().waitUntilEventReaches("connectionPropertyChanged", 8); + + Assert.assertEquals("Wrong role in Connection object", OpenViduRole.SUBSCRIBER, connection.getRole()); + Assert.assertFalse("Session object should not have changed", session.fetch()); + connection = session.updateConnection(connectionId, new ConnectionProperties.Builder() + .role(OpenViduRole.MODERATOR).record(false).data("NO CHANGE").build()); + + user.getEventManager().waitUntilEventReaches("connectionPropertyChanged", 9); + + Assert.assertFalse("Session object should not have changed", session.fetch()); + Assert.assertEquals("Wrong role in Connection object", OpenViduRole.MODERATOR, connection.getRole()); + Assert.assertFalse("Wrong record in Connection object", connection.record()); + Assert.assertEquals("Wrong data in Connection object", "MY_SERVER_PRO_DATA", connection.getServerData()); + Assert.assertEquals("Wrong status in Connection object", "active", connection.getStatus()); + + user.getEventManager().resetEventThread(); + + user.getWaiter().until(ExpectedConditions.elementToBeClickable(By.cssSelector(".republish-error-btn"))); + user.getDriver().findElement(By.cssSelector(".republish-error-btn")).click(); + + user.getEventManager().waitUntilEventReaches("accessAllowed", 1); + user.getEventManager().waitUntilEventReaches("streamPlaying", 1); + user.getEventManager().waitUntilEventReaches("streamCreated", 1); + + // connectionId should be equal to the one brought by the token + Assert.assertEquals("Wrong connectionId", connectionId, + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/CUSTOM_SESSION_ID", HttpStatus.SC_OK) + .get("connections").getAsJsonObject().get("content").getAsJsonArray().get(0).getAsJsonObject() + .get("connectionId").getAsString()); + + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/CUSTOM_SESSION_ID", HttpStatus.SC_NO_CONTENT); + + // GET /openvidu/api/sessions should return empty again + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions", null, HttpStatus.SC_OK, true, true, true, + "{'numberOfElements':0,'content':[]}"); + + /** GET /openvidu/api/config **/ + restClient.rest(HttpMethod.GET, "/openvidu/api/config", null, HttpStatus.SC_OK, true, false, true, + "{'VERSION':'STR','DOMAIN_OR_PUBLIC_IP':'STR','HTTPS_PORT':0,'OPENVIDU_PUBLICURL':'STR','OPENVIDU_CDR':false,'OPENVIDU_STREAMS_VIDEO_MAX_RECV_BANDWIDTH':0,'OPENVIDU_STREAMS_VIDEO_MIN_RECV_BANDWIDTH':0," + + "'OPENVIDU_STREAMS_VIDEO_MAX_SEND_BANDWIDTH':0,'OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH':0,'OPENVIDU_SESSIONS_GARBAGE_INTERVAL':0,'OPENVIDU_SESSIONS_GARBAGE_THRESHOLD':0," + + "'OPENVIDU_RECORDING':false,'OPENVIDU_RECORDING_VERSION':'STR','OPENVIDU_RECORDING_PATH':'STR','OPENVIDU_RECORDING_PUBLIC_ACCESS':false,'OPENVIDU_RECORDING_NOTIFICATION':'STR'," + + "'OPENVIDU_RECORDING_CUSTOM_LAYOUT':'STR','OPENVIDU_RECORDING_AUTOSTOP_TIMEOUT':0,'OPENVIDU_WEBHOOK':false,'OPENVIDU_SERVER_DEPENDENCY_VERSION':'STR','KMS_URIS':[]," + + "'OPENVIDU_PRO_STATS_MONITORING_INTERVAL':0,'OPENVIDU_PRO_STATS_WEBRTC_INTERVAL':0,'OPENVIDU_PRO_CLUSTER_ID':'STR'," + + "'OPENVIDU_PRO_CLUSTER_ENVIRONMENT':'STR','OPENVIDU_PRO_CLUSTER_MEDIA_NODES':0,'OPENVIDU_PRO_CLUSTER_PATH':'STR','OPENVIDU_PRO_CLUSTER_AUTOSCALING':false," + + "'OPENVIDU_PRO_ELASTICSEARCH':true,'OPENVIDU_PRO_ELASTICSEARCH_VERSION':'STR','OPENVIDU_PRO_ELASTICSEARCH_HOST':'STR','OPENVIDU_PRO_KIBANA':true,'OPENVIDU_PRO_KIBANA_VERSION':'STR'," + + "'OPENVIDU_PRO_KIBANA_HOST':'STR','OPENVIDU_PRO_RECORDING_STORAGE':'STR','OPENVIDU_PRO_NETWORK_QUALITY':false,'OPENVIDU_STREAMS_ALLOW_TRANSCODING':false,'OPENVIDU_STREAMS_FORCED_VIDEO_CODEC':'STR'}"); + + /** GET /openvidu/api/health **/ + restClient.rest(HttpMethod.GET, "/openvidu/api/health", null, HttpStatus.SC_OK, true, true, true, + "{'status':'UP'}"); + } + + @Test + @DisplayName("openvidu-java-client PRO test") + void openViduJavaClientProTest() throws Exception { + + log.info("openvidu-java-client PRO test"); + + // Create default Connection + Session session = OV.createSession(); + Assert.assertFalse(session.fetch()); + Connection connectionDefault = session.createConnection(); + Assert.assertFalse(session.fetch()); + Assert.assertEquals("Wrong role property", OpenViduRole.PUBLISHER, connectionDefault.getRole()); + Assert.assertTrue("Wrong record property", connectionDefault.record()); + Assert.assertEquals("Wrong data property", "", connectionDefault.getServerData()); + // Update Connection + session.updateConnection(connectionDefault.getConnectionId(), new ConnectionProperties.Builder() + .role(OpenViduRole.SUBSCRIBER).record(false).data("WILL HAVE NO EFFECT").build()); + Assert.assertEquals("Wrong role property", OpenViduRole.SUBSCRIBER, connectionDefault.getRole()); + Assert.assertFalse("Wrong record property", connectionDefault.record()); + Assert.assertEquals("Wrong data property", "", connectionDefault.getServerData()); + Assert.assertFalse(session.fetch()); + + // Create custom properties Connection + long timestamp = System.currentTimeMillis(); + Connection connection = session.createConnection( + new ConnectionProperties.Builder().record(false).role(OpenViduRole.MODERATOR).data("SERVER_SIDE_DATA") + .kurentoOptions(new KurentoOptions.Builder().videoMaxRecvBandwidth(555) + .videoMinRecvBandwidth(555).videoMaxSendBandwidth(555).videoMinSendBandwidth(555) + .allowedFilters(new String[] { "555" }).build()) + .build()); + Assert.assertEquals("Wrong status Connection property", "pending", connection.getStatus()); + Assert.assertTrue("Wrong timestamp Connection property", connection.createdAt() > timestamp); + Assert.assertTrue("Wrong activeAt Connection property", connection.activeAt() == null); + Assert.assertTrue("Wrong location Connection property", connection.getLocation() == null); + Assert.assertTrue("Wrong platform Connection property", connection.getPlatform() == null); + Assert.assertTrue("Wrong clientData Connection property", connection.getClientData() == null); + Assert.assertTrue("Wrong publishers Connection property", connection.getPublishers().size() == 0); + Assert.assertTrue("Wrong subscribers Connection property", connection.getSubscribers().size() == 0); + Assert.assertTrue("Wrong token Connection property", connection.getToken().contains(session.getSessionId())); + Assert.assertEquals("Wrong type property", ConnectionType.WEBRTC, connection.getType()); + Assert.assertEquals("Wrong data property", "SERVER_SIDE_DATA", connection.getServerData()); + Assert.assertFalse("Wrong record property", connection.record()); + Assert.assertEquals("Wrong role property", OpenViduRole.MODERATOR, connection.getRole()); + Assert.assertTrue("Wrong rtspUri property", connection.getRtspUri() == null); + Assert.assertTrue("Wrong adaptativeBitrate property", connection.adaptativeBitrate() == null); + Assert.assertTrue("Wrong onlyPlayWithSubscribers property", connection.onlyPlayWithSubscribers() == null); + Assert.assertTrue("Wrong networkCache property", connection.getNetworkCache() == null); + } + + @Test + @DisplayName("Network quality test") + void networkQualityTest() throws Exception { + + isNetworkQualityTest = true; + + log.info("Network quality test"); + + CustomHttpClient restClient = new CustomHttpClient(OPENVIDU_URL, "OPENVIDUAPP", OPENVIDU_SECRET); + String body = "{'OPENVIDU_PRO_NETWORK_QUALITY':true, 'OPENVIDU_PRO_NETWORK_QUALITY_INTERVAL':5}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/restart", body, 200); + waitUntilOpenViduRestarted(30); + + setupBrowser("chrome"); + user.getDriver().findElement(By.id("add-user-btn")).click(); + user.getDriver().findElement(By.className("join-btn")).click(); + + user.getEventManager().waitUntilEventReaches("connectionCreated", 1); + user.getEventManager().waitUntilEventReaches("streamPlaying", 1); + JsonObject res = restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/TestSession/connection", + HttpStatus.SC_OK); + final String connectionId = res.getAsJsonObject().get("content").getAsJsonArray().get(0).getAsJsonObject() + .get("id").getAsString(); + + user.getDriver().findElement(By.id("add-user-btn")).click(); + user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .publish-checkbox")).click(); + user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .join-btn")).click(); + + final CountDownLatch latch1 = new CountDownLatch(1); + Queue threadAssertions = new ConcurrentLinkedQueue(); + user.getEventManager().on("networkQualityLevelChanged", (event) -> { + try { + threadAssertions.add("networkQualityLevelChanged".equals(event.get("type").getAsString())); + threadAssertions.add(event.get("oldValue") == null); + threadAssertions.add(event.has("newValue") && event.get("newValue").getAsInt() > 0 + && event.get("newValue").getAsInt() < 6); + latch1.countDown(); + } catch (Exception e) { + log.error("Error analysing NetworkQualityLevelChangedEvent: {}. {}", e.getCause(), e.getMessage()); + fail("Error analysing NetworkQualityLevelChangedEvent: " + e.getCause() + ". " + e.getMessage()); + } + }); + + user.getEventManager().waitUntilEventReaches("connectionCreated", 4); + user.getEventManager().waitUntilEventReaches("streamPlaying", 2); + user.getEventManager().waitUntilEventReaches("networkQualityLevelChanged", 2); + + if (!latch1.await(30000, TimeUnit.MILLISECONDS)) { + gracefullyLeaveParticipants(1); + fail(); + return; + } + + user.getEventManager().off("networkQualityLevelChanged"); + log.info("Thread assertions: {}", threadAssertions.toString()); + for (Iterator iter = threadAssertions.iterator(); iter.hasNext();) { + Assert.assertTrue("Some Event property was wrong", iter.next()); + iter.remove(); + } + + // Both events should have publisher's connection ID + Assert.assertTrue("Wrong connectionId in event NetworkQualityLevelChangedEvent", user.getDriver() + .findElement(By.cssSelector("#openvidu-instance-0 .mat-expansion-panel:last-child .event-content")) + .getAttribute("textContent").contains(connectionId)); + Assert.assertTrue("Wrong connectionId in event NetworkQualityLevelChangedEvent", user.getDriver() + .findElement(By.cssSelector("#openvidu-instance-1 .mat-expansion-panel:last-child .event-content")) + .getAttribute("textContent").contains(connectionId)); + + gracefullyLeaveParticipants(1); + } + + private void waitUntilOpenViduRestarted(int maxSecondsWait) throws Exception { + boolean restarted = false; + int msInterval = 500; + int attempts = 0; + final int maxAttempts = maxSecondsWait * 1000 / msInterval; + Thread.sleep(500); + while (!restarted && attempts < maxAttempts) { + try { + CustomHttpClient restClient = new CustomHttpClient(OPENVIDU_URL, "OPENVIDUAPP", OPENVIDU_SECRET); + restClient.rest(HttpMethod.GET, "/openvidu/api/health", 200); + restarted = true; + } catch (Exception e) { + try { + log.warn("Waiting for OpenVidu Server..."); + Thread.sleep(msInterval); + } catch (InterruptedException e1) { + log.error("Sleep interrupted"); + } + attempts++; + } + } + if (!restarted && attempts == maxAttempts) { + throw new TimeoutException(); + } + } + +} diff --git a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduTestAppE2eTest.java b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduTestAppE2eTest.java index 93ce5a63..287d0297 100644 --- a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduTestAppE2eTest.java +++ b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduTestAppE2eTest.java @@ -18,24 +18,11 @@ package io.openvidu.test.e2e; import static org.junit.Assert.fail; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.openqa.selenium.OutputType.BASE64; -import java.awt.Color; -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.math.RoundingMode; -import java.nio.file.Path; import java.nio.file.Paths; -import java.text.DecimalFormat; -import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; -import java.util.Collection; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -44,17 +31,10 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import javax.imageio.ImageIO; - -import org.apache.commons.io.FileUtils; import org.apache.http.HttpStatus; -import org.jcodec.api.FrameGrab; -import org.jcodec.api.JCodecException; -import org.jcodec.common.model.Picture; -import org.jcodec.scale.AWTUtil; import org.junit.Assert; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -63,30 +43,25 @@ import org.openqa.selenium.Alert; import org.openqa.selenium.By; import org.openqa.selenium.Dimension; import org.openqa.selenium.Keys; -import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedCondition; import org.openqa.selenium.support.ui.ExpectedConditions; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.test.context.junit.jupiter.SpringExtension; -import com.google.common.collect.ImmutableMap; -import com.google.gson.Gson; import com.google.gson.JsonArray; +import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import com.google.gson.stream.JsonReader; import com.mashape.unirest.http.HttpMethod; -import io.github.bonigarcia.wdm.WebDriverManager; import io.openvidu.java.client.Connection; +import io.openvidu.java.client.ConnectionProperties; +import io.openvidu.java.client.ConnectionType; import io.openvidu.java.client.KurentoOptions; import io.openvidu.java.client.MediaMode; import io.openvidu.java.client.OpenVidu; import io.openvidu.java.client.OpenViduHttpException; -import io.openvidu.java.client.OpenViduJavaClientException; import io.openvidu.java.client.OpenViduRole; import io.openvidu.java.client.Publisher; import io.openvidu.java.client.Recording; @@ -96,18 +71,12 @@ import io.openvidu.java.client.RecordingMode; import io.openvidu.java.client.RecordingProperties; import io.openvidu.java.client.Session; import io.openvidu.java.client.SessionProperties; -import io.openvidu.java.client.TokenOptions; import io.openvidu.java.client.VideoCodec; -import io.openvidu.test.browsers.BrowserUser; -import io.openvidu.test.browsers.ChromeAndroidUser; -import io.openvidu.test.browsers.ChromeUser; import io.openvidu.test.browsers.FirefoxUser; -import io.openvidu.test.browsers.OperaUser; -import io.openvidu.test.browsers.utils.CommandLineExecutor; import io.openvidu.test.browsers.utils.CustomHttpClient; -import io.openvidu.test.browsers.utils.CustomWebhook; -import io.openvidu.test.browsers.utils.MultimediaFileMetadata; -import io.openvidu.test.browsers.utils.Unzipper; +import io.openvidu.test.browsers.utils.RecordingUtils; +import io.openvidu.test.browsers.utils.layout.CustomLayoutHandler; +import io.openvidu.test.browsers.utils.webhook.CustomWebhook; /** * E2E tests for openvidu-testapp. @@ -118,159 +87,15 @@ import io.openvidu.test.browsers.utils.Unzipper; @Tag("e2e") @DisplayName("E2E tests for OpenVidu TestApp") @ExtendWith(SpringExtension.class) -public class OpenViduTestAppE2eTest { - - static String OPENVIDU_SECRET = "MY_SECRET"; - static String OPENVIDU_URL = "https://localhost:4443/"; - static String APP_URL = "http://localhost:4200/"; - static Exception ex = null; - private final Object lock = new Object(); - - private static final Logger log = LoggerFactory.getLogger(OpenViduTestAppE2eTest.class); - private static final CommandLineExecutor commandLine = new CommandLineExecutor(); - private static final String RECORDING_IMAGE = "openvidu/openvidu-recording"; - - MyUser user; - Collection otherUsers = new ArrayList<>(); - volatile static boolean isRecordingTest; - volatile static boolean isKurentoRestartTest; - private static OpenVidu OV; +public class OpenViduTestAppE2eTest extends AbstractOpenViduTestAppE2eTest { @BeforeAll() - static void setupAll() { - - String ffmpegOutput = commandLine.executeCommand("which ffmpeg"); - if (ffmpegOutput == null || ffmpegOutput.isEmpty()) { - log.error("ffmpeg package is not installed in the host machine"); - Assert.fail(); - return; - } else { - log.info("ffmpeg is installed and accesible"); - } - - WebDriverManager.chromedriver().setup(); - WebDriverManager.firefoxdriver().setup(); - - String appUrl = System.getProperty("APP_URL"); - if (appUrl != null) { - APP_URL = appUrl; - } - log.info("Using URL {} to connect to openvidu-testapp", APP_URL); - - String openviduUrl = System.getProperty("OPENVIDU_URL"); - if (openviduUrl != null) { - OPENVIDU_URL = openviduUrl; - } - log.info("Using URL {} to connect to openvidu-server", OPENVIDU_URL); - - String openvidusecret = System.getProperty("OPENVIDU_SECRET"); - if (openvidusecret != null) { - OPENVIDU_SECRET = openvidusecret; - } - log.info("Using secret {} to connect to openvidu-server", OPENVIDU_SECRET); - - try { - log.info("Cleaning folder /opt/openvidu/recordings"); - FileUtils.cleanDirectory(new File("/opt/openvidu/recordings")); - } catch (IOException e) { - log.error(e.getMessage()); - } - OV = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET); - } - - void setupBrowser(String browser) { - - BrowserUser browserUser; - - switch (browser) { - case "chrome": - browserUser = new ChromeUser("TestUser", 50, false); - break; - case "firefox": - browserUser = new FirefoxUser("TestUser", 50); - break; - case "opera": - browserUser = new OperaUser("TestUser", 50); - break; - case "chromeAndroid": - browserUser = new ChromeAndroidUser("TestUser", 50); - break; - case "chromeAlternateScreenShare": - browserUser = new ChromeUser("TestUser", 50, "OpenVidu TestApp", false); - break; - case "chromeAsRoot": - browserUser = new ChromeUser("TestUser", 50, true); - break; - default: - browserUser = new ChromeUser("TestUser", 50, false); - } - - this.user = new MyUser(browserUser); - - user.getDriver().get(APP_URL); - - WebElement urlInput = user.getDriver().findElement(By.id("openvidu-url")); - urlInput.clear(); - urlInput.sendKeys(OPENVIDU_URL); - WebElement secretInput = user.getDriver().findElement(By.id("openvidu-secret")); - secretInput.clear(); - secretInput.sendKeys(OPENVIDU_SECRET); - - user.getEventManager().startPolling(); - } - - void setupChromeWithFakeVideo(Path videoFileLocation) { - this.user = new MyUser(new ChromeUser("TestUser", 50, videoFileLocation)); - user.getDriver().get(APP_URL); - WebElement urlInput = user.getDriver().findElement(By.id("openvidu-url")); - urlInput.clear(); - urlInput.sendKeys(OPENVIDU_URL); - WebElement secretInput = user.getDriver().findElement(By.id("openvidu-secret")); - secretInput.clear(); - secretInput.sendKeys(OPENVIDU_SECRET); - user.getEventManager().startPolling(); - } - - @AfterEach - void dispose() { - if (user != null) { - user.dispose(); - } - Iterator it = otherUsers.iterator(); - while (it.hasNext()) { - MyUser other = it.next(); - other.dispose(); - it.remove(); - } - try { - OV.fetch(); - } catch (OpenViduJavaClientException | OpenViduHttpException e1) { - log.error("Error fetching sessions: {}", e1.getMessage()); - } - OV.getActiveSessions().forEach(session -> { - try { - session.close(); - log.info("Session {} successfully closed", session.getSessionId()); - } catch (OpenViduJavaClientException e) { - log.error("Error closing session: {}", e.getMessage()); - } catch (OpenViduHttpException e) { - log.error("Error closing session: {}", e.getMessage()); - } - }); - if (isRecordingTest) { - removeAllRecordingContiners(); - try { - FileUtils.cleanDirectory(new File("/opt/openvidu/recordings")); - } catch (IOException e) { - log.error(e.getMessage()); - } - isRecordingTest = false; - } - if (isKurentoRestartTest) { - this.restartKms(); - isKurentoRestartTest = false; - } - OV = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET); + protected static void setupAll() throws Exception { + checkFfmpegInstallation(); + loadEnvironmentVariables(); + setupBrowserDrivers(); + cleanFoldersAndSetUpOpenViduJavaClient(); + getDefaultTranscodingValues(); } @Test @@ -297,6 +122,54 @@ public class OpenViduTestAppE2eTest { gracefullyLeaveParticipants(2); } + @Test + @DisplayName("One2One Firefox [Video + Audio]") + void oneToOneVideoAudioSessionFirefox() throws Exception { + + setupBrowser("firefox"); + + log.info("One2One Firefox [Video + Audio]"); + + user.getDriver().findElement(By.id("auto-join-checkbox")).click(); + user.getDriver().findElement(By.id("one2one-btn")).click(); + + user.getEventManager().waitUntilEventReaches("connectionCreated", 4); + user.getEventManager().waitUntilEventReaches("accessAllowed", 2); + user.getEventManager().waitUntilEventReaches("streamCreated", 4); + user.getEventManager().waitUntilEventReaches("streamPlaying", 4); + + final int numberOfVideos = user.getDriver().findElements(By.tagName("video")).size(); + Assert.assertEquals("Expected 4 videos but found " + numberOfVideos, 4, numberOfVideos); + Assert.assertTrue("Videos were expected to have audio and video tracks", user.getEventManager() + .assertMediaTracks(user.getDriver().findElements(By.tagName("video")), true, true)); + + gracefullyLeaveParticipants(2); + } + + @Test + @DisplayName("One2One Opera [Video + Audio]") + void oneToOneVideoAudioSessionOpera() throws Exception { + + setupBrowser("opera"); + + log.info("One2One Opera [Video + Audio]"); + + user.getDriver().findElement(By.id("auto-join-checkbox")).click(); + user.getDriver().findElement(By.id("one2one-btn")).click(); + + user.getEventManager().waitUntilEventReaches("connectionCreated", 4); + user.getEventManager().waitUntilEventReaches("accessAllowed", 2); + user.getEventManager().waitUntilEventReaches("streamCreated", 4); + user.getEventManager().waitUntilEventReaches("streamPlaying", 4); + + final int numberOfVideos = user.getDriver().findElements(By.tagName("video")).size(); + Assert.assertEquals("Expected 4 videos but found " + numberOfVideos, 4, numberOfVideos); + Assert.assertTrue("Videos were expected to have audio and video tracks", user.getEventManager() + .assertMediaTracks(user.getDriver().findElements(By.tagName("video")), true, true)); + + gracefullyLeaveParticipants(2); + } + @Test @DisplayName("One2One Chrome [Video + Audio] - Force VP8") void oneToOneVideoAudioSessionChromeForceVP8() throws Exception { @@ -345,7 +218,7 @@ public class OpenViduTestAppE2eTest { statButton.click(); Thread.sleep(1000); String videoCodecUsed = user.getDriver().findElement(By.id("video-codec-used")).getText(); - assertEquals(videoCodecUsed, "video/" + codec.name()); + Assert.assertEquals("Expected video codec", videoCodecUsed, "video/" + codec.name()); user.getDriver().findElement(By.id("close-dialog-btn")).click(); } } @@ -547,36 +420,10 @@ public class OpenViduTestAppE2eTest { gracefullyLeaveParticipants(4); } - @Test - @DisplayName("One2One Firefox [Video + Audio]") - void oneToOneVideoAudioSessionFirefox() throws Exception { - - setupBrowser("firefox"); - - log.info("One2One Firefox [Video + Audio]"); - - user.getDriver().findElement(By.id("auto-join-checkbox")).click(); - user.getDriver().findElement(By.id("one2one-btn")).click(); - - user.getEventManager().waitUntilEventReaches("connectionCreated", 4); - user.getEventManager().waitUntilEventReaches("accessAllowed", 2); - user.getEventManager().waitUntilEventReaches("streamCreated", 4); - user.getEventManager().waitUntilEventReaches("streamPlaying", 4); - - final int numberOfVideos = user.getDriver().findElements(By.tagName("video")).size(); - Assert.assertEquals("Expected 4 videos but found " + numberOfVideos, 4, numberOfVideos); - Assert.assertTrue("Videos were expected to have audio and video tracks", user.getEventManager() - .assertMediaTracks(user.getDriver().findElements(By.tagName("video")), true, true)); - - gracefullyLeaveParticipants(2); - } - @Test @DisplayName("Cross-Browser test") void crossBrowserTest() throws Exception { - setupBrowser("chrome"); - log.info("Cross-Browser test"); Thread.UncaughtExceptionHandler h = new Thread.UncaughtExceptionHandler() { @@ -588,8 +435,10 @@ public class OpenViduTestAppE2eTest { } }; - Thread t = new Thread(() -> { - MyUser user2 = new MyUser(new FirefoxUser("TestUser", 30)); + final CountDownLatch latch = new CountDownLatch(2); + + Thread threadFirefox = new Thread(() -> { + MyUser user2 = new MyUser(new FirefoxUser("TestUser", 30, false)); otherUsers.add(user2); user2.getDriver().get(APP_URL); WebElement urlInput = user2.getDriver().findElement(By.id("openvidu-url")); @@ -614,36 +463,61 @@ public class OpenViduTestAppE2eTest { Assert.assertTrue("Videos were expected to have audio and video tracks", user.getEventManager() .assertMediaTracks(user.getDriver().findElements(By.tagName("video")), true, true)); + latch.countDown(); + if (!latch.await(30, TimeUnit.SECONDS)) { + Assert.fail("The other browser didn't play the stream within the timeout"); + } + user2.getEventManager().waitUntilEventReaches("streamDestroyed", 1); user2.getEventManager().waitUntilEventReaches("connectionDestroyed", 1); user2.getDriver().findElement(By.id("remove-user-btn")).click(); user2.getEventManager().waitUntilEventReaches("sessionDisconnected", 1); } catch (Exception e) { e.printStackTrace(); - user2.dispose(); Thread.currentThread().interrupt(); + Assert.fail("Exception on Firefox participant: " + e.getMessage()); + } finally { + user2.dispose(); } - user2.dispose(); }); - t.setUncaughtExceptionHandler(h); - t.start(); - user.getDriver().findElement(By.id("add-user-btn")).click(); - user.getDriver().findElement(By.className("join-btn")).click(); + Thread threadChrome = new Thread(() -> { + setupBrowser("chrome"); + user.getDriver().findElement(By.id("add-user-btn")).click(); + user.getDriver().findElement(By.className("join-btn")).click(); - user.getEventManager().waitUntilEventReaches("connectionCreated", 2); - user.getEventManager().waitUntilEventReaches("accessAllowed", 1); - user.getEventManager().waitUntilEventReaches("streamCreated", 2); - user.getEventManager().waitUntilEventReaches("streamPlaying", 2); + try { + user.getEventManager().waitUntilEventReaches("connectionCreated", 2); + user.getEventManager().waitUntilEventReaches("accessAllowed", 1); + user.getEventManager().waitUntilEventReaches("streamCreated", 2); + user.getEventManager().waitUntilEventReaches("streamPlaying", 2); - final int numberOfVideos = user.getDriver().findElements(By.tagName("video")).size(); - Assert.assertEquals("Expected 2 videos but found " + numberOfVideos, 2, numberOfVideos); - Assert.assertTrue("Videos were expected to have audio and video tracks", user.getEventManager() - .assertMediaTracks(user.getDriver().findElements(By.tagName("video")), true, true)); + final int numberOfVideos = user.getDriver().findElements(By.tagName("video")).size(); + Assert.assertEquals("Expected 2 videos but found " + numberOfVideos, 2, numberOfVideos); + Assert.assertTrue("Videos were expected to have audio and video tracks", user.getEventManager() + .assertMediaTracks(user.getDriver().findElements(By.tagName("video")), true, true)); - gracefullyLeaveParticipants(1); + latch.countDown(); + if (!latch.await(30, TimeUnit.SECONDS)) { + Assert.fail("The other browser didn't play the stream within the timeout"); + } - t.join(); + gracefullyLeaveParticipants(1); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail("Exception on Chrome participant: " + e.getMessage()); + Thread.currentThread().interrupt(); + } finally { + user.dispose(); + } + }); + + threadFirefox.setUncaughtExceptionHandler(h); + threadChrome.setUncaughtExceptionHandler(h); + threadFirefox.start(); + threadChrome.start(); + threadFirefox.join(); + threadChrome.join(); synchronized (lock) { if (OpenViduTestAppE2eTest.ex != null) { @@ -1007,9 +881,13 @@ public class OpenViduTestAppE2eTest { user.getEventManager().waitUntilEventReaches("streamCreated", 2); user.getEventManager().waitUntilEventReaches("streamPlaying", 2); + // Give some time for the screen sharing warning to stop resizing the viewport + Thread.sleep(3000); + // Unpublish video final CountDownLatch latch1 = new CountDownLatch(2); user.getEventManager().on("streamPropertyChanged", (event) -> { + System.out.println(event.toString()); threadAssertions.add("videoActive".equals(event.get("changedProperty").getAsString())); threadAssertions.add(!event.get("newValue").getAsBoolean()); latch1.countDown(); @@ -1033,6 +911,7 @@ public class OpenViduTestAppE2eTest { // Unpublish audio final CountDownLatch latch2 = new CountDownLatch(2); user.getEventManager().on("streamPropertyChanged", (event) -> { + System.out.println(event.toString()); threadAssertions.add("audioActive".equals(event.get("changedProperty").getAsString())); threadAssertions.add(!event.get("newValue").getAsBoolean()); latch2.countDown(); @@ -1065,9 +944,11 @@ public class OpenViduTestAppE2eTest { + "}"; System.out.println("Publisher dimensions: " + event.get("newValue").getAsJsonObject().toString()); System.out.println("Real dimensions of viewport: " + expectedDimensions); - threadAssertions.add("videoDimensions".equals(event.get("changedProperty").getAsString())); - threadAssertions.add(expectedDimensions.equals(event.get("newValue").getAsJsonObject().toString())); - latch3.countDown(); + if ("videoDimensions".equals(event.get("changedProperty").getAsString())) { + if (expectedDimensions.equals(event.get("newValue").getAsJsonObject().toString())) { + latch3.countDown(); + } + } }); user.getDriver().manage().window().setSize(new Dimension(newWidth, newHeight)); @@ -1082,7 +963,7 @@ public class OpenViduTestAppE2eTest { user.getEventManager().waitUntilEventReaches("streamPropertyChanged", 6); - if (!latch3.await(5000, TimeUnit.MILLISECONDS)) { + if (!latch3.await(6000, TimeUnit.MILLISECONDS)) { gracefullyLeaveParticipants(2); fail(); return; @@ -1091,22 +972,17 @@ public class OpenViduTestAppE2eTest { System.out.println(getBase64Screenshot(user)); user.getEventManager().off("streamPropertyChanged"); - log.info("Thread assertions: {}", threadAssertions.toString()); - for (Iterator iter = threadAssertions.iterator(); iter.hasNext();) { - Assert.assertTrue("Some Event property was wrong", iter.next()); - iter.remove(); - } gracefullyLeaveParticipants(2); } @Test - @DisplayName("Local record") - void localRecordTest() throws Exception { + @DisplayName("Local browser record") + void localBrowserRecordTest() throws Exception { setupBrowser("chrome"); - log.info("Local record"); + log.info("Local browser record"); user.getDriver().findElement(By.id("add-user-btn")).click(); user.getDriver().findElement(By.className("join-btn")).click(); @@ -1158,13 +1034,13 @@ public class OpenViduTestAppE2eTest { } @Test - @DisplayName("Remote composed record") - void remoteComposedRecordTest() throws Exception { + @DisplayName("Composed record") + void composedRecordTest() throws Exception { isRecordingTest = true; setupBrowser("chrome"); - log.info("Remote composed record"); + log.info("Composed record"); final String sessionName = "COMPOSED_RECORDED_SESSION"; final String resolution = "1280x720"; @@ -1279,9 +1155,10 @@ public class OpenViduTestAppE2eTest { Assert.assertTrue("File " + file3.getAbsolutePath() + " does not exist or is empty", file3.exists() && file3.length() > 0); - Assert.assertTrue("Recorded file " + file1.getAbsolutePath() + " is not fine", - this.recordedFileFine(file1, new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(sessionName))); - Assert.assertTrue("Thumbnail " + file3.getAbsolutePath() + " is not fine", this.thumbnailIsFine(file3)); + Assert.assertTrue("Recorded file " + file1.getAbsolutePath() + " is not fine", this.recordingUtils + .recordedGreenFileFine(file1, new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(sessionName))); + Assert.assertTrue("Thumbnail " + file3.getAbsolutePath() + " is not fine", + this.recordingUtils.thumbnailIsFine(file3, RecordingUtils::checkVideoAverageRgbGreen)); // Try to get the stopped recording user.getDriver().findElement(By.id("get-recording-btn")).click(); @@ -1309,113 +1186,188 @@ public class OpenViduTestAppE2eTest { } @Test - @DisplayName("Remote composed quick start record") - void remoteComposedQuickStartRecordTest() throws Exception { + @DisplayName("Composed quick start record") + void composedQuickStartRecordTest() throws Exception { isRecordingTest = true; setupBrowser("chrome"); - log.info("Remote composed quick start record"); + log.info("Composed quick start record"); - final String sessionName = "COMPOSED_QUICK_START_RECORDED_SESSION"; + CountDownLatch initLatch = new CountDownLatch(1); + io.openvidu.test.browsers.utils.webhook.CustomWebhook.main(new String[0], initLatch); - // 1. MANUAL mode and recording explicitly stopped + try { - user.getDriver().findElement(By.id("add-user-btn")).click(); - user.getDriver().findElement(By.id("session-name-input-0")).clear(); - user.getDriver().findElement(By.id("session-name-input-0")).sendKeys(sessionName); + if (!initLatch.await(30, TimeUnit.SECONDS)) { + Assert.fail("Timeout waiting for webhook springboot app to start"); + CustomWebhook.shutDown(); + return; + } - user.getDriver().findElement(By.id("session-settings-btn-0")).click(); - Thread.sleep(1000); - user.getDriver().findElement(By.id("output-mode-select")).click(); - Thread.sleep(500); - user.getDriver().findElement(By.id("option-COMPOSED_QUICK_START")).click(); - Thread.sleep(500); - user.getDriver().findElement(By.id("save-btn")).click(); - Thread.sleep(1000); + final String sessionName = "COMPOSED_QUICK_START_RECORDED_SESSION"; + JsonObject event; - // Join the subscriber user to the session - user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .publish-checkbox")).click(); - user.getDriver().findElement(By.className("join-btn")).click(); - user.getEventManager().waitUntilEventReaches("connectionCreated", 1); + // 1. MANUAL mode and recording explicitly stopped - // Check the recording container is up and running but no ongoing recordings - checkDockerContainerRunning(RECORDING_IMAGE, 1); - Assert.assertEquals("Wrong number of recordings found", 0, OV.listRecordings().size()); + user.getDriver().findElement(By.id("add-user-btn")).click(); + user.getDriver().findElement(By.id("session-name-input-0")).clear(); + user.getDriver().findElement(By.id("session-name-input-0")).sendKeys(sessionName); - // Join the publisher user to the session - user.getDriver().findElement(By.id("add-user-btn")).click(); - user.getDriver().findElement(By.id("session-name-input-1")).clear(); - user.getDriver().findElement(By.id("session-name-input-1")).sendKeys(sessionName); - user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .join-btn")).click(); + user.getDriver().findElement(By.id("session-settings-btn-0")).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.id("output-mode-select")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("option-COMPOSED_QUICK_START")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(1000); - user.getEventManager().waitUntilEventReaches("connectionCreated", 4); - user.getEventManager().waitUntilEventReaches("accessAllowed", 1); - user.getEventManager().waitUntilEventReaches("streamCreated", 2); - user.getEventManager().waitUntilEventReaches("streamPlaying", 2); + // Join the subscriber user to the session + user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .publish-checkbox")).click(); + user.getDriver().findElement(By.className("join-btn")).click(); + user.getEventManager().waitUntilEventReaches("connectionCreated", 1); - // Start recording - OV.fetch(); - String recId = OV.startRecording(sessionName).getId(); - user.getEventManager().waitUntilEventReaches("recordingStarted", 2); - checkDockerContainerRunning("openvidu/openvidu-recording", 1); + // Check the recording container is up and running but no ongoing recordings + checkDockerContainerRunning(RECORDING_IMAGE, 1); + Assert.assertEquals("Wrong number of recordings found", 0, OV.listRecordings().size()); - Thread.sleep(1000); + // Join the publisher user to the session + user.getDriver().findElement(By.id("add-user-btn")).click(); + user.getDriver().findElement(By.id("session-name-input-1")).clear(); + user.getDriver().findElement(By.id("session-name-input-1")).sendKeys(sessionName); + user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .join-btn")).click(); - Assert.assertEquals("Wrong number of recordings found", 1, OV.listRecordings().size()); - OV.stopRecording(recId); - user.getEventManager().waitUntilEventReaches("recordingStopped", 2); - checkDockerContainerRunning("openvidu/openvidu-recording", 1); + user.getEventManager().waitUntilEventReaches("connectionCreated", 4); + user.getEventManager().waitUntilEventReaches("accessAllowed", 1); + user.getEventManager().waitUntilEventReaches("streamCreated", 2); + user.getEventManager().waitUntilEventReaches("streamPlaying", 2); - Assert.assertEquals("Wrong number of sessions", 1, OV.getActiveSessions().size()); - Session session = OV.getActiveSessions().get(0); - session.close(); + // Start recording + OV.fetch(); + String recId = OV.startRecording(sessionName).getId(); + user.getEventManager().waitUntilEventReaches("recordingStarted", 2); + CustomWebhook.waitForEvent("recordingStatusChanged", 5); + checkDockerContainerRunning("openvidu/openvidu-recording", 1); - checkDockerContainerRunning("openvidu/openvidu-recording", 0); + Thread.sleep(2000); - // 2. ALWAYS mode and recording stopped by session close up - user.getDriver().findElement(By.id("remove-all-users-btn")).click(); - user.getDriver().findElement(By.id("add-user-btn")).click(); - user.getDriver().findElement(By.id("session-name-input-0")).clear(); - user.getDriver().findElement(By.id("session-name-input-0")).sendKeys(sessionName); + Assert.assertEquals("Wrong number of recordings found", 1, OV.listRecordings().size()); + OV.stopRecording(recId); + user.getEventManager().waitUntilEventReaches("recordingStopped", 2); + checkDockerContainerRunning("openvidu/openvidu-recording", 1); - user.getDriver().findElement(By.id("session-settings-btn-0")).click(); - Thread.sleep(1000); - user.getDriver().findElement(By.id("recording-mode-select")).click(); - Thread.sleep(500); - user.getDriver().findElement(By.id("option-ALWAYS")).click(); - Thread.sleep(500); - user.getDriver().findElement(By.id("output-mode-select")).click(); - Thread.sleep(500); - user.getDriver().findElement(By.id("option-COMPOSED_QUICK_START")).click(); - Thread.sleep(500); - user.getDriver().findElement(By.id("save-btn")).click(); - Thread.sleep(1000); + Assert.assertEquals("Wrong number of sessions", 1, OV.getActiveSessions().size()); + Session session = OV.getActiveSessions().get(0); + session.close(); - user.getDriver().findElement(By.className("join-btn")).click(); - user.getEventManager().waitUntilEventReaches("connectionCreated", 5); - user.getEventManager().waitUntilEventReaches("accessAllowed", 2); - user.getEventManager().waitUntilEventReaches("streamCreated", 3); - user.getEventManager().waitUntilEventReaches("streamPlaying", 3); - user.getEventManager().waitUntilEventReaches("recordingStarted", 3); + checkDockerContainerRunning("openvidu/openvidu-recording", 0); - checkDockerContainerRunning("openvidu/openvidu-recording", 1); + Assert.assertEquals("Wrong recording status", Recording.Status.ready, + OV.getRecording(sessionName).getStatus()); - OV.fetch(); - session = OV.getActiveSessions().get(0); - session.close(); + // 2. ALWAYS mode and recording stopped by session close up + CustomWebhook.clean(); + user.getDriver().findElement(By.id("remove-all-users-btn")).click(); + user.getDriver().findElement(By.id("add-user-btn")).click(); + user.getDriver().findElement(By.id("session-name-input-0")).clear(); + user.getDriver().findElement(By.id("session-name-input-0")).sendKeys(sessionName); - checkDockerContainerRunning("openvidu/openvidu-recording", 0); + user.getDriver().findElement(By.id("session-settings-btn-0")).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.id("recording-mode-select")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("option-ALWAYS")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("output-mode-select")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("option-COMPOSED_QUICK_START")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(1000); + + user.getDriver().findElement(By.className("join-btn")).click(); + user.getEventManager().waitUntilEventReaches("connectionCreated", 5); + user.getEventManager().waitUntilEventReaches("accessAllowed", 2); + user.getEventManager().waitUntilEventReaches("streamCreated", 3); + user.getEventManager().waitUntilEventReaches("streamPlaying", 3); + user.getEventManager().waitUntilEventReaches("recordingStarted", 3); + + event = CustomWebhook.waitForEvent("recordingStatusChanged", 5); // started + Assert.assertEquals("Wrong status in recordingStatusChanged event", "started", + event.get("status").getAsString()); + + checkDockerContainerRunning("openvidu/openvidu-recording", 1); + + OV.fetch(); + session = OV.getActiveSessions().get(0); + session.close(); + + checkDockerContainerRunning("openvidu/openvidu-recording", 0); + + Assert.assertEquals("Wrong recording status", Recording.Status.ready, + OV.getRecording(sessionName + "-1").getStatus()); + + // 3. Session closed before recording started should trigger + CustomWebhook.clean(); + user.getDriver().findElement(By.id("remove-all-users-btn")).click(); + user.getDriver().findElement(By.id("add-user-btn")).click(); + user.getDriver().findElement(By.id("session-name-input-0")).clear(); + user.getDriver().findElement(By.id("session-name-input-0")).sendKeys(sessionName); + + user.getDriver().findElement(By.id("session-settings-btn-0")).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.id("recording-mode-select")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("option-ALWAYS")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("output-mode-select")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("option-COMPOSED_QUICK_START")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(1000); + + user.getDriver().findElement(By.className("join-btn")).click(); + user.getEventManager().waitUntilEventReaches("connectionCreated", 6); + user.getEventManager().waitUntilEventReaches("accessAllowed", 3); + user.getEventManager().waitUntilEventReaches("streamCreated", 4); + user.getEventManager().waitUntilEventReaches("streamPlaying", 4); + checkDockerContainerRunning("openvidu/openvidu-recording", 1); + + OV.fetch(); + session = OV.getActiveSessions().get(0); + session.close(); + + // Recording hasn't had time to start. Should trigger stopped, started, failed + event = CustomWebhook.waitForEvent("recordingStatusChanged", 1); // stopped + Assert.assertEquals("Wrong status in recordingStatusChanged event", "stopped", + event.get("status").getAsString()); + event = CustomWebhook.waitForEvent("recordingStatusChanged", 5); // started + Assert.assertEquals("Wrong status in recordingStatusChanged event", "started", + event.get("status").getAsString()); + event = CustomWebhook.waitForEvent("recordingStatusChanged", 1); // failed + Assert.assertEquals("Wrong status in recordingStatusChanged event", "failed", + event.get("status").getAsString()); + + checkDockerContainerRunning("openvidu/openvidu-recording", 0); + + Assert.assertEquals("Wrong recording status", Recording.Status.failed, + OV.getRecording(sessionName + "-2").getStatus()); + + } finally { + CustomWebhook.shutDown(); + } } @Test - @DisplayName("Remote individual record") - void remoteIndividualRecordTest() throws Exception { + @DisplayName("Individual record") + void individualRecordTest() throws Exception { isRecordingTest = true; setupBrowser("chrome"); - log.info("Remote individual record"); + log.info("Individual record"); final String sessionName = "TestSession"; final String recordingName = "CUSTOM_NAME"; @@ -1489,7 +1441,7 @@ public class OpenViduTestAppE2eTest { String recPath = recordingsPath + sessionName + "/"; Recording recording = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(sessionName); - this.checkIndividualRecording(recPath, recording, 2, "opus", "vp8", true); + this.recordingUtils.checkIndividualRecording(recPath, recording, 2, "opus", "vp8", true); // Try to get the stopped recording user.getDriver().findElement(By.id("get-recording-btn")).click(); @@ -1515,13 +1467,13 @@ public class OpenViduTestAppE2eTest { } @Test - @DisplayName("Remote record cross-browser audio-only and video-only") - void remoteRecordAudioOnlyVideoOnlyTest() throws Exception { + @DisplayName("Record cross-browser audio-only and video-only") + void audioOnlyVideoOnlyRecordTest() throws Exception { isRecordingTest = true; setupBrowser("chromeAlternateScreenShare"); - log.info("Remote record cross-browser audio-only and video-only"); + log.info("Record cross-browser audio-only and video-only"); final String SESSION_NAME = "TestSession"; final String RECORDING_COMPOSED_VIDEO = "COMPOSED_VIDEO_ONLY"; @@ -1540,7 +1492,7 @@ public class OpenViduTestAppE2eTest { }; Thread t = new Thread(() -> { - MyUser user2 = new MyUser(new FirefoxUser("FirefoxUser", 30)); + MyUser user2 = new MyUser(new FirefoxUser("FirefoxUser", 30, false)); otherUsers.add(user2); user2.getDriver().get(APP_URL); WebElement urlInput = user2.getDriver().findElement(By.id("openvidu-url")); @@ -1713,24 +1665,24 @@ public class OpenViduTestAppE2eTest { // Check video-only COMPOSED recording String recPath = recordingsPath + SESSION_NAME + "/"; Recording recording = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(SESSION_NAME); - this.checkMultimediaFile(new File(recPath + recording.getName() + ".mp4"), false, true, recording.getDuration(), - recording.getResolution(), null, "h264", true); + this.recordingUtils.checkMultimediaFile(new File(recPath + recording.getName() + ".mp4"), false, true, + recording.getDuration(), recording.getResolution(), null, "h264", true); // Check audio-only COMPOSED recording recPath = recordingsPath + SESSION_NAME + "-1/"; recording = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(SESSION_NAME + "-1"); - this.checkMultimediaFile(new File(recPath + recording.getName() + ".webm"), true, false, + this.recordingUtils.checkMultimediaFile(new File(recPath + recording.getName() + ".webm"), true, false, recording.getDuration(), null, "opus", null, true); // Check video-only INDIVIDUAL recording recPath = recordingsPath + SESSION_NAME + "-2/"; recording = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(SESSION_NAME + "-2"); - this.checkIndividualRecording(recPath, recording, 3, "opus", "vp8", true); + this.recordingUtils.checkIndividualRecording(recPath, recording, 3, "opus", "vp8", true); // Check audio-only INDIVIDUAL recording recPath = recordingsPath + SESSION_NAME + "-3/"; recording = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(SESSION_NAME + "-3"); - this.checkIndividualRecording(recPath, recording, 2, "opus", "vp8", true); + this.recordingUtils.checkIndividualRecording(recPath, recording, 2, "opus", "vp8", true); user.getDriver().findElement(By.id("close-dialog-btn")).click(); Thread.sleep(500); @@ -1746,6 +1698,129 @@ public class OpenViduTestAppE2eTest { } } + @Test + @DisplayName("Custom layout recording") + void customLayoutRecordTest() throws Exception { + isRecordingTest = true; + + setupBrowser("chrome"); + + log.info("Custom layout recording"); + + final String SESSION_NAME = "CUSTOM_LAYOUT_SESSION"; + + user.getDriver().findElement(By.id("add-user-btn")).click(); + user.getDriver().findElement(By.id("session-name-input-0")).clear(); + user.getDriver().findElement(By.id("session-name-input-0")).sendKeys(SESSION_NAME); + + // Custom layout from local storage + user.getDriver().findElement(By.id("session-settings-btn-0")).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.id("recording-mode-select")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("option-ALWAYS")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("recording-layout-select")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("option-CUSTOM")).click(); + Thread.sleep(500); + WebElement tokeInput = user.getDriver().findElement(By.id("default-custom-layout-input")); + tokeInput.clear(); + tokeInput.sendKeys("layout1"); + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(1000); + + user.getDriver().findElement(By.className("join-btn")).sendKeys(Keys.ENTER); + + user.getEventManager().waitUntilEventReaches("connectionCreated", 1); + user.getEventManager().waitUntilEventReaches("accessAllowed", 1); + user.getEventManager().waitUntilEventReaches("streamCreated", 1); + user.getEventManager().waitUntilEventReaches("streamPlaying", 1); + user.getEventManager().waitUntilEventReaches("recordingStarted", 1); + + Thread.sleep(4000); + + user.getDriver().findElement(By.id("session-api-btn-0")).click(); + Thread.sleep(1000); + + user.getDriver().findElement(By.id("recording-id-field")).clear(); + user.getDriver().findElement(By.id("recording-id-field")).sendKeys(SESSION_NAME); + user.getDriver().findElement(By.id("stop-recording-btn")).click(); + user.getWaiter().until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", + "Recording stopped [" + SESSION_NAME + "]")); + user.getEventManager().waitUntilEventReaches("recordingStopped", 1); + user.getDriver().findElement(By.id("close-session-btn")).click(); + user.getEventManager().waitUntilEventReaches("streamDestroyed", 1); + user.getEventManager().waitUntilEventReaches("sessionDisconnected", 1); + user.getDriver().findElement(By.id("close-dialog-btn")).click(); + + String recordingsPath = "/opt/openvidu/recordings/" + SESSION_NAME + "/"; + File file1 = new File(recordingsPath + SESSION_NAME + ".mp4"); + File file2 = new File(recordingsPath + SESSION_NAME + ".jpg"); + + Assert.assertTrue("Recorded file " + file1.getAbsolutePath() + " is not fine", this.recordingUtils + .recordedRedFileFine(file1, new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(SESSION_NAME))); + Assert.assertTrue("Thumbnail " + file2.getAbsolutePath() + " is not fine", + this.recordingUtils.thumbnailIsFine(file2, RecordingUtils::checkVideoAverageRgbRed)); + + // Custom layout from external URL + CountDownLatch initLatch = new CountDownLatch(1); + CustomLayoutHandler.main(new String[0], initLatch); + try { + + if (!initLatch.await(30, TimeUnit.SECONDS)) { + Assert.fail("Timeout waiting for webhook springboot app to start"); + CustomLayoutHandler.shutDown(); + return; + } + + user.getDriver().findElement(By.id("session-settings-btn-0")).click(); + Thread.sleep(1000); + tokeInput = user.getDriver().findElement(By.id("default-custom-layout-input")); + tokeInput.clear(); + tokeInput.sendKeys(EXTERNAL_CUSTOM_LAYOUT_URL + "?" + EXTERNAL_CUSTOM_LAYOUT_PARAMS); + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(1000); + + user.getDriver().findElement(By.className("join-btn")).sendKeys(Keys.ENTER); + + user.getEventManager().waitUntilEventReaches("connectionCreated", 2); + user.getEventManager().waitUntilEventReaches("accessAllowed", 2); + user.getEventManager().waitUntilEventReaches("streamCreated", 2); + user.getEventManager().waitUntilEventReaches("streamPlaying", 2); + user.getEventManager().waitUntilEventReaches("recordingStarted", 2); + + Thread.sleep(4000); + + user.getDriver().findElement(By.id("session-api-btn-0")).click(); + Thread.sleep(1000); + + user.getDriver().findElement(By.id("recording-id-field")).clear(); + user.getDriver().findElement(By.id("recording-id-field")).sendKeys(SESSION_NAME + "-1"); + user.getDriver().findElement(By.id("stop-recording-btn")).click(); + user.getWaiter().until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", + "Recording stopped [" + SESSION_NAME + "-1]")); + user.getEventManager().waitUntilEventReaches("recordingStopped", 2); + user.getDriver().findElement(By.id("close-session-btn")).click(); + user.getEventManager().waitUntilEventReaches("streamDestroyed", 2); + user.getEventManager().waitUntilEventReaches("sessionDisconnected", 2); + user.getDriver().findElement(By.id("close-dialog-btn")).click(); + + recordingsPath = "/opt/openvidu/recordings/" + SESSION_NAME + "-1/"; + file1 = new File(recordingsPath + SESSION_NAME + "-1.mp4"); + file2 = new File(recordingsPath + SESSION_NAME + "-1.jpg"); + + Assert.assertTrue("Recorded file " + file1.getAbsolutePath() + " is not fine", + this.recordingUtils.recordedRedFileFine(file1, + new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(SESSION_NAME + "-1"))); + Assert.assertTrue("Thumbnail " + file2.getAbsolutePath() + " is not fine", + this.recordingUtils.thumbnailIsFine(file2, RecordingUtils::checkVideoAverageRgbRed)); + + } finally { + CustomLayoutHandler.shutDown(); + } + } + @Test @DisplayName("REST API: Fetch all, fetch one, force disconnect, force unpublish, close session") void restApiFetchForce() throws Exception { @@ -1816,15 +1891,15 @@ public class OpenViduTestAppE2eTest { "Number: 1. Changes: false")); // Force unpublish wrong - user.getDriver().findElement(By.id("resource-id-field")).clear(); - user.getDriver().findElement(By.id("resource-id-field")).sendKeys("FAIL"); + user.getDriver().findElement(By.id("stream-id-field")).clear(); + user.getDriver().findElement(By.id("stream-id-field")).sendKeys("FAIL"); user.getDriver().findElement(By.id("force-unpublish-api-btn")).click(); user.getWaiter() .until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", "Error [404]")); // Force unpublish right - user.getDriver().findElement(By.id("resource-id-field")).clear(); - user.getDriver().findElement(By.id("resource-id-field")).sendKeys(streamId); + user.getDriver().findElement(By.id("stream-id-field")).clear(); + user.getDriver().findElement(By.id("stream-id-field")).sendKeys(streamId); user.getDriver().findElement(By.id("force-unpublish-api-btn")).click(); user.getWaiter().until( ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", "Stream unpublished")); @@ -1834,15 +1909,15 @@ public class OpenViduTestAppE2eTest { Assert.assertEquals("Expected 3 videos but found " + numberOfVideos, 3, numberOfVideos); // Force disconnect wrong - user.getDriver().findElement(By.id("resource-id-field")).clear(); - user.getDriver().findElement(By.id("resource-id-field")).sendKeys("FAIL"); + user.getDriver().findElement(By.id("connection-id-field")).clear(); + user.getDriver().findElement(By.id("connection-id-field")).sendKeys("FAIL"); user.getDriver().findElement(By.id("force-disconnect-api-btn")).click(); user.getWaiter() .until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", "Error [404]")); // Force disconnect right - user.getDriver().findElement(By.id("resource-id-field")).clear(); - user.getDriver().findElement(By.id("resource-id-field")).sendKeys(connectionId); + user.getDriver().findElement(By.id("connection-id-field")).clear(); + user.getDriver().findElement(By.id("connection-id-field")).sendKeys(connectionId); user.getDriver().findElement(By.id("force-disconnect-api-btn")).click(); user.getWaiter() .until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", "User disconnected")); @@ -1904,7 +1979,7 @@ public class OpenViduTestAppE2eTest { // Analyze Chrome fake video stream without gray filter (GREEN color) Map rgb = user.getEventManager().getAverageRgbFromVideo(subscriberVideo); System.out.println(rgb.toString()); - Assert.assertTrue("Video is not average green", checkVideoAverageRgbGreen(rgb)); + Assert.assertTrue("Video is not average green", RecordingUtils.checkVideoAverageRgbGreen(rgb)); // Try to apply none allowed filter user.getDriver().findElement(By.cssSelector(".filter-btn")).click(); @@ -1943,7 +2018,7 @@ public class OpenViduTestAppE2eTest { Thread.sleep(500); rgb = user.getEventManager().getAverageRgbFromVideo(subscriberVideo); System.out.println(rgb.toString()); - Assert.assertTrue("Video is not average gray", checkVideoAverageRgbGray(rgb)); + Assert.assertTrue("Video is not average gray", RecordingUtils.checkVideoAverageRgbGray(rgb)); // Execute filter method WebElement filterMethodInput = user.getDriver().findElement(By.id("filter-method-field")); @@ -1961,7 +2036,7 @@ public class OpenViduTestAppE2eTest { Thread.sleep(500); rgb = user.getEventManager().getAverageRgbFromVideo(subscriberVideo); System.out.println(rgb.toString()); - Assert.assertTrue("Video is not average green", checkVideoAverageRgbGreen(rgb)); + Assert.assertTrue("Video is not average green", RecordingUtils.checkVideoAverageRgbGreen(rgb)); user.getDriver().findElement(By.id("close-dialog-btn")).click(); Thread.sleep(500); @@ -1986,7 +2061,7 @@ public class OpenViduTestAppE2eTest { subscriberVideo = user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 video")); rgb = user.getEventManager().getAverageRgbFromVideo(subscriberVideo); System.out.println(rgb.toString()); - Assert.assertTrue("Video is not average gray", checkVideoAverageRgbGray(rgb)); + Assert.assertTrue("Video is not average gray", RecordingUtils.checkVideoAverageRgbGray(rgb)); // Remove filter user.getDriver().findElement(By.cssSelector(".filter-btn")).click(); @@ -2000,7 +2075,7 @@ public class OpenViduTestAppE2eTest { // Analyze Chrome fake video stream with gray filter (GREEN color) rgb = user.getEventManager().getAverageRgbFromVideo(subscriberVideo); System.out.println(rgb.toString()); - Assert.assertTrue("Video is not average green", checkVideoAverageRgbGreen(rgb)); + Assert.assertTrue("Video is not average green", RecordingUtils.checkVideoAverageRgbGreen(rgb)); user.getDriver().findElement(By.id("close-dialog-btn")).click(); Thread.sleep(500); @@ -2146,8 +2221,6 @@ public class OpenViduTestAppE2eTest { log.info("openvidu-java-client test"); - System.out.println(getBase64Screenshot(user)); - user.getDriver().findElement(By.id("one2one-btn")).click(); final String customSessionId = "openviduJavaClientSession"; @@ -2172,15 +2245,29 @@ public class OpenViduTestAppE2eTest { KurentoOptions kurentoOptions = new KurentoOptions.Builder().videoMaxRecvBandwidth(250) .allowedFilters(new String[] { "GStreamerFilter" }).build(); - TokenOptions tokenOptionsModerator = new TokenOptions.Builder().role(OpenViduRole.MODERATOR) - .data(serverDataModerator).kurentoOptions(kurentoOptions).build(); - String tokenModerator = session.generateToken(tokenOptionsModerator); + ConnectionProperties moderatorConnectionProperties = new ConnectionProperties.Builder() + .role(OpenViduRole.MODERATOR).data(serverDataModerator).kurentoOptions(kurentoOptions).build(); + Connection connectionModerator = session.createConnection(moderatorConnectionProperties); - TokenOptions tokenOptionsSubscriber = new TokenOptions.Builder().role(OpenViduRole.SUBSCRIBER) - .data(serverDataSubscriber).build(); - String tokenSubscriber = session.generateToken(tokenOptionsSubscriber); + ConnectionProperties subscriberConnectionProperties = new ConnectionProperties.Builder() + .type(ConnectionType.WEBRTC).role(OpenViduRole.SUBSCRIBER).data(serverDataSubscriber).build(); + Connection connectionSubscriber = session.createConnection(subscriberConnectionProperties); - Assert.assertFalse("Session.fetch() should return false until a user has connected", session.fetch()); + Assert.assertFalse("Session.fetch() should return false after Session.createConnection", session.fetch()); + Assert.assertFalse("OpenVidu.fetch() should return false after Session.fetch()", OV.fetch()); + + Assert.assertEquals("Wrong number of active connections", 0, session.getActiveConnections().size()); + Assert.assertEquals("Wrong number of connections", 2, session.getConnections().size()); + Assert.assertEquals("Wrong status property", "pending", connectionModerator.getStatus()); + Assert.assertEquals("Wrong role property", OpenViduRole.MODERATOR, connectionModerator.getRole()); + Assert.assertTrue("Wrong record property", connectionModerator.record()); + Assert.assertNull("Wrong location property", connectionModerator.getLocation()); + Assert.assertNull("Wrong platform property", connectionModerator.getPlatform()); + Assert.assertTrue("Wrong createdAt property", connectionModerator.createdAt() > 0); + Assert.assertNull("Wrong activeAt property", connectionModerator.activeAt()); + Assert.assertNull("Wrong clientData property", connectionModerator.getClientData()); + Assert.assertEquals("Wrong publishers property", 0, connectionModerator.getPublishers().size()); + Assert.assertEquals("Wrong subscribers property", 0, connectionModerator.getSubscribers().size()); // Set client data 1 WebElement clientDataInput = user.getDriver().findElement(By.cssSelector("#client-data-input-0")); @@ -2192,7 +2279,7 @@ public class OpenViduTestAppE2eTest { Thread.sleep(1000); WebElement tokeInput = user.getDriver().findElement(By.cssSelector("#custom-token-div input")); tokeInput.clear(); - tokeInput.sendKeys(tokenModerator); + tokeInput.sendKeys(connectionModerator.getToken()); user.getDriver().findElement(By.id("save-btn")).click(); Thread.sleep(1000); @@ -2207,7 +2294,7 @@ public class OpenViduTestAppE2eTest { Thread.sleep(1000); tokeInput = user.getDriver().findElement(By.cssSelector("#custom-token-div input")); tokeInput.clear(); - tokeInput.sendKeys(tokenSubscriber); + tokeInput.sendKeys(connectionSubscriber.getToken()); user.getDriver().findElement(By.id("save-btn")).click(); Thread.sleep(1000); @@ -2247,17 +2334,14 @@ public class OpenViduTestAppE2eTest { Assert.assertEquals("Expected 2 active connections but found " + session.getActiveConnections().size(), 2, session.getActiveConnections().size()); - Connection connectionModerator; - Connection connectionSubscriber; - if (OpenViduRole.MODERATOR.equals(session.getActiveConnections().get(0).getRole())) { - connectionModerator = session.getActiveConnections().get(0); - connectionSubscriber = session.getActiveConnections().get(1); - } else { - connectionModerator = session.getActiveConnections().get(1); - connectionSubscriber = session.getActiveConnections().get(0); - } + // Verify status + Assert.assertEquals("Wrong status for moderator connection", "active", connectionModerator.getStatus()); + Assert.assertEquals("Wrong status for subscriber connection", "active", connectionSubscriber.getStatus()); - Assert.assertEquals(OpenViduRole.SUBSCRIBER, connectionSubscriber.getRole()); + // Verify createdAt and activeAt + Assert.assertTrue("Wrong createdAt property", connectionModerator.createdAt() > 0); + Assert.assertTrue("Wrong activeAt property", connectionModerator.activeAt() > 0); + Assert.assertTrue("Wrong activeAt property", connectionModerator.activeAt() > connectionModerator.createdAt()); // Verify platform Assert.assertTrue("Wrong platform for moderator connection", @@ -2293,7 +2377,7 @@ public class OpenViduTestAppE2eTest { // Verify publisher properties Publisher pub = connectionModerator.getPublishers().get(0); Assert.assertEquals("{\"width\":640,\"height\":480}", pub.getVideoDimensions()); - Assert.assertEquals(new Integer(30), pub.getFrameRate()); + Assert.assertEquals(Integer.valueOf(30), pub.getFrameRate()); Assert.assertEquals("CAMERA", pub.getTypeOfVideo()); Assert.assertTrue(pub.hasVideo()); Assert.assertTrue(pub.isVideoActive()); @@ -2337,9 +2421,9 @@ public class OpenViduTestAppE2eTest { String widthAndHeight = user.getEventManager().getDimensionOfViewport(); JsonObject obj = JsonParser.parseString(widthAndHeight).getAsJsonObject(); Assert.assertEquals( - "{\"width\":" + obj.get("width").getAsLong() + ",\"height\":" + (obj.get("height").getAsLong()) + "}", + "{\"width\":" + obj.get("width").getAsLong() + ",\"height\":" + obj.get("height").getAsLong() + "}", pub.getVideoDimensions()); - Assert.assertEquals(new Integer(30), pub.getFrameRate()); + Assert.assertEquals(Integer.valueOf(30), pub.getFrameRate()); Assert.assertEquals("SCREEN", pub.getTypeOfVideo()); Assert.assertTrue(pub.hasVideo()); Assert.assertTrue(pub.isVideoActive()); @@ -2350,6 +2434,7 @@ public class OpenViduTestAppE2eTest { RecordingProperties recordingProperties; try { OV.startRecording("NOT_EXISTS"); + Assert.fail("Expected OpenViduHttpException"); } catch (OpenViduHttpException e) { Assert.assertEquals("Wrong HTTP status on OpenVidu.startRecording()", 404, e.getStatus()); } @@ -2357,6 +2442,7 @@ public class OpenViduTestAppE2eTest { Assert.assertFalse("OpenVidu.fetch() should return false", OV.fetch()); try { OV.startRecording(sessionAux.getSessionId()); + Assert.fail("Expected OpenViduHttpException"); } catch (OpenViduHttpException e) { Assert.assertEquals("Wrong HTTP status on OpenVidu.startRecording()", 406, e.getStatus()); } finally { @@ -2364,25 +2450,49 @@ public class OpenViduTestAppE2eTest { sessionAux.close(); try { sessionAux.fetch(); + Assert.fail("Expected OpenViduHttpException"); } catch (OpenViduHttpException e2) { Assert.assertEquals("Wrong HTTP status on Session.fetch()", 404, e2.getStatus()); } Assert.assertFalse("OpenVidu.fetch() should return false", OV.fetch()); } + // Not recorded session + Session notRecordedSession = OV.createSession(); + notRecordedSession.createConnection(new ConnectionProperties.Builder().type(ConnectionType.IPCAM) + .rtspUri("rtsp://does-not-matter.com").build()); try { recordingProperties = new RecordingProperties.Builder().hasAudio(false).hasVideo(false).build(); - OV.startRecording(session.getSessionId(), recordingProperties); + OV.startRecording(notRecordedSession.getSessionId(), recordingProperties); + Assert.fail("Expected OpenViduHttpException"); } catch (OpenViduHttpException e) { Assert.assertEquals("Wrong HTTP status on OpenVidu.startRecording()", 422, e.getStatus()); } try { recordingProperties = new RecordingProperties.Builder().resolution("99x1080").build(); - OV.startRecording(session.getSessionId(), recordingProperties); + OV.startRecording(notRecordedSession.getSessionId(), recordingProperties); + Assert.fail("Expected OpenViduHttpException"); } catch (OpenViduHttpException e) { Assert.assertEquals("Wrong HTTP status on OpenVidu.startRecording()", 422, e.getStatus()); } + notRecordedSession.close(); + // Recorded session + try { + recordingProperties = new RecordingProperties.Builder().hasAudio(false).hasVideo(false).build(); + OV.startRecording(session.getSessionId(), recordingProperties); + Assert.fail("Expected OpenViduHttpException"); + } catch (OpenViduHttpException e) { + Assert.assertEquals("Wrong HTTP status on OpenVidu.startRecording()", 409, e.getStatus()); + } + try { + recordingProperties = new RecordingProperties.Builder().resolution("99x1080").build(); + OV.startRecording(session.getSessionId(), recordingProperties); + Assert.fail("Expected OpenViduHttpException"); + } catch (OpenViduHttpException e) { + Assert.assertEquals("Wrong HTTP status on OpenVidu.startRecording()", 409, e.getStatus()); + } try { OV.startRecording(session.getSessionId()); + Assert.fail("Expected OpenViduHttpException"); } catch (OpenViduHttpException e) { Assert.assertEquals("Wrong HTTP status on OpenVidu.startRecording()", 409, e.getStatus()); } @@ -2427,8 +2537,8 @@ public class OpenViduTestAppE2eTest { Assert.assertFalse("Session shouldn't be being recorded", session.isBeingRecorded()); Assert.assertFalse("OpenVidu.fetch() should return false", OV.fetch()); - this.checkIndividualRecording("/opt/openvidu/recordings/" + customSessionId + "/", recording, 2, "opus", "vp8", - false); + this.recordingUtils.checkIndividualRecording("/opt/openvidu/recordings/" + customSessionId + "/", recording, 2, + "opus", "vp8", false); user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .change-publisher-btn")).click(); user.getEventManager().waitUntilEventReaches("streamDestroyed", 4); @@ -2443,9 +2553,10 @@ public class OpenViduTestAppE2eTest { .recordingLayout(RecordingLayout.BEST_FIT).resolution("1280x720").hasVideo(true).hasAudio(false) .name(customRecordingName).build(); + // Start recording method should block until video exists and size > 0 Recording recording2 = OV.startRecording(session.getSessionId(), recordingProperties); recording2 = OV.stopRecording(recording2.getId()); - Assert.assertEquals("Wrong recording status", Recording.Status.failed, recording2.getStatus()); + Assert.assertEquals("Wrong recording status", Recording.Status.ready, recording2.getStatus()); OV.deleteRecording(recording2.getId()); recording2 = OV.startRecording(session.getSessionId(), recordingProperties); @@ -2491,8 +2602,9 @@ public class OpenViduTestAppE2eTest { file3.exists() && file3.length() > 0); Assert.assertTrue("Recorded file " + file1.getAbsolutePath() + " is not fine", - this.recordedFileFine(file1, recording2)); - Assert.assertTrue("Thumbnail " + file3.getAbsolutePath() + " is not fine", this.thumbnailIsFine(file3)); + this.recordingUtils.recordedGreenFileFine(file1, recording2)); + Assert.assertTrue("Thumbnail " + file3.getAbsolutePath() + " is not fine", + this.recordingUtils.thumbnailIsFine(file3, RecordingUtils::checkVideoAverageRgbGreen)); try { OV.deleteRecording("NOT_EXISTS"); @@ -2507,12 +2619,12 @@ public class OpenViduTestAppE2eTest { try { session.forceUnpublish("NOT_EXISTS"); } catch (OpenViduHttpException e) { - Assert.assertEquals("Wrong HTTP status on Session.fetch()", 404, e.getStatus()); + Assert.assertEquals("Wrong HTTP status on Session.forceUnpublish()", 404, e.getStatus()); } try { session.forceDisconnect("NOT_EXISTS"); } catch (OpenViduHttpException e) { - Assert.assertEquals("Wrong HTTP status on Session.fetch()", 404, e.getStatus()); + Assert.assertEquals("Wrong HTTP status on Session.forceDisconnect()", 404, e.getStatus()); } if (OpenViduRole.MODERATOR.equals(session.getActiveConnections().get(0).getRole())) { @@ -2524,19 +2636,26 @@ public class OpenViduTestAppE2eTest { } pub = connectionModerator.getPublishers().get(0); + // TODO: test delete unused Connection + session.forceUnpublish(pub); user.getEventManager().waitUntilEventReaches("streamDestroyed", 6); - Assert.assertFalse("OpenVidu.fetch() should return false", OV.fetch()); + Assert.assertFalse( + "OpenVidu.fetch() should return false because Session.forceUnpublish() already updates local objects", + OV.fetch()); session.getActiveConnections().forEach(con -> { Assert.assertEquals("Wrong number of Publishers", 0, con.getPublishers().size()); Assert.assertEquals("Wrong number of Subscribers", 0, con.getSubscribers().size()); }); + // Delete active Connection session.forceDisconnect(connectionModerator); user.getEventManager().waitUntilEventReaches("sessionDisconnected", 1); user.getEventManager().waitUntilEventReaches("connectionDestroyed", 1); - Assert.assertFalse("OpenVidu.fetch() should return false", OV.fetch()); + Assert.assertFalse( + "OpenVidu.fetch() should return false because Session.forceDisconnect() already updates local objects", + OV.fetch()); session.close(); @@ -2544,7 +2663,71 @@ public class OpenViduTestAppE2eTest { Assert.assertFalse("Session.fetch() should return true", OV.fetch()); + // Delete pending Connection + session = OV.createSession(); + Connection con = session.createConnection(); + Assert.assertEquals("Wrong number of Connections", 1, session.getConnections().size()); + Assert.assertEquals("Wrong number of active Connections", 0, session.getActiveConnections().size()); + Assert.assertFalse(session.fetch()); + session.forceDisconnect(con); + Assert.assertEquals("Wrong number of Connections", 0, session.getConnections().size()); + Assert.assertEquals("Wrong number of active Connections", 0, session.getActiveConnections().size()); + Assert.assertFalse(session.fetch()); + + // Test IPCAM + final String rtsp = "rtsp://dummyurl.com"; + Connection ipcamera = session.createConnection(new ConnectionProperties.Builder().type(ConnectionType.IPCAM) + .rtspUri(rtsp).adaptativeBitrate(false).onlyPlayWithSubscribers(false).networkCache(50).build()); + Assert.assertFalse("OpenVidu.fetch() should return false", OV.fetch()); + Assert.assertFalse("Session.fetch() should return false", session.fetch()); + Assert.assertEquals("Wrong number of active connections", 1, session.getActiveConnections().size()); + Assert.assertEquals("Wrong number of connections", 1, session.getConnections().size()); + ipcamera = session.getConnection(ipcamera.getConnectionId()); + Assert.assertEquals("Wrong type property of Connection object", "IPCAM", ipcamera.getType().name()); + Assert.assertNull("Property role of an IPCAM connection should be null", ipcamera.getRole()); + Assert.assertEquals("Wrong property rtspUri", rtsp, ipcamera.getRtspUri()); + Assert.assertFalse("Wrong property adaptativeBitrate", ipcamera.adaptativeBitrate()); + Assert.assertFalse("Wrong property onlyPlayWithSubscribers", ipcamera.onlyPlayWithSubscribers()); + Assert.assertEquals("Wrong property networkCache", Integer.valueOf(50), ipcamera.getNetworkCache()); + gracefullyLeaveParticipants(2); + session.close(); + + /** Test transcoding defined properties */ + SessionProperties.Builder basePropertiesBuilder = new SessionProperties.Builder().mediaMode(MediaMode.ROUTED) + .recordingMode(RecordingMode.ALWAYS).defaultOutputMode(OutputMode.INDIVIDUAL); + + SessionProperties propertiesDefaultCodec = basePropertiesBuilder.build(); + SessionProperties propertiesH264AllowTranscoding = basePropertiesBuilder.forcedVideoCodec(VideoCodec.H264) + .allowTranscoding(true).build(); + SessionProperties propertiesVP9AllowTranscoding = basePropertiesBuilder.forcedVideoCodec(VideoCodec.VP9) + .allowTranscoding(true).build(); + + Session sessionDefaultCodec = OV.createSession(propertiesDefaultCodec); + Session sessionH264AllowTranscoding = OV.createSession(propertiesH264AllowTranscoding); + Session sessionVP9AllowTranscoding = OV.createSession(propertiesVP9AllowTranscoding); + assertTranscodingSessionProperties(sessionDefaultCodec, sessionH264AllowTranscoding, + sessionVP9AllowTranscoding); + + // Fetch sessions + Assert.assertFalse(sessionDefaultCodec.fetch()); + Assert.assertFalse(sessionH264AllowTranscoding.fetch()); + Assert.assertFalse(sessionVP9AllowTranscoding.fetch()); + + // Check transcoding session properties + assertTranscodingSessionProperties(sessionDefaultCodec, sessionH264AllowTranscoding, + sessionVP9AllowTranscoding); + + // Fetch all sessions + Assert.assertFalse(OV.fetch()); + + // Check transcoding session properties + assertTranscodingSessionProperties(sessionDefaultCodec, sessionH264AllowTranscoding, + sessionVP9AllowTranscoding); + + sessionDefaultCodec.close(); + sessionH264AllowTranscoding.close(); + sessionVP9AllowTranscoding.close(); } @Test @@ -2560,127 +2743,177 @@ public class OpenViduTestAppE2eTest { String wrongCredentials = "Basic " + Base64.getEncoder().encodeToString(("OPENVIDUAPP:WRONG_SECRET").getBytes()); Assert.assertEquals("Expected unauthorized status", HttpStatus.SC_UNAUTHORIZED, - restClient.getAndReturnStatus("/config", wrongCredentials)); + restClient.getAndReturnStatus("/openvidu/api/config", wrongCredentials)); - /** GET /api/sessions (before session created) **/ - restClient.rest(HttpMethod.GET, "/api/sessions/NOT_EXISTS", HttpStatus.SC_NOT_FOUND); - restClient.rest(HttpMethod.GET, "/api/sessions", null, HttpStatus.SC_OK, true, - ImmutableMap.of("numberOfElements", new Integer(0), "content", new JsonArray())); + /** GET /openvidu/api/sessions (before session created) **/ + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/NOT_EXISTS", HttpStatus.SC_NOT_FOUND); + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions", null, HttpStatus.SC_OK, true, true, true, + "{'numberOfElements': 0, 'content': []}"); - /** POST /api/sessions **/ + /** POST /openvidu/api/sessions **/ // 400 String body = "{'mediaMode': 'NOT_EXISTS'}"; - restClient.rest(HttpMethod.POST, "/api/sessions", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", body, HttpStatus.SC_BAD_REQUEST); body = "{'mediaMode': 'ROUTED', 'recordingMode': false}"; - restClient.rest(HttpMethod.POST, "/api/sessions", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", body, HttpStatus.SC_BAD_REQUEST); body = "{'mediaMode': 'ROUTED', 'recordingMode': 'ALWAYS', 'customSessionId': 999}"; - restClient.rest(HttpMethod.POST, "/api/sessions", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", body, HttpStatus.SC_BAD_REQUEST); body = "{'mediaMode': 'ROUTED', 'recordingMode': 'NOT_EXISTS'}"; - restClient.rest(HttpMethod.POST, "/api/sessions", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", body, HttpStatus.SC_BAD_REQUEST); body = "{'mediaMode': 'ROUTED', 'recordingMode': 'ALWAYS', 'defaultOutputMode': 'NOT_EXISTS'}"; - restClient.rest(HttpMethod.POST, "/api/sessions", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", body, HttpStatus.SC_BAD_REQUEST); body = "{'mediaMode': 'ROUTED', 'recordingMode': 'ALWAYS', 'defaultOutputMode': 'INDIVIDUAL', 'defaultRecordingLayout': 'NOT_EXISTS'}"; - restClient.rest(HttpMethod.POST, "/api/sessions", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", body, HttpStatus.SC_BAD_REQUEST); // 200 body = "{'mediaMode': 'ROUTED', 'recordingMode': 'MANUAL', 'customSessionId': 'CUSTOM_SESSION_ID', 'defaultOutputMode': 'COMPOSED', 'defaultRecordingLayout': 'BEST_FIT'}"; - restClient.rest(HttpMethod.POST, "/api/sessions", body, HttpStatus.SC_OK, true, - "{'id':'STR','createdAt':0,'mediaMode':'STR','recordingMode':'STR','defaultOutputMode':'STR','defaultRecordingLayout':'STR','customSessionId':'STR','forcedVideoCodec':'STR','allowTranscoding':false,'connections':{'numberOfElements':0,'content':[]},'recording':true}"); - + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", body, HttpStatus.SC_OK, true, false, true, + DEFAULT_JSON_SESSION); // Default values - JsonObject res = restClient.rest(HttpMethod.POST, "/api/sessions", "{}", HttpStatus.SC_OK, true, - "{'id':'STR','createdAt':0,'mediaMode':'STR','recordingMode':'STR','defaultOutputMode':'STR','defaultRecordingLayout':'STR','customSessionId':'STR','forcedVideoCodec':'STR','allowTranscoding':false,'connections':{'numberOfElements':0,'content':[]},'recording':true}"); - restClient.rest(HttpMethod.DELETE, "/api/sessions/" + res.get("id").getAsString(), HttpStatus.SC_NO_CONTENT); + JsonObject res = restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", "{}", HttpStatus.SC_OK, true, false, + true, DEFAULT_JSON_SESSION); + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/" + res.get("id").getAsString(), + HttpStatus.SC_NO_CONTENT); // 409 body = "{'customSessionId': 'CUSTOM_SESSION_ID'}"; - restClient.rest(HttpMethod.POST, "/api/sessions", body, HttpStatus.SC_CONFLICT); + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", body, HttpStatus.SC_CONFLICT); - /** GET /api/sessions (after session created) **/ - restClient.rest(HttpMethod.GET, "/api/sessions/CUSTOM_SESSION_ID", null, HttpStatus.SC_OK, true, - "{'sessionId':'STR','createdAt':0,'mediaMode':'STR','recordingMode':'STR','defaultOutputMode':'STR','defaultRecordingLayout':'STR','customSessionId':'STR','forcedVideoCodec':'STR','allowTranscoding':false,'connections':{'numberOfElements':0,'content':[]},'recording':true}"); - restClient.rest(HttpMethod.GET, "/api/sessions", null, HttpStatus.SC_OK, true, - ImmutableMap.of("numberOfElements", new Integer(1), "content", new JsonArray())); + /** GET /openvidu/api/sessions (after session created) **/ + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/CUSTOM_SESSION_ID", null, HttpStatus.SC_OK, true, false, + true, DEFAULT_JSON_SESSION); + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions", null, HttpStatus.SC_OK, true, true, false, + "{'numberOfElements': 1, 'content': []}"); - /** POST /api/tokens **/ + /** GET /openvidu/api/sessions/ID/connection (with no connections) **/ + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/NOT_EXISTS/connection", HttpStatus.SC_NOT_FOUND); + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", null, HttpStatus.SC_OK, + true, true, true, "{'numberOfElements':0,'content':[]}"); + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/NOT_EXISTS/connection/NOT_EXISTS", + HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/NOT_EXISTS", + HttpStatus.SC_NOT_FOUND); + + /** POST /openvidu/api/tokens **/ // 400 body = "{}"; - restClient.rest(HttpMethod.POST, "/api/tokens", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/tokens", body, HttpStatus.SC_BAD_REQUEST); body = "{'session': true}"; - restClient.rest(HttpMethod.POST, "/api/tokens", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/tokens", body, HttpStatus.SC_BAD_REQUEST); body = "{'session': 'CUSTOM_SESSION_ID', 'role': 'NOT_EXISTS'}"; - restClient.rest(HttpMethod.POST, "/api/tokens", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/tokens", body, HttpStatus.SC_BAD_REQUEST); body = "{'session': 'CUSTOM_SESSION_ID', 'role': 'MODERATOR', 'data': 999}"; - restClient.rest(HttpMethod.POST, "/api/tokens", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/tokens", body, HttpStatus.SC_BAD_REQUEST); body = "{'session': 'CUSTOM_SESSION_ID', 'role': 'MODERATOR', 'data': 'SERVER_DATA', 'kurentoOptions': false}"; - restClient.rest(HttpMethod.POST, "/api/tokens", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/tokens", body, HttpStatus.SC_BAD_REQUEST); body = "{'session': 'CUSTOM_SESSION_ID', 'role': 'MODERATOR', 'data': 'SERVER_DATA', 'kurentoOptions': {'allowedFilters': 'NOT_EXISTS'}}"; - restClient.rest(HttpMethod.POST, "/api/tokens", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/tokens", body, HttpStatus.SC_BAD_REQUEST); // 200 - body = "{'session': 'CUSTOM_SESSION_ID', 'role': 'MODERATOR', 'data': 'SERVER_DATA', 'kurentoOptions': {'allowedFilters': ['GStreamerFilter']}}"; - res = restClient.rest(HttpMethod.POST, "/api/tokens", body, HttpStatus.SC_OK, true, - "{'id':'STR','session':'STR','role':'STR','data':'STR','token':'STR','kurentoOptions':{'allowedFilters':['STR']}}"); + body = "{'session': 'CUSTOM_SESSION_ID', 'role': 'MODERATOR', 'data': 'SERVER_DATA', 'kurentoOptions': {'videoMaxSendBandwidth':777,'allowedFilters': ['GStreamerFilter']}}"; + res = restClient.rest(HttpMethod.POST, "/openvidu/api/tokens", body, HttpStatus.SC_OK, true, false, true, + mergeJson(DEFAULT_JSON_TOKEN, + "{'kurentoOptions':{'videoMaxSendBandwidth':777,'allowedFilters':['STR']}}", new String[0])); final String token1 = res.get("token").getAsString(); - Assert.assertEquals("JSON return value from /api/tokens should have equal srtings in 'id' and 'token'", + Assert.assertEquals("JSON return value from /openvidu/api/tokens should have equal srtings in 'id' and 'token'", res.get("id").getAsString(), token1); Assert.assertEquals("Wrong session parameter", "CUSTOM_SESSION_ID", res.get("session").getAsString()); + res = restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", null, + HttpStatus.SC_OK, true, true, false, "{'numberOfElements':1,'content':[]}"); + JsonObject connection1 = res.getAsJsonObject().get("content").getAsJsonArray().get(0).getAsJsonObject(); + final String connectionId1 = connection1.get("id").getAsString(); + final long createdAt1 = connection1.get("createdAt").getAsLong(); + /** POST /openvidu/api/sessions/CUSTOM_SESSION_ID/connection **/ + // 400 + body = "{'type':false}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", body, + HttpStatus.SC_BAD_REQUEST); + body = "{'type':'NOT_EXISTS'}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", body, + HttpStatus.SC_BAD_REQUEST); + body = "{'type':'WEBRTC','role':123}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", body, + HttpStatus.SC_BAD_REQUEST); + body = "{'type':'WEBRTC','role':123}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", body, + HttpStatus.SC_BAD_REQUEST); + body = "{'type':'WEBRTC','role':'MODERATOR','data':true}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", body, + HttpStatus.SC_BAD_REQUEST); + // 200 + String kurentoOpts = "'kurentoOptions':{'videoMaxSendBandwidth':777,'allowedFilters':['GStreamerFilter']}"; + body = "{'type':'WEBRTC','role':'MODERATOR','data':'SERVER_DATA'," + kurentoOpts + "}"; + res = restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", body, + HttpStatus.SC_OK, true, false, true, + mergeJson(DEFAULT_JSON_PENDING_CONNECTION, "{" + kurentoOpts + "}", new String[0])); + restClient.rest(HttpMethod.DELETE, + "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + res.get("id").getAsString(), + HttpStatus.SC_NO_CONTENT); // Default values - body = "{'session': 'CUSTOM_SESSION_ID'}"; - res = restClient.rest(HttpMethod.POST, "/api/tokens", body, HttpStatus.SC_OK, true, - "{'id':'STR','session':'STR','role':'STR','data':'STR','token':'STR'}"); - final String token2 = res.get("id").getAsString(); + res = restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", "{}", + HttpStatus.SC_OK); + final String token2 = res.get("token").getAsString(); + final String connectionId2 = res.get("id").getAsString(); - /** POST /api/signal (NOT ACTIVE SESSION) **/ + /** GET /openvidu/api/sessions (with pending connections) **/ + res = restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/CUSTOM_SESSION_ID", null, HttpStatus.SC_OK, true, + false, true, DEFAULT_JSON_SESSION); + Assert.assertEquals("GET session should not bring pending connections", 0, + res.get("connections").getAsJsonObject().get("numberOfElements").getAsInt()); + res = restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/CUSTOM_SESSION_ID?pendingConnections=true", null, + HttpStatus.SC_OK, true, false, true, DEFAULT_JSON_SESSION); + Assert.assertEquals("GET session should bring pending connections if query params pendingConnections=true", 2, + res.get("connections").getAsJsonObject().get("numberOfElements").getAsInt()); + + /** GET /openvidu/api/sessions/ID/connection (with pending connections) **/ + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", null, HttpStatus.SC_OK, + true, true, false, "{'numberOfElements':2,'content':[]}"); + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId1, null, + HttpStatus.SC_OK, true, true, true, + mergeJson(DEFAULT_JSON_PENDING_CONNECTION, + "{'id':'" + connectionId1 + "','connectionId':'" + connectionId1 + + "','sessionId':'CUSTOM_SESSION_ID','token':'" + token1 + + "','serverData':'SERVER_DATA','role':'MODERATOR'," + kurentoOpts + ",'createdAt':" + + createdAt1 + "}", + new String[0])); + + /** POST /openvidu/api/signal (NOT ACTIVE SESSION) **/ body = "{}"; - restClient.rest(HttpMethod.POST, "/api/signal", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/signal", body, HttpStatus.SC_BAD_REQUEST); body = "{'session': true}"; - restClient.rest(HttpMethod.POST, "/api/signal", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/signal", body, HttpStatus.SC_BAD_REQUEST); body = "{'session':'CUSTOM_SESSION_ID','to':12}"; - restClient.rest(HttpMethod.POST, "/api/signal", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/signal", body, HttpStatus.SC_BAD_REQUEST); body = "{'session':'CUSTOM_SESSION_ID','to':[],'data':false}"; - restClient.rest(HttpMethod.POST, "/api/signal", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/signal", body, HttpStatus.SC_BAD_REQUEST); body = "{'session':'CUSTOM_SESSION_ID','to':[],'data': 'SERVERMESSAGE', 'type': true}"; - restClient.rest(HttpMethod.POST, "/api/signal", body, HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.POST, "/openvidu/api/signal", body, HttpStatus.SC_BAD_REQUEST); body = "{'session':'CUSTOM_SESSION_ID'}"; - restClient.rest(HttpMethod.POST, "/api/signal", body, HttpStatus.SC_NOT_ACCEPTABLE); // No connections + restClient.rest(HttpMethod.POST, "/openvidu/api/signal", body, HttpStatus.SC_NOT_ACCEPTABLE); // No connections - /** POST /api/recordings/start (NOT ACTIVE SESSION) **/ + /** POST /openvidu/api/recordings/start (NOT ACTIVE SESSION) **/ // 400 body = "{}"; - restClient.rest(HttpMethod.POST, "/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST); - body = "{'session': true}"; - restClient.rest(HttpMethod.POST, "/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST); - body = "{'session':'SESSION_ID','name':999}"; - restClient.rest(HttpMethod.POST, "/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST); - body = "{'session':'SESSION_ID','name':'NAME','outputMode':'NOT_EXISTS'}"; - restClient.rest(HttpMethod.POST, "/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST); - body = "{'session':'SESSION_ID','name':'NAME','outputMode':'COMPOSED','recordingLayout':'NOT_EXISTS'}"; - restClient.rest(HttpMethod.POST, "/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST); - body = "{'session':'SESSION_ID','name':'NAME','outputMode':'COMPOSED','recordingLayout':'BEST_FIT','customLayout':'CUSTOM_LAYOUT','hasAudio':true,'hasVideo':true,'resolution':999}"; - restClient.rest(HttpMethod.POST, "/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST); - - // 422 - body = "{'session':'SESSION_ID','name':'NAME','outputMode':'COMPOSED','recordingLayout':'BEST_FIT','customLayout':'CUSTOM_LAYOUT','hasAudio':false,'hasVideo':false}"; - restClient.rest(HttpMethod.POST, "/api/recordings/start", body, HttpStatus.SC_UNPROCESSABLE_ENTITY); - body = "{'session':'SESSION_ID','name':'NAME','outputMode':'COMPOSED','recordingLayout':'BEST_FIT','customLayout':'CUSTOM_LAYOUT','hasAudio':true,'hasVideo':true,'resolution':'1920x2000'}"; - restClient.rest(HttpMethod.POST, "/api/recordings/start", body, HttpStatus.SC_UNPROCESSABLE_ENTITY); + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST); // 404 body = "{'session':'SESSION_ID'}"; - restClient.rest(HttpMethod.POST, "/api/recordings/start", body, HttpStatus.SC_NOT_FOUND); + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_NOT_FOUND); + body = "{'session':'SESSION_ID','name':999}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_NOT_FOUND); // 406 body = "{'session':'CUSTOM_SESSION_ID'}"; - restClient.rest(HttpMethod.POST, "/api/recordings/start", body, HttpStatus.SC_NOT_ACCEPTABLE); + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_NOT_ACCEPTABLE); // 409 (RELAYED media mode) - res = restClient.rest(HttpMethod.POST, "/api/sessions", "{'mediaMode':'RELAYED'}", HttpStatus.SC_OK, true, - "{'id':'STR','createdAt':0,'mediaMode':'STR','recordingMode':'STR','defaultOutputMode':'STR','defaultRecordingLayout':'STR','customSessionId':'STR','forcedVideoCodec':'STR','allowTranscoding':false,'connections':{'numberOfElements':0,'content':[]},'recording':true}"); + res = restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", "{'mediaMode':'RELAYED'}", HttpStatus.SC_OK, + true, false, false, DEFAULT_JSON_SESSION); body = "{'session':'" + res.get("id").getAsString() + "'}"; - restClient.rest(HttpMethod.POST, "/api/recordings/start", body, HttpStatus.SC_CONFLICT); - restClient.rest(HttpMethod.DELETE, "/api/sessions/" + res.get("id").getAsString(), HttpStatus.SC_NO_CONTENT); + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_CONFLICT); + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/" + res.get("id").getAsString(), + HttpStatus.SC_NO_CONTENT); // Start session setupBrowser("chrome"); @@ -2689,9 +2922,9 @@ public class OpenViduTestAppE2eTest { Thread.sleep(1000); // Set token 1 - WebElement tokeInput = user.getDriver().findElement(By.cssSelector("#custom-token-div input")); - tokeInput.clear(); - tokeInput.sendKeys(token1); + WebElement tokenInput = user.getDriver().findElement(By.cssSelector("#custom-token-div input")); + tokenInput.clear(); + tokenInput.sendKeys(token1); user.getDriver().findElement(By.id("save-btn")).click(); Thread.sleep(1000); @@ -2699,9 +2932,9 @@ public class OpenViduTestAppE2eTest { Thread.sleep(1000); // Set token 2 - tokeInput = user.getDriver().findElement(By.cssSelector("#custom-token-div input")); - tokeInput.clear(); - tokeInput.sendKeys(token2); + tokenInput = user.getDriver().findElement(By.cssSelector("#custom-token-div input")); + tokenInput.clear(); + tokenInput.sendKeys(token2); user.getDriver().findElement(By.id("save-btn")).click(); Thread.sleep(1000); @@ -2717,116 +2950,268 @@ public class OpenViduTestAppE2eTest { Assert.assertTrue("Videos were expected to have audio and video tracks", user.getEventManager() .assertMediaTracks(user.getDriver().findElements(By.tagName("video")), true, true)); - /** GET /api/recordings (before recording started) **/ - restClient.rest(HttpMethod.GET, "/api/recordings/NOT_EXISTS", HttpStatus.SC_NOT_FOUND); - restClient.rest(HttpMethod.GET, "/api/recordings", null, HttpStatus.SC_OK, true, - ImmutableMap.of("count", new Integer(0), "items", new JsonArray())); + /** GET /openvidu/api/sessions/ID/connection (with active connections) **/ + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", null, HttpStatus.SC_OK, + true, true, false, "{'numberOfElements':2,'content':[]}"); + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId1, null, + HttpStatus.SC_OK, false, true, false, + "{'id':'" + connectionId1 + "','connectionId':'" + connectionId1 + + "','object':'connection','type':'WEBRTC','status':'active','sessionId':'CUSTOM_SESSION_ID','token':'" + + token1 + "','role':'MODERATOR','serverData':'SERVER_DATA','record':true}"); + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId2, null, + HttpStatus.SC_OK, false, true, false, + "{'id':'" + connectionId2 + "','connectionId':'" + connectionId2 + + "','object':'connection','type':'WEBRTC','status':'active','sessionId':'CUSTOM_SESSION_ID','token':'" + + token2 + "','role':'PUBLISHER','serverData':'','record':true}"); + + /** GET /openvidu/api/recordings (before recording started) **/ + restClient.rest(HttpMethod.GET, "/openvidu/api/recordings/NOT_EXISTS", HttpStatus.SC_NOT_FOUND); + restClient.rest(HttpMethod.GET, "/openvidu/api/recordings", null, HttpStatus.SC_OK, true, true, true, + "{'count':0,'items':[]}"); + + /** POST /openvidu/api/recordings/start (ACTIVE SESSION) **/ + // 400 + body = "{'session': true}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST); + body = "{'session':'CUSTOM_SESSION_ID','name':999}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST); + body = "{'session':'CUSTOM_SESSION_ID','name':'NAME','outputMode':'NOT_EXISTS'}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST); + body = "{'session':'CUSTOM_SESSION_ID','name':'NAME','outputMode':'COMPOSED','recordingLayout':'NOT_EXISTS'}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST); + body = "{'session':'CUSTOM_SESSION_ID','name':'NAME','outputMode':'COMPOSED','recordingLayout':'BEST_FIT','customLayout':'CUSTOM_LAYOUT','hasAudio':true,'hasVideo':true,'resolution':999}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST); + + // 422 + body = "{'session':'CUSTOM_SESSION_ID','name':'NAME','outputMode':'COMPOSED','recordingLayout':'BEST_FIT','customLayout':'CUSTOM_LAYOUT','hasAudio':false,'hasVideo':false}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_UNPROCESSABLE_ENTITY); + body = "{'session':'CUSTOM_SESSION_ID','name':'NAME','outputMode':'COMPOSED','recordingLayout':'BEST_FIT','customLayout':'CUSTOM_LAYOUT','hasAudio':true,'hasVideo':true,'resolution':'1920x2000'}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_UNPROCESSABLE_ENTITY); - /** POST /api/recordings/start (ACTIVE SESSION) **/ // 200 body = "{'session':'CUSTOM_SESSION_ID'}"; - restClient.rest(HttpMethod.POST, "/api/recordings/start", body, HttpStatus.SC_OK, true, - "{'id':'STR','sessionId':'STR','name':'STR','outputMode':'STR','recordingLayout':'STR','hasAudio':false,'hasVideo':false,'resolution':'STR','createdAt':0,'size':0,'duration':0,'url':null,'status':'STR'}"); + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_OK, true, false, true, + "{'id':'STR','object':'STR','sessionId':'STR','name':'STR','outputMode':'STR','recordingLayout':'STR','hasAudio':false,'hasVideo':false,'resolution':'STR','createdAt':0,'size':0,'duration':0,'url':null,'status':'STR'}"); user.getEventManager().waitUntilEventReaches("recordingStarted", 2); Thread.sleep(2000); // 409 (already recording) - restClient.rest(HttpMethod.POST, "/api/recordings/start", body, HttpStatus.SC_CONFLICT); + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_CONFLICT); - /** POST /api/recordings/stop **/ + /** POST /openvidu/api/recordings/stop **/ // 405 - restClient.rest(HttpMethod.POST, "/api/recordings/stop", body, HttpStatus.SC_METHOD_NOT_ALLOWED); + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/stop", body, HttpStatus.SC_METHOD_NOT_ALLOWED); // 404 - restClient.rest(HttpMethod.POST, "/api/recordings/stop/NOT_EXISTS", body, HttpStatus.SC_NOT_FOUND); + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/stop/NOT_EXISTS", body, HttpStatus.SC_NOT_FOUND); // 200 - restClient.rest(HttpMethod.DELETE, "/api/recordings/CUSTOM_SESSION_ID", HttpStatus.SC_CONFLICT); - restClient.rest(HttpMethod.POST, "/api/recordings/stop/CUSTOM_SESSION_ID", body, HttpStatus.SC_OK, true, - "{'id':'STR','sessionId':'STR','name':'STR','outputMode':'STR','recordingLayout':'STR','hasAudio':false,'hasVideo':false,'resolution':'STR','createdAt':0,'size':0,'duration':0,'url':'STR','status':'STR'}"); - /** GET /api/recordings (after recording created) **/ - restClient.rest(HttpMethod.GET, "/api/recordings/CUSTOM_SESSION_ID", null, HttpStatus.SC_OK, true, - "{'id':'STR','sessionId':'STR','name':'STR','outputMode':'STR','recordingLayout':'STR','hasAudio':false,'hasVideo':false,'resolution':'STR','createdAt':0,'size':0,'duration':0,'url':'STR','status':'STR'}"); - restClient.rest(HttpMethod.GET, "/api/recordings", null, HttpStatus.SC_OK, true, - ImmutableMap.of("count", new Integer(1), "items", new JsonArray())); + restClient.rest(HttpMethod.DELETE, "/openvidu/api/recordings/CUSTOM_SESSION_ID", HttpStatus.SC_CONFLICT); + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/stop/CUSTOM_SESSION_ID", body, HttpStatus.SC_OK, + true, false, true, + "{'id':'STR','object':'STR','sessionId':'STR','name':'STR','outputMode':'STR','recordingLayout':'STR','hasAudio':false,'hasVideo':false,'resolution':'STR','createdAt':0,'size':0,'duration':0,'url':'STR','status':'STR'}"); + /** GET /openvidu/api/recordings (after recording created) **/ + restClient.rest(HttpMethod.GET, "/openvidu/api/recordings/CUSTOM_SESSION_ID", null, HttpStatus.SC_OK, true, + false, true, + "{'id':'STR','object':'STR','sessionId':'STR','name':'STR','outputMode':'STR','recordingLayout':'STR','hasAudio':false,'hasVideo':false,'resolution':'STR','createdAt':0,'size':0,'duration':0,'url':'STR','status':'STR'}"); + restClient.rest(HttpMethod.GET, "/openvidu/api/recordings", null, HttpStatus.SC_OK, true, true, false, + "{'count':1,'items':[]}"); user.getEventManager().waitUntilEventReaches("recordingStopped", 2); - /** DELETE /api/recordings **/ - restClient.rest(HttpMethod.DELETE, "/api/recordings", HttpStatus.SC_METHOD_NOT_ALLOWED); - restClient.rest(HttpMethod.DELETE, "/api/recordings/NOT_EXISTS", HttpStatus.SC_NOT_FOUND); - restClient.rest(HttpMethod.DELETE, "/api/recordings/CUSTOM_SESSION_ID", HttpStatus.SC_NO_CONTENT); + /** DELETE /openvidu/api/recordings **/ + restClient.rest(HttpMethod.DELETE, "/openvidu/api/recordings", HttpStatus.SC_METHOD_NOT_ALLOWED); + restClient.rest(HttpMethod.DELETE, "/openvidu/api/recordings/NOT_EXISTS", HttpStatus.SC_NOT_FOUND); + restClient.rest(HttpMethod.DELETE, "/openvidu/api/recordings/CUSTOM_SESSION_ID", HttpStatus.SC_NO_CONTENT); - // GET /api/recordings should return empty again - restClient.rest(HttpMethod.GET, "/api/recordings", null, HttpStatus.SC_OK, true, - ImmutableMap.of("count", new Integer(0), "items", new JsonArray())); + // GET /openvidu/api/recordings should return empty again + restClient.rest(HttpMethod.GET, "/openvidu/api/recordings", null, HttpStatus.SC_OK, true, true, true, + "{'count':0,'items':[]}"); - /** DELETE /api/sessions//stream/ **/ - restClient.rest(HttpMethod.DELETE, "/api/sessions/NOT_EXISTS/stream/NOT_EXISTS", HttpStatus.SC_BAD_REQUEST); - restClient.rest(HttpMethod.DELETE, "/api/sessions/CUSTOM_SESSION_ID/stream/NOT_EXISTS", + /** DELETE /openvidu/api/sessions//stream/ **/ + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/NOT_EXISTS/stream/NOT_EXISTS", + HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/CUSTOM_SESSION_ID/stream/NOT_EXISTS", HttpStatus.SC_NOT_FOUND); - res = restClient.rest(HttpMethod.GET, "/api/sessions/CUSTOM_SESSION_ID", null, HttpStatus.SC_OK, true, - "{'sessionId':'STR','createdAt':0,'mediaMode':'STR','recordingMode':'STR','defaultOutputMode':'STR','defaultRecordingLayout':'STR','customSessionId':'STR','forcedVideoCodec':'STR','allowTranscoding':false,'connections':{'numberOfElements':2,'content'" + res = restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/CUSTOM_SESSION_ID", null, HttpStatus.SC_OK, true, + false, true, + "{'id':'STR','object':'STR','sessionId':'STR','createdAt':0,'mediaMode':'STR','recordingMode':'STR','defaultOutputMode':'STR','defaultRecordingLayout':'STR','customSessionId':'STR','connections':{'numberOfElements':2,'content'" + ":[{'connectionId':'STR','createdAt':0,'location':'STR','platform':'STR','token':'STR','role':'STR','serverData':'STR','clientData':'STR','publishers':[" + "{'createdAt':0,'streamId':'STR','mediaOptions':{'hasAudio':false,'audioActive':false,'hasVideo':false,'videoActive':false,'typeOfVideo':'STR','frameRate':0," + "'videoDimensions':'STR','filter':{}}}],'subscribers':[{'createdAt':0,'streamId':'STR','publisher':'STR'}]},{'connectionId':'STR','createdAt':0,'location':'STR'," + "'platform':'STR','token':'STR','role':'STR','serverData':'STR','clientData':'STR','publishers':[{'createdAt':0,'streamId':'STR','mediaOptions':{'hasAudio':false," - + "'audioActive':false,'hasVideo':false,'videoActive':false,'typeOfVideo':'STR','frameRate':0,'videoDimensions':'STR','filter':{}}}],'subscribers':[{'createdAt':0,'streamId':'STR','publisher':'STR'}]}]},'recording':false}"); + + "'audioActive':false,'hasVideo':false,'videoActive':false,'typeOfVideo':'STR','frameRate':0,'videoDimensions':'STR','filter':{}}}],'subscribers':[{'createdAt':0,'streamId':'STR','publisher':'STR'}]}]},'recording':false,'forcedVideoCodec':'STR','allowTranscoding':false}"); String streamId = res.get("connections").getAsJsonObject().get("content").getAsJsonArray().get(0) .getAsJsonObject().get("publishers").getAsJsonArray().get(0).getAsJsonObject().get("streamId") .getAsString(); - restClient.rest(HttpMethod.DELETE, "/api/sessions/CUSTOM_SESSION_ID/stream/" + streamId, + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/CUSTOM_SESSION_ID/stream/" + streamId, HttpStatus.SC_NO_CONTENT); final String connectionId = res.get("connections").getAsJsonObject().get("content").getAsJsonArray().get(0) .getAsJsonObject().get("connectionId").getAsString(); - /** POST /api/signal (ACTIVE SESSION) **/ + /** POST /openvidu/api/signal (ACTIVE SESSION) **/ body = "{'session':'CUSTOM_SESSION_ID','to':['wrongConnectionId']}"; - restClient.rest(HttpMethod.POST, "/api/signal", body, HttpStatus.SC_NOT_ACCEPTABLE); // No valid connectionId + restClient.rest(HttpMethod.POST, "/openvidu/api/signal", body, HttpStatus.SC_NOT_ACCEPTABLE); // No valid + // connectionId body = "{'session':'CUSTOM_SESSION_ID','to':['" + connectionId + "','wrongConnectionId']}"; - restClient.rest(HttpMethod.POST, "/api/signal", body, HttpStatus.SC_NOT_ACCEPTABLE); // No valid connectionId + restClient.rest(HttpMethod.POST, "/openvidu/api/signal", body, HttpStatus.SC_NOT_ACCEPTABLE); // No valid + // connectionId body = "{'session':'CUSTOM_SESSION_ID'}"; - restClient.rest(HttpMethod.POST, "/api/signal", body, HttpStatus.SC_OK); + restClient.rest(HttpMethod.POST, "/openvidu/api/signal", body, HttpStatus.SC_OK); user.getEventManager().waitUntilEventReaches("signal", 2); body = "{'session':'CUSTOM_SESSION_ID','to':[],'type':'server1','data':'SERVER EVENT!'}"; - restClient.rest(HttpMethod.POST, "/api/signal", body, HttpStatus.SC_OK); + restClient.rest(HttpMethod.POST, "/openvidu/api/signal", body, HttpStatus.SC_OK); user.getEventManager().waitUntilEventReaches("signal:server1", 2); body = "{'session':'CUSTOM_SESSION_ID','to':['" + connectionId + "'],'type':'server2','data':'SERVER EVENT!'}"; - restClient.rest(HttpMethod.POST, "/api/signal", body, HttpStatus.SC_OK); + restClient.rest(HttpMethod.POST, "/openvidu/api/signal", body, HttpStatus.SC_OK); user.getEventManager().waitUntilEventReaches("signal:server2", 1); Assert.assertEquals("", 1, user.getDriver() .findElements(By.xpath("//*[text()='server - signal:server2 - SERVER EVENT!']")).size()); - /** DELETE /api/sessions//connection/ **/ - restClient.rest(HttpMethod.DELETE, "/api/sessions/NOT_EXISTS/connection/NOT_EXISTS", HttpStatus.SC_BAD_REQUEST); - restClient.rest(HttpMethod.DELETE, "/api/sessions/CUSTOM_SESSION_ID/connection/NOT_EXISTS", + /** DELETE /openvidu/api/sessions//connection/ **/ + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/NOT_EXISTS/connection/NOT_EXISTS", + HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/NOT_EXISTS", HttpStatus.SC_NOT_FOUND); - restClient.rest(HttpMethod.DELETE, "/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionId, HttpStatus.SC_NO_CONTENT); - /** DELETE /api/sessions **/ - restClient.rest(HttpMethod.DELETE, "/api/sessions", HttpStatus.SC_METHOD_NOT_ALLOWED); - restClient.rest(HttpMethod.DELETE, "/api/sessions/NOT_EXISTS", HttpStatus.SC_NOT_FOUND); - restClient.rest(HttpMethod.DELETE, "/api/sessions/CUSTOM_SESSION_ID", HttpStatus.SC_NO_CONTENT); + /** DELETE /openvidu/api/sessions **/ + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions", HttpStatus.SC_METHOD_NOT_ALLOWED); + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/NOT_EXISTS", HttpStatus.SC_NOT_FOUND); + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/CUSTOM_SESSION_ID", HttpStatus.SC_NO_CONTENT); - // GET /api/sessions should return empty again - restClient.rest(HttpMethod.GET, "/api/sessions", null, HttpStatus.SC_OK, true, - ImmutableMap.of("numberOfElements", new Integer(0), "content", new JsonArray())); + // GET /openvidu/api/sessions should return empty again + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions", null, HttpStatus.SC_OK, true, true, true, + "{'numberOfElements':0,'content':[]}"); - /** GET /config **/ - restClient.rest(HttpMethod.GET, "/config", null, HttpStatus.SC_OK, true, + /** + * DELETE /openvidu/api/sessions//connection/ (unused + * token) + **/ + body = "{'customSessionId': 'CUSTOM_SESSION_ID'}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", body, HttpStatus.SC_OK); + body = "{'type': 'WEBRTC', 'role': 'SUBSCRIBER'}"; + res = restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", body, + HttpStatus.SC_OK, true, false, true, DEFAULT_JSON_PENDING_CONNECTION); + final String connectionIdA = res.get("id").getAsString(); + final String tokenA = res.get("token").getAsString(); + res = restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", body, + HttpStatus.SC_OK, true, false, true, DEFAULT_JSON_PENDING_CONNECTION); + final String connectionIdB = res.get("connectionId").getAsString(); + final String tokenB = res.get("token").getAsString(); + + user.getDriver().findElement(By.id("one2one-btn")).click(); + + // Set token 1 + user.getDriver().findElement(By.id("session-settings-btn-0")).click(); + Thread.sleep(1000); + tokenInput = user.getDriver().findElement(By.cssSelector("#custom-token-div input")); + tokenInput.clear(); + tokenInput.sendKeys(tokenA); + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(1000); + + // Set token 2 + user.getDriver().findElement(By.id("session-settings-btn-1")).click(); + Thread.sleep(1000); + tokenInput = user.getDriver().findElement(By.cssSelector("#custom-token-div input")); + tokenInput.clear(); + tokenInput.sendKeys(tokenB); + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(1000); + + // Invalidate token + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + connectionIdA, + HttpStatus.SC_NO_CONTENT); + + // User should pop up invalid token + user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .join-btn")).sendKeys(Keys.ENTER); + try { + user.getWaiter().until(ExpectedConditions.alertIsPresent()); + Alert alert = user.getDriver().switchTo().alert(); + Assert.assertTrue("Alert does not contain expected text", + alert.getText().contains("Token " + tokenA + "is not valid")); + alert.accept(); + } catch (Exception e) { + Assert.fail("Alert exception"); + } + + Thread.sleep(500); + user.getEventManager().resetEventThread(); + + user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .join-btn")).sendKeys(Keys.ENTER); + + user.getEventManager().waitUntilEventReaches("connectionCreated", 1); + + // connectionId should be equal to the one brought by the token + Assert.assertEquals("Wrong connectionId", connectionIdB, + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/CUSTOM_SESSION_ID", HttpStatus.SC_OK) + .get("connections").getAsJsonObject().get("content").getAsJsonArray().get(0).getAsJsonObject() + .get("connectionId").getAsString()); + + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/CUSTOM_SESSION_ID", HttpStatus.SC_NO_CONTENT); + + // GET /openvidu/api/sessions should return empty again + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions", null, HttpStatus.SC_OK, true, true, true, + "{'numberOfElements':0,'content':[]}"); + + /** GET /openvidu/api/config **/ + restClient.rest(HttpMethod.GET, "/openvidu/api/config", null, HttpStatus.SC_OK, true, false, true, "{'VERSION':'STR','DOMAIN_OR_PUBLIC_IP':'STR','HTTPS_PORT':0,'OPENVIDU_PUBLICURL':'STR','OPENVIDU_CDR':false,'OPENVIDU_STREAMS_VIDEO_MAX_RECV_BANDWIDTH':0,'OPENVIDU_STREAMS_VIDEO_MIN_RECV_BANDWIDTH':0," - + "'OPENVIDU_STREAMS_VIDEO_MAX_SEND_BANDWIDTH':0,'OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH':0,'OPENVIDU_SESSIONS_GARBAGE_INTERVAL':0,'OPENVIDU_SESSIONS_GARBAGE_THRESHOLD':0," + + "'OPENVIDU_STREAMS_VIDEO_MAX_SEND_BANDWIDTH':0,'OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH':0,'OPENVIDU_STREAMS_FORCED_VIDEO_CODEC':'STR','OPENVIDU_STREAMS_ALLOW_TRANSCODING':false,'OPENVIDU_SESSIONS_GARBAGE_INTERVAL':0," + "'OPENVIDU_RECORDING':false,'OPENVIDU_RECORDING_VERSION':'STR','OPENVIDU_RECORDING_PATH':'STR','OPENVIDU_RECORDING_PUBLIC_ACCESS':false,'OPENVIDU_RECORDING_NOTIFICATION':'STR'," - + "'OPENVIDU_RECORDING_CUSTOM_LAYOUT':'STR','OPENVIDU_RECORDING_AUTOSTOP_TIMEOUT':0,'OPENVIDU_WEBHOOK':false,'OPENVIDU_WEBHOOK_ENDPOINT':'STR','OPENVIDU_WEBHOOK_HEADERS':[]," + + "'OPENVIDU_SESSIONS_GARBAGE_THRESHOLD':0,'OPENVIDU_RECORDING_CUSTOM_LAYOUT':'STR','OPENVIDU_RECORDING_AUTOSTOP_TIMEOUT':0,'OPENVIDU_WEBHOOK':false,'OPENVIDU_WEBHOOK_ENDPOINT':'STR','OPENVIDU_WEBHOOK_HEADERS':[]," + "'OPENVIDU_WEBHOOK_EVENTS':[]}"); + + /** POST /openvidu/api/sessions (default transcoding parameters) **/ + + body = "{'mediaMode': 'ROUTED', 'recordingMode': 'MANUAL', 'customSessionId': 'CUSTOM_SESSION_ID', 'defaultOutputMode': 'COMPOSED', 'defaultRecordingLayout': 'BEST_FIT'}"; + res = restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", body, HttpStatus.SC_OK); + + // Check session info + Assert.assertEquals(VideoCodec.valueOf(res.get("forcedVideoCodec").getAsString()), defaultForcedVideoCodec); + Assert.assertEquals(res.get("allowTranscoding").getAsBoolean(), defaultAllowTranscoding); + + // Check all sessions data + res = restClient.rest(HttpMethod.GET, "/openvidu/api/sessions", HttpStatus.SC_OK); + Assert.assertEquals(res.get("numberOfElements").getAsInt(), 1); + Assert.assertEquals(VideoCodec.valueOf( + res.get("content").getAsJsonArray().get(0).getAsJsonObject().get("forcedVideoCodec").getAsString()), + defaultForcedVideoCodec); + Assert.assertEquals( + res.get("content").getAsJsonArray().get(0).getAsJsonObject().get("allowTranscoding").getAsBoolean(), + defaultAllowTranscoding); + + // Remove session + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/CUSTOM_SESSION_ID", HttpStatus.SC_NO_CONTENT); + + /** POST /openvidu/api/sessions (define forceCodec and allowTranscoding) **/ + body = "{'mediaMode': 'ROUTED', 'recordingMode': 'MANUAL', 'customSessionId': 'CUSTOM_SESSION_ID', 'defaultOutputMode': 'COMPOSED', 'defaultRecordingLayout': 'BEST_FIT', 'forcedVideoCodec': 'H264', 'allowTranscoding': true}"; + res = restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", body, HttpStatus.SC_OK); + + Assert.assertEquals(VideoCodec.valueOf(res.get("forcedVideoCodec").getAsString()), VideoCodec.H264); + Assert.assertEquals(res.get("allowTranscoding").getAsBoolean(), true); + + // Check all sessions data + res = restClient.rest(HttpMethod.GET, "/openvidu/api/sessions", HttpStatus.SC_OK); + Assert.assertEquals(res.get("numberOfElements").getAsInt(), 1); + Assert.assertEquals(VideoCodec.valueOf( + res.get("content").getAsJsonArray().get(0).getAsJsonObject().get("forcedVideoCodec").getAsString()), + VideoCodec.H264); + Assert.assertEquals( + res.get("content").getAsJsonArray().get(0).getAsJsonObject().get("allowTranscoding").getAsBoolean(), + true); + + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/CUSTOM_SESSION_ID", HttpStatus.SC_NO_CONTENT); } @Test @DisplayName("Kurento reconnect test") + @Disabled void kurentoReconnectTest() throws Exception { isRecordingTest = true; isKurentoRestartTest = true; @@ -2950,7 +3335,8 @@ public class OpenViduTestAppE2eTest { Assert.assertTrue("Recording duration exceeds valid value. Expected no more than 0.2 seconds, got " + differenceInDuration, differenceInDuration < 0.2); - this.checkIndividualRecording("/opt/openvidu/recordings/TestSession/", rec, 1, "opus", "vp8", true); + this.recordingUtils.checkIndividualRecording("/opt/openvidu/recordings/TestSession/", rec, 1, "opus", "vp8", + true); WebElement pubBtn = user.getDriver().findElements(By.cssSelector("#openvidu-instance-1 .pub-btn")).get(0); pubBtn.click(); @@ -2982,7 +3368,7 @@ public class OpenViduTestAppE2eTest { log.info("Webhook test"); CountDownLatch initLatch = new CountDownLatch(1); - io.openvidu.test.browsers.utils.CustomWebhook.main(new String[0], initLatch); + io.openvidu.test.browsers.utils.webhook.CustomWebhook.main(new String[0], initLatch); try { @@ -3024,6 +3410,7 @@ public class OpenViduTestAppE2eTest { event = CustomWebhook.waitForEvent("webrtcConnectionCreated", 2); Assert.assertEquals("Wrong number of properties in event 'webrtcConnectionCreated'", 10 + 1, event.keySet().size()); + String connectionId1 = event.get("participantId").getAsString(); event = CustomWebhook.waitForEvent("recordingStatusChanged", 10); Assert.assertEquals("Wrong number of properties in event 'recordingStatusChanged'", 11 + 1, @@ -3065,11 +3452,53 @@ public class OpenViduTestAppE2eTest { user.getDriver().findElement(By.id("add-user-btn")).click(); user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .join-btn")).click(); - CustomWebhook.waitForEvent("participantJoined", 2); + event = CustomWebhook.waitForEvent("participantJoined", 2); CustomWebhook.waitForEvent("webrtcConnectionCreated", 2); CustomWebhook.waitForEvent("webrtcConnectionCreated", 2); CustomWebhook.waitForEvent("webrtcConnectionCreated", 2); + String connectionId2 = event.get("participantId").getAsString(); + + // signalSent from client + long timestamp = System.currentTimeMillis(); + user.getDriver().findElement(By.cssSelector(("#openvidu-instance-0 .message-btn"))).click(); + user.getEventManager().waitUntilEventReaches("signal:chat", 2); + event = CustomWebhook.waitForEvent("signalSent", 1); + Assert.assertEquals("Wrong number of properties in event 'signalSent'", 6 + 1, event.keySet().size()); + Assert.assertEquals("Wrong sessionId in webhook event", "TestSession", + event.get("sessionId").getAsString()); + Assert.assertTrue("Wrong timestamp in webhook event", event.get("timestamp").getAsLong() > timestamp); + Assert.assertEquals("Wrong from in webhook event", connectionId1, event.get("from").getAsString()); + Assert.assertEquals("Wrong type in webhook event", "chat", event.get("type").getAsString()); + Assert.assertTrue("Wrong data in webhook event", !event.get("data").getAsString().isEmpty()); + Assert.assertEquals("Wrong event name in webhook event", "signalSent", event.get("event").getAsString()); + JsonArray toArray = event.get("to").getAsJsonArray(); + Assert.assertEquals("Wrong to array size", 2, toArray.size()); + Assert.assertTrue("Wrong to array content in webhook event", + toArray.contains(JsonParser.parseString(connectionId1))); + Assert.assertTrue("Wrong to array content in webhook event", + toArray.contains(JsonParser.parseString(connectionId2))); + + // signalSent from server + CustomHttpClient restClient = new CustomHttpClient(OPENVIDU_URL, "OPENVIDUAPP", OPENVIDU_SECRET); + restClient.rest(HttpMethod.POST, "/openvidu/api/signal", + "{'session':'TestSession','type':'chat','to':['" + connectionId1 + "'],'data':'SERVER_DATA'}", + HttpStatus.SC_OK); + user.getEventManager().waitUntilEventReaches("signal:chat", 3); + event = CustomWebhook.waitForEvent("signalSent", 1); + Assert.assertEquals("Wrong number of properties in event 'signalSent'", 6 + 1, event.keySet().size()); + Assert.assertEquals("Wrong sessionId in webhook event", "TestSession", + event.get("sessionId").getAsString()); + Assert.assertTrue("Wrong timestamp in webhook event", event.get("timestamp").getAsLong() > timestamp); + Assert.assertTrue("Wrong from in webhook event", event.get("from").isJsonNull()); + Assert.assertEquals("Wrong type in webhook event", "chat", event.get("type").getAsString()); + Assert.assertEquals("Wrong data in webhook event", "SERVER_DATA", event.get("data").getAsString()); + Assert.assertEquals("Wrong event name in webhook event", "signalSent", event.get("event").getAsString()); + toArray = event.get("to").getAsJsonArray(); + Assert.assertEquals("Wrong to array size", 1, toArray.size()); + Assert.assertTrue("Wrong to array content in webhook event", + toArray.contains(JsonParser.parseString(connectionId1))); + user.getDriver().findElement(By.id("session-api-btn-0")).click(); Thread.sleep(1000); user.getDriver().findElement(By.id("close-session-btn")).click(); @@ -3135,7 +3564,7 @@ public class OpenViduTestAppE2eTest { log.info("IP camera test"); CountDownLatch initLatch = new CountDownLatch(1); - io.openvidu.test.browsers.utils.CustomWebhook.main(new String[0], initLatch); + io.openvidu.test.browsers.utils.webhook.CustomWebhook.main(new String[0], initLatch); try { @@ -3151,26 +3580,29 @@ public class OpenViduTestAppE2eTest { CustomHttpClient restClient = new CustomHttpClient(OPENVIDU_URL, "OPENVIDUAPP", OPENVIDU_SECRET); // Wrong session [404] - restClient.rest(HttpMethod.POST, "/api/sessions/WRONG_SESSION/connection", "{}", HttpStatus.SC_NOT_FOUND); + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/WRONG_SESSION/connection", "{'type':'IPCAM'}", + HttpStatus.SC_NOT_FOUND); // Init a session and publish IP camera AS FIRST PARTICIPANT - restClient.rest(HttpMethod.POST, "/api/sessions", "{'customSessionId':'IP_CAM_SESSION'}", HttpStatus.SC_OK, - true, - "{'id':'STR','createdAt':0,'mediaMode':'STR','recordingMode':'STR','defaultOutputMode':'STR','defaultRecordingLayout':'STR','customSessionId':'STR','forcedVideoCodec':'STR','allowTranscoding':false,'connections':{'numberOfElements':0,'content':[]},'recording':true}"); + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", "{'customSessionId':'IP_CAM_SESSION'}", + HttpStatus.SC_OK, true, false, true, DEFAULT_JSON_SESSION); // No rtspUri [400] - restClient.rest(HttpMethod.POST, "/api/sessions/IP_CAM_SESSION/connection", "{}", + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/IP_CAM_SESSION/connection", "{'type':'IPCAM'}", HttpStatus.SC_BAD_REQUEST); // Wrong rtspUri (invalid url format) [400] - restClient.rest(HttpMethod.POST, "/api/sessions/IP_CAM_SESSION/connection", "{'rtspUri': 'NOT_A_URL'}", + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/IP_CAM_SESSION/connection", + "{'type':'IPCAM','rtspUri': 'NOT_A_URL'}", HttpStatus.SC_BAD_REQUEST); + // Wrong adaptativeBitrate [400] + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/IP_CAM_SESSION/connection", + "{'type':'IPCAM','rtspUri':'rtsp://dummyurl.com','adaptativeBitrate':123,}", HttpStatus.SC_BAD_REQUEST); // Publish IP camera. Dummy URL because no user will subscribe to it [200] - String ipCamBody = "{'type':'IPCAM','rtspUri':'rtsp://dummyurl.com','adaptativeBitrate':true,'onlyPlayWithSubscribers':true,'data':'MY_IP_CAMERA'}"; - JsonObject response = restClient.rest(HttpMethod.POST, "/api/sessions/IP_CAM_SESSION/connection", ipCamBody, - HttpStatus.SC_OK, true, - "{'connectionId':'STR','createdAt':0,'location':'STR','platform':'STR','role':'STR','serverData':'STR','clientData':'STR','publishers':[],'subscribers':[]}"); + String ipCamBody = "{'type':'IPCAM','rtspUri':'rtsp://dummyurl.com','adaptativeBitrate':true,'onlyPlayWithSubscribers':true,'networkCache':1000,'data':'MY_IP_CAMERA'}"; + JsonObject response = restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/IP_CAM_SESSION/connection", + ipCamBody, HttpStatus.SC_OK, true, false, true, DEFAULT_JSON_IPCAM_CONNECTION); CustomWebhook.waitForEvent("sessionCreated", 1); CustomWebhook.waitForEvent("participantJoined", 1); @@ -3178,7 +3610,8 @@ public class OpenViduTestAppE2eTest { Assert.assertEquals("Wrong serverData property", "MY_IP_CAMERA", response.get("serverData").getAsString()); Assert.assertEquals("Wrong platform property", "IPCAM", response.get("platform").getAsString()); - Assert.assertEquals("Wrong role property", "PUBLISHER", response.get("role").getAsString()); + Assert.assertEquals("Wrong role property", JsonNull.INSTANCE, response.get("role")); + Assert.assertEquals("Wrong type property", "IPCAM", response.get("type").getAsString()); Assert.assertEquals("Wrong number of publishers in IPCAM participant", 1, response.get("publishers").getAsJsonArray().size()); @@ -3187,19 +3620,19 @@ public class OpenViduTestAppE2eTest { Assert.assertEquals("Wrong rtspUri property", "rtsp://dummyurl.com", ipCamPublisher.get("rtspUri").getAsString()); JsonObject mediaOptions = ipCamPublisher.get("mediaOptions").getAsJsonObject(); - Assert.assertEquals("Wrong number of properties in MediaOptions", 10, mediaOptions.size()); + Assert.assertEquals("Wrong number of properties in MediaOptions", 11, mediaOptions.size()); Assert.assertTrue("Wrong adaptativeBitrate property", mediaOptions.get("adaptativeBitrate").getAsBoolean()); Assert.assertTrue("Wrong onlyPlayWithSubscribers property", mediaOptions.get("onlyPlayWithSubscribers").getAsBoolean()); // Can't delete the stream [405] restClient.rest(HttpMethod.DELETE, - "/api/sessions/IP_CAM_SESSION/stream/" + ipCamPublisher.get("streamId").getAsString(), + "/openvidu/api/sessions/IP_CAM_SESSION/stream/" + ipCamPublisher.get("streamId").getAsString(), HttpStatus.SC_METHOD_NOT_ALLOWED); // Can delete the connection [204] restClient.rest(HttpMethod.DELETE, - "/api/sessions/IP_CAM_SESSION/connection/" + response.get("connectionId").getAsString(), + "/openvidu/api/sessions/IP_CAM_SESSION/connection/" + response.get("connectionId").getAsString(), HttpStatus.SC_NO_CONTENT); response = CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 1); @@ -3237,37 +3670,37 @@ public class OpenViduTestAppE2eTest { CustomWebhook.waitForEvent("webrtcConnectionCreated", 1); // Composed recording to get an MP4 file AUDIO + VIDEO - restClient.rest(HttpMethod.POST, "/api/recordings/start", + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", "{'session':'TestSession','name':'audioVideo','hasAudio':true,'hasVideo':true}", HttpStatus.SC_OK); user.getEventManager().waitUntilEventReaches("recordingStarted", 1); // Started CustomWebhook.waitForEvent("recordingStatusChanged", 1); Thread.sleep(4000); - restClient.rest(HttpMethod.POST, "/api/recordings/stop/TestSession", HttpStatus.SC_OK); + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/stop/TestSession", HttpStatus.SC_OK); user.getEventManager().waitUntilEventReaches("recordingStopped", 1); CustomWebhook.waitForEvent("recordingStatusChanged", 1); // Stopped CustomWebhook.waitForEvent("recordingStatusChanged", 1); // Ready // Composed recording to get an MP4 file ONLY VIDEO - restClient.rest(HttpMethod.POST, "/api/recordings/start", + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", "{'session':'TestSession','name':'videoOnly','hasAudio':false,'hasVideo':true}", HttpStatus.SC_OK); user.getEventManager().waitUntilEventReaches("recordingStarted", 1); // Started CustomWebhook.waitForEvent("recordingStatusChanged", 1); // Started Thread.sleep(4000); - restClient.rest(HttpMethod.POST, "/api/recordings/stop/TestSession-1", HttpStatus.SC_OK); + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/stop/TestSession-1", HttpStatus.SC_OK); user.getEventManager().waitUntilEventReaches("recordingStopped", 1); CustomWebhook.waitForEvent("recordingStatusChanged", 1); // Stopped CustomWebhook.waitForEvent("recordingStatusChanged", 1); // Ready // Publish the MP4 file as an IPCAM - String recPath = restClient.rest(HttpMethod.GET, "/config", HttpStatus.SC_OK).get("OPENVIDU_RECORDING_PATH") - .getAsString(); + String recPath = restClient.rest(HttpMethod.GET, "/openvidu/api/config", HttpStatus.SC_OK) + .get("OPENVIDU_RECORDING_PATH").getAsString(); recPath = recPath.endsWith("/") ? recPath : (recPath + "/"); String fullRecordingPath = "file://" + recPath + "TestSession/audioVideo.mp4"; ipCamBody = "{'type':'IPCAM','rtspUri':'" + fullRecordingPath - + "','adaptativeBitrate':true,'onlyPlayWithSubscribers':true,'data':'MY_IP_CAMERA'}"; + + "','adaptativeBitrate':true,'onlyPlayWithSubscribers':true,'networkCache':1000,'data':'MY_IP_CAMERA'}"; - restClient.rest(HttpMethod.POST, "/api/sessions/TestSession/connection", ipCamBody, HttpStatus.SC_OK, true, - "{'connectionId':'STR','createdAt':0,'location':'STR','platform':'STR','role':'STR','serverData':'STR','clientData':'STR','publishers':[],'subscribers':[]}"); + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/TestSession/connection", ipCamBody, + HttpStatus.SC_OK, true, false, true, DEFAULT_JSON_IPCAM_CONNECTION); user.getEventManager().waitUntilEventReaches("connectionCreated", 2); user.getEventManager().waitUntilEventReaches("streamCreated", 2); @@ -3293,9 +3726,8 @@ public class OpenViduTestAppE2eTest { user.getWaiter().until(ExpectedConditions.numberOfElementsToBe(By.tagName("video"), 1)); // Publish again the IPCAM - response = restClient.rest(HttpMethod.POST, "/api/sessions/TestSession/connection", ipCamBody, - HttpStatus.SC_OK, true, - "{'connectionId':'STR','createdAt':0,'location':'STR','platform':'STR','role':'STR','serverData':'STR','clientData':'STR','publishers':[],'subscribers':[]}"); + response = restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/TestSession/connection", ipCamBody, + HttpStatus.SC_OK, true, false, true, DEFAULT_JSON_IPCAM_CONNECTION); user.getEventManager().waitUntilEventReaches("connectionCreated", 3); user.getEventManager().waitUntilEventReaches("streamCreated", 3); user.getEventManager().waitUntilEventReaches("streamPlaying", 3); @@ -3313,27 +3745,27 @@ public class OpenViduTestAppE2eTest { CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 1); CustomWebhook.waitForEvent("participantLeft", 1); - restClient.rest(HttpMethod.GET, "/api/sessions/TestSession", null, HttpStatus.SC_OK); + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/TestSession", null, HttpStatus.SC_OK); // Test IPCAM individual recording (IPCAM audio+video, recording audio+video) - response = restClient.rest(HttpMethod.POST, "/api/recordings/start", + response = restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", "{'session':'TestSession','outputMode':'INDIVIDUAL','hasAudio':true,'hasVideo':true}", HttpStatus.SC_OK); String recId = response.get("id").getAsString(); CustomWebhook.waitForEvent("recordingStatusChanged", 1); // Started Thread.sleep(2000); - restClient.rest(HttpMethod.POST, "/api/recordings/stop/" + recId, HttpStatus.SC_OK); + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/stop/" + recId, HttpStatus.SC_OK); CustomWebhook.waitForEvent("recordingStatusChanged", 1); // Stopped CustomWebhook.waitForEvent("recordingStatusChanged", 1); // Ready Recording recording = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(recId); - this.checkIndividualRecording(recPath + recId + "/", recording, 1, "opus", "vp8", true); + this.recordingUtils.checkIndividualRecording(recPath + recId + "/", recording, 1, "opus", "vp8", true); // Test IPCAM individual recording (IPCAM video only, recording audio and video) // Disconnect audio+video IPCAM - restClient.rest(HttpMethod.DELETE, "/api/sessions/TestSession/connection/" + connectionId, + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/TestSession/connection/" + connectionId, HttpStatus.SC_NO_CONTENT); // Session is closed (create new session) @@ -3341,12 +3773,13 @@ public class OpenViduTestAppE2eTest { CustomWebhook.waitForEvent("participantLeft", 1); CustomWebhook.waitForEvent("sessionDestroyed", 1); - restClient.rest(HttpMethod.POST, "/api/sessions", "{'customSessionId':'TestSession'}", HttpStatus.SC_OK); + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", "{'customSessionId':'TestSession'}", + HttpStatus.SC_OK); // Publish video only IPCAM fullRecordingPath = "file://" + recPath + "TestSession-1/videoOnly.mp4"; - ipCamBody = "{'rtspUri':'" + fullRecordingPath + "'}"; - response = restClient.rest(HttpMethod.POST, "/api/sessions/TestSession/connection", ipCamBody, + ipCamBody = "{'type':'IPCAM','rtspUri':'" + fullRecordingPath + "'}"; + response = restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/TestSession/connection", ipCamBody, HttpStatus.SC_OK); CustomWebhook.waitForEvent("sessionCreated", 1); CustomWebhook.waitForEvent("participantJoined", 1); @@ -3354,21 +3787,21 @@ public class OpenViduTestAppE2eTest { // Record audio and video // TODO: THIS SHOULD WORK -// response = restClient.rest(HttpMethod.POST, "/api/recordings/start", +// response = restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", // "{'session':'TestSession','outputMode':'INDIVIDUAL','hasAudio':true,'hasVideo':true}", // HttpStatus.SC_OK); // recId = response.get("id").getAsString(); // CustomWebhook.waitForEvent("recordingStatusChanged", 1); // Started // // Thread.sleep(2000); -// restClient.rest(HttpMethod.POST, "/api/recordings/stop/TestSession-2", HttpStatus.SC_OK); +// restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/stop/TestSession-2", HttpStatus.SC_OK); // CustomWebhook.waitForEvent("recordingStatusChanged", 1); // Stopped // CustomWebhook.waitForEvent("recordingStatusChanged", 1); // Ready // // recording = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(recId); // this.checkIndividualRecording(recPath + recId + "/", recording, 1, "opus", "vp8", true); - restClient.rest(HttpMethod.DELETE, "/api/sessions/TestSession", HttpStatus.SC_NO_CONTENT); + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/TestSession", HttpStatus.SC_NO_CONTENT); CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 1); CustomWebhook.waitForEvent("participantLeft", 1); @@ -3379,281 +3812,502 @@ public class OpenViduTestAppE2eTest { } } - private void listEmptyRecordings() { - // List existing recordings (empty) - user.getDriver().findElement(By.id("list-recording-btn")).click(); - user.getWaiter() - .until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", "Recording list []")); - } + @Test + @DisplayName("OpenVidu SDK fetch test") + void openviduSdkFetchTest() throws Exception { + isRecordingTest = true; - private ExpectedCondition waitForVideoDuration(WebElement element, int durationInSeconds) { - return new ExpectedCondition() { - @Override - public Boolean apply(WebDriver input) { - return element.getAttribute("duration").matches( - durationInSeconds - 1 + "\\.[5-9][0-9]{0,5}|" + durationInSeconds + "\\.[0-5][0-9]{0,5}"); - } - }; - } + setupBrowser("chrome"); - private boolean checkVideoAverageRgbGreen(Map rgb) { - // GREEN color: {r < 15, g > 130, b <15} - return (rgb.get("r") < 15) && (rgb.get("g") > 130) && (rgb.get("b") < 15); - } + log.info("OpenVidu SDK fetch test"); - private boolean checkVideoAverageRgbGray(Map rgb) { - // GRAY color: {r < 50, g < 50, b < 50} and the absolute difference between them - // not greater than 2 - return (rgb.get("r") < 50) && (rgb.get("g") < 50) && (rgb.get("b") < 50) - && (Math.abs(rgb.get("r") - rgb.get("g")) <= 2) && (Math.abs(rgb.get("r") - rgb.get("b")) <= 2) - && (Math.abs(rgb.get("b") - rgb.get("g")) <= 2); - } + user.getDriver().findElement(By.id("add-user-btn")).click(); + user.getDriver().findElement(By.id("session-api-btn-0")).click(); + Thread.sleep(1000); - private void gracefullyLeaveParticipants(int numberOfParticipants) throws Exception { - int accumulatedConnectionDestroyed = 0; - for (int j = 1; j <= numberOfParticipants; j++) { - user.getDriver().findElement(By.id("remove-user-btn")).sendKeys(Keys.ENTER); - user.getEventManager().waitUntilEventReaches("sessionDisconnected", j); - accumulatedConnectionDestroyed = (j != numberOfParticipants) - ? (accumulatedConnectionDestroyed + numberOfParticipants - j) - : (accumulatedConnectionDestroyed); - user.getEventManager().waitUntilEventReaches("connectionDestroyed", accumulatedConnectionDestroyed); - } - } + Session session = OV.createSession(); + Assert.assertFalse("Java fetch should be false", OV.fetch()); + checkNodeFetchChanged(true, true); + checkNodeFetchChanged(true, false); - private String getBase64Screenshot(MyUser user) throws Exception { - String screenshotBase64 = ((TakesScreenshot) user.getDriver()).getScreenshotAs(BASE64); - return "data:image/png;base64," + screenshotBase64; - } + CustomHttpClient restClient = new CustomHttpClient(OPENVIDU_URL, "OPENVIDUAPP", OPENVIDU_SECRET); + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", "{'customSessionId':'REST_SESSION'}", + HttpStatus.SC_OK); + Assert.assertTrue("Java fetch should be true", OV.fetch()); + Assert.assertFalse("Java fetch should be false", OV.fetch()); + Assert.assertFalse("Java fetch should be false", session.fetch()); + checkNodeFetchChanged(true, true); + checkNodeFetchChanged(true, false); - private boolean recordedFileFine(File file, Recording recording) throws IOException { - this.checkMultimediaFile(file, recording.hasAudio(), recording.hasVideo(), recording.getDuration(), - recording.getResolution(), "aac", "h264", true); + Connection connection = session.createConnection(); + Assert.assertFalse("Java fetch should be false", session.fetch()); + Assert.assertFalse("Java fetch should be false", OV.fetch()); + checkNodeFetchChanged(true, true); + checkNodeFetchChanged(true, false); - boolean isFine = false; - Picture frame; + // OpenVidu CE does not support Session#updateConnection method try { - // Get a frame at 75% duration and check that it has the expected color - frame = FrameGrab.getFrameAtSec(file, (double) (recording.getDuration() * 0.75)); - BufferedImage image = AWTUtil.toBufferedImage(frame); - Map colorMap = this.averageColor(image); - - String realResolution = image.getWidth() + "x" + image.getHeight(); - Assert.assertEquals( - "Resolution (" + recording.getResolution() - + ") of recording entity is not equal to real video resolution (" + realResolution + ")", - recording.getResolution(), realResolution); - - log.info("Recording map color: {}", colorMap.toString()); - log.info("Recording frame below"); - System.out.println(bufferedImageToBase64PngString(image)); - isFine = this.checkVideoAverageRgbGreen(colorMap); - } catch (IOException | JCodecException e) { - log.warn("Error getting frame from video recording: {}", e.getMessage()); - isFine = false; - } - return isFine; - } - - private String bufferedImageToBase64PngString(BufferedImage image) { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - String imageString = null; - try { - ImageIO.write(image, "png", bos); - byte[] imageBytes = bos.toByteArray(); - imageString = "data:image/png;base64," + Base64.getEncoder().encodeToString(imageBytes); - bos.close(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - return imageString; - } - - private void checkIndividualRecording(String recPath, Recording recording, int numberOfVideoFiles, - String audioDecoder, String videoDecoder, boolean checkAudio) throws IOException { - - // Should be only 2 files: zip and metadata - File folder = new File(recPath); - Assert.assertEquals("There are more than 2 files (ZIP and metadata) inside individual recording folder " - + recPath + ": " + Arrays.toString(folder.listFiles()), 2, folder.listFiles().length); - - File file1 = new File(recPath + recording.getName() + ".zip"); - File file2 = new File(recPath + ".recording." + recording.getId()); - - Assert.assertTrue("File " + file1.getAbsolutePath() + " does not exist or is empty", - file1.exists() && file1.length() > 0); - Assert.assertTrue("File " + file2.getAbsolutePath() + " does not exist or is empty", - file2.exists() && file2.length() > 0); - - List unzippedWebmFiles = new Unzipper().unzipFile(recPath, recording.getName() + ".zip"); - - Assert.assertEquals("Expecting " + numberOfVideoFiles + " videos inside ZIP file but " - + unzippedWebmFiles.size() + " found: " + unzippedWebmFiles.toString(), numberOfVideoFiles, - unzippedWebmFiles.size()); - - File jsonSyncFile = new File(recPath + recording.getName() + ".json"); - Assert.assertTrue("JSON sync file " + jsonSyncFile.getAbsolutePath() + "does not exist or is empty", - jsonSyncFile.exists() && jsonSyncFile.length() > 0); - - JsonObject jsonSyncMetadata; - try { - Gson gson = new Gson(); - JsonReader reader = new JsonReader(new FileReader(jsonSyncFile)); - jsonSyncMetadata = gson.fromJson(reader, JsonObject.class); + session.updateConnection(connection.getConnectionId(), new ConnectionProperties.Builder().build()); + Assert.fail("Expected exception was not thrown by OpenVidu Java Client"); + } catch (OpenViduHttpException e) { + Assert.assertEquals("Wrong OpenViduException status", HttpStatus.SC_METHOD_NOT_ALLOWED, e.getStatus()); } catch (Exception e) { - log.error("Cannot read JSON sync metadata file from {}. Error: {}", jsonSyncFile.getAbsolutePath(), - e.getMessage()); - Assert.fail("Cannot read JSON sync metadata file from " + jsonSyncFile.getAbsolutePath()); - return; + Assert.fail("Wrong exception type thrown by OpenVidu Java Client"); } - long totalFileSize = 0; - JsonArray syncArray = jsonSyncMetadata.get("files").getAsJsonArray(); - for (File webmFile : unzippedWebmFiles) { - totalFileSize += webmFile.length(); + restClient.rest(HttpMethod.POST, "/openvidu/api/tokens", "{'session':'REST_SESSION'}", HttpStatus.SC_OK); + Assert.assertFalse("Fetch should be true", session.fetch()); + Assert.assertTrue("Fetch should be false", OV.fetch()); + checkNodeFetchChanged(true, true); + checkNodeFetchChanged(true, false); - Assert.assertTrue("WEBM file " + webmFile.getAbsolutePath() + " does not exist or is empty", - webmFile.exists() && webmFile.length() > 0); + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/REST_SESSION", HttpStatus.SC_NO_CONTENT); + Assert.assertFalse("Java fetch should be true", session.fetch()); + Assert.assertTrue("Java fetch should be true", OV.fetch()); + Assert.assertFalse("Java fetch should be false", OV.fetch()); + checkNodeFetchChanged(true, true); + checkNodeFetchChanged(true, false); - double durationInSeconds = 0; - boolean found = false; - for (int i = 0; i < syncArray.size(); i++) { - JsonObject j = syncArray.get(i).getAsJsonObject(); - if (webmFile.getName().contains(j.get("streamId").getAsString())) { - durationInSeconds = (double) (j.get("endTimeOffset").getAsDouble() - - j.get("startTimeOffset").getAsDouble()) / 1000; - found = true; - break; - } - } + // Set token and join session + user.getDriver().findElement(By.id("close-dialog-btn")).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.id("session-settings-btn-0")).click(); + Thread.sleep(1000); + WebElement tokeInput = user.getDriver().findElement(By.cssSelector("#custom-token-div input")); + tokeInput.clear(); + tokeInput.sendKeys(connection.getToken()); + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.className("join-btn")).click(); - Assert.assertTrue("Couldn't find in JSON sync object information for webm file " + webmFile.getName(), - found); + user.getEventManager().waitUntilEventReaches("connectionCreated", 1); + user.getEventManager().waitUntilEventReaches("accessAllowed", 1); + user.getEventManager().waitUntilEventReaches("streamCreated", 1); + user.getEventManager().waitUntilEventReaches("streamPlaying", 1); - log.info("Duration of {} according to sync metadata json file: {} s", webmFile.getName(), - durationInSeconds); - this.checkMultimediaFile(webmFile, recording.hasAudio(), recording.hasVideo(), durationInSeconds, - recording.getResolution(), audioDecoder, videoDecoder, checkAudio); - webmFile.delete(); + user.getDriver().findElement(By.id("session-api-btn-0")).click(); + Thread.sleep(1000); + + Assert.assertTrue("Java fetch should be true", OV.fetch()); + Assert.assertFalse("Java fetch should be false", session.fetch()); + checkNodeFetchChanged(false, true); + checkNodeFetchChanged(true, false); + checkNodeFetchChanged(true, false); + + // Modify connection properties + user.getDriver().findElement(By.id("record-checkbox")).click(); + user.getDriver().findElement(By.id("token-role-select")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("option-MODERATOR")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("connection-data-field")).sendKeys("MY_SERVER_DATA"); + + // Create Connection with openvidu-node-client + long timestamp = System.currentTimeMillis(); + final String successMessage = "Connection created: "; + user.getDriver().findElement(By.id("crate-connection-api-btn")).click(); + user.getWaiter() + .until(ExpectedConditions.attributeContains(By.id("api-response-text-area"), "value", successMessage)); + String value = user.getDriver().findElement(By.id("api-response-text-area")).getAttribute("value"); + // Check openvidu-node-client Connection properties + JsonObject connectionJson = JsonParser + .parseString(value.substring(value.lastIndexOf(successMessage) + successMessage.length())) + .getAsJsonObject(); + JsonObject connectionProperties = connectionJson.get("connectionProperties").getAsJsonObject(); + String connectionId = connectionJson.get("connectionId").getAsString(); + Assert.assertEquals("Wrong status Connection property", "pending", connectionJson.get("status").getAsString()); + Assert.assertTrue("Wrong timestamp Connection property", + connectionJson.get("createdAt").getAsLong() > timestamp); + Assert.assertTrue("Wrong activeAt Connection property", connectionJson.get("activeAt").isJsonNull()); + Assert.assertTrue("Wrong location Connection property", connectionJson.get("location").isJsonNull()); + Assert.assertTrue("Wrong platform Connection property", connectionJson.get("platform").isJsonNull()); + Assert.assertTrue("Wrong clientData Connection property", connectionJson.get("clientData").isJsonNull()); + Assert.assertTrue("Wrong publishers Connection property", + connectionJson.get("publishers").getAsJsonArray().size() == 0); + Assert.assertTrue("Wrong subscribers Connection property", + connectionJson.get("subscribers").getAsJsonArray().size() == 0); + Assert.assertTrue("Wrong token Connection property", + connectionJson.get("token").getAsString().contains(session.getSessionId())); + Assert.assertEquals("Wrong number of keys in connectionProperties", 9, connectionProperties.keySet().size()); + Assert.assertEquals("Wrong type property", ConnectionType.WEBRTC.name(), + connectionProperties.get("type").getAsString()); + Assert.assertEquals("Wrong data property", "MY_SERVER_DATA", connectionProperties.get("data").getAsString()); + Assert.assertTrue("Wrong record property", connectionProperties.get("record").getAsBoolean()); // Is true in CE + Assert.assertEquals("Wrong role property", OpenViduRole.MODERATOR.name(), + connectionProperties.get("role").getAsString()); + Assert.assertTrue("Wrong kurentoOptions property", connectionProperties.get("kurentoOptions").isJsonNull()); + Assert.assertTrue("Wrong rtspUri property", connectionProperties.get("rtspUri").isJsonNull()); + Assert.assertTrue("Wrong adaptativeBitrate property", + connectionProperties.get("adaptativeBitrate").isJsonNull()); + Assert.assertTrue("Wrong onlyPlayWithSubscribers property", + connectionProperties.get("onlyPlayWithSubscribers").isJsonNull()); + Assert.assertTrue("Wrong networkCache property", connectionProperties.get("networkCache").isJsonNull()); + + Assert.assertTrue("Java fetch should be true", session.fetch()); + Assert.assertFalse("Java fetch should be false", OV.fetch()); + checkNodeFetchChanged(true, false); + checkNodeFetchChanged(false, false); + checkNodeFetchChanged(true, false); + + // Delete Connection with openvidu-node-client + user.getDriver().findElement(By.id("connection-id-field")).clear(); + user.getDriver().findElement(By.id("connection-id-field")).sendKeys(connectionId); + user.getDriver().findElement(By.id("force-disconnect-api-btn")).click(); + user.getWaiter() + .until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", "User disconnected")); + Assert.assertTrue("Java fetch should be true", OV.fetch()); + Assert.assertFalse("Java fetch should be false", session.fetch()); + checkNodeFetchChanged(false, false); + checkNodeFetchChanged(true, false); + checkNodeFetchChanged(false, false); + + // RECORD + user.getDriver().findElement(By.id("rec-properties-btn")).click(); + user.getDriver().findElement(By.id("rec-hasvideo-checkbox")).click(); + user.getDriver().findElement(By.id("rec-outputmode-select")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("option-INDIVIDUAL")).click(); + Thread.sleep(500); + + user.getDriver().findElement(By.id("start-recording-btn")).click(); + user.getEventManager().waitUntilEventReaches("recordingStarted", 1); + + // Node SDK should return false as the recording has been started with it + checkNodeFetchChanged(false, false); + checkNodeFetchChanged(true, false); + checkNodeFetchChanged(true, false); + // Java SDK should return true as it doesn't know about the recording yet + Assert.assertTrue("Java fetch should be true", session.fetch()); + Assert.assertFalse("Java fetch should be false", OV.fetch()); + + OV.stopRecording(session.getSessionId()); + user.getEventManager().waitUntilEventReaches("recordingStopped", 1); + // Java SDK should return false as the recording has been stopped with it + Assert.assertFalse("Java fetch should be false", session.fetch()); + Assert.assertFalse("Java fetch should be false", OV.fetch()); + // Node SDK should return true as it doesn't know about the recording stooped + checkNodeFetchChanged(false, true); + checkNodeFetchChanged(false, false); + checkNodeFetchChanged(true, false); + + // NEW SUBSCRIBER + user.getDriver().findElement(By.id("close-dialog-btn")).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.id("add-user-btn")).click(); + user.getDriver().findElement(By.id("session-name-input-1")).clear(); + user.getDriver().findElement(By.id("session-name-input-1")).sendKeys(session.getSessionId()); + user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .publish-checkbox")).click(); + user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .join-btn")).click(); + user.getEventManager().waitUntilEventReaches("streamPlaying", 2); + + user.getDriver().findElement(By.id("session-api-btn-0")).click(); + Thread.sleep(1000); + Assert.assertTrue("Java fetch should be true", OV.fetch()); + Assert.assertFalse("Java fetch should be false", session.fetch()); + checkNodeFetchChanged(true, true); + checkNodeFetchChanged(true, false); + checkNodeFetchChanged(false, false); + + // MODIFY STREAM + user.getDriver().findElement(By.id("close-dialog-btn")).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .pub-video-btn")).click(); + user.getEventManager().waitUntilEventReaches("streamPropertyChanged", 2); + user.getDriver().findElement(By.id("session-api-btn-0")).click(); + Thread.sleep(1000); + Assert.assertTrue("Java fetch should be true", session.fetch()); + Assert.assertFalse("Java fetch should be false", OV.fetch()); + checkNodeFetchChanged(false, true); + checkNodeFetchChanged(true, false); + checkNodeFetchChanged(false, false); + + // REMOVE STREAM + user.getDriver().findElement(By.id("close-dialog-btn")).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .sub-btn")).click(); + user.getDriver().findElement(By.id("session-api-btn-0")).click(); + Thread.sleep(1000); + Assert.assertTrue("Java fetch should be true", OV.fetch()); + Assert.assertFalse("Java fetch should be false", session.fetch()); + checkNodeFetchChanged(true, true); + checkNodeFetchChanged(true, false); + checkNodeFetchChanged(false, false); + + // REMOVE USER + user.getDriver().findElement(By.id("close-dialog-btn")).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .leave-btn")).click(); + user.getEventManager().waitUntilEventReaches("connectionDestroyed", 1); + user.getDriver().findElement(By.id("session-api-btn-0")).click(); + Thread.sleep(1000); + Assert.assertTrue("Java fetch should be true", session.fetch()); + Assert.assertFalse("Java fetch should be false", OV.fetch()); + checkNodeFetchChanged(false, true); + checkNodeFetchChanged(false, false); + checkNodeFetchChanged(true, false); + } + + @Test + @DisplayName("Force codec default config") + void forceDefaultCodec() throws Exception { + log.info("Force codec default config"); + setupBrowser("chrome"); + this.forceCodecGenericE2eTest(); + } + + @Test + @DisplayName("Force valid codec - Not Allow Transcoding") + void forceValidCodecNotAllowTranscodingTest() throws Exception { + log.info("Force codec Chrome - Force VP8 - Not Allow Transcoding"); + setupBrowser("chrome"); + this.forceCodecGenericE2eTest(VideoCodec.VP8, false); + this.user.getDriver().close(); + + log.info("Force codec Chrome - Force H264 - Not Allow Transcoding"); + setupBrowser("chrome"); + this.forceCodecGenericE2eTest(VideoCodec.H264, false); + this.user.getDriver().close(); + } + + @Test + @DisplayName("Force valid codec - Allow Transcoding") + void forceValidCodecAllowTranscodingTest() throws Exception { + log.info("Force codec Chrome - Force VP8 - Allow Transcoding"); + setupBrowser("chrome"); + this.forceCodecGenericE2eTest(VideoCodec.VP8, true); + this.user.getDriver().close(); + + log.info("Force codec Chrome - Force H264 - Allow Transcoding"); + setupBrowser("chrome"); + this.forceCodecGenericE2eTest(VideoCodec.H264, true); + this.user.getDriver().close(); + } + + @Test + @DisplayName("Force not valid codec - Not Allow Transcoding") + void forceCodecNotValidCodecNotAllowTranscoding() throws Exception { + // Start firefox with OpenH264 disabled to check not supported codecs + log.info("Force codec Firefox - Force H264 - Allow Transcoding - Disabled H264 in Firefox"); + setupBrowser("firefoxDisabledOpenH264"); + this.forceNotSupportedCodec(VideoCodec.H264, false); + } + + @Test + @DisplayName("Force not valid codec - Allow Transcoding") + void forceCodecNotValidCodecAllowTranscoding() throws Exception { + // Start firefox with OpenH264 disabled to check not supported codecs + setupBrowser("firefoxDisabledOpenH264"); + log.info("Force codec Firefox - Force H264 - Allow Transcoding - Disabled H264 in Firefox"); + this.forceNotSupportedCodec(VideoCodec.H264, true); + } + + private void checkNodeFetchChanged(boolean global, boolean hasChanged) { + user.getDriver().findElement(By.id(global ? "list-sessions-btn" : "get-session-btn")).click(); + user.getWaiter().until(new NodeFetchHasChanged(hasChanged)); + } + + private class NodeFetchHasChanged implements ExpectedCondition { + + private boolean hasChanged; + + public NodeFetchHasChanged(boolean hasChanged) { + this.hasChanged = hasChanged; } - Assert.assertEquals("Size of recording entity (" + recording.getSessionId() - + ") is not equal to real file size (" + totalFileSize + ")", recording.getSize(), totalFileSize); - - jsonSyncFile.delete(); - } - - private void checkMultimediaFile(File file, boolean hasAudio, boolean hasVideo, double duration, String resolution, - String audioDecoder, String videoDecoder, boolean checkAudio) throws IOException { - // Check tracks, duration, resolution, framerate and decoders - MultimediaFileMetadata metadata = new MultimediaFileMetadata(file.getAbsolutePath()); - - if (hasVideo) { - if (checkAudio) { - if (hasAudio) { - Assert.assertTrue("Media file " + file.getAbsolutePath() + " should have audio", - metadata.hasAudio() && metadata.hasVideo()); - Assert.assertTrue(metadata.getAudioDecoder().toLowerCase().contains(audioDecoder)); - } else { - Assert.assertTrue("Media file " + file.getAbsolutePath() + " should have video", - metadata.hasVideo()); - Assert.assertFalse(metadata.hasAudio()); - } - } - if (resolution != null) { - Assert.assertEquals(resolution, metadata.getVideoWidth() + "x" + metadata.getVideoHeight()); - } - Assert.assertTrue(metadata.getVideoDecoder().toLowerCase().contains(videoDecoder)); - } else if (hasAudio && checkAudio) { - Assert.assertTrue(metadata.hasAudio()); - Assert.assertFalse(metadata.hasVideo()); - Assert.assertTrue(metadata.getAudioDecoder().toLowerCase().contains(audioDecoder)); - } else { - Assert.fail("Cannot check a file witho no audio and no video"); + @Override + public Boolean apply(WebDriver driver) { + return driver.findElement(By.id("api-response-text-area")).getAttribute("value") + .endsWith("Changes: " + hasChanged); } - // Check duration with 1 decimal precision - DecimalFormat df = new DecimalFormat("#0.0"); - df.setRoundingMode(RoundingMode.UP); - log.info("Duration of {} according to ffmpeg: {} s", file.getName(), metadata.getDuration()); - log.info("Duration of {} according to 'duration' property: {} s", file.getName(), duration); - log.info("Difference in s duration: {}", Math.abs(metadata.getDuration() - duration)); - final double difference = 10; - Assert.assertTrue( - "Difference between recording entity duration (" + duration + ") and real video duration (" - + metadata.getDuration() + ") is greater than " + difference + " in file " + file.getName(), - Math.abs((metadata.getDuration() - duration)) < difference); } - private boolean thumbnailIsFine(File file) { - boolean isFine = false; - BufferedImage image = null; - try { - image = ImageIO.read(file); - } catch (IOException e) { - log.error(e.getMessage()); - return false; - } - log.info("Recording thumbnail dimensions: {}x{}", image.getWidth(), image.getHeight()); - Map colorMap = this.averageColor(image); - log.info("Thumbnail map color: {}", colorMap.toString()); - isFine = this.checkVideoAverageRgbGreen(colorMap); - return isFine; + /** + * Test default config of forced codec and allowTranscoding + */ + private void forceCodecGenericE2eTest() throws Exception { + forceCodecGenericE2eTest(null, null); } - private Map averageColor(BufferedImage bi) { - int x0 = 0; - int y0 = 0; - int w = bi.getWidth(); - int h = bi.getHeight(); - int x1 = x0 + w; - int y1 = y0 + h; - long sumr = 0, sumg = 0, sumb = 0; - for (int x = x0; x < x1; x++) { - for (int y = y0; y < y1; y++) { - Color pixel = new Color(bi.getRGB(x, y)); - sumr += pixel.getRed(); - sumg += pixel.getGreen(); - sumb += pixel.getBlue(); - } - } - int num = w * h; - Map colorMap = new HashMap<>(); - colorMap.put("r", (long) (sumr / num)); - colorMap.put("g", (long) (sumg / num)); - colorMap.put("b", (long) (sumb / num)); - return colorMap; - } + /** + * Test to force specified codec and allowTranscoding + * + * @param codec codec to force. If null, default value in openvidu + * config will be used. + * @param allowTranscoding If true, allow transcoding. If null, default value in + * openvidu config will be used. + */ + private void forceCodecGenericE2eTest(VideoCodec codec, Boolean allowTranscoding) throws Exception { + CustomHttpClient restClient = new CustomHttpClient(OPENVIDU_URL, "OPENVIDUAPP", OPENVIDU_SECRET); + String sessionName = "CUSTOM_SESSION_" + ((codec != null) ? codec.name() : "DEFAULT_FORCE_CODEC"); - private void startKms() { - log.info("Starting KMS"); - commandLine.executeCommand("/usr/bin/kurento-media-server &>> /kms.log &"); - } + // Configure Session to force Codec + user.getDriver().findElement(By.id("add-user-btn")).click(); + WebElement sessionName1 = user.getDriver().findElement(By.id("session-name-input-0")); + sessionName1.clear(); + sessionName1.sendKeys(sessionName); + user.getDriver().findElement(By.id("session-settings-btn-0")).click(); + Thread.sleep(1000); - private void stopKms() { - log.info("Stopping KMS"); - commandLine.executeCommand("kill -9 $(pidof kurento-media-server)"); - } - - private void restartKms() { - this.stopKms(); - try { + if (codec != null) { + user.getDriver().findElement(By.id("forced-video-codec-select")).click(); Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); + user.getDriver().findElement(By.id("option-" + codec.name())).click(); } - this.startKms(); + if (allowTranscoding != null && allowTranscoding) { + user.getDriver().findElement(By.id("allow-transcoding-checkbox")).click(); + } + + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(1000); + + // Join Session + user.getDriver().findElement(By.id("add-user-btn")).click(); + WebElement sessionName2 = user.getDriver().findElement(By.id("session-name-input-1")); + sessionName2.clear(); + sessionName2.sendKeys(sessionName); + + user.getDriver().findElements(By.className("join-btn")).forEach(el -> el.sendKeys(Keys.ENTER)); + + user.getEventManager().waitUntilEventReaches("connectionCreated", 4); + user.getEventManager().waitUntilEventReaches("accessAllowed", 2); + user.getEventManager().waitUntilEventReaches("streamCreated", 4); + user.getEventManager().waitUntilEventReaches("streamPlaying", 4); + + // Load properties from session object of node-client + user.getDriver().findElement(By.id("session-info-btn-0")).click(); + JsonObject res = JsonParser + .parseString(user.getDriver().findElement(By.id("session-text-area")).getAttribute("value")) + .getAsJsonObject(); + VideoCodec sessionCodecNodeClient = VideoCodec + .valueOf(res.get("properties").getAsJsonObject().get("forcedVideoCodec").getAsString()); + boolean sessionAllowTranscodingNodeClient = res.get("properties").getAsJsonObject().get("allowTranscoding") + .getAsBoolean(); + user.getDriver().findElement(By.id("close-dialog-btn")).click(); + + final int numberOfVideos = user.getDriver().findElements(By.tagName("video")).size(); + Assert.assertEquals("Expected 4 videos but found " + numberOfVideos, 4, numberOfVideos); + Assert.assertTrue("Videos were expected to have audio and video tracks", user.getEventManager() + .assertMediaTracks(user.getDriver().findElements(By.tagName("video")), true, true)); + + // Assert Selected Codec in node-client session object + if (codec != null) { + // If specified codec, assert selected codec + Assert.assertEquals(sessionCodecNodeClient, codec); + } else { + // If not specified, assert default codec + Assert.assertEquals(sessionCodecNodeClient, defaultForcedVideoCodec); + } + + // Assert Selected allow transcoding in node-client session object + if (allowTranscoding != null) { + // If specified allowTranscoding, assert selected + Assert.assertEquals(sessionAllowTranscodingNodeClient, allowTranscoding); + } else { + // If not specified, assert default allowTranscoding + Assert.assertEquals(sessionAllowTranscodingNodeClient, defaultAllowTranscoding); + } + + // Check browser codecs + VideoCodec codecToCheck = (codec != null) ? codec : defaultForcedVideoCodec; + List statsButtons = user.getDriver().findElements(By.className("stats-button")); + for (WebElement statButton : statsButtons) { + statButton.click(); + Thread.sleep(1000); + String videoCodecUsed = user.getDriver().findElement(By.id("video-codec-used")).getText(); + Assert.assertEquals(videoCodecUsed, "video/" + codecToCheck); + user.getDriver().findElement(By.id("close-dialog-btn")).click(); + } + + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/" + sessionName, HttpStatus.SC_NO_CONTENT); } - private void checkDockerContainerRunning(String imageName, int amount) { - int number = Integer.parseInt(commandLine.executeCommand("docker ps | grep " + imageName + " | wc -l")); - Assert.assertEquals("Wrong number of Docker containers for image " + imageName + " running", amount, number); + /** + * Force codec not allowed by opened browser + * + * @throws Exception + */ + private void forceNotSupportedCodec(VideoCodec codec, boolean allowTranscoding) throws Exception { + CustomHttpClient restClient = new CustomHttpClient(OPENVIDU_URL, "OPENVIDUAPP", OPENVIDU_SECRET); + + String sessionName = "CUSTOM_SESSION_CODEC_NOT_SUPPORTED"; + + // Configure Session to force Codec + user.getDriver().findElement(By.id("add-user-btn")).click(); + WebElement sessionNameElem = user.getDriver().findElement(By.id("session-name-input-0")); + sessionNameElem.clear(); + sessionNameElem.sendKeys(sessionName); + user.getDriver().findElement(By.id("session-settings-btn-0")).click(); + Thread.sleep(1000); + + user.getDriver().findElement(By.id("forced-video-codec-select")).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.id("option-" + codec.name())).click(); + + if (allowTranscoding) { + user.getDriver().findElement(By.id("allow-transcoding-checkbox")).click(); + } + + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(1000); + + if (allowTranscoding) { + // If transcoding is enabled everything should work fine + + // Join another user + user.getDriver().findElement(By.id("add-user-btn")).click(); + WebElement sessionName2 = user.getDriver().findElement(By.id("session-name-input-1")); + sessionName2.clear(); + sessionName2.sendKeys(sessionName); + + List joinButtons = user.getDriver().findElements(By.className("join-btn")); + for (WebElement el : joinButtons) { + Thread.sleep(5000); + el.sendKeys(Keys.ENTER); + } + + user.getEventManager().waitUntilEventReaches("connectionCreated", 4); + user.getEventManager().waitUntilEventReaches("accessAllowed", 2); + user.getEventManager().waitUntilEventReaches("streamCreated", 4); + user.getEventManager().waitUntilEventReaches("streamPlaying", 4); + + final int numberOfVideos = user.getDriver().findElements(By.tagName("video")).size(); + Assert.assertEquals("Expected 4 videos but found " + numberOfVideos, 4, numberOfVideos); + Assert.assertTrue("Videos were expected to have audio and video tracks", user.getEventManager() + .assertMediaTracks(user.getDriver().findElements(By.tagName("video")), true, true)); + + } else { + // If transcoding not allowed it should return an alert with error + user.getDriver().findElements(By.className("join-btn")).forEach(el -> el.sendKeys(Keys.ENTER)); + user.getWaiter().until(ExpectedConditions.alertIsPresent()); + Alert alert = user.getDriver().switchTo().alert(); + Assert.assertTrue("Alert does not contain expected text", + alert.getText().contains("Error forcing codec: '" + codec.name() + "'")); + alert.accept(); + } + + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/" + sessionName, HttpStatus.SC_NO_CONTENT); + Thread.sleep(1000); } - private void removeAllRecordingContiners() { - commandLine.executeCommand("docker ps -a | awk '{ print $1,$2 }' | grep " + RECORDING_IMAGE - + " | awk '{print $1 }' | xargs -I {} docker rm -f {}"); + private void assertTranscodingSessionProperties(Session sessionDefaultCodec, Session sessionH264AllowTranscoding, + Session sessionVP9AllowTranscoding) { + // Check session with default transcoding params + Assert.assertEquals("Wrong default forcedVideoCodec", defaultForcedVideoCodec, + sessionDefaultCodec.getProperties().forcedVideoCodec()); + Assert.assertEquals("Wrong default allowTranscoding", defaultAllowTranscoding, + sessionDefaultCodec.getProperties().isTranscodingAllowed()); + + // Check session which use H264 and allow transcoding + Assert.assertEquals("Wrong default forcedVideoCodec", VideoCodec.H264, + sessionH264AllowTranscoding.getProperties().forcedVideoCodec()); + Assert.assertEquals("Wrong default allowTranscoding", true, + sessionH264AllowTranscoding.getProperties().isTranscodingAllowed()); + + // Check session which use VP9 and allow transcoding + Assert.assertEquals("Wrong default forcedVideoCodec", VideoCodec.VP9, + sessionVP9AllowTranscoding.getProperties().forcedVideoCodec()); + Assert.assertEquals("Wrong default allowTranscoding", true, + sessionVP9AllowTranscoding.getProperties().isTranscodingAllowed()); } } diff --git a/openvidu-testapp/docker/Dockerfile b/openvidu-testapp/docker/Dockerfile index 849344d3..da002e91 100644 --- a/openvidu-testapp/docker/Dockerfile +++ b/openvidu-testapp/docker/Dockerfile @@ -1,5 +1,5 @@ FROM ubuntu:16.04 -MAINTAINER openvidu@gmail.com +MAINTAINER info@openvidu.io # Install Kurento Media Server (KMS) RUN echo "deb [arch=amd64] http://ubuntu.openvidu.io/6.7.2 xenial kms6" | tee /etc/apt/sources.list.d/kurento.list \ diff --git a/openvidu-testapp/package.json b/openvidu-testapp/package.json index bf2e5bbe..4b450a9e 100644 --- a/openvidu-testapp/package.json +++ b/openvidu-testapp/package.json @@ -15,8 +15,8 @@ "colormap": "2.3.1", "core-js": "3.4.7", "hammerjs": "2.0.8", - "openvidu-browser": "2.15.0", - "openvidu-node-client": "2.11.0", + "openvidu-browser": "2.16.0", + "openvidu-node-client": "2.16.0", "rxjs": "6.5.3", "zone.js": "0.10.2" }, @@ -44,5 +44,5 @@ "start": "ng serve", "test": "ng test" }, - "version": "2.15.0" + "version": "2.16.0" } \ No newline at end of file diff --git a/openvidu-testapp/src/app/app.module.ts b/openvidu-testapp/src/app/app.module.ts index 419f888b..29dadb34 100644 --- a/openvidu-testapp/src/app/app.module.ts +++ b/openvidu-testapp/src/app/app.module.ts @@ -31,6 +31,7 @@ import { OpenviduRestService } from './services/openvidu-rest.service'; import { OpenviduParamsService } from './services/openvidu-params.service'; import { TestFeedService } from './services/test-feed.service'; import { MuteSubscribersService } from './services/mute-subscribers.service'; +import {SessionInfoDialogComponent} from "./components/dialogs/session-info-dialog/session-info-dialog.component"; @NgModule({ declarations: [ @@ -49,6 +50,8 @@ import { MuteSubscribersService } from './services/mute-subscribers.service'; PublisherPropertiesDialogComponent, ScenarioPropertiesDialogComponent, FilterDialogComponent, + ShowCodecDialogComponent, + SessionInfoDialogComponent, UsersTableComponent, TableVideoComponent, ShowCodecDialogComponent @@ -77,7 +80,8 @@ import { MuteSubscribersService } from './services/mute-subscribers.service'; PublisherPropertiesDialogComponent, ScenarioPropertiesDialogComponent, FilterDialogComponent, - ShowCodecDialogComponent + ShowCodecDialogComponent, + SessionInfoDialogComponent ], bootstrap: [AppComponent] }) diff --git a/openvidu-testapp/src/app/components/dialogs/filter-dialog/filter-dialog.component.html b/openvidu-testapp/src/app/components/dialogs/filter-dialog/filter-dialog.component.html index bd7a5f8d..23fa0dcd 100644 --- a/openvidu-testapp/src/app/components/dialogs/filter-dialog/filter-dialog.component.html +++ b/openvidu-testapp/src/app/components/dialogs/filter-dialog/filter-dialog.component.html @@ -5,19 +5,19 @@ - + - + - + - + @@ -34,7 +34,7 @@

    Filter events

    - + diff --git a/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.css b/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.css index 7df97a20..5ff30ddb 100644 --- a/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.css +++ b/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.css @@ -31,6 +31,7 @@ mat-dialog-content button { .inner-text-input { margin-left: 9px; + width: 30%; } #rec-properties-btn { diff --git a/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.html b/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.html index cc50ca9d..ddd9bf27 100644 --- a/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.html +++ b/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.html @@ -6,26 +6,55 @@ -
    - - - -
    - -
    + + + + + + + + +
    + Record + + + + + {{ enumerator }} + + + + + + +
    +
    + + + + + +
    + -
    + + + @@ -37,7 +66,9 @@
    -
    +
    @@ -45,17 +76,19 @@ - +
    Has audio Has video - - + +
    @@ -63,8 +96,10 @@
    - - + +
    @@ -73,6 +108,7 @@ - +
    diff --git a/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.ts b/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.ts index f1acab0e..d5890f95 100644 --- a/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.ts +++ b/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.ts @@ -1,7 +1,7 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; -import { OpenVidu as OpenViduAPI, Session as SessionAPI, Recording, RecordingProperties, RecordingLayout } from 'openvidu-node-client'; +import { OpenVidu as OpenViduAPI, Session as SessionAPI, Recording, RecordingProperties, RecordingLayout, ConnectionProperties, OpenViduRole } from 'openvidu-node-client'; @Component({ selector: 'app-session-api-dialog', @@ -14,16 +14,24 @@ export class SessionApiDialogComponent { session: SessionAPI; sessionId: string; recordingId: string; - resourceId: string; + connectionId: string; + streamId: string; response: string; recordingProperties: RecordingProperties; recMode = Recording.OutputMode; recLayouts = RecordingLayout; + openviduRoles = OpenViduRole; customLayout = ''; recPropertiesIcon = 'add_circle'; showRecProperties = false; + connectionProperties: ConnectionProperties = { + record: true, + role: OpenViduRole.PUBLISHER, + data: '' + }; + constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data) { this.OV = data.openVidu; @@ -50,7 +58,18 @@ export class SessionApiDialogComponent { startRecording() { console.log('Starting recording'); - this.OV.startRecording(this.sessionId, this.recordingProperties) + const finalRecordingProperties = { + name: this.recordingProperties.name, + outputMode: this.recordingProperties.outputMode, + recordingLayout: this.recordingProperties.recordingLayout, + customLayout: this.recordingProperties.customLayout, + resolution: this.recordingProperties.resolution, + hasAudio: this.recordingProperties.hasAudio, + hasVideo: this.recordingProperties.hasVideo, + shmSize: this.recordingProperties.shmSize, + mediaNode: !this.recordingProperties.mediaNode.id ? undefined : this.recordingProperties.mediaNode + } + this.OV.startRecording(this.sessionId, finalRecordingProperties) .then(recording => { this.response = 'Recording started [' + recording.id + ']'; }) @@ -145,7 +164,7 @@ export class SessionApiDialogComponent { forceDisconnect() { console.log('Forcing disconnect'); - this.session.forceDisconnect(this.resourceId) + this.session.forceDisconnect(this.connectionId) .then(() => { this.response = 'User disconnected'; }) @@ -156,7 +175,7 @@ export class SessionApiDialogComponent { forceUnpublish() { console.log('Forcing unpublish'); - this.session.forceUnpublish(this.resourceId) + this.session.forceUnpublish(this.streamId) .then(() => { this.response = 'Stream unpublished'; }) @@ -165,6 +184,32 @@ export class SessionApiDialogComponent { }); } + createConnection() { + console.log('Creating connection'); + this.session.createConnection(this.connectionProperties) + .then(connection => { + this.response = 'Connection created: ' + JSON.stringify(connection); + }) + .catch(error => { + this.response = 'Error [' + error.message + ']'; + }); + } + + updateConnection() { + console.log('Updating connection'); + this.session.updateConnection(this.connectionId, this.connectionProperties) + .then(modifiedConnection => { + this.response = 'Connection updated: ' + JSON.stringify({ + role: modifiedConnection.connectionProperties.role, + record: modifiedConnection.connectionProperties.record, + data: modifiedConnection.connectionProperties.data + }); + }) + .catch(error => { + this.response = 'Error [' + error.message + ']'; + }); + } + enumToArray(enumerator: any) { return Object.keys(enumerator); } diff --git a/openvidu-testapp/src/app/components/dialogs/session-info-dialog/session-info-dialog.component.ts b/openvidu-testapp/src/app/components/dialogs/session-info-dialog/session-info-dialog.component.ts new file mode 100644 index 00000000..8c166ea7 --- /dev/null +++ b/openvidu-testapp/src/app/components/dialogs/session-info-dialog/session-info-dialog.component.ts @@ -0,0 +1,38 @@ +import {Component, Inject} from "@angular/core"; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; +import {Session as SessionAPI} from "openvidu-node-client"; + +@Component({ + selector: 'app-session-info-dialog', + template: ` +
    + + + +
    +
    + +
    + `, + styles: [` + #app-session-info-dialog-container { + text-align: center + } + #response-text-area { + width: 100%; + } + `] +}) +export class SessionInfoDialogComponent { + + sessionAPIContent: string; + + constructor(public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data) { + const sessionApi = data.sessionAPI; + delete sessionApi.ov; + this.sessionAPIContent = JSON.stringify(sessionApi, null, 4); + } + + +} diff --git a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.css b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.css index 782565c8..d151819b 100644 --- a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.css +++ b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.css @@ -39,5 +39,9 @@ mat-radio-button:first-child { } #allow-transcoding-div { - margin-bottom: 10px; -} \ No newline at end of file + margin-bottom: 10px; +} + +#record-div { + padding-bottom: 20px; +} diff --git a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.html b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.html index e9eb8de6..6b06c0fa 100644 --- a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.html +++ b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.html @@ -27,16 +27,29 @@
    - + - {{ enumerator }} + {{ enumerator }} + [(ngModel)]="sessionProperties.defaultCustomLayout" id="default-custom-layout-input"> + +
    + Allow Transcoding +
    + + + + {{ enumerator }} + +
    - + SUB PUB MOD @@ -90,19 +103,19 @@
    + [(ngModel)]="connectionProperties.kurentoOptions.videoMaxRecvBandwidth"> + [(ngModel)]="connectionProperties.kurentoOptions.videoMinRecvBandwidth"> + [(ngModel)]="connectionProperties.kurentoOptions.videoMaxSendBandwidth"> + [(ngModel)]="connectionProperties.kurentoOptions.videoMinSendBandwidth">
    +
    + Record + +
    +
    + +
    + Force + publishing + +
    +
    @@ -131,6 +156,6 @@ + [mat-dialog-close]="{sessionProperties: sessionProperties, turnConf: turnConf, manualTurnConf: manualTurnConf, customToken: customToken, forcePublishing: forcePublishing, connectionProperties: generateConnectionProperties()}">SAVE diff --git a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.ts b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.ts index f06f3201..964d4f89 100644 --- a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.ts +++ b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.ts @@ -1,7 +1,7 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; -import { SessionProperties, MediaMode, Recording, RecordingMode, RecordingLayout, TokenOptions, VideoCodec } from 'openvidu-node-client'; +import { SessionProperties, MediaMode, Recording, RecordingMode, RecordingLayout, ConnectionProperties, VideoCodec } from 'openvidu-node-client'; @Component({ selector: 'app-session-properties-dialog', @@ -14,7 +14,9 @@ export class SessionPropertiesDialogComponent { turnConf: string; manualTurnConf: RTCIceServer = { urls: [] }; customToken: string; - tokenOptions: TokenOptions; + forcePublishing: boolean = false; + connectionProperties: ConnectionProperties; + forceVideoCodec = VideoCodec; filterName = 'GStreamerFilter'; filters: string[] = []; @@ -30,17 +32,18 @@ export class SessionPropertiesDialogComponent { this.sessionProperties = data.sessionProperties; this.turnConf = data.turnConf; this.manualTurnConf = data.manualTurnConf; - this.tokenOptions = data.tokenOptions; this.customToken = data.customToken; + this.forcePublishing = data.forcePublishing; + this.connectionProperties = data.connectionProperties; } enumToArray(enumerator: any) { return Object.keys(enumerator); } - generateTokenOptions(): TokenOptions { - this.tokenOptions.kurentoOptions.allowedFilters = this.filters; - return this.tokenOptions; + generateConnectionProperties(): ConnectionProperties { + this.connectionProperties.kurentoOptions.allowedFilters = this.filters; + return this.connectionProperties; } } diff --git a/openvidu-testapp/src/app/components/dialogs/show-codec-dialog/show-codec-dialog.component.ts b/openvidu-testapp/src/app/components/dialogs/show-codec-dialog/show-codec-dialog.component.ts index c5b9678a..80b5f7a8 100644 --- a/openvidu-testapp/src/app/components/dialogs/show-codec-dialog/show-codec-dialog.component.ts +++ b/openvidu-testapp/src/app/components/dialogs/show-codec-dialog/show-codec-dialog.component.ts @@ -2,26 +2,25 @@ import { Component, Inject } from '@angular/core'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; @Component({ - selector: 'app-codec-used-dialog', - template: ` + selector: 'app-codec-used-dialog', + template: `

    Used Codec: {{usedVideoCodec}}

    -
    `, - styles: [` + styles: [` #app-codec-dialog-container { text-align: center } `] }) export class ShowCodecDialogComponent { - - usedVideoCodec; - constructor(public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data) { - this.usedVideoCodec = data.usedVideoCodec; - } -} \ No newline at end of file + usedVideoCodec; + + constructor(public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data) { + this.usedVideoCodec = data.usedVideoCodec; + } +} diff --git a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html index 8014c7ea..7bef7a03 100644 --- a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html +++ b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html @@ -18,26 +18,29 @@
    - - +
    -
    - + Publish
    @@ -48,26 +51,32 @@

    Send

    - Audio - Video + Audio + + Video +

    Enter active

    - Audio - Video + Audio + Video
    - +
    Video
    @@ -75,15 +84,16 @@ Screen
    -