UNPKG

jssip

Version:

the Javascript SIP library

1,504 lines (1,182 loc) 104 kB
"use strict"; function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e2) { throw _e2; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e3) { didErr = true; err = _e3; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } /* globals RTCPeerConnection: false, RTCSessionDescription: false */ var EventEmitter = require('events').EventEmitter; var sdp_transform = require('sdp-transform'); var Logger = require('./Logger'); var JsSIP_C = require('./Constants'); var Exceptions = require('./Exceptions'); var Transactions = require('./Transactions'); var Utils = require('./Utils'); var Timers = require('./Timers'); var SIPMessage = require('./SIPMessage'); var Dialog = require('./Dialog'); var RequestSender = require('./RequestSender'); var RTCSession_DTMF = require('./RTCSession/DTMF'); var RTCSession_Info = require('./RTCSession/Info'); var RTCSession_ReferNotifier = require('./RTCSession/ReferNotifier'); var RTCSession_ReferSubscriber = require('./RTCSession/ReferSubscriber'); var URI = require('./URI'); var logger = new Logger('RTCSession'); var C = { // RTCSession states. STATUS_NULL: 0, STATUS_INVITE_SENT: 1, STATUS_1XX_RECEIVED: 2, STATUS_INVITE_RECEIVED: 3, STATUS_WAITING_FOR_ANSWER: 4, STATUS_ANSWERED: 5, STATUS_WAITING_FOR_ACK: 6, STATUS_CANCELED: 7, STATUS_TERMINATED: 8, STATUS_CONFIRMED: 9 }; /** * Local variables. */ var holdMediaTypes = ['audio', 'video']; module.exports = /*#__PURE__*/function (_EventEmitter) { _inherits(RTCSession, _EventEmitter); var _super = _createSuper(RTCSession); _createClass(RTCSession, null, [{ key: "C", /** * Expose C object. */ get: function get() { return C; } }]); function RTCSession(ua) { var _this; _classCallCheck(this, RTCSession); logger.debug('new'); _this = _super.call(this); _this._id = null; _this._ua = ua; _this._status = C.STATUS_NULL; _this._dialog = null; _this._earlyDialogs = {}; _this._contact = null; _this._from_tag = null; _this._to_tag = null; // The RTCPeerConnection instance (public attribute). _this._connection = null; // Prevent races on serial PeerConnction operations. _this._connectionPromiseQueue = Promise.resolve(); // Incoming/Outgoing request being currently processed. _this._request = null; // Cancel state for initial outgoing request. _this._is_canceled = false; _this._cancel_reason = ''; // RTCSession confirmation flag. _this._is_confirmed = false; // Is late SDP being negotiated. _this._late_sdp = false; // Default rtcOfferConstraints and rtcAnswerConstrainsts (passed in connect() or answer()). _this._rtcOfferConstraints = null; _this._rtcAnswerConstraints = null; // Local MediaStream. _this._localMediaStream = null; _this._localMediaStreamLocallyGenerated = false; // Flag to indicate PeerConnection ready for new actions. _this._rtcReady = true; // Flag to indicate ICE candidate gathering is finished even if iceGatheringState is not yet 'complete'. _this._iceReady = false; // SIP Timers. _this._timers = { ackTimer: null, expiresTimer: null, invite2xxTimer: null, userNoAnswerTimer: null }; // Session info. _this._direction = null; _this._local_identity = null; _this._remote_identity = null; _this._start_time = null; _this._end_time = null; _this._tones = null; // Mute/Hold state. _this._audioMuted = false; _this._videoMuted = false; _this._localHold = false; _this._remoteHold = false; // Session Timers (RFC 4028). _this._sessionTimers = { enabled: _this._ua.configuration.session_timers, refreshMethod: _this._ua.configuration.session_timers_refresh_method, defaultExpires: JsSIP_C.SESSION_EXPIRES, currentExpires: null, running: false, refresher: false, timer: null // A setTimeout. }; // Map of ReferSubscriber instances indexed by the REFER's CSeq number. _this._referSubscribers = {}; // Custom session empty object for high level use. _this._data = {}; return _this; } /** * User API */ // Expose RTCSession constants as a property of the RTCSession instance. _createClass(RTCSession, [{ key: "isInProgress", value: function isInProgress() { switch (this._status) { case C.STATUS_NULL: case C.STATUS_INVITE_SENT: case C.STATUS_1XX_RECEIVED: case C.STATUS_INVITE_RECEIVED: case C.STATUS_WAITING_FOR_ANSWER: return true; default: return false; } } }, { key: "isEstablished", value: function isEstablished() { switch (this._status) { case C.STATUS_ANSWERED: case C.STATUS_WAITING_FOR_ACK: case C.STATUS_CONFIRMED: return true; default: return false; } } }, { key: "isEnded", value: function isEnded() { switch (this._status) { case C.STATUS_CANCELED: case C.STATUS_TERMINATED: return true; default: return false; } } }, { key: "isMuted", value: function isMuted() { return { audio: this._audioMuted, video: this._videoMuted }; } }, { key: "isOnHold", value: function isOnHold() { return { local: this._localHold, remote: this._remoteHold }; } }, { key: "connect", value: function connect(target) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var initCallback = arguments.length > 2 ? arguments[2] : undefined; logger.debug('connect()'); var originalTarget = target; var eventHandlers = Utils.cloneObject(options.eventHandlers); var extraHeaders = Utils.cloneArray(options.extraHeaders); var mediaConstraints = Utils.cloneObject(options.mediaConstraints, { audio: true, video: true }); var mediaStream = options.mediaStream || null; var pcConfig = Utils.cloneObject(options.pcConfig, { iceServers: [] }); var rtcConstraints = options.rtcConstraints || null; var rtcOfferConstraints = options.rtcOfferConstraints || null; this._rtcOfferConstraints = rtcOfferConstraints; this._rtcAnswerConstraints = options.rtcAnswerConstraints || null; this._data = options.data || this._data; // Check target. if (target === undefined) { throw new TypeError('Not enough arguments'); } // Check Session Status. if (this._status !== C.STATUS_NULL) { throw new Exceptions.InvalidStateError(this._status); } // Check WebRTC support. if (!window.RTCPeerConnection) { throw new Exceptions.NotSupportedError('WebRTC not supported'); } // Check target validity. target = this._ua.normalizeTarget(target); if (!target) { throw new TypeError("Invalid target: ".concat(originalTarget)); } // Session Timers. if (this._sessionTimers.enabled) { if (Utils.isDecimal(options.sessionTimersExpires)) { if (options.sessionTimersExpires >= JsSIP_C.MIN_SESSION_EXPIRES) { this._sessionTimers.defaultExpires = options.sessionTimersExpires; } else { this._sessionTimers.defaultExpires = JsSIP_C.SESSION_EXPIRES; } } } // Set event handlers. for (var event in eventHandlers) { if (Object.prototype.hasOwnProperty.call(eventHandlers, event)) { this.on(event, eventHandlers[event]); } } // Session parameter initialization. this._from_tag = Utils.newTag(); // Set anonymous property. var anonymous = options.anonymous || false; var requestParams = { from_tag: this._from_tag }; this._contact = this._ua.contact.toString({ anonymous: anonymous, outbound: true }); if (anonymous) { requestParams.from_display_name = 'Anonymous'; requestParams.from_uri = new URI('sip', 'anonymous', 'anonymous.invalid'); extraHeaders.push("P-Preferred-Identity: ".concat(this._ua.configuration.uri.toString())); extraHeaders.push('Privacy: id'); } else if (options.fromUserName) { requestParams.from_uri = new URI('sip', options.fromUserName, this._ua.configuration.uri.host); extraHeaders.push("P-Preferred-Identity: ".concat(this._ua.configuration.uri.toString())); } if (options.fromDisplayName) { requestParams.from_display_name = options.fromDisplayName; } extraHeaders.push("Contact: ".concat(this._contact)); extraHeaders.push('Content-Type: application/sdp'); if (this._sessionTimers.enabled) { extraHeaders.push("Session-Expires: ".concat(this._sessionTimers.defaultExpires).concat(this._ua.configuration.session_timers_force_refresher ? ';refresher=uac' : '')); } this._request = new SIPMessage.InitialOutgoingInviteRequest(target, this._ua, requestParams, extraHeaders); this._id = this._request.call_id + this._from_tag; // Create a new RTCPeerConnection instance. this._createRTCConnection(pcConfig, rtcConstraints); // Set internal properties. this._direction = 'outgoing'; this._local_identity = this._request.from; this._remote_identity = this._request.to; // User explicitly provided a newRTCSession callback for this session. if (initCallback) { initCallback(this); } this._newRTCSession('local', this._request); this._sendInitialRequest(mediaConstraints, rtcOfferConstraints, mediaStream); } }, { key: "init_incoming", value: function init_incoming(request, initCallback) { var _this2 = this; logger.debug('init_incoming()'); var expires; var contentType = request.hasHeader('Content-Type') ? request.getHeader('Content-Type').toLowerCase() : undefined; // Check body and content type. if (request.body && contentType !== 'application/sdp') { request.reply(415); return; } // Session parameter initialization. this._status = C.STATUS_INVITE_RECEIVED; this._from_tag = request.from_tag; this._id = request.call_id + this._from_tag; this._request = request; this._contact = this._ua.contact.toString(); // Get the Expires header value if exists. if (request.hasHeader('expires')) { expires = request.getHeader('expires') * 1000; } /* Set the to_tag before * replying a response code that will create a dialog. */ request.to_tag = Utils.newTag(); // An error on dialog creation will fire 'failed' event. if (!this._createDialog(request, 'UAS', true)) { request.reply(500, 'Missing Contact header field'); return; } if (request.body) { this._late_sdp = false; } else { this._late_sdp = true; } this._status = C.STATUS_WAITING_FOR_ANSWER; // Set userNoAnswerTimer. this._timers.userNoAnswerTimer = setTimeout(function () { request.reply(408); _this2._failed('local', null, JsSIP_C.causes.NO_ANSWER); }, this._ua.configuration.no_answer_timeout); /* Set expiresTimer * RFC3261 13.3.1 */ if (expires) { this._timers.expiresTimer = setTimeout(function () { if (_this2._status === C.STATUS_WAITING_FOR_ANSWER) { request.reply(487); _this2._failed('system', null, JsSIP_C.causes.EXPIRES); } }, expires); } // Set internal properties. this._direction = 'incoming'; this._local_identity = request.to; this._remote_identity = request.from; // A init callback was specifically defined. if (initCallback) { initCallback(this); } // Fire 'newRTCSession' event. this._newRTCSession('remote', request); // The user may have rejected the call in the 'newRTCSession' event. if (this._status === C.STATUS_TERMINATED) { return; } // Reply 180. request.reply(180, null, ["Contact: ".concat(this._contact)]); // Fire 'progress' event. // TODO: Document that 'response' field in 'progress' event is null for incoming calls. this._progress('local', null); } /** * Answer the call. */ }, { key: "answer", value: function answer() { var _this3 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; logger.debug('answer()'); var request = this._request; var extraHeaders = Utils.cloneArray(options.extraHeaders); var mediaConstraints = Utils.cloneObject(options.mediaConstraints); var mediaStream = options.mediaStream || null; var pcConfig = Utils.cloneObject(options.pcConfig, { iceServers: [] }); var rtcConstraints = options.rtcConstraints || null; var rtcAnswerConstraints = options.rtcAnswerConstraints || null; var rtcOfferConstraints = Utils.cloneObject(options.rtcOfferConstraints); var tracks; var peerHasAudioLine = false; var peerHasVideoLine = false; var peerOffersFullAudio = false; var peerOffersFullVideo = false; this._rtcAnswerConstraints = rtcAnswerConstraints; this._rtcOfferConstraints = options.rtcOfferConstraints || null; this._data = options.data || this._data; // Check Session Direction and Status. if (this._direction !== 'incoming') { throw new Exceptions.NotSupportedError('"answer" not supported for outgoing RTCSession'); } // Check Session status. if (this._status !== C.STATUS_WAITING_FOR_ANSWER) { throw new Exceptions.InvalidStateError(this._status); } // Session Timers. if (this._sessionTimers.enabled) { if (Utils.isDecimal(options.sessionTimersExpires)) { if (options.sessionTimersExpires >= JsSIP_C.MIN_SESSION_EXPIRES) { this._sessionTimers.defaultExpires = options.sessionTimersExpires; } else { this._sessionTimers.defaultExpires = JsSIP_C.SESSION_EXPIRES; } } } this._status = C.STATUS_ANSWERED; // An error on dialog creation will fire 'failed' event. if (!this._createDialog(request, 'UAS')) { request.reply(500, 'Error creating dialog'); return; } clearTimeout(this._timers.userNoAnswerTimer); extraHeaders.unshift("Contact: ".concat(this._contact)); // Determine incoming media from incoming SDP offer (if any). var sdp = request.parseSDP(); // Make sure sdp.media is an array, not the case if there is only one media. if (!Array.isArray(sdp.media)) { sdp.media = [sdp.media]; } // Go through all medias in SDP to find offered capabilities to answer with. var _iterator = _createForOfIteratorHelper(sdp.media), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var m = _step.value; if (m.type === 'audio') { peerHasAudioLine = true; if (!m.direction || m.direction === 'sendrecv') { peerOffersFullAudio = true; } } if (m.type === 'video') { peerHasVideoLine = true; if (!m.direction || m.direction === 'sendrecv') { peerOffersFullVideo = true; } } } // Remove audio from mediaStream if suggested by mediaConstraints. } catch (err) { _iterator.e(err); } finally { _iterator.f(); } if (mediaStream && mediaConstraints.audio === false) { tracks = mediaStream.getAudioTracks(); var _iterator2 = _createForOfIteratorHelper(tracks), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var track = _step2.value; mediaStream.removeTrack(track); } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } } // Remove video from mediaStream if suggested by mediaConstraints. if (mediaStream && mediaConstraints.video === false) { tracks = mediaStream.getVideoTracks(); var _iterator3 = _createForOfIteratorHelper(tracks), _step3; try { for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { var _track = _step3.value; mediaStream.removeTrack(_track); } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } } // Set audio constraints based on incoming stream if not supplied. if (!mediaStream && mediaConstraints.audio === undefined) { mediaConstraints.audio = peerOffersFullAudio; } // Set video constraints based on incoming stream if not supplied. if (!mediaStream && mediaConstraints.video === undefined) { mediaConstraints.video = peerOffersFullVideo; } // Don't ask for audio if the incoming offer has no audio section. if (!mediaStream && !peerHasAudioLine && !rtcOfferConstraints.offerToReceiveAudio) { mediaConstraints.audio = false; } // Don't ask for video if the incoming offer has no video section. if (!mediaStream && !peerHasVideoLine && !rtcOfferConstraints.offerToReceiveVideo) { mediaConstraints.video = false; } // Create a new RTCPeerConnection instance. // TODO: This may throw an error, should react. this._createRTCConnection(pcConfig, rtcConstraints); Promise.resolve() // Handle local MediaStream. .then(function () { // A local MediaStream is given, use it. if (mediaStream) { return mediaStream; } // Audio and/or video requested, prompt getUserMedia. else if (mediaConstraints.audio || mediaConstraints.video) { _this3._localMediaStreamLocallyGenerated = true; return navigator.mediaDevices.getUserMedia(mediaConstraints)["catch"](function (error) { if (_this3._status === C.STATUS_TERMINATED) { throw new Error('terminated'); } request.reply(480); _this3._failed('local', null, JsSIP_C.causes.USER_DENIED_MEDIA_ACCESS); logger.warn('emit "getusermediafailed" [error:%o]', error); _this3.emit('getusermediafailed', error); throw new Error('getUserMedia() failed'); }); } }) // Attach MediaStream to RTCPeerconnection. .then(function (stream) { if (_this3._status === C.STATUS_TERMINATED) { throw new Error('terminated'); } _this3._localMediaStream = stream; if (stream) { stream.getTracks().forEach(function (track) { _this3._connection.addTrack(track, stream); }); } }) // Set remote description. .then(function () { if (_this3._late_sdp) { return; } var e = { originator: 'remote', type: 'offer', sdp: request.body }; logger.debug('emit "sdp"'); _this3.emit('sdp', e); var offer = new RTCSessionDescription({ type: 'offer', sdp: e.sdp }); _this3._connectionPromiseQueue = _this3._connectionPromiseQueue.then(function () { return _this3._connection.setRemoteDescription(offer); })["catch"](function (error) { request.reply(488); _this3._failed('system', null, JsSIP_C.causes.WEBRTC_ERROR); logger.warn('emit "peerconnection:setremotedescriptionfailed" [error:%o]', error); _this3.emit('peerconnection:setremotedescriptionfailed', error); throw new Error('peerconnection.setRemoteDescription() failed'); }); return _this3._connectionPromiseQueue; }) // Create local description. .then(function () { if (_this3._status === C.STATUS_TERMINATED) { throw new Error('terminated'); } // TODO: Is this event already useful? _this3._connecting(request); if (!_this3._late_sdp) { return _this3._createLocalDescription('answer', rtcAnswerConstraints)["catch"](function () { request.reply(500); throw new Error('_createLocalDescription() failed'); }); } else { return _this3._createLocalDescription('offer', _this3._rtcOfferConstraints)["catch"](function () { request.reply(500); throw new Error('_createLocalDescription() failed'); }); } }) // Send reply. .then(function (desc) { if (_this3._status === C.STATUS_TERMINATED) { throw new Error('terminated'); } _this3._handleSessionTimersInIncomingRequest(request, extraHeaders); request.reply(200, null, extraHeaders, desc, function () { _this3._status = C.STATUS_WAITING_FOR_ACK; _this3._setInvite2xxTimer(request, desc); _this3._setACKTimer(); _this3._accepted('local'); }, function () { _this3._failed('system', null, JsSIP_C.causes.CONNECTION_ERROR); }); })["catch"](function (error) { if (_this3._status === C.STATUS_TERMINATED) { return; } logger.warn(error); }); } /** * Terminate the call. */ }, { key: "terminate", value: function terminate() { var _this4 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; logger.debug('terminate()'); var cause = options.cause || JsSIP_C.causes.BYE; var extraHeaders = Utils.cloneArray(options.extraHeaders); var body = options.body; var cancel_reason; var status_code = options.status_code; var reason_phrase = options.reason_phrase; // Check Session Status. if (this._status === C.STATUS_TERMINATED) { throw new Exceptions.InvalidStateError(this._status); } switch (this._status) { // - UAC - case C.STATUS_NULL: case C.STATUS_INVITE_SENT: case C.STATUS_1XX_RECEIVED: logger.debug('canceling session'); if (status_code && (status_code < 200 || status_code >= 700)) { throw new TypeError("Invalid status_code: ".concat(status_code)); } else if (status_code) { reason_phrase = reason_phrase || JsSIP_C.REASON_PHRASE[status_code] || ''; cancel_reason = "SIP ;cause=".concat(status_code, " ;text=\"").concat(reason_phrase, "\""); } // Check Session Status. if (this._status === C.STATUS_NULL || this._status === C.STATUS_INVITE_SENT) { this._is_canceled = true; this._cancel_reason = cancel_reason; } else if (this._status === C.STATUS_1XX_RECEIVED) { this._request.cancel(cancel_reason); } this._status = C.STATUS_CANCELED; this._failed('local', null, JsSIP_C.causes.CANCELED); break; // - UAS - case C.STATUS_WAITING_FOR_ANSWER: case C.STATUS_ANSWERED: logger.debug('rejecting session'); status_code = status_code || 480; if (status_code < 300 || status_code >= 700) { throw new TypeError("Invalid status_code: ".concat(status_code)); } this._request.reply(status_code, reason_phrase, extraHeaders, body); this._failed('local', null, JsSIP_C.causes.REJECTED); break; case C.STATUS_WAITING_FOR_ACK: case C.STATUS_CONFIRMED: logger.debug('terminating session'); reason_phrase = options.reason_phrase || JsSIP_C.REASON_PHRASE[status_code] || ''; if (status_code && (status_code < 200 || status_code >= 700)) { throw new TypeError("Invalid status_code: ".concat(status_code)); } else if (status_code) { extraHeaders.push("Reason: SIP ;cause=".concat(status_code, "; text=\"").concat(reason_phrase, "\"")); } /* RFC 3261 section 15 (Terminating a session): * * "...the callee's UA MUST NOT send a BYE on a confirmed dialog * until it has received an ACK for its 2xx response or until the server * transaction times out." */ if (this._status === C.STATUS_WAITING_FOR_ACK && this._direction === 'incoming' && this._request.server_transaction.state !== Transactions.C.STATUS_TERMINATED) { // Save the dialog for later restoration. var dialog = this._dialog; // Send the BYE as soon as the ACK is received... this.receiveRequest = function (_ref) { var method = _ref.method; if (method === JsSIP_C.ACK) { _this4.sendRequest(JsSIP_C.BYE, { extraHeaders: extraHeaders, body: body }); dialog.terminate(); } }; // .., or when the INVITE transaction times out this._request.server_transaction.on('stateChanged', function () { if (_this4._request.server_transaction.state === Transactions.C.STATUS_TERMINATED) { _this4.sendRequest(JsSIP_C.BYE, { extraHeaders: extraHeaders, body: body }); dialog.terminate(); } }); this._ended('local', null, cause); // Restore the dialog into 'this' in order to be able to send the in-dialog BYE :-). this._dialog = dialog; // Restore the dialog into 'ua' so the ACK can reach 'this' session. this._ua.newDialog(dialog); } else { this.sendRequest(JsSIP_C.BYE, { extraHeaders: extraHeaders, body: body }); this._ended('local', null, cause); } } } }, { key: "sendDTMF", value: function sendDTMF(tones) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; logger.debug('sendDTMF() | tones: %s', tones); var position = 0; var duration = options.duration || null; var interToneGap = options.interToneGap || null; var transportType = options.transportType || JsSIP_C.DTMF_TRANSPORT.INFO; if (tones === undefined) { throw new TypeError('Not enough arguments'); } // Check Session Status. if (this._status !== C.STATUS_CONFIRMED && this._status !== C.STATUS_WAITING_FOR_ACK && this._status !== C.STATUS_1XX_RECEIVED) { throw new Exceptions.InvalidStateError(this._status); } // Check Transport type. if (transportType !== JsSIP_C.DTMF_TRANSPORT.INFO && transportType !== JsSIP_C.DTMF_TRANSPORT.RFC2833) { throw new TypeError("invalid transportType: ".concat(transportType)); } // Convert to string. if (typeof tones === 'number') { tones = tones.toString(); } // Check tones. if (!tones || typeof tones !== 'string' || !tones.match(/^[0-9A-DR#*,]+$/i)) { throw new TypeError("Invalid tones: ".concat(tones)); } // Check duration. if (duration && !Utils.isDecimal(duration)) { throw new TypeError("Invalid tone duration: ".concat(duration)); } else if (!duration) { duration = RTCSession_DTMF.C.DEFAULT_DURATION; } else if (duration < RTCSession_DTMF.C.MIN_DURATION) { logger.debug("\"duration\" value is lower than the minimum allowed, setting it to ".concat(RTCSession_DTMF.C.MIN_DURATION, " milliseconds")); duration = RTCSession_DTMF.C.MIN_DURATION; } else if (duration > RTCSession_DTMF.C.MAX_DURATION) { logger.debug("\"duration\" value is greater than the maximum allowed, setting it to ".concat(RTCSession_DTMF.C.MAX_DURATION, " milliseconds")); duration = RTCSession_DTMF.C.MAX_DURATION; } else { duration = Math.abs(duration); } options.duration = duration; // Check interToneGap. if (interToneGap && !Utils.isDecimal(interToneGap)) { throw new TypeError("Invalid interToneGap: ".concat(interToneGap)); } else if (!interToneGap) { interToneGap = RTCSession_DTMF.C.DEFAULT_INTER_TONE_GAP; } else if (interToneGap < RTCSession_DTMF.C.MIN_INTER_TONE_GAP) { logger.debug("\"interToneGap\" value is lower than the minimum allowed, setting it to ".concat(RTCSession_DTMF.C.MIN_INTER_TONE_GAP, " milliseconds")); interToneGap = RTCSession_DTMF.C.MIN_INTER_TONE_GAP; } else { interToneGap = Math.abs(interToneGap); } // RFC2833. Let RTCDTMFSender enqueue the DTMFs. if (transportType === JsSIP_C.DTMF_TRANSPORT.RFC2833) { // Send DTMF in current audio RTP stream. var sender = this._getDTMFRTPSender(); if (sender) { // Add remaining buffered tones. tones = sender.toneBuffer + tones; // Insert tones. sender.insertDTMF(tones, duration, interToneGap); } return; } if (this._tones) { // Tones are already queued, just add to the queue. this._tones += tones; return; } this._tones = tones; // Send the first tone. _sendDTMF.call(this); function _sendDTMF() { var _this5 = this; var timeout; if (this._status === C.STATUS_TERMINATED || !this._tones || position >= this._tones.length) { // Stop sending DTMF. this._tones = null; return; } var tone = this._tones[position]; position += 1; if (tone === ',') { timeout = 2000; } else { // Send DTMF via SIP INFO messages. var dtmf = new RTCSession_DTMF(this); options.eventHandlers = { onFailed: function onFailed() { _this5._tones = null; } }; dtmf.send(tone, options); timeout = duration + interToneGap; } // Set timeout for the next tone. setTimeout(_sendDTMF.bind(this), timeout); } } }, { key: "sendInfo", value: function sendInfo(contentType, body) { var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; logger.debug('sendInfo()'); // Check Session Status. if (this._status !== C.STATUS_CONFIRMED && this._status !== C.STATUS_WAITING_FOR_ACK && this._status !== C.STATUS_1XX_RECEIVED) { throw new Exceptions.InvalidStateError(this._status); } var info = new RTCSession_Info(this); info.send(contentType, body, options); } /** * Mute */ }, { key: "mute", value: function mute() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { audio: true, video: false }; logger.debug('mute()'); var audioMuted = false, videoMuted = false; if (this._audioMuted === false && options.audio) { audioMuted = true; this._audioMuted = true; this._toggleMuteAudio(true); } if (this._videoMuted === false && options.video) { videoMuted = true; this._videoMuted = true; this._toggleMuteVideo(true); } if (audioMuted === true || videoMuted === true) { this._onmute({ audio: audioMuted, video: videoMuted }); } } /** * Unmute */ }, { key: "unmute", value: function unmute() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { audio: true, video: true }; logger.debug('unmute()'); var audioUnMuted = false, videoUnMuted = false; if (this._audioMuted === true && options.audio) { audioUnMuted = true; this._audioMuted = false; if (this._localHold === false) { this._toggleMuteAudio(false); } } if (this._videoMuted === true && options.video) { videoUnMuted = true; this._videoMuted = false; if (this._localHold === false) { this._toggleMuteVideo(false); } } if (audioUnMuted === true || videoUnMuted === true) { this._onunmute({ audio: audioUnMuted, video: videoUnMuted }); } } /** * Hold */ }, { key: "hold", value: function hold() { var _this6 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var done = arguments.length > 1 ? arguments[1] : undefined; logger.debug('hold()'); if (this._status !== C.STATUS_WAITING_FOR_ACK && this._status !== C.STATUS_CONFIRMED) { return false; } if (this._localHold === true) { return false; } if (!this._isReadyToReOffer()) { return false; } this._localHold = true; this._onhold('local'); var eventHandlers = { succeeded: function succeeded() { if (done) { done(); } }, failed: function failed() { _this6.terminate({ cause: JsSIP_C.causes.WEBRTC_ERROR, status_code: 500, reason_phrase: 'Hold Failed' }); } }; if (options.useUpdate) { this._sendUpdate({ sdpOffer: true, eventHandlers: eventHandlers, extraHeaders: options.extraHeaders }); } else { this._sendReinvite({ eventHandlers: eventHandlers, extraHeaders: options.extraHeaders }); } return true; } }, { key: "unhold", value: function unhold() { var _this7 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var done = arguments.length > 1 ? arguments[1] : undefined; logger.debug('unhold()'); if (this._status !== C.STATUS_WAITING_FOR_ACK && this._status !== C.STATUS_CONFIRMED) { return false; } if (this._localHold === false) { return false; } if (!this._isReadyToReOffer()) { return false; } this._localHold = false; this._onunhold('local'); var eventHandlers = { succeeded: function succeeded() { if (done) { done(); } }, failed: function failed() { _this7.terminate({ cause: JsSIP_C.causes.WEBRTC_ERROR, status_code: 500, reason_phrase: 'Unhold Failed' }); } }; if (options.useUpdate) { this._sendUpdate({ sdpOffer: true, eventHandlers: eventHandlers, extraHeaders: options.extraHeaders }); } else { this._sendReinvite({ eventHandlers: eventHandlers, extraHeaders: options.extraHeaders }); } return true; } }, { key: "renegotiate", value: function renegotiate() { var _this8 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var done = arguments.length > 1 ? arguments[1] : undefined; logger.debug('renegotiate()'); var rtcOfferConstraints = options.rtcOfferConstraints || null; if (this._status !== C.STATUS_WAITING_FOR_ACK && this._status !== C.STATUS_CONFIRMED) { return false; } if (!this._isReadyToReOffer()) { return false; } var eventHandlers = { succeeded: function succeeded() { if (done) { done(); } }, failed: function failed() { _this8.terminate({ cause: JsSIP_C.causes.WEBRTC_ERROR, status_code: 500, reason_phrase: 'Media Renegotiation Failed' }); } }; this._setLocalMediaStatus(); if (options.useUpdate) { this._sendUpdate({ sdpOffer: true, eventHandlers: eventHandlers, rtcOfferConstraints: rtcOfferConstraints, extraHeaders: options.extraHeaders }); } else { this._sendReinvite({ eventHandlers: eventHandlers, rtcOfferConstraints: rtcOfferConstraints, extraHeaders: options.extraHeaders }); } return true; } /** * Refer */ }, { key: "refer", value: function refer(target, options) { var _this9 = this; logger.debug('refer()'); var originalTarget = target; if (this._status !== C.STATUS_WAITING_FOR_ACK && this._status !== C.STATUS_CONFIRMED) { return false; } // Check target validity. target = this._ua.normalizeTarget(target); if (!target) { throw new TypeError("Invalid target: ".concat(originalTarget)); } var referSubscriber = new RTCSession_ReferSubscriber(this); referSubscriber.sendRefer(target, options); // Store in the map. var id = referSubscriber.id; this._referSubscribers[id] = referSubscriber; // Listen for ending events so we can remove it from the map. referSubscriber.on('requestFailed', function () { delete _this9._referSubscribers[id]; }); referSubscriber.on('accepted', function () { delete _this9._referSubscribers[id]; }); referSubscriber.on('failed', function () { delete _this9._referSubscribers[id]; }); return referSubscriber; } /** * Send a generic in-dialog Request */ }, { key: "sendRequest", value: function sendRequest(method, options) { logger.debug('sendRequest()'); return this._dialog.sendRequest(method, options); } /** * In dialog Request Reception */ }, { key: "receiveRequest", value: function receiveRequest(request) { var _this10 = this; logger.debug('receiveRequest()'); if (request.method === JsSIP_C.CANCEL) { /* RFC3261 15 States that a UAS may have accepted an invitation while a CANCEL * was in progress and that the UAC MAY continue with the session established by * any 2xx response, or MAY terminate with BYE. JsSIP does continue with the * established session. So the CANCEL is processed only if the session is not yet * established. */ /* * Terminate the whole session in case the user didn't accept (or yet send the answer) * nor reject the request opening the session. */ if (this._status === C.STATUS_WAITING_FOR_ANSWER || this._status === C.STATUS_ANSWERED) { this._status = C.STATUS_CANCELED; this._request.reply(487); this._failed('remote', request, JsSIP_C.causes.CANCELED); } } else { // Requests arriving here are in-dialog requests. switch (request.method) { case JsSIP_C.ACK: if (this._status !== C.STATUS_WAITING_FOR_ACK) { return; } // Update signaling status. this._status = C.STATUS_CONFIRMED; clearTimeout(this._timers.ackTimer); clearTimeout(this._timers.invite2xxTimer); if (this._late_sdp) { if (!request.body) { this.terminate({ cause: JsSIP_C.causes.MISSING_SDP, status_code: 400 }); break; } var e = { originator: 'remote', type: 'answer', sdp: request.body }; logger.debug('emit "sdp"'); this.emit('sdp', e); var answer = new RTCSessionDescription({ type: 'answer', sdp: e.sdp }); this._connectionPromiseQueue = this._connectionPromiseQueue.then(function () { return _this10._connection.setRemoteDescription(answer); }).then(function () { if (!_this10._is_confirmed) { _this10._confirmed('remote', request); } })["catch"](function (error) { _this10.terminate({ cause: JsSIP_C.causes.BAD_MEDIA_DESCRIPTION, status_code: 488 }); logger.warn('emit "peerconnection:setremotedescriptionfailed" [error:%o]', error); _this10.emit('peerconnection:setremotedescriptionfailed', error); }); } else if (!this._is_confirmed) { this._confirmed('remote', request); } break; case JsSIP_C.BYE: if (this._status === C.STATUS_CONFIRMED || this._status === C.STATUS_WAITING_FOR_ACK) { request.reply(200); this._ended('remote', request, JsSIP_C.causes.BYE); } else if (this._status === C.STATUS_INVITE_RECEIVED || this._status === C.STATUS_WAITING_FOR_ANSWER) { request.reply(200); this._request.reply(487, 'BYE Received'); this._ended('remote', request, JsSIP_C.causes.BYE); } else { request.reply(403, 'Wrong Status'); } break; case JsSIP_C.INVITE: if (this._status === C.STATUS_CONFIRMED) { if (request.hasHeader('replaces')) { this._receiveReplaces(request); } else { this._receiveReinvite(request); } } else { request.reply(403, 'Wrong Status'); } break; case JsSIP_C.INFO: if (this._status === C.STATUS_1XX_RECEIVED || this._status === C.STATUS_WAITING_FOR_ANSWER || this._status === C.STATUS_ANSWERED || this._status === C.STATUS_WAITING_FOR_ACK || this._status === C.STATUS_CONFIRMED) { var contentType = request.hasHeader('Content-Type') ? request.getHeader('Content-Type').toLowerCase() : undefined; if (contentType && contentType.match(/^application\/dtmf-relay/i)) { new RTCSession_DTMF(this).init_incoming(request); } else if (contentType !== undefined) { new RTCSession_Info(this).init_incoming(request); } else { request.reply(415); } } else { request.reply(403, 'Wrong Status'); } break; case JsSIP_C.UPDATE: if (this._status === C.STATUS_CONFIRMED) { this._receiveUpdate(request); } else { request.reply(403, 'Wrong Status'); } break; case JsSIP_C.REFER: if (this._status === C.STATUS_CONFIRMED) { this._receiveRefer(request); } else { request.reply(403, 'Wrong Status'); } break; case JsSIP_C.NOTIFY: if (this._status === C.STATUS_CONFIRMED) { this._receiveNotify(request); } else { request.reply(403, 'Wrong Status'); } break; default: request.reply(501); } } } /** * Session Callbacks */ }, { key: "onTransportError", value: function onTransportError() { logger.warn('onTransportError()'); if (this._status !== C.STATUS_TERMINATED) { this.terminate({ status_code: 500, reason_phrase: JsSIP_C.causes.CONNECTION_ERROR, cause: JsSIP_C.causes.CONNECTION_ERROR }); } } }, { key: "onRequestTimeout", value: function onRequestTimeout() { logger.warn('onRequestTimeout()'); if (this._status !== C.STATUS_TERMINATED) { this.terminate({ status_code: 408, reason_phrase: JsSIP_C.causes.REQUEST_TIMEOUT, cause: JsSIP_C.causes.REQUEST_TIMEOUT }); } } }, { key: "onDialogError", value: function onDialogError() { logger.warn('onDialogError()'); if (this._status !== C.STATUS_TERMINATED) { this.terminate({ status_code: 500, reason_phrase: JsSIP_C.causes.DIALOG_ERROR, cause: JsSIP_C.causes.DIALOG_ERROR }); } } // Called from DTMF handler. }, { key: "newDTMF", value: function newDTMF(data) { logger.debug('newDTMF()'); this.emit('newDTMF', data); } // Called from Info handler. }, { key: "newInfo", value: function newInfo(data) { logger.debug('newInfo()'); this.emit('newInfo', data); } /** * Check if RTCSession is ready for an outgoing re-INVITE or UPDATE with SDP. */ }, { key: "_isReadyToReOffer", value: function _isReadyToReOffer() { if (!this._rtcReady) { logger.debug('_isReadyToReOffer() | internal WebRTC status not ready'); return false; } // No established yet. if (!this._dialog) { logger.debug('_isReadyToReOffer() | session not established yet'); return false; } // Another INVITE transaction is in progress. if (this._dialog.uac_pending_reply === true || th