jssip
Version:
the Javascript SIP library
1,504 lines (1,182 loc) • 104 kB
JavaScript
"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