UNPKG

janode

Version:

Meetecho adapter for the Janus WebRTC Server

515 lines (457 loc) 15.8 kB
'use strict'; /** * This module contains the Connection class definition. * @module connection * @private */ import { EventEmitter } from 'events'; import Logger from './utils/logger.js'; const LOG_NS = '[connection.js]'; import { getNumericID, checkUrl, newIterator } from './utils/utils.js'; import { JANODE, JANUS, isResponseData, isErrorData } from './protocol.js'; import WsTransport from './transport-ws.js'; import UnixTransport from './transport-unix.js'; import JanodeSession from './session.js'; import TransactionManager from './tmanager.js'; /** * Class representing a Janode connection.<br> * * Specific transports are picked by checking the connection URI.<br> * * This class implements both the Janus API and Admin API.<br> * * Connection extends EventEmitter, so an instance can emit events and users can subscribe to them.<br> * * Users are not expected to create Connection instances, but insted use the Janode.connect() API.<br> * * @hideconstructor */ class Connection extends EventEmitter { /** * Create a Janode Connection. * * @param {module:configuration~Configuration} server_config - The Janode configuration as created by the Configuration constructor. */ constructor(server_config) { super(); /** * The configuration in use for this connection. * * @private * @type {module:configuration~Configuration} */ this._config = server_config; /** * The transaction manager used by this connection. * * @private * @type {module:tmanager~TransactionManager} */ this._tm = new TransactionManager(); /** * Keep track of the sessions. * * @private * @type {Map} */ this._sessions = new Map(); /** * The iterator to select available Janus addresses. * * @private * @type {module:utils~CircularIterator} */ this._address_iterator = newIterator(this._config.getAddress()); /** * A numerical identifier assigned for logging purposes. * * @type {number} */ this.id = parseInt(getNumericID()); /** * A more descriptive, not unique string (used for logging). * * @type {string} */ this.name = `[${this.id}]`; /** * The transport used by this connection. * * @private */ this._transport = { open: async _ => { throw new Error('transport does not implement the "open" function'); }, close: async _ => { throw new Error('transport does not implement the "close" function'); }, send: async _ => { throw new Error('transport does not implement the "send" function'); }, getRemoteHostname: _ => { throw new Error('transport does not implement the "getRemoteHostname" function'); }, }; try { let transport; /* Check the protocol to define the kind of transport */ if (checkUrl(server_config.getAddress()[0].url, ['ws', 'wss', 'ws+unix', 'wss+unix'])) { transport = new WsTransport(this); } if (checkUrl(server_config.getAddress()[0].url, ['file'])) { transport = new UnixTransport(this); } if (transport) this._transport = transport; } catch (error) { Logger.error(`${LOG_NS} ${this.name} error while initializing transport (${error.message})`); } /* Set a dummy error listener to avoid unmanaged errors */ this.on('error', e => `${LOG_NS} ${this.name} catched unmanaged error ${e.message}`); } /** * Cleanup the connection closing all owned transactions and emitting the destroyed event * and removing all registered listeners. * * @private * @param {boolean} graceful - True if this is an expected disconnection */ _signalClose(graceful) { /* Close all pending transactions inside this connection with an error */ this._tm.closeAllTransactionsWithError(null, new Error('connection closed')); /* Clear tx table */ this._tm.clear(); /* Clear session table */ this._sessions.clear(); /* Did we really mean to close it? */ if (graceful) { /* This is a greceful teardown */ /** * The connection has been gracefully closed. * * @event module:connection~Connection#event:CONNECTION_CLOSED * @type {Object} * @property {number} id - The connection identifier */ this.emit(JANODE.EVENT.CONNECTION_CLOSED, { id: this.id }); } else { /* If this event is unexpected emit an error */ const error = new Error('unexpected disconnection'); /** * The connection has been unexpectedly closed. * * @event module:connection~Connection#event:CONNECTION_ERROR * @type {Error} */ this.emit(JANODE.EVENT.CONNECTION_ERROR, error); } /* Remove all listeners to avoid leaks */ this.removeAllListeners(); } /** * Open a connection using the transport defined open method. * Users do not need to call this method, since the connection is opened by Janode.connect(). * * @returns {Promise<module:connection~Connection>} A promise resolving with the Janode connection */ async open() { await this._transport.open(); return this; } /** * Manage a message sent to this session. If a session is involved let it manage the message. * If the message involves a owned transaction and the response is a definitive one, * the transaction will be closed. * * @private * @param {Object} janus_message */ _handleMessage(janus_message) { const { session_id, transaction, janus } = janus_message; /* Check if a session is involved */ if (session_id && !this._config.isAdmin()) { /* Look for the session in the map */ const session = this._sessions.get(session_id); /* If the handle is missing notifies the user */ if (!session) { Logger.warn(`${LOG_NS} ${this.name} session ${session_id} not found for incoming message ${janus}`); return; } try { /* Let the session manage the message */ session._handleMessage(janus_message); } catch (error) { Logger.error(`${LOG_NS} ${this.name} error while handling message (${error.message})`); } return; } /* Check if a transaction is involved */ if (transaction) { Logger.verbose(`${LOG_NS} ${this.name} received ${janus} for transaction ${transaction}`); /* Not owned by this connection? */ if (this._tm.getTransactionOwner(transaction) !== this) { Logger.warn(`${LOG_NS} ${this.name} transaction ${transaction} not found for incoming messsage ${janus}`); return; } /* * Pending connection transaction management. * Close transaction in case of: * 1) Definitive response */ if (isResponseData(janus_message)) { if (isErrorData(janus_message)) { const error = new Error(`${janus_message.error.code} ${janus_message.error.reason}`); return this._tm.closeTransactionWithError(transaction, this, error); } this._tm.closeTransactionWithSuccess(transaction, this, janus_message); } return; } /* No session, no transaction? */ Logger.error(`${LOG_NS} ${this.name} unexpected janus message directed to the connection ${JSON.stringify(janus_message)}`); } /** * Decorate request with apisecret, token and transaction (if missing). * * @private * @param {Object} request */ _decorateRequest(request) { request.transaction = request.transaction || getNumericID(); if (this._address_iterator.currElem().apisecret) { if (!this._config.isAdmin()) request.apisecret = request.apisecret || this._address_iterator.currElem().apisecret; else request.admin_secret = request.admin_secret || this._address_iterator.currElem().apisecret; } if (this._address_iterator.currElem().token) request.token = this._address_iterator.currElem().token; } /** * Gracefully close the connection using the transport defined close method. * * @returns {Promise<void>} */ async close() { await this._transport.close(); return; } /** * Send a request from this connection using the transport defined send method. * * @param {Object} request - The request to be sent * @returns {Promise<Object>} A promise resolving with a response from Janus */ async sendRequest(request) { /* Add connection properties */ this._decorateRequest(request); return new Promise((resolve, reject) => { /* Create a new transaction if the transaction does not exist */ /* Use promise resolve and reject fn as callbacks for the transaction */ this._tm.createTransaction(request.transaction, this, request.janus, resolve, reject); this._transport.send(request).catch(error => { /* In case of error quickly close the transaction */ this._tm.closeTransactionWithError(request.transaction, this, error); reject(error); return; }); }); } /** * Get the remote Janus hostname using the transport defined method. * * @returns {string} The hostname of the Janus server */ getRemoteHostname() { return this._transport.getRemoteHostname(); } /** * Create a new session in this connection. * * @param {number} [ka_interval] - The time interval (seconds) for session keep-alive requests * @returns {Promise<module:session~Session>} The newly created session * * @example * * const session = await connection.create(); * Logger.info(`***** SESSION CREATED *****`); */ async create(ka_interval) { Logger.info(`${LOG_NS} ${this.name} creating new session`); const request = { janus: JANUS.REQUEST.CREATE_SESSION, }; try { const { data: { id } } = await this.sendRequest(request); /* Increase the maximum number of listeners for this connection */ /* The session will register two listeners */ this.setMaxListeners(this.getMaxListeners() + 2); /* Create a new Janode Session and add it to the table */ const session_instance = new JanodeSession(this, id, ka_interval); this._sessions.set(session_instance.id, session_instance); /* On session destroy delete the entry from session map and decrease the number of listeners */ session_instance.once(JANODE.EVENT.SESSION_DESTROYED, ({ id }) => { this._sessions.delete(id); this.setMaxListeners(this.getMaxListeners() - 2); }); Logger.info(`${LOG_NS} ${this.name} session created (id=${id})`); return session_instance; } catch (error) { Logger.error(`${LOG_NS} ${this.name} session creation error (${error.message})`); throw error; } } /** * Janus GET INFO API. * * @returns {Promise<Object>} The Get Info response * * @example * * const info = await connection.getInfo(); * Logger.info(`${info.name} ${info.version_string}`); */ async getInfo() { Logger.info(`${LOG_NS} ${this.name} requesting server info`); const request = { janus: JANUS.REQUEST.SERVER_INFO, }; return this.sendRequest(request); } /*************/ /* ADMIN API */ /*************/ /* The following APIs are available only if a connection has been created with is_admin = true in the config */ /** * (Admin API) List the sessions in a janus instance. * * @returns {Promise<Object>} * * @example * * const data = await connection.listSessions(); * Logger.info(`${JSON.stringify(data)}`); */ async listSessions() { Logger.verbose(`${LOG_NS} ${this.name} requesting session list`); const request = { janus: JANUS.ADMIN.LIST_SESSIONS, }; return this.sendRequest(request); } /** * (Admin API) List the handles in a session. * * @param {number} session_id - The identifier of the session * @returns {Promise<Object>} * * @example * * const data = await connection.listSessions(); * Logger.info(`${JSON.stringify(data)}`); */ async listHandles(session_id) { Logger.info(`${LOG_NS} ${this.name} requesting handle list`); if (!session_id) { const error = new Error('session_id parameter not specified'); Logger.error(`${LOG_NS} ${this.name} ${error.message}`); throw error; } const request = { janus: JANUS.ADMIN.LIST_HANDLES, session_id, }; return this.sendRequest(request); } /** * (Admin API) Get an handle info. * * @param {number} session_id - The session identifier * @param {number} handle_id - The handle identifier * @returns {Promise<Object>} The Get Handle Info response * * @example * * const data = await connection.handleInfo(session.id, handle.id); * Logger.info(`${JSON.stringify(data)}`); */ async handleInfo(session_id, handle_id) { Logger.info(`${LOG_NS} ${this.name} requesting handle info`); if (!session_id) { const error = new Error('session_id parameter not specified'); Logger.error(`${LOG_NS} ${this.name} ${error.message}`); throw error; } if (!handle_id) { const error = new Error('handle_id parameter not specified'); Logger.error(`${LOG_NS} ${this.name} ${error.message}`); throw error; } const request = { janus: JANUS.ADMIN.HANDLE_INFO, session_id, handle_id, }; return this.sendRequest(request); } /** * (Admin API) Start a packet capture on an handle. * * @param {number} session_id - The session identifier * @param {number} handle_id - The handle identifier * @param {string} folder - The folder in which save the pcap * @param {string} filename - The pcap file name * @param {number} [truncate] - Number of bytes to truncate the pcap to * @returns {Promise<Object>} The start pcap response */ async startPcap(session_id, handle_id, folder, filename, truncate) { Logger.info(`${LOG_NS} ${this.name} requesting pcap start for handle ${handle_id}`); if (!session_id) { const error = new Error('session_id parameter not specified'); Logger.error(`${LOG_NS} ${this.name} ${error.message}`); throw error; } if (!handle_id) { const error = new Error('handle_id parameter not specified'); Logger.error(`${LOG_NS} ${this.name} ${error.message}`); throw error; } if (typeof folder !== 'string' || typeof filename !== 'string') { const error = new Error('invalid folder or filename specified'); Logger.error(`${LOG_NS} ${this.name} ${error.message}`); throw error; } const request = { janus: JANUS.ADMIN.START_PCAP, session_id, handle_id, folder, filename, }; if ((typeof truncate === 'number') && truncate > 0) { request.truncate = truncate; } return this.sendRequest(request); } /** * Stop an ogoing packet capture. * * @param {number} session_id - The session identifier * @param {number} handle_id - The handle identifier * @returns {Promise<Object>} The stop pcap response */ async stopPcap(session_id, handle_id) { Logger.info(`${LOG_NS} ${this.name} requesting pcap stop for handle ${handle_id}`); if (!session_id) { const error = new Error('session_id parameter not specified'); Logger.error(`${LOG_NS} ${this.name} ${error.message}`); throw error; } if (!handle_id) { const error = new Error('handle_id parameter not specified'); Logger.error(`${LOG_NS} ${this.name} ${error.message}`); throw error; } const request = { janus: JANUS.ADMIN.STOP_PCAP, session_id, handle_id, }; return this.sendRequest(request); } } export default Connection;