/**
* The current version of the internal Data Transfer (DT) Protocol that Skylink is using.

* - This is not a feature for developers to use but rather for SDK developers to
* see the Protocol version used in this Skylink version.
* - In some cases, this information may be used for reporting issues with Skylink.
* - DT_PROTOCOL VERSION: 0.1.0.
* @attribute DT_PROTOCOL_VERSION
* @type String
* @readOnly
* @component DataTransfer
* @for Skylink
* @since 0.5.10
*/
Skylink.prototype.DT_PROTOCOL_VERSION = '0.1.0';
/**
* The fixed delimiter that is used in Skylink to
* concat the DataChannel channelName and the actual
* transfer ID together based on the transfer ID provided in
* {{#crossLink "Skylink/dataTransferState:event"}}dataTransferState{{/crossLink}}.
* @attribute _TRANSFER_DELIMITER
* @type String
* @final
* @private
* @component DataTransfer
* @for Skylink
* @since 0.5.10
*/
Skylink.prototype._TRANSFER_DELIMITER = '_skylink__';
/**
* The list of Protocol types that is used for transfers and messaging using
* the DataChannel connection.
* @attribute _DC_PROTOCOL_TYPE
* @type JSON
* @param {String} WRQ Protocol to initiate a transfer request on the current
* DataChannel connection. Data transfer step 1.
* @param {String} ACK Protocol to accept or reject the transfer request.
* Data transfer step 2.
* @param {String} DATA Actual binary data or string send based on the
* ackN in the ACK packet received.
* Data transfer step 3. This may not occur is step 2 is rejected.
* @param {String} CANCEL Protocol to terminate an ongoing transfer.
* This data transfer step can happen after step 2 or 3.
* @param {String} ERROR Protocol that is sent when a transfer occurs an exception
* which using causes it to be terminated.
* This data transfer step can happen after step 2 or 3.
* @param {String} MESSAGE Protocol that is used to send P2P message objects
* over the DataChannel connection.
* This is not related to any data transfer step, but for messaging purposes.
* @final
* @private
* @for Skylink
* @component DataTransfer
* @since 0.5.2
*/
Skylink.prototype._DC_PROTOCOL_TYPE = {
WRQ: 'WRQ',
ACK: 'ACK',
ERROR: 'ERROR',
CANCEL: 'CANCEL',
MESSAGE: 'MESSAGE'
};
/**
* The list of platforms that Skylink should fallback to use the
* {{#crossLink "Skylink/DATA_CHANNEL_TYPE:attr"}}
* DATA_CHANNEL_TYPE.MESSAGING{{/crossLink}}
* channel for transfers instead of using multi-transfers
* due to the lack of support in the platform implementations.
* @attribute _INTEROP_MULTI_TRANSFERS
* @type Array
* @final
* @private
* @for Skylink
* @component DataTransfer
* @since 0.6.1
*/
Skylink.prototype._INTEROP_MULTI_TRANSFERS = ['Android', 'iOS'];
/**
* These are the types of data transfers that indicates if transfer is an
* outgoing (uploading) or incoming (downloding) transfers.
* @attribute DATA_TRANSFER_TYPE
* @type JSON
* @param {String} UPLOAD Value "upload"
* This data transfer is an outgoing (uploading) transfer.

* Data is sent to the receiving Peer using the associated DataChannel connection.
* @param {String} DOWNLOAD Value "download"
* The data transfer is an incoming (downloading) transfer.

* Data is received from the sending Peer using the associated DataChannel connection.
* @readOnly
* @component DataTransfer
* @for Skylink
* @since 0.1.0
*/
Skylink.prototype.DATA_TRANSFER_TYPE = {
UPLOAD: 'upload',
DOWNLOAD: 'download'
};
/**
* These are the list of data transfer states that Skylink would trigger.
* @attribute DATA_TRANSFER_STATE
* @type JSON
* @param {String} UPLOAD_REQUEST Value "request"
* The state when a data transfer request has been received from Peer.
* This happens after Peer starts a data transfer using
* {{#crossLink "Skylink/sendBlobData:method"}}sendBlobData(){{/crossLink}} or
* {{#crossLink "Skylink/sendURLData:method"}}sendURLData(){{/crossLink}}.
* @param {String} UPLOAD_STARTED Value "uploadStarted"
* The state when the data transfer will begin and start to upload the first data
* packets to receiving Peer.

* This happens after receiving Peer accepts a data transfer using
* {{#crossLink "Skylink/acceptDataTransfer:method"}}acceptDataTransfer(){{/crossLink}}.
* @param {String} DOWNLOAD_STARTED Value "downloadStarted"
* The state when the data transfer has begin and associated DataChannel connection is
* expected to receive the first data packet from sending Peer.

* This happens after self accepts a data transfer using
* {{#crossLink "Skylink/acceptDataTransfer:method"}}acceptDataTransfer(){{/crossLink}} upon
* the triggered state of UPLOAD_REQUEST.
* @param {String} REJECTED Value "rejected"
* The state when the data transfer has been rejected by receiving Peer and data transfer is
* terminated.

* This happens after Peer rejects a data transfer using
* {{#crossLink "Skylink/acceptDataTransfer:method"}}acceptDataTransfer(){{/crossLink}}.
* @param {String} UPLOADING Value "uploading"
* The state when the data transfer is still being transferred to receiving Peer.

* This happens after state UPLOAD_STARTED.
* @param {String} DOWNLOADING Value "downloading"
* The state when the data transfer is still being transferred from sending Peer.

* This happens after state DOWNLOAD_STARTED.
* @param {String} UPLOAD_COMPLETED Value "uploadCompleted"
* The state when the data transfer has been transferred to receiving Peer successfully.

* This happens after state UPLOADING or UPLOAD_STARTED, depending
* on how huge the data being transferred is.
* @param {String} DOWNLOAD_COMPLETED Value "downloadCompleted"
* The state when the data transfer has been transferred from sending Peer successfully.

* This happens after state DOWNLOADING or DOWNLOAD_STARTED, depending
* on how huge the data being transferred is.
* @param {String} CANCEL Value "cancel"
* The state when the data transfer has been terminated by Peer.

* This happens after state DOWNLOAD_STARTED or UPLOAD_STARTED.
* @param {String} ERROR Value "error"
* The state when the data transfer has occurred an exception.

* At this stage, the data transfer would usually be terminated and may lead to state CANCEL.
* @readOnly
* @component DataTransfer
* @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 list of ongoing data transfers data packets (chunks) to be sent to receiving end
* in a DataChannel connection based on the associated DataChannel ID.
* @attribute _uploadDataTransfers
* @param {Array} (#channelName) The ongoing data transfer packets to be sent to
* receiving end associated with the DataChannel connection.
* @param {Blob|String} (#channelName).(#index) The packet index of chunked Blob data object or
* dataURL string (base64 binary string) to be sent to received end.
* @type JSON
* @private
* @required
* @component DataTransfer
* @for Skylink
* @since 0.4.1
*/
Skylink.prototype._uploadDataTransfers = {};
/**
* Stores the list of ongoing data transfer state informations that is sent to receiving end
* in a DataChannel connection based on the associated DataChannel ID.
* @attribute _uploadDataSessions
* @param {JSON} (#channelName) The ongoing data transfer information that is sent
* to receiving end associated with the DataChannel connection.
* @param {String} (#channelName).name The data transfer name.
* @param {Number} (#channelName).size The expected data size of the
* completed data transfer.
* @param {Boolean} (#channelName).isUpload The flag that indicates if the
* transfer is an upload data transfer.
* In this case, the value should be true.
* @param {String} (#channelName).senderPeerId The Peer uploader ID.
* @param {String} (#channelName).transferId The data transfer ID.
* @param {Number} (#channelName).percentage The data transfer percentage.
* @param {Number} (#channelName).timeout The data transfer timeout.
* @param {Number} (#channelName).chunkSize The data transfer packet (chunk) size.
* @param {String} (#channelName).dataType The data transfer packet (chunk) data type.
* @type JSON
* @private
* @required
* @component DataTransfer
* @for Skylink
* @since 0.4.1
*/
Skylink.prototype._uploadDataSessions = {};
/**
* Stores the list of ongoing data transfers data packets (chunks) to be received from
* sending point in a DataChannel connection based on the associated DataChannel ID.
* @attribute _downloadDataTransfers
* @param {Array} (#channelName) The ongoing data transfer packets received
* associated with DataChannel.
* @param {Blob|String} (#channelName).(#index) The packet index of chunked Blob data object or
* dataURL string (base64 binary string) received from sending point.
* @type JSON
* @private
* @required
* @component DataTransfer
* @for Skylink
* @since 0.4.1
*/
Skylink.prototype._downloadDataTransfers = {};
/**
* Stores the list of ongoing data transfer state informations that is received from
* the sender point in a DataChannel connection based on the associated DataChannel ID.
* @attribute _downloadDataSessions
* @param {JSON} (#channelName) The ongoing data transfer information that is sent
* to receiving end associated with the DataChannel connection.
* @param {String} (#channelName).name The data transfer name.
* @param {Number} (#channelName).size The expected data size of the
* completed data transfer.
* @param {Boolean} (#channelName).isUpload The flag that indicates if the
* transfer is an upload data transfer.
* In this case, the value should be false.
* @param {String} (#channelName).senderPeerId The Peer uploader ID.
* @param {String} (#channelName).transferId The data transfer ID.
* @param {Number} (#channelName).percentage The data transfer percentage.
* @param {Number} (#channelName).timeout The data transfer timeout to wait for response
* before throwing a timeout error.
* @param {Number} (#channelName).chunkSize The data transfer packet (chunk) size.
* @param {String} (#channelName).dataType The data transfer packet (chunk) data type.
* @type JSON
* @private
* @required
* @component DataTransfer
* @for Skylink
* @since 0.4.1
*/
Skylink.prototype._downloadDataSessions = {};
/**
* Stores the list of ongoing data transfer timeouts using the
* setTimeout objects for each DataChannel connection transfer.
* @attribute _dataTransfersTimeout
* @param {Object} (#channelName) The timeout for the associated DataChannel
* connection.
* @type JSON
* @private
* @required
* @component DataTransfer
* @for Skylink
* @since 0.4.1
*/
Skylink.prototype._dataTransfersTimeout = {};
/**
* Sets a waiting timeout for every response sent to DataChannel connection receiving
* end. Once the timeout has ended, a timeout error will be thrown and
* data transfer will be terminated.
* @method _setDataChannelTimeout
* @param {String} peerId The Peer ID associated with the DataChannel connection.
* @param {Number} timeout The waiting timeout in seconds.
* @param {Boolean} [isSender=false] The flag thats indicates if the response
* is related to a downloading or uploading data transfer.
* @param {String} channelName The DataChannel connection ID.
* @private
* @component DataTransfer
* @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);
};
/**
* Stops and clears the waitig timeout for the associated DataChannel connection.
* @method _clearDataChannelTimeout
* @param {String} peerId The Peer ID associated with the DataChannel connection.
* @param {Boolean} [isSender=false] The flag thats indicates if the response
* is related to a downloading or uploading data transfer.
* @param {String} channelName The DataChannel connection ID.
* @private
* @component DataTransfer
* @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']);
}
};
/**
* Starts a data transfer with a Peer. If multi-transfer is supported,
* Skylink would open a new DataChannel connection with Peer to start
* data transfer. If mutli-transfer is not supported in
* {{#crossLink "Skylink/_INTEROP_MULTI_TRANSFERS:attr"}}_INTEROP_MULTI_TRANSFERS{{/crossLink}},
* the data transfer would start in the {{#crossLink "Skylink/DATA_CHANNEL_TYPE:attr"}}
* DATA_CHANNEL_TYPE.MESSAGING{{/crossLink}} channel instead.
* @method _sendBlobDataToPeer
* @param {Blob} data The Blob data object to send.
* @param {JSON} dataInfo The data transfer information.
* @param {String} dataInfo.transferId The transfer ID of the data transfer.
* @param {String} dataInfo.name The transfer Blob data object name.
* @param {Number} [dataInfo.timeout=60] The timeout set to await in seconds
* for response from DataChannel connection.
* @param {Number} dataInfo.size The Blob data binary size expected to be received in the receiving end.
* @param {Boolean} [dataInfo.isPrivate=false] The flag to indicate if the data transfer is a private
* transfer to the Peer directly and not broadcasted to all Peers.
* @param {String|Array} [targetPeerId=null] The receiving Peer ID. Array is used for
* MCU connection where multi-targeted Peers are used. By default, the
* value is null, which indicates that the data transfer is requested with all
* connected Peers.
* @return {String} The DataChannel connection ID associated with the transfer. If returned
* as null or empty, it indicates an error.
* @private
* @component DataTransfer
* @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 ? 'MCU' : 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;
};
/**
* Routes the data received to the relevant Protocol handler based on the data received.
* @method _dataChannelProtocolHandler
* @param {String|Object} data The data received from the DataChannel connection.
* @param {String} senderPeerId The Peer ID associated with the DataChannel connection.
* @param {String} channelName The DataChannel connection ID.
* @param {String} channelType The DataChannel connection functionality type.
* [Rel: Skylink.DATA_CHANNEL_TYPE]
* @private
* @component DataTransfer
* @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
});
}
}
};
/**
* Handles the WRQ Protocol request received from the DataChannel connection.
* @method _WRQProtocolHandler
* @param {String} senderPeerId The Peer ID associated with the DataChannel connection.
* @param {JSON} data The data object received from the DataChannel connection.
* This should contain the WRQ payload.
* @param {String} data.agent The sender Peer platform browser or agent name.
* @param {Number} data.version The sender Peer platform browser or agent version.
* @param {String} data.name The transfer data object name.
* @param {Number} data.size The transfer data object expected received size.
* @param {Number} data.chunkSize The expected data transfer packet (chunk) size.
* @param {Number} data.timeout The timeout set to await in seconds
* for response from DataChannel connection.
* @param {Boolean} data.isPrivate The flag to indicate if the data transfer is a private
* transfer to the Peer connection directly and not broadcasted to all Peers conneciton.
* @param {String} data.sender The Peer ID of the sender.
* @param {String} data.type Protocol step "WRQ".
* @param {String} channelName The DataChannel connection ID associated with the transfer.
* @trigger dataTransferState
* @private
* @component DataTransfer
* @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);
};
/**
* Handles the ACK Protocol request received from the DataChannel connection.
* @method _ACKProtocolHandler
* @param {String} peerId The Peer ID associated with the DataChannel connection.
* @param {JSON} data The data object received from the DataChannel connection.
* This should contain the ACK payload.
* @param {Number} data.ackN The ACK response of the current data transfer.
* If 0, it means that the request has been accepted and the sending Peer
* has to send the first data transfer packet (chunk). If it's greater than 0,
* it means that the previous data transfer packet (chunk) has been received and is expecting
* for the next data transfer packet. The number always increment based on the number of data
* packets the receiving end has received. If it's -1, it means that the data
* transfer request has been rejected and the data transfer will be terminated.
* @param {String} data.sender The Peer ID of sender.
* @param {String} data.type Protocol step "ACK".
* @param {String} channelName The DataChannel connection ID associated with the transfer.
* @trigger dataTransferState
* @private
* @component DataTransfer
* @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);
}
}
};
/**
* Handles the MESSAGE Protocol request received from the DataChannel connection.
* @method _MESSAGEProtocolHandler
* @param {String} peerId The Peer ID associated with the DataChannel connection.
* @param {JSON} data The data object received from the DataChannel connection.
* This should contain the MESSAGE payload.
* @param {String} data.target The targeted Peer ID to receive the message object.
* @param {String|JSON} data.data The message object.
* @param {String} data.sender The Peer ID of the sender.
* @param {String} data.type Protocol step "MESSAGE".
* @param {String} channelName The DataChannel connection ID associated with the transfer.
* @trigger incomingMessage
* @private
* @component DataTransfer
* @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);
};
/**
* Handles the ERROR Protocol request received from the DataChannel connection.
* @method _ERRORProtocolHandler
* @param {String} senderPeerId The Peer ID associated with the DataChannel connection.
* @param {JSON} data The data object received from the DataChannel connection.
* This should contain the ERROR payload.
* @param {String} data.name The transfer data object name.
* @param {String} data.content The error message.
* @param {Boolean} [data.isUploadError=false] The flag thats indicates if the response
* is related to a downloading or uploading data transfer.
* @param {String} data.sender The Peer ID of the sender.
* @param {String} data.type Protocol step "ERROR".
* @param {String} channelName The DataChannel connection ID associated with the transfer.
* @trigger dataTransferState
* @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)
});
};
/**
* Handles the CANCEL Protocol request received from the DataChannel connection.
* @method _CANCELProtocolHandler
* @param {String} senderPeerId The Peer ID associated with the DataChannel connection.
* @param {JSON} data The data object received from the DataChannel connection.
* This should contain the CANCEL payload.
* @param {String} data.name The transfer data object name.
* @param {String} data.content The reason for termination as a message.
* @param {String} data.sender The Peer ID of the sender.
* @param {String} data.type Protocol step "CANCEL".
* @param {String} channelName The DataChannel connection ID associated with the transfer.
* @trigger dataTransferState
* @private
* @component DataTransfer
* @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
});
}
};
/**
* Handles the DATA Protocol request received from the DataChannel connection.
* In this handler, it actually handles and manipulates the received data transfer packet.
* @method _DATAProtocolHandler
* @param {String} senderPeerId The Peer ID associated with the DataChannel connection.
* @param {ArrayBuffer|Blob|String} dataString The data transfer packet (chunk) received.
* @param {String} dataType The data transfer packet (chunk) data type received.
* [Rel: Skylink.DATA_TRANSFER_DATA_TYPE]
* @param {String} channelName The DataChannel connection ID associated with the transfer.
* @trigger dataTransferState
* @private
* @component DataTransfer
* @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
});
}
};
/**
* Starts a data transfer with Peers using the DataChannel connections with
* [Blob](https://developer.mozilla.org/en/docs/Web/API/Blob datas).
* - You can transfer files using the input [fileupload object](
* http://www.w3schools.com/jsref/dom_obj_fileupload.asp) and accessing the receiving
* files using [FileUpload files property](http://www.w3schools.com/jsref/prop_fileupload_files.asp).
* - The [File](https://developer.mozilla.org/en/docs/Web/API/File) object inherits from
* the Blob interface which is passable in this method as a Blob object.
* - The receiving Peers have the option to accept or reject the data transfer with
* acceptDataTransfer().
* - For Peers connecting from our mobile platforms
* (iOS / Android),
* the DataChannel connection channel type would be DATA_CHANNEL_TYPE.MESSAGING.

* For Peers connecting from the Web platform, the DataChannel connection channel type would be
* DATA_CHANNEL_TYPE.DATA.
* @method sendBlobData
* @param {Blob} data The Blob data object to transfer to Peer.
* @param {Number} [timeout=60] The waiting timeout in seconds that the DataChannel connection
* data transfer should wait before throwing an exception and terminating the data transfer.
* @param {String|Array} [targetPeerId] The array of targeted Peers to transfer the
* data object to. Alternatively, you may provide this parameter as a string to a specific
* targeted Peer to transfer the data object.
* @param {Function} [callback] The callback fired after all the data transfers is completed
* successfully or met with an exception. The callback signature is function (error, success).
* @param {JSON} callback.error The error object received in the callback.
* If received as null, it means that there is no errors.
* @param {String} [callback.error.state=null] Deprecated. The
* dataTransferState
* when the error has occurred. This only triggers for a single targeted Peer data transfer.
* @param {Object|String} [callback.error.error=null] Deprecated. The error received when the
* data transfer fails. This only triggers for single targeted Peer data transfer.
* @param {String} callback.error.transferId The transfer ID of the failed data transfer.
* @param {String} [callback.error.peerId=null] The single targeted Peer ID for the data transfer.
* This only triggers for single targeted Peer data transfer.
* @param {Array} callback.error.listOfPeers The list of Peer that the data transfer has been
* initiated with.
* @param {Boolean} callback.error.isPrivate The flag to indicate if the data transfer is a private
* transfer to the Peer directly and not broadcasted to all Peers.
* @param {JSON} callback.error.transferErrors The list of errors occurred based on per Peer
* basis.
* @param {Object|String} callback.error.transferErrors.(#peerId) The error that occurred when having
* a DataChannel connection data transfer with associated Peer.
* @param {JSON} callback.error.transferInfo The transfer data object information.
* @param {String} [callback.error.transferInfo.name=transferId] The transfer data object name.
* If there is no name based on the Blob given, the name would be the transfer ID.
* @param {Number} callback.error.transferInfo.size The transfer data size.
* @param {String} callback.error.transferInfo.transferId The data transfer ID.
* @param {String} callback.error.transferInfo.dataType The type of data transfer initiated.
* Available types are "dataURL" and "blob".
* @param {String} callback.error.transferInfo.timeout The waiting timeout in seconds that the DataChannel
* connection data transfer should wait before throwing an exception and terminating the data transfer.
* @param {Boolean} callback.error.transferInfo.isPrivate The flag to indicate if the data transfer is a private
* transfer to the Peer directly and not broadcasted to all Peers.
* @param {JSON} callback.success The success object received in the callback.
* If received as null, it means that there are errors.
* @param {String} [callback.success.state=null] Deprecated. The
* dataTransferState
* when the data transfer has been completed successfully.
* This only triggers for a single targeted Peer data transfer.
* @param {String} callback.success.transferId The transfer ID of the successful data transfer.
* @param {String} [callback.success.peerId=null] The single targeted Peer ID for the data transfer.
* This only triggers for single targeted Peer data transfer.
* @param {Array} callback.success.listOfPeers The list of Peer that the data transfer has been
* initiated with.
* @param {Boolean} callback.success.isPrivate The flag to indicate if the data transfer is a private
* transfer to the Peer directly and not broadcasted to all Peers.
* @param {JSON} callback.success.transferInfo The transfer data object information.
* @param {String} [callback.success.transferInfo.name=transferId] The transfer data object name.
* If there is no name based on the Blob given, the name would be the transfer ID.
* @param {Number} callback.success.transferInfo.size The transfer data size.
* @param {String} callback.success.transferInfo.transferId The data transfer ID.
* @param {String} callback.success.transferInfo.dataType The type of data transfer initiated.
* Available types are "dataURL" and "blob".
* @param {String} callback.success.transferInfo.timeout The waiting timeout in seconds that the DataChannel
* connection data transfer should wait before throwing an exception and terminating the data transfer.
* @param {Boolean} callback.success.transferInfo.isPrivate The flag to indicate if the data transfer is a private
* transfer to the Peer directly and not broadcasted to all Peers.
* @example
* // Example 1: Send file to all peers connected
* SkylinkDemo.sendBlobData(file, 67);
*
* // Example 2: Send file to individual peer
* SkylinkDemo.sendBlobData(blob, 87, targetPeerId);
*
* // Example 3: Send file with callback
* SkylinkDemo.sendBlobData(data,{
* name: data.name,
* size: data.size
* },function(error, success){
* if (error){
* console.error("Error happened. Could not send file", error);
* }
* else{
* console.info("Successfully uploaded file");
* }
* });
*
* @trigger incomingData, incomingDataRequest, dataTransferState, dataChannelState
* @since 0.5.5
* @component DataTransfer
* @for Skylink
*/
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);
};
/**
* Starts the actual data transfers with the array of Peers provided
* and based on the data transfer type to start the DataChannel connection data transfer.
* @method _startDataTransfer
* @param {Blob|String} data The transfer data object.
* @param {JSON} dataInfo The transfer data object information.
* @param {String} [dataInfo.name=transferId] The transfer data object name.
* If there is no name based on the Blob given, the name would be the transfer ID.
* @param {Number} dataInfo.size The transfer data size.
* @param {String} dataInfo.transferId The data transfer ID.
* @param {String} dataInfo.dataType The type of data transfer initiated.
* Available types are "dataURL" and "blob".
* @param {String} dataInfo.timeout The waiting timeout in seconds that the DataChannel
* connection data transfer should wait before throwing an exception and terminating the data transfer.
* @param {Boolean} dataInfo.isPrivate The flag to indicate if the data transfer is a private
* transfer to the Peer directly and not broadcasted to all Peers.
* @param {Array} [listOfPeers] The array of targeted Peer to transfer the
* data object to.
* @param {Function} [callback] The callback fired after all the data transfers is completed
* successfully or met with an exception. The callback signature is function (error, success).
* @param {JSON} callback.error The error object received in the callback.
* If received as null, it means that there is no errors.
* @param {String} [callback.error.state=null] Deprecated. The
* dataTransferState
* when the error has occurred. This only triggers for a single targeted Peer data transfer.
* @param {Object|String} [callback.error.error=null] Deprecated. The error received when the
* data transfer fails. This only triggers for single targeted Peer data transfer.
* @param {String} callback.error.transferId The transfer ID of the failed data transfer.
* @param {String} [callback.error.peerId=null] The single targeted Peer ID for the data transfer.
* This only triggers for single targeted Peer data transfer.
* @param {Array} callback.error.listOfPeers The list of Peer that the data transfer has been
* initiated with.
* @param {Boolean} callback.error.isPrivate The flag to indicate if the data transfer is a private
* transfer to the Peer directly and not broadcasted to all Peers.
* @param {JSON} callback.error.transferErrors The list of errors occurred based on per Peer
* basis.
* @param {Object|String} callback.error.transferErrors.(#peerId) The error that occurred when having
* a DataChannel connection data transfer with associated Peer.
* @param {JSON} callback.error.transferInfo The transfer data object information.
* @param {String} [callback.error.transferInfo.name=transferId] The transfer data object name.
* If there is no name based on the Blob given, the name would be the transfer ID.
* @param {Number} callback.error.transferInfo.size The transfer data size.
* @param {String} callback.error.transferInfo.transferId The data transfer ID.
* @param {String} callback.error.transferInfo.dataType The type of data transfer initiated.
* The received type would be "blob".
* @param {String} callback.error.transferInfo.timeout The waiting timeout in seconds that the DataChannel
* connection data transfer should wait before throwing an exception and terminating the data transfer.
* @param {Boolean} callback.error.transferInfo.isPrivate The flag to indicate if the data transfer is a private
* transfer to the Peer directly and not broadcasted to all Peers.
* @param {JSON} callback.success The success object received in the callback.
* If received as null, it means that there are errors.
* @param {String} [callback.success.state=null] Deprecated. The
* dataTransferState
* when the data transfer has been completed successfully.
* This only triggers for a single targeted Peer data transfer.
* @param {String} callback.success.transferId The transfer ID of the successful data transfer.
* @param {String} [callback.success.peerId=null] The single targeted Peer ID for the data transfer.
* This only triggers for single targeted Peer data transfer.
* @param {Array} callback.success.listOfPeers The list of Peer that the data transfer has been
* initiated with.
* @param {Boolean} callback.success.isPrivate The flag to indicate if the data transfer is a private
* transfer to the Peer directly and not broadcasted to all Peers.
* @param {JSON} callback.success.transferInfo The transfer data object information.
* @param {String} [callback.success.transferInfo.name=transferId] The transfer data object name.
* If there is no name based on the Blob given, the name would be the transfer ID.
* @param {Number} callback.success.transferInfo.size The transfer data size.
* @param {String} callback.success.transferInfo.transferId The data transfer ID.
* @param {String} callback.success.transferInfo.dataType The type of data transfer initiated.
* The received type would be "blob".
* @param {String} callback.success.transferInfo.timeout The waiting timeout in seconds that the DataChannel
* connection data transfer should wait before throwing an exception and terminating the data transfer.
* @param {Boolean} callback.success.transferInfo.isPrivate The flag to indicate if the data transfer is a private
* transfer to the Peer directly and not broadcasted to all Peers.
* @private
* @component DataTransfer
* @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);
}
};
/**
* Responds to a data transfer request by a Peer.
* @method respondBlobRequest
* @param {String} peerId The sender Peer ID.
* @param {String} transferId The data transfer ID of the data transfer request
* to accept or reject.
* @param {Boolean} [accept=false] The flag that indicates true as a response
* to accept the data transfer and false as a response to reject the
* data transfer request.
* @trigger dataTransferState, incomingDataRequest, incomingData
* @component DataTransfer
* @deprecated Use .acceptDataTransfer()
* @for Skylink
* @since 0.5.0
*/
Skylink.prototype.respondBlobRequest =
/**
* Responds to a data transfer request by a Peer.
* @method acceptDataTransfer
* @param {String} peerId The sender Peer ID.
* @param {String} transferId The data transfer ID of the data transfer request
* to accept or reject.
* @param {Boolean} [accept=false] The flag that indicates true as a response
* to accept the data transfer and false as a response to reject the
* data transfer request.
* @trigger dataTransferState, incomingDataRequest, incomingData
* @component DataTransfer
* @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];
}
};
/**
* Terminates a current data transfer with Peer.
* @method cancelBlobTransfer
* @param {String} peerId The Peer ID associated with the data transfer.
* @param {String} transferId The data transfer ID of the data transfer request
* to terminate the request.
* @trigger dataTransferState
* @component DataTransfer
* @deprecated Use .cancelDataTransfer()
* @for Skylink
* @since 0.5.7
*/
Skylink.prototype.cancelBlobTransfer =
/**
* Terminates a current data transfer with Peer.
* @method cancelDataTransfer
* @param {String} peerId The Peer ID associated with the data transfer.
* @param {String} transferId The data transfer ID of the data transfer request
* to terminate the request.
* @trigger dataTransferState
* @component DataTransfer
* @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
});
};
/**
* Send a message object or string using the DataChannel connection
* associated with the list of targeted Peers.
* - The maximum size for the message object would be16Kb.

* - To send a string length longer than 16kb, please considered
* to use {{#crossLink "Skylink/sendURLData:method"}}sendURLData(){{/crossLink}}
* to send longer strings (for that instance base64 binary strings are long).
* - To send message objects with platform signaling socket connection, see
* {{#crossLink "Skylink/sendMessage:method"}}sendMessage(){{/crossLink}}.
* @method sendP2PMessage
* @param {String|JSON} message The message object.
* @param {String|Array} [targetPeerId] The array of targeted Peers to
* transfer the message object to. Alternatively, you may provide this parameter
* as a string to a specific targeted Peer to transfer the message object.
* @example
* // Example 1: Send to all peers
* SkylinkDemo.sendP2PMessage("Hi there! This is from a DataChannel connection!"");
*
* // Example 2: Send to specific peer
* SkylinkDemo.sendP2PMessage("Hi there peer! This is from a DataChannel connection!", targetPeerId);
* @trigger incomingMessage
* @since 0.5.5
* @component DataTransfer
* @for Skylink
*/
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 && !isPrivate) {
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
});
}
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;
}
if (isPrivate || !self._hasMCU) {
if (self._hasMCU) {
log.log([peerId, null, useChannel, 'Sending private P2P message to peer']);
} else {
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);
};
/**
* Starts a [data URI](https://developer.mozilla.org/en-US/docs/Web/API/FileReader
* /readAsDataURL) transfer with Peers using the DataChannel connection.
* - The receiving Peers have the option to accept or reject the data transfer with
* acceptDataTransfer().
* - For Peers connecting from our mobile platforms
* (iOS / Android),
* the DataChannel connection channel type would be DATA_CHANNEL_TYPE.MESSAGING.

* For Peers connecting from the Web platform, the DataChannel connection channel type would be
* DATA_CHANNEL_TYPE.DATA.
* @method sendURLData
* @param {String} data The dataURL (base64 binary string) string to transfer to Peers.
* @param {Number} [timeout=60] The waiting timeout in seconds that the DataChannel connection
* data transfer should wait before throwing an exception and terminating the data transfer.
* @param {String|Array} [targetPeerId] The array of targeted Peers to transfer the
* data object to. Alternatively, you may provide this parameter as a string to a specific
* targeted Peer to transfer the data object.
* @param {Function} [callback] The callback fired after all the data transfers is completed
* successfully or met with an exception. The callback signature is function (error, success).
* @param {JSON} callback.error The error object received in the callback.
* If received as null, it means that there is no errors.
* @param {String} [callback.error.state=null] Deprecated. The
* dataTransferState
* when the error has occurred. This only triggers for a single targeted Peer data transfer.
* @param {Object|String} [callback.error.error=null] Deprecated. The error received when the
* data transfer fails. This only triggers for single targeted Peer data transfer.
* @param {String} callback.error.transferId The transfer ID of the failed data transfer.
* @param {String} [callback.error.peerId=null] The single targeted Peer ID for the data transfer.
* This only triggers for single targeted Peer data transfer.
* @param {Array} callback.error.listOfPeers The list of Peer that the data transfer has been
* initiated with.
* @param {Boolean} callback.error.isPrivate The flag to indicate if the data transfer is a private
* transfer to the Peer directly and not broadcasted to all Peers.
* @param {JSON} callback.error.transferErrors The list of errors occurred based on per Peer
* basis.
* @param {Object|String} callback.error.transferErrors.(#peerId) The error that occurred when having
* a DataChannel connection data transfer with associated Peer.
* @param {JSON} callback.error.transferInfo The transfer data object information.
* @param {String} [callback.error.transferInfo.name=transferId] The data transfer ID.
* @param {Number} callback.error.transferInfo.size The transfer data size.
* @param {String} callback.error.transferInfo.transferId The data transfer ID.
* @param {String} callback.error.transferInfo.dataType The type of data transfer initiated.
* The received type would be "dataURL".
* @param {String} callback.error.transferInfo.timeout The waiting timeout in seconds that the DataChannel
* connection data transfer should wait before throwing an exception and terminating the data transfer.
* @param {Boolean} callback.error.transferInfo.isPrivate The flag to indicate if the data transfer is a private
* transfer to the Peer directly and not broadcasted to all Peers.
* @param {JSON} callback.success The success object received in the callback.
* If received as null, it means that there are errors.
* @param {String} [callback.success.state=null] Deprecated. The
* dataTransferState
* when the data transfer has been completed successfully.
* This only triggers for a single targeted Peer data transfer.
* @param {String} callback.success.transferId The transfer ID of the successful data transfer.
* @param {String} [callback.success.peerId=null] The single targeted Peer ID for the data transfer.
* This only triggers for single targeted Peer data transfer.
* @param {Array} callback.success.listOfPeers The list of Peer that the data transfer has been
* initiated with.
* @param {Boolean} callback.success.isPrivate The flag to indicate if the data transfer is a private
* transfer to the Peer directly and not broadcasted to all Peers.
* @param {JSON} callback.success.transferInfo The transfer data object information.
* @param {String} [callback.success.transferInfo.name=transferId] The data transfer ID.
* @param {Number} callback.success.transferInfo.size The transfer data size.
* @param {String} callback.success.transferInfo.transferId The data transfer ID.
* @param {String} callback.success.transferInfo.dataType The type of data transfer initiated.
* The received type would be "dataURL".
* @param {String} callback.success.transferInfo.timeout The waiting timeout in seconds that the DataChannel
* connection data transfer should wait before throwing an exception and terminating the data transfer.
* @param {Boolean} callback.success.transferInfo.isPrivate The flag to indicate if the data transfer is a private
* transfer to the Peer directly and not broadcasted to all Peers.
* @example
*
* // Example 1: Send dataURL to all peers connected
* SkylinkDemo.sendURLData(dataURL, 67);
*
* // Example 2: Send dataURL to individual peer
* SkylinkDemo.sendURLData(dataURL, 87, targetPeerId);
*
* // Example 3: Send dataURL with callback
* SkylinkDemo.sendURLData(dataURL, 87, function(error, success){
* if (error){
* console.error("Error happened. Could not send dataURL", error);
* }
* else{
* console.info("Successfully sent dataURL");
* }
* });
*
* @trigger incomingData, incomingDataRequest, dataTransferState, dataChannelState
* @since 0.6.1
* @component DataTransfer
* @for Skylink
*/
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);
};


