UNPKG

jssip

Version:

the Javascript SIP library

399 lines (322 loc) 8.25 kB
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 (evt) { logger.debug('received binary message failed to be converted into string,' + ' message discarded'); 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 }); } };