UNPKG

jssip

Version:

The Javascript SIP library

272 lines (271 loc) 9.03 kB
"use strict"; const Logger = require('./Logger'); const Socket = require('./Socket'); const JsSIP_C = require('./Constants'); const logger = new Logger('Transport'); /** * Constants */ const C = { // Transport status. STATUS_CONNECTED: 0, STATUS_CONNECTING: 1, STATUS_DISCONNECTED: 2, // Socket status. SOCKET_STATUS_READY: 0, SOCKET_STATUS_ERROR: 1, // Recovery options. recovery_options: { // minimum interval in seconds between recover attempts. min_interval: JsSIP_C.CONNECTION_RECOVERY_MIN_INTERVAL, // maximum interval in seconds between recover attempts. max_interval: JsSIP_C.CONNECTION_RECOVERY_MAX_INTERVAL, }, }; /* * Manages one or multiple JsSIP.Socket instances. * Is reponsible for transport recovery logic among all socket instances. * * @socket JsSIP::Socket instance */ module.exports = class Transport { constructor(sockets, recovery_options = C.recovery_options) { logger.debug('new()'); this.status = C.STATUS_DISCONNECTED; // Current socket. this.socket = null; // Socket collection. this.sockets = []; this.recovery_options = recovery_options; this.recover_attempts = 0; this.recovery_timer = null; this.close_requested = false; // It seems that TextDecoder is not available in some versions of React-Native. // See https://github.com/versatica/JsSIP/issues/695 try { this.textDecoder = new TextDecoder('utf8'); } catch (error) { logger.warn(`cannot use TextDecoder: ${error}`); } if (typeof sockets === 'undefined') { throw new TypeError("Invalid argument. undefined 'sockets' argument"); } if (!(sockets instanceof Array)) { sockets = [sockets]; } sockets.forEach(function (socket) { if (!Socket.isSocket(socket.socket)) { throw new TypeError("Invalid argument. invalid 'JsSIP.Socket' instance"); } if (socket.weight && !Number(socket.weight)) { throw new TypeError("Invalid argument. 'weight' attribute is not a number"); } this.sockets.push({ socket: socket.socket, weight: socket.weight || 0, status: C.SOCKET_STATUS_READY, }); }, this); // Get the socket with higher weight. this._getSocket(); } /** * Instance Methods */ get via_transport() { return this.socket.via_transport; } get url() { return this.socket.url; } get sip_uri() { return this.socket.sip_uri; } connect() { logger.debug('connect()'); if (this.isConnected()) { logger.debug('Transport is already connected'); return; } else if (this.isConnecting()) { logger.debug('Transport is connecting'); return; } this.close_requested = false; this.status = C.STATUS_CONNECTING; this.onconnecting({ socket: this.socket, attempts: this.recover_attempts }); if (!this.close_requested) { // Bind socket event callbacks. this.socket.onconnect = this._onConnect.bind(this); this.socket.ondisconnect = this._onDisconnect.bind(this); this.socket.ondata = this._onData.bind(this); this.socket.connect(); } return; } disconnect() { logger.debug('close()'); this.close_requested = true; this.recover_attempts = 0; this.status = C.STATUS_DISCONNECTED; // Clear recovery_timer. if (this.recovery_timer !== null) { clearTimeout(this.recovery_timer); this.recovery_timer = null; } // Unbind socket event callbacks. this.socket.onconnect = () => { }; this.socket.ondisconnect = () => { }; this.socket.ondata = () => { }; this.socket.disconnect(); this.ondisconnect({ socket: this.socket, error: false, }); } send(data) { logger.debug('send()'); if (!this.isConnected()) { logger.warn('unable to send message, transport is not connected'); return false; } const message = data.toString(); logger.debug(`sending message:\n\n${message}\n`); return this.socket.send(message); } isConnected() { return this.status === C.STATUS_CONNECTED; } isConnecting() { return this.status === C.STATUS_CONNECTING; } /** * Private API. */ _reconnect() { this.recover_attempts += 1; let k = Math.floor(Math.random() * Math.pow(2, this.recover_attempts) + 1); if (k < this.recovery_options.min_interval) { k = this.recovery_options.min_interval; } else if (k > this.recovery_options.max_interval) { k = this.recovery_options.max_interval; } logger.debug(`reconnection attempt: ${this.recover_attempts}. next connection attempt in ${k} seconds`); this.recovery_timer = setTimeout(() => { if (!this.close_requested && !(this.isConnected() || this.isConnecting())) { // Get the next available socket with higher weight. this._getSocket(); // Connect the socket. this.connect(); } }, k * 1000); } /** * get the next available socket with higher weight */ _getSocket() { let candidates = []; this.sockets.forEach(socket => { if (socket.status === C.SOCKET_STATUS_ERROR) { return; // continue the array iteration } else if (candidates.length === 0) { candidates.push(socket); } else if (socket.weight > candidates[0].weight) { candidates = [socket]; } else if (socket.weight === candidates[0].weight) { candidates.push(socket); } }); if (candidates.length === 0) { // All sockets have failed. reset sockets status. this.sockets.forEach(socket => { socket.status = C.SOCKET_STATUS_READY; }); // Get next available socket. this._getSocket(); return; } const idx = Math.floor(Math.random() * candidates.length); this.socket = candidates[idx].socket; } /** * Socket Event Handlers */ _onConnect() { this.recover_attempts = 0; this.status = C.STATUS_CONNECTED; // Clear recovery_timer. if (this.recovery_timer !== null) { clearTimeout(this.recovery_timer); this.recovery_timer = null; } this.onconnect({ socket: this }); } _onDisconnect(error, code, reason) { this.status = C.STATUS_DISCONNECTED; this.ondisconnect({ socket: this.socket, error, code, reason, }); if (this.close_requested) { return; } // Update socket status. else { this.sockets.forEach(function (socket) { if (this.socket === socket.socket) { socket.status = C.SOCKET_STATUS_ERROR; } }, this); } this._reconnect(error); } _onData(data) { // CRLF Keep Alive request from server, reply. if (data === '\r\n\r\n') { logger.debug('received message with double-CRLF Keep Alive request'); try { // Reply with single CRLF. this.socket.send('\r\n'); } catch (error) { logger.warn(`error sending Keep Alive response: ${error}`); } return; } // CRLF Keep Alive response from server, ignore it. if (data === '\r\n') { logger.debug('received message with CRLF Keep Alive response'); return; } // Binary message. else if (typeof data !== 'string') { try { if (this.textDecoder) { data = this.textDecoder.decode(data); } else { data = String.fromCharCode.apply(null, new Uint8Array(data)); } } catch (error) { logger.debug(`received binary message failed to be converted into string: ${error}`); return; } logger.debug(`received binary message:\n\n${data}\n`); } // Text message. else { logger.debug(`received text message:\n\n${data}\n`); } this.ondata({ transport: this, message: data }); } };