According to the paper “The allegory of the hawk and serpent”. The dragon portfolio consists of:
So, that’s the allocation I plan of using.
We Europeans have a different problem then the Americans. It’s easier to access hedge fund vehicles. But the regulations MiFID II and PRIIP makes it hard to buy North American ETFs. So, I will use different vehicles then the ones suggested in the paper.
Lets look at the various components.
I will split this “bucket” the following way:
I’m using global equities to get a broad exposure and Swedish equities since that’s where I’m based. If you are in another country you should probably replace the Swedish equity part. To implement this I’m using the following instruments:
Avanza Global place all its money in “AMUNDI INDEX MSCI WORLD I13SK” (should be this product https://www.amundietf.fr/professional/product/view/LU1681043599) So, you could get the same exposure by using that fund. My first draft had 25% US equities (and 50% global). But the global fund is mostly USA anyway so you get plenty of exposure with this allocation. If investment sentiment shifts from the US a global fund will probably capture this better as well.
This one is a bit tricky since European bonds barely pays anything. How should you divide this bucket up? The options in Sweden are pretty limited. The only developed country where you can get any yield is basically the US. So, I’m thinking about dividing it like this:
The funds I’ve found for this is:
I’m not very happy about paying a 0.91% fee to get exposure to US bonds that doesn’t pay that much more. But my options are very limited. I don’t want 100% exposure to Swedish (or other European) bonds either. One option is to put some money in “SEB Korträntefond C USD - Lux”. That fund has a fee of 0.5% and buys US notes (2-10 years). But as of this writing that would mean that the fee would be greater then the rate of the 2-5 year notes the fund buys. So, I’m sticking to those two for now.
This one is fairly easy. I’m going to divide it like this;
In the paper it is suggested to put everything in physical gold. But, for tax reasons and ease of rebalancing, 25% is going in an ETF. This gives me the following:
Finding a great gold ETF in Europe was harder then expected. But I’ve used the above fund for some time and hopefully they have all of the gold they say they have.
Now comes the interesting stuff. For a retail investor, getting information about commodity trend or long volatility funds is not easy. It usually requires subscriptions for 1000s of dollars and that you are an accredited investor. I have not managed to find the components of the fund in the paper (HFRX Systematic Macro CTA Index). HFR wanted me to sign a yearly subscription of $3750 to release that information.
But, I’ve managed to find the components of the “SG trend index”. That’s the 10 largest trend following strategy funds in the world. Not strictly commodities as I think the paper talks about. But most of the funds should be “market neutral” which I believe serves it purpose. However, I only found one of those funds that I easily could invest in. That is the Swedish fund “Lynx”.
In the paper, Chris suggests you could use global macro funds as well (but he couldn’t backtest that). I’m reading this as that you could put some global macro in this “market neutral” bucket as well. This gives me a few more options. I’ve found 4 funds that I’m going to use here:
Ideally I would like more funds here. I will keep my eyes open to diversify this bucket. But, this is what I have found now and I think it’s OK. As this is mostly Swedish funds you could probably have more (or fewer) alternatives depending on where you live.
You should be able to invest smaller amounts in all of these funds (but may have to pay a higher fee if the amount is small)
This is probably the most interesting “bucket”. In the paper funds in “Eurekahedge CBOE Long Volatility Hedge Fund Index” is suggested as alternatives. As before, very hard to find information. But here I managed to dig out the components from 2019 at least. 11 funds (but only 8 different companies). One of these funds looks readily available to European investors. That’s the fund “Amundi Fds Volatil Wld”. That fund has been around since 2007 and must be one of the longest standing actors in this space.
So, that seems like a great alternative. Another fund of the index is “Seeyond Volatility Strategy (NVOLACI LX Equity)” https://www.im.natixis.com/latam/funds/seeyond-equity-volatility-strategies/lu0935232610. From what I can find, it should be possible for European investors to invest with a minimum investment of €50 000. However I have not found any way for me in Sweden to do this. But maybe it’s easier in other countries. The same goes for the fund “Bréhat Class I” https://www.vivienne-investissement.com/EN/OurFunds.php?p=BI. It’s not in the long vol index (at least as of 2019), but it’s a long vol fund. Minimum investment in Brehat is €100 000, but haven’t found a way to invest from Sweden here either.
If you have $100 000 to invest in this bucket there’s a new “fund of funds” you could look into. It’s called “Mutiny Fund” https://mutinyfund.com/ and gives you exposure to funds like Logica and Artemis Capital. It is open to European investors. I’m going to try to invest myself. But there is some hurdles to get over for this to happen (taxes, forms to fill out etc). So, not sure if I will be successful.
My only other option is to invest everything in Amundis fund. It’s most likely a fine fund, but I’d like a bit more diversification then that. The limit for Mutiny is about what I plan to put in this bucket. So, the plan is to divide it like this:
I’ve tried to minimize the number of vehicles in the first three buckets and maximize them in the last two. It wasn’t easy, but I believe the end result is fairly close to the “Dragon portfolio”. As I am investing in Swedish Krona (SEK) there is some currency risk for some of the instruments. I’ve decided to not try to hedge that risk. The risk that I would mess something up trying to do that is probably greater then the currency risk.
The portfolio will then look like this:
Next, let’s backtest this portfolio and decide for a strategy on how to implement it: Backtest


We are linux users from Delhi NCR region. We mostly include students and professionals who uses linux. We believe in endorsing OSS/FOSS and linux use and development of the same. As of now we organise monthly meetup. We respect all technologies from Perl to Python. If you want to volunteer for ILUGD you can join us in any of our social channels.


Jermaine Jenas brands Arsenal's season "disastrous" and makes top four prediction
The former Tottenham midfielder has forecast who he believes will qualify for next season's Champions League
by Darren Wells, https://www.mirror.co.uk/authors/darren-wells/Jermaine Jenas has laid into Arsenal's start to the season and believes neither they nor former club Tottenham will make the top four.
The Gunners are sat just outside the Champions League qualification spots despite winning just one of their last five matches.
Liverpool and Man City lead the race as they look set to battle it out for the Premier League title, while Leicester and Chelsea occupy the other two spots.
Asked who he thought would secure a top four place, Jenas said: "Originally I was Spurs and Arsenal [along with Liverpool and Man City].
"But I think Arsenal’s season has been disastrous. Although they’re much higher in the table than Spurs, it’s just been more mayhem at that football club.
"And even the manager now seems to be under a lot of pressure, so I don’t think they’ll make it.
"I’ll replace them with Chelsea, because I think what Frank’s [Lampard] got going there is solid. The players are top drawer."
Tottenham meanwhile are languishing in mid-table, and face a battle to address their slump and catch their rivals.
They've picked up just three wins out of a possible 11 matches so far this season as problems behind the scenes continue.
Quizzed on whether he thought his old club would miss out, Jenas said: "I do. I never thought they would, but I thought they’d have picked up more results by now.
"When I look at them, they're not a team that’s going to make top four. They’re not playing that way.
"I think in that fourth spot, it’s going to be tough to shake Leicester now. With the free weeks that they have, only the cups to come into it.

"But I think that lead they’ve developed, they might be able to hold onto it."


About
Spicy jalapeno bacon ipsum dolor amet flank jowl porchetta hamburger corned beef cupim turkey venison kielbasa cow. Tail sirloin landjaeger shankle, tenderloin jerky turkey tri-tip corned beef frankfurter jowl ham hock. Picanha tongue pork loin, ground round bacon tri-tip meatball cow kevin sirloin shankle turkey boudin spare ribs. Boudin landjaeger spare ribs jowl short loin shankle ribeye tail capicola jerky ball tip fatback.
Our Locations
- USA
- New York
- Columbus
- Austin
- Philly
- World
Upcoming Classes
| Location |
Topic |
| New York |
Javascript & jQuery |
| San Francisco |
HTML & CSS |
Sign Up!


Scrum has no project manager role. It does however defines activities and artifacts to conduct
project management.
Click the boxes to flip between PMBOK and
Scrum project management activities.
Project Human Resource Management
- Acquire Project Team
- Develop Project Team
- Manage Project Team
- Plan Human Resource Management
- Stable and fixed development teams
Project Scope Management
- Collect Requirements
- Control Scope
- Create WBS
- Define Scope
- Plan Scope Management
- Validate Scope
- Product Backlog
- Sprint Backlog
- Backlog refinement
Project Procurement Management
- Close Procurements
- Conduct Procurements
- Control Procurements
- Plan Procurement Management
- Stable and fixed development teams
Project Time Management
- Control Schedule
- Define Activities
- Develop Schedule
- Estimate Activity Duration
- Estimate Activity Resources
- Plan Schedule Management
- Sequence Activity
- Sprint
- Planning poker
- Product Backlog burn down
Project Cost Management
- Control Costs
- Determine Budget
- Estimate Costs
- Plan Cost Management
- Cost per sprint
- Product Backlog burn down
Project Integration Management
- Close Project or Phase
- Develop Project Charter
- Develop Project Management Plan
- Direct and Manage Project Work
- Monitor and Control Project Work
- Perform Integrated Change Control
- Definition of Done
- Sprint Review
Project Quality Management
- Control Quality
- Perform Quality Assurance
- Plan Quality Management
Project Communications Management
- Control Communications
- Manage Communications
- Plan Communications Management
Project Risk Management
- Control Risks
- Identify Risks
- Perform Qualitative Risk Analysis
- Perform Quantitative Risk Analysis
- Plan Risk Management
- Plan Risk Responses
- Impediments
- Retrospective
- Burn downs


Welcome to Montenegro
Montenegro is small yet beautiful country situated in Southeasten Europe. Due to it's small size it is often overlooked as a tourist destination and much of it's landscape remains unblemished by hotels and tourist hotspots. Kotor is a medieval old town located on a scenic bay on the east coast of the country. Kotor is a UNESCO World Heritage site, built in the 12th to 14th centuries and has lots of medieval architecture and museums.
A bit of a did you know section?
- Lake Skadar is the largest in the Balkan region, with two-thirds lying in Montenegro and the remainder in Albania.
- In terms of volume, Tara Canyon in Montenegro is Europe’s biggest canyon. It is 82km (51mi) in length, 1,300m (4,300ft) deep and 914m (3,000ft) at its widest point.
- Crvena Stijena by the village Petrovici near Niksic is one of Montenegro’s oldest cultural and historical monuments. The cave is unique in the world because of the numerous cultural artifacts it contains.


Muiasar Al Ani
Manager
Manager
John Wick
Engineer
Engineer
James Bond
Engineer
Engineer
Arya Stark
Engineer
Engineer
Jon Snow
Engineer
Engineer
Jonny English
Intern
Intern


Dark Sidebar
Lifetime Sales
3,487
Total Sales
814
Open Campaign
5,324
Daily Sales
Statistics
1,284
Total Sales
7,841
Open Campaign
Income Amounts
2,845
Total Sales
6,487
Open Campaign
201
Daily Sales
Projects
| Project Name | Start Date | Due Date | Team | Status | Clients |
|---|---|---|---|---|---|
| App design and development | Jan 03, 2015 | Oct 12, 2018 | Work in Progress | Halette Boivin | |
| Coffee detail page - Main Page | Sep 21, 2016 | May 05, 2018 | Pending | Durandana Jolicoeur | |
| Poster illustation design | Mar 08, 2018 | Sep 22, 2018 | Completed | Lucas Sabourin | |
| Drinking bottle graphics | Oct 10, 2017 | May 07, 2018 | Work in Progress | Donatien Brunelle | |
| Landing page design - Home | Coming Soon | May 25, 2021 | Coming Soon | Karel Auberjo |


{% extends "base.html" %}
{% load static %}
{% block header %}
{% endblock %}
{% block content %}
Welcome to the Pythonic website, analyze your projects and learn to write idiomatic Python codeAnalyze your Python projects
Select your python repositories
Select
Name
Forked
Main language
Link
Are there missing repositories you contributed to?


The Kailash mountain in the Tibetean Himalayas stands at 6,638m and is considered to be the source of some of the longest Asian rivers. The mountain is considered holy by Hindus, Buddhists and Jains. I don't why I chose it for this blog, perhaps because I've always wanted to seet it. Learning web development is somewhat like conquering a mountain. I've got my own Kailash to climb it seems.


turbo::MusicStream Class Reference
Inheritance diagram for turbo::MusicStream:
Collaboration diagram for turbo::MusicStream:
Public Member Functions | |
| MusicStream (Path *path) | |
| void | play () override |
| void | pause () override |
| void | reset () override |
| bool | is_playing () const override |
Protected Attributes | |
| ALLEGRO_AUDIO_STREAM * | stream = nullptr |
The documentation for this class was generated from the following files:
- include/turbo/sound/MusicStream.hpp
- src/sound/MusicStream.cpp


GUI Team
Ashley Pancho
Ashley is a 2nd year Computer Science major at Southern Methodist University in Dallas, Texas. She is from Long Island, New York.
Travis Siems
Travis is a 2nd year Computer Science major at Southern Methodist University in Dallas, Texas. He is from Dallas, Texas.
Emely Villeda-Principe
Emely is a 3rd year Creative Computation major at Southern Methodist University in Dallas, Texas. She is from Garland, Texas.
Database Team
Nigel Saurino
"I once had aspirations, then I took data structures..."
Nigel is a 4th year Finance and Math double major with Computer Science minor at Southern Methodist University in Dallas, Texas. He is from Tulsa, Oklahoma.
Elena Sharp
Elena is a 2nd year Computer Science and Math double major at Southern Methodist University in Dallas, Texas. She is from Aurora, Colorado. She looks like she can do yoga but she can't even touch her toes.
Emily Stephanian
"I'm a Virgo, what's your sign?"
Emily is a 2nd year Computer Science student at Southern Methodist University in Dallas, Texas. She is from Terre Haute, Indiana. She used to be able to touch her nose with her tongue, but she can't anymore after breaking her nose.


--- layout: pagefullwidth title: "Journals" logo: img/logos/oj_logo.png description: "The Open Journals" header-img: "img/home-bg.jpg" ordernumber: 1 ---