/**
* <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
* <body>
* <input type="radio" name="timeout" onchange="setTransferTimeout(0)"> 1s timeout (Default)
* <input type="radio" name="timeout" onchange="setTransferTimeout(120)"> 2s timeout
* <input type="radio" name="timeout" onchange="setTransferTimeout(300)"> 5s timeout
* <hr>
* <input type="file" onchange="uploadFile(this.files[0], this.getAttribute('data'))" data="peerId">
* <input type="file" onchange="uploadFileGroup(this.files[0], this.getAttribute('data').split(',')))" data="peerIdA,peerIdB">
* <input type="file" onchange="uploadFileAll(this.files[0])" data="">
* <script>
* 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);
* }
* }
* </script>
* </body>
* @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
* <body>
* <input type="radio" name="timeout" onchange="setTransferTimeout(0)"> 1s timeout (Default)
* <input type="radio" name="timeout" onchange="setTransferTimeout(120)"> 2s timeout
* <input type="radio" name="timeout" onchange="setTransferTimeout(300)"> 5s timeout
* <hr>
* <input type="file" onchange="showImage(this.files[0], this.getAttribute('data'))" data="peerId">
* <input type="file" onchange="showImageGroup(this.files[0], this.getAttribute('data').split(',')))" data="peerIdA,peerIdB">
* <input type="file" onchange="showImageAll(this.files[0])" data="">
* <image id="target-1" src="">
* <image id="target-2" src="">
* <image id="target-3" src="">
* <script>
* 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;
* });
* }
* </script>
* </body>
* @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 and the
* <code>enableSimultaneousTransfers</code> flag has to be enabled in the <a href="#method_init">
* <code>init()</code> method</a> in order for this functionality to work.<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.');
}
if (!self._enableSimultaneousTransfers) {
return emitErrorBeforeStreamingFn('Unable to start data streaming as this feature requires simultaneous data transfers to be enabled');
}
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') || !self._enableSimultaneousTransfers ? '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: AdapterJS.webrtcDetectedBrowser,
version: AdapterJS.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 :
(AdapterJS.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 (AdapterJS.webrtcDetectedType === 'plugin' && 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: AdapterJS.webrtcDetectedBrowser,
version: AdapterJS.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' ? (AdapterJS.webrtcDetectedBrowser === 'firefox' ?
self._MOZ_CHUNK_FILE_SIZE : self._CHUNK_FILE_SIZE) : (AdapterJS.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 = AdapterJS.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: AdapterJS.webrtcDetectedBrowser,
version: AdapterJS.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') || !self._enableSimultaneousTransfers;
// 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' ?
(AdapterJS.webrtcDetectedBrowser === 'firefox' ? 16384 : 65546) : // After conversion to base64 string computed size
(AdapterJS.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 (AdapterJS.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);
};