Source: RTCSession.js

module.exports = 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
};

/**
 * Expose C object.
 */
RTCSession.C = C;


/**
 * Dependencies.
 */
var debug = require('debug')('JsSIP:RTCSession');
var JsSIP_C = require('./Constants');
var EventEmitter = require('./EventEmitter');
var Exceptions = require('./Exceptions');
var Transactions = require('./Transactions');
var Parser = require('./Parser');
var Utils = require('./Utils');
var Timers = require('./Timers');
var UA = require('./UA');
var WebRTC = require('./WebRTC');
var SIPMessage = require('./SIPMessage');
var Dialog = require('./Dialog');
var RequestSender = require('./RequestSender');
var RTCSession_RTCMediaHandler = require('./RTCSession/RTCMediaHandler');
var RTCSession_Request = require('./RTCSession/Request');
var RTCSession_DTMF = require('./RTCSession/DTMF');


function RTCSession(ua) {
  var events = [
    'connecting',
    'progress',
    'failed',
    'accepted',
    'confirmed',
    'ended',
    'newDTMF',
    'hold',
    'unhold',
    'muted',
    'unmuted'
  ];

  this.ua = ua;
  this.status = C.STATUS_NULL;
  this.dialog = null;
  this.earlyDialogs = {};
  this.rtcMediaHandler = null;

  // RTCSession confirmation flag
  this.is_confirmed = false;

  // is late SDP being negotiated
  this.late_sdp = false;

  // Session 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.local_hold = false;
  this.remote_hold = false;

  this.pending_actions = {
    actions: [],

    length: function() {
      return this.actions.length;
    },

    isPending: function(name){
      var
        idx = 0,
        length = this.actions.length;

      for (idx; idx<length; idx++) {
        if (this.actions[idx].name === name) {
          return true;
        }
      }
      return false;
    },

    shift: function() {
      return this.actions.shift();
    },

    push: function(name) {
      this.actions.push({
        name: name
      });
    },

    pop: function(name) {
      var
        idx = 0,
        length = this.actions.length;

      for (idx; idx<length; idx++) {
        if (this.actions[idx].name === name) {
          this.actions.splice(idx,1);
        }
      }
    }
  };

  // Custom session empty object for high level use
  this.data = {};

  this.initEvents(events);
}

RTCSession.prototype = new EventEmitter();


/**
 * User API
 */

/**
 * Terminate the call.
 */
RTCSession.prototype.terminate = function(options) {
  options = options || {};

  var cancel_reason, dialog,
    cause = options.cause || JsSIP_C.causes.BYE,
    status_code = options.status_code,
    reason_phrase = options.reason_phrase,
    extraHeaders = options.extraHeaders && options.extraHeaders.slice() || [],
    body = options.body,
    self = this;

  // 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:
      debug('canceling sesssion');

      if (status_code && (status_code < 200 || status_code >= 700)) {
        throw new TypeError('Invalid status_code: '+ status_code);
      } else if (status_code) {
        reason_phrase = reason_phrase || JsSIP_C.REASON_PHRASE[status_code] || '';
        cancel_reason = 'SIP ;cause=' + status_code + ' ;text="' + reason_phrase + '"';
      }

      // Check Session Status
      if (this.status === C.STATUS_NULL) {
        this.isCanceled = true;
        this.cancelReason = cancel_reason;
      } else if (this.status === C.STATUS_INVITE_SENT) {
        this.isCanceled = true;
        this.cancelReason = 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:
      debug('rejecting session');

      status_code = status_code || 480;

      if (status_code < 300 || status_code >= 700) {
        throw new TypeError('Invalid status_code: '+ 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:
      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: '+ status_code);
      } else if (status_code) {
        extraHeaders.push('Reason: SIP ;cause=' + status_code + '; text="' + 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
        dialog = this.dialog;

        // Send the BYE as soon as the ACK is received...
        this.receiveRequest = function(request) {
          if(request.method === JsSIP_C.ACK) {
            this.sendRequest(JsSIP_C.BYE, {
              extraHeaders: extraHeaders,
              body: body
            });
            dialog.terminate();
          }
        };

        // .., or when the INVITE transaction times out
        this.request.server_transaction.on('stateChanged', function(e){
          if (e.sender.state === Transactions.C.STATUS_TERMINATED) {
            self.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.dialogs[dialog.id.toString()] = dialog;

      } else {
        this.sendRequest(JsSIP_C.BYE, {
          extraHeaders: extraHeaders,
          body: body
        });

        this.ended('local', null, cause);
      }
  }

  this.close();
};

/**
 * Answer the call.
 */
RTCSession.prototype.answer = function(options) {
  options = options || {};

  var idx, length, sdp, remoteDescription,
    hasAudio = false,
    hasVideo = false,
    self = this,
    request = this.request,
    extraHeaders = options.extraHeaders && options.extraHeaders.slice() || [],
    mediaConstraints = options.mediaConstraints || {},
    RTCAnswerConstraints = options.RTCAnswerConstraints || {},
    mediaStream = options.mediaStream || null,

  // User media succeeded
  userMediaSucceeded = function(stream) {
    self.rtcMediaHandler.addStream(
      stream,
      streamAdditionSucceeded,
      streamAdditionFailed
    );
  },

  // User media failed
  userMediaFailed = function() {
    request.reply(480);
    self.failed('local', null, JsSIP_C.causes.USER_DENIED_MEDIA_ACCESS);
  },

  // rtcMediaHandler.addStream successfully added
  streamAdditionSucceeded = function() {
    self.connecting(request);

    if (self.status === C.STATUS_TERMINATED) {
      return;
    }

    if (self.late_sdp) {
      self.rtcMediaHandler.createOffer(
        sdpCreationSucceeded,
        sdpCreationFailed,
        RTCAnswerConstraints
      );
    } else {
      self.rtcMediaHandler.createAnswer(
        sdpCreationSucceeded,
        sdpCreationFailed,
        RTCAnswerConstraints
      );
    }
  },

  // rtcMediaHandler.addStream failed
  streamAdditionFailed = function() {
    if (self.status === C.STATUS_TERMINATED) {
      return;
    }

    self.failed('system', null, JsSIP_C.causes.WEBRTC_ERROR);
  },

  // rtcMediaHandler.createAnswer or rtcMediaHandler.createOffer succeeded
  sdpCreationSucceeded = function(body) {
    var
      // run for reply success callback
      replySucceeded = function() {
        self.status = C.STATUS_WAITING_FOR_ACK;

        self.setInvite2xxTimer(request, body);
        self.setACKTimer();
        self.accepted('local');
      },

      // run for reply failure callback
      replyFailed = function() {
        self.failed('system', null, JsSIP_C.causes.CONNECTION_ERROR);
      };

    request.reply(200, null, extraHeaders,
      body,
      replySucceeded,
      replyFailed
    );
  },

  // rtcMediaHandler.createAnswer or rtcMediaHandler.createOffer failed
  sdpCreationFailed = function() {
    if (self.status === C.STATUS_TERMINATED) {
      return;
    }

    self.failed('system', null, JsSIP_C.causes.WEBRTC_ERROR);
  };

  this.data = options.data || {};

  // Check Session Direction and Status
  if (this.direction !== 'incoming') {
    throw new Exceptions.NotSupportedError('"answer" not supported for outgoing RTCSession');
  } else if (this.status !== C.STATUS_WAITING_FOR_ANSWER) {
    throw new Exceptions.InvalidStateError(this.status);
  }

  this.status = C.STATUS_ANSWERED;

  // An error on dialog creation will fire 'failed' event
  if(!this.createDialog(request, 'UAS')) {
    request.reply(500, 'Missing Contact header field');
    return;
  }

  clearTimeout(this.timers.userNoAnswerTimer);

  extraHeaders.unshift('Contact: ' + self.contact);

  // Determine incoming media from remote session description
  remoteDescription = this.rtcMediaHandler.peerConnection.remoteDescription || {};
  sdp = Parser.parseSDP(remoteDescription.sdp || '');

  // Make sure sdp is an array, not the case if there is only one media
  if(!(sdp.media instanceof Array)) {
    sdp.media = [sdp.media || []];
  }

  // Go through all medias in SDP to find offered capabilities to answer with
  idx = sdp.media.length;
  while(idx--) {
    if(sdp.media[idx].type === 'audio' &&
        (sdp.media[idx].direction === 'sendrecv' ||
         sdp.media[idx].direction === 'recvonly')) {
      hasAudio=true;
    }
    if(sdp.media[idx].type === 'video' &&
        (sdp.media[idx].direction === 'sendrecv' ||
         sdp.media[idx].direction === 'recvonly')) {
      hasVideo=true;
    }
  }

  // Remove audio from mediaStream if suggested by mediaConstraints
   if (mediaStream && mediaConstraints.audio === false) {
    length = mediaStream.getAudioTracks().length;
    for (idx=0; idx<length; idx++) {
      mediaStream.removeTrack(mediaStream.getAudioTracks()[idx]);
    }
  }

  // Remove video from mediaStream if suggested by mediaConstraints
  if (mediaStream && mediaConstraints.video === false) {
    length = mediaStream.getVideoTracks().length;
    for (idx=0; idx<length; idx++) {
      mediaStream.removeTrack(mediaStream.getVideoTracks()[idx]);
    }
  }

  // Set audio constraints based on incoming stream if not supplied
  if (mediaConstraints.audio === undefined) {
      mediaConstraints.audio = hasAudio;
  }

  // Set video constraints based on incoming stream if not supplied
  if (mediaConstraints.video === undefined) {
      mediaConstraints.video = hasVideo;
  }

  if (mediaStream) {
    userMediaSucceeded(mediaStream);
  } else {
    this.rtcMediaHandler.getUserMedia(
      userMediaSucceeded,
      userMediaFailed,
      mediaConstraints
    );
  }
};

/**
 * Send a DTMF
 */
RTCSession.prototype.sendDTMF = function(tones, options) {
  var duration, interToneGap,
    position = 0,
    self = this;

  options = options || {};
  duration = options.duration || null;
  interToneGap = options.interToneGap || null;

  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) {
    throw new Exceptions.InvalidStateError(this.status);
  }

  // Convert to string
  if(typeof tones === 'number') {
    tones = tones.toString();
  }

  // Check tones
  if (!tones || typeof tones !== 'string' || !tones.match(/^[0-9A-D#*,]+$/i)) {
    throw new TypeError('Invalid tones: '+ tones);
  }

  // Check duration
  if (duration && !Utils.isDecimal(duration)) {
    throw new TypeError('Invalid tone duration: '+ duration);
  } else if (!duration) {
    duration = RTCSession_DTMF.C.DEFAULT_DURATION;
  } else if (duration < RTCSession_DTMF.C.MIN_DURATION) {
    debug('"duration" value is lower than the minimum allowed, setting it to '+ RTCSession_DTMF.C.MIN_DURATION+ ' milliseconds');
    duration = RTCSession_DTMF.C.MIN_DURATION;
  } else if (duration > RTCSession_DTMF.C.MAX_DURATION) {
    debug('"duration" value is greater than the maximum allowed, setting it to '+ 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: '+ interToneGap);
  } else if (!interToneGap) {
    interToneGap = RTCSession_DTMF.C.DEFAULT_INTER_TONE_GAP;
  } else if (interToneGap < RTCSession_DTMF.C.MIN_INTER_TONE_GAP) {
    debug('"interToneGap" value is lower than the minimum allowed, setting it to '+ RTCSession_DTMF.C.MIN_INTER_TONE_GAP +' milliseconds');
    interToneGap = RTCSession_DTMF.C.MIN_INTER_TONE_GAP;
  } else {
    interToneGap = Math.abs(interToneGap);
  }

  if (this.tones) {
    // Tones are already queued, just add to the queue
    this.tones += tones;
    return;
  }

  // New set of tones to start sending
  this.tones = tones;

  var sendDTMF = function () {
    var tone, timeout,
      tones = self.tones;

    if (self.status === C.STATUS_TERMINATED || !tones || position >= tones.length) {
      // Stop sending DTMF
      self.tones = null;
      return;
    }

    tone = tones[position];
    position += 1;

    if (tone === ',') {
      timeout = 2000;
    } else {
      var dtmf = new RTCSession_DTMF(self);
      dtmf.on('failed', function(){self.tones = null;});
      dtmf.send(tone, options);
      timeout = duration + interToneGap;
    }

    // Set timeout for the next tone
    setTimeout(sendDTMF, timeout);
  };

  // Send the first tone
  sendDTMF();
};

/**
 * Send a generic in-dialog Request
 */
RTCSession.prototype.sendRequest = function(method, options) {
  var request = new RTCSession_Request(this);

  request.send(method, options);
};

/**
 * Check if RTCSession is ready for a re-INVITE
 */
RTCSession.prototype.isReadyToReinvite = function() {
  // rtcMediaHandler is not ready
  if (!this.rtcMediaHandler.isReady()) {
    return;
  }

  // Another INVITE transaction is in progress
  if (this.dialog.uac_pending_reply === true || this.dialog.uas_pending_reply === true) {
    return false;
  } else {
    return true;
  }
};


/**
 * Mute
 */
RTCSession.prototype.mute = function(options) {
  options = options || {audio:true, video:false};

  var
    audioMuted = false,
    videoMuted = false;

  if (this.audioMuted === false && options.audio) {
    audioMuted = true;
    this.audioMuted = true;
    this.toogleMuteAudio(true);
  }

  if (this.videoMuted === false && options.video) {
    videoMuted = true;
    this.videoMuted = true;
    this.toogleMuteVideo(true);
  }

  if (audioMuted === true || videoMuted === true) {
    this.onmute({
      audio: audioMuted,
      video: videoMuted
    });
  }
};

/**
 * Unmute
 */
RTCSession.prototype.unmute = function(options) {
  options = options || {audio:true, video:true};

  var
    audioUnMuted = false,
    videoUnMuted = false;

  if (this.audioMuted === true && options.audio) {
    audioUnMuted = true;
    this.audioMuted = false;

    if (this.local_hold === false) {
      this.toogleMuteAudio(false);
    }
  }

  if (this.videoMuted === true && options.video) {
    videoUnMuted = true;
    this.videoMuted = false;

    if (this.local_hold === false) {
      this.toogleMuteVideo(false);
    }
  }

  if (audioUnMuted === true || videoUnMuted === true) {
    this.onunmute({
      audio: audioUnMuted,
      video: videoUnMuted
    });
  }
};

/**
 * isMuted
 */
RTCSession.prototype.isMuted = function() {
  return {
    audio: this.audioMuted,
    video: this.videoMuted
  };
};

/**
 * Hold
 */
RTCSession.prototype.hold = function() {

  if (this.status !== C.STATUS_WAITING_FOR_ACK && this.status !== C.STATUS_CONFIRMED) {
    throw new Exceptions.InvalidStateError(this.status);
  }

  this.toogleMuteAudio(true);
  this.toogleMuteVideo(true);

  if (!this.isReadyToReinvite()) {
    /* If there is a pending 'unhold' action, cancel it and don't queue this one
     * Else, if there isn't any 'hold' action, add this one to the queue
     * Else, if there is already a 'hold' action, skip
     */
    if (this.pending_actions.isPending('unhold')) {
      this.pending_actions.pop('unhold');
      return;
    } else if (!this.pending_actions.isPending('hold')) {
      this.pending_actions.push('hold');
      return;
    } else {
      return;
    }
  } else {
    if (this.local_hold === true) {
      return;
    }
  }

  this.onhold('local');

  this.sendReinvite({
    mangle: function(body){
      var idx, length;

      body = Parser.parseSDP(body);

      length = body.media.length;
      for (idx=0; idx<length; idx++) {
        if (body.media[idx].direction === undefined) {
          body.media[idx].direction = 'sendonly';
        } else if (body.media[idx].direction === 'sendrecv') {
          body.media[idx].direction = 'sendonly';
        } else if (body.media[idx].direction === 'sendonly') {
          body.media[idx].direction = 'inactive';
        }
      }

      return Parser.writeSDP(body);
    }
  });
};

/**
 * Unhold
 */
RTCSession.prototype.unhold = function() {

  if (this.status !== C.STATUS_WAITING_FOR_ACK && this.status !== C.STATUS_CONFIRMED) {
    throw new Exceptions.InvalidStateError(this.status);
  }

  if (!this.audioMuted) {
    this.toogleMuteAudio(false);
  }

  if (!this.videoMuted) {
    this.toogleMuteVideo(false);
  }

  if (!this.isReadyToReinvite()) {
    /* If there is a pending 'hold' action, cancel it and don't queue this one
     * Else, if there isn't any 'unhold' action, add this one to the queue
     * Else, if there is already an 'unhold' action, skip
     */
    if (this.pending_actions.isPending('hold')) {
      this.pending_actions.pop('hold');
      return;
    } else if (!this.pending_actions.isPending('unhold')) {
      this.pending_actions.push('unhold');
      return;
    } else {
      return;
    }
  } else {
    if (this.local_hold === false) {
      return;
    }
  }

  this.onunhold('local');

  this.sendReinvite();
};

/**
 * isOnHold
 */
RTCSession.prototype.isOnHold = function() {
  return {
    local: this.local_hold,
    remote: this.remote_hold
  };
};


/**
 * Session Timers
 */


/**
 * RFC3261 13.3.1.4
 * Response retransmissions cannot be accomplished by transaction layer
 *  since it is destroyed when receiving the first 2xx answer
 */
RTCSession.prototype.setInvite2xxTimer = function(request, body) {
  var
    self = this,
    timeout = Timers.T1;

  this.timers.invite2xxTimer = setTimeout(function invite2xxRetransmission() {
    if (self.status !== C.STATUS_WAITING_FOR_ACK) {
      return;
    }

    request.reply(200, null, ['Contact: '+ self.contact], body);

    if (timeout < Timers.T2) {
      timeout = timeout * 2;
      if (timeout > Timers.T2) {
        timeout = Timers.T2;
      }
    }
    self.timers.invite2xxTimer = setTimeout(
      invite2xxRetransmission, timeout
    );
  }, timeout);
};


/**
 * RFC3261 14.2
 * If a UAS generates a 2xx response and never receives an ACK,
 *  it SHOULD generate a BYE to terminate the dialog.
 */
RTCSession.prototype.setACKTimer = function() {
  var self = this;

  this.timers.ackTimer = setTimeout(function() {
    if(self.status === C.STATUS_WAITING_FOR_ACK) {
      debug('no ACK received, terminating the session');
      clearTimeout(self.timers.invite2xxTimer);
      self.sendRequest(JsSIP_C.BYE);
      self.ended('remote', null, JsSIP_C.causes.NO_ACK);
    }
  }, Timers.TIMER_H);
};



/**
 * RTCPeerconnection handlers
 */
RTCSession.prototype.getLocalStreams = function() {
  return this.rtcMediaHandler &&
    this.rtcMediaHandler.peerConnection &&
    this.rtcMediaHandler.peerConnection.getLocalStreams() || [];
};

RTCSession.prototype.getRemoteStreams = function() {
  return this.rtcMediaHandler &&
    this.rtcMediaHandler.peerConnection &&
    this.rtcMediaHandler.peerConnection.getRemoteStreams() || [];
};


/**
 * Session Management
 */

RTCSession.prototype.init_incoming = function(request) {
  var expires,
    self = this,
    contentType = request.getHeader('Content-Type'),

    waitForAnswer =  function() {
      self.status = C.STATUS_WAITING_FOR_ANSWER;

      // Set userNoAnswerTimer
      self.timers.userNoAnswerTimer = setTimeout(function() {
          request.reply(408);
          self.failed('local',null, JsSIP_C.causes.NO_ANSWER);
        }, self.ua.configuration.no_answer_timeout
      );

      /* Set expiresTimer
       * RFC3261 13.3.1
       */
      if (expires) {
        self.timers.expiresTimer = setTimeout(function() {
            if(self.status === C.STATUS_WAITING_FOR_ANSWER) {
              request.reply(487);
              self.failed('system', null, JsSIP_C.causes.EXPIRES);
            }
          }, expires
        );
      }

      // Fire 'newRTCSession' event.
      self.newRTCSession('remote', request);

      // Reply 180.
      request.reply(180, null, ['Contact: ' + self.contact]);

      // Fire 'progress' event.
      // TODO: Document that 'response' field in 'progress' event is null for
      // incoming calls.
      self.progress('local', null);
    };

  // 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();

  //Save the session into the ua sessions collection.
  this.ua.sessions[this.id] = this;

  //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;
  }

  //Initialize Media Session
  this.rtcMediaHandler = new RTCSession_RTCMediaHandler(this, {
    constraints: {"optional": [{'DtlsSrtpKeyAgreement': 'true'}]}
    });

  if (request.body) {
    this.rtcMediaHandler.onMessage(
      'offer',
      request.body,
      /*
       * onSuccess
       * SDP Offer is valid. Fire UA newRTCSession
       */
      waitForAnswer,
      /*
       * onFailure
       * Bad media description
       */
      function(e) {
        debug('invalid SDP: ' + e);
        request.reply(488);
      }
    );
  } else {
    this.late_sdp = true;
    waitForAnswer();
  }
};

RTCSession.prototype.connect = function(target, options) {
  options = options || {};

  var event, requestParams, iceServers,
    originalTarget = target,
    eventHandlers = options.eventHandlers || {},
    extraHeaders = options.extraHeaders && options.extraHeaders.slice() || [],
    mediaConstraints = options.mediaConstraints || {audio: true, video: true},
    mediaStream = options.mediaStream || null,
    RTCConstraints = options.RTCConstraints || {},
    RTCOfferConstraints = options.RTCOfferConstraints || {},
    stun_servers = options.stun_servers || null,
    turn_servers = options.turn_servers || null;

  this.data = options.data || {};

  if (stun_servers) {
    iceServers = UA.configuration_check.optional.stun_servers(stun_servers);
    if (!iceServers) {
      throw new TypeError('Invalid stun_servers: '+ stun_servers);
    } else {
      stun_servers = iceServers;
    }
  }

  if (turn_servers) {
    iceServers = UA.configuration_check.optional.turn_servers(turn_servers);
    if (!iceServers){
      throw new TypeError('Invalid turn_servers: '+ turn_servers);
    } else {
      turn_servers = iceServers;
    }
  }

  if (target === undefined) {
    throw new TypeError('Not enough arguments');
  }

  // Check WebRTC support
  if (!WebRTC.isSupported) {
    throw new Exceptions.NotSupportedError('WebRTC not supported');
  }

  // Check target validity
  target = this.ua.normalizeTarget(target);
  if (!target) {
    throw new TypeError('Invalid target: '+ originalTarget);
  }

  // Check Session Status
  if (this.status !== C.STATUS_NULL) {
    throw new Exceptions.InvalidStateError(this.status);
  }

  // Set event handlers
  for (event in eventHandlers) {
    this.on(event, eventHandlers[event]);
  }

  // Session parameter initialization
  this.from_tag = Utils.newTag();

  // Set anonymous property
  this.anonymous = options.anonymous || false;

  // OutgoingSession specific parameters
  this.isCanceled = false;

  requestParams = {from_tag: this.from_tag};

  this.contact = this.ua.contact.toString({
    anonymous: this.anonymous,
    outbound: true
  });

  if (this.anonymous) {
    requestParams.from_display_name = 'Anonymous';
    requestParams.from_uri = 'sip:anonymous@anonymous.invalid';

    extraHeaders.push('P-Preferred-Identity: '+ this.ua.configuration.uri.toString());
    extraHeaders.push('Privacy: id');
  }

  extraHeaders.push('Contact: '+ this.contact);
  extraHeaders.push('Content-Type: application/sdp');

  this.request = new SIPMessage.OutgoingRequest(JsSIP_C.INVITE, target, this.ua, requestParams, extraHeaders);

  this.id = this.request.call_id + this.from_tag;

  this.rtcMediaHandler = new RTCSession_RTCMediaHandler(this, {
    constraints: RTCConstraints,
    stun_servers: stun_servers,
    turn_servers: turn_servers
    });

  //Save the session into the ua sessions collection.
  this.ua.sessions[this.id] = this;

  this.newRTCSession('local', this.request);

  this.sendInitialRequest(mediaConstraints, RTCOfferConstraints, mediaStream);
};

RTCSession.prototype.close = function() {
  var idx;

  if(this.status === C.STATUS_TERMINATED) {
    return;
  }

  debug('closing INVITE session ' + this.id);

  // 1st Step. Terminate media.
  if (this.rtcMediaHandler){
    this.rtcMediaHandler.close();
  }

  // 2nd Step. Terminate signaling.

  // Clear session timers
  for(idx in this.timers) {
    clearTimeout(this.timers[idx]);
  }

  // Terminate dialogs

  // Terminate confirmed dialog
  if(this.dialog) {
    this.dialog.terminate();
    delete this.dialog;
  }

  // Terminate early dialogs
  for(idx in this.earlyDialogs) {
    this.earlyDialogs[idx].terminate();
    delete this.earlyDialogs[idx];
  }

  this.status = C.STATUS_TERMINATED;

  delete this.ua.sessions[this.id];
};

/**
 * Dialog Management
 */
RTCSession.prototype.createDialog = function(message, type, early) {
  var dialog, early_dialog,
    local_tag = (type === 'UAS') ? message.to_tag : message.from_tag,
    remote_tag = (type === 'UAS') ? message.from_tag : message.to_tag,
    id = message.call_id + local_tag + remote_tag;

    early_dialog = this.earlyDialogs[id];

  // Early Dialog
  if (early) {
    if (early_dialog) {
      return true;
    } else {
      early_dialog = new Dialog(this, message, type, Dialog.C.STATUS_EARLY);

      // Dialog has been successfully created.
      if(early_dialog.error) {
        debug(dialog.error);
        this.failed('remote', message, JsSIP_C.causes.INTERNAL_ERROR);
        return false;
      } else {
        this.earlyDialogs[id] = early_dialog;
        return true;
      }
    }
  }

  // Confirmed Dialog
  else {
    // In case the dialog is in _early_ state, update it
    if (early_dialog) {
      early_dialog.update(message, type);
      this.dialog = early_dialog;
      delete this.earlyDialogs[id];
      return true;
    }

    // Otherwise, create a _confirmed_ dialog
    dialog = new Dialog(this, message, type);

    if(dialog.error) {
      debug(dialog.error);
      this.failed('remote', message, JsSIP_C.causes.INTERNAL_ERROR);
      return false;
    } else {
      this.to_tag = message.to_tag;
      this.dialog = dialog;
      return true;
    }
  }
};

/**
 * In dialog INVITE Reception
 */

RTCSession.prototype.receiveReinvite = function(request) {
  var
    sdp, idx, direction,
    self = this,
    contentType = request.getHeader('Content-Type'),
    hold = false,

    createSdp = function(onSuccess, onFailure) {
      if (self.late_sdp) {
        self.rtcMediaHandler.createOffer(onSuccess, onFailure);
      } else {
        self.rtcMediaHandler.createAnswer(onSuccess, onFailure);
      }
    },

    answer =  function() {
      createSdp(
        // onSuccess
        function(body) {
          request.reply(200, null, ['Contact: ' + self.contact], body,
            function() {
              self.status = C.STATUS_WAITING_FOR_ACK;
              self.setInvite2xxTimer(request, body);
              self.setACKTimer();

              if (self.remote_hold === true && hold === false) {
                self.onunhold('remote');
              } else if (self.remote_hold === false && hold === true) {
                self.onhold('remote');
              }
            }
          );
        },
        // onFailure
        function() {
          request.reply(500);
        }
      );
    };


  if (request.body) {
    if (contentType !== 'application/sdp') {
      debug('invalid Content-Type');
      request.reply(415);
      return;
    }

    sdp = Parser.parseSDP(request.body);

    for (idx=0; idx < sdp.media.length; idx++) {
      direction = sdp.direction || sdp.media[idx].direction || 'sendrecv';

      if (direction === 'sendonly' || direction === 'inactive') {
        hold = true;
      }
    }

    this.rtcMediaHandler.onMessage(
      'offer',
      request.body,
      /*
      * onSuccess
      * SDP Offer is valid
      */
      answer,
      /*
       * onFailure
       * Bad media description
       */
      function(e) {
        debug(e);
        request.reply(488);
      }
    );
  } else {
    this.late_sdp = true;
    answer();
  }
};

/**
 * In dialog UPDATE Reception
 */

RTCSession.prototype.receiveUpdate = function(request) {
  var
    sdp, idx, direction,
    self = this,
    contentType = request.getHeader('Content-Type'),
    hold = true;

  if (! request.body) {
    request.reply(200);
    return;
  }

  if (contentType !== 'application/sdp') {
    debug('invalid Content-Type');
    request.reply(415);
    return;
  }

  sdp = Parser.parseSDP(request.body);

  for (idx=0; idx < sdp.media.length; idx++) {
    direction = sdp.direction || sdp.media[idx].direction || 'sendrecv';

    if (direction !== 'sendonly' && direction !== 'inactive') {
      hold = false;
    }
  }

  this.rtcMediaHandler.onMessage(
    'offer',
    request.body,
    /*
    * onSuccess
    * SDP Offer is valid
    */
    function() {
      self.rtcMediaHandler.createAnswer(
        function(body) {
          request.reply(200, null, ['Contact: ' + self.contact], body,
            function() {
              if (self.remote_hold === true && hold === false) {
                self.onunhold('remote');
              } else if (self.remote_hold === false && hold === true) {
                self.onhold('remote');
              }
            }
          );
        },
        function() {
          request.reply(500);
        }
      );
    },
    /*
     * onFailure
     * Bad media description
     */
    function(e) {
      debug(e);
      request.reply(488);
    }
  );
};

/**
 * In dialog Request Reception
 */
RTCSession.prototype.receiveRequest = function(request) {
  var contentType,
      self = this;

  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) {
          clearTimeout(this.timers.ackTimer);
          clearTimeout(this.timers.invite2xxTimer);

          if (this.late_sdp) {
            if (!request.body) {
              self.ended('remote', request, JsSIP_C.causes.MISSING_SDP);
              break;
            }

            this.rtcMediaHandler.onMessage(
              'answer',
              request.body,
              /*
               * onSuccess
               * SDP Answer fits with Offer. Media will start
               */
              function() {
                self.late_sdp = false;
                self.status = C.STATUS_CONFIRMED;
              },
              /*
               * onFailure
               * SDP Answer does not fit the Offer. Accept the call and Terminate.
               */
              function(e) {
                debug(e);
                self.ended('remote', request, JsSIP_C.causes.BAD_MEDIA_DESCRIPTION);
              }
            );
          } else {
            this.status = C.STATUS_CONFIRMED;
          }

          if (this.status === C.STATUS_CONFIRMED && !this.is_confirmed) {
            this.confirmed('remote', request);
          }
        }
        break;
      case JsSIP_C.BYE:
        if(this.status === C.STATUS_CONFIRMED) {
          request.reply(200);
          this.ended('remote', request, JsSIP_C.causes.BYE);
        }
        else if (this.status === C.STATUS_INVITE_RECEIVED) {
          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) {
          debug('re-INVITE received');
          this.receiveReinvite(request);
        }
        else {
          request.reply(403, 'Wrong Status');
        }
        break;
      case JsSIP_C.INFO:
        if(this.status === C.STATUS_CONFIRMED || this.status === C.STATUS_WAITING_FOR_ACK || this.status === C.STATUS_INVITE_RECEIVED) {
          contentType = request.getHeader('content-type');
          if (contentType && (contentType.match(/^application\/dtmf-relay/i))) {
            new RTCSession_DTMF(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) {
          debug('UPDATE received');
          this.receiveUpdate(request);
        }
        else {
          request.reply(403, 'Wrong Status');
        }
        break;
      default:
        request.reply(501);
    }
  }
};


/**
 * Initial Request Sender
 */
RTCSession.prototype.sendInitialRequest = function(mediaConstraints, RTCOfferConstraints, mediaStream) {
  var
  self = this,
 request_sender = new RequestSender(self, this.ua),

 // User media succeeded
 userMediaSucceeded = function(stream) {
   self.rtcMediaHandler.addStream(
     stream,
     streamAdditionSucceeded,
     streamAdditionFailed
   );
 },

 // User media failed
 userMediaFailed = function() {
   if (self.status === C.STATUS_TERMINATED) {
     return;
   }

   self.failed('local', null, JsSIP_C.causes.USER_DENIED_MEDIA_ACCESS);
 },

 // rtcMediaHandler.addStream successfully added
 streamAdditionSucceeded = function() {
   self.connecting(self.request);

   if (self.status === C.STATUS_TERMINATED) {
     return;
   }

   self.rtcMediaHandler.createOffer(
     offerCreationSucceeded,
     offerCreationFailed,
     RTCOfferConstraints
   );
 },

 // rtcMediaHandler.addStream failed
 streamAdditionFailed = function() {
   if (self.status === C.STATUS_TERMINATED) {
     return;
   }

   self.failed('system', null, JsSIP_C.causes.WEBRTC_ERROR);
 },

 // rtcMediaHandler.createOffer succeeded
 offerCreationSucceeded = function(offer) {
   if (self.isCanceled || self.status === C.STATUS_TERMINATED) {
     return;
   }

   self.request.body = offer;
   self.status = C.STATUS_INVITE_SENT;
   request_sender.send();
 },

 // rtcMediaHandler.createOffer failed
 offerCreationFailed = function() {
   if (self.status === C.STATUS_TERMINATED) {
     return;
   }

   self.failed('system', null, JsSIP_C.causes.WEBRTC_ERROR);
 };

 this.receiveResponse = this.receiveInviteResponse;

 if (mediaStream) {
   userMediaSucceeded(mediaStream);
 } else {
   this.rtcMediaHandler.getUserMedia(
     userMediaSucceeded,
     userMediaFailed,
     mediaConstraints
   );
 }
};

/**
 * Send Re-INVITE
 */
RTCSession.prototype.sendReinvite = function(options) {
  options = options || {};

  var
    self = this,
    extraHeaders = options.extraHeaders || [],
    eventHandlers = options.eventHandlers || {},
    mangle = options.mangle || null;

  if (eventHandlers.succeeded) {
    this.reinviteSucceeded = eventHandlers.succeeded;
  } else {
    this.reinviteSucceeded = function(){};
  }
  if (eventHandlers.failed) {
    this.reinviteFailed = eventHandlers.failed;
  } else {
    this.reinviteFailed = function(){};
  }

  extraHeaders.push('Contact: ' + this.contact);
  extraHeaders.push('Content-Type: application/sdp');

  this.receiveResponse = this.receiveReinviteResponse;

  this.rtcMediaHandler.createOffer(
    function(body){
      if (mangle) {
        body = mangle(body);
      }

      self.dialog.sendRequest(self, JsSIP_C.INVITE, {
        extraHeaders: extraHeaders,
        body: body
      });
    },
    function() {
      if (self.isReadyToReinvite()) {
        self.onReadyToReinvite();
      }
      self.reinviteFailed();
    }
  );
};


/**
 * Reception of Response for Initial INVITE
 */
RTCSession.prototype.receiveInviteResponse = function(response) {
  var cause, dialog,
    session = this;

  // Handle 2XX retransmissions and responses from forked requests
  if (this.dialog && (response.status_code >=200 && response.status_code <=299)) {

    /*
     * If it is a retransmission from the endpoint that established
     * the dialog, send an ACK
     */
    if (this.dialog.id.call_id === response.call_id &&
        this.dialog.id.local_tag === response.from_tag &&
        this.dialog.id.remote_tag === response.to_tag) {
      this.sendRequest(JsSIP_C.ACK);
      return;
    }

    // If not, send an ACK  and terminate
    else  {
      dialog = new Dialog(this, response, 'UAC');

      if (dialog.error !== undefined) {
        debug(dialog.error);
        return;
      }

      dialog.sendRequest({
          owner: {status: C.STATUS_TERMINATED},
          onRequestTimeout: function(){},
          onTransportError: function(){},
          onDialogError: function(){},
          receiveResponse: function(){}
        }, JsSIP_C.ACK);

      dialog.sendRequest({
          owner: {status: C.STATUS_TERMINATED},
          onRequestTimeout: function(){},
          onTransportError: function(){},
          onDialogError: function(){},
          receiveResponse: function(){}
        }, JsSIP_C.BYE);
      return;
    }

  }

  // Proceed to cancellation if the user requested.
  if(this.isCanceled) {
    // Remove the flag. We are done.
    this.isCanceled = false;

    if(response.status_code >= 100 && response.status_code < 200) {
      this.request.cancel(this.cancelReason);
    } else if(response.status_code >= 200 && response.status_code < 299) {
      this.acceptAndTerminate(response);
    }
    return;
  }

  if(this.status !== C.STATUS_INVITE_SENT && this.status !== C.STATUS_1XX_RECEIVED) {
    return;
  }

  switch(true) {
    case /^100$/.test(response.status_code):
      break;
    case /^1[0-9]{2}$/.test(response.status_code):
      if(this.status !== C.STATUS_INVITE_SENT && this.status !== C.STATUS_1XX_RECEIVED) {
        break;
      }

      // Do nothing with 1xx responses without To tag.
      if(!response.to_tag) {
        debug('1xx response received without to tag');
        break;
      }

      // Create Early Dialog if 1XX comes with contact
      if(response.hasHeader('contact')) {
        // An error on dialog creation will fire 'failed' event
        if(!this.createDialog(response, 'UAC', true)) {
          break;
        }
      }

      this.status = C.STATUS_1XX_RECEIVED;
      this.progress('remote', response);

      if (!response.body) {
        break;
      }

      this.rtcMediaHandler.onMessage(
        'pranswer',
        response.body,
        /*
        * OnSuccess.
        * SDP Answer fits with Offer.
        */
        function() { },
        /*
        * OnFailure.
        * SDP Answer does not fit with Offer.
        */
        function(e) {
          debug(e);
          session.earlyDialogs[response.call_id + response.from_tag + response.to_tag].terminate();
        }
      );
      break;
    case /^2[0-9]{2}$/.test(response.status_code):
      this.status = C.STATUS_CONFIRMED;

      if(!response.body) {
        this.acceptAndTerminate(response, 400, JsSIP_C.causes.MISSING_SDP);
        this.failed('remote', response, JsSIP_C.causes.BAD_MEDIA_DESCRIPTION);
        break;
      }

      // An error on dialog creation will fire 'failed' event
      if (!this.createDialog(response, 'UAC')) {
        break;
      }

      this.rtcMediaHandler.onMessage(
        'answer',
        response.body,
        /*
         * onSuccess
         * SDP Answer fits with Offer. Media will start
         */
        function() {
          session.accepted('remote', response);
          session.sendRequest(JsSIP_C.ACK);
          session.confirmed('local', null);
        },
        /*
         * onFailure
         * SDP Answer does not fit the Offer. Accept the call and Terminate.
         */
        function(e) {
          debug(e);
          session.acceptAndTerminate(response, 488, 'Not Acceptable Here');
          session.failed('remote', response, JsSIP_C.causes.BAD_MEDIA_DESCRIPTION);
        }
      );
      break;
    default:
      cause = Utils.sipErrorCause(response.status_code);
      this.failed('remote', response, cause);
  }
};

/**
 * Reception of Response for in-dialog INVITE
 */
RTCSession.prototype.receiveReinviteResponse = function(response) {
  var
    self = this,
    contentType = response.getHeader('Content-Type');

  if (this.status === C.STATUS_TERMINATED) {
    return;
  }

  switch(true) {
    case /^1[0-9]{2}$/.test(response.status_code):
      break;
    case /^2[0-9]{2}$/.test(response.status_code):
      this.status = C.STATUS_CONFIRMED;
      this.sendRequest(JsSIP_C.ACK);

      if(!response.body) {
        this.reinviteFailed();
        break;
      } else if (contentType !== 'application/sdp') {
        this.reinviteFailed();
        break;
      }

      this.rtcMediaHandler.onMessage(
        'answer',
        response.body,
        /*
         * onSuccess
         * SDP Answer fits with Offer.
         */
        function() {
          self.reinviteSucceeded();
        },
        /*
         * onFailure
         * SDP Answer does not fit the Offer.
         */
        function() {
          self.reinviteFailed();
        }
      );
      break;
    default:
      this.reinviteFailed();
  }
};



RTCSession.prototype.acceptAndTerminate = function(response, status_code, reason_phrase) {
  var extraHeaders = [];

  if (status_code) {
    reason_phrase = reason_phrase || JsSIP_C.REASON_PHRASE[status_code] || '';
    extraHeaders.push('Reason: SIP ;cause=' + status_code + '; text="' + reason_phrase + '"');
  }

  // An error on dialog creation will fire 'failed' event
  if (this.dialog || this.createDialog(response, 'UAC')) {
    this.sendRequest(JsSIP_C.ACK);
    this.sendRequest(JsSIP_C.BYE, {
      extraHeaders: extraHeaders
    });
  }

  // Update session status.
  this.status = C.STATUS_TERMINATED;
};


RTCSession.prototype.toogleMuteAudio = function(mute) {
  var streamIdx, trackIdx, tracks,
    localStreams = this.getLocalStreams();

  for (streamIdx in localStreams) {
    tracks = localStreams[streamIdx].getAudioTracks();
    for (trackIdx in tracks) {
      tracks[trackIdx].enabled = !mute;
    }
  }
};

RTCSession.prototype.toogleMuteVideo = function(mute) {
  var streamIdx, trackIdx, tracks,
    localStreams = this.getLocalStreams();

  for (streamIdx in localStreams) {
    tracks = localStreams[streamIdx].getVideoTracks();
    for (trackIdx in tracks) {
      tracks[trackIdx].enabled = !mute;
    }
  }
};

/**
 * Session Callbacks
 */

RTCSession.prototype.onTransportError = function() {
  if(this.status !== C.STATUS_TERMINATED) {
    if (this.status === C.STATUS_CONFIRMED) {
      this.ended('system', null, JsSIP_C.causes.CONNECTION_ERROR);
    } else {
      this.failed('system', null, JsSIP_C.causes.CONNECTION_ERROR);
    }
  }
};

RTCSession.prototype.onRequestTimeout = function() {
  if(this.status !== C.STATUS_TERMINATED) {
    if (this.status === C.STATUS_CONFIRMED) {
      this.ended('system', null, JsSIP_C.causes.REQUEST_TIMEOUT);
    } else {
      this.failed('system', null, JsSIP_C.causes.REQUEST_TIMEOUT);
    }
  }
};

RTCSession.prototype.onDialogError = function(response) {
  if(this.status !== C.STATUS_TERMINATED) {
    if (this.status === C.STATUS_CONFIRMED) {
      this.ended('remote', response, JsSIP_C.causes.DIALOG_ERROR);
    } else {
      this.failed('remote', response, JsSIP_C.causes.DIALOG_ERROR);
    }
  }
};

/**
 * Internal Callbacks
 */

RTCSession.prototype.newRTCSession = function(originator, request) {
  var session = this,
    event_name = 'newRTCSession';

  if (originator === 'remote') {
    session.direction = 'incoming';
    session.local_identity = request.to;
    session.remote_identity = request.from;
  } else if (originator === 'local'){
    session.direction = 'outgoing';
    session.local_identity = request.from;
    session.remote_identity = request.to;
  }

  session.ua.emit(event_name, session.ua, {
    originator: originator,
    session: session,
    request: request
  });
};

RTCSession.prototype.connecting = function(request) {
  var session = this,
  event_name = 'connecting';

  session.emit(event_name, session, {
    request: request
  });
};

RTCSession.prototype.progress = function(originator, response) {
  var session = this,
    event_name = 'progress';

  session.emit(event_name, session, {
    originator: originator,
    response: response || null
  });
};

RTCSession.prototype.accepted = function(originator, message) {
  var session = this,
    event_name = 'accepted';

  session.start_time = new Date();

  session.emit(event_name, session, {
    originator: originator,
    response: message || null
  });
};

RTCSession.prototype.confirmed = function(originator, ack) {
  var session = this,
    event_name = 'confirmed';

  this.is_confirmed = true;

  session.emit(event_name, session, {
    originator: originator,
    ack: ack || null
  });
};

RTCSession.prototype.ended = function(originator, message, cause) {
  var session = this,
    event_name = 'ended';

  session.end_time = new Date();

  session.close();
  session.emit(event_name, session, {
    originator: originator,
    message: message || null,
    cause: cause
  });
};

RTCSession.prototype.failed = function(originator, message, cause) {
  var session = this,
    event_name = 'failed';

  session.close();
  session.emit(event_name, session, {
    originator: originator,
    message: message || null,
    cause: cause
  });
};

RTCSession.prototype.onhold = function(originator) {
  if (originator === 'local') {
    this.local_hold = true;
  } else {
    this.remote_hold = true;
  }

  this.emit('hold', this, {
    originator: originator
  });
};

RTCSession.prototype.onunhold = function(originator) {
  if (originator === 'local') {
    this.local_hold = false;
  } else {
    this.remote_hold = false;
  }

  this.emit('unhold', this, {
    originator: originator
  });
};

RTCSession.prototype.onmute = function(options) {
  this.emit('muted', this, {
    audio: options.audio,
    video: options.video
  });
};

RTCSession.prototype.onunmute = function(options) {
  this.emit('unmuted', this, {
    audio: options.audio,
    video: options.video
  });
};

RTCSession.prototype.onReadyToReinvite = function() {
  var action = (this.pending_actions.length() > 0)? this.pending_actions.shift() : null;

  if (!action) {
    return;
  }

  if (action.name === 'hold') {
    this.hold();
  } else if (action.name === 'unhold') {
    this.unhold();
  }
};