File: source/data-transfer.js

                      /**
                       * <blockquote class="info">
                       *   Note that this is used only for SDK developer purposes.<br>
                       *   Current version: <code>0.1.0</code>
                       * </blockquote>
                       * The value of the current version of the data transfer protocol.
                       * @attribute DT_PROTOCOL_VERSION
                       * @type String
                       * @readOnly
                       * @for Skylink
                       * @since 0.5.10
                       */
                      Skylink.prototype.DT_PROTOCOL_VERSION = '0.1.3';
                      
                      /**
                       * The list of data transfers directions.
                       * @attribute DATA_TRANSFER_TYPE
                       * @param {String} UPLOAD <small>Value <code>"upload"</code></small>
                       *   The value of the data transfer direction when User is uploading data to Peer.
                       * @param {String} DOWNLOAD <small>Value <code>"download"</code></small>
                       *   The value of the data transfer direction when User is downloading data from Peer.
                       * @type JSON
                       * @readOnly
                       * @for Skylink
                       * @since 0.1.0
                       */
                      Skylink.prototype.DATA_TRANSFER_TYPE = {
                        UPLOAD: 'upload',
                        DOWNLOAD: 'download'
                      };
                      
                      /**
                       * The list of data transfers session types.
                       * @attribute DATA_TRANSFER_SESSION_TYPE
                       * @param {String} BLOB     <small>Value <code>"blob"</code></small>
                       *   The value of the session type for
                       *   <a href="#method_sendURLData"><code>sendURLData()</code> method</a> data transfer.
                       * @param {String} DATA_URL <small>Value <code>"dataURL"</code></small>
                       *   The value of the session type for
                       *   <a href="#method_sendBlobData"><code>method_sendBlobData()</code> method</a> data transfer.
                       * @type JSON
                       * @readOnly
                       * @for Skylink
                       * @since 0.1.0
                       */
                      Skylink.prototype.DATA_TRANSFER_SESSION_TYPE = {
                        BLOB: 'blob',
                        DATA_URL: 'dataURL'
                      };
                      
                      /**
                       * The list of data transfer states.
                       * @attribute DATA_TRANSFER_STATE
                       * @param {String} UPLOAD_REQUEST     <small>Value <code>"request"</code></small>
                       *   The value of the state when receiving an upload data transfer request from Peer to User.
                       *   <small>At this stage, the upload data transfer request from Peer may be accepted or rejected with the
                       *   <a href="#method_acceptDataTransfer"><code>acceptDataTransfer()</code> method</a> invoked by User.</small>
                       * @parma {String} USER_UPLOAD_REQUEST <small>Value <code>"userRequest"</code></small>
                       *   The value of the state when User sent an upload data transfer request to Peer.
                       *   <small>At this stage, the upload data transfer request to Peer may be accepted or rejected with the
                       *   <a href="#method_acceptDataTransfer"><code>acceptDataTransfer()</code> method</a> invoked by Peer.</small>
                       * @param {String} UPLOAD_STARTED     <small>Value <code>"uploadStarted"</code></small>
                       *   The value of the state when the data transfer request has been accepted
                       *   and data transfer will start uploading data to Peer.
                       *   <small>At this stage, the data transfer may be terminated with the
                       *   <a href="#method_cancelDataTransfer"><code>cancelDataTransfer()</code> method</a>.</small>
                       * @param {String} DOWNLOAD_STARTED   <small>Value <code>"downloadStarted"</code></small>
                       *   The value of the state when the data transfer request has been accepted
                       *   and data transfer will start downloading data from Peer.
                       *   <small>At this stage, the data transfer may be terminated with the
                       *   <a href="#method_cancelDataTransfer"><code>cancelDataTransfer()</code> method</a>.</small>
                       * @param {String} REJECTED           <small>Value <code>"rejected"</code></small>
                       *   The value of the state when upload data transfer request to Peer has been rejected and terminated.
                       * @param {String} USER_REJECTED      <small>Value <code>"userRejected"</code></small>
                       *   The value of the state when User rejected and terminated upload data transfer request from Peer.
                       * @param {String} UPLOADING          <small>Value <code>"uploading"</code></small>
                       *   The value of the state when data transfer is uploading data to Peer.
                       * @param {String} DOWNLOADING        <small>Value <code>"downloading"</code></small>
                       *   The value of the state when data transfer is downloading data from Peer.
                       * @param {String} UPLOAD_COMPLETED   <small>Value <code>"uploadCompleted"</code></small>
                       *   The value of the state when data transfer has uploaded successfully to Peer.
                       * @param {String} DOWNLOAD_COMPLETED <small>Value <code>"downloadCompleted"</code></small>
                       *   The value of the state when data transfer has downloaded successfully from Peer.
                       * @param {String} CANCEL             <small>Value <code>"cancel"</code></small>
                       *   The value of the state when data transfer has been terminated from / to Peer.
                       * @param {String} ERROR              <small>Value <code>"error"</code></small>
                       *   The value of the state when data transfer has errors and has been terminated from / to Peer.
                       * @type JSON
                       * @readOnly
                       * @for Skylink
                       * @since 0.4.0
                       */
                      Skylink.prototype.DATA_TRANSFER_STATE = {
                        UPLOAD_REQUEST: 'request',
                        UPLOAD_STARTED: 'uploadStarted',
                        DOWNLOAD_STARTED: 'downloadStarted',
                        REJECTED: 'rejected',
                        CANCEL: 'cancel',
                        ERROR: 'error',
                        UPLOADING: 'uploading',
                        DOWNLOADING: 'downloading',
                        UPLOAD_COMPLETED: 'uploadCompleted',
                        DOWNLOAD_COMPLETED: 'downloadCompleted',
                        USER_REJECTED: 'userRejected',
                        USER_UPLOAD_REQUEST: 'userRequest',
                        START_ERROR: 'startError'
                      };
                      
                      /**
                       * The list of data streaming states.
                       * @attribute DATA_STREAM_STATE
                       * @param {String} SENDING_STARTED   <small>Value <code>"sendStart"</code></small>
                       *   The value of the state when data streaming session has started from User to Peer.
                       * @param {String} RECEIVING_STARTED <small>Value <code>"receiveStart"</code></small>
                       *   The value of the state when data streaming session has started from Peer to Peer.
                       * @param {String} RECEIVED          <small>Value <code>"received"</code></small>
                       *   The value of the state when data streaming session data chunk has been received from Peer to User.
                       * @param {String} SENT              <small>Value <code>"sent"</code></small>
                       *   The value of the state when data streaming session data chunk has been sent from User to Peer.
                       * @param {String} SENDING_STOPPED   <small>Value <code>"sendStop"</code></small>
                       *   The value of the state when data streaming session has stopped from User to Peer.
                       * @param {String} RECEIVING_STOPPED <small>Value <code>"receivingStop"</code></small>
                       *   The value of the state when data streaming session has stopped from Peer to User.
                       * @param {String} ERROR             <small>Value <code>"error"</code></small>
                       *   The value of the state when data streaming session has errors.
                       *   <small>At this stage, the data streaming state is considered <code>SENDING_STOPPED</code> or
                       *   <code>RECEIVING_STOPPED</code>.</small>
                       * @param {String} START_ERROR       <small>Value <code>"startError"</code></small>
                       *   The value of the state when data streaming session failed to start from User to Peer.
                       * @type JSON
                       * @readOnly
                       * @for Skylink
                       * @since 0.6.18
                       */
                      Skylink.prototype.DATA_STREAM_STATE = {
                        SENDING_STARTED: 'sendStart',
                        SENDING_STOPPED: 'sendStop',
                        RECEIVING_STARTED: 'receiveStart',
                        RECEIVING_STOPPED: 'receiveStop',
                        RECEIVED: 'received',
                        SENT: 'sent',
                        ERROR: 'error',
                        START_ERROR: 'startError'
                      };
                      
                      /**
                       * Stores the list of data transfer protocols.
                       * @attribute _DC_PROTOCOL_TYPE
                       * @param {String} WRQ The protocol to initiate data transfer.
                       * @param {String} ACK The protocol to request for data transfer chunk.
                       *   Give <code>-1</code> to reject the request at the beginning and <code>0</code> to accept
                       *   the data transfer request.
                       * @param {String} CANCEL The protocol to terminate data transfer.
                       * @param {String} ERROR The protocol when data transfer has errors and has to be terminated.
                       * @param {String} MESSAGE The protocol that is used to send P2P messages.
                       * @type JSON
                       * @readOnly
                       * @private
                       * @for Skylink
                       * @since 0.5.2
                       */
                      Skylink.prototype._DC_PROTOCOL_TYPE = {
                        WRQ: 'WRQ',
                        ACK: 'ACK',
                        ERROR: 'ERROR',
                        CANCEL: 'CANCEL',
                        MESSAGE: 'MESSAGE'
                      };
                      
                      /**
                       * Function that starts an uploading data transfer from User to Peers.
                       * @method sendBlobData
                       * @param {Blob} data The Blob object.
                       * @param {Number} [timeout=60] The timeout to wait for response from Peer.
                       * @param {String|Array} [targetPeerId] The target Peer ID to start data transfer with.
                       * - When provided as an Array, it will start uploading data transfers with all connections
                       *   with all the Peer IDs provided.
                       * - When not provided, it will start uploading data transfers with all the currently connected Peers in the Room.
                       * @param {Boolean} [sendChunksAsBinary=false] <blockquote class="info">
                       *   Note that this is currently not supported for MCU enabled Peer connections or Peer connections connecting from
                       *   Android, iOS and Linux SDKs. This would fallback to <code>transferInfo.chunkType</code> to
                       *   <code>BINARY_STRING</code> when MCU is connected. </blockquote> The flag if data transfer
                       *   binary data chunks should not be encoded as Base64 string during data transfers.
                       * @param {Function} [callback] The callback function fired when request has completed.
                       *   <small>Function parameters signature is <code>function (error, success)</code></small>
                       *   <small>Function request completion is determined by the <a href="#event_dataTransferState">
                       *   <code>dataTransferState</code> event</a> triggering <code>state</code> parameter payload
                       *   as <code>UPLOAD_COMPLETED</code> for all Peers targeted for request success.</small>
                       * @param {JSON} callback.error The error result in request.
                       *   <small>Defined as <code>null</code> when there are no errors in request</small>
                       * @param {String} callback.error.transferId The data transfer ID.
                       *   <small>Defined as <code>null</code> when <code>sendBlobData()</code> fails to start data transfer.</small>
                       * @param {Array} callback.error.listOfPeers The list Peer IDs targeted for the data transfer.
                       * @param {JSON} callback.error.transferErrors The list of data transfer errors.
                       * @param {Error|String} callback.error.transferErrors.#peerId The data transfer error associated
                       *   with the Peer ID defined in <code>#peerId</code> property.
                       *   <small>If <code>#peerId</code> value is <code>"self"</code>, it means that it is the error when there
                       *   are no Peer connections to start data transfer with.</small>
                       * @param {JSON} callback.error.transferInfo The data transfer information.
                       *   <small>Object signature matches the <code>transferInfo</code> parameter payload received in the
                       *   <a href="#event_dataTransferState"><code>dataTransferState</code> event</a> except without the
                       *   <code>percentage</code> and <code>data</code> property.</small>
                       * @param {JSON} callback.success The success result in request.
                       *   <small>Defined as <code>null</code> when there are errors in request</small>
                       * @param {String} callback.success.transferId The data transfer ID.
                       * @param {Array} callback.success.listOfPeers The list Peer IDs targeted for the data transfer.
                       * @param {JSON} callback.success.transferInfo The data transfer information.
                       *   <small>Object signature matches the <code>transferInfo</code> parameter payload received in the
                       *   <a href="#event_dataTransferState"><code>dataTransferState</code> event</a> except without the
                       *   <code>percentage</code> property and <code>data</code>.</small>
                       * @trigger <ol class="desc-seq">
                       *   <li>Checks if User is in Room. <ol>
                       *   <li>If User is not in Room: <ol><li><a href="#event_dataTransferState">
                       *   <code>dataTransferState</code> event</a> triggers parameter payload <code>state</code>
                       *   as <code>START_ERROR</code>.</li><li><b>ABORT</b> step and return error.</li></ol></li></ol></li>
                       *   <li>Checks if there is any available Datachannel connections. <ol>
                       *   <li>If User is not in Room: <ol><li><a href="#event_dataTransferState">
                       *   <code>dataTransferState</code> event</a> triggers parameter payload <code>state</code>
                       *   as <code>START_ERROR</code>.</li><li><b>ABORT</b> step and return error.</li></ol></li></ol></li>
                       *   <li>Checks if provided <code>data</code> parameter is valid. <ol>
                       *   <li>If it is invalid: <ol><li><a href="#event_dataTransferState">
                       *   <code>dataTransferState</code> event</a> triggers parameter payload <code>state</code>
                       *   as <code>START_ERROR</code>.</li><li><b>ABORT</b> step and return error.</li></ol></li></ol></li>
                       *   <li>Checks if Peer connection and Datachannel connection are in correct states. <ol>
                       *   <li>If Peer connection or session does not exists: <ol><li><a href="#event_dataTransferState">
                       *   <code>dataTransferState</code> event</a> triggers parameter payload <code>state</code>
                       *   as <code>ERROR</code>.</li><li><b>ABORT</b> step and return error.</li></ol></li>
                       *   <li>If Peer connection is not stable: <small>The stable state can be checked with <a href="#event_peerConnectionState">
                       *   <code>peerConnectionState</code> event</a> triggering parameter payload <code>state</code> as <code>STABLE</code>
                       *   for Peer.</small> <ol><li><a href="#event_dataTransferState"><code>dataTransferState</code> event</a> triggers
                       *   parameter payload <code>state</code> as <code>ERROR</code>.</li><li><b>ABORT</b> step and return error.</li></ol></li>
                       *   <li>If Peer connection messaging Datachannel has not been opened: <small>This can be checked with
                       *   <a href="#event_dataChannelState"><code>dataChannelState</code> event</a> triggering parameter
                       *   payload <code>state</code> as <code>OPEN</code> and <code>channelType</code> as
                       *   <code>MESSAGING</code> for Peer.</small> <ol><li><a href="#event_dataTransferState">
                       *   <code>dataTransferState</code> event</a> triggers parameter payload <code>state</code> as <code>ERROR</code>.</li>
                       *   <li><b>ABORT</b> step and return error.</li></ol></li>
                       *   <li>If MCU is enabled for the App Key provided in <a href="#method_init"><code>init()</code>method</a> and connected: <ol>
                       *   <li>If MCU Peer connection is not stable: <small>The stable state can be checked with <a href="#event_peerConnectionState">
                       *   <code>peerConnectionState</code> event</a> triggering parameter payload <code>state</code> as <code>STABLE</code>
                       *   and <code>peerId</code> value as <code>"MCU"</code> for MCU Peer.</small>
                       *   <ol><li><a href="#event_dataTransferState"><code>dataTransferState</code> event</a> triggers
                       *   parameter payload <code>state</code> as <code>ERROR</code>.</li><li><b>ABORT</b> step and return error.</li></ol></li>
                       *   <li>If MCU Peer connection messaging Datachannel has not been opened: <small>This can be checked with
                       *   <a href="#event_dataChannelState"><code>dataChannelState</code> event</a> triggering parameter
                       *   payload <code>state</code> as <code>OPEN</code>, <code>peerId</code> value as <code>"MCU"</code>
                       *   and <code>channelType</code> as <code>MESSAGING</code> for MCU Peer.</small>
                       *   <ol><li><a href="#event_dataTransferState"><code>dataTransferState</code> event</a> triggers
                       *   parameter payload <code>state</code> as <code>ERROR</code>.</li>
                       *   <li><b>ABORT</b> step and return error.</li></ol></li></ol></li>
                       *   <li>Checks if should open a new data Datachannel.<ol>
                       *   <li>If Peer supports simultaneous data transfer, open new data Datachannel: <small>If MCU is connected,
                       *   this opens a new data Datachannel with MCU Peer with all the Peers IDs information that supports
                       *   simultaneous data transfers targeted for the data transfer session instead of opening new data Datachannel
                       *   with all Peers targeted for the data transfer session.</small> <ol>
                       *   <li><a href="#event_dataChannelState"><code>dataChannelState</code> event</a> triggers parameter
                       *   payload <code>state</code> as <code>CONNECTING</code> and <code>channelType</code> as <code>DATA</code>.
                       *   <small>Note that there is no timeout to wait for parameter payload <code>state</code> to be
                       *   <code>OPEN</code>.</small></li>
                       *   <li>If Datachannel has been created and opened successfully: <ol>
                       *   <li><a href="#event_dataChannelState"><code>dataChannelState</code> event</a> triggers parameter payload
                       *   <code>state</code> as <code>OPEN</code> and <code>channelType</code> as <code>DATA</code>.</li></ol></li>
                       *   <li>Else: <ol><li><a href="#event_dataChannelState"><code>dataChannelState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>CREATE_ERROR</code> and <code>channelType</code> as
                       *   <code>DATA</code>.</li><li><a href="#event_dataTransferState"><code>dataTransferState</code> event</a> triggers
                       *   parameter payload <code>state</code> as <code>ERROR</code>.</li><li><b>ABORT</b> step and
                       *   return error.</li></ol></li></ol></li><li>Else: <small>If MCU is connected,
                       *   this uses the messaging Datachannel with MCU Peer with all the Peers IDs information that supports
                       *   simultaneous data transfers targeted for the data transfer session instead of using the messaging Datachannels
                       *   with all Peers targeted for the data transfer session.</small> <ol><li>If messaging Datachannel connection has a
                       *   data transfer in-progress: <ol><li><a href="#event_dataTransferState"><code>dataTransferState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>ERROR</code>.</li><li><b>ABORT</b> step and
                       *   return error.</li></ol></li><li>If there is any conflicting <a href="#method_streamData"><code>streamData()</code>
                       *   method</a> data streaming session: <small>If <code>sendChunksAsBinary</code> is provided as <code>true</code>,
                       *   it cannot start if existing data streaming session is expected binary data chunks, and if provided as
                       *   <code>false</code>, or method invoked is <a href="#method_sendURLData"><code>sendURLData()</code> method</a>,
                       *   or Peer is using string data chunks fallback due to its support despite provided as <code>true</code>,
                       *   it cannot start if existing data streaming session is expected string data chunks.</small><ol>
                       *   <li><a href="#event_dataTransferState"><code>dataTransferState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>ERROR</code>.</li><li><b>ABORT</b> step and
                       *   return error.</li></ol></li></li></ol></ol></li></ol></li>
                       *   <li>Starts the data transfer to Peer. <ol>
                       *   <li><a href="#event_incomingDataRequest"><code>incomingDataRequest</code> event</a> triggers.</li>
                       *   <li><em>For User only</em> <a href="#event_dataTransferState"><code>dataTransferState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>USER_UPLOAD_REQUEST</code>.</li>
                       *   <li><em>For Peer only</em> <a href="#event_dataTransferState"><code>dataTransferState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>UPLOAD_REQUEST</code>.</li>
                       *   <li>Peer invokes <a href="#method_acceptDataTransfer"><code>acceptDataTransfer()</code> method</a>. <ol>
                       *   <li>If parameter <code>accept</code> value is <code>true</code>: <ol>
                       *   <li>User starts upload data transfer to Peer. <ol>
                       *   <li><em>For User only</em> <a href="#event_dataTransferState"><code>dataTransferState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>UPLOAD_STARTED</code>.</li>
                       *   <li><em>For Peer only</em> <a href="#event_dataTransferState"><code>dataTransferState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>DOWNLOAD_STARTED</code>.</li></ol></li>
                       *   <li>If Peer / User invokes <a href="#method_cancelDataTransfer"><code>cancelDataTransfer()</code> method</a>: <ol>
                       *   <li><a href="#event_dataTransferState"><code>dataTransferState</code> event</a> triggers parameter
                       *   <code>state</code> as <code>CANCEL</code>.</li><li><b>ABORT</b> step and return error.</li></ol></li>
                       *   <li>If data transfer has timeout errors: <ol>
                       *   <li><a href="#event_dataTransferState"><code>dataTransferState</code> event</a> triggers parameter
                       *   <code>state</code> as <code>ERROR</code>.</li><li><b>ABORT</b> step and return error.</li></ol></li>
                       *   <li>Checks for Peer connection and Datachannel connection during data transfer: <ol>
                       *   <li>If MCU is enabled for the App Key provided in <a href="#method_init"><code>init()</code>
                       *   method</a> and connected: <ol>
                       *   <li>If MCU Datachannel has closed abruptly during data transfer: <ol>
                       *   <small>This can be checked with <a href="#event_dataChannelState"><code>dataChannelState</code> event</a>
                       *   triggering parameter payload <code>state</code> as <code>CLOSED</code>, <code>peerId</code> value as
                       *   <code>"MCU"</code> and <code>channelType</code> as <code>DATA</code> for targeted Peers that supports simultaneous
                       *   data transfer or <code>MESSAGING</code> for targeted Peers that do not support it.</small> <ol>
                       *   <li><a href="#event_dataTransferState"><code>dataTransferState</code> event</a> triggers parameter
                       *   <code>state</code> as <code>ERROR</code>.</li><li><b>ABORT</b> step and return error.</li></ol></li></ol></li>
                       *   <li>If MCU Peer connection has changed from not being stable: <ol>
                       *   <small>This can be checked with <a href="#event_peerConnectionState"><code>peerConnection</code> event</a>
                       *   triggering parameter payload <code>state</code> as not <code>STABLE</code>, <code>peerId</code> value as
                       *   <code>"MCU"</code>.</small> <ol><li><a href="#event_dataTransferState"><code>dataTransferState</code> event</a> triggers parameter
                       *   <code>state</code> as <code>ERROR</code>.</li><li><b>ABORT</b> step and return error.</li></ol></li></ol></li>
                       *   <li>If Peer connection has changed from not being stable: <ol>
                       *   <small>This can be checked with <a href="#event_peerConnectionState"><code>peerConnection</code> event</a>
                       *   triggering parameter payload <code>state</code> as not <code>STABLE</code>.</small> <ol>
                       *   <li><a href="#event_dataTransferState"><code>dataTransferState</code> event</a> triggers parameter
                       *   <code>state</code> as <code>ERROR</code>.</li><li><b>ABORT</b> step and return error.</li></ol></li></ol></li></ol></li>
                       *   <li>Else: <ol><li>If Datachannel has closed abruptly during data transfer:
                       *   <small>This can be checked with <a href="#event_dataChannelState"><code>dataChannelState</code> event</a>
                       *   triggering parameter payload <code>state</code> as <code>CLOSED</code> and <code>channelType</code>
                       *   as <code>DATA</code> for Peer that supports simultaneous data transfer or <code>MESSAGING</code>
                       *   for Peer that do not support it.</small> <ol>
                       *   <li><a href="#event_dataTransferState"><code>dataTransferState</code> event</a> triggers parameter
                       *   <code>state</code> as <code>ERROR</code>.</li><li><b>ABORT</b> step and return error.</li></ol></li></ol></li></ol></li>
                       *   <li>If data transfer is still progressing: <ol>
                       *   <li><em>For User only</em> <a href="#event_dataTransferState"><code>dataTransferState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>UPLOADING</code>.</li>
                       *   <li><em>For Peer only</em> <a href="#event_dataTransferState"><code>dataTransferState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>DOWNLOADING</code>.</li></ol></li>
                       *   <li>If data transfer has completed <ol>
                       *   <li><a href="#event_incomingData"><code>incomingData</code> event</a> triggers.</li>
                       *   <li><em>For User only</em> <a href="#event_dataTransferState"><code>dataTransferState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>UPLOAD_COMPLETED</code>.</li>
                       *   <li><em>For Peer only</em> <a href="#event_dataTransferState"><code>dataTransferState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>DOWNLOAD_COMPLETED</code>.</li></ol></li></ol></li>
                       *   <li>If parameter <code>accept</code> value is <code>false</code>: <ol>
                       *   <li><em>For User only</em> <a href="#event_dataTransferState"><code>dataTransferState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>REJECTED</code>.</li>
                       *   <li><em>For Peer only</em> <a href="#event_dataTransferState"><code>dataTransferState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>USER_REJECTED</code>.</li>
                       *   <li><b>ABORT</b> step and return error.</li></ol></li></ol>
                       * @example
                       * &lt;body&gt;
                       *  &lt;input type="radio" name="timeout" onchange="setTransferTimeout(0)"&gt; 1s timeout (Default)
                       *  &lt;input type="radio" name="timeout" onchange="setTransferTimeout(120)"&gt; 2s timeout
                       *  &lt;input type="radio" name="timeout" onchange="setTransferTimeout(300)"&gt; 5s timeout
                       *  &lt;hr&gt;
                       *  &lt;input type="file" onchange="uploadFile(this.files[0], this.getAttribute('data'))" data="peerId"&gt;
                       *  &lt;input type="file" onchange="uploadFileGroup(this.files[0], this.getAttribute('data').split(',')))" data="peerIdA,peerIdB"&gt;
                       *  &lt;input type="file" onchange="uploadFileAll(this.files[0])" data=""&gt;
                       *  &lt;script&gt;
                       *    var transferTimeout = 0;
                       *
                       *    function setTransferTimeout (timeout) {
                       *      transferTimeout = timeout;
                       *    }
                       *
                       *    // Example 1: Upload data to a Peer
                       *    function uploadFile (file, peerId) {
                       *      var cb = function (error, success) {
                       *        if (error) return;
                       *        console.info("File has been transferred to '" + peerId + "' successfully");
                       *      };
                       *      if (transferTimeout > 0) {
                       *        skylinkDemo.sendBlobData(file, peerId, transferTimeout, cb);
                       *      } else {
                       *        skylinkDemo.sendBlobData(file, peerId, cb);
                       *      }
                       *    }
                       *
                       *    // Example 2: Upload data to a list of Peers
                       *    function uploadFileGroup (file, peerIds) {
                       *      var cb = function (error, success) {
                       *        var listOfPeers = error ? error.listOfPeers : success.listOfPeers;
                       *        var listOfPeersErrors = error ? error.transferErrors : {};
                       *        for (var i = 0; i < listOfPeers.length; i++) {
                       *          if (listOfPeersErrors[listOfPeers[i]]) {
                       *            console.error("Failed file transfer to '" + listOfPeers[i] + "'");
                       *          } else {
                       *            console.info("File has been transferred to '" + listOfPeers[i] + "' successfully");
                       *          }
                       *        }
                       *      };
                       *      if (transferTimeout > 0) {
                       *        skylinkDemo.sendBlobData(file, peerIds, transferTimeout, cb);
                       *      } else {
                       *        skylinkDemo.sendBlobData(file, peerIds, cb);
                       *      }
                       *    }
                       *
                       *    // Example 2: Upload data to a list of Peers
                       *    function uploadFileAll (file) {
                       *      var cb = function (error, success) {
                       *        var listOfPeers = error ? error.listOfPeers : success.listOfPeers;
                       *        var listOfPeersErrors = error ? error.transferErrors : {};
                       *        for (var i = 0; i < listOfPeers.length; i++) {
                       *          if (listOfPeersErrors[listOfPeers[i]]) {
                       *            console.error("Failed file transfer to '" + listOfPeers[i] + "'");
                       *          } else {
                       *            console.info("File has been transferred to '" + listOfPeers[i] + "' successfully");
                       *          }
                       *        }
                       *      };
                       *      if (transferTimeout > 0) {
                       *        skylinkDemo.sendBlobData(file, transferTimeout, cb);
                       *      } else {
                       *        skylinkDemo.sendBlobData(file, cb);
                       *      }
                       *    }
                       * &lt;/script&gt;
                       * &lt;/body&gt;
                       * @for Skylink
                       * @since 0.5.5
                       */
                      Skylink.prototype.sendBlobData = function(data, timeout, targetPeerId, sendChunksAsBinary, callback) {
                        this._startDataTransfer(data, timeout, targetPeerId, sendChunksAsBinary, callback, 'blob');
                      };
                      
                      /**
                       * Function that starts an uploading string data transfer from User to Peers.
                       * @method sendURLData
                       * @param {String} data The data string to transfer to Peer.
                       * @param {Number} [timeout=60] The timeout to wait for response from Peer.
                       * @param {String|Array} [targetPeerId] The target Peer ID to start data transfer with.
                       * - When provided as an Array, it will start uploading data transfers with all connections
                       *   with all the Peer IDs provided.
                       * - When not provided, it will start uploading data transfers with all the currently connected Peers in the Room.
                       * @param {Function} [callback] The callback function fired when request has completed.
                       *   <small>Function parameters signature is <code>function (error, success)</code></small>
                       *   <small>Function request completion is determined by the <a href="#event_dataTransferState">
                       *   <code>dataTransferState</code> event</a> triggering <code>state</code> parameter payload
                       *   as <code>UPLOAD_COMPLETED</code> for all Peers targeted for request success.</small>
                       * @param {JSON} callback.error The error result in request.
                       *   <small>Defined as <code>null</code> when there are no errors in request</small>
                       * @param {String} callback.error.transferId The data transfer ID.
                       *   <small>Defined as <code>null</code> when <code>sendURLData()</code> fails to start data transfer.</small>
                       * @param {Array} callback.error.listOfPeers The list Peer IDs targeted for the data transfer.
                       * @param {JSON} callback.error.transferErrors The list of data transfer errors.
                       * @param {Error|String} callback.error.transferErrors.#peerId The data transfer error associated
                       *   with the Peer ID defined in <code>#peerId</code> property.
                       *   <small>If <code>#peerId</code> value is <code>"self"</code>, it means that it is the error when there
                       *   are no Peer connections to start data transfer with.</small>
                       * @param {JSON} callback.error.transferInfo The data transfer information.
                       *   <small>Object signature matches the <code>transferInfo</code> parameter payload received in the
                       *   <a href="#event_dataTransferState"><code>dataTransferState</code> event</a> except without the
                       *   <code>percentage</code> property and <code>data</code>.</small>
                       * @param {JSON} callback.success The success result in request.
                       *   <small>Defined as <code>null</code> when there are errors in request</small>
                       * @param {String} callback.success.transferId The data transfer ID.
                       * @param {Array} callback.success.listOfPeers The list Peer IDs targeted for the data transfer.
                       * @param {JSON} callback.success.transferInfo The data transfer information.
                       *   <small>Object signature matches the <code>transferInfo</code> parameter payload received in the
                       *   <a href="#event_dataTransferState"><code>dataTransferState</code> event</a> except without the
                       *   <code>percentage</code> property and <code>data</code>.</small>
                       * @trigger <small>Event sequence follows <a href="#method_sendBlobData">
                       * <code>sendBlobData()</code> method</a>.</small>
                       * @example
                       * &lt;body&gt;
                       *  &lt;input type="radio" name="timeout" onchange="setTransferTimeout(0)"&gt; 1s timeout (Default)
                       *  &lt;input type="radio" name="timeout" onchange="setTransferTimeout(120)"&gt; 2s timeout
                       *  &lt;input type="radio" name="timeout" onchange="setTransferTimeout(300)"&gt; 5s timeout
                       *  &lt;hr&gt;
                       *  &lt;input type="file" onchange="showImage(this.files[0], this.getAttribute('data'))" data="peerId"&gt;
                       *  &lt;input type="file" onchange="showImageGroup(this.files[0], this.getAttribute('data').split(',')))" data="peerIdA,peerIdB"&gt;
                       *  &lt;input type="file" onchange="showImageAll(this.files[0])" data=""&gt;
                       *  &lt;image id="target-1" src=""&gt;
                       *  &lt;image id="target-2" src=""&gt;
                       *  &lt;image id="target-3" src=""&gt;
                       *  &lt;script&gt;
                       *    var transferTimeout = 0;
                       *
                       *    function setTransferTimeout (timeout) {
                       *      transferTimeout = timeout;
                       *    }
                       *
                       *    function retrieveImageDataURL(file, cb) {
                       *      var fr = new FileReader();
                       *      fr.onload = function () {
                       *        cb(fr.result);
                       *      };
                       *      fr.readAsDataURL(files[0]);
                       *    }
                       *
                       *    // Example 1: Send image data URL to a Peer
                       *    function showImage (file, peerId) {
                       *      var cb = function (error, success) {
                       *        if (error) return;
                       *        console.info("Image has been transferred to '" + peerId + "' successfully");
                       *      };
                       *      retrieveImageDataURL(file, function (str) {
                       *        if (transferTimeout > 0) {
                       *          skylinkDemo.sendURLData(str, peerId, transferTimeout, cb);
                       *        } else {
                       *          skylinkDemo.sendURLData(str, peerId, cb);
                       *        }
                       *        document.getElementById("target-1").src = str;
                       *      });
                       *    }
                       *
                       *    // Example 2: Send image data URL to a list of Peers
                       *    function showImageGroup (file, peerIds) {
                       *      var cb = function (error, success) {
                       *        var listOfPeers = error ? error.listOfPeers : success.listOfPeers;
                       *        var listOfPeersErrors = error ? error.transferErrors : {};
                       *        for (var i = 0; i < listOfPeers.length; i++) {
                       *          if (listOfPeersErrors[listOfPeers[i]]) {
                       *            console.error("Failed image transfer to '" + listOfPeers[i] + "'");
                       *          } else {
                       *            console.info("Image has been transferred to '" + listOfPeers[i] + "' successfully");
                       *          }
                       *        }
                       *      };
                       *      retrieveImageDataURL(file, function (str) {
                       *        if (transferTimeout > 0) {
                       *          skylinkDemo.sendURLData(str, peerIds, transferTimeout, cb);
                       *        } else {
                       *          skylinkDemo.sendURLData(str, peerIds, cb);
                       *        }
                       *        document.getElementById("target-2").src = str;
                       *      });
                       *    }
                       *
                       *    // Example 2: Send image data URL to a list of Peers
                       *    function uploadFileAll (file) {
                       *      var cb = function (error, success) {
                       *        var listOfPeers = error ? error.listOfPeers : success.listOfPeers;
                       *        var listOfPeersErrors = error ? error.transferErrors : {};
                       *        for (var i = 0; i < listOfPeers.length; i++) {
                       *          if (listOfPeersErrors[listOfPeers[i]]) {
                       *            console.error("Failed image transfer to '" + listOfPeers[i] + "'");
                       *          } else {
                       *            console.info("Image has been transferred to '" + listOfPeers[i] + "' successfully");
                       *          }
                       *        }
                       *      };
                       *      retrieveImageDataURL(file, function (str) {
                       *        if (transferTimeout > 0) {
                       *          skylinkDemo.sendURLData(str, transferTimeout, cb);
                       *        } else {
                       *          skylinkDemo.sendURLData(str, cb);
                       *        }
                       *        document.getElementById("target-3").src = str;
                       *      });
                       *    }
                       * &lt;/script&gt;
                       * &lt;/body&gt;
                       * @for Skylink
                       * @since 0.6.1
                       */
                      Skylink.prototype.sendURLData = function(data, timeout, targetPeerId, callback) {
                        this._startDataTransfer(data, timeout, targetPeerId, callback, null, 'data');
                      };
                      
                      /**
                       * Function that accepts or rejects an upload data transfer request from Peer to User.
                       * @method acceptDataTransfer
                       * @param {String} peerId The Peer ID.
                       * @param {String} transferId The data transfer ID.
                       * @param {Boolean} [accept=false] The flag if User accepts the upload data transfer request from Peer.
                       * @example
                       *   // Example 1: Accept Peer upload data transfer request
                       *   skylinkDemo.on("incomingDataRequest", function (transferId, peerId, transferInfo, isSelf) {
                       *      if (!isSelf) {
                       *        skylinkDemo.acceptDataTransfer(peerId, transferId, true);
                       *      }
                       *   });
                       *
                       *   // Example 2: Reject Peer upload data transfer request
                       *   skylinkDemo.on("incomingDataRequest", function (transferId, peerId, transferInfo, isSelf) {
                       *      if (!isSelf) {
                       *        skylinkDemo.acceptDataTransfer(peerId, transferId, false);
                       *      }
                       *   });
                       * @trigger <small>Event sequence follows <a href="#method_sendBlobData">
                       * <code>sendBlobData()</code> method</a> after <code>acceptDataTransfer()</code> method is invoked.</small>
                       * @for Skylink
                       * @since 0.6.1
                       */
                      Skylink.prototype.respondBlobRequest =
                      Skylink.prototype.acceptDataTransfer = function (peerId, transferId, accept) {
                        var self = this;
                      
                        if (typeof transferId !== 'string' && typeof peerId !== 'string') {
                          log.error([peerId, 'RTCDataChannel', transferId, 'Aborting accept data transfer as ' +
                            'data transfer ID or peer ID is not provided']);
                          return;
                        }
                      
                        if (!self._dataChannels[peerId]) {
                          log.error([peerId, 'RTCDataChannel', transferId, 'Aborting accept data transfer as ' +
                            'Peer does not have any Datachannel connections']);
                          return;
                        }
                      
                        if (!self._dataTransfers[transferId]) {
                          log.error([peerId, 'RTCDataChannel', transferId, 'Aborting accept data transfer as ' +
                            'invalid transfer ID is provided']);
                          return;
                        }
                      
                        // Check Datachannel property in _dataChannels[peerId] list
                        var channelProp = 'main';
                      
                        if (self._dataChannels[peerId][transferId]) {
                          channelProp = transferId;
                        }
                      
                        if (accept) {
                          log.debug([peerId, 'RTCDataChannel', transferId, 'Accepted data transfer and starting ...']);
                      
                          var dataChannelStateCbFn = function (state, evtPeerId, error, cN, cT) {
                            console.info(evtPeerId, error, cN, cT);
                            self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.ERROR, transferId, peerId,
                              self._getTransferInfo(transferId, peerId, true, false, false), {
                              transferType: self.DATA_TRANSFER_TYPE.DOWNLOAD,
                              message: new Error('Data transfer terminated as Peer Datachannel connection closed abruptly.')
                            });
                          };
                      
                          self.once('dataChannelState', dataChannelStateCbFn, function (state, evtPeerId, error, channelName, channelType) {
                            if (!(self._dataTransfers[transferId] && self._dataTransfers[transferId].sessions[peerId])) {
                              self.off('dataChannelState', dataChannelStateCbFn);
                              return;
                            }
                            return evtPeerId === peerId && (channelProp === 'main' ? channelType === self.DATA_CHANNEL_STATE.MESSAGING :
                              channelName === transferId) && [self.DATA_CHANNEL_STATE.CLOSING, self.DATA_CHANNEL_STATE.CLOSED,
                              self.DATA_CHANNEL_STATE.ERROR].indexOf(state) > -1;
                          });
                      
                          // From here we start detecting as completion for data transfer downloads
                          self.once('dataTransferState', function () {
                            if (dataChannelStateCbFn) {
                              self.off('dataChannelState', dataChannelStateCbFn);
                            }
                      
                            delete self._dataTransfers[transferId];
                      
                            if (self._dataChannels[peerId]) {
                              if (channelProp === 'main' && self._dataChannels[peerId].main) {
                                self._dataChannels[peerId].main.transferId = null;
                              }
                      
                              if (channelProp === transferId) {
                                self._closeDataChannel(peerId, transferId);
                              }
                            }
                          }, function (state, evtTransferId, evtPeerId) {
                            return evtTransferId === transferId && evtPeerId === peerId &&
                              [self.DATA_TRANSFER_STATE.ERROR, self.DATA_TRANSFER_STATE.CANCEL,
                              self.DATA_TRANSFER_STATE.DOWNLOAD_COMPLETED].indexOf(state) > -1;
                          });
                      
                          // Send ACK protocol to start data transfer
                          // MCU sends the data transfer from the "P2P" Datachannel
                          self._sendMessageToDataChannel(peerId, {
                            type: self._DC_PROTOCOL_TYPE.ACK,
                            sender: self._user.sid,
                            ackN: 0
                          }, channelProp);
                      
                          self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.DOWNLOAD_STARTED, transferId, peerId,
                            self._getTransferInfo(transferId, peerId, true, false, false), null);
                      
                        } else {
                          log.warn([peerId, 'RTCDataChannel', transferId, 'Rejected data transfer and data transfer request has been aborted']);
                      
                          // Send ACK protocol to terminate data transfer request
                          // MCU sends the data transfer from the "P2P" Datachannel
                          self._sendMessageToDataChannel(peerId, {
                            type: self._DC_PROTOCOL_TYPE.ACK,
                            sender: self._user.sid,
                            ackN: -1
                          }, channelProp);
                      
                          // Insanity check
                          if (channelProp === 'main' && self._dataChannels[peerId].main) {
                            self._dataChannels[peerId].main.transferId = null;
                          }
                      
                          self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.USER_REJECTED, transferId, peerId,
                            self._getTransferInfo(transferId, peerId, true, false, false), {
                            message: new Error('Data transfer terminated as User has rejected data transfer request.'),
                            transferType: self.DATA_TRANSFER_TYPE.DOWNLOAD
                          });
                      
                          delete self._dataTransfers[transferId];
                        }
                      };
                      
                      /**
                       * <blockquote class="info">
                       *   For MCU enabled Peer connections, the cancel data transfer functionality may differ, as it
                       *   will result in all Peers related to the data transfer ID to be terminated.
                       * </blockquote>
                       * Function that terminates a currently uploading / downloading data transfer from / to Peer.
                       * @method cancelDataTransfer
                       * @param {String} peerId The Peer ID.
                       * @param {String} transferId The data transfer ID.
                       * @example
                       *   // Example 1: Cancel Peer data transfer
                       *   var transferSessions = {};
                       *
                       *   skylinkDemo.on("dataTransferState", function (state, transferId, peerId) {
                       *     if ([skylinkDemo.DATA_TRANSFER_STATE.DOWNLOAD_STARTED,
                       *       skylinkDemo.DATA_TRANSFER_STATE.UPLOAD_STARTED].indexOf(state) > -1) {
                       *       if (!Array.isArray(transferSessions[transferId])) {
                       *         transferSessions[transferId] = [];
                       *       }
                       *       transferSessions[transferId].push(peerId);
                       *     } else {
                       *       transferSessions[transferId].splice(transferSessions[transferId].indexOf(peerId), 1);
                       *     }
                       *   });
                       *
                       *   function cancelTransfer (peerId, transferId) {
                       *     skylinkDemo.cancelDataTransfer(peerId, transferId);
                       *   }
                       * @trigger <small>Event sequence follows <a href="#method_sendBlobData">
                       * <code>sendBlobData()</code> method</a> after <code>cancelDataTransfer()</code> method is invoked.</small>
                       * @for Skylink
                       * @since 0.6.1
                       */
                      Skylink.prototype.cancelBlobTransfer =
                      Skylink.prototype.cancelDataTransfer = function (peerId, transferId) {
                        var self = this;
                      
                        if (!(transferId && typeof transferId === 'string')) {
                          log.error([peerId, 'RTCDataChannel', transferId, 'Aborting cancel data transfer as data transfer ID is not provided']);
                          return;
                        }
                      
                        if (!(peerId && typeof peerId === 'string')) {
                          log.error([peerId, 'RTCDataChannel', transferId, 'Aborting cancel data transfer as peer ID is not provided']);
                          return;
                        }
                      
                        if (!self._dataTransfers[transferId]) {
                          log.error([peerId, 'RTCDataChannel', transferId, 'Aborting cancel data transfer as ' +
                            'data transfer session does not exists.']);
                          return;
                        }
                      
                        log.debug([peerId, 'RTCDataChannel', transferId, 'Canceling data transfer ...']);
                      
                        /**
                         * Emit data state event function.
                         */
                        var emitEventFn = function (peers, transferInfoPeerId) {
                          for (var i = 0; i < peers.length; i++) {
                            self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.CANCEL, transferId, peers[i],
                              self._getTransferInfo(transferId, transferInfoPeerId, false, false, false), {
                              transferType: self._dataTransfers[transferId].direction,
                              message: new Error('User cancelled download transfer')
                            });
                          }
                        };
                      
                        // For uploading from Peer to MCU case of broadcast
                        if (self._hasMCU && self._dataTransfers[transferId].direction === self.DATA_TRANSFER_TYPE.UPLOAD) {
                          if (!self._dataChannels.MCU) {
                            log.error([peerId, 'RTCDataChannel', transferId, 'Aborting cancel data transfer as ' +
                              'Peer does not have any Datachannel connections']);
                            return;
                          }
                      
                          // We abort all data transfers to all Peers if uploading via MCU since it broadcasts to MCU
                          log.warn([peerId, 'RTCDataChannel', transferId, 'Aborting all data transfers to Peers']);
                      
                          // If data transfer to MCU broadcast has interop Peers, send to MCU via the "main" Datachannel
                          if (Object.keys(self._dataTransfers[transferId].peers.main).length > 0) {
                            self._sendMessageToDataChannel('MCU', {
                              type: self._DC_PROTOCOL_TYPE.CANCEL,
                              sender: self._user.sid,
                              content: 'Peer cancelled download transfer',
                              name: self._dataTransfers[transferId].name,
                              ackN: 0
                            }, 'main');
                          }
                      
                          // If data transfer to MCU broadcast has non-interop Peers, send to MCU via the new Datachanel
                          if (Object.keys(self._dataTransfers[transferId].peers[transferId]).length > 0) {
                            self._sendMessageToDataChannel('MCU', {
                              type: self._DC_PROTOCOL_TYPE.CANCEL,
                              sender: self._user.sid,
                              content: 'Peer cancelled download transfer',
                              name: self._dataTransfers[transferId].name,
                              ackN: 0
                            }, transferId);
                          }
                      
                          emitEventFn(Object.keys(self._dataTransfers[transferId].peers.main).concat(
                            Object.keys(self._dataTransfers[transferId].peers[transferId])));
                        } else {
                          var channelProp = 'main';
                      
                          if (!self._dataChannels[peerId]) {
                            log.error([peerId, 'RTCDataChannel', transferId, 'Aborting cancel data transfer as ' +
                              'Peer does not have any Datachannel connections']);
                            return;
                          }
                      
                          if (self._dataChannels[peerId][transferId]) {
                            channelProp = transferId;
                          }
                      
                          self._sendMessageToDataChannel(peerId, {
                            type: self._DC_PROTOCOL_TYPE.CANCEL,
                            sender: self._user.sid,
                            content: 'Peer cancelled download transfer',
                            name: self._dataTransfers[transferId].name,
                            ackN: 0
                          }, channelProp);
                      
                          emitEventFn([peerId], peerId);
                        }
                      };
                      
                      /**
                       * Function that sends a message to Peers via the Datachannel connection.
                       * <small>Consider using <a href="#method_sendURLData"><code>sendURLData()</code> method</a> if you are
                       * sending large strings to Peers.</small>
                       * @method sendP2PMessage
                       * @param {String|JSON} message The message.
                       * @param {String|Array} [targetPeerId] The target Peer ID to send message to.
                       * - When provided as an Array, it will send the message to only Peers which IDs are in the list.
                       * - When not provided, it will broadcast the message to all connected Peers with Datachannel connection in the Room.
                       * @trigger <ol class="desc-seq">
                       *  <li>Sends P2P message to all targeted Peers. <ol>
                       *  <li>If Peer connection Datachannel has not been opened: <small>This can be checked with
                       *  <a href="#event_dataChannelState"><code>dataChannelState</code> event</a>
                       *  triggering parameter payload <code>state</code> as <code>OPEN</code> and
                       *  <code>channelType</code> as <code>MESSAGING</code> for Peer.</small> <ol>
                       *  <li><a href="#event_dataChannelState"><code>dataChannelState</code> event</a> triggers
                       *  parameter payload <code>state</code> as <code>SEND_MESSAGE_ERROR</code>.</li>
                       *  <li><b>ABORT</b> step and return error.</li></ol></li>
                       *  <li><a href="#event_incomingMessage"><code>incomingMessage</code> event</a> triggers
                       *  parameter payload <code>message.isDataChannel</code> value as <code>true</code> and
                       *  <code>isSelf</code> value as <code>true</code>.</li></ol></li></ol>
                       * @example
                       *   // Example 1: Broadcasting to all Peers
                       *   skylinkDemo.on("dataChannelState", function (state, peerId, error, channelName, channelType) {
                       *      if (state === skylinkDemo.DATA_CHANNEL_STATE.OPEN &&
                       *        channelType === skylinkDemo.DATA_CHANNEL_TYPE.MESSAGING) {
                       *        skylinkDemo.sendP2PMessage("Hi all!");
                       *      }
                       *   });
                       *
                       *   // Example 2: Sending to specific Peers
                       *   var peersInExclusiveParty = [];
                       *
                       *   skylinkDemo.on("peerJoined", function (peerId, peerInfo, isSelf) {
                       *     if (isSelf) return;
                       *     if (peerInfo.userData.exclusive) {
                       *       peersInExclusiveParty[peerId] = false;
                       *     }
                       *   });
                       *
                       *   skylinkDemo.on("dataChannelState", function (state, peerId, error, channelName, channelType) {
                       *      if (state === skylinkDemo.DATA_CHANNEL_STATE.OPEN &&
                       *        channelType === skylinkDemo.DATA_CHANNEL_TYPE.MESSAGING) {
                       *        peersInExclusiveParty[peerId] = true;
                       *      }
                       *   });
                       *
                       *   function updateExclusivePartyStatus (message) {
                       *     var readyToSend = [];
                       *     for (var p in peersInExclusiveParty) {
                       *       if (peersInExclusiveParty.hasOwnProperty(p)) {
                       *         readyToSend.push(p);
                       *       }
                       *     }
                       *     skylinkDemo.sendP2PMessage(message, readyToSend);
                       *   }
                       * @for Skylink
                       * @since 0.5.5
                       */
                      Skylink.prototype.sendP2PMessage = function(message, targetPeerId) {
                        var listOfPeers = Object.keys(this._dataChannels);
                        var isPrivate = false;
                      
                        if (Array.isArray(targetPeerId)) {
                          listOfPeers = targetPeerId;
                          isPrivate = true;
                        } else if (targetPeerId && typeof targetPeerId === 'string') {
                          listOfPeers = [targetPeerId];
                          isPrivate = true;
                        }
                      
                        if (!this._inRoom || !(this._user && this._user.sid)) {
                          log.error('Unable to send message as User is not in Room. ->', message);
                          return;
                        }
                      
                        if (!this._enableDataChannel) {
                          log.error('Unable to send message as User does not have Datachannel enabled. ->', message);
                          return;
                        }
                      
                        // Loop out unwanted Peers
                        for (var i = 0; i < listOfPeers.length; i++) {
                          var peerId = listOfPeers[i];
                      
                          if (!this._dataChannels[peerId]) {
                            log.error([peerId, 'RTCDataChannel', null, 'Dropping of sending message to Peer as ' +
                              'Datachannel connection does not exists']);
                            listOfPeers.splice(i, 1);
                            i--;
                          } else if (peerId === 'MCU') {
                            listOfPeers.splice(i, 1);
                            i--;
                          } else if (!this._hasMCU) {
                            log.debug([peerId, 'RTCDataChannel', null, 'Sending ' + (isPrivate ? 'private' : '') +
                              ' P2P message to Peer']);
                      
                            this._sendMessageToDataChannel(peerId, {
                              type: this._DC_PROTOCOL_TYPE.MESSAGE,
                              isPrivate: isPrivate,
                              sender: this._user.sid,
                              target: peerId,
                              data: message
                            }, 'main');
                          }
                        }
                      
                        if (listOfPeers.length === 0) {
                          log.warn('Currently there are no Peers to send P2P message to (unless the message is queued ' +
                            'and there are Peer connected by then).');
                        }
                      
                        if (this._hasMCU) {
                          log.debug(['MCU', 'RTCDataChannel', null, 'Broadcasting ' + (isPrivate ? 'private' : '') +
                            ' P2P message to Peers']);
                      
                          this._sendMessageToDataChannel('MCU', {
                            type: this._DC_PROTOCOL_TYPE.MESSAGE,
                            isPrivate: isPrivate,
                            sender: this._user.sid,
                            target: listOfPeers,
                            data: message
                          }, 'main');
                        }
                      
                        this._trigger('incomingMessage', {
                          content: message,
                          isPrivate: isPrivate,
                          targetPeerId: targetPeerId || null,
                          listOfPeers: listOfPeers,
                          isDataChannel: true,
                          senderPeerId: this._user.sid
                        }, this._user.sid, this.getPeerInfo(), true);
                      };
                      
                      /**
                       * <blockquote class="info">
                       *   Note that this feature is not supported by MCU enabled Peer connections.<br>
                       *   To start streaming data, see the <a href="#method_streamData"><code>streamData()</code>
                       *   method</a>. To stop data streaming session, see the <a href="#method_stopStreamingData"><code>
                       *   stopStreamingData()</code> method</a>.
                       * </blockquote>
                       * Function that starts a data chunks streaming session from User to Peers.
                       * @method startStreamingData
                       * @param {Boolean} [isStringStream=false] The flag if data streaming session sending data chunks
                       *   should be expected as string data chunks sent.
                       *   <small>By default, data chunks are expected to be sent in Blob or ArrayBuffer, and ArrayBuffer
                       *   data chunks will be converted to Blob.</small>
                       * @param {String|Array} [targetPeerId] The target Peer ID to send message to.
                       * - When provided as an Array, it will start streaming session to only Peers which IDs are in the list.
                       * - When not provided, it will start the streaming session to all connected Peers with Datachannel connection in the Room.
                       * @trigger <ol class="desc-seq">
                       * @trigger <ol class="desc-seq">
                       *   <li>Checks if User is in Room. <ol>
                       *   <li>If User is not in Room: <ol><li><a href="#event_dataStreamState">
                       *   <code>dataStreamState</code> event</a> triggers parameter payload <code>state</code>
                       *   as <code>START_ERROR</code>.</li><li><b>ABORT</b> step and return error.</li></ol></li></ol></li>
                       *   <li>Checks if there is any available Datachannel connections. <ol>
                       *   <li>If User is not in Room: <ol><li><a href="#event_dataStreamState">
                       *   <code>dataStreamState</code> event</a> triggers parameter payload <code>state</code>
                       *   as <code>START_ERROR</code>.</li><li><b>ABORT</b> step and return error.</li></ol></li></ol></li>
                       *   <li>Checks if Peer connection and Datachannel connection are in correct states. <ol>
                       *   <li>If Peer connection or session does not exists: <ol><li><a href="#event_dataStreamState">
                       *   <code>dataStreamState</code> event</a> triggers parameter payload <code>state</code>
                       *   as <code>START_ERROR</code>.</li><li><b>ABORT</b> step and return error.</li></ol></li>
                       *   <li>If Peer connection messaging Datachannel has not been opened: <small>This can be checked with
                       *   <a href="#event_dataChannelState"><code>dataChannelState</code> event</a> triggering parameter
                       *   payload <code>state</code> as <code>OPEN</code> and <code>channelType</code> as
                       *   <code>MESSAGING</code> for Peer.</small> <ol><li><a href="#event_dataStreamState">
                       *   <code>dataStreamState</code> event</a> triggers parameter payload <code>state</code> as <code>START_ERROR</code>.</li>
                       *   <li><b>ABORT</b> step and return error.</li></ol></li>
                       *   <li>If MCU is enabled for the App Key provided in <a href="#method_init"><code>init()</code>method</a> and connected: <ol>
                       *   <li>If MCU Peer connection messaging Datachannel has not been opened: <small>This can be checked with
                       *   <a href="#event_dataChannelState"><code>dataChannelState</code> event</a> triggering parameter
                       *   payload <code>state</code> as <code>OPEN</code>, <code>peerId</code> value as <code>"MCU"</code>
                       *   and <code>channelType</code> as <code>MESSAGING</code> for MCU Peer.</small>
                       *   <ol><li><a href="#event_dataStreamState"><code>dataStreamState</code> event</a> triggers
                       *   parameter payload <code>state</code> as <code>START_ERROR</code>.</li>
                       *   <li><b>ABORT</b> step and return error.</li></ol></li></ol></li>
                       *   <li>Checks if should open a new data Datachannel.<ol>
                       *   <li>If Peer supports simultaneous data streaming, open new data Datachannel: <small>If MCU is connected,
                       *   this opens a new data Datachannel with MCU Peer with all the Peers IDs information that supports
                       *   simultaneous data transfers targeted for the data streaming session instead of opening new data Datachannel
                       *   with all Peers targeted for the data streaming session.</small> <ol>
                       *   <li><a href="#event_dataChannelState"><code>dataChannelState</code> event</a> triggers parameter
                       *   payload <code>state</code> as <code>CONNECTING</code> and <code>channelType</code> as <code>DATA</code>.
                       *   <small>Note that there is no timeout to wait for parameter payload <code>state</code> to be
                       *   <code>OPEN</code>.</small></li>
                       *   <li>If Datachannel has been created and opened successfully: <ol>
                       *   <li><a href="#event_dataChannelState"><code>dataChannelState</code> event</a> triggers parameter payload
                       *   <code>state</code> as <code>OPEN</code> and <code>channelType</code> as <code>DATA</code>.</li></ol></li>
                       *   <li>Else: <ol><li><a href="#event_dataChannelState"><code>dataChannelState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>CREATE_ERROR</code> and <code>channelType</code> as
                       *   <code>DATA</code>.</li><li><a href="#event_dataStreamState"><code>dataStreamState</code> event</a> triggers
                       *   parameter payload <code>state</code> as <code>START_ERROR</code>.</li><li><b>ABORT</b> step and
                       *   return error.</li></ol></li></ol></li><li>Else: <small>If MCU is connected,
                       *   this uses the messaging Datachannel with MCU Peer with all the Peers IDs information that supports
                       *   simultaneous data transfers targeted for the data streaming session instead of using the messaging Datachannels
                       *   with all Peers targeted for the data streaming session.</small> <ol><li>If messaging Datachannel connection has a
                       *   data streaming in-progress: <ol><li><a href="#event_dataStreamState"><code>dataStreamState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>START_ERROR</code>.</li><li><b>ABORT</b> step and
                       *   return error.</li></ol></li><li>If there is any conflicting <a href="#method_streamData"><code>streamData()</code>
                       *   method</a> data streaming session: <small>If <code>isStringStream</code> is provided as <code>true</code> and
                       *   <a href="#method_sendBlobData"><code>sendBlobData()</code> method</a> or <a href="#method_sendURLData">
                       *   <code>sendURLData()</code> method</a> has an existing binary string transfer, it cannot start string data
                       *   streaming session. Else if <a href="#method_sendBlobData"><code>sendBlobData()</code> method</a>
                       *   has an existing binary data transfer, it cannot start binary data streaming session.</small><ol>
                       *   <li><a href="#event_dataStreamState"><code>dataStreamState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>START_ERROR</code>.</li><li><b>ABORT</b> step and
                       *   return error.</li></ol></li></li></ol></ol></li></ol></li>
                       *   <li>Starts the data streaming session with Peer. <ol>
                       *   <li><a href="#event_incomingDataStreamStarted"><code>incomingDataStreamStarted</code> event</a> triggers.</li>
                       *   <li><em>For User only</em> <a href="#event_dataStreamState"><code>dataStreamState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>SENDING_STARTED</code>.</li>
                       *   <li><em>For Peer only</em> <a href="#event_dataStreamState"><code>dataStreamState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>RECEIVING_STARTED</code>.</li></ol></li></ol>
                       * @example
                       *   // Example 1: Start streaming to all Peers
                       *   skylinkDemo.on("dataChannelState", function (state, peerId, error, channelName, channelType) {
                       *      if (state === skylinkDemo.DATA_CHANNEL_STATE.OPEN &&
                       *        channelType === skylinkDemo.DATA_CHANNEL_TYPE.MESSAGING) {
                       *        skylinkDemo.startStreamingData(false);
                       *      }
                       *   });
                       *
                       *   // Example 2: Start streaming to specific Peers
                       *   var peersInExclusiveParty = [];
                       *
                       *   skylinkDemo.on("peerJoined", function (peerId, peerInfo, isSelf) {
                       *     if (isSelf) return;
                       *     if (peerInfo.userData.exclusive) {
                       *       peersInExclusiveParty[peerId] = false;
                       *     }
                       *   });
                       *
                       *   skylinkDemo.on("dataChannelState", function (state, peerId, error, channelName, channelType) {
                       *      if (state === skylinkDemo.DATA_CHANNEL_STATE.OPEN &&
                       *        channelType === skylinkDemo.DATA_CHANNEL_TYPE.MESSAGING) {
                       *        peersInExclusiveParty[peerId] = true;
                       *      }
                       *   });
                       *
                       *   function updateExclusivePartyStatus (message) {
                       *     var readyToSend = [];
                       *     for (var p in peersInExclusiveParty) {
                       *       if (peersInExclusiveParty.hasOwnProperty(p)) {
                       *         readyToSend.push(p);
                       *       }
                       *     }
                       *     skylinkDemo.startStreamingData(message, readyToSend);
                       *   }
                       * @beta
                       * @for Skylink
                       * @since 0.6.18
                       */
                      Skylink.prototype.startStreamingData = function(isStringStream, targetPeerId) {
                        var self = this;
                        var listOfPeers = Object.keys(self._dataChannels);
                        var isPrivate = false;
                        var sessionChunkType = 'binary';
                      
                        if (Array.isArray(targetPeerId)) {
                          listOfPeers = targetPeerId;
                          isPrivate = true;
                        } else if (targetPeerId && typeof targetPeerId === 'string') {
                          listOfPeers = [targetPeerId];
                          isPrivate = true;
                        }
                      
                        if (Array.isArray(isStringStream)) {
                          listOfPeers = isStringStream;
                          targetPeerId = isStringStream;
                          isPrivate = true;
                        } else if (isStringStream && typeof isStringStream === 'string') {
                          listOfPeers = [isStringStream];
                          targetPeerId = isStringStream;
                          isPrivate = true;
                        } else if (isStringStream && typeof isStringStream === 'boolean') {
                          sessionChunkType = 'string';
                        }
                      
                        var sessionInfo = {
                          chunk: null,
                          chunkSize: 0,
                          chunkType: sessionChunkType === 'string' ? self.DATA_TRANSFER_DATA_TYPE.STRING : self._binaryChunkType,
                          isPrivate: isPrivate,
                          isStringStream: sessionChunkType === 'string',
                          senderPeerId: self._user && self._user.sid ? self._user.sid : null
                        };
                      
                        // Remove MCU from list
                        if (listOfPeers.indexOf('MCU') > -1) {
                          listOfPeers.splice(listOfPeers.indexOf('MCU'), 1);
                        }
                      
                        var emitErrorBeforeStreamingFn = function (error) {
                          log.error(error);
                      
                          /*if (listOfPeers.length > 0) {
                            for (var i = 0; i < listOfPeers.length; i++) {
                              self._trigger('dataStreamState', self.DATA_STREAM_STATE.START_ERROR, null,
                                listOfPeers[i], sessionInfo, new Error(error));
                            }
                          } else {
                            self._trigger('dataStreamState', self.DATA_STREAM_STATE.START_ERROR, null,
                              null, sessionInfo, new Error(error));
                          }*/
                        };
                      
                        if (!this._inRoom || !(this._user && this._user.sid)) {
                          return emitErrorBeforeStreamingFn('Unable to start data streaming as User is not in Room.');
                        }
                      
                        if (!this._enableDataChannel) {
                          return emitErrorBeforeStreamingFn('Unable to start data streaming as User does not have Datachannel enabled.');
                        }
                      
                        if (listOfPeers.length === 0) {
                          return emitErrorBeforeStreamingFn('Unable to start data streaming as there are no Peers to start session with.');
                        }
                      
                        if (self._hasMCU) {
                          return emitErrorBeforeStreamingFn('Unable to start data streaming as this feature is current not supported by MCU yet.');
                        }
                      
                        var transferId = 'stream_' + (self._user && self._user.sid ? self._user.sid : '-') + '_' + (new Date()).getTime();
                        var peersInterop = [];
                        var peersNonInterop = [];
                        var sessions = {};
                        var listenToPeerFn = function (peerId, channelProp) {
                          var hasStarted = false;
                          sessions[peerId] = channelProp;
                      
                          self.once('dataStreamState', function () {}, function (state, evtTransferId, evtPeerId, evtSessionInfo) {
                            if (!(evtTransferId === transferId && evtPeerId === peerId)) {
                              return;
                            }
                      
                            var dataChunk = evtSessionInfo.chunk;
                            var updatedSessionInfo = clone(evtSessionInfo);
                            delete updatedSessionInfo.chunk;
                      
                            if (state === self.DATA_STREAM_STATE.SENDING_STARTED) {
                              hasStarted = true;
                              return;
                            }
                      
                            if (hasStarted && [self.DATA_STREAM_STATE.ERROR, self.DATA_STREAM_STATE.SENDING_STOPPED].indexOf(state) > -1) {
                              if (channelProp === transferId) {
                                self._closeDataChannel(peerId, transferId);
                              }
                      
                              if (self._dataStreams[transferId] && self._dataStreams[transferId].sessions[peerId]) {
                                delete self._dataStreams[transferId].sessions[peerId];
                      
                                if (Object.keys(self._dataStreams[transferId].sessions).length === 0) {
                                  delete self._dataStreams[transferId];
                                }
                              }
                              return true;
                            }
                          });
                        };
                      
                        // Loop out unwanted Peers
                        for (var i = 0; i < listOfPeers.length; i++) {
                          var peerId = listOfPeers[i];
                          var error = null;
                          var dtProtocolVersion = ((self._peerInformations[peerId] || {}).agent || {}).DTProtocolVersion || '';
                          var channelProp = self._isLowerThanVersion(dtProtocolVersion, '0.1.2') ? 'main' : transferId;
                      
                          if (!(self._dataChannels[peerId] && self._dataChannels[peerId].main)) {
                            error = 'Datachannel connection does not exists';
                          } else if (self._hasMCU && !(self._dataChannels.MCU && self._dataChannels.MCU.main)) {
                            error = 'MCU Datachannel connection does not exists';
                          } else if (self._isLowerThanVersion(dtProtocolVersion, '0.1.3')) {
                            error = 'Peer DTProtocolVersion does not support data streaming. (received: "' + dtProtocolVersion + '", expected: "0.1.3")';
                          } else {
                            if (channelProp === 'main') {
                              var dataTransferId = self._hasMCU ? self._dataChannels.MCU.main.transferId : self._dataChannels[peerId].main.transferId;
                      
                              if (self._dataChannels[peerId].main.streamId) {
                                error = 'Peer Datachannel currently has an active data transfer session.';
                              } else if (self._hasMCU && self._dataChannels.MCU.main.streamId) {
                                error = 'MCU Peer Datachannel currently has an active data transfer session.';
                              } else if (self._dataTransfers[dataTransferId] && self._dataTransfers[dataTransferId].sessionChunkType === sessionChunkType) {
                                error = (self._hasMCU ? 'MCU ' : '') + 'Peer Datachannel currently has an active ' + sessionChunkType + ' data transfer.';
                              } else {
                                peersInterop.push(peerId);
                              }
                            } else {
                              peersNonInterop.push(peerId);
                            }
                          }
                      
                          if (error) {
                            self._trigger('dataStreamState', self.DATA_STREAM_STATE.START_ERROR, transferId, peerId, sessionInfo, new Error(error));
                            listOfPeers.splice(i, 1);
                            i--;
                          } else {
                            listenToPeerFn(peerId, channelProp);
                          }
                        }
                      
                        if (listOfPeers.length === 0) {
                          log.warn('There are no Peers to start data session with.');
                          return;
                        }
                      
                        self._dataStreams[transferId] = {
                          sessions: sessions,
                          chunkType: sessionChunkType === 'string' ? self.DATA_TRANSFER_DATA_TYPE.STRING : self._binaryChunkType,
                          sessionChunkType: sessionChunkType,
                          isPrivate: isPrivate,
                          isStringStream: sessionChunkType === 'string',
                          senderPeerId: self._user && self._user.sid ? self._user.sid : null,
                          isUpload: true
                        };
                      
                        var startDataSessionFn = function (peerId, channelProp, targetPeers) {
                          self.once('dataChannelState', function () {}, function (state, evtPeerId, channelName, channelType, error) {
                            if (!self._dataStreams[transferId]) {
                              return true;
                            }
                      
                            if (!(evtPeerId === peerId && (channelProp === 'main' ? channelType === self.DATA_CHANNEL_TYPE.MESSAGING :
                              channelName === transferId && channelType === self.DATA_CHANNEL_TYPE.DATA))) {
                              return;
                            }
                      
                            if ([self.DATA_CHANNEL_STATE.ERROR, self.DATA_CHANNEL_STATE.CLOSED].indexOf(state) > -1) {
                              var updatedError = new Error(error && error.message ? error.message :
                                'Failed data transfer as datachannel state is "' + state + '".');
                      
                              if (peerId === 'MCU') {
                                for (var mp = 0; mp < targetPeers.length; mp++) {
                                  self._trigger('dataStreamState', self.DATA_STREAM_STATE.ERROR, transferId, targetPeers[mp], sessionInfo, updatedError);
                                }
                              } else {
                                self._trigger('dataStreamState', self.DATA_STREAM_STATE.ERROR, transferId, peerId, sessionInfo, updatedError);
                              }
                              return true;
                            }
                          });
                      
                          if (!(self._dataChannels[peerId][channelProp] &&
                            self._dataChannels[peerId][channelProp].channel.readyState === self.DATA_CHANNEL_STATE.OPEN)) {
                            var notOpenError = new Error('Failed starting data streaming session as channel is not opened.');
                            if (peerId === 'MCU') {
                              for (i = 0; i < targetPeers.length; i++) {
                                self._trigger('dataStreamState', self.DATA_STREAM_STATE.START_ERROR, transferId, targetPeers[i], sessionInfo, notOpenError);
                              }
                            } else {
                              self._trigger('dataStreamState', self.DATA_STREAM_STATE.START_ERROR, transferId, peerId, sessionInfo, notOpenError);
                            }
                          }
                      
                          self._sendMessageToDataChannel(peerId, {
                            type: self._DC_PROTOCOL_TYPE.WRQ,
                            transferId: transferId,
                            name: transferId,
                            size: 0,
                            originalSize: 0,
                            dataType: 'fastBinaryStart',
                            mimeType: null,
                            chunkType: sessionChunkType,
                            chunkSize: 0,
                            timeout: 0,
                            isPrivate: isPrivate,
                            sender: self._user.sid,
                            agent: window.webrtcDetectedBrowser,
                            version: window.webrtcDetectedVersion,
                            target: peerId === 'MCU' ? targetPeers : peerId
                          }, channelProp);
                          self._dataChannels[peerId][channelProp].streamId = transferId;
                      
                          var updatedSessionInfo = clone(sessionInfo);
                          delete updatedSessionInfo.chunk;
                      
                          if (peerId === 'MCU') {
                            for (var tp = 0; tp < targetPeers.length; tp++) {
                              self._trigger('dataStreamState', self.DATA_STREAM_STATE.SENDING_STARTED, transferId, targetPeers[tp], sessionInfo, null);
                              self._trigger('incomingDataStreamStarted', transferId, targetPeers[tp], updatedSessionInfo, true);
                            }
                          } else {
                            self._trigger('dataStreamState', self.DATA_STREAM_STATE.SENDING_STARTED, transferId, peerId, sessionInfo, null);
                            self._trigger('incomingDataStreamStarted', transferId, peerId, updatedSessionInfo, true);
                          }
                        };
                      
                        var waitForChannelOpenFn = function (peerId, targetPeers) {
                          self.once('dataChannelState', function (state, evtPeerId, error) {
                            if (state === self.DATA_CHANNEL_STATE.CREATE_ERROR) {
                              if (peerId === 'MCU') {
                                for (var mp = 0; mp < targetPeers.length; mp++) {
                                  self._trigger('dataStreamState', self.DATA_STREAM_STATE.START_ERROR, transferId, targetPeers[mp], sessionInfo, error);
                                }
                              } else {
                                self._trigger('dataStreamState', self.DATA_STREAM_STATE.START_ERROR, transferId, peerId, sessionInfo, error);
                              }
                              return;
                            }
                            startDataSessionFn(peerId, transferId, targetPeers);
                          }, function (state, evtPeerId, error, channelName, channelType) {
                            if (evtPeerId === peerId && channelName === transferId && channelType === self.DATA_CHANNEL_TYPE.DATA) {
                              return [self.DATA_CHANNEL_STATE.CREATE_ERROR, self.DATA_CHANNEL_STATE.OPEN].indexOf(state) > -1;
                            }
                          });
                          self._createDataChannel(peerId, transferId, sessionChunkType === 'string' ? self._CHUNK_DATAURL_SIZE :
                            (window.webrtcDetectedBrowser === 'firefox' ? self._MOZ_BINARY_FILE_SIZE : self._BINARY_FILE_SIZE));
                        };
                      
                        if (peersNonInterop.length > 0) {
                          if (self._hasMCU) {
                            waitForChannelOpenFn('MCU', peersNonInterop);
                          } else {
                            for (var pni = 0; pni < peersNonInterop.length; pni++) {
                              waitForChannelOpenFn(peersNonInterop[pni], null);
                            }
                          }
                        }
                      
                        if (peersInterop.length > 0) {
                          if (self._hasMCU) {
                            startDataSessionFn('MCU', 'main', peersInterop);
                          } else {
                            for (var pi = 0; pi < peersInterop.length; pi++) {
                              startDataSessionFn(peersInterop[pi], 'main', null);
                            }
                          }
                        }
                      };
                      
                      /**
                       * <blockquote class="info">
                       *   Note that this feature is not supported by MCU enabled Peer connections.<br>
                       *   To start data streaming session, see the <a href="#method_startStreamingData"><code>startStreamingData()</code>
                       *   method</a>. To stop data streaming session, see the <a href="#method_stopStreamingData"><code>stopStreamingData()</code> method</a>
                       * </blockquote>
                       * Function that sends a data chunk from User to Peers for an existing active data streaming session.
                       * @method streamData
                       * @param {String} streamId The data streaming session ID.
                       * @param {String|Blob|ArrayBuffer} chunk The data chunk.
                       *   <small>By default when it is not string data streaming, data chunks when is are expected to be
                       *   sent in Blob or ArrayBuffer, and ArrayBuffer data chunks will be converted to Blob.</small>
                       *   <small>For binary data chunks, the limit is <code>65456</code>.</small>
                       *   <small>For string data chunks, the limit is <code>1212</code>.</small>
                       * @trigger <ol class="desc-seq">
                       *   <li>Checks if Peer connection and Datachannel connection are in correct states. <ol>
                       *   <li>If Peer connection (or MCU Peer connection if enabled)
                       *   data streaming Datachannel has not been opened: <small>This can be checked with
                       *   <a href="#event_dataChannelState"><code>dataChannelState</code> event</a> triggering parameter
                       *   payload <code>state</code> as <code>OPEN</code> and <code>channelType</code> as
                       *   <code>MESSAGING</code> for Peer.</small> <ol><li><a href="#event_dataStreamState">
                       *   <code>dataStreamState</code> event</a> triggers parameter payload <code>state</code> as <code>ERROR</code>.</li>
                       *   <li><b>ABORT</b> step and return error.</li></ol></li></ol></li>
                       *   <li>Starts sending the data chunk to Peer. <ol>
                       *   <li><a href="#event_incomingDataStream"><code>incomingDataStream</code> event</a> triggers.</li>
                       *   <li><em>For User only</em> <a href="#event_dataStreamState"><code>dataStreamState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>SENT</code>.</li>
                       *   <li><em>For Peer only</em> <a href="#event_dataStreamState"><code>dataStreamState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>RECEIVED</code>.</li></ol></li></ol>
                       * @example
                       *   // Example 1: Start streaming
                       *   var currentStreamId = null
                       *   if (file.size > chunkLimit) {
                       *     while ((file.size - 1) > endCount) {
                       *       endCount = startCount + chunkLimit;
                       *       chunks.push(file.slice(startCount, endCount));
                       *       startCount += chunkLimit;
                       *     }
                       *     if ((file.size - (startCount + 1)) > 0) {
                       *       chunks.push(file.slice(startCount, file.size - 1));
                       *     }
                       *   } else {
                       *     chunks.push(file);
                       *   }
                       *   var processNextFn = function () {
                       *     if (chunks.length > 0) {
                       *       skylinkDemo.once("incomingDataStream", function () {
                       *         setTimeout(processNextFn, 1);
                       *       }, function (data, evtStreamId, evtPeerId, streamInfo, isSelf) {
                       *         return isSelf && evtStreamId === currentStreamId;
                       *       });
                       *       var chunk = chunks[0];
                       *       chunks.splice(0, 1);
                       *       skylinkDemo.streamData(currentStreamId, chunk);
                       *     } else {
                       *       skylinkDemo.stopStreamingData(currentStreamId);
                       *     }
                       *   };
                       *   skylinkDemo.once("incomingDataStreamStarted", processNextFn, function (streamId, peerId, streamInfo, isSelf) {
                       *     currentStreamId = streamId;
                       *     return isSelf;
                       *   });
                       *   skylinkDemo.once("incomingDataStreamStopped", function () {
                       *     // Render file
                       *   }, function (streamId, peerId, streamInfo, isSelf) {
                       *     return currentStreamId === streamId && isSelf;
                       *   });
                       *   skylinkDemo.startStreamingData(false);
                       * @beta
                       * @for Skylink
                       * @since 0.6.18
                       */
                      Skylink.prototype.streamData = function(transferId, dataChunk) {
                        var self = this;
                      
                        if (!(transferId && typeof transferId === 'string')) {
                          log.error('Failed streaming data chunk as stream session ID is not provided.');
                          return;
                        }
                      
                        if (!(dataChunk && typeof dataChunk === 'object' && (dataChunk instanceof Blob || dataChunk instanceof ArrayBuffer))) {
                          log.error('Failed streaming data chunk as it is not provided.');
                          return;
                        }
                      
                        if (!(self._inRoom && self._user && self._user.sid)) {
                          log.error('Failed streaming data chunk as User is not in the Room.');
                          return;
                        }
                      
                        if (!self._dataStreams[transferId]) {
                          log.error('Failed streaming data chunk as session does not exists.');
                          return;
                        }
                      
                        if (!self._dataStreams[transferId].isUpload) {
                          log.error('Failed streaming data chunk as session is not sending.');
                          return;
                        }
                      
                        if (self._dataStreams[transferId].sessionChunkType === 'string' ? typeof dataChunk !== 'string' :
                          typeof dataChunk !== 'object') {
                          log.error('Failed streaming data chunk as data chunk does not match expected data type.');
                          return;
                        }
                      
                        if (self._hasMCU) {
                          log.error('Failed streaming data chunk as MCU does not support this feature yet.');
                          return;
                        }
                      
                        var updatedDataChunk = dataChunk instanceof ArrayBuffer ? new Blob(dataChunk) : dataChunk;
                      
                        if (self._dataStreams[transferId].sessionChunkType === 'string' ? updatedDataChunk.length > self._CHUNK_DATAURL_SIZE :
                          updatedDataChunk.length > self._BINARY_FILE_SIZE) {
                          log.error('Failed streaming data chunk as data chunk exceeds maximum chunk limit.');
                          return;
                        }
                      
                        var sessionInfo = {
                          chunk: updatedDataChunk,
                          chunkSize: updatedDataChunk.size || updatedDataChunk.length || updatedDataChunk.byteLength,
                          chunkType: self._dataStreams[transferId].sessionChunkType === 'string' ?
                            self.DATA_TRANSFER_DATA_TYPE.STRING : self._binaryChunkType,
                          isPrivate: self._dataStreams[transferId].sessionChunkType.isPrivate,
                          isStringStream: self._dataStreams[transferId].sessionChunkType === 'string',
                          senderPeerId: self._user && self._user.sid ? self._user.sid : null
                        };
                      
                        var peersInterop = [];
                        var peersNonInterop = [];
                        var sendDataFn = function (peerId, channelProp, targetPeers) {
                          // When ready to be sent
                          var onSendDataFn = function (buffer) {
                            self._sendMessageToDataChannel(peerId, buffer, channelProp, true);
                      
                            var updatedSessionInfo = clone(sessionInfo);
                            delete updatedSessionInfo.chunk;
                      
                            if (targetPeers) {
                              for (var i = 0; i < targetPeers.length; i++) {
                                self._trigger('dataStreamState', self.DATA_STREAM_STATE.SENT, transferId, targetPeers[i], sessionInfo, null);
                                self._trigger('incomingDataStream', dataChunk, transferId, targetPeers[i], updatedSessionInfo, true);
                              }
                            } else {
                              self._trigger('dataStreamState', self.DATA_STREAM_STATE.SENT, transferId, peerId, sessionInfo, null);
                              self._trigger('incomingDataStream', dataChunk, transferId, peerId, updatedSessionInfo, true);
                            }
                          };
                      
                          if (dataChunk instanceof Blob && sessionInfo.chunkType === self.DATA_TRANSFER_DATA_TYPE.ARRAY_BUFFER) {
                            self._blobToArrayBuffer(dataChunk, onSendDataFn);
                          } else if (!(dataChunk instanceof Blob) && sessionInfo.chunkType === self.DATA_TRANSFER_DATA_TYPE.BLOB) {
                            onSendDataFn(new Blob([dataChunk]));
                          } else if (['IE', 'safari'].indexOf(window.webrtcDetectedBrowser) > -1 && typeof dataChunk !== 'string') {
                            onSendDataFn(new Int8Array(dataChunk));
                          } else {
                            onSendDataFn(dataChunk);
                          }
                        };
                      
                        for (var peerId in self._dataStreams[transferId].sessions) {
                          if (self._dataStreams[transferId].sessions.hasOwnProperty(peerId) && self._dataStreams[transferId].sessions[peerId]) {
                            var channelProp = self._dataStreams[transferId].sessions[peerId];
                      
                            if (!(self._dataChannels[self._hasMCU ? 'MCU' : peerId] && self._dataChannels[self._hasMCU ? 'MCU' : peerId][channelProp] &&
                              self._dataChannels[self._hasMCU ? 'MCU' : peerId][channelProp].channel.readyState === self.DATA_CHANNEL_STATE.OPEN &&
                              self._dataChannels[self._hasMCU ? 'MCU' : peerId][channelProp].streamId === transferId)) {
                              log.error([peerId, 'RTCDataChannel', transferId, 'Failed streaming data as it has not started or is ready.']);
                              self._trigger('dataStreamState', self.DATA_STREAM_STATE.ERROR, transferId, peerId, sessionInfo,
                                new Error('Streaming as it has not started or Datachannel connection is not open.'));
                              return;
                            }
                      
                            if (self._hasMCU) {
                              if (channelProp === 'main') {
                                peersInterop.push(peerId);
                              } else {
                                peersNonInterop.push(peerId);
                              }
                            } else {
                              sendDataFn(peerId, channelProp);
                            }
                          }
                        }
                      
                        if (self._hasMCU) {
                          if (peersInterop.length > 0) {
                            sendDataFn(peerId, 'main', peersInterop);
                          }
                          if (peersNonInterop.length > 0) {
                            sendDataFn(peerId, transferId, peersNonInterop);
                          }
                        }
                      };
                      
                      /**
                       * <blockquote class="info">
                       *   To start data streaming session, see the <a href="#method_startStreamingData"><code>startStreamingData()</code>
                       *   method</a> To start streaming data, see the <a href="#method_streamData"><code>streamData()</code>
                       *   method</a>.
                       * </blockquote>
                       * Function that stops a data chunks streaming session from User to Peers.
                       * @method stopStreamingData
                       * @param {String} streamId The data streaming session ID.
                       * @trigger <ol class="desc-seq">
                       *   <li>Checks if Peer connection and Datachannel connection are in correct states. <ol>
                       *   <li>If Peer connection (or MCU Peer connection if enabled)
                       *   data streaming Datachannel has not been opened: <small>This can be checked with
                       *   <a href="#event_dataChannelState"><code>dataChannelState</code> event</a> triggering parameter
                       *   payload <code>state</code> as <code>OPEN</code> and <code>channelType</code> as
                       *   <code>MESSAGING</code> for Peer.</small> <ol><li><a href="#event_dataStreamState">
                       *   <code>dataStreamState</code> event</a> triggers parameter payload <code>state</code> as <code>ERROR</code>.</li>
                       *   <li><b>ABORT</b> step and return error.</li></ol></li></ol></li>
                       *   <li>Stops the data streaming session to Peer. <ol>
                       *   <li><a href="#event_incomingDataStreamStopped"><code>incomingDataStreamStopped</code> event</a> triggers.</li>
                       *   <li><em>For User only</em> <a href="#event_dataStreamState"><code>dataStreamState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>SENDING_STOPPED</code>.</li>
                       *   <li><em>For Peer only</em> <a href="#event_dataStreamState"><code>dataStreamState</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>RECEIVING_STOPPED</code>.</li></ol></li></ol>
                       * @example
                       *   skylinkDemo.stopStreamData(streamId);
                       * @beta
                       * @for Skylink
                       * @since 0.6.18
                       */
                      Skylink.prototype.stopStreamingData = function(transferId) {
                        var self = this;
                      
                        if (!(transferId && typeof transferId === 'string')) {
                          log.error('Failed streaming data chunk as stream session ID is not provided.');
                          return;
                        }
                      
                        if (!(self._inRoom && self._user && self._user.sid)) {
                          log.error('Failed streaming data chunk as User is not in the Room.');
                          return;
                        }
                      
                        if (!self._dataStreams[transferId]) {
                          log.error('Failed stopping data streaming session as it does not exists.');
                          return;
                        }
                      
                        if (!self._dataStreams[transferId].isUpload) {
                          log.error('Failed stopping data streaming session as it is not sending.');
                          return;
                        }
                      
                        if (self._hasMCU) {
                          log.error('Failed stopping data streaming session as MCU does not support this feature yet.');
                          return;
                        }
                      
                        var sessionInfo = {
                          chunk: null,
                          chunkSize: 0,
                          chunkType: self._dataStreams[transferId].sessionChunkType === 'string' ?
                            self.DATA_TRANSFER_DATA_TYPE.STRING : self._binaryChunkType,
                          isPrivate: self._dataStreams[transferId].sessionChunkType.isPrivate,
                          isStringStream: self._dataStreams[transferId].sessionChunkType === 'string',
                          senderPeerId: self._user && self._user.sid ? self._user.sid : null
                        };
                      
                        var peersInterop = [];
                        var peersNonInterop = [];
                        var sendDataFn = function (peerId, channelProp, targetPeers) {
                          self._sendMessageToDataChannel(peerId, {
                            type: self._DC_PROTOCOL_TYPE.WRQ,
                            transferId: transferId,
                            name: transferId,
                            size: 0,
                            originalSize: 0,
                            dataType: 'fastBinaryStop',
                            mimeType: null,
                            chunkType: self._dataStreams[transferId].sessionChunkType,
                            chunkSize: 0,
                            timeout: 0,
                            isPrivate: self._dataStreams[transferId].isPrivate,
                            sender: self._user.sid,
                            agent: window.webrtcDetectedBrowser,
                            version: window.webrtcDetectedVersion,
                            target: targetPeers ? targetPeers : peerId
                          }, channelProp);
                      
                          var updatedSessionInfo = clone(sessionInfo);
                          delete updatedSessionInfo.chunk;
                      
                          if (targetPeers) {
                            for (var i = 0; i < targetPeers.length; i++) {
                              self._trigger('dataStreamState', self.DATA_STREAM_STATE.SENDING_STOPPED, transferId, targetPeers[i], sessionInfo, null);
                              self._trigger('incomingDataStreamStopped', transferId, targetPeers[i], updatedSessionInfo, true);
                            }
                          } else {
                            self._trigger('dataStreamState', self.DATA_STREAM_STATE.SENDING_STOPPED, transferId, peerId, sessionInfo, null);
                            self._trigger('incomingDataStreamStopped', transferId, peerId, updatedSessionInfo, true);
                          }
                        };
                      
                        for (var peerId in self._dataStreams[transferId].sessions) {
                          if (self._dataStreams[transferId].sessions.hasOwnProperty(peerId) && self._dataStreams[transferId].sessions[peerId]) {
                            var channelProp = self._dataStreams[transferId].sessions[peerId];
                      
                            if (!(self._dataChannels[self._hasMCU ? 'MCU' : peerId] && self._dataChannels[self._hasMCU ? 'MCU' : peerId][channelProp] &&
                              self._dataChannels[self._hasMCU ? 'MCU' : peerId][channelProp].channel.readyState === self.DATA_CHANNEL_STATE.OPEN &&
                              self._dataChannels[self._hasMCU ? 'MCU' : peerId][channelProp].streamId === transferId)) {
                              log.error([peerId, 'RTCDataChannel', transferId, 'Failed stopping data streaming session as channel is closed.']);
                              self._trigger('dataStreamState', self.DATA_STREAM_STATE.ERROR, transferId, peerId, sessionInfo,
                                new Error('Failed stopping data streaming session as Datachannel connection is not open or is active.'));
                              return;
                            }
                      
                            if (self._hasMCU) {
                              if (self._dataStreams[transferId].sessions[peerId] === 'main') {
                                peersInterop.push(peerId);
                              } else {
                                peersNonInterop.push(peerId);
                              }
                            } else {
                              sendDataFn(peerId, channelProp);
                            }
                          }
                        }
                      
                        if (self._hasMCU) {
                          if (peersInterop.length > 0) {
                            sendDataFn(peerId, 'main', peersInterop);
                          }
                          if (peersNonInterop.length > 0) {
                            sendDataFn(peerId, transferId, peersNonInterop);
                          }
                        }
                      };
                      
                      
                      /**
                       * Function that starts the data transfer to Peers.
                       * @method _startDataTransfer
                       * @private
                       * @for Skylink
                       * @since 0.6.1
                       */
                      Skylink.prototype._startDataTransfer = function(data, timeout, targetPeerId, sendChunksAsBinary, callback, sessionType) {
                        var self = this;
                        var transferId = (self._user ? self._user.sid : '') + '_' + (new Date()).getTime();
                        var transferErrors = {};
                        var transferCompleted = [];
                        var chunks = [];
                        var listOfPeers = Object.keys(self._peerConnections);
                        var sessionChunkType = 'string';
                        var transferInfo = {
                          name: null,
                          size: null,
                          chunkSize: null,
                          chunkType: null,
                          dataType: null,
                          mimeType: null,
                          direction: self.DATA_TRANSFER_TYPE.UPLOAD,
                          timeout: 60,
                          isPrivate: false,
                          percentage: 0
                        };
                      
                        // sendBlobData(.., timeout)
                        if (typeof timeout === 'number') {
                          transferInfo.timeout = timeout;
                        } else if (Array.isArray(timeout)) {
                          listOfPeers = timeout;
                        } else if (timeout && typeof timeout === 'string') {
                          listOfPeers = [timeout];
                        } else if (timeout && typeof timeout === 'boolean') {
                          sessionChunkType = 'binary';
                        } else if (typeof timeout === 'function') {
                          callback = timeout;
                        }
                      
                        // sendBlobData(.., .., targetPeerId)
                        if (Array.isArray(targetPeerId)) {
                          listOfPeers = targetPeerId;
                        } else if (targetPeerId && typeof targetPeerId === 'string') {
                          listOfPeers = [targetPeerId];
                        } else if (targetPeerId && typeof targetPeerId === 'boolean') {
                          sessionChunkType = 'binary';
                        } else if (typeof targetPeerId === 'function') {
                          callback = targetPeerId;
                        }
                      
                        // sendBlobData(.., .., .., sendChunksAsBinary)
                        if (sendChunksAsBinary && typeof sendChunksAsBinary === 'boolean') {
                          sessionChunkType = 'binary';
                        } else if (typeof sendChunksAsBinary === 'function') {
                          callback = sendChunksAsBinary;
                        }
                      
                        // Remove MCU Peer as list of Peers
                        if (listOfPeers.indexOf('MCU') > -1) {
                          listOfPeers.splice(listOfPeers.indexOf('MCU'), 1);
                        }
                      
                        // Function that returns the error emitted before data transfer has started
                        var emitErrorBeforeDataTransferFn = function (error) {
                          log.error(error);
                      
                          if (typeof callback === 'function') {
                            var transferErrors = {};
                      
                            if (listOfPeers.length === 0) {
                              transferErrors.self = new Error(error);
                              /*self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.START_ERROR, null, null, transferInfo, {
                                transferType: self.DATA_TRANSFER_TYPE.DOWNLOAD,
                                message: new Error(error)
                              });*/
                            } else {
                              for (var i = 0; i < listOfPeers.length; i++) {
                                transferErrors[listOfPeers[i]] = new Error(error);
                                /*self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.START_ERROR, null, listOfPeers[i], transferInfo, {
                                  transferType: self.DATA_TRANSFER_TYPE.DOWNLOAD,
                                  message: new Error(error)
                                });*/
                              }
                            }
                      
                            callback({
                              transferId: null,
                              transferInfo: transferInfo,
                              listOfPeers: listOfPeers,
                              transferErrors: transferErrors
                            }, null);
                          }
                        };
                      
                        if (sessionType === 'blob') {
                          if (self._hasMCU && sessionChunkType === 'binary') {
                            log.warn('Binary data chunks transfer is not yet supported with MCU environment. ' +
                              'Fallbacking to binary string data chunks transfer.');
                            sessionChunkType = 'string';
                          }
                      
                          var chunkSize = sessionChunkType === 'string' ? (window.webrtcDetectedBrowser === 'firefox' ?
                            self._MOZ_CHUNK_FILE_SIZE : self._CHUNK_FILE_SIZE) : (window.webrtcDetectedBrowser === 'firefox' ?
                            self._MOZ_BINARY_FILE_SIZE : self._BINARY_FILE_SIZE);
                      
                          transferInfo.dataType = self.DATA_TRANSFER_SESSION_TYPE.BLOB;
                          transferInfo.chunkSize = sessionChunkType === 'string' ? 4 * Math.ceil(chunkSize / 3) : chunkSize;
                          transferInfo.chunkType = sessionChunkType === 'binary' ? self._binaryChunkType : self.DATA_TRANSFER_DATA_TYPE.BINARY_STRING;
                      
                          // Start checking if data transfer can start
                          if (!(data && typeof data === 'object' && data instanceof Blob)) {
                            emitErrorBeforeDataTransferFn('Provided data is not a Blob data');
                            return;
                          }
                      
                          transferInfo.name = data.name || transferId;
                          transferInfo.mimeType = data.type || null;
                      
                          if (data.size < 1) {
                            emitErrorBeforeDataTransferFn('Provided data is not a valid Blob data.');
                            return;
                          }
                      
                          transferInfo.originalSize = data.size;
                          transferInfo.size = sessionChunkType === 'string' ? 4 * Math.ceil(data.size / 3) : data.size;
                          chunks = self._chunkBlobData(data, chunkSize);
                        } else {
                          transferInfo.dataType = self.DATA_TRANSFER_SESSION_TYPE.DATA_URL;
                          transferInfo.chunkSize = self._CHUNK_DATAURL_SIZE;
                          transferInfo.chunkType = self.DATA_TRANSFER_DATA_TYPE.STRING;
                      
                          // Start checking if data transfer can start
                          if (!(data && typeof data === 'string')) {
                            emitErrorBeforeDataTransferFn('Provided data is not a dataURL');
                            return;
                          }
                      
                          transferInfo.originalSize = transferInfo.size = data.length || data.size;
                          chunks = self._chunkDataURL(data, transferInfo.chunkSize);
                        }
                      
                        if (!(self._user && self._user.sid)) {
                          emitErrorBeforeDataTransferFn('Unable to send any ' +
                            sessionType.replace('data', 'dataURL') + ' data. User is not in Room.');
                          return;
                        }
                      
                        if (!self._enableDataChannel) {
                          emitErrorBeforeDataTransferFn('Unable to send any ' +
                            sessionType.replace('data', 'dataURL') + ' data. Datachannel is disabled');
                          return;
                        }
                      
                        if (listOfPeers.length === 0) {
                          emitErrorBeforeDataTransferFn('Unable to send any ' +
                            sessionType.replace('data', 'dataURL') + ' data. There are no Peers to start data transfer with');
                          return;
                        }
                      
                        self._dataTransfers[transferId] = clone(transferInfo);
                        self._dataTransfers[transferId].peers = {};
                        self._dataTransfers[transferId].peers.main = {};
                        self._dataTransfers[transferId].peers[transferId] = {};
                        self._dataTransfers[transferId].sessions = {};
                        self._dataTransfers[transferId].chunks = chunks;
                        self._dataTransfers[transferId].enforceBSPeers = [];
                        self._dataTransfers[transferId].enforcedBSInfo = {};
                        self._dataTransfers[transferId].sessionType = sessionType;
                        self._dataTransfers[transferId].sessionChunkType = sessionChunkType;
                        self._dataTransfers[transferId].senderPeerId = self._user.sid;
                      
                        // Check if fallback chunks is required
                        if (sessionType === 'blob' && sessionChunkType === 'binary') {
                          for (var p = 0; p < listOfPeers.length; p++) {
                            var protocolVer = (((self._peerInformations[listOfPeers[p]]) || {}).agent || {}).DTProtocolVersion || '0.1.0';
                      
                            // C++ SDK does not support binary file transfer for now
                            if (self._isLowerThanVersion(protocolVer, '0.1.3')) {
                              self._dataTransfers[transferId].enforceBSPeers.push(listOfPeers[p]);
                            }
                          }
                      
                          if (self._dataTransfers[transferId].enforceBSPeers.length > 0) {
                            var bsChunkSize = window.webrtcDetectedBrowser === 'firefox' ? self._MOZ_CHUNK_FILE_SIZE : self._CHUNK_FILE_SIZE;
                            var bsChunks = self._chunkBlobData(new Blob(chunks), bsChunkSize);
                      
                            self._dataTransfers[transferId].enforceBSInfo = {
                              chunkSize: 4 * Math.ceil(bsChunkSize / 3),
                              chunkType: self.DATA_TRANSFER_DATA_TYPE.BINARY_STRING,
                              size: 4 * Math.ceil(transferInfo.originalSize / 3),
                              chunks: bsChunks
                            };
                          }
                        }
                      
                        /**
                         * Complete Peer function.
                         */
                        var completeFn = function (peerId, error) {
                          // Ignore if already added.
                          if (transferCompleted.indexOf(peerId) > -1) {
                            return;
                          }
                      
                          log.debug([peerId, 'RTCDataChannel', transferId, 'Data transfer result. Is errors present? ->'], error);
                      
                          transferCompleted.push(peerId);
                      
                          if (error) {
                            transferErrors[peerId] = new Error(error);
                          }
                      
                          if (listOfPeers.length === transferCompleted.length) {
                            log.log([null, 'RTCDataChannel', transferId, 'Data transfer request completed']);
                      
                            if (typeof callback === 'function') {
                              if (Object.keys(transferErrors).length > 0) {
                                callback({
                                  transferId: transferId,
                                  transferInfo: self._getTransferInfo(transferId, peerId, false, true, false),
                                  transferErrors: transferErrors,
                                  listOfPeers: listOfPeers
                                }, null);
                              } else {
                                callback(null, {
                                  transferId: transferId,
                                  transferInfo: self._getTransferInfo(transferId, peerId, false, true, false),
                                  listOfPeers: listOfPeers
                                });
                              }
                            }
                          }
                        };
                      
                        for (var i = 0; i < listOfPeers.length; i++) {
                          var MCUInteropStatus = self._startDataTransferToPeer(transferId, listOfPeers[i], completeFn, null, null);
                      
                          if (typeof MCUInteropStatus === 'boolean') {
                            if (MCUInteropStatus === true) {
                              self._dataTransfers[transferId].peers.main[listOfPeers[i]] = true;
                            } else {
                              self._dataTransfers[transferId].peers[transferId][listOfPeers[i]] = true;
                            }
                          }
                        }
                      
                        if (self._hasMCU) {
                          if (Object.keys(self._dataTransfers[transferId].peers.main).length > 0) {
                            self._startDataTransferToPeer(transferId, 'MCU', completeFn, 'main',
                              Object.keys(self._dataTransfers[transferId].peers.main));
                          }
                      
                          if (Object.keys(self._dataTransfers[transferId].peers[transferId]).length > 0) {
                            self._startDataTransferToPeer(transferId, 'MCU', completeFn, transferId,
                              Object.keys(self._dataTransfers[transferId].peers[transferId]));
                          }
                        }
                      };
                      
                      /**
                       * Function that starts or listens the data transfer status to Peer.
                       * This reacts differently during MCU environment.
                       * @method _startDataTransferToPeer
                       * @return {Boolean} Returns a Boolean only during MCU environment which flag indicates if Peer requires interop
                       *   (Use messaging Datachannel connection instead).
                       * @private
                       * @since 0.6.16
                       */
                      Skylink.prototype._startDataTransferToPeer = function (transferId, peerId, callback, channelProp, targetPeers) {
                        var self = this;
                      
                        var peerConnectionStateCbFn = null;
                        var dataChannelStateCbFn = null;
                      
                        /**
                         * Emit event for Peers function.
                         */
                        var emitEventFn = function (cb) {
                          var peers = targetPeers || [peerId];
                          for (var i = 0; i < peers.length; i++) {
                            cb(peers[i]);
                          }
                        };
                      
                        /**
                         * Return error and trigger them if failed before or during data transfers function.
                         */
                        var returnErrorBeforeTransferFn = function (error) {
                          // Replace if it is a MCU Peer errors for clear indication in error message
                          var updatedError = peerId === 'MCU' ? error.replace(/Peer/g, 'MCU Peer') : error;
                      
                          emitEventFn(function (evtPeerId) {
                            self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.ERROR, transferId, evtPeerId,
                              self._getTransferInfo(transferId, peerId, true, true, false), {
                              message: new Error(updatedError),
                              transferType: self.DATA_TRANSFER_TYPE.UPLOAD
                            });
                          });
                        };
                      
                        /**
                         * Send WRQ protocol to start data transfers.
                         */
                        var sendWRQFn = function () {
                          var size = self._dataTransfers[transferId].size;
                          var chunkSize = self._dataTransfers[transferId].chunkSize;
                          var chunkType = self._dataTransfers[transferId].sessionChunkType;
                      
                          if (self._dataTransfers[transferId].enforceBSPeers.indexOf(peerId) > -1) {
                            log.warn([peerId, 'RTCDataChannel', transferId,
                              'Binary data chunks transfer is not yet supported with Peer connecting from ' +
                              'Android, iOS and C++ SDK. Fallbacking to binary string data chunks transfer.']);
                      
                            size = self._dataTransfers[transferId].enforceBSInfo.size;
                            chunkSize = self._dataTransfers[transferId].enforceBSInfo.chunkSize;
                            chunkType = 'string';
                          }
                      
                          self._sendMessageToDataChannel(peerId, {
                            type: self._DC_PROTOCOL_TYPE.WRQ,
                            transferId: transferId,
                            name: self._dataTransfers[transferId].name,
                            size: size,
                            originalSize: self._dataTransfers[transferId].originalSize,
                            dataType: self._dataTransfers[transferId].sessionType,
                            mimeType: self._dataTransfers[transferId].mimeType,
                            chunkType: chunkType,
                            chunkSize: chunkSize,
                            timeout: self._dataTransfers[transferId].timeout,
                            isPrivate: self._dataTransfers[transferId].isPrivate,
                            sender: self._user.sid,
                            agent: window.webrtcDetectedBrowser,
                            version: window.webrtcDetectedVersion,
                            target: targetPeers ? targetPeers : peerId
                          }, channelProp);
                      
                          emitEventFn(function (evtPeerId) {
                            self._trigger('incomingDataRequest', transferId, evtPeerId,
                              self._getTransferInfo(transferId, peerId, false, false, false), true);
                      
                            self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.USER_UPLOAD_REQUEST, transferId, evtPeerId,
                              self._getTransferInfo(transferId, peerId, true, false, false), null);
                          });
                        };
                      
                        // Listen to data transfer state
                        if (peerId !== 'MCU') {
                          var dataTransferStateCbFn = function (state, evtTransferId, evtPeerId, transferInfo, error) {
                            if (peerConnectionStateCbFn) {
                              self.off('peerConnectionState', peerConnectionStateCbFn);
                            }
                      
                            if (dataChannelStateCbFn) {
                              self.off('dataChannelState', dataChannelStateCbFn);
                            }
                      
                            if (channelProp) {
                              delete self._dataTransfers[transferId].peers[channelProp][peerId];
                            }
                      
                            if (state === self.DATA_TRANSFER_STATE.UPLOAD_COMPLETED) {
                              callback(peerId, null);
                            } else {
                              callback(peerId, error.message.message || error.message.toString());
                            }
                      
                            // Handle Peer uploading to MCU case
                            if (self._hasMCU && self._dataTransfers[transferId].direction === self.DATA_TRANSFER_TYPE.UPLOAD) {
                              if (!(Object.keys(self._dataTransfers[transferId].peers.main).length === 0 &&
                                Object.keys(self._dataTransfers[transferId].peers[transferId]).length === 0)) {
                                return;
                              }
                      
                              delete self._dataTransfers[transferId];
                      
                            } else {
                              delete self._dataTransfers[transferId].sessions[peerId];
                      
                              if (Object.keys(self._dataTransfers[transferId].sessions).length === 0) {
                                delete self._dataTransfers[transferId];
                              }
                            }
                          };
                      
                          self.once('dataTransferState', dataTransferStateCbFn, function (state, evtTransferId, evtPeerId) {
                            if (!(self._dataTransfers[transferId] && (self._hasMCU ? (self._dataTransfers[transferId].peers.main[peerId] ||
                              self._dataTransfers[transferId].peers[transferId][peerId]) : self._dataTransfers[transferId].sessions[peerId]))) {
                              if (dataTransferStateCbFn) {
                                self.off('dataTransferState', dataTransferStateCbFn);
                              }
                              if (peerConnectionStateCbFn) {
                                self.off('peerConnectionState', peerConnectionStateCbFn);
                              }
                              if (dataChannelStateCbFn) {
                                self.off('dataChannelState', dataChannelStateCbFn);
                              }
                              return;
                            }
                            return evtTransferId === transferId && evtPeerId === peerId &&
                              [self.DATA_TRANSFER_STATE.UPLOAD_COMPLETED, self.DATA_TRANSFER_STATE.ERROR,
                              self.DATA_TRANSFER_STATE.CANCEL, self.DATA_TRANSFER_STATE.REJECTED].indexOf(state) > -1;
                          });
                        }
                      
                        // When Peer connection does not exists
                        if (!self._peerConnections[peerId]) {
                          returnErrorBeforeTransferFn('Unable to start data transfer as Peer connection does not exists.');
                          return;
                        }
                      
                        // When Peer session does not exists
                        if (!self._peerInformations[peerId]) {
                          returnErrorBeforeTransferFn('Unable to start data transfer as Peer connection does not exists.');
                          return;
                        }
                      
                        // When Peer connection is not STABLE
                        if (self._peerConnections[peerId].signalingState !== self.PEER_CONNECTION_STATE.STABLE) {
                          returnErrorBeforeTransferFn('Unable to start data transfer as Peer connection is not stable.');
                          return;
                        }
                      
                        if (!self._dataTransfers[transferId]) {
                          returnErrorBeforeTransferFn('Unable to start data transfer as data transfer session is not in order.');
                          return;
                        }
                      
                        if (!(self._dataChannels[peerId] && self._dataChannels[peerId].main)) {
                          returnErrorBeforeTransferFn('Unable to start data transfer as Peer Datachannel connection does not exists.');
                          return;
                        }
                      
                        if (self._dataChannels[peerId].main.channel.readyState !== self.DATA_CHANNEL_STATE.OPEN) {
                          returnErrorBeforeTransferFn('Unable to start data transfer as Peer Datachannel connection is not opened.');
                          return;
                        }
                      
                        var streamId = self._dataChannels[peerId].main.streamId;
                      
                        if (streamId && channelProp === 'main' && self._dataStreams[streamId] &&
                        // Check if session chunk streaming is string and sending is string for Peer
                          ((self._dataStreams[streamId].sessionChunkType === 'string' &&
                          (self._dataTransfers[transferId].sessionChunkType === 'string' ||
                          self._dataTransfers[transferId].enforceBSPeers.indexOf(peerId) > -1)) ||
                        // Check if session chunk streaming is binary and sending is binary for Peer
                          (self._dataStreams[streamId].sessionChunkType === 'binary' &&
                          self._dataStreams[streamId].sessionChunkType === 'binary' &&
                          self._dataTransfers[transferId].enforceBSPeers.indexOf(peerId) === -1))) {
                          returnErrorBeforeTransferFn('Unable to start data transfer as Peer Datachannel currently has an active ' +
                            self._dataStreams[streamId].sessionChunkType + ' data streaming session.');
                          return;
                        }
                      
                        var protocolVer = (self._peerInformations[peerId].agent || {}).DTProtocolVersion || '0.1.0';
                        var requireInterop = self._isLowerThanVersion(protocolVer, '0.1.2');
                      
                        // Prevent DATA_URL (or "string" dataType transfers) with Android / iOS / C++ SDKs
                        if (self._isLowerThanVersion(protocolVer, '0.1.2') && self._dataTransfers[transferId].sessionType === 'data' &&
                          self._dataTransfers[transferId].sessionChunkType === 'string') {
                          returnErrorBeforeTransferFn('Unable to start data transfer as Peer do not support DATA_URL type of data transfers');
                          return;
                        }
                      
                        // Listen to Peer connection state for MCU Peer
                        if (peerId !== 'MCU' && self._hasMCU) {
                          channelProp = requireInterop ? 'main' : transferId;
                      
                          peerConnectionStateCbFn = function () {
                            returnErrorBeforeTransferFn('Data transfer terminated as Peer connection is not stable.');
                          };
                      
                          self.once('peerConnectionState', peerConnectionStateCbFn, function (state, evtPeerId) {
                            if (!self._dataTransfers[transferId]) {
                              self.off('peerConnectionState', peerConnectionStateCbFn);
                              return;
                            }
                            return state !== self.PEER_CONNECTION_STATE.STABLE && evtPeerId === peerId;
                          });
                          return requireInterop;
                        }
                      
                        if (requireInterop || channelProp === 'main') {
                          // When MCU Datachannel connection has a transfer in-progress
                          if (self._dataChannels[peerId].main.transferId) {
                            returnErrorBeforeTransferFn('Unable to start data transfer as Peer Datachannel has a data transfer in-progress.');
                            return;
                          }
                        }
                      
                        self._dataTransfers[transferId].sessions[peerId] = {
                          timer: null,
                          ackN: 0
                        };
                      
                        dataChannelStateCbFn = function (state, evtPeerId, error) {
                          // Prevent from triggering in instances where the ackN === chunks.length
                          if (self._dataTransfers[transferId].sessions[peerId].ackN >= (self._dataTransfers[transferId].chunks.length - 1)) {
                            return;
                          }
                      
                          if (error) {
                            returnErrorBeforeTransferFn(error.message || error.toString());
                          } else {
                            returnErrorBeforeTransferFn('Data transfer terminated as Peer Datachannel connection closed abruptly.');
                          }
                        };
                      
                        self.once('dataChannelState', dataChannelStateCbFn, function (state, evtPeerId, error, channelName, channelType) {
                          if (!(self._dataTransfers[transferId] && self._dataTransfers[transferId].sessions[peerId])) {
                            self.off('dataChannelState', dataChannelStateCbFn);
                            return;
                          }
                      
                          if (evtPeerId === peerId && (channelType === self.DATA_CHANNEL_TYPE.DATA ? channelName === transferId : true)) {
                            if (state === self.DATA_CHANNEL_STATE.OPEN && channelType === self.DATA_CHANNEL_TYPE.DATA &&
                              channelName === transferId) {
                              self._dataChannels[peerId][channelProp].transferId = transferId;
                              sendWRQFn();
                              return false;
                            }
                            return [self.DATA_CHANNEL_STATE.CREATE_ERROR, self.DATA_CHANNEL_STATE.ERROR,
                              self.DATA_CHANNEL_STATE.CLOSING, self.DATA_CHANNEL_STATE.CLOSED].indexOf(state) > -1;
                          }
                        });
                      
                        // Create new Datachannel for Peer to start data transfer
                        if (!((requireInterop && peerId !== 'MCU') || channelProp === 'main')) {
                          channelProp = transferId;
                          self._createDataChannel(peerId, transferId, self._dataTransfers[transferId].sessionType === 'data' ?
                            self._CHUNK_DATAURL_SIZE : (self._dataTransfers[transferId].sessionChunkType === 'string' ?
                            (window.webrtcDetectedBrowser === 'firefox' ? 16384 : 65546) : // After conversion to base64 string computed size
                            (window.webrtcDetectedBrowser === 'firefox' ? self._MOZ_BINARY_FILE_SIZE : self._BINARY_FILE_SIZE)));
                        } else {
                          self._dataChannels[peerId].main.transferId = transferId;
                          sendWRQFn();
                        }
                      };
                      
                      /**
                       * Function that returns the data transfer session.
                       * @method _getTransferInfo
                       * @private
                       * @for Skylink
                       * @since 0.6.16
                       */
                      Skylink.prototype._getTransferInfo = function (transferId, peerId, returnDataProp, hidePercentage, returnDataAtStart) {
                        if (!this._dataTransfers[transferId]) {
                          return {};
                        }
                      
                        var transferInfo = {
                          name: this._dataTransfers[transferId].name,
                          size: this._dataTransfers[transferId].size,
                          dataType: this._dataTransfers[transferId].dataType || this.DATA_TRANSFER_SESSION_TYPE.BLOB,
                          mimeType: this._dataTransfers[transferId].mimeType || null,
                          chunkSize: this._dataTransfers[transferId].chunkSize,
                          chunkType: this._dataTransfers[transferId].chunkType,
                          timeout: this._dataTransfers[transferId].timeout,
                          isPrivate: this._dataTransfers[transferId].isPrivate,
                          direction: this._dataTransfers[transferId].direction
                        };
                      
                        if (this._dataTransfers[transferId].originalSize) {
                          transferInfo.size = this._dataTransfers[transferId].originalSize;
                      
                        } else if (this._dataTransfers[transferId].chunkType === this.DATA_TRANSFER_DATA_TYPE.BINARY_STRING) {
                          transferInfo.size = Math.ceil(transferInfo.size * 3 / 4);
                        }
                      
                        if (!hidePercentage) {
                          transferInfo.percentage = 0;
                      
                          if (!this._dataTransfers[transferId].sessions[peerId]) {
                            if (returnDataProp) {
                              transferInfo.data = null;
                            }
                            return transferInfo;
                          }
                      
                          if (this._dataTransfers[transferId].direction === this.DATA_TRANSFER_TYPE.DOWNLOAD) {
                            if (this._dataTransfers[transferId].sessions[peerId].receivedSize === this._dataTransfers[transferId].sessions[peerId].size) {
                              transferInfo.percentage = 100;
                      
                            } else {
                              transferInfo.percentage = parseFloat(((this._dataTransfers[transferId].sessions[peerId].receivedSize /
                                this._dataTransfers[transferId].size) * 100).toFixed(2), 10);
                            }
                          } else {
                            var chunksLength = (this._dataTransfers[transferId].enforceBSPeers.indexOf(peerId) > -1 ?
                              this._dataTransfers[transferId].enforceBSInfo.chunks.length : this._dataTransfers[transferId].chunks.length);
                      
                            if (this._dataTransfers[transferId].sessions[peerId].ackN === chunksLength) {
                              transferInfo.percentage = 100;
                      
                            } else {
                              transferInfo.percentage = parseFloat(((this._dataTransfers[transferId].sessions[peerId].ackN /
                                chunksLength) * 100).toFixed(2), 10);
                            }
                          }
                      
                          if (returnDataProp) {
                            if (typeof returnDataAtStart !== 'number') {
                              if (transferInfo.percentage === 100) {
                                transferInfo.data = this._getTransferData(transferId);
                              } else {
                                transferInfo.data = null;
                              }
                            } else {
                              transferInfo.percentage = returnDataAtStart;
                      
                              if (returnDataAtStart === 0) {
                                transferInfo.data = this._getTransferData(transferId);
                              }
                            }
                          }
                        }
                      
                        return transferInfo;
                      };
                      
                      /**
                       * Function that returns the compiled data transfer data.
                       * @method _getTransferData
                       * @private
                       * @for Skylink
                       * @since 0.6.16
                       */
                      Skylink.prototype._getTransferData = function (transferId) {
                        if (!this._dataTransfers[transferId]) {
                          return null;
                        }
                      
                        if (this._dataTransfers[transferId].dataType === this.DATA_TRANSFER_SESSION_TYPE.BLOB) {
                          var mimeType = {
                            name: this._dataTransfers[transferId].name
                          };
                      
                          if (this._dataTransfers[transferId].mimeType) {
                            mimeType.type = this._dataTransfers[transferId].mimeType;
                          }
                      
                          return new Blob(this._dataTransfers[transferId].chunks, mimeType);
                        }
                      
                        return this._dataTransfers[transferId].chunks.join('');
                      };
                      
                      /**
                       * Function that handles the data transfers sessions timeouts.
                       * @method _handleDataTransferTimeoutForPeer
                       * @private
                       * @for Skylink
                       * @since 0.6.16
                       */
                      Skylink.prototype._handleDataTransferTimeoutForPeer = function (transferId, peerId, setPeerTO) {
                        var self = this;
                      
                        if (!(self._dataTransfers[transferId] && self._dataTransfers[transferId].sessions[peerId])) {
                          log.debug([peerId, 'RTCDataChannel', transferId, 'Data transfer does not exists for Peer. Ignoring timeout.']);
                          return;
                        }
                      
                        log.debug([peerId, 'RTCDataChannel', transferId, 'Clearing data transfer timer for Peer.']);
                      
                        if (self._dataTransfers[transferId].sessions[peerId].timer) {
                          clearTimeout(self._dataTransfers[transferId].sessions[peerId].timer);
                        }
                      
                        self._dataTransfers[transferId].sessions[peerId].timer = null;
                      
                        if (setPeerTO) {
                          log.debug([peerId, 'RTCDataChannel', transferId, 'Setting data transfer timer for Peer.']);
                      
                          self._dataTransfers[transferId].sessions[peerId].timer = setTimeout(function () {
                            if (!(self._dataTransfers[transferId] && self._dataTransfers[transferId].sessions[peerId])) {
                              log.debug([peerId, 'RTCDataChannel', transferId, 'Data transfer already ended for Peer. Ignoring expired timeout.']);
                              return;
                            }
                      
                            if (!(self._user && self._user.sid)) {
                              log.debug([peerId, 'RTCDataChannel', transferId, 'User is not in Room. Ignoring expired timeout.']);
                              return;
                            }
                      
                            if (!self._dataChannels[peerId]) {
                              log.debug([peerId, 'RTCDataChannel', transferId, 'Datachannel connection does not exists. Ignoring expired timeout.']);
                              return;
                            }
                      
                            log.error([peerId, 'RTCDataChannel', transferId, 'Data transfer response has timed out.']);
                      
                            /**
                             * Emit event for Peers function.
                             */
                            var emitEventFn = function (cb) {
                              if (peerId === 'MCU') {
                                var broadcastedPeers = [self._dataTransfers[transferId].peers.main,
                                  self._dataTransfers[transferId].peers[transferId]];
                      
                                for (var i = 0; i < broadcastedPeers.length; i++) {
                                  // Should not happen but insanity check
                                  if (!broadcastedPeers[i]) {
                                    continue;
                                  }
                      
                                  for (var bcPeerId in broadcastedPeers[i]) {
                                    if (broadcastedPeers[i].hasOwnProperty(bcPeerId) && broadcastedPeers[i][bcPeerId]) {
                                      cb(bcPeerId);
                                    }
                                  }
                                }
                              } else {
                                cb(peerId);
                              }
                            };
                      
                            var errorMsg = 'Connection Timeout. Longer than ' + self._dataTransfers[transferId].timeout +
                              ' seconds. Connection is abolished.';
                      
                            self._sendMessageToDataChannel(peerId, {
                              type: self._DC_PROTOCOL_TYPE.ERROR,
                              content: errorMsg,
                              isUploadError: self._dataTransfers[transferId].direction === self.DATA_TRANSFER_TYPE.UPLOAD,
                              sender: self._user.sid,
                              name: self._dataTransfers[transferId].name
                            }, self._dataChannels[peerId][transferId] ? transferId : 'main');
                      
                            emitEventFn(function (evtPeerId) {
                              self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.ERROR, transferId, peerId,
                                self._getTransferInfo(transferId, peerId, true, false, false), {
                                transferType: self.DATA_TRANSFER_TYPE.DOWNLOAD,
                                message: new Error(errorMsg)
                              });
                            });
                          }, self._dataTransfers[transferId].timeout * 1000);
                        }
                      };
                      
                      /**
                       * Function that handles the data received from Datachannel and
                       * routes to the relevant data transfer protocol handler.
                       * @method _processDataChannelData
                       * @private
                       * @for Skylink
                       * @since 0.6.16
                       */
                      Skylink.prototype._processDataChannelData = function(rawData, peerId, channelName, channelType) {
                        var self = this;
                      
                        var channelProp = channelType === self.DATA_CHANNEL_TYPE.MESSAGING ? 'main' : channelName;
                        var transferId = self._dataChannels[peerId][channelProp].transferId || null;
                        var streamId = self._dataChannels[peerId][channelProp].streamId || null;
                        var isStreamChunk = false;
                      
                        if (streamId && self._dataStreams[streamId]) {
                          isStreamChunk = self._dataStreams[streamId].sessionChunkType === 'string' ? typeof rawData === 'string' :
                            typeof rawData === 'object';
                        }
                      
                        if (!self._peerConnections[peerId]) {
                          log.warn([peerId, 'RTCDataChannel', channelProp, 'Dropping data received from Peer ' +
                            'as connection is not present ->'], rawData);
                          return;
                        }
                      
                        if (!(self._dataChannels[peerId] && self._dataChannels[peerId][channelProp])) {
                          log.warn([peerId, 'RTCDataChannel', channelProp, 'Dropping data received from Peer ' +
                            'as Datachannel connection is not present ->'], rawData);
                          return;
                        }
                      
                        // Expect as string
                        if (typeof rawData === 'string') {
                          try {
                            var protocolData = JSON.parse(rawData);
                            isStreamChunk = false;
                      
                            log.debug([peerId, 'RTCDataChannel', channelProp, 'Received protocol "' + protocolData.type + '" message ->'], protocolData);
                      
                            // Ignore ACK, ERROR and CANCEL if there is no data transfer session in-progress
                            if ([self._DC_PROTOCOL_TYPE.ACK, self._DC_PROTOCOL_TYPE.ERROR, self._DC_PROTOCOL_TYPE.CANCEL].indexOf(protocolData.type) > -1 &&
                              !(transferId && self._dataTransfers[transferId] && self._dataTransfers[transferId].sessions[peerId])) {
                                log.warn([peerId, 'RTCDataChannel', channelProp, 'Discarded protocol message as data transfer session ' +
                                  'is not present ->'], protocolData);
                                return;
                            }
                      
                            switch (protocolData.type) {
                              case self._DC_PROTOCOL_TYPE.WRQ:
                                // Discard iOS bidirectional upload when Datachannel is in-progress for data transfers
                                if (transferId && self._dataTransfers[transferId] && self._dataTransfers[transferId].sessions[peerId]) {
                                  log.warn([peerId, 'RTCDataChannel', channelProp, 'Rejecting bidirectional data transfer request as ' +
                                    'it is currently not supported in the SDK ->'], protocolData);
                      
                                  self._sendMessageToDataChannel(peerId, {
                                    type: self._DC_PROTOCOL_TYPE.ACK,
                                    ackN: -1,
                                    sender: self._user.sid
                                  }, channelProp);
                                  return;
                                }
                                self._WRQProtocolHandler(peerId, protocolData, channelProp);
                                break;
                              case self._DC_PROTOCOL_TYPE.ACK:
                                self._ACKProtocolHandler(peerId, protocolData, channelProp);
                                break;
                              case self._DC_PROTOCOL_TYPE.ERROR:
                                self._ERRORProtocolHandler(peerId, protocolData, channelProp);
                                break;
                              case self._DC_PROTOCOL_TYPE.CANCEL:
                                self._CANCELProtocolHandler(peerId, protocolData, channelProp);
                                break;
                              case self._DC_PROTOCOL_TYPE.MESSAGE:
                                self._MESSAGEProtocolHandler(peerId, protocolData, channelProp);
                                break;
                              default:
                                log.warn([peerId, 'RTCDataChannel', channelProp, 'Discarded unknown "' + protocolData.type + '" message ->'], protocolData);
                            }
                      
                          } catch (error) {
                            if (rawData.indexOf('{') > -1 && rawData.indexOf('}') > 0) {
                              log.error([peerId, 'RTCDataChannel', channelProp, 'Failed parsing protocol step data error ->'], {
                                data: rawData,
                                error: error
                              });
                      
                              self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.ERROR, peerId, error, channelName,
                                channelType, null, self._getDataChannelBuffer(peerId, channelProp));
                              throw error;
                            }
                      
                            if (!isStreamChunk && !(transferId && self._dataTransfers[transferId] && self._dataTransfers[transferId].sessions[peerId])) {
                              log.warn([peerId, 'RTCDataChannel', channelProp, 'Discarded data chunk without session ->'], rawData);
                              return;
                            }
                      
                            if (!isStreamChunk && transferId) {
                              if (self._dataTransfers[transferId].chunks[self._dataTransfers[transferId].sessions[peerId].ackN]) {
                                log.warn([peerId, 'RTCDataChannel', transferId, 'Dropping data chunk ' + (!isStreamChunk ? '@' +
                                  self._dataTransfers[transferId].sessions[peerId].ackN : '') + ' as it has already been added ->'], rawData);
                                return;
                              }
                            }
                      
                            var chunkType = self.DATA_TRANSFER_DATA_TYPE.BINARY_STRING;
                      
                            if (!isStreamChunk ? self._dataTransfers[transferId].dataType === self.DATA_TRANSFER_SESSION_TYPE.DATA_URL : true) {
                              log.debug([peerId, 'RTCDataChannel', channelProp, 'Received string data chunk ' + (!isStreamChunk ? '@' +
                                self._dataTransfers[transferId].sessions[peerId].ackN : '') + ' with size ->'], rawData.length || rawData.size);
                      
                              self._DATAProtocolHandler(peerId, rawData, self.DATA_TRANSFER_DATA_TYPE.STRING,
                                rawData.length || rawData.size || 0, channelProp);
                      
                            } else {
                              var removeSpaceData = rawData.replace(/\s|\r|\n/g, '');
                      
                              log.debug([peerId, 'RTCDataChannel', channelProp, 'Received binary string data chunk @' +
                                self._dataTransfers[transferId].sessions[peerId].ackN + ' with size ->'],
                                removeSpaceData.length || removeSpaceData.size);
                      
                              self._DATAProtocolHandler(peerId, self._base64ToBlob(removeSpaceData), self.DATA_TRANSFER_DATA_TYPE.BINARY_STRING,
                                removeSpaceData.length || removeSpaceData.size || 0, channelProp);
                            }
                          }
                        } else {
                          if (!isStreamChunk && !(transferId && self._dataTransfers[transferId] && self._dataTransfers[transferId].sessions[peerId])) {
                            log.warn([peerId, 'RTCDataChannel', channelProp, 'Discarded data chunk without session ->'], rawData);
                            return;
                          }
                      
                          if (!isStreamChunk && transferId) {
                            if (self._dataTransfers[transferId].chunks[self._dataTransfers[transferId].sessions[peerId].ackN]) {
                              log.warn([peerId, 'RTCDataChannel', transferId, 'Dropping data chunk ' + (!isStreamChunk ? '@' +
                                self._dataTransfers[transferId].sessions[peerId].ackN : '') + ' as it has already been added ->'], rawData);
                              return;
                            }
                          }
                      
                          if (rawData instanceof Blob) {
                            log.debug([peerId, 'RTCDataChannel', channelProp, 'Received blob data chunk ' + (isStreamChunk ? '' :
                              '@' + self._dataTransfers[transferId].sessions[peerId].ackN) + ' with size ->'], rawData.size);
                      
                            self._DATAProtocolHandler(peerId, rawData, self.DATA_TRANSFER_DATA_TYPE.BLOB, rawData.size, channelProp);
                      
                          } else {
                            var byteArray = rawData;
                            var blob = null;
                      
                            // Plugin binary handling
                            if (rawData.constructor && rawData.constructor.name === 'Array') {
                              // Need to re-parse on some browsers
                              byteArray = new Int8Array(rawData);
                            }
                      
                            // Fallback for older IE versions
                            if (window.webrtcDetectedBrowser === 'IE') {
                              if (window.BlobBuilder) {
                                var bb = new BlobBuilder();
                                bb.append(rawData.constructor && rawData.constructor.name === 'ArrayBuffer' ?
                                  byteArray : (new Uint8Array(byteArray)).buffer);
                                blob = bb.getBlob();
                              }
                            } else {
                              blob = new Blob([byteArray]);
                            }
                      
                            log.debug([peerId, 'RTCDataChannel', channelProp, 'Received arraybuffer data chunk ' + (isStreamChunk ? '' :
                              '@' + self._dataTransfers[transferId].sessions[peerId].ackN) + ' with size ->'], blob.size);
                      
                            self._DATAProtocolHandler(peerId, blob, self.DATA_TRANSFER_DATA_TYPE.ARRAY_BUFFER, blob.size, channelProp);
                          }
                        }
                      };
                      
                      /**
                       * Function that handles the "WRQ" data transfer protocol.
                       * @method _WRQProtocolHandler
                       * @private
                       * @for Skylink
                       * @since 0.5.2
                       */
                      Skylink.prototype._WRQProtocolHandler = function(peerId, data, channelProp) {
                        var self = this;
                        var transferId = channelProp === 'main' ? data.transferId || null : channelProp;
                        var senderPeerId = data.sender || peerId;
                      
                        if (['fastBinaryStart', 'fastBinaryStop'].indexOf(data.dataType) > -1) {
                          if (data.dataType === 'fastBinaryStart') {
                            if (!transferId) {
                              transferId = 'stream_' + peerId + '_' + (new Date()).getTime();
                            }
                            self._dataStreams[transferId] = {
                              chunkSize: 0,
                              chunkType: data.chunkType === 'string' ? self.DATA_TRANSFER_DATA_TYPE.STRING : self._binaryChunkType,
                              sessionChunkType: data.chunkType,
                              isPrivate: !!data.isPrivate,
                              isStringStream: data.chunkType === 'string',
                              senderPeerId: senderPeerId,
                              isUpload: false
                            };
                            self._dataChannels[peerId][channelProp].streamId = transferId;
                            var hasStarted = false;
                            self.once('dataChannelState', function () {}, function (state, evtPeerId, channelName, channelType, error) {
                              if (!self._dataStreams[transferId]) {
                                return true;
                              }
                      
                              if (!(evtPeerId === peerId && (channelProp === 'main' ? channelType === self.DATA_CHANNEL_TYPE.MESSAGING :
                                channelName === transferId && channelType === self.DATA_CHANNEL_TYPE.DATA))) {
                                return;
                              }
                      
                              if ([self.DATA_CHANNEL_STATE.ERROR, self.DATA_CHANNEL_STATE.CLOSED].indexOf(state) > -1) {
                                var updatedError = new Error(error && error.message ? error.message :
                                  'Failed data transfer as datachannel state is "' + state + '".');
                      
                                self._trigger('dataStreamState', self.DATA_STREAM_STATE.ERROR, transferId, senderPeerId, {
                                  chunk: null,
                                  chunkSize: 0,
                                  chunkType: self._dataStreams[transferId].chunkType,
                                  isPrivate: self._dataStreams[transferId].isPrivate,
                                  isStringStream: self._dataStreams[transferId].sessionChunkType === 'string',
                                  senderPeerId: senderPeerId
                                }, updatedError);
                                return true;
                              }
                            });
                      
                            self._trigger('dataStreamState', self.DATA_STREAM_STATE.RECEIVING_STARTED, transferId, senderPeerId, {
                              chunk: null,
                              chunkSize: 0,
                              chunkType: self._dataStreams[transferId].chunkType,
                              isPrivate: self._dataStreams[transferId].isPrivate,
                              isStringStream: self._dataStreams[transferId].sessionChunkType === 'string',
                              senderPeerId: senderPeerId
                            }, null);
                            self._trigger('incomingDataStreamStarted', transferId, senderPeerId, {
                              chunkSize: 0,
                              chunkType: self._dataStreams[transferId].chunkType,
                              isPrivate: self._dataStreams[transferId].isPrivate,
                              isStringStream: self._dataStreams[transferId].sessionChunkType === 'string',
                              senderPeerId: senderPeerId
                            }, false);
                      
                          } else {
                            transferId = self._dataChannels[peerId][channelProp].streamId;
                            if (self._dataStreams[transferId] && !self._dataStreams[transferId].isUpload) {
                              self._trigger('dataStreamState', self.DATA_STREAM_STATE.RECEIVING_STOPPED, transferId, senderPeerId, {
                                chunk: null,
                                chunkSize: 0,
                                chunkType: self._dataStreams[transferId].chunkType,
                                isPrivate: self._dataStreams[transferId].isPrivate,
                                isStringStream: self._dataStreams[transferId].sessionChunkType === 'string',
                                senderPeerId: senderPeerId
                              }, null);
                              self._trigger('incomingDataStreamStopped', transferId, senderPeerId, {
                                chunkSize: 0,
                                chunkType: self._dataStreams[transferId].chunkType,
                                isPrivate: self._dataStreams[transferId].isPrivate,
                                isStringStream: self._dataStreams[transferId].sessionChunkType === 'string',
                                senderPeerId: senderPeerId
                              }, false);
                              self._dataChannels[peerId][channelProp].streamId = null;
                              if (channelProp !== 'main') {
                                self._closeDataChannel(peerId, channelProp);
                              }
                      
                              delete self._dataStreams[transferId];
                            }
                          }
                        } else {
                          if (!transferId) {
                            transferId = 'transfer_' + peerId + '_' + (new Date()).getTime();
                          }
                      
                          self._dataTransfers[transferId] = {
                            name: data.name || transferId,
                            size: data.size || 0,
                            chunkSize: data.chunkSize,
                            originalSize: data.originalSize || 0,
                            timeout: data.timeout || 60,
                            isPrivate: !!data.isPrivate,
                            senderPeerId: data.sender || peerId,
                            dataType: self.DATA_TRANSFER_SESSION_TYPE.BLOB,
                            mimeType: data.mimeType || null,
                            chunkType: self.DATA_TRANSFER_DATA_TYPE.BINARY_STRING,
                            direction: self.DATA_TRANSFER_TYPE.DOWNLOAD,
                            chunks: [],
                            sessions: {},
                            sessionType: data.dataType || 'blob',
                            sessionChunkType: data.chunkType || 'string'
                          };
                      
                          if (self._dataTransfers[transferId].sessionType === 'data' &&
                            self._dataTransfers[transferId].sessionChunkType === 'string') {
                            self._dataTransfers[transferId].dataType = self.DATA_TRANSFER_SESSION_TYPE.DATA_URL;
                            self._dataTransfers[transferId].chunkType = self.DATA_TRANSFER_DATA_TYPE.STRING;
                          } else if (self._dataTransfers[transferId].sessionType === 'blob' &&
                            self._dataTransfers[transferId].sessionChunkType === 'binary') {
                            self._dataTransfers[transferId].chunkType = self._binaryChunkType;
                          }
                      
                          self._dataChannels[peerId][channelProp].transferId = transferId;
                          self._dataTransfers[transferId].sessions[peerId] = {
                            timer: null,
                            ackN: 0,
                            receivedSize: 0
                          };
                      
                          self._trigger('incomingDataRequest', transferId, senderPeerId,
                            self._getTransferInfo(transferId, peerId, false, false, false), false);
                      
                          self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.UPLOAD_REQUEST, transferId, senderPeerId,
                            self._getTransferInfo(transferId, peerId, true, false, false), null);
                        }
                      };
                      
                      /**
                       * Function that handles the "ACK" data transfer protocol.
                       * @method _ACKProtocolHandler
                       * @private
                       * @for Skylink
                       * @since 0.5.2
                       */
                      Skylink.prototype._ACKProtocolHandler = function(peerId, data, channelProp) {
                        var self = this;
                      
                        var transferId = channelProp;
                        var senderPeerId = data.sender || peerId;
                      
                        if (channelProp === 'main') {
                          transferId = self._dataChannels[peerId].main.transferId;
                        }
                      
                        self._handleDataTransferTimeoutForPeer(transferId, peerId, false);
                      
                        /**
                         * Emit event for Peers function.
                         */
                        var emitEventFn = function (cb) {
                          if (peerId === 'MCU') {
                            if (!self._dataTransfers[transferId].peers[channelProp]) {
                              log.warn([peerId, 'RTCDataChannel', channelProp, 'Dropping triggering of ACK event as ' +
                                'Peers list does not exists']);
                              return;
                            }
                            for (var evtPeerId in self._dataTransfers[transferId].peers[channelProp]) {
                              if (self._dataTransfers[transferId].peers[channelProp].hasOwnProperty(evtPeerId) &&
                                self._dataTransfers[transferId].peers[channelProp][evtPeerId]) {
                                cb(evtPeerId);
                              }
                            }
                          } else {
                            cb(senderPeerId);
                          }
                        };
                      
                        if (data.ackN > -1) {
                          if (data.ackN === 0) {
                            emitEventFn(function (evtPeerId) {
                              self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.UPLOAD_STARTED, transferId, evtPeerId,
                                self._getTransferInfo(transferId, peerId, true, false, 0), null);
                            });
                          } else if (self._dataTransfers[transferId].enforceBSPeers.indexOf(peerId) > -1 ?
                            data.ackN === self._dataTransfers[transferId].enforceBSInfo.chunks.length :
                            data.ackN === self._dataTransfers[transferId].chunks.length) {
                            self._dataTransfers[transferId].sessions[peerId].ackN = data.ackN;
                      
                            emitEventFn(function (evtPeerId) {
                              self._trigger('incomingData', self._getTransferData(transferId), transferId, evtPeerId,
                                self._getTransferInfo(transferId, peerId, false, false, false), true);
                      
                              self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.UPLOAD_COMPLETED, transferId, evtPeerId,
                                self._getTransferInfo(transferId, peerId, true, false, 100), null);
                            });
                      
                            if (self._dataChannels[peerId][channelProp]) {
                              self._dataChannels[peerId][channelProp].transferId = null;
                      
                              if (channelProp !== 'main') {
                                self._closeDataChannel(peerId, channelProp);
                              }
                            }
                            return;
                          }
                      
                          var uploadFn = function (chunk) {
                            self._sendMessageToDataChannel(peerId, chunk, channelProp, true);
                      
                            if (data.ackN < self._dataTransfers[transferId].chunks.length) {
                              emitEventFn(function (evtPeerId) {
                                self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.UPLOADING, transferId, evtPeerId,
                                  self._getTransferInfo(transferId, peerId, true, false, false), null);
                              });
                            }
                      
                            self._handleDataTransferTimeoutForPeer(transferId, peerId, true);
                          };
                      
                          self._dataTransfers[transferId].sessions[peerId].ackN = data.ackN;
                      
                          if (self._dataTransfers[transferId].enforceBSPeers.indexOf(peerId) > -1) {
                            self._blobToBase64(self._dataTransfers[transferId].enforceBSInfo.chunks[data.ackN], uploadFn);
                          } else if (self._dataTransfers[transferId].chunkType === self.DATA_TRANSFER_DATA_TYPE.BINARY_STRING) {
                            self._blobToBase64(self._dataTransfers[transferId].chunks[data.ackN], uploadFn);
                          } else if (self._dataTransfers[transferId].chunkType === self.DATA_TRANSFER_DATA_TYPE.ARRAY_BUFFER) {
                            self._blobToArrayBuffer(self._dataTransfers[transferId].chunks[data.ackN], uploadFn);
                          } else {
                            uploadFn(self._dataTransfers[transferId].chunks[data.ackN]);
                          }
                        } else {
                          self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.REJECTED, transferId, senderPeerId,
                            self._getTransferInfo(transferId, peerId, true, false, false), {
                            message: new Error('Data transfer terminated as Peer has rejected data transfer request'),
                            transferType: self.DATA_TRANSFER_TYPE.UPLOAD
                          });
                        }
                      };
                      
                      /**
                       * Function that handles the "MESSAGE" data transfer protocol.
                       * @method _MESSAGEProtocolHandler
                       * @private
                       * @for Skylink
                       * @since 0.5.2
                       */
                      Skylink.prototype._MESSAGEProtocolHandler = function(peerId, data, channelProp) {
                        var senderPeerId = data.sender || peerId;
                      
                        log.log([senderPeerId, 'RTCDataChannel', channelProp, 'Received P2P message from peer:'], data);
                      
                        this._trigger('incomingMessage', {
                          content: data.data,
                          isPrivate: data.isPrivate,
                          isDataChannel: true,
                          targetPeerId: this._user.sid,
                          senderPeerId: senderPeerId
                        }, senderPeerId, this.getPeerInfo(senderPeerId), false);
                      };
                      
                      /**
                       * Function that handles the "ERROR" data transfer protocol.
                       * @method _ERRORProtocolHandler
                       * @private
                       * @for Skylink
                       * @since 0.5.2
                       */
                      Skylink.prototype._ERRORProtocolHandler = function(peerId, data, channelProp) {
                        var self = this;
                      
                        var transferId = channelProp;
                        var senderPeerId = data.sender || peerId;
                      
                        if (channelProp === 'main') {
                          transferId = self._dataChannels[peerId].main.transferId;
                        }
                      
                        self._handleDataTransferTimeoutForPeer(transferId, peerId, false);
                      
                        /**
                         * Emit event for Peers function.
                         */
                        var emitEventFn = function (cb) {
                          if (peerId === 'MCU') {
                            if (!self._dataTransfers[transferId].peers[channelProp]) {
                              log.warn([peerId, 'RTCDataChannel', channelProp, 'Dropping triggering of ERROR event as ' +
                                'Peers list does not exists']);
                              return;
                            }
                            for (var evtPeerId in self._dataTransfers[transferId].peers[channelProp]) {
                              if (self._dataTransfers[transferId].peers[channelProp].hasOwnProperty(evtPeerId) &&
                                self._dataTransfers[transferId].peers[channelProp][evtPeerId]) {
                                cb(evtPeerId);
                              }
                            }
                          } else {
                            cb(senderPeerId);
                          }
                        };
                      
                        log.error([peerId, 'RTCDataChannel', channelProp, 'Received an error from peer ->'], data);
                      
                        emitEventFn(function (evtPeerId) {
                          self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.ERROR, transferId, evtPeerId,
                            self._getTransferInfo(transferId, peerID, true, false, false), {
                            message: new Error(data.content),
                            transferType: self._dataTransfers[transferId].direction
                          });
                        });
                      };
                      
                      /**
                       * Function that handles the "CANCEL" data transfer protocol.
                       * @method _CANCELProtocolHandler
                       * @private
                       * @for Skylink
                       * @since 0.5.0
                       */
                      Skylink.prototype._CANCELProtocolHandler = function(peerId, data, channelProp) {
                        var self = this;
                        var transferId = channelProp;
                      
                        if (channelProp === 'main') {
                          transferId = self._dataChannels[peerId].main.transferId;
                        }
                      
                        self._handleDataTransferTimeoutForPeer(transferId, peerId, false);
                      
                        /**
                         * Emit event for Peers function.
                         */
                        var emitEventFn = function (cb) {
                          if (peerId === 'MCU') {
                            if (!self._dataTransfers[transferId].peers[channelProp]) {
                              log.warn([peerId, 'RTCDataChannel', channelProp, 'Dropping triggering of CANCEL event as ' +
                                'Peers list does not exists']);
                              return;
                            }
                            for (var evtPeerId in self._dataTransfers[transferId].peers[channelProp]) {
                              if (self._dataTransfers[transferId].peers[channelProp].hasOwnProperty(evtPeerId) &&
                                self._dataTransfers[transferId].peers[channelProp][evtPeerId]) {
                                cb(evtPeerId);
                              }
                            }
                          } else {
                            cb(peerId);
                          }
                        };
                      
                        log.error([peerId, 'RTCDataChannel', channelProp, 'Received data transfer termination from peer ->'], data);
                      
                        emitEventFn(function (evtPeerId) {
                          self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.CANCEL, transferId, evtPeerId,
                            self._getTransferInfo(transferId, peerId, true, false, false), {
                            message: new Error(data.content || 'Peer has terminated data transfer.'),
                            transferType: self._dataTransfers[transferId].direction
                          });
                        });
                      };
                      
                      /**
                       * Function that handles the data transfer chunk received.
                       * @method _DATAProtocolHandler
                       * @private
                       * @for Skylink
                       * @since 0.5.5
                       */
                      Skylink.prototype._DATAProtocolHandler = function(peerId, chunk, chunkType, chunkSize, channelProp) {
                        var self = this;
                        var transferId = channelProp;
                        var senderPeerId = peerId;
                      
                        if (!(self._dataChannels[peerId] && self._dataChannels[peerId][channelProp])) {
                          return;
                        }
                      
                        var streamId = self._dataChannels[peerId][channelProp].streamId;
                      
                        if (streamId && self._dataStreams[streamId] && ((typeof chunk === 'string' &&
                          self._dataStreams[streamId].sessionChunkType === 'string') || (chunk instanceof Blob &&
                          self._dataStreams[streamId].sessionChunkType === 'binary'))) {
                          senderPeerId = self._dataStreams[streamId].senderPeerId || peerId;
                          self._trigger('dataStreamState', self.DATA_STREAM_STATE.RECEIVED, streamId, senderPeerId, {
                            chunk: chunk,
                            chunkSize: chunkSize,
                            chunkType: chunkType,
                            isPrivate: self._dataStreams[streamId].sessionChunkType.isPrivate,
                            isStringStream: self._dataStreams[streamId].sessionChunkType === 'string',
                            senderPeerId: senderPeerId
                          }, null);
                          self._trigger('incomingDataStream', chunk, transferId, senderPeerId, {
                            chunkSize: chunkSize,
                            chunkType: chunkType,
                            isPrivate: self._dataStreams[streamId].sessionChunkType.isPrivate,
                            isStringStream: self._dataStreams[streamId].sessionChunkType === 'string',
                            senderPeerId: senderPeerId
                          }, false);
                          return;
                        }
                      
                        if (channelProp === 'main') {
                          transferId = self._dataChannels[peerId].main.transferId;
                        }
                      
                        if (self._dataTransfers[transferId].senderPeerId) {
                          senderPeerId = self._dataTransfers[transferId].senderPeerId;
                        }
                      
                        self._handleDataTransferTimeoutForPeer(transferId, peerId, false);
                      
                        self._dataTransfers[transferId].chunkType = chunkType;
                        self._dataTransfers[transferId].sessions[peerId].receivedSize += chunkSize;
                        self._dataTransfers[transferId].chunks[self._dataTransfers[transferId].sessions[peerId].ackN] = chunk;
                      
                        if (self._dataTransfers[transferId].sessions[peerId].receivedSize >= self._dataTransfers[transferId].size) {
                          log.log([peerId, 'RTCDataChannel', channelProp, 'Data transfer has been completed. Computed size ->'],
                            self._dataTransfers[transferId].sessions[peerId].receivedSize);
                      
                          // Send last ACK to Peer to indicate completion of data transfers
                          self._sendMessageToDataChannel(peerId, {
                            type: self._DC_PROTOCOL_TYPE.ACK,
                            sender: self._user.sid,
                            ackN: self._dataTransfers[transferId].sessions[peerId].ackN + 1
                          }, channelProp);
                      
                          self._trigger('incomingData', self._getTransferData(transferId), transferId, senderPeerId,
                            self._getTransferInfo(transferId, peerId, false, false, false), null);
                      
                          self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.DOWNLOAD_COMPLETED, transferId, senderPeerId,
                            self._getTransferInfo(transferId, peerId, true, false, false), null);
                          return;
                        }
                      
                        self._dataTransfers[transferId].sessions[peerId].ackN += 1;
                      
                        self._sendMessageToDataChannel(peerId, {
                          type: self._DC_PROTOCOL_TYPE.ACK,
                          sender: self._user.sid,
                          ackN: self._dataTransfers[transferId].sessions[peerId].ackN
                        }, channelProp);
                      
                        self._handleDataTransferTimeoutForPeer(transferId, peerId, true);
                      
                        self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.DOWNLOADING, transferId, senderPeerId,
                          self._getTransferInfo(transferId, peerId, true, false, false), null);
                      };