From 42a4feb1d99dc27505c79b16ad1576725560800e Mon Sep 17 00:00:00 2001 From: pabloFuente Date: Sun, 22 Jul 2018 22:13:45 +0200 Subject: [PATCH] openvidu-node-client: support 2.3.0 REST API --- openvidu-node-client/src/Connection.ts | 77 +++++ openvidu-node-client/src/MediaMode.ts | 3 + openvidu-node-client/src/OpenVidu.ts | 188 ++++++++++-- openvidu-node-client/src/OpenViduRole.ts | 3 + openvidu-node-client/src/Publisher.ts | 80 +++++ openvidu-node-client/src/Recording.ts | 12 +- openvidu-node-client/src/RecordingLayout.ts | 3 + openvidu-node-client/src/RecordingMode.ts | 3 + .../src/RecordingProperties.ts | 6 +- openvidu-node-client/src/Session.ts | 279 ++++++++++++++++-- openvidu-node-client/src/SessionProperties.ts | 6 + openvidu-node-client/src/TokenOptions.ts | 3 + openvidu-node-client/src/index.ts | 4 +- 13 files changed, 617 insertions(+), 50 deletions(-) create mode 100644 openvidu-node-client/src/Connection.ts create mode 100644 openvidu-node-client/src/Publisher.ts diff --git a/openvidu-node-client/src/Connection.ts b/openvidu-node-client/src/Connection.ts new file mode 100644 index 00000000..39240ce6 --- /dev/null +++ b/openvidu-node-client/src/Connection.ts @@ -0,0 +1,77 @@ +/* + * (C) Copyright 2017-2018 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 { OpenViduRole } from './OpenViduRole'; +import { Publisher } from './Publisher'; + +/** + * See [[Session.activeConnections]] + */ +export class Connection { + + /** + * Identifier of the connection. You can call [[Session.forceDisconnect]] passing this property as parameter + */ + connectionId: string; + + /** + * Role of the connection + */ + role: OpenViduRole; + + /** + * Token associated to the connection + */ + token: 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 + * [Session.connect](/api/openvidu-browser/classes/session.html#connect) in OpenVidu Browser + */ + clientData: 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 + */ + publishers: Publisher[] = []; + + /** + * Array of streams (their `streamId` properties) this particular Connection is subscribed to. Each one always corresponds to one + * Publisher of some other Connection: each string of this array must be equal to one [[Publisher.streamId]] of other Connection + */ + subscribers: string[] = []; + + /** + * @hidden + */ + constructor(connectionId: string, role: OpenViduRole, token: string, serverData: string, clientData: string, + publishers: Publisher[], subscribers: string[]) { + this.connectionId = connectionId; + this.role = role; + this.token = token; + this.serverData = serverData; + this.clientData = clientData; + this.publishers = publishers; + this.subscribers = subscribers; + } +} \ No newline at end of file diff --git a/openvidu-node-client/src/MediaMode.ts b/openvidu-node-client/src/MediaMode.ts index c9eee488..319fc57d 100644 --- a/openvidu-node-client/src/MediaMode.ts +++ b/openvidu-node-client/src/MediaMode.ts @@ -15,6 +15,9 @@ * */ +/** + * See [[SessionProperties.mediaMode]] + */ export enum MediaMode { /** diff --git a/openvidu-node-client/src/OpenVidu.ts b/openvidu-node-client/src/OpenVidu.ts index 3e71f0a7..39a1e5b8 100644 --- a/openvidu-node-client/src/OpenVidu.ts +++ b/openvidu-node-client/src/OpenVidu.ts @@ -24,34 +24,82 @@ import axios from 'axios'; export class OpenVidu { - private static readonly API_RECORDINGS: string = '/api/recordings'; - private static readonly API_RECORDINGS_START: string = '/start'; - private static readonly API_RECORDINGS_STOP: string = '/stop'; - - private hostname: string; - private port: number; - private basicAuth: string; private Buffer = require('buffer/').Buffer; + /** + * @hidden + */ + static hostname: string; + /** + * @hidden + */ + static port: number; + /** + * @hidden + */ + static basicAuth: string; + + /** + * @hidden + */ + static readonly API_RECORDINGS: string = '/api/recordings'; + /** + * @hidden + */ + static readonly API_RECORDINGS_START: string = '/start'; + /** + * @hidden + */ + static readonly API_RECORDINGS_STOP: string = '/stop'; + /** + * @hidden + */ + static readonly API_SESSIONS = '/api/sessions'; + /** + * @hidden + */ + static readonly API_TOKENS = '/api/tokens'; + + private static o: OpenVidu; + + + /** + * Array of active sessions. **This value will remain unchanged since the last time method [[OpenVidu.fetch]] + * was called**. Exceptions to this rule are: + * + * - 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 + * - Calling [[Session.forceUnpublish]] also automatically updates the inner affected connections for that specific Session + * - Calling [[OpenVidu.startRecording]] and [[OpenVidu.stopRecording]] automatically updates the recording status of the + * Session ([[Session.recording]]) + * + * To get the array of active sessions with their current actual value, you must call [[OpenVidu.fetch]] before consulting + * property [[activeSessions]] + */ + activeSessions: Session[] = []; + /** * @param urlOpenViduServer Public accessible IP where your instance of OpenVidu Server is up an running * @param secret Secret used on OpenVidu Server initialization */ constructor(private urlOpenViduServer: string, secret: string) { this.setHostnameAndPort(); - this.basicAuth = this.getBasicAuth(secret); + OpenVidu.basicAuth = this.getBasicAuth(secret); + OpenVidu.o = this; } /** - * Creates an OpenVidu session. You can call [[Session.getSessionId]] in the resolved promise to retrieve the `sessionId` + * Creates an OpenVidu session. You can call [[Session.getSessionId]] inside the resolved promise to retrieve the `sessionId` * * @returns A Promise that is resolved to the [[Session]] if success and rejected with an Error object if not. */ public createSession(properties?: SessionProperties): Promise { return new Promise((resolve, reject) => { - const session = new Session(this.hostname, this.port, this.basicAuth, properties); + const session = new Session(properties); session.getSessionIdHttp() .then(sessionId => { + this.activeSessions.push(session); resolve(session); }) .catch(error => { @@ -69,6 +117,7 @@ export class OpenVidu { * * @param sessionId The `sessionId` of the [[Session]] you want to start recording * @param name The name you want to give to the video file. You can access this same value in your clients on recording events (`recordingStarted`, `recordingStopped`) + * **WARNING: this parameter follows an overwriting policy.** If you name two recordings the same, the newest MP4 file will overwrite the oldest one * * @returns A Promise that is resolved to the [[Recording]] if it successfully started (the recording can be stopped with guarantees) and rejected with an Error object if not. This Error object has as `message` property with the following values: * - `404`: no session exists for the passed `sessionId` @@ -108,11 +157,11 @@ export class OpenVidu { } axios.post( - 'https://' + this.hostname + ':' + this.port + OpenVidu.API_RECORDINGS + OpenVidu.API_RECORDINGS_START, + 'https://' + OpenVidu.hostname + ':' + OpenVidu.port + OpenVidu.API_RECORDINGS + OpenVidu.API_RECORDINGS_START, data, { headers: { - 'Authorization': this.basicAuth, + 'Authorization': OpenVidu.basicAuth, 'Content-Type': 'application/json' } } @@ -120,7 +169,9 @@ export class OpenVidu { .then(res => { if (res.status === 200) { // SUCCESS response from openvidu-server (Recording in JSON format). Resolve new Recording - resolve(new Recording(res.data)); + const r: Recording = new Recording(res.data); + this.activeSessions.find(s => s.sessionId === r.sessionId).recording = true; + resolve(r); } else { // ERROR response from openvidu-server. Resolve HTTP status reject(new Error(res.status.toString())); @@ -155,11 +206,11 @@ export class OpenVidu { return new Promise((resolve, reject) => { axios.post( - 'https://' + this.hostname + ':' + this.port + OpenVidu.API_RECORDINGS + OpenVidu.API_RECORDINGS_STOP + '/' + recordingId, + 'https://' + OpenVidu.hostname + ':' + OpenVidu.port + OpenVidu.API_RECORDINGS + OpenVidu.API_RECORDINGS_STOP + '/' + recordingId, undefined, { headers: { - 'Authorization': this.basicAuth, + 'Authorization': OpenVidu.basicAuth, 'Content-Type': 'application/x-www-form-urlencoded' } } @@ -167,7 +218,9 @@ export class OpenVidu { .then(res => { if (res.status === 200) { // SUCCESS response from openvidu-server (Recording in JSON format). Resolve new Recording - resolve(new Recording(res.data)); + const r: Recording = new Recording(res.data); + this.activeSessions.find(s => s.sessionId === r.sessionId).recording = false; + resolve(r); } else { // ERROR response from openvidu-server. Resolve HTTP status reject(new Error(res.status.toString())); @@ -177,9 +230,8 @@ export class OpenVidu { // 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 + // 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); } else { // Something happened in setting up the request that triggered an Error @@ -201,10 +253,10 @@ export class OpenVidu { return new Promise((resolve, reject) => { axios.get( - 'https://' + this.hostname + ':' + this.port + OpenVidu.API_RECORDINGS + '/' + recordingId, + 'https://' + OpenVidu.hostname + ':' + OpenVidu.port + OpenVidu.API_RECORDINGS + '/' + recordingId, { headers: { - 'Authorization': this.basicAuth, + 'Authorization': OpenVidu.basicAuth, 'Content-Type': 'application/x-www-form-urlencoded' } } @@ -243,11 +295,10 @@ export class OpenVidu { return new Promise((resolve, reject) => { axios.get( - 'https://' + this.hostname + ':' + this.port + OpenVidu.API_RECORDINGS, + 'https://' + OpenVidu.hostname + ':' + OpenVidu.port + OpenVidu.API_RECORDINGS, { headers: { - 'Authorization': this.basicAuth, - 'Content-Type': 'application/x-www-form-urlencoded' + Authorization: OpenVidu.basicAuth } } ) @@ -294,10 +345,10 @@ export class OpenVidu { return new Promise((resolve, reject) => { axios.delete( - 'https://' + this.hostname + ':' + this.port + OpenVidu.API_RECORDINGS + '/' + recordingId, + 'https://' + OpenVidu.hostname + ':' + OpenVidu.port + OpenVidu.API_RECORDINGS + '/' + recordingId, { headers: { - 'Authorization': this.basicAuth, + 'Authorization': OpenVidu.basicAuth, 'Content-Type': 'application/x-www-form-urlencoded' } } @@ -327,6 +378,76 @@ export class OpenVidu { }); } + /** + * Updates every property of every active Session with the current status they have in OpenVidu Server. + * After calling this method you can access the updated array of active sessions in [[activeSessions]] + * + * @returns A promise resolved to true if any Session status has changed with respect to the server, or to false if not. + * This applies to any property or sub-property of any of the sessions locally stored in OpenVidu Node Client + */ + public fetch(): Promise { + return new Promise((resolve, reject) => { + axios.get( + 'https://' + OpenVidu.hostname + ':' + OpenVidu.port + OpenVidu.API_SESSIONS, + { + headers: { + Authorization: OpenVidu.basicAuth + } + } + ) + .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 storedSession = this.activeSessions.find(s => s.sessionId === session.sessionId); + if (!!storedSession) { + const beforeJSON: string = JSON.stringify(storedSession); + storedSession = storedSession.resetSessionWithJson(session); + const afterJSON: string = JSON.stringify(storedSession); + const changed: boolean = !(beforeJSON === afterJSON); + console.log("Available session '" + storedSession.sessionId + "' info fetched. Any change: " + changed); + hasChanged = hasChanged || changed; + } else { + this.activeSessions.push(new Session(session)); + console.log("New session '" + session.sessionId + "' info fetched"); + hasChanged = true; + } + + // Remove closed sessions from activeSessions array + this.activeSessions = this.activeSessions.filter(session => (!!fetchedSessionIds.find(sId => sId === session.sessionId))); + console.log('Active sessions info fetched: ', fetchedSessionIds); + }); + if (res.data.content.length === 0) { + console.warn('No active sessions'); + } + resolve(hasChanged); + } 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); + } else { + // Something happened in setting up the request that triggered an Error + console.error('Error', error.message); + } + }); + }); + } + private getBasicAuth(secret: string): string { return 'Basic ' + this.Buffer('OPENVIDUAPP:' + secret).toString('base64'); } @@ -334,14 +455,21 @@ export class OpenVidu { private setHostnameAndPort(): void { const urlSplitted = this.urlOpenViduServer.split(':'); if (urlSplitted.length === 3) { // URL has format: http:// + hostname + :port - this.hostname = this.urlOpenViduServer.split(':')[1].replace(/\//g, ''); - this.port = parseInt(this.urlOpenViduServer.split(':')[2].replace(/\//g, '')); + OpenVidu.hostname = this.urlOpenViduServer.split(':')[1].replace(/\//g, ''); + OpenVidu.port = parseInt(this.urlOpenViduServer.split(':')[2].replace(/\//g, '')); } else if (urlSplitted.length === 2) { // URL has format: hostname + :port - this.hostname = this.urlOpenViduServer.split(':')[0].replace(/\//g, ''); - this.port = parseInt(this.urlOpenViduServer.split(':')[1].replace(/\//g, '')); + OpenVidu.hostname = this.urlOpenViduServer.split(':')[0].replace(/\//g, ''); + OpenVidu.port = parseInt(this.urlOpenViduServer.split(':')[1].replace(/\//g, '')); } else { console.error("URL format incorrect: it must contain hostname and port (current value: '" + this.urlOpenViduServer + "')"); } } + /** + * @hidden + */ + static getActiveSessions(): Session[] { + return this.o.activeSessions; + } + } \ No newline at end of file diff --git a/openvidu-node-client/src/OpenViduRole.ts b/openvidu-node-client/src/OpenViduRole.ts index 4c0e2091..8b821d11 100644 --- a/openvidu-node-client/src/OpenViduRole.ts +++ b/openvidu-node-client/src/OpenViduRole.ts @@ -15,6 +15,9 @@ * */ +/** + * See [[TokenOptions.role]] + */ export enum OpenViduRole { /** diff --git a/openvidu-node-client/src/Publisher.ts b/openvidu-node-client/src/Publisher.ts new file mode 100644 index 00000000..a7233377 --- /dev/null +++ b/openvidu-node-client/src/Publisher.ts @@ -0,0 +1,80 @@ +/* + * (C) Copyright 2017-2018 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 { OpenViduRole } from './OpenViduRole'; + +/** + * See [[Connection.publishers]] + * + * This is a backend representation of a published media stream (see [OpenVidu Browser Stream class](/api/openvidu-browser/classes/stream.html)) + */ +export class Publisher { + + /** + * Unique identifier of the [Stream](/api/openvidu-browser/classes/stream.html) associated to this Publisher. + * Each Publisher is paired with only one Stream, so you can identify each Publisher by its + * [`Stream.streamId`](/api/openvidu-browser/classes/stream.html#streamid) + */ + streamId: string; + + /** + * See properties of [Stream](/api/openvidu-browser/classes/stream.html) object in OpenVidu Browser library to find out more + */ + hasAudio: boolean; + + /** + * See properties of [Stream](/api/openvidu-browser/classes/stream.html) object in OpenVidu Browser library to find out more + */ + hasVideo: boolean; + + /** + * See properties of [Stream](/api/openvidu-browser/classes/stream.html) object in OpenVidu Browser library to find out more + */ + audioActive: boolean; + + /** + * See properties of [Stream](/api/openvidu-browser/classes/stream.html) object in OpenVidu Browser library to find out more + */ + videoActive: boolean; + + /** + * See properties of [Stream](/api/openvidu-browser/classes/stream.html) object in OpenVidu Browser library to find out more + */ + frameRate: number; + + /** + * See properties of [Stream](/api/openvidu-browser/classes/stream.html) object in OpenVidu Browser library to find out more + */ + typeOfVideo: string; + + /** + * See properties of [Stream](/api/openvidu-browser/classes/stream.html) object in OpenVidu Browser library to find out more + */ + videoDimensions: string; + + constructor(json) { + this.streamId = json.streamId; + this.hasAudio = json.mediaOptions.hasAudio; + this.hasVideo = json.mediaOptions.hasVideo; + this.audioActive = json.mediaOptions.audioActive; + this.videoActive = json.mediaOptions.videoActive; + this.frameRate = json.mediaOptions.frameRate; + this.typeOfVideo = json.mediaOptions.typeOfVideo; + this.videoDimensions = json.mediaOptions.videoDimensions; + } + +} \ No newline at end of file diff --git a/openvidu-node-client/src/Recording.ts b/openvidu-node-client/src/Recording.ts index eea39b23..bb605348 100644 --- a/openvidu-node-client/src/Recording.ts +++ b/openvidu-node-client/src/Recording.ts @@ -17,6 +17,9 @@ import { RecordingLayout } from './RecordingLayout'; +/** + * See [[OpenVidu.startRecording]] + */ export class Recording { /** @@ -77,6 +80,9 @@ export class Recording { recordingLayout: RecordingLayout; /* tslint:disable:no-string-literal */ + /** + * @hidden + */ constructor(json: JSON) { this.id = json['id']; this.sessionId = json['sessionId']; @@ -94,6 +100,10 @@ export class Recording { } export namespace Recording { + + /** + * See [[Recording.status]] + */ export enum Status { /** @@ -114,7 +124,7 @@ export namespace Recording { /** * The recording is available for downloading. This status is reached for all * stopped recordings if [OpenVidu Server configuration](https://openvidu.io/docs/reference-docs/openvidu-server-params/) - * property openvidu.recording.public-access is true + * property `openvidu.recording.public-access` is true */ available, diff --git a/openvidu-node-client/src/RecordingLayout.ts b/openvidu-node-client/src/RecordingLayout.ts index c22e8a17..d62801ca 100644 --- a/openvidu-node-client/src/RecordingLayout.ts +++ b/openvidu-node-client/src/RecordingLayout.ts @@ -15,6 +15,9 @@ * */ +/** + * See [[SessionProperties.defaultRecordingLayout]] and [[RecordingProperties.recordingLayout]] + */ export enum RecordingLayout { /** diff --git a/openvidu-node-client/src/RecordingMode.ts b/openvidu-node-client/src/RecordingMode.ts index 8faa14d0..178188f9 100644 --- a/openvidu-node-client/src/RecordingMode.ts +++ b/openvidu-node-client/src/RecordingMode.ts @@ -15,6 +15,9 @@ * */ +/** + * See [[SessionProperties.recordingMode]] + */ export enum RecordingMode { /** diff --git a/openvidu-node-client/src/RecordingProperties.ts b/openvidu-node-client/src/RecordingProperties.ts index 6662c253..247e4c60 100644 --- a/openvidu-node-client/src/RecordingProperties.ts +++ b/openvidu-node-client/src/RecordingProperties.ts @@ -17,10 +17,14 @@ import { RecordingLayout } from './RecordingLayout'; +/** + * See [[OpenVidu.startRecording]] + */ export interface RecordingProperties { /** - * The name you want to give to the video file. You can access this same value in your clients on recording events (`recordingStarted`, `recordingStopped`) + * The name you want to give to the video file. You can access this same value in your clients on recording events (`recordingStarted`, `recordingStopped`). + * **WARNING: this parameter follows an overwriting policy.** If you name two recordings the same, the newest MP4 file will overwrite the oldest one */ name?: string; diff --git a/openvidu-node-client/src/Session.ts b/openvidu-node-client/src/Session.ts index 303ed086..2b989aaa 100644 --- a/openvidu-node-client/src/Session.ts +++ b/openvidu-node-client/src/Session.ts @@ -15,8 +15,11 @@ * */ +import { Connection } from './Connection'; import { MediaMode } from './MediaMode'; +import { OpenVidu } from './OpenVidu'; import { OpenViduRole } from './OpenViduRole'; +import { Publisher } from './Publisher'; import { RecordingLayout } from './RecordingLayout'; import { RecordingMode } from './RecordingMode'; import { SessionProperties } from './SessionProperties'; @@ -26,20 +29,53 @@ import axios from 'axios'; export class Session { - private static readonly API_SESSIONS = '/api/sessions'; - private static readonly API_TOKENS = '/api/tokens'; - + /** + * Unique identifier of the Session + */ sessionId: string; + + /** + * Properties defining the session + */ properties: SessionProperties; - private Buffer = require('buffer/').Buffer; + /** + * 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: + * + * - Calling [[Session.forceUnpublish]] also automatically updates each affected Connection status + * - Calling [[Session.forceDisconnect]] automatically updates each affected Connection status + * + * To get the array of active connections with their current actual value, you must call [[Session.fetch]] before consulting + * property [[activeConnections]] + */ + activeConnections: Connection[] = []; - constructor(private hostname: string, private port: number, private basicAuth: string, properties?: SessionProperties) { - if (!properties) { - this.properties = {}; + /** + * Whether the session is being recorded or not + */ + recording = false; + + /** + * @hidden + */ + constructor(propertiesOrJson?) { + if (!!propertiesOrJson) { + // Defined parameter + if (!!propertiesOrJson.sessionId) { + // Parameter is a JSON representation of Session ('sessionId' property always defined) + this.resetSessionWithJson(propertiesOrJson); + } else { + // Parameter is a SessionProperties object + this.properties = propertiesOrJson; + } } else { - this.properties = properties; + // Empty parameter + this.properties = {}; } + this.properties.mediaMode = !!this.properties.mediaMode ? this.properties.mediaMode : MediaMode.ROUTED; + this.properties.recordingMode = !!this.properties.recordingMode ? this.properties.recordingMode : RecordingMode.MANUAL; + this.properties.defaultRecordingLayout = !!this.properties.defaultRecordingLayout ? this.properties.defaultRecordingLayout : RecordingLayout.BEST_FIT; } /** @@ -64,11 +100,11 @@ export class Session { }); axios.post( - 'https://' + this.hostname + ':' + this.port + Session.API_TOKENS, + 'https://' + OpenVidu.hostname + ':' + OpenVidu.port + OpenVidu.API_TOKENS, data, { headers: { - 'Authorization': this.basicAuth, + 'Authorization': OpenVidu.basicAuth, 'Content-Type': 'application/json' } } @@ -101,15 +137,15 @@ export class Session { /** * Gracefully closes the Session: unpublishes all streams and evicts every participant * - * @returns A Promise that is resolved to the if the session has been closed successfully and rejected with an Error object if not + * @returns A Promise that is resolved if the session has been closed successfully and rejected with an Error object if not */ - public close() { - return new Promise((resolve, reject) => { + public close(): Promise { + return new Promise((resolve, reject) => { axios.delete( - 'https://' + this.hostname + ':' + this.port + Session.API_SESSIONS + '/' + this.sessionId, + 'https://' + OpenVidu.hostname + ':' + OpenVidu.port + OpenVidu.API_SESSIONS + '/' + this.sessionId, { headers: { - 'Authorization': this.basicAuth, + 'Authorization': OpenVidu.basicAuth, 'Content-Type': 'application/x-www-form-urlencoded' } } @@ -117,6 +153,176 @@ export class Session { .then(res => { if (res.status === 204) { // SUCCESS response from openvidu-server + const indexToRemove: number = OpenVidu.getActiveSessions().findIndex(s => s.sessionId === this.sessionId); + OpenVidu.getActiveSessions().splice(indexToRemove, 1); + resolve(); + } 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); + } else { + // Something happened in setting up the request that triggered an Error + console.error('Error', error.message); + } + }); + }); + } + + /** + * 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 to the Session ([[Session.activeConnections]]) and use those values to call [[Session.forceDisconnect]] or [[Session.forceUnpublish]] + * + * @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 + */ + public fetch(): Promise { + return new Promise((resolve, reject) => { + const beforeJSON: string = JSON.stringify(this); + axios.get( + 'https://' + OpenVidu.hostname + ':' + OpenVidu.port + OpenVidu.API_SESSIONS + '/' + this.sessionId, + { + headers: { + 'Authorization': OpenVidu.basicAuth, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ) + .then(res => { + if (res.status === 200) { + // SUCCESS response from openvidu-server + this.resetSessionWithJson(res.data); + const afterJSON: string = JSON.stringify(this); + const hasChanged: boolean = !(beforeJSON === afterJSON); + console.log("Session info fetched for session '" + this.sessionId + "'. Any change: " + hasChanged); + resolve(hasChanged); + } 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); + } else { + // Something happened in setting up the request that triggered an Error + console.error('Error', error.message); + } + }); + }); + } + + /** + * 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 `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 + * + * @returns A Promise that is resolved if the user was successfully disconnected and rejected with an Error object if not + */ + public forceDisconnect(connection: string | Connection): Promise { + return new Promise((resolve, reject) => { + const connectionId: string = typeof connection === 'string' ? connection : (connection).connectionId; + axios.delete( + 'https://' + OpenVidu.hostname + ':' + OpenVidu.port + OpenVidu.API_SESSIONS + '/' + this.sessionId + '/connection/' + connectionId, + { + headers: { + 'Authorization': OpenVidu.basicAuth, + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + .then(res => { + if (res.status === 204) { + // SUCCESS response from openvidu-server + // Remove connection from activeConnections array + let connectionClosed; + this.activeConnections = this.activeConnections.filter(con => { + if (con.connectionId !== connectionId) { + return true; + } else { + connectionClosed = con; + return false; + } + }); + // Remove every Publisher of the closed connection from every subscriber list of other connections + if (!!connectionClosed) { + connectionClosed.publishers.forEach(publisher => { + this.activeConnections.forEach(con => { + con.subscribers = con.subscribers.filter(subscriber => subscriber !== publisher.streamId); + }); + }); + } 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.log("Connection '" + connectionId + "' closed"); + resolve(); + } 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); + } else { + // Something happened in setting up the request that triggered an Error + console.error('Error', error.message); + } + }); + }); + } + + /** + * Forces some user 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 + * + * @returns A Promise that is resolved if the stream was successfully unpublished and rejected with an Error object if not + */ + public forceUnpublish(publisher: string | Publisher): Promise { + return new Promise((resolve, reject) => { + const streamId: string = typeof publisher === 'string' ? publisher : (publisher).streamId; + axios.delete( + 'https://' + OpenVidu.hostname + ':' + OpenVidu.port + OpenVidu.API_SESSIONS + '/' + this.sessionId + '/stream/' + streamId, + { + headers: { + 'Authorization': OpenVidu.basicAuth, + 'Content-Type': 'application/x-www-form-urlencoded' + } + } + ) + .then(res => { + if (res.status === 204) { + // SUCCESS response from openvidu-server + this.activeConnections.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 + connection.subscribers = connection.subscribers.filter(sub => sub !== streamId); + }); + console.log("Stream '" + streamId + "' unpublished"); resolve(); } else { // ERROR response from openvidu-server. Resolve HTTP status @@ -158,11 +364,11 @@ export class Session { }); axios.post( - 'https://' + this.hostname + ':' + this.port + Session.API_SESSIONS, + 'https://' + OpenVidu.hostname + ':' + OpenVidu.port + OpenVidu.API_SESSIONS, data, { headers: { - 'Authorization': this.basicAuth, + 'Authorization': OpenVidu.basicAuth, 'Content-Type': 'application/json' } } @@ -199,4 +405,43 @@ export class Session { }); } + /** + * @hidden + */ + public resetSessionWithJson(json): Session { + this.sessionId = json.sessionId; + 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 = { + mediaMode: json.mediaMode, + recordingMode: json.recordingMode, + defaultRecordingLayout: json.defaultRecordingLayout + }; + if (!!customSessionId) { + this.properties.customSessionId = customSessionId; + } + if (!!defaultCustomLayout) { + this.properties.defaultCustomLayout = defaultCustomLayout; + } + + 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.role, connection.token, connection.serverData, connection.clientData, publishers, subscribers)); + }); + return this; + } + } \ No newline at end of file diff --git a/openvidu-node-client/src/SessionProperties.ts b/openvidu-node-client/src/SessionProperties.ts index 0787ab9a..efb1a0e4 100644 --- a/openvidu-node-client/src/SessionProperties.ts +++ b/openvidu-node-client/src/SessionProperties.ts @@ -19,6 +19,9 @@ import { MediaMode } from './MediaMode'; import { RecordingLayout } from './RecordingLayout'; import { RecordingMode } from './RecordingMode'; +/** + * See [[OpenVidu.createSession]] + */ export interface SessionProperties { /** @@ -34,12 +37,15 @@ export interface SessionProperties { /** * Default value used to initialize property [[RecordingProperties.recordingLayout]] of every recording of this session. + * * You can easily override this value later by setting [[RecordingProperties.recordingLayout]] to any other value */ defaultRecordingLayout?: RecordingLayout; /** * Default value used to initialize property [[RecordingProperties.customLayout]] of every recording of this session. + * This property can only be defined if [[SessionProperties.defaultRecordingLayout]] is set to [[RecordingLayout.CUSTOM]]. + * * You can easily override this value later by setting [[RecordingProperties.customLayout]] to any other value */ defaultCustomLayout?: string; diff --git a/openvidu-node-client/src/TokenOptions.ts b/openvidu-node-client/src/TokenOptions.ts index d84a7fd9..65088eb6 100644 --- a/openvidu-node-client/src/TokenOptions.ts +++ b/openvidu-node-client/src/TokenOptions.ts @@ -17,6 +17,9 @@ import { OpenViduRole } from './OpenViduRole'; +/** + * See [[Session.generateToken]] + */ export interface TokenOptions { /** diff --git a/openvidu-node-client/src/index.ts b/openvidu-node-client/src/index.ts index 6df32be9..50ce477a 100644 --- a/openvidu-node-client/src/index.ts +++ b/openvidu-node-client/src/index.ts @@ -7,4 +7,6 @@ export * from './MediaMode'; export * from './RecordingLayout'; export * from './RecordingMode'; export * from './Recording'; -export * from './RecordingProperties'; \ No newline at end of file +export * from './RecordingProperties'; +export * from './Connection'; +export * from './Publisher'; \ No newline at end of file