Skip to content

Instantly share code, notes, and snippets.

@EricDavies
Created June 9, 2015 05:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save EricDavies/74ab35b7c3819b8ca4db to your computer and use it in GitHub Desktop.
Save EricDavies/74ab35b7c3819b8ca4db to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <!--skip-->
<title>EasyRTC Demo: Data Channel File-sharing</title>
<link rel="stylesheet" type="text/css" href="css/landing.css" />
<link rel="stylesheet" type="text/css" href="/easyrtc/easyrtc.css" />
<!--hide-->
<!-- Prettify Code -->
<script type="text/javascript" src="js/prettify/prettify.js"></script>
<link rel="stylesheet" type="text/css" href="js/prettify/prettify.css" />
<script type="text/javascript" src="js/prettify/loadAndFilter.js"></script>
<script type="text/javascript" src="js/prettify/jquery-1.9.1.min.js"></script>
<!--show-->
<!-- Assumes global locations for socket.io.js and easyrtc.js -->
<script src="/socket.io/socket.io.js"></script>
<script type="text/javascript" src="/easyrtc/easyrtc.js"></script>
<script type="text/javascript" src="js/easyrtc_ft2.js"></script>
<script type="text/javascript" src="js/demo_data_channel_filesharing2.js"></script>
<!--hide-->
<!-- Styles used within the demo -->
<style type="text/css">
.alert img {
float:left;
padding-right: 10px;
}
.peerblock {
width: 800px;
border-radius:2px;
border:1px solid gray;
}
.receiveBlock {
width: 500px;
margin:10px;
display: inline-block;
}
.dragndrop {
display: inline-block;
padding:2em;
margin: 2px;
border:2px solid black;
border-radius: 5px;
}
.dragndropStatus {
display: inline-block;
}
.connecting {
background-color: yellow;
}
.usesSockets {
background-color: pink;
}
.notConnected {
visibility:hidden;
}
.connected {
background-color: #e0ffe0;
}
.notConnected:hover {
margin: 1px;
border-width: 3px;
}
.connected:hover {
margin: 1px;
border-width:3px;
}
.connectButton {
padding:2em;
}
</style>
<!--show-->
</head>
<body onload="connect()">
<!--hide-->
<div id="container">
<div id="header">
<a href="index.html"><img id="logo_easyrtc" src="images/easyrtc_logo.png" alt="easyrtc" style="" /></a>
</div>
<div id="menu"><a class="menu_link" href="index.html"><div class="menu_item">Local Demos</div></a><a class="menu_link" href="https://github.com/priologic/easyrtc/tree/master/docs"><div class="menu_item">Documentation</div></a><a class="menu_link" href="https://groups.google.com/forum/#!forum/easyrtc"><div class="menu_item">Support: Discussion Group</div></a><a class="menu_link" href="http://www.easyrtc.com/"><div class="menu_item">EasyRTC.com</div></a></div>
<div id="main">
<!-- Main Content -->
<h1>EasyRTC Demo: Data Channel File-sharing</h1>
<p>This application demonstrates file sharing using the easyrtc.sendData method.
It should connect to the server upon start up, and display drag-and-drop areas for other peers.</p>
<p>To use it, connect to a peer then drop a file into the drag-and-drop area for that peer. The peer should receive it.
</p>
<p class="alert"><img alt="Warn" height="32" width="32" src="images/br_status_warn.png" />
This demo requires reliable data channels which means it needs Firefox or Chrome (version 32+).
In the absence of reliable data channels it will fallback to using websockets to transfer a file.
Notice that it uses easyrtc_ft.js as well as easyrtc.js.
</p>
<hr />
<h2>The Demo</h2>
<!--show-->
<div id="iam">Obtaining ID...</div><BR />
<div id="peerZone">
</div>
<!--hide-->
<hr />
<h2>The Code</h2>
<h3>HTML</h3>
<pre id="prettyHtml" class="prettyprint linenums:1">
</pre>
<h3>JavaScript</h3>
<p>The contents of demo_instant_messaging.js:</p>
<pre id="prettyJS" class="prettyprint linenums:1">
</pre>
<!-- Runs the SyntaxHighlighter -->
<script type="text/javascript">
loadAndFilter(window.location.href, "prettyHtml",2 );
loadAndFilter(getHelperPath("js"), "prettyJS", 2);
</script>
<!-- End Main Content -->
</div>
<div id="footer">
<a href="http://www.priologic.com/?from=landing"><img id="logo_priologic" src="images/priologic_logo_white.png" height="40" width="150" alt="Created By Priologic Software Inc." /></a>
<a href="http://www.easyrtc.com/?from=landing"><img id="logo_pb_easyrtc" src="/easyrtc/img/powered_by_easyrtc.png" height="60" width="200" alt="Powered By EasyRTC" /></a>
<p><em>&copy; 2015 - Priologic Software Inc., All Rights Reserved.</em></p>
</div>
</div>
<!--show-->
</body>
</html>
//
//Copyright (c) 2014, Priologic Software Inc.
//All rights reserved.
//
//Redistribution and use in source and binary forms, with or without
//modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
//AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
//IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
//ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
//LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
//CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
//SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
//INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
//CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
//ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
//POSSIBILITY OF SUCH DAMAGE.
//
var selfEasyrtcid = "";
var peers = {};
function buildPeerBlockName(easyrtcid) {
return "peerzone_" + easyrtcid;
}
function buildDragNDropName(easyrtcid) {
return "dragndrop_" + easyrtcid;
}
function buildReceiveAreaName(easyrtcid) {
return "receivearea_" + easyrtcid;
}
function connect() {
var otherClientsDiv = document.getElementById('otherClients');
easyrtc.enableDataChannels(true);
easyrtc.enableVideo(false);
easyrtc.enableAudio(false);
easyrtc.setRoomOccupantListener(convertListToButtons);
easyrtc.setAcceptChecker(function(easyrtcid, responsefn) {
responsefn(true);
document.getElementById("connectbutton_" + easyrtcid).style.visibility = "hidden";
});
easyrtc.setDataChannelOpenListener(function(easyrtcid, usesPeer) {
var obj = document.getElementById(buildDragNDropName(easyrtcid));
if (!obj) {
console.log("no such object ");
}
jQuery(obj).addClass("connected");
jQuery(obj).removeClass("notConnected");
});
easyrtc.setDataChannelCloseListener(function(easyrtcid) {
jQuery(buildDragNDropName(easyrtcid)).addClass("notConnected");
jQuery(buildDragNDropName(easyrtcid)).removeClass("connected");
});
easyrtc.connect("easyrtc.dataFileTransfer", loginSuccess, loginFailure);
}
function removeIfPresent(parent, childname) {
var item = document.getElementById(childname);
if (item) {
parent.removeChild(item);
}
else {
console.log("didn't see item " + childname + " for delete eh");
}
}
function convertListToButtons(roomName, occupants, isPrimary) {
var peerZone = document.getElementById('peerZone');
for (var oldPeer in peers) {
if (!occupants[oldPeer]) {
removeIfPresent(peerZone, buildPeerBlockName(oldPeer));
delete peers[oldPeer];
}
}
function buildDropDiv(easyrtcid) {
var statusDiv = document.createElement("div");
statusDiv.className = "dragndropStatus";
var dropArea = document.createElement("div");
var connectButton = document.createElement("button");
connectButton.appendChild(document.createTextNode("Connect"));
connectButton.className = "connectButton";
connectButton.id = "connectbutton_" + easyrtcid;
connectButton.onclick = function() {
statusDiv.innerHTML = "Waiting for connection to be established";
easyrtc.call(easyrtcid,
function(caller, mediatype) {
statusDiv.innerHTML = "Connection established";
dropArea.className = "dragndrop connected";
connectButton.style.visibility = "hidden";
},
function(errorCode, errorText) {
dropArea.className = "dragndrop connected";
statusDiv.innerHTML = "Connection failed";
connectButton.style.visibility = "hidden";
noDCs[easyrtcid] = true;
},
function wasAccepted(yup) {
}
);
}
dropArea.id = buildDragNDropName(easyrtcid);
dropArea.className = "dragndrop notConnected";
dropArea.innerHTML = "File drop area";
function updateStatusDiv(state) {
switch (state.status) {
case "waiting":
statusDiv.innerHTML = "waiting for other party<br\>to accept transmission";
break;
case "started_file":
statusDiv.innerHTML = "started file: " + state.name;
case "working":
statusDiv.innerHTML = state.name + ":" + state.position + "/" + state.size + "(" + state.numFiles + " files)";
break;
case "rejected":
statusDiv.innerHTML = "cancelled";
setTimeout(function() {
statusDiv.innerHTML = "";
}, 2000);
break;
case "done":
statusDiv.innerHTML = "done";
setTimeout(function() {
statusDiv.innerHTML = "";
}, 3000);
break;
}
return true;
}
var noDCs = {}; // which users don't support data channels
var fileSender = null;
function filesHandler(files) {
// if we haven't eastablished a connection to the other party yet, do so now,
// and on completion, send the files. Otherwise send the files now.
var timer = null;
if (easyrtc.getConnectStatus(easyrtcid) === easyrtc.NOT_CONNECTED && noDCs[easyrtcid] === undefined) {
//
// calls between firefrox and chrome ( version 30) have problems one way if you
// use data channels.
//
}
else if (easyrtc.getConnectStatus(easyrtcid) === easyrtc.IS_CONNECTED || noDCs[easyrtcid]) {
if (!fileSender) {
fileSender = easyrtc_ft.buildFileSender(easyrtcid, updateStatusDiv);
}
var magicDataOut = {"LeprachaunwearGreen": 33 };
fileSender(files, true /* assume binary */, magicDataOut);
}
else {
easyrtc.showError("user-error", "Wait for the connection to complete before adding more files!");
}
}
easyrtc_ft.buildDragNDropRegion(dropArea, filesHandler);
var container = document.createElement("div");
container.appendChild(connectButton);
container.appendChild(dropArea);
container.appendChild(statusDiv);
return container;
}
function buildReceiveDiv(i) {
var div = document.createElement("div");
div.id = buildReceiveAreaName(i);
div.className = "receiveBlock";
div.style.display = "none";
return div;
}
for (var easyrtcid in occupants) {
if (!peers[easyrtcid]) {
var peerBlock = document.createElement("div");
peerBlock.id = buildPeerBlockName(easyrtcid);
peerBlock.className = "peerblock";
peerBlock.appendChild(document.createTextNode(" For peer " + easyrtcid));
peerBlock.appendChild(document.createElement("br"));
peerBlock.appendChild(buildDropDiv(easyrtcid));
peerBlock.appendChild(buildReceiveDiv(easyrtcid));
peerZone.appendChild(peerBlock);
peers[easyrtcid] = true;
}
}
}
function acceptRejectCB(otherGuy, fileNameList, wasAccepted, magicDataIn) {
console.log( "received magic data+ " + JSON.stringify(magicDataIn));
var receiveBlock = document.getElementById(buildReceiveAreaName(otherGuy));
jQuery(receiveBlock).empty();
receiveBlock.style.display = "inline-block";
//
// list the files being offered
//
receiveBlock.appendChild(document.createTextNode("Files offered"));
receiveBlock.appendChild(document.createElement("br"));
for (var i = 0; i < fileNameList.length; i++) {
receiveBlock.appendChild(
document.createTextNode(" " + fileNameList[i].name + "(" + fileNameList[i].size + " bytes)"));
receiveBlock.appendChild(document.createElement("br"));
}
//
// provide accept/reject buttons
//
var button = document.createElement("button");
button.appendChild(document.createTextNode("Accept"));
button.onclick = function() {
jQuery(receiveBlock).empty();
wasAccepted(true);
};
receiveBlock.appendChild(button);
button = document.createElement("button");
button.appendChild(document.createTextNode("Reject"));
button.onclick = function() {
wasAccepted(false);
receiveBlock.style.display = "none";
};
receiveBlock.appendChild(button);
}
function receiveStatusCB(otherGuy, msg) {
var receiveBlock = document.getElementById(buildReceiveAreaName(otherGuy));
if( !receiveBlock) return;
switch (msg.status) {
case "started":
break;
case "eof":
receiveBlock.innerHTML = "Finished file";
break;
case "done":
receiveBlock.innerHTML = "Stopped because " +msg.reason;
setTimeout(function() {
receiveBlock.style.display = "none";
}, 1000);
break;
case "started_file":
receiveBlock.innerHTML = "Beginning receive of " + msg.name;
break;
case "progress":
receiveBlock.innerHTML = msg.name + " " + msg.received + "/" + msg.size;
break;
default:
console.log("strange file receive cb message = ", JSON.stringify(msg));
}
return true;
}
function blobAcceptor(otherGuy, blob, filename) {
easyrtc_ft.saveAs(blob, filename);
}
function loginSuccess(easyrtcid) {
selfEasyrtcid = easyrtcid;
document.getElementById("iam").innerHTML = "I am " + easyrtcid;
easyrtc_ft.buildFileReceiver(acceptRejectCB, blobAcceptor, receiveStatusCB);
}
function loginFailure(errorCode, message) {
easyrtc.showError(errorCode, message);
}
/** @class
*@version 2.0.0-beta
*<p>
* Provides support file and data transfer support to easyrtc.
* </p>
*<p>
*copyright Copyright (c) 2015, Priologic Software Inc.
*All rights reserved.</p>
*
*<p>
*Redistribution and use in source and binary forms, with or without
*modification, are permitted provided that the following conditions are met:
*</p>
* <ul>
* <li> Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer. </li>
* <li> Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution. </li>
*</ul>
*<p>
*THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
*AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
*IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
*ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
*LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
*CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
*SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
*INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
*CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
*ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
*POSSIBILITY OF SUCH DAMAGE.
*</p>
*/
var easyrtc_ft = {};
/**
* Establish an area as a drag-n-drop drop site for files.
* @param {DOMString} droptargetName - the id of the drag-and-drop site or the actual DOM object.
* @param {Function} filesHandler - function that accepts an array of File's.
*/
easyrtc_ft.buildDragNDropRegion = function(droptargetName, filesHandler) {
var droptarget;
if (typeof droptargetName === 'string') {
droptarget = document.getElementById(droptargetName);
if (!droptarget) {
alert("Developer error: attempt to call BuildFileSender on unknown object " + droptargetName);
throw("unknown object " + droptargetName);
}
}
else {
droptarget = droptargetName;
}
function ignore(e) {
e.stopPropagation();
e.preventDefault();
return false;
}
function drageventcancel(e) {
if (e.preventDefault)
e.preventDefault(); // required by FF + Safari
e.dataTransfer.dropEffect = 'copy'; // tells the browser what drop effect is allowed here
return false; // required by IE
}
function dropHandler(e) {
removeClass(droptarget, dropCueClass);
var dt = e.dataTransfer;
var files = dt.files;
if (dt.files.length > 0) {
try {
filesHandler(files);
} catch (errorEvent) {
console.log("dragndrop errorEvent", errorEvent);
}
}
return ignore(e);
}
var dropCueClass = "easyrtcfiledrop";
function dragEnterHandler(e) {
addClass(droptarget, dropCueClass);
return drageventcancel(e);
}
function dragLeaveHandler(e) {
removeClass(droptarget, dropCueClass);
return drageventcancel(e);
}
var addEvent = (function() {
if (document.addEventListener) {
return function(el, type, fn) {
if (el && el.nodeName || el === window) {
el.addEventListener(type, fn, false);
} else if (el && el.length) {
for (var i = 0; i < el.length; i++) {
addEvent(el[i], type, fn);
}
}
};
} else {
return function(el, type, fn) {
if (el && el.nodeName || el === window) {
el.attachEvent('on' + type, function() {
return fn.call(el, window.event);
});
} else if (el && el.length) {
for (var i = 0; i < el.length; i++) {
addEvent(el[i], type, fn);
}
}
};
}
})();
droptarget.ondrop = dropHandler;
droptarget.ondragenter = dragEnterHandler;
droptarget.ondragleave = dragLeaveHandler;
droptarget.ondragover = drageventcancel;
function addClass(target, classname) {
if (target.className) {
if (target.className.indexOf(classname, 0) >= 0) {
return;
}
else {
target.className = target.className + " " + classname;
}
}
else {
target.className = classname;
}
target.className = target.className.replace(" ", " ");
}
function removeClass(target, classname) {
if (!target.className) {
return;
}
target.className = target.className.replace(classname, "").replace(" ", " ");
}
};
/**
* Builds a function that can be used to send a group of files to a peer.
* @param {String} destUser easyrtcid of the person being sent to.
* @param {Function} progressListener - if provided, is called with the following objects:
* {status:"waiting"} // once a file offer has been sent but not accepted or rejected yet
* {status:"started_file", name: filename}
* {status:"working", name:filename, position:position_in_file, size:size_of_current_file, numFiles:number_of_files_left}
* {status:"cancelled"} // if the remote user cancels the sending
* {status:"done"} // when the file is done
* the progressListener should always return true for normal operation, false to cancel a filetransfer.
* @return {Function} an object that accepts an array of File (the Files to be sent), and a boolean
* argument that is true if the files are binary, false if they are text.
* It is safe to treat all files as binary, it will just require more bandwidth.
*/
easyrtc_ft.buildFileSender = function(destUser, progressListener) {
var droptarget;
var seq = 0;
var positionAcked = 0;
var filePosition = 0;
var filesOffered = [];
var filesBeingSent = [];
var sendStarted = false;
var curFile = null;
var curFileSize;
var filesAreBinary;
var maxChunkSize = 10 * 1024;
var waitingForAck = false;
var ackThreshold = 100 * 1024; // send is allowed to be 150KB ahead of receiver
var filesWaiting = [];
var haveFilesWaiting = false;
if (!progressListener) {
progressListener = function() {
return true;
};
}
var roomOccupantListener = function(eventType, eventData) {
var roomName;
var foundUser = false;
for (roomName in eventData) {
if (eventData[roomName][destUser]) {
foundUser = true;
}
}
if (!foundUser) {
easyrtc.removeEventListener("roomOccupant", roomOccupantListener);
if (filesBeingSent.length > 0 || filesOffered.length > 0) {
progressListener({status: "cancelled"});
}
}
};
easyrtc.addEventListener("roomOccupant", roomOccupantListener);
//
// if a file offer is rejected, we delete references to it.
//
function fileOfferRejected(sender, msgType, msgData, targeting) {
if (!msgData.seq)
return;
delete filesOffered[msgData.seq];
progressListener({status: "rejected"});
filesOffered.length = 0;
sendFilesWaiting();
}
//
// if a file offer is accepted, initiate sending of files.
//
function fileOfferAccepted(sender, msgType, msgData, targeting) {
if (!msgData.seq || !filesOffered[msgData.seq])
return;
var alreadySending = filesBeingSent.length > 0;
for (var i = 0; i < filesOffered[msgData.seq].length; i++) {
filesBeingSent.push(filesOffered[msgData.seq][i]);
}
delete filesOffered[msgData.seq];
if (!alreadySending) {
filePosition = 0;
sendChunk(); // this starts the file reading
}
}
function fileCancelReceived(sender, msgType, msgData, targeting) {
filesBeingSent.empty();
progressListener({status: "cancelled"});
filesOffered.length = 0;
filesBeingSent.length = 0;
sendStarted = false;
sendFilesWaiting();
}
function packageAckReceived(sender, msgType, msgData) {
positionAcked = msgData.positionAck;
if (waitingForAck && filePosition < positionAcked + ackThreshold) {
waitingForAck = false;
sendChunk();
}
}
easyrtc.setPeerListener(fileOfferRejected, "filesReject", destUser);
easyrtc.setPeerListener(fileOfferAccepted, "filesAccept", destUser);
easyrtc.setPeerListener(fileCancelReceived, "filesCancel", destUser);
easyrtc.setPeerListener(packageAckReceived, "filesAck", destUser);
var outseq = 0;
function sendChunk() {
if (!curFile) {
if (filesBeingSent.length === 0) {
outseq = 0;
easyrtc.sendData(destUser, "filesChunk", {done: "all"});
filesOffered.length = 0;
progressListener({status: "done"});
sendFilesWaiting();
return;
}
else {
curFile = filesBeingSent.shift();
progressListener({status: "started_file", name: curFile.name});
curFileSize = curFile.size;
positionAcked = 0;
waitingForAck = false;
easyrtc.sendData(destUser, "filesChunk", {name: curFile.name, type: curFile.type, outseq: outseq, size: curFile.size});
outseq++;
}
}
var amountToRead = Math.min(maxChunkSize, curFileSize - filePosition);
if (!progressListener({status: "working", name: curFile.name, position: filePosition, size: curFileSize, numFiles: filesBeingSent.length + 1})) {
filesOffered.length = 0;
filePosition = 0;
easyrtc.sendData(destUser, "filesChunk", {done: "cancelled"});
sendFilesWaiting();
return;
}
var nextLocation = filePosition + amountToRead;
var blobSlice = curFile.slice(filePosition, nextLocation);
var reader = new FileReader();
reader.onloadend = function(evt) {
if (evt.target.readyState === FileReader.DONE) { // DONE == 2
var binaryString = evt.target.result;
var maxchar = 32, minchar = 32;
for (var pp = 0; pp < binaryString.length; pp++) {
var oneChar = binaryString.charCodeAt(pp);
maxchar = Math.max(maxchar, oneChar);
minchar = Math.min(minchar, oneChar);
}
var maxPacketSize = 400; // size in bytes
for (var pos = 0; pos < binaryString.length; pos += maxPacketSize) {
var packetLen = Math.min(maxPacketSize, amountToRead - pos);
var packetData = binaryString.substring(pos, pos + packetLen);
var packetObject = {outseq: outseq};
if (filesAreBinary) {
packetObject.data64 = btoa(packetData);
}
else {
packetObject.datatxt = packetData;
}
easyrtc.sendData(destUser, "filesChunk", packetObject);
outseq++;
}
if (nextLocation >= curFileSize) {
easyrtc.sendData(destUser, "filesChunk", {done: "file"});
}
if (filePosition < positionAcked + ackThreshold) {
sendChunk();
}
else {
waitingForAck = true;
}
}
};
reader.readAsBinaryString(blobSlice);
filePosition = nextLocation;
// advance to the next file if we've read all of this file
if (nextLocation >= curFileSize) {
curFile = null;
filePosition = 0;
}
}
function sendFilesWaiting() {
haveFilesWaiting = false;
if (filesWaiting.length > 0) {
setTimeout(function() {
var fileset = filesWaiting.shift();
sendFilesOffer(fileset.files, fileset.areBinary, fileset.userData);
}, 240);
}
}
function sendFilesOffer(files, areBinary, userData) {
if (haveFilesWaiting) {
filesWaiting.push({files: files, areBinary: areBinary, userData:userData});
}
else {
haveFilesWaiting = true;
filesAreBinary = areBinary;
progressListener({status: "waiting"});
var fileNameList = [];
for (var i = 0; i < files.length; i++) {
fileNameList[i] = {name: files[i].name, size: files[i].size};
}
seq++;
filesOffered[seq] = files;
easyrtc.sendDataWS(destUser, "filesOffer", {seq: seq, fileNameList: fileNameList, userData:userData});
}
}
return sendFilesOffer;
};
/**
* Enable datachannel based file receiving. The received blobs get passed to the statusCB in the 'eof' typed message.
* @param {Function(otherGuy,fileNameList, wasAccepted} acceptRejectCB - this function is called when another peer
* (otherGuy) offers to send you a list of files. this function should call it's wasAccepted function with true to
* allow those files to be sent, or false to disallow them.
* @param {Function} blobAcceptor - this function is called three arguments arguments: the suppliers easyrtcid, a blob and a filename. It is responsible for
* saving the blob to the file, usually using easyrtc_ft.saveAs.
* @param {type} statusCB - this function is called with the current state of file receiving. It is passed two arguments:
* otherGuy - the easyrtcid of the person sending the files. *
* msg - one of the following structures:
* {status:"done", reason:"accept_failed"}
* {status:"done", reason:"success"}
* {status:"done", reason:"cancelled"}
* {status:"eof"},
* {status:"started_file, name:"filename"}
* {status:"progress", name:filename,
* received:received_size_in_bytes,
* size:file_size_in_bytes }
* @example
*
* easyrtc_ft(
* function(otherGuy, filenamelist, wasAccepted) { wasAccepted(true);},
* function(otherGuy, blob, filename) { easyrtc_ft(blob, filename);},
* function(otherGuy, status) { console.log("status:" + JSON.stringify(status))}
* );
*/
easyrtc_ft.buildFileReceiver = function(acceptRejectCB, blobAcceptor, statusCB) {
var userStreams = {};
var ackThreshold = 10000; // receiver is allowed to be 10KB behind of sender
var positionAcked = 0;
var roomOccupantListener = function(eventType, eventData) {
var user;
var foundUser;
var roomName;
for (destUser in userStreams) {
foundUser = false;
for (roomName in eventData) {
if (eventData[roomName][destUser]) {
foundUser = true;
}
}
if (!foundUser) {
easyrtc.removeEventListener("roomOccupant", roomOccupantListener);
statusCB(destUser, {status: "done", reason: "cancelled"});
delete userStreams[destUser];
}
}
};
easyrtc.addEventListener("roomOccupant", roomOccupantListener);
function fileOfferHandler(otherGuy, msgType, msgData) {
if (!userStreams[otherGuy]) {
userStreams[otherGuy] = {};
}
acceptRejectCB(otherGuy, msgData.fileNameList, function(wasAccepted) {
var ackHandler = function(ackMesg) {
if (ackMesg.msgType === "error") {
statusCB(otherGuy, {status: "done", reason: "accept_failed"});
delete userStreams[otherGuy];
}
else {
statusCB(otherGuy, {status: "started"});
}
};
if (wasAccepted) {
userStreams[otherGuy] = {
groupSeq: msgData.seq,
nextPacketSeq: 0
};
easyrtc.sendDataWS(otherGuy, "filesAccept", {seq: msgData.seq}, ackHandler);
}
else {
easyrtc.sendDataWS(otherGuy, "filesReject", {seq: msgData.seq});
delete userStreams[otherGuy];
statusCB(otherGuy, {status: "rejected"});
}
}, msgData.userData);
}
function fileChunkHandler(otherGuy, msgType, msgData) {
var i;
var userStream = userStreams[otherGuy];
if (!userStream) {
return;
}
if (msgData.done) {
switch (msgData.done) {
case "file":
var blob = new Blob(userStream.currentData, {type: userStream.currentFileType});
blobAcceptor(otherGuy, blob, userStream.currentFileName);
statusCB(otherGuy, {status: "eof", name: userStream.currentFileName});
blob = null;
positionAcked = 0;
userStream.currentData = [];
break;
case "all":
statusCB(otherGuy, {status: "done", reason: "success"});
break;
case "cancelled":
delete userStreams[otherGuy];
statusCB(otherGuy, {status: "done", reason: "cancelled"});
break;
}
}
else if (msgData.name) {
statusCB(otherGuy, {status: "started_file", name: msgData.name});
userStream.currentFileName = msgData.name;
userStream.currentFileType = msgData.type;
userStream.lengthReceived = 0;
userStream.lengthExpected = msgData.size;
userStream.currentData = [];
}
else if (msgData.data64 || msgData.datatxt) {
var binData;
if (msgData.data64) {
binData = atob(msgData.data64);
}
else {
binData = msgData.datatxt;
}
var n = binData.length;
var binheap = new Uint8Array(n);
for (i = 0; i < n; i += 1) {
binheap[i] = binData.charCodeAt(i);
}
userStream.lengthReceived += n;
if (!userStream.currentData) {
console.log("Lost my currentData!!!");
}
userStream.currentData.push(binheap);
statusCB(otherGuy, {
status: "progress",
name: userStream.currentFileName,
received: userStream.lengthReceived,
size: userStream.lengthExpected});
if (userStream.lengthReceived > positionAcked + ackThreshold) {
positionAcked = userStream.lengthReceived;
easyrtc.sendData(otherGuy, "filesAck", {positionAck: positionAcked});
}
}
else {
console.log("Unexpected data structure in filesChunk=", msgData);
}
}
easyrtc.setPeerListener(fileOfferHandler, "filesOffer");
easyrtc.setPeerListener(fileChunkHandler, "filesChunk");
};
/** This is a wrapper around Eli Grey's saveAs function. This saves to the browser's downloads directory.
* @param {Blob} Blob - the data to be saved.
* @param {String} filename - the name of the file the blob should be written to.
*/
easyrtc_ft.saveAs = (function() {
/* FileSaver.js
* A saveAs() FileSaver implementation.
* 2013-01-23
*
* By Eli Grey, http://eligrey.com
* License: X11/MIT
* See LICENSE.md
*/
/*global self */
/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
plusplus: true */
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
var saveAs = window.saveAs
|| (navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator))
|| (function(view) {
var
doc = view.document
// only get URL when necessary in case BlobBuilder.js hasn't overridden it yet
, get_URL = function() {
return view.URL || view.webkitURL || view;
}
, URL = view.URL || view.webkitURL || view
, save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
, can_use_save_link = !view.externalHost && "download" in save_link
, click = function(node) {
var event = doc.createEvent("MouseEvents");
event.initMouseEvent(
"click", true, false, view, 0, 0, 0, 0, 0
, false, false, false, false, 0, null
);
node.dispatchEvent(event);
}
, webkit_req_fs = view.webkitRequestFileSystem
, req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
, throw_outside = function(ex) {
(view.setImmediate || view.setTimeout)(function() {
throw ex;
}, 0);
}
, force_saveable_type = "application/octet-stream"
, fs_min_size = 0
, deletion_queue = []
, process_deletion_queue = function() {
var i = deletion_queue.length;
while (i--) {
var file = deletion_queue[i];
if (typeof file === "string") { // file is an object URL
URL.revokeObjectURL(file);
} else { // file is a File
file.remove();
}
}
deletion_queue.length = 0; // clear queue
}
, dispatch = function(filesaver, event_types, event) {
event_types = [].concat(event_types);
var i = event_types.length;
while (i--) {
var listener = filesaver["on" + event_types[i]];
if (typeof listener === "function") {
try {
listener.call(filesaver, event || filesaver);
} catch (ex) {
throw_outside(ex);
}
}
}
}
, FileSaver = function(blob, name) {
// First try a.download, then web filesystem, then object URLs
var
filesaver = this
, type = blob.type
, blob_changed = false
, object_url
, target_view
, get_object_url = function() {
var object_url = get_URL().createObjectURL(blob);
deletion_queue.push(object_url);
return object_url;
}
, dispatch_all = function() {
dispatch(filesaver, "writestart progress write writeend".split(" "));
}
// on any filesys errors revert to saving with object URLs
, fs_error = function() {
// don't create more object URLs than needed
if (blob_changed || !object_url) {
object_url = get_object_url(blob);
}
if (target_view) {
target_view.location.href = object_url;
} else {
window.open(object_url, "_blank");
}
filesaver.readyState = filesaver.DONE;
dispatch_all();
}
, abortable = function(func) {
return function() {
if (filesaver.readyState !== filesaver.DONE) {
return func.apply(this, arguments);
}
else {
return null;
}
};
}
, create_if_not_found = {create: true, exclusive: false}
, slice
;
filesaver.readyState = filesaver.INIT;
if (!name) {
name = "download";
}
if (can_use_save_link) {
object_url = get_object_url(blob);
save_link.href = object_url;
save_link.download = name;
click(save_link);
filesaver.readyState = filesaver.DONE;
dispatch_all();
return;
}
// Object and web filesystem URLs have a problem saving in Google Chrome when
// viewed in a tab, so I force save with application/octet-stream
// http://code.google.com/p/chromium/issues/detail?id=91158
if (view.chrome && type && type !== force_saveable_type) {
slice = blob.slice || blob.webkitSlice;
blob = slice.call(blob, 0, blob.size, force_saveable_type);
blob_changed = true;
}
// Since I can't be sure that the guessed media type will trigger a download
// in WebKit, I append .download to the filename.
// https://bugs.webkit.org/show_bug.cgi?id=65440
if (webkit_req_fs && name !== "download") {
name += ".download";
}
if (type === force_saveable_type || webkit_req_fs) {
target_view = view;
}
if (!req_fs) {
fs_error();
return;
}
fs_min_size += blob.size;
req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
var save = function() {
dir.getFile(name, create_if_not_found, abortable(function(file) {
file.createWriter(abortable(function(writer) {
writer.onwriteend = function(event) {
target_view.location.href = file.toURL();
deletion_queue.push(file);
filesaver.readyState = filesaver.DONE;
dispatch(filesaver, "writeend", event);
};
writer.onerror = function() {
var error = writer.error;
if (error.code !== error.ABORT_ERR) {
fs_error();
}
};
"writestart progress write abort".split(" ").forEach(function(event) {
writer["on" + event] = filesaver["on" + event];
});
writer.write(blob);
filesaver.abort = function() {
writer.abort();
filesaver.readyState = filesaver.DONE;
};
filesaver.readyState = filesaver.WRITING;
}), fs_error);
}), fs_error);
};
dir.getFile(name, {create: false}, abortable(function(file) {
// delete file if it already exists
file.remove();
save();
}), abortable(function(ex) {
if (ex.code === ex.NOT_FOUND_ERR) {
save();
} else {
fs_error();
}
}));
}), fs_error);
}), fs_error);
}
, FS_proto = FileSaver.prototype
, saveAs = function(blob, name) {
return new FileSaver(blob, name);
}
;
FS_proto.abort = function() {
var filesaver = this;
filesaver.readyState = filesaver.DONE;
dispatch(filesaver, "abort");
};
FS_proto.readyState = FS_proto.INIT = 0;
FS_proto.WRITING = 1;
FS_proto.DONE = 2;
FS_proto.error =
FS_proto.onwritestart =
FS_proto.onprogress =
FS_proto.onwrite =
FS_proto.onabort =
FS_proto.onerror =
FS_proto.onwriteend =
null;
view.addEventListener("unload", process_deletion_queue, false);
return saveAs;
}(self));
return saveAs;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment