/**
* <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.0';
/**
* 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>.</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} 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'
};
/**
* Stores the fixed delimiter that concats the Datachannel label and actual transfer ID.
* @attribute _TRANSFER_DELIMITER
* @type String
* @readOnly
* @private
* @for Skylink
* @since 0.5.10
*/
Skylink.prototype._TRANSFER_DELIMITER = '_skylink__';
/**
* 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'
};
/**
* Stores the list of types of SDKs that do not support simultaneous data transfers.
* @attribute _INTEROP_MULTI_TRANSFERS
* @type Array
* @readOnly
* @private
* @for Skylink
* @since 0.6.1
*/
Skylink.prototype._INTEROP_MULTI_TRANSFERS = ['Android', 'iOS'];
/**
* Stores the list of uploading data transfers chunks to Peers.
* @attribute _uploadDataTransfers
* @param {Array} <#transferId> The uploading data transfer chunks.
* @type JSON
* @private
* @for Skylink
* @since 0.4.1
*/
Skylink.prototype._uploadDataTransfers = {};
/**
* Stores the list of uploading data transfer sessions to Peers.
* @attribute _uploadDataSessions
* @param {JSON} <#transferId> The uploading data transfer session.
* @type JSON
* @private
* @for Skylink
* @since 0.4.1
*/
Skylink.prototype._uploadDataSessions = {};
/**
* Stores the list of downloading data transfers chunks to Peers.
* @attribute _downloadDataTransfers
* @param {Array} <#transferId> The downloading data transfer chunks.
* @type JSON
* @private
* @for Skylink
* @since 0.4.1
*/
Skylink.prototype._downloadDataTransfers = {};
/**
* Stores the list of downloading data transfer sessions to Peers.
* @attribute _downloadDataSessions
* @param {JSON} <#transferId> The downloading data transfer session.
* @type JSON
* @private
* @for Skylink
* @since 0.4.1
*/
Skylink.prototype._downloadDataSessions = {};
/**
* Stores the list of data transfer "wait-for-response" timeouts.
* @attribute _dataTransfersTimeout
* @param {Object} <#transferId> The data transfer session "wait-for-response" timeout.
* @type JSON
* @private
* @for Skylink
* @since 0.4.1
*/
Skylink.prototype._dataTransfersTimeout = {};
/**
* <blockquote class="info">
* Note that Android and iOS SDKs do not support simultaneous data transfers.
* </blockquote>
* 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 {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 <blockquote class="info">
* <b>Deprecation Warning!</b> This property has been deprecated.
* Please use <code>callback.error.transferInfo.transferId</code> instead.
* </blockquote> The data transfer ID.
* <small>Defined only for single targeted Peer data transfer.</small>
* @param {String} callback.error.state <blockquote class="info">
* <b>Deprecation Warning!</b> This property has been deprecated.
* Please use <a href="#event_dataTransferState"><code>dataTransferState</code>
* event</a> instead. </blockquote> The data transfer state that resulted in error.
* <small>Defined only for single targeted Peer data transfer.</small> [Rel: Skylink.DATA_TRANSFER_STATE]
* @param {String} callback.error.peerId <blockquote class="info">
* <b>Deprecation Warning!</b> This property has been deprecated.
* Please use <code>callback.error.listOfPeers</code> instead.
* </blockquote> The targeted Peer ID for data transfer.
* <small>Defined only for single targeted Peer data transfer.</small>
* @param {Boolean} callback.error.isPrivate <blockquote class="info">
* <b>Deprecation Warning!</b> This property has been deprecated.
* Please use <code>callback.error.transferInfo.isPrivate</code> instead.
* </blockquote> The flag if data transfer is targeted or not, basing
* off the <code>targetPeerId</code> parameter being defined.
* <small>Defined only for single targeted Peer data transfer.</small>
* @param {Error|String} callback.error.error <blockquote class="info">
* <b>Deprecation Warning!</b> This property has been deprecated.
* Please use <code>callback.error.transferErrors</code> instead.
* </blockquote> The error received that resulted in error.
* <small>Defined only for single targeted Peer 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>.</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 <blockquote class="info">
* <b>Deprecation Warning!</b> This property has been deprecated.
* Please use <code>callback.success.transferInfo.transferId</code> instead.
* </blockquote> The data transfer ID.
* @param {String} callback.success.state <blockquote class="info">
* <b>Deprecation Warning!</b> This property has been deprecated.
* Please use <a href="#event_dataTransferState"><code>dataTransferState</code>
* event</a> instead. </blockquote> The data transfer state that resulted in error.
* <small>Defined only for single targeted Peer data transfer.</small> [Rel: Skylink.DATA_TRANSFER_STATE]
* @param {String} callback.success.peerId <blockquote class="info">
* <b>Deprecation Warning!</b> This property has been deprecated.
* Please use <code>callback.success.listOfPeers</code> instead.
* </blockquote> The targeted Peer ID for data transfer.
* <small>Defined only for single targeted Peer data transfer.</small>
* @param {Boolean} callback.success.isPrivate <blockquote class="info">
* <b>Deprecation Warning!</b> This property has been deprecated.
* Please use <code>callback.success.transferInfo.isPrivate</code> instead.
* </blockquote> The flag if data transfer is targeted or not, basing
* off the <code>targetPeerId</code> parameter being defined.
* <small>Defined only for single targeted Peer data transfer.</small>
* @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>.</small>
* @trigger <ol class="desc-seq">
* <li>Checks if should open a new Datachannel <ol>
* <li>If Peer connection has closed: <small>This can be checked with <a href="#event_peerConnectionState">
* <code>peerConnectionState</code> event</a> triggering parameter payload <code>state</code> as <code>CLOSED</code>
* for Peer.</small> <ol><li><b>ABORT</b> step and return error.</li></ol></li>
* <li>If Peer supports simultaneous data transfer, open new Datachannel: <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>.</li>
* <li>If Datachannel has 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></ol></li>
* <li>Else: <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><b>ABORT</b> step and return error.</li></ol></li></ol></li></ol></li>
* <li>Starts the data transfer to Peer <ol>
* <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><a href="#event_incomingDataRequest"><code>incomingDataRequest</code> event</a> triggers.</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 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>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>
* <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><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>
* <li><a href="#event_incomingData"><code>incomingData</code> event</a> triggers.</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><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, callback) {
var listOfPeers = Object.keys(this._peerConnections);
var isPrivate = false;
var dataInfo = {};
var transferId = this._user.sid + this.DATA_TRANSFER_TYPE.UPLOAD +
(((new Date()).toISOString().replace(/-/g, '').replace(/:/g, ''))).replace('.', '');
// for error case
var errorMsg, errorPayload, i, peerId; // for jshint
var singleError = null;
var transferErrors = {};
var stateError = null;
var singlePeerId = null;
//Shift parameters
// timeout
if (typeof timeout === 'function') {
callback = timeout;
} else if (typeof timeout === 'string') {
listOfPeers = [timeout];
isPrivate = true;
} else if (Array.isArray(timeout)) {
listOfPeers = timeout;
isPrivate = true;
}
// targetPeerId
if (typeof targetPeerId === 'function'){
callback = targetPeerId;
// data, timeout, target [array], callback
} else if(Array.isArray(targetPeerId)) {
listOfPeers = targetPeerId;
isPrivate = true;
// data, timeout, target [string], callback
} else if (typeof targetPeerId === 'string') {
listOfPeers = [targetPeerId];
isPrivate = true;
}
//state: String, Deprecated. But for consistency purposes. Null if not a single peer
//error: Object, Deprecated. But for consistency purposes. Null if not a single peer
//transferId: String,
//peerId: String, Deprecated. But for consistency purposes. Null if not a single peer
//listOfPeers: Array, NEW!!
//isPrivate: isPrivate, NEW!!
//transferErrors: JSON, NEW!! - Array of errors
//transferInfo: JSON The same payload as dataTransferState transferInfo payload
// check if it's blob data
if (!(typeof data === 'object' && data instanceof Blob)) {
errorMsg = 'Provided data is not a Blob data';
if (listOfPeers.length === 0) {
transferErrors.self = errorMsg;
} else {
for (i = 0; i < listOfPeers.length; i++) {
peerId = listOfPeers[i];
transferErrors[peerId] = errorMsg;
}
// Deprecated but for consistency purposes. Null if not a single peer.
if (listOfPeers.length === 1 && isPrivate) {
stateError = self.DATA_TRANSFER_STATE.ERROR;
singleError = errorMsg;
singlePeerId = listOfPeers[0];
}
}
errorPayload = {
state: stateError,
error: singleError,
transferId: transferId,
peerId: singlePeerId,
listOfPeers: listOfPeers,
transferErrors: transferErrors,
transferInfo: dataInfo,
isPrivate: isPrivate
};
log.error(errorMsg, errorPayload);
if (typeof callback === 'function'){
log.log([null, 'RTCDataChannel', null, 'Error occurred. Firing callback ' +
'with error -> '],errorPayload);
callback(errorPayload, null);
}
return;
}
// populate data
dataInfo.name = data.name || transferId;
dataInfo.size = data.size;
dataInfo.timeout = typeof timeout === 'number' ? timeout : 60;
dataInfo.transferId = transferId;
dataInfo.dataType = 'blob';
dataInfo.isPrivate = isPrivate;
// check if datachannel is enabled first or not
if (!this._enableDataChannel) {
errorMsg = 'Unable to send any blob data. Datachannel is disabled';
if (listOfPeers.length === 0) {
transferErrors.self = errorMsg;
} else {
for (i = 0; i < listOfPeers.length; i++) {
peerId = listOfPeers[i];
transferErrors[peerId] = errorMsg;
}
// Deprecated but for consistency purposes. Null if not a single peer.
if (listOfPeers.length === 1 && isPrivate) {
stateError = self.DATA_TRANSFER_STATE.ERROR;
singleError = errorMsg;
singlePeerId = listOfPeers[0];
}
}
errorPayload = {
state: stateError,
error: singleError,
transferId: transferId,
peerId: singlePeerId,
listOfPeers: listOfPeers,
transferErrors: transferErrors,
transferInfo: dataInfo,
isPrivate: isPrivate
};
log.error(errorMsg, errorPayload);
if (typeof callback === 'function'){
log.log([null, 'RTCDataChannel', null, 'Error occurred. Firing callback ' +
'with error -> '], errorPayload);
callback(errorPayload, null);
}
return;
}
this._startDataTransfer(data, dataInfo, listOfPeers, callback);
};
/**
* <blockquote class="info">
* <b>Deprecation Warning!</b> This method has been deprecated, please use <a href="#method_acceptDataTransfer">
* <code>acceptDataTransfer()</code> method</a> instead.
* </blockquote>
* Function that accepts or rejects an upload data transfer request from Peer to User.
* <small>Parameter signature follows <a href="#method_acceptDataTransfer">
* <code>acceptDataTransfer()</code> method</a>.</small>
* @method respondBlobRequest
* @example
* // Example 1: Accept Peer upload data transfer request
* skylinkDemo.on("incomingDataRequest", function (transferId, peerId, transferInfo, isSelf) {
* if (!isSelf) {
* skylinkDemo.respondBlobRequest(peerId, transferId, true);
* }
* });
*
* // Example 2: Reject Peer upload data transfer request
* skylinkDemo.on("incomingDataRequest", function (transferId, peerId, transferInfo, isSelf) {
* if (!isSelf) {
* skylinkDemo.respondBlobRequest(peerId, transferId, false);
* }
* });
* @deprecated true
* @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.5.0
*/
Skylink.prototype.respondBlobRequest =
/**
* 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.acceptDataTransfer = function (peerId, transferId, accept) {
if (typeof transferId !== 'string' && typeof peerId !== 'string') {
log.error([peerId, 'RTCDataChannel', null, 'Aborting accept data transfer as ' +
'transfer ID and peer ID is not provided'], {
accept: accept,
peerId: peerId,
transferId: transferId
});
return;
}
if (transferId.indexOf(this._TRANSFER_DELIMITER) === -1) {
log.error([peerId, 'RTCDataChannel', null, 'Aborting accept data transfer as ' +
'invalid transfer ID is provided'], {
accept: accept,
transferId: transferId
});
return;
}
var channelName = transferId.split(this._TRANSFER_DELIMITER)[0];
if (accept) {
log.info([peerId, 'RTCDataChannel', channelName, 'User accepted peer\'s request'], {
accept: accept,
transferId: transferId
});
if (!this._peerInformations[peerId] && !this._peerInformations[peerId].agent) {
log.error([peerId, 'RTCDataChannel', channelName, 'Aborting accept data transfer as ' +
'Peer informations for peer is missing'], {
accept: accept,
transferId: transferId
});
return;
}
this._downloadDataTransfers[channelName] = [];
var data = this._downloadDataSessions[channelName];
this._sendDataChannelMessage(peerId, {
type: this._DC_PROTOCOL_TYPE.ACK,
sender: this._user.sid,
ackN: 0,
agent: window.webrtcDetectedBrowser
}, channelName);
this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.DOWNLOAD_STARTED,
data.transferId, peerId, {
name: data.name,
size: data.size,
data: null,
dataType: data.dataType,
percentage: 0,
senderPeerId: peerId,
timeout: data.timeout,
isPrivate: data.isPrivate
});
} else {
log.info([peerId, 'RTCDataChannel', channelName, 'User rejected peer\'s request'], {
accept: accept,
transferId: transferId
});
this._sendDataChannelMessage(peerId, {
type: this._DC_PROTOCOL_TYPE.ACK,
sender: this._user.sid,
ackN: -1
}, channelName);
delete this._downloadDataSessions[channelName];
delete this._downloadDataTransfers[channelName];
}
};
/**
* <blockquote class="info">
* <b>Deprecation Warning!</b> This method has been deprecated, please use <a href="#method_cancelDataTransfer">
* <code>method_cancelDataTransfer()</code> method</a> instead.
* </blockquote>
* Function that terminates a currently uploading / downloading data transfer from / to Peer.
* <small>Parameter signature follows <a href="#method_cancelDataTransfer">
* <code>cancelDataTransfer()</code> method</a>.</small>
* @method cancelBlobTransfer
* @trigger <small>Event sequence follows <a href="#method_acceptDataTransfer">
* @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.cancelBlobTransfer(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
* @deprecated true
* @for Skylink
* @since 0.5.7
*/
Skylink.prototype.cancelBlobTransfer =
/**
* 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.cancelDataTransfer = function (peerId, transferId) {
var data;
// targetPeerId + '-' + transferId
var channelName = peerId + '-' + transferId;
if (transferId.indexOf(this._TRANSFER_DELIMITER) > 0) {
channelName = transferId.split(this._TRANSFER_DELIMITER)[0];
} else {
var peerAgent = (this._peerInformations[peerId] || {}).agent;
if (!peerAgent && !peerAgent.name) {
log.error([peerId, 'RTCDataChannel', null, 'Cancel transfer to peer ' +
'failed as peer agent information for peer does not exists'], transferId);
return;
}
if (self._INTEROP_MULTI_TRANSFERS.indexOf(peerAgent.name) > -1) {
channelName = peerId;
}
}
if (this._uploadDataSessions[channelName]) {
data = this._uploadDataSessions[channelName];
delete this._uploadDataSessions[channelName];
delete this._uploadDataTransfers[channelName];
// send message
this._sendDataChannelMessage(peerId, {
type: this._DC_PROTOCOL_TYPE.CANCEL,
sender: this._user.sid,
name: data.name,
content: 'Peer cancelled upload transfer'
}, channelName);
log.debug([peerId, 'RTCDataChannel', channelName,
'Cancelling upload data transfers'], transferId);
} else if (this._downloadDataSessions[channelName]) {
data = this._downloadDataSessions[channelName];
delete this._downloadDataSessions[channelName];
delete this._downloadDataTransfers[channelName];
// send message
this._sendDataChannelMessage(peerId, {
type: this._DC_PROTOCOL_TYPE.CANCEL,
sender: this._user.sid,
name: data.name,
content: 'Peer cancelled download transfer'
}, channelName);
log.debug([peerId, 'RTCDataChannel', channelName,
'Cancelling download data transfers'], transferId);
} else {
log.error([peerId, 'RTCDataChannel', null, 'Cancel transfer to peer ' +
'failed as transfer session with peer does not exists'], transferId);
return;
}
this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.CANCEL,
data.transferId, peerId, {
name: data.name,
size: data.size,
percentage: data.percentage,
data: null,
dataType: data.dataType,
senderPeerId: data.senderPeerId,
timeout: data.timeout,
isPrivate: data.isPrivate
});
};
/**
* 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 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><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 self = this;
// check if datachannel is enabled first or not
if (!self._enableDataChannel) {
log.warn('Unable to send any P2P message. Datachannel is disabled');
return;
}
var listOfPeers = Object.keys(self._dataChannels);
var isPrivate = false;
//targetPeerId is defined -> private message
if (Array.isArray(targetPeerId)) {
listOfPeers = targetPeerId;
isPrivate = true;
} else if (typeof targetPeerId === 'string') {
listOfPeers = [targetPeerId];
isPrivate = true;
}
// sending public message to MCU to relay. MCU case only
if (self._hasMCU) {
if (isPrivate) {
log.log(['MCU', null, null, 'Relaying private P2P message to peers'], listOfPeers);
self._sendDataChannelMessage('MCU', {
type: self._DC_PROTOCOL_TYPE.MESSAGE,
isPrivate: isPrivate,
sender: self._user.sid,
target: listOfPeers,
data: message
});
} else {
log.log(['MCU', null, null, 'Relaying P2P message to peers']);
self._sendDataChannelMessage('MCU', {
type: self._DC_PROTOCOL_TYPE.MESSAGE,
isPrivate: isPrivate,
sender: self._user.sid,
target: 'MCU',
data: message
});
}
} else {
for (var i = 0; i < listOfPeers.length; i++) {
var peerId = listOfPeers[i];
var useChannel = (self._hasMCU) ? 'MCU' : peerId;
// Ignore MCU peer
if (peerId === 'MCU') {
continue;
}
log.log([peerId, null, useChannel, 'Sending P2P message to peer']);
self._sendDataChannelMessage(useChannel, {
type: self._DC_PROTOCOL_TYPE.MESSAGE,
isPrivate: isPrivate,
sender: self._user.sid,
target: peerId,
data: message
});
}
}
self._trigger('incomingMessage', {
content: message,
isPrivate: isPrivate,
targetPeerId: targetPeerId || null,
isDataChannel: true,
senderPeerId: self._user.sid
}, self._user.sid, self.getPeerInfo(), true);
};
/**
* <blockquote class="info">
* Currently, the Android and iOS SDKs do not support this type of data transfer.
* </blockquote>
* Function that starts an uploading string data transfer from User to Peers.
* @method sendURLData
* @param {String} data The data string to transfer to Peer.
* <small>Parameter signature follows <a href="#method_sendBlobData">
* <code>sendBlobData()</code> method</a> except <code>data</code> parameter.</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) {
var listOfPeers = Object.keys(this._peerConnections);
var isPrivate = false;
var dataInfo = {};
var transferId = this._user.sid + this.DATA_TRANSFER_TYPE.UPLOAD +
(((new Date()).toISOString().replace(/-/g, '').replace(/:/g, ''))).replace('.', '');
// for error case
var errorMsg, errorPayload, i, peerId; // for jshint
var singleError = null;
var transferErrors = {};
var stateError = null;
var singlePeerId = null;
//Shift parameters
// timeout
if (typeof timeout === 'function') {
callback = timeout;
} else if (typeof timeout === 'string') {
listOfPeers = [timeout];
isPrivate = true;
} else if (Array.isArray(timeout)) {
listOfPeers = timeout;
isPrivate = true;
}
// targetPeerId
if (typeof targetPeerId === 'function'){
callback = targetPeerId;
// data, timeout, target [array], callback
} else if(Array.isArray(targetPeerId)) {
listOfPeers = targetPeerId;
isPrivate = true;
// data, timeout, target [string], callback
} else if (typeof targetPeerId === 'string') {
listOfPeers = [targetPeerId];
isPrivate = true;
}
//state: String, Deprecated. But for consistency purposes. Null if not a single peer
//error: Object, Deprecated. But for consistency purposes. Null if not a single peer
//transferId: String,
//peerId: String, Deprecated. But for consistency purposes. Null if not a single peer
//listOfPeers: Array, NEW!!
//isPrivate: isPrivate, NEW!!
//transferErrors: JSON, NEW!! - Array of errors
//transferInfo: JSON The same payload as dataTransferState transferInfo payload
// check if it's blob data
if (typeof data !== 'string') {
errorMsg = 'Provided data is not a dataURL';
if (listOfPeers.length === 0) {
transferErrors.self = errorMsg;
} else {
for (i = 0; i < listOfPeers.length; i++) {
peerId = listOfPeers[i];
transferErrors[peerId] = errorMsg;
}
// Deprecated but for consistency purposes. Null if not a single peer.
if (listOfPeers.length === 1 && isPrivate) {
stateError = self.DATA_TRANSFER_STATE.ERROR;
singleError = errorMsg;
singlePeerId = listOfPeers[0];
}
}
errorPayload = {
state: stateError,
error: singleError,
transferId: transferId,
peerId: singlePeerId,
listOfPeers: listOfPeers,
transferErrors: transferErrors,
transferInfo: dataInfo,
isPrivate: isPrivate
};
log.error(errorMsg, errorPayload);
if (typeof callback === 'function'){
log.log([null, 'RTCDataChannel', null, 'Error occurred. Firing callback ' +
'with error -> '],errorPayload);
callback(errorPayload, null);
}
return;
}
// populate data
dataInfo.name = data.name || transferId;
dataInfo.size = data.size || data.length;
dataInfo.timeout = typeof timeout === 'number' ? timeout : 60;
dataInfo.transferId = transferId;
dataInfo.dataType = 'dataURL';
dataInfo.isPrivate = isPrivate;
// check if datachannel is enabled first or not
if (!this._enableDataChannel) {
errorMsg = 'Unable to send any dataURL. Datachannel is disabled';
if (listOfPeers.length === 0) {
transferErrors.self = errorMsg;
} else {
for (i = 0; i < listOfPeers.length; i++) {
peerId = listOfPeers[i];
transferErrors[peerId] = errorMsg;
}
// Deprecated but for consistency purposes. Null if not a single peer.
if (listOfPeers.length === 1 && isPrivate) {
stateError = self.DATA_TRANSFER_STATE.ERROR;
singleError = errorMsg;
singlePeerId = listOfPeers[0];
}
}
errorPayload = {
state: stateError,
error: singleError,
transferId: transferId,
peerId: singlePeerId,
listOfPeers: listOfPeers,
transferErrors: transferErrors,
transferInfo: dataInfo,
isPrivate: isPrivate
};
log.error(errorMsg, errorPayload);
if (typeof callback === 'function'){
log.log([null, 'RTCDataChannel', null, 'Error occurred. Firing callback ' +
'with error -> '], errorPayload);
callback(errorPayload, null);
}
return;
}
this._startDataTransfer(data, dataInfo, listOfPeers, callback);
};
/**
* Function that sets the data transfer "wait-for-response" timeout.
* When there is not response after timeout, the data transfer will be terminated.
* @method _setDataChannelTimeout
* @private
* @for Skylink
* @since 0.5.0
*/
Skylink.prototype._setDataChannelTimeout = function(peerId, timeout, isSender, channelName) {
var self = this;
if (!self._dataTransfersTimeout[channelName]) {
self._dataTransfersTimeout[channelName] = null;
}
var type = (isSender) ? self.DATA_TRANSFER_TYPE.UPLOAD :
self.DATA_TRANSFER_TYPE.DOWNLOAD;
self._dataTransfersTimeout[channelName] = setTimeout(function() {
var name;
if (self._dataTransfersTimeout[channelName][type]) {
if (isSender) {
name = self._uploadDataSessions[channelName].name;
delete self._uploadDataTransfers[channelName];
delete self._uploadDataSessions[channelName];
} else {
name = self._downloadDataSessions[channelName].name;
delete self._downloadDataTransfers[channelName];
delete self._downloadDataSessions[channelName];
}
self._sendDataChannelMessage(peerId, {
type: self._DC_PROTOCOL_TYPE.ERROR,
sender: self._user.sid,
name: name,
content: 'Connection Timeout. Longer than ' + timeout +
' seconds. Connection is abolished.',
isUploadError: isSender
}, channelName);
// TODO: Find a way to add channel name so it's more specific
log.error([peerId, 'RTCDataChannel', channelName, 'Failed transfering data:'],
'Transfer ' + ((isSender) ? 'for': 'from') + ' ' + peerId +
' failed. Connection timeout');
self._clearDataChannelTimeout(peerId, isSender, channelName);
}
}, 1000 * timeout);
};
/**
* Function that stops and clears the data transfer "wait-for-response" timeout.
* @method _clearDataChannelTimeout
* @private
* @for Skylink
* @since 0.5.0
*/
Skylink.prototype._clearDataChannelTimeout = function(peerId, isSender, channelName) {
if (this._dataTransfersTimeout[channelName]) {
clearTimeout(this._dataTransfersTimeout[channelName]);
delete this._dataTransfersTimeout[channelName];
log.debug([peerId, 'RTCDataChannel', channelName, 'Clear datachannel timeout']);
} else {
log.debug([peerId, 'RTCDataChannel', channelName, 'Unable to find timeouts. ' +
'Not clearing the datachannel timeouts']);
}
};
/**
* Function that starts a data transfer to Peer.
* This will open a new data type of Datachannel connection with Peer if
* simultaneous data transfers is supported by Peer.
* @method _sendBlobDataToPeer
* @private
* @for Skylink
* @since 0.5.5
*/
Skylink.prototype._sendBlobDataToPeer = function(data, dataInfo, targetPeerId) {
var self = this;
//If there is MCU then directs all messages to MCU
var targetChannel = targetPeerId;//(self._hasMCU) ? 'MCU' : targetPeerId;
var targetPeerList = [];
var binarySize = parseInt((dataInfo.size * (4 / 3)).toFixed(), 10);
var binaryChunkSize = 0;
var chunkSize = 0;
var i;
var hasSend = false;
// move list of peers to targetPeerList
if (self._hasMCU) {
if (Array.isArray(targetPeerList)) {
targetPeerList = targetPeerId;
} else {
targetPeerList = [targetPeerId];
}
targetPeerId = 'MCU';
}
if (dataInfo.dataType !== 'blob') {
// output: 1616
binaryChunkSize = self._CHUNK_DATAURL_SIZE;
chunkSize = self._CHUNK_DATAURL_SIZE;
binarySize = dataInfo.size;
} else if (window.webrtcDetectedBrowser === 'firefox') {
// output: 16384
binaryChunkSize = self._MOZ_CHUNK_FILE_SIZE * (4 / 3);
chunkSize = self._MOZ_CHUNK_FILE_SIZE;
} else {
// output: 65536
binaryChunkSize = parseInt((self._CHUNK_FILE_SIZE * (4 / 3)).toFixed(), 10);
chunkSize = self._CHUNK_FILE_SIZE;
}
var throwTransferErrorFn = function (message) {
// MCU targetPeerId case - list of peers
if (self._hasMCU) {
for (i = 0; i < targetPeerList.length; i++) {
var peerId = targetPeerList[i];
self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.ERROR,
dataInfo.transferId, peerId, {
name: dataInfo.name,
size: dataInfo.size,
percentage: 0,
data: null,
dataType: dataInfo.dataType,
senderPeerId: self._user.sid,
timeout: dataInfo.timeout,
isPrivate: dataInfo.isPrivate
},{
message: message,
transferType: self.DATA_TRANSFER_TYPE.UPLOAD
});
}
} else {
self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.ERROR,
dataInfo.transferId, targetPeerId, {
name: dataInfo.name,
size: dataInfo.size,
percentage: 0,
data: null,
dataType: dataInfo.dataType,
senderPeerId: self._user.sid,
timeout: dataInfo.timeout,
isPrivate: dataInfo.isPrivate
},{
message: message,
transferType: self.DATA_TRANSFER_TYPE.UPLOAD
});
}
};
var startTransferFn = function (targetId, channel) {
if (!hasSend) {
hasSend = true;
var payload = {
type: self._DC_PROTOCOL_TYPE.WRQ,
sender: self._user.sid,
agent: window.webrtcDetectedBrowser,
version: window.webrtcDetectedVersion,
name: dataInfo.name,
size: binarySize,
dataType: dataInfo.dataType,
chunkSize: binaryChunkSize,
timeout: dataInfo.timeout,
target: self._hasMCU ? targetPeerList : targetPeerId,
isPrivate: dataInfo.isPrivate
};
if (self._hasMCU) {
// if has MCU and is public, do not send individually
self._sendDataChannelMessage('MCU', payload, channel);
try {
var mainChannel = self._dataChannels.MCU.main.label;
self._setDataChannelTimeout('MCU', dataInfo.timeout, true, mainChannel);
} catch (error) {
log.error(['MCU', 'RTCDataChannel', 'MCU', 'Failed setting datachannel ' +
'timeout for MCU'], error);
}
} else {
// if has MCU and is public, do not send individually
self._sendDataChannelMessage(targetId, payload, channel);
self._setDataChannelTimeout(targetId, dataInfo.timeout, true, channel);
}
}
};
log.log([targetPeerId, 'RTCDataChannel', targetChannel, 'Chunk size of data:'], {
chunkSize: chunkSize,
binaryChunkSize: binaryChunkSize,
transferId: dataInfo.transferId,
dataType: dataInfo.dataType
});
var supportMulti = false;
var peerAgent = (self._peerInformations[targetPeerId] || {}).agent || {};
if (!peerAgent && !peerAgent.name) {
log.error([targetPeerId, 'RTCDataChannel', targetChannel, 'Aborting transfer to peer ' +
'as peer agent information for peer does not exists'], dataInfo);
throwTransferErrorFn('Peer agent information for peer does not exists');
return;
}
if (self._INTEROP_MULTI_TRANSFERS.indexOf(peerAgent.name) === -1) {
targetChannel = targetPeerId + '-' + dataInfo.transferId;
supportMulti = true;
if (!(self._dataChannels[targetPeerId] || {}).main) {
log.error([targetPeerId, 'RTCDataChannel', targetChannel,
'Main datachannel does not exists'], dataInfo);
throwTransferErrorFn('Main datachannel does not exists');
return;
} else if (self._dataChannels[targetPeerId].main.readyState !==
self.DATA_CHANNEL_STATE.OPEN) {
log.error([targetPeerId, 'RTCDataChannel', targetChannel,
'Main datachannel is not opened'], {
transferId: dataInfo.transferId,
readyState: self._dataChannels[targetPeerId].main.readyState
});
throwTransferErrorFn('Main datachannel is not opened');
return;
}
self._dataChannels[targetPeerId][targetChannel] =
self._createDataChannel(targetPeerId, self.DATA_CHANNEL_TYPE.DATA, null, targetChannel);
} else {
var ongoingTransfer = null;
if (self._uploadDataSessions[targetChannel]) {
ongoingTransfer = self.DATA_TRANSFER_TYPE.UPLOAD;
} else if (self._downloadDataSessions[targetChannel]) {
ongoingTransfer = self.DATA_TRANSFER_TYPE.DOWNLOAD;
}
if (ongoingTransfer) {
log.error([targetPeerId, 'RTCDataChannel', targetChannel, 'User have ongoing ' +
ongoingTransfer + ' transfer session with peer. Unable to send data'], dataInfo);
throwTransferErrorFn('Another ' + ongoingTransfer +
' transfer is ongoing. Unable to send data.');
return;
}
}
if (dataInfo.dataType === 'blob') {
self._uploadDataTransfers[targetChannel] = self._chunkBlobData(data, chunkSize);
} else {
self._uploadDataTransfers[targetChannel] = self._chunkDataURL(data, chunkSize);
}
self._uploadDataSessions[targetChannel] = {
name: dataInfo.name,
size: binarySize,
isUpload: true,
senderPeerId: self._user.sid,
transferId: dataInfo.transferId,
percentage: 0,
timeout: dataInfo.timeout,
chunkSize: chunkSize,
dataType: dataInfo.dataType,
isPrivate: dataInfo.isPrivate
};
if (supportMulti) {
self._condition('dataChannelState', function () {
startTransferFn(targetPeerId, targetChannel);
}, function () {
return self._dataChannels[targetPeerId][targetChannel].readyState ===
self.DATA_CHANNEL_STATE.OPEN;
}, function (state) {
return state === self.DATA_CHANNEL_STATE.OPEN;
});
} else {
startTransferFn(targetChannel, targetChannel);
}
return targetChannel;
};
/**
* Function that handles the data received from Datachannel and
* routes to the relevant data transfer protocol handler.
* @method _dataChannelProtocolHandler
* @private
* @for Skylink
* @since 0.5.2
*/
Skylink.prototype._dataChannelProtocolHandler = function(dataString, peerId, channelName, channelType) {
// PROTOCOL ESTABLISHMENT
if (!(this._peerInformations[peerId] || {}).agent) {
log.error([peerId, 'RTCDataChannel', channelName, 'Peer informations is missing during protocol ' +
'handling. Dropping packet'], dataString);
return;
}
/*var useChannel = channelName;
var peerAgent = this._peerInformations[peerId].agent.name;
if (channelType === this.DATA_CHANNEL_TYPE.MESSAGING ||
this._INTEROP_MULTI_TRANSFERS[peerAgent] > -1) {
useChannel = peerId;
}*/
if (typeof dataString === 'string') {
var data = {};
try {
data = JSON.parse(dataString);
} catch (error) {
log.debug([peerId, 'RTCDataChannel', channelName, 'Received from peer ->'], {
type: 'DATA',
data: dataString
});
this._DATAProtocolHandler(peerId, dataString,
this.DATA_TRANSFER_DATA_TYPE.BINARY_STRING, channelName);
return;
}
log.debug([peerId, 'RTCDataChannel', channelName, 'Received from peer ->'], {
type: data.type,
data: data
});
switch (data.type) {
case this._DC_PROTOCOL_TYPE.WRQ:
this._WRQProtocolHandler(peerId, data, channelName);
break;
case this._DC_PROTOCOL_TYPE.ACK:
this._ACKProtocolHandler(peerId, data, channelName);
break;
case this._DC_PROTOCOL_TYPE.ERROR:
this._ERRORProtocolHandler(peerId, data, channelName);
break;
case this._DC_PROTOCOL_TYPE.CANCEL:
this._CANCELProtocolHandler(peerId, data, channelName);
break;
case this._DC_PROTOCOL_TYPE.MESSAGE: // Not considered a protocol actually?
this._MESSAGEProtocolHandler(peerId, data, channelName);
break;
default:
log.error([peerId, 'RTCDataChannel', channelName, 'Unsupported message ->'], {
type: data.type,
data: data
});
}
}
};
/**
* Function that handles the "WRQ" data transfer protocol.
* @method _WRQProtocolHandler
* @private
* @for Skylink
* @since 0.5.2
*/
Skylink.prototype._WRQProtocolHandler = function(peerId, data, channelName) {
var transferId = channelName + this._TRANSFER_DELIMITER + (new Date()).getTime();
log.log([peerId, 'RTCDataChannel', channelName,
'Received file request from peer:'], data);
var name = data.name;
var binarySize = data.size;
var expectedSize = data.chunkSize;
var timeout = data.timeout;
this._downloadDataSessions[channelName] = {
transferId: transferId,
name: name,
isUpload: false,
senderPeerId: peerId,
size: binarySize,
percentage: 0,
dataType: data.dataType,
ackN: 0,
receivedSize: 0,
chunkSize: expectedSize,
timeout: timeout,
isPrivate: data.isPrivate
};
this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.UPLOAD_REQUEST,
transferId, peerId, {
name: name,
size: binarySize,
percentage: 0,
data: null,
dataType: data.dataType,
senderPeerId: peerId,
timeout: timeout,
isPrivate: data.isPrivate
});
this._trigger('incomingDataRequest', transferId, peerId, {
name: name,
size: binarySize,
percentage: 0,
dataType: data.dataType,
senderPeerId: peerId,
timeout: timeout,
isPrivate: data.isPrivate
}, false);
};
/**
* Function that handles the "ACK" data transfer protocol.
* @method _ACKProtocolHandler
* @private
* @for Skylink
* @since 0.5.2
*/
Skylink.prototype._ACKProtocolHandler = function(peerId, data, channelName) {
var self = this;
var ackN = data.ackN;
var transferStatus = self._uploadDataSessions[channelName];
if (!transferStatus) {
log.error([peerId, 'RTCDataChannel', channelName, 'Ignoring data received as ' +
'upload data transfers is empty'], {
status: transferStatus,
data: data
});
return;
}
if (!this._uploadDataTransfers[channelName]) {
log.error([peerId, 'RTCDataChannel', channelName,
'Ignoring data received as upload data transfers array is missing'], {
data: data
});
return;
}
//peerId = (peerId === 'MCU') ? data.sender : peerId;
var chunksLength = self._uploadDataTransfers[channelName].length;
var transferId = transferStatus.transferId;
var timeout = transferStatus.timeout;
self._clearDataChannelTimeout(peerId, true, channelName);
log.log([peerId, 'RTCDataChannel', channelName, 'ACK stage (' +
transferStatus.transferId + ') ->'], ackN + ' / ' + chunksLength);
if (ackN > -1) {
// Still uploading
if (ackN < chunksLength) {
var sendDataFn = function (base64BinaryString) {
var percentage = parseFloat((((ackN + 1) / chunksLength) * 100).toFixed(2), 10);
if (!self._uploadDataSessions[channelName]) {
log.error([peerId, 'RTCDataChannel', channelName,
'Failed uploading as data session is empty'], {
status: transferStatus,
data: data
});
return;
}
self._uploadDataSessions[channelName].percentage = percentage;
self._sendDataChannelMessage(peerId, base64BinaryString, channelName);
self._setDataChannelTimeout(peerId, timeout, true, channelName);
// to prevent from firing upload = 100;
if (percentage !== 100) {
self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.UPLOADING,
transferId, peerId, {
name: transferStatus.name,
size: transferStatus.size,
percentage: percentage,
data: null,
dataType: transferStatus.dataType,
senderPeerId: transferStatus.senderPeerId,
timeout: transferStatus.timeout,
isPrivate: transferStatus.isPrivate
});
}
};
if (transferStatus.dataType === 'blob') {
self._blobToBase64(self._uploadDataTransfers[channelName][ackN], sendDataFn);
} else {
sendDataFn(self._uploadDataTransfers[channelName][ackN]);
}
} else if (ackN === chunksLength) {
log.log([peerId, 'RTCDataChannel', channelName, 'Upload completed (' +
transferStatus.transferId + ')'], transferStatus);
self._trigger('dataTransferState',
self.DATA_TRANSFER_STATE.UPLOAD_COMPLETED, transferId, peerId, {
name: transferStatus.name,
size: transferStatus.size,
percentage: 100,
data: null,
dataType: transferStatus.dataType,
senderPeerId: transferStatus.senderPeerId,
timeout: transferStatus.timeout,
isPrivate: transferStatus.isPrivate
});
var blob = null;
if (transferStatus.dataType === 'blob') {
blob = new Blob(self._uploadDataTransfers[channelName]);
} else {
blob = self._assembleDataURL(self._uploadDataTransfers[channelName]);
}
self._trigger('incomingData', blob, transferId, peerId, {
name: transferStatus.name,
size: transferStatus.size,
percentage: 100,
dataType: transferStatus.dataType,
senderPeerId: transferStatus.senderPeerId,
timeout: transferStatus.timeout,
isPrivate: transferStatus.isPrivate
}, true);
delete self._uploadDataTransfers[channelName];
delete self._uploadDataSessions[channelName];
// close datachannel after transfer
if (self._dataChannels[peerId] && self._dataChannels[peerId][channelName]) {
log.debug([peerId, 'RTCDataChannel', channelName, 'Closing datachannel for upload transfer']);
self._closeDataChannel(peerId, channelName);
}
}
} else {
log.debug([peerId, 'RTCDataChannel', channelName, 'Upload rejected (' +
transferStatus.transferId + ')'], transferStatus);
self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.REJECTED,
transferId, peerId, {
name: transferStatus.name, //self._uploadDataSessions[channelName].name,
size: transferStatus.size, //self._uploadDataSessions[channelName].size,
percentage: 0,
data: null,
dataType: transferStatus.dataType,
senderPeerId: transferStatus.senderPeerId,
timeout: transferStatus.timeout,
isPrivate: transferStatus.isPrivate
});
delete self._uploadDataTransfers[channelName];
delete self._uploadDataSessions[channelName];
// close datachannel if rejected
if (self._dataChannels[peerId] && self._dataChannels[peerId][channelName]) {
log.debug([peerId, 'RTCDataChannel', channelName, 'Closing datachannel for upload transfer']);
self._closeDataChannel(peerId, channelName);
}
}
};
/**
* Function that handles the "MESSAGE" data transfer protocol.
* @method _MESSAGEProtocolHandler
* @private
* @for Skylink
* @since 0.5.2
*/
Skylink.prototype._MESSAGEProtocolHandler = function(peerId, data, channelName) {
var targetMid = data.sender;
log.log([targetMid, 'RTCDataChannel', channelName,
'Received P2P message from peer:'], data);
this._trigger('incomingMessage', {
content: data.data,
isPrivate: data.isPrivate,
isDataChannel: true,
targetPeerId: this._user.sid,
senderPeerId: targetMid
}, targetMid, this.getPeerInfo(targetMid), false);
};
/**
* Function that handles the "ERROR" data transfer protocol.
* @method _ERRORProtocolHandler
* @private
* @for Skylink
* @since 0.5.2
*/
Skylink.prototype._ERRORProtocolHandler = function(peerId, data, channelName) {
var isUploader = data.isUploadError;
var transferStatus = (isUploader) ? this._uploadDataSessions[channelName] :
this._downloadDataSessions[channelName];
if (!transferStatus) {
log.error([peerId, 'RTCDataChannel', channelName, 'Ignoring data received as ' +
(isUploader ? 'upload' : 'download') + ' data session is empty'], data);
return;
}
var transferId = transferStatus.transferId;
log.error([peerId, 'RTCDataChannel', channelName,
'Received an error from peer:'], data);
this._clearDataChannelTimeout(peerId, isUploader, channelName);
this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.ERROR,
transferId, peerId, {
name: transferStatus.name,
size: transferStatus.size,
percentage: transferStatus.percentage,
data: null,
dataType: transferStatus.dataType,
senderPeerId: transferStatus.senderPeerId,
timeout: transferStatus.timeout,
isPrivate: transferStatus.isPrivate
}, {
message: data.content,
transferType: ((isUploader) ? this.DATA_TRANSFER_TYPE.UPLOAD :
this.DATA_TRANSFER_TYPE.DOWNLOAD)
});
};
/**
* Function that handles the "CANCEL" data transfer protocol.
* @method _CANCELProtocolHandler
* @private
* @for Skylink
* @since 0.5.0
*/
Skylink.prototype._CANCELProtocolHandler = function(peerId, data, channelName) {
var isUpload = !!this._uploadDataSessions[channelName];
var isDownload = !!this._downloadDataSessions[channelName];
var transferStatus = (isUpload) ? this._uploadDataSessions[channelName] :
this._downloadDataSessions[channelName];
if (!transferStatus) {
log.error([peerId, 'RTCDataChannel', channelName, 'Ignoring data received as ' +
(isUpload ? 'upload' : 'download') + ' data session is empty'], data);
return;
}
var transferId = transferStatus.transferId;
log.log([peerId, 'RTCDataChannel', channelName,
'Received file transfer cancel request:'], data);
this._clearDataChannelTimeout(peerId, isUpload, channelName);
try {
if (isUpload) {
delete this._uploadDataSessions[channelName];
delete this._uploadDataTransfers[channelName];
} else {
delete this._downloadDataSessions[channelName];
delete this._downloadDataTransfers[channelName];
}
this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.CANCEL,
transferId, peerId, {
name: transferStatus.name,
size: transferStatus.size,
data: null,
dataType: transferStatus.dataType,
percentage: transferStatus.percentage,
senderPeerId: transferStatus.senderPeerId,
timeout: transferStatus.timeout,
isPrivate: transferStatus.isPrivate
}, {
message: data.content,
transferType: ((isUpload) ? this.DATA_TRANSFER_TYPE.UPLOAD :
this.DATA_TRANSFER_TYPE.DOWNLOAD)
});
log.log([peerId, 'RTCDataChannel', channelName,
'Emptied file transfer session:'], data);
} catch (error) {
this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.ERROR,
transferId, peerId, {
name: transferStatus.name,
size: transferStatus.size,
data: null,
dataType: transferStatus.dataType,
percentage: transferStatus.percentage,
senderPeerId: transferStatus.senderPeerId,
timeout: transferStatus.timeout,
isPrivate: transferStatus.isPrivate
}, {
message: 'Failed cancelling data request from peer',
transferType: ((isUpload) ? this.DATA_TRANSFER_TYPE.UPLOAD :
this.DATA_TRANSFER_TYPE.DOWNLOAD)
});
log.error([peerId, 'RTCDataChannel', channelName,
'Failed emptying file transfer session:'], {
data: data,
error: error
});
}
};
/**
* Function that handles the data transfer chunk received.
* @method _DATAProtocolHandler
* @private
* @for Skylink
* @since 0.5.5
*/
Skylink.prototype._DATAProtocolHandler = function(peerId, dataString, dataType, channelName) {
var chunk, error = '';
var transferStatus = this._downloadDataSessions[channelName];
log.log([peerId, 'RTCDataChannel', channelName,
'Received data chunk from peer ->'], {
dataType: dataType,
data: dataString,
type: 'DATA'
});
if (!transferStatus) {
log.error([peerId, 'RTCDataChannel', channelName,
'Ignoring data received as download data session is empty'], {
dataType: dataType,
data: dataString,
type: 'DATA'
});
return;
}
if (!this._downloadDataTransfers[channelName]) {
log.error([peerId, 'RTCDataChannel', channelName,
'Ignoring data received as download data transfers array is missing'], {
dataType: dataType,
data: dataString,
type: 'DATA'
});
return;
}
var transferId = transferStatus.transferId;
var dataTransferType = transferStatus.dataType;
var receivedSize = 0;
this._clearDataChannelTimeout(peerId, false, channelName);
if (dataType === this.DATA_TRANSFER_DATA_TYPE.BINARY_STRING) {
if (dataTransferType === 'blob') {
chunk = this._base64ToBlob(dataString);
receivedSize = (chunk.size * (4 / 3));
} else {
chunk = dataString;
receivedSize = dataString.length;
}
} else if (dataType === this.DATA_TRANSFER_DATA_TYPE.ARRAY_BUFFER) {
chunk = new Blob(dataString);
} else if (dataType === this.DATA_TRANSFER_DATA_TYPE.BLOB) {
chunk = dataString;
} else {
error = 'Unhandled data exception: ' + dataType;
log.error([peerId, 'RTCDataChannel', channelName, 'Failed downloading data packets:'], {
dataType: dataType,
data: dataString,
type: 'DATA',
error: error
});
this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.ERROR,
transferId, peerId, {
name: transferStatus.name,
size: transferStatus.size,
percentage: transferStatus.percentage,
data: null,
dataType: dataTransferType,
senderPeerId: transferStatus.senderPeerId,
timeout: transferStatus.timeout,
isPrivate: transferStatus.isPrivate
}, {
message: error,
transferType: this.DATA_TRANSFER_TYPE.DOWNLOAD
});
return;
}
log.log([peerId, 'RTCDataChannel', channelName,
'Received and expected data chunk size (' + receivedSize + ' === ' +
transferStatus.chunkSize + ')'], {
dataType: dataType,
data: dataString,
receivedSize: receivedSize,
expectedSize: transferStatus.chunkSize,
type: 'DATA'
});
if (transferStatus.chunkSize >= receivedSize) {
this._downloadDataTransfers[channelName].push(chunk);
transferStatus.ackN += 1;
transferStatus.receivedSize += receivedSize;
var totalReceivedSize = transferStatus.receivedSize;
var percentage = parseFloat(((totalReceivedSize / transferStatus.size) * 100).toFixed(2), 10);
this._sendDataChannelMessage(peerId, {
type: this._DC_PROTOCOL_TYPE.ACK,
sender: this._user.sid,
ackN: transferStatus.ackN
}, channelName);
// update the percentage
this._downloadDataSessions[channelName].percentage = percentage;
if (transferStatus.chunkSize === receivedSize && percentage < 100) {
log.log([peerId, 'RTCDataChannel', channelName,
'Transfer in progress ACK n (' + transferStatus.ackN + ')'], {
dataType: dataType,
data: dataString,
ackN: transferStatus.ackN,
type: 'DATA'
});
this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.DOWNLOADING,
transferId, peerId, {
name: transferStatus.name,
size: transferStatus.size,
percentage: percentage,
data: null,
dataType: dataTransferType,
senderPeerId: transferStatus.senderPeerId,
timeout: transferStatus.timeout,
isPrivate: transferStatus.isPrivate
});
this._setDataChannelTimeout(peerId, transferStatus.timeout, false, channelName);
if (!this._downloadDataSessions[channelName]) {
log.error([peerId, 'RTCDataChannel', channelName,
'Failed downloading as data session is empty'], {
dataType: dataType,
data: dataString,
type: 'DATA'
});
return;
}
this._downloadDataSessions[channelName].info = transferStatus;
} else {
log.log([peerId, 'RTCDataChannel', channelName,
'Download complete'], {
dataType: dataType,
data: dataString,
type: 'DATA',
transferInfo: transferStatus
});
var blob = null;
if (dataTransferType === 'blob') {
blob = new Blob(this._downloadDataTransfers[channelName]);
} else {
blob = this._assembleDataURL(this._downloadDataTransfers[channelName]);
}
this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.DOWNLOAD_COMPLETED,
transferId, peerId, {
name: transferStatus.name,
size: transferStatus.size,
percentage: 100,
data: blob,
dataType: dataTransferType,
senderPeerId: transferStatus.senderPeerId,
timeout: transferStatus.timeout,
isPrivate: transferStatus.isPrivate
});
this._trigger('incomingData', blob, transferId, peerId, {
name: transferStatus.name,
size: transferStatus.size,
percentage: 100,
dataType: dataTransferType,
senderPeerId: transferStatus.senderPeerId,
timeout: transferStatus.timeout,
isPrivate: transferStatus.isPrivate
}, false);
delete this._downloadDataTransfers[channelName];
delete this._downloadDataSessions[channelName];
log.log([peerId, 'RTCDataChannel', channelName,
'Converted to Blob as download'], {
dataType: dataType,
data: dataString,
type: 'DATA',
transferInfo: transferStatus
});
// close datachannel after transfer
if (this._dataChannels[peerId] && this._dataChannels[peerId][channelName]) {
log.debug([peerId, 'RTCDataChannel', channelName, 'Closing datachannel for download transfer']);
this._closeDataChannel(peerId, channelName);
}
}
} else {
error = 'Packet not match - [Received]' + receivedSize +
' / [Expected]' + transferStatus.chunkSize;
this._trigger('dataTransferState',
this.DATA_TRANSFER_STATE.ERROR, transferId, peerId, {
name: transferStatus.name,
size: transferStatus.size,
percentage: transferStatus.percentage,
data: null,
dataType: dataTransferType,
senderPeerId: transferStatus.senderPeerId,
timeout: transferStatus.timeout,
isPrivate: transferStatus.isPrivate
}, {
message: error,
transferType: this.DATA_TRANSFER_TYPE.DOWNLOAD
});
log.error([peerId, 'RTCDataChannel', channelName,
'Failed downloading data packets:'], {
dataType: dataType,
data: dataString,
type: 'DATA',
transferInfo: transferStatus,
error: error
});
}
};
/**
* Function that start the data transfer with the list of targeted Peer IDs provided.
* At this stage, it will open a new Datachannel connection if simultaneous data transfers is
* supported by Peer, or it will using the messaging type Datachannel connection.
* Note that 1 data transfer can occur at a time in 1 Datachannel connection.
* @method _startDataTransfer
* @private
* @for Skylink
* @since 0.6.1
*/
Skylink.prototype._startDataTransfer = function(data, dataInfo, listOfPeers, callback) {
var self = this;
var error = '';
var noOfPeersSent = 0;
var transferId = dataInfo.transferId;
var dataType = dataInfo.dataType;
var isPrivate = dataInfo.isPrivate;
var i;
var peerId;
// for callback
var listOfPeersTransferState = {};
var transferSuccess = true;
var listOfPeersTransferErrors = {};
var listOfPeersChannels = {};
var successfulPeerTransfers = [];
var triggerCallbackFn = function () {
for (i = 0; i < listOfPeers.length; i++) {
var transferPeerId = listOfPeers[i];
if (!listOfPeersTransferState[transferPeerId]) {
// if error, make as false and break
transferSuccess = false;
break;
}
}
if (transferSuccess) {
log.log([null, 'RTCDataChannel', transferId, 'Firing success callback for data transfer'], dataInfo);
// should we even support this? maybe keeping to not break older impl
if (listOfPeers.length === 1 && isPrivate) {
callback(null,{
state: self.DATA_TRANSFER_STATE.UPLOAD_COMPLETED,
peerId: listOfPeers[0],
listOfPeers: listOfPeers,
transferId: transferId,
isPrivate: isPrivate, // added new flag to indicate privacy
transferInfo: dataInfo
});
} else {
callback(null,{
state: null,
peerId: null,
transferId: transferId,
listOfPeers: listOfPeers,
isPrivate: isPrivate, // added new flag to indicate privacy
transferInfo: dataInfo
});
}
} else {
log.log([null, 'RTCDataChannel', transferId, 'Firing failure callback for data transfer'], dataInfo);
// should we even support this? maybe keeping to not break older impl
if (listOfPeers.length === 1 && isPrivate) {
callback({
state: self.DATA_TRANSFER_STATE.ERROR,
error: listOfPeersTransferErrors[listOfPeers[0]],
peerId: listOfPeers[0],
transferId: transferId,
transferErrors: listOfPeersTransferErrors,
transferInfo: dataInfo,
isPrivate: isPrivate, // added new flag to indicate privacy
listOfPeers: listOfPeers
}, null);
} else {
callback({
state: null,
peerId: null,
error: null,
transferId: transferId,
listOfPeers: listOfPeers,
isPrivate: isPrivate, // added new flag to indicate privacy
transferInfo: dataInfo,
transferErrors: listOfPeersTransferErrors
}, null);
}
}
};
for (i = 0; i < listOfPeers.length; i++) {
peerId = listOfPeers[i];
if (peerId === 'MCU') {
continue;
}
if (self._dataChannels[peerId] && self._dataChannels[peerId].main) {
log.log([peerId, 'RTCDataChannel', null, 'Sending blob data ->'], dataInfo);
self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.UPLOAD_STARTED,
transferId, peerId, {
name: dataInfo.name,
size: dataInfo.size,
percentage: 0,
data: data,
dataType: dataType,
senderPeerId: self._user.sid,
timeout: dataInfo.timeout,
isPrivate: isPrivate
});
self._trigger('incomingDataRequest', transferId, peerId, {
name: dataInfo.name,
size: dataInfo.size,
percentage: 0,
dataType: dataType,
senderPeerId: self._user.sid,
timeout: dataInfo.timeout,
isPrivate: isPrivate
}, true);
if (!self._hasMCU) {
listOfPeersChannels[peerId] =
self._sendBlobDataToPeer(data, dataInfo, peerId);
} else {
listOfPeersChannels[peerId] = self._dataChannels[peerId].main.label;
}
noOfPeersSent++;
} else {
error = 'Datachannel does not exist. Unable to start data transfer with peer';
log.error([peerId, 'RTCDataChannel', null, error]);
listOfPeersTransferErrors[peerId] = error;
}
}
// if has MCU
if (self._hasMCU) {
self._sendBlobDataToPeer(data, dataInfo, listOfPeers, isPrivate, transferId);
}
if (noOfPeersSent === 0) {
error = 'Failed sending data as there is no available datachannels to send data';
for (i = 0; i < listOfPeers.length; i++) {
peerId = listOfPeers[i];
self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.ERROR,
transferId, peerId, {
name: dataInfo.name,
size: dataInfo.size,
data: null,
dataType: dataType,
percentage: 0,
senderPeerId: self._user.sid,
timeout: dataInfo.timeout,
isPrivate: isPrivate
}, {
message: error,
transferType: self.DATA_TRANSFER_TYPE.UPLOAD
});
listOfPeersTransferErrors[peerId] = error;
}
log.error([null, 'RTCDataChannel', null, error]);
self._uploadDataTransfers = [];
self._uploadDataSessions = [];
transferSuccess = false;
if (typeof callback === 'function') {
triggerCallbackFn();
}
return;
}
if (typeof callback === 'function') {
var dataChannelStateFn = function(state, transferringPeerId, errorObj, channelName, channelType){
// check if error or closed halfway, if so abort
if (state === self.DATA_CHANNEL_STATE.ERROR &&
state === self.DATA_CHANNEL_STATE.CLOSED &&
listOfPeersChannels[peerId] === channelName) {
// if peer has already been inside, ignore
if (successfulPeerTransfers.indexOf(transferringPeerId) === -1) {
listOfPeersTransferState[transferringPeerId] = false;
listOfPeersTransferErrors[transferringPeerId] = errorObj;
log.error([transferringPeerId, 'RTCDataChannel', null,
'Data channel state has met a failure state for peer (datachannel) ->'], {
state: state,
error: errorObj
});
}
}
if (Object.keys(listOfPeersTransferState).length === listOfPeers.length) {
self.off('dataTransferState', dataTransferStateFn);
self.off('dataChannelState', dataChannelStateFn);
log.log([null, 'RTCDataChannel', transferId,
'Transfer states have been gathered completely in dataChannelState'], state);
triggerCallbackFn();
}
};
var dataTransferStateFn = function(state, stateTransferId, transferringPeerId, transferInfo, errorObj){
// check if transfer is related to this transfer
if (stateTransferId === transferId) {
// check if state upload has completed
if (state === self.DATA_TRANSFER_STATE.UPLOAD_COMPLETED) {
log.debug([transferringPeerId, 'RTCDataChannel', stateTransferId,
'Data transfer state has met a success state for peer ->'], state);
// if peer has already been inside, ignore
if (successfulPeerTransfers.indexOf(transferringPeerId) === -1) {
listOfPeersTransferState[transferringPeerId] = true;
}
} else if(state === self.DATA_TRANSFER_STATE.REJECTED ||
state === self.DATA_TRANSFER_STATE.CANCEL ||
state === self.DATA_TRANSFER_STATE.ERROR) {
if (state === self.DATA_TRANSFER_STATE.REJECTED) {
errorObj = new Error('Peer has rejected data transfer request');
}
log.error([transferringPeerId, 'RTCDataChannel', stateTransferId,
'Data transfer state has met a failure state for peer ->'], {
state: state,
error: errorObj
});
// if peer has already been inside, ignore
if (successfulPeerTransfers.indexOf(transferringPeerId) === -1) {
listOfPeersTransferState[transferringPeerId] = false;
listOfPeersTransferErrors[transferringPeerId] = errorObj;
}
}
}
if (Object.keys(listOfPeersTransferState).length === listOfPeers.length) {
self.off('dataTransferState', dataTransferStateFn);
self.off('dataChannelState', dataChannelStateFn);
log.log([null, 'RTCDataChannel', stateTransferId,
'Transfer states have been gathered completely in dataTransferState'], state);
triggerCallbackFn();
}
};
self.on('dataTransferState', dataTransferStateFn);
self.on('dataChannelState', dataChannelStateFn);
}
};