/* * (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 axios, { AxiosError } from 'axios'; import { VideoCodec } from './VideoCodec'; import { Connection } from './Connection'; import { ConnectionProperties } from './ConnectionProperties'; import { MediaMode } from './MediaMode'; import { OpenVidu } from './OpenVidu'; import { Publisher } from './Publisher'; import { Recording } from './Recording'; import { RecordingLayout } from './RecordingLayout'; import { RecordingMode } from './RecordingMode'; import { SessionProperties } from './SessionProperties'; import { TokenOptions } from './TokenOptions'; export class Session { /** * Unique identifier of the Session */ sessionId: string; /** * Timestamp when this session was created, in UTC milliseconds (ms since Jan 1, 1970, 00:00:00 UTC) */ createdAt: number; /** * Properties defining the session */ properties: SessionProperties; /** * 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.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 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[] = []; /** * Whether the session is being recorded or not */ recording = false; /** * @hidden */ constructor(private ov: OpenVidu, propertiesOrJson?) { if (!!propertiesOrJson) { // Defined parameter if (!!propertiesOrJson.sessionId) { // Parameter is a JSON representation of Session ('sessionId' property always defined) this.resetWithJson(propertiesOrJson); } else { // Parameter is a SessionProperties object this.properties = propertiesOrJson; } } else { // Empty parameter this.properties = {}; } this.initDefaultSessionProperties(); } /** * @deprecated Use [[Session.createConnection]] instead to get a [[Connection]] object. * * @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 : null, data: (!!tokenOptions && !!tokenOptions.data) ? tokenOptions.data : null, kurentoOptions: (!!tokenOptions && !!tokenOptions.kurentoOptions) ? tokenOptions.kurentoOptions : null }); axios.post( this.ov.host + OpenVidu.API_TOKENS, data, { headers: { 'Authorization': this.ov.basicAuth, 'Content-Type': 'application/json' } } ) .then(res => { if (res.status === 200) { // SUCCESS response from openvidu-server. Resolve token resolve(res.data.token); } else { // ERROR response from openvidu-server. Resolve HTTP status reject(new Error(res.status.toString())); } }).catch(error => { 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({ type: (!!connectionProperties && !!connectionProperties.type) ? connectionProperties.type : null, data: (!!connectionProperties && !!connectionProperties.data) ? connectionProperties.data : null, record: !!connectionProperties ? connectionProperties.record : null, role: (!!connectionProperties && !!connectionProperties.role) ? connectionProperties.role : null, kurentoOptions: (!!connectionProperties && !!connectionProperties.kurentoOptions) ? connectionProperties.kurentoOptions : null, rtspUri: (!!connectionProperties && !!connectionProperties.rtspUri) ? connectionProperties.rtspUri : null, adaptativeBitrate: !!connectionProperties ? connectionProperties.adaptativeBitrate : null, onlyPlayWithSubscribers: !!connectionProperties ? connectionProperties.onlyPlayWithSubscribers : null, networkCache: (!!connectionProperties && (connectionProperties.networkCache != null)) ? connectionProperties.networkCache : 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); }); }); } /** * Gracefully closes the Session: unpublishes all streams and evicts every participant * * @returns A Promise that is resolved if the session has been closed successfully and rejected with an Error object if not */ public close(): Promise { return new Promise((resolve, reject) => { axios.delete( this.ov.host + OpenVidu.API_SESSIONS + '/' + this.sessionId, { headers: { 'Authorization': this.ov.basicAuth, 'Content-Type': 'application/x-www-form-urlencoded' } } ) .then(res => { if (res.status === 204) { // SUCCESS response from openvidu-server const indexToRemove: number = this.ov.activeSessions.findIndex(s => s.sessionId === this.sessionId); this.ov.activeSessions.splice(indexToRemove, 1); resolve(); } else { // ERROR response from openvidu-server. Resolve HTTP status reject(new Error(res.status.toString())); } }).catch(error => { 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 * Connections of the Session ([[Session.connections]], [[Session.activeConnections]]) and use those values to call [[Session.forceDisconnect]], * [[Session.forceUnpublish]] or [[Session.updateConnection]]. * * 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 */ public fetch(): Promise { return new Promise((resolve, reject) => { const beforeJSON: string = JSON.stringify(this, this.removeCircularOpenViduReference); axios.get( this.ov.host + OpenVidu.API_SESSIONS + '/' + this.sessionId + '?pendingConnections=true', { headers: { 'Authorization': this.ov.basicAuth, 'Content-Type': 'application/x-www-form-urlencoded' } } ) .then(res => { if (res.status === 200) { // SUCCESS response from openvidu-server 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); resolve(hasChanged); } else { // ERROR response from openvidu-server. Resolve HTTP status reject(new Error(res.status.toString())); } }).catch(error => { this.handleError(error, reject); }); }); } /** * 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 in the client-side (`streamDestroyed`, `connectionDestroyed`, * `sessionDisconnected`) with reason set to `"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 * [[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) => { const connectionId: string = typeof connection === 'string' ? connection : (connection).connectionId; axios.delete( this.ov.host + OpenVidu.API_SESSIONS + '/' + this.sessionId + '/connection/' + connectionId, { headers: { 'Authorization': this.ov.basicAuth, 'Content-Type': 'application/x-www-form-urlencoded' } }) .then(res => { if (res.status === 204) { // SUCCESS response from openvidu-server // Remove connection from connections array let connectionClosed; this.connections = this.connections.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.connections.forEach(con => { con.subscribers = con.subscribers.filter(subscriber => { // tslint:disable:no-string-literal if (!!subscriber['streamId']) { // Subscriber with advanced webRtc configuration properties return (subscriber['streamId'] !== publisher.streamId); // tslint:enable:no-string-literal } else { // Regular string subscribers return subscriber !== publisher.streamId; } }); }); }); } else { 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 { // ERROR response from openvidu-server. Resolve HTTP status reject(new Error(res.status.toString())); } }) .catch(error => { this.handleError(error, reject); }); }); } /** * 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]] 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 */ public forceUnpublish(publisher: string | Publisher): Promise { return new Promise((resolve, reject) => { const streamId: string = typeof publisher === 'string' ? publisher : (publisher).streamId; axios.delete( this.ov.host + OpenVidu.API_SESSIONS + '/' + this.sessionId + '/stream/' + streamId, { headers: { 'Authorization': this.ov.basicAuth, 'Content-Type': 'application/x-www-form-urlencoded' } } ) .then(res => { if (res.status === 204) { // SUCCESS response from openvidu-server 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 if (!!connection.subscribers && connection.subscribers.length > 0) { // tslint:disable:no-string-literal if (!!connection.subscribers[0]['streamId']) { // Subscriber with advanced webRtc configuration properties connection.subscribers = connection.subscribers.filter(sub => sub['streamId'] !== streamId); // tslint:enable:no-string-literal } else { // Regular string subscribers connection.subscribers = connection.subscribers.filter(sub => sub !== streamId); } } }); this.updateActiveConnectionsArray(); console.log("Stream '" + streamId + "' unpublished"); resolve(); } else { // ERROR response from openvidu-server. Resolve HTTP status reject(new Error(res.status.toString())); } }).catch(error => { 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 */ public getSessionHttp(): Promise { return new Promise((resolve, reject) => { if (!!this.sessionId) { resolve(this.sessionId); } this.initDefaultSessionProperties(); const data = JSON.stringify( this.properties ); axios.post( this.ov.host + OpenVidu.API_SESSIONS, data, { headers: { 'Authorization': this.ov.basicAuth, 'Content-Type': 'application/json' } } ) .then(res => { if (res.status === 200) { // SUCCESS response from openvidu-server. Resolve token this.sessionId = res.data.id; this.createdAt = res.data.createdAt; this.properties.mediaMode = res.data.mediaMode; this.properties.recordingMode = res.data.recordingMode; this.properties.defaultRecordingProperties = res.data.defaultRecordingProperties; this.properties.customSessionId = res.data.customSessionId; this.properties.mediaNode = res.data.mediaNode; this.properties.forcedVideoCodec = res.data.forcedVideoCodec; this.properties.allowTranscoding = res.data.allowTranscoding; resolve(this.sessionId); } 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) if (error.response.status === 409) { // 'customSessionId' already existed this.sessionId = this.properties.customSessionId; resolve(this.sessionId); } else { 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)); } }); }); } /** * @hidden */ public resetWithJson(json): Session { this.sessionId = json.sessionId; this.createdAt = json.createdAt; this.recording = json.recording; this.properties = { customSessionId: json.customSessionId, mediaMode: json.mediaMode, recordingMode: json.recordingMode, defaultRecordingProperties: json.defaultRecordingProperties, forcedVideoCodec: json.forcedVideoCodec, allowTranscoding: json.allowTranscoding }; this.initDefaultSessionProperties(); if (json.defaultRecordingProperties == null) { delete this.properties.defaultRecordingProperties; } if (json.customSessionId == null) { delete this.properties.customSessionId; } if (json.mediaNode == null) { delete this.properties.mediaNode; } if (json.forcedVideoCodec == null) { delete this.properties.forcedVideoCodec; } if (json.allowTranscoding == null) { delete this.properties.allowTranscoding; } // 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.connections.sort((c1, c2) => (c1.createdAt > c2.createdAt) ? 1 : ((c2.createdAt > c1.createdAt) ? -1 : 0)); // Populate activeConnections array this.updateActiveConnectionsArray(); return this; } /** * @hidden */ equalTo(other: Session): boolean { let equals: boolean = ( this.sessionId === other.sessionId && this.createdAt === other.createdAt && this.recording === other.recording && this.connections.length === other.connections.length && JSON.stringify(this.properties) === JSON.stringify(other.properties) ); if (equals) { let i = 0; while (equals && i < this.connections.length) { equals = this.connections[i].equalTo(other.connections[i]); i++; } return equals; } else { return false; } } /** * @hidden */ private removeCircularOpenViduReference(key: string, value: any) { if (key === 'ov' && value instanceof OpenVidu) { return; } else { return value; } } /** * @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)); } } /** * @hidden */ private initDefaultSessionProperties() { this.properties.mediaMode = !!this.properties.mediaMode ? this.properties.mediaMode : MediaMode.ROUTED; this.properties.recordingMode = !!this.properties.recordingMode ? this.properties.recordingMode : RecordingMode.MANUAL; if (!this.properties.defaultRecordingProperties) { this.properties.defaultRecordingProperties = {}; } this.properties.defaultRecordingProperties.name = (this.properties.defaultRecordingProperties?.name != null) ? this.properties.defaultRecordingProperties.name : ''; this.properties.defaultRecordingProperties.hasAudio = (this.properties.defaultRecordingProperties?.hasAudio != null) ? this.properties.defaultRecordingProperties.hasAudio : true; this.properties.defaultRecordingProperties.hasVideo = (this.properties.defaultRecordingProperties?.hasVideo != null) ? this.properties.defaultRecordingProperties.hasVideo : true; this.properties.defaultRecordingProperties.outputMode = (this.properties.defaultRecordingProperties?.outputMode != null) ? this.properties.defaultRecordingProperties.outputMode : Recording.OutputMode.COMPOSED; this.properties.defaultRecordingProperties.mediaNode = this.properties.defaultRecordingProperties?.mediaNode; if ((this.properties.defaultRecordingProperties.outputMode === Recording.OutputMode.COMPOSED || this.properties.defaultRecordingProperties.outputMode == Recording.OutputMode.COMPOSED_QUICK_START) && this.properties.defaultRecordingProperties.hasVideo) { this.properties.defaultRecordingProperties.recordingLayout = (this.properties.defaultRecordingProperties.recordingLayout != null) ? this.properties.defaultRecordingProperties.recordingLayout : RecordingLayout.BEST_FIT; this.properties.defaultRecordingProperties.resolution = (this.properties.defaultRecordingProperties.resolution != null) ? this.properties.defaultRecordingProperties.resolution : '1280x720'; this.properties.defaultRecordingProperties.frameRate = (this.properties.defaultRecordingProperties.frameRate != null) ? this.properties.defaultRecordingProperties.frameRate : 25; this.properties.defaultRecordingProperties.shmSize = (this.properties.defaultRecordingProperties.shmSize != null) ? this.properties.defaultRecordingProperties.shmSize : 536870912; if (this.properties.defaultRecordingProperties.recordingLayout === RecordingLayout.CUSTOM) { this.properties.defaultRecordingProperties.customLayout = (this.properties.defaultRecordingProperties.customLayout != null) ? this.properties.defaultRecordingProperties.customLayout : ''; } } this.properties.customSessionId = (this.properties.customSessionId != null) ? this.properties.customSessionId : ''; this.properties.mediaNode = (this.properties.mediaNode != null) ? this.properties.mediaNode : undefined; this.properties.forcedVideoCodec = (this.properties.forcedVideoCodec != null) ? this.properties.forcedVideoCodec : VideoCodec.VP8; this.properties.allowTranscoding = (this.properties.allowTranscoding != null) ? this.properties.allowTranscoding : false; } }