Added partial file transfers in MeshMessenger

Ylian Saint-Hilaire committed Dec 17, 2018 at 14:11 UTC 13fffd4f021418c9276be11a23c743e28195af1c
6 files changed +203 -8
public/images/MeshComm64b.png
Binary files /dev/null and b/public/images/MeshComm64b.png differ
public/images/messenger16.png
Binary files a/public/images/messenger16.png and b/public/images/messenger16.png differ
public/images/messenger32.png
Binary files a/public/images/messenger32.png and b/public/images/messenger32.png differ
public/images/messenger64.png
Binary files a/public/images/messenger64.png and /dev/null differ
public/messenger.htm
+164 -6
@@ -14,7 +14,7 @@
14 <div id="fileButton" class="icon4 topButton" title="Share a file" style="display:none" onclick="fileButtonClick()"></div>
15 <div id="camButton" class="icon2 topButton" title="Activate camera & microphone" style="display:none" onclick="camButtonClick()"></div>
16 <div id="micButton" class="icon6 topButton" title="Activate microphone" style="display:none" onclick="micButtonClick()"></div>
17 - <div id="hangupButton" class="icon11 topButton" title="Hang up" style="display:none" onclick="hangUpButtonClick(1)"></div>
17 + <div id="hangupButton" class="icon11 topRedButton" title="Hang up" style="display:none" onclick="hangUpButtonClick(1)"></div>
18 <div style="display:inline-block;width:2px"></div>
19 <div style="padding-top:9px;padding-left:6px;font-size:20px;display:inline-block"><b>MeshMessenger<span id="xtitle"></span></b></div>
20 </div>
@@ -36,6 +36,7 @@
36 <div style="position:absolute;right:0;left:0;top:2.5px;text-align:center">Local</div>
37 <video id="localVideoCanvas" autoplay muted style="position:absolute;top:20px;left:0;width:100%;height:calc(100% - 30px);background-color:black"></video>
38 </div>
39 + <input id="uploadFileInput" type="file" style="display:none">
40 <script type="text/javascript">
41 var userInputFocus = 0;
42 var args = parseUriArgs();
@@ -47,11 +48,18 @@
48 var webrtcconfiguration = null; //{ "iceServers": [ { 'urls': 'stun:stun.services.mozilla.com' }, { 'urls': 'stun:stun.l.google.com:19302' } ] };
49 var localStream = null;
50 var remoteStream = null;
50 - var senders = null;
51 + var fileUploads = [];
52 + var fileDownloads = {};
53 + var currentFileUpload = null;
54 + var currentFileDownload = null;
55
56 // Set the title
57 if (args.title) { QH('xtitle', ' - ' + args.title); document.title = document.title + ' - ' + args.title; }
58
59 + document.addEventListener('dragover', haltEvent, false);
60 + document.addEventListener('dragleave', haltEvent, false);
61 + document.addEventListener('drop', fileDrop, false);
62 +
63 document.onkeyup = function ondockeypress(e) {
64 if (state == 2) {
65 if ((e.keyCode == 8) && (userInputFocus == 0)) {
@@ -119,6 +127,7 @@
127 QE('sendButton', state == 2);
128 QE('clearButton', state == 2);
129 QE('xouttext', state == 2);
130 + QV('fileButton', state == 2);
131 QV('camButton', webchannel && webchannel.ok && !localStream);
132 QV('micButton', webchannel && webchannel.ok && !localStream);
133 QV('hangupButton', webchannel && webchannel.ok && localStream);
@@ -195,6 +204,7 @@
204 function disconnect() {
205 if (state > 0) { displayControl('Connection closed.'); }
206 if (state > 1) { setTimeout(start, 500); }
207 + cancelAllFileTransfers();
208 hangUpButtonClick(0, true); // Data channel
209 hangUpButtonClick(1, true); // Local audio/video
210 hangUpButtonClick(2, true); // Remote audio/video
@@ -208,7 +218,7 @@
218 if (state != 2) return; // If not in connected state, ignore this.
219 if (typeof data == 'object') { data = JSON.stringify(data); } // If this is an object, convert it to a string.
220 if (webchannel && webchannel.ok) { if (webchannel.xoutBuffer != null) { webchannel.xoutBuffer.push(data); } else { webchannel.send(data); } } // If WebRTC channel is possible, use it or hold until we can use it.
211 - else { if (socket != null) { socket.send(data); } } // If a websocket channel is present, use that.
221 + else { if (socket != null) { try { socket.send(data); } catch (ex) { } } } // If a websocket channel is present, use that.
222 }
223
224 // Send data over the websocket transport (WebSocket only)
@@ -240,6 +250,39 @@
250 }
251 break;
252 }
253 + case 'file': { startFileDownload(data); break; }
254 + case 'fileUploadCancel': { cancelFileTransfer(data.id); break; }
255 + case 'fileUploadStart': {
256 + if (fileDownloads[data.id]) {
257 + currentFileDownload = fileDownloads[data.id];
258 + currentFileDownload.data = '';
259 + changeFileInfo(data.id, 2, 0);
260 + continueFileDownload(data);
261 + send({ action: 'fileUploadAck', id: data.id });
262 + } break;
263 + }
264 + case 'fileUploadEnd': {
265 + if (currentFileDownload && (currentFileDownload.id == data.id)) {
266 + changeFileInfo(data.id, 3, 200);
267 + currentFileDownload.done = 1;
268 + currentFileDownload = null;
269 + send({ action: 'fileUploadAck', id: data.id });
270 + }
271 + currentFileDownload = null;
272 + break;
273 + }
274 + case 'fileUploadAck': {
275 + continueFileUpload();
276 + break;
277 + }
278 + case 'fileData': {
279 + if (currentFileDownload && (currentFileDownload.id == data.id)) {
280 + currentFileDownload.data += data.data;
281 + changeFileInfo(data.id, 2, (currentFileDownload.data.length * 200 / currentFileDownload.size));
282 + send({ action: 'fileUploadAck', id: data.id });
283 + }
284 + break;
285 + }
286 default: { console.log('Unhandled object data', data); break; }
287 }
288 } else {
@@ -249,7 +292,122 @@
292
293 // File sharing button
294 function fileButtonClick() {
295 + var chooser = Q('uploadFileInput');
296 + if (chooser.getAttribute("eventset") != 1) {
297 + chooser.setAttribute("eventset", "1");
298 + chooser.addEventListener("change", fileSelect, false);
299 + }
300 + chooser.value = null;
301 + chooser.click();
302 + }
303 +
304 + function fileSelect() {
305 + if (state != 2) return;
306 + var x = Q('uploadFileInput');
307 + if (x.files.length != 1) return;
308 + var reader = new FileReader();
309 + reader.onload = function (e) { this.xfile.data = e.target.result; startFileUpload(this.xfile); };
310 + reader.xfile = x.files[0];
311 + reader.readAsBinaryString(x.files[0]);
312 + }
313 +
314 + function fileDrop(e) {
315 + haltEvent(e);
316 + if (state != 2) return;
317 + if (e.dataTransfer == null || e.dataTransfer.files.length != 1) return;
318 + var reader = new FileReader();
319 + reader.onload = function (e) { this.xfile.data = e.target.result; startFileUpload(this.xfile); };
320 + reader.xfile = e.dataTransfer.files[0];
321 + reader.readAsBinaryString(e.dataTransfer.files[0]);
322 + }
323 +
324 + function startFileUpload(file) {
325 + if (state != 2) return;
326 + file.id = Math.random();
327 + fileUploads.push(file);
328 + //console.log('XXX', file);
329 + QA('xmsg', '<div style="clear:both"></div><div id="FILEUP-' + file.id + '" class="localBubble" style="width:240px;cursor:pointer" onclick="cancelFileTransfer(\'' + file.id + '\')"><div id="FILEUP-ICON-' + file.id + '" class="fileicon" style="float:left;width:32px;height:32px"></div><div><div id="FILEUP-NAME-' + file.id + '" style="height:16px;overflow:hidden">' + file.name + '</div><div style="width:200px;background-color:lightgray;margin-left:32px;border-radius:3px;margin-top:3px;height:11px"><div id="FILEUP-PROGRESS-' + file.id + '" style="width:0px;background-color:green;border-radius:3px;height:11px">&nbsp;</div></div></div></div>');
330 + Q('xmsg').scrollTop = Q('xmsg').scrollHeight;
331 + send({ action: 'file', size: file.size, id: file.id, type: file.type, name: file.name });
332 + if (currentFileUpload == null) continueFileUpload();
333 + }
334 +
335 + function startFileDownload(file) {
336 + if (state != 2) return;
337 + fileDownloads[file.id] = file;
338 + QA('xmsg', '<div style="clear:both"></div><div id="FILEUP-' + file.id + '" class="remoteBubble" style="width:240px;cursor:pointer"><div id="FILEUP-ICON-' + file.id + '" class="fileicon" style="float:left;width:32px;height:32px"></div><div><div id="FILEUP-NAME-' + file.id + '" style="height:16px;overflow:hidden">' + file.name + '</div><div style="width:200px;background-color:lightgray;margin-left:32px;border-radius:3px;margin-top:3px;height:11px"><div id="FILEUP-PROGRESS-' + file.id + '" style="width:0px;background-color:green;border-radius:3px;height:11px">&nbsp;</div></div></div></div>');
339 + Q('xmsg').scrollTop = Q('xmsg').scrollHeight;
340 + }
341 +
342 + // Change the file icon and progress
343 + function changeFileInfo(id, icon, progress, progressColor) {
344 + if (icon) {
345 + Q('FILEUP-ICON-' + id).classList.remove('fileicon');
346 + Q('FILEUP-ICON-' + id).classList.remove('fileiconx');
347 + Q('FILEUP-ICON-' + id).classList.remove('fileicontransfer');
348 + Q('FILEUP-ICON-' + id).classList.remove('fileicondone');
349 + Q('FILEUP-ICON-' + id).classList.add(['fileicon', 'fileiconx', 'fileicontransfer', 'fileicondone'][icon]);
350 + }
351 + if (progress) { QS('FILEUP-PROGRESS-' + id)['width'] = progress + 'px'; }
352 + if (progressColor) { QS('FILEUP-PROGRESS-' + id)['background-color'] = progressColor; }
353 + }
354 +
355 + function cancelFileTransfer(id) {
356 + if ((currentFileUpload != null) && (currentFileUpload.id == id)) { currentFileUpload = null; }
357 + if ((currentFileDownload != null) && (currentFileDownload.id == id)) { currentFileDownload = null; }
358 +
359 + var found = false;
360 + if (fileDownloads[id] && (fileDownloads[id].done != 1)) {
361 + delete fileDownloads[id];
362 + found = true;
363 + } else {
364 + for (var i in fileUploads) {
365 + if (fileUploads[i].id == id) {
366 + send({ action: 'fileUploadCancel', id: id });
367 + fileUploads.splice(i, 1);
368 + found = true;
369 + break;
370 + }
371 + }
372 + }
373 + if (found) { changeFileInfo(id, 1, 200, 'gray'); } // Only cancel a file if it was in the file queue.
374 + }
375 +
376 + function cancelAllFileTransfers() {
377 + for (var i in fileDownloads) { cancelFileTransfer(fileDownloads[i].id); }
378 + for (var i in fileUploads) { cancelFileTransfer(fileUploads[i].id); }
379 + }
380 +
381 + function continueFileUpload() {
382 + if (currentFileUpload == null) {
383 + // Select the next file to upload
384 + if (fileUploads.length == 0) { return; } // Nothing to do
385 + currentFileUpload = fileUploads[0];
386 + currentFileUpload.ptr = 0;
387 +
388 + // Indicate that we are sending this file
389 + send({ action: 'fileUploadStart', size: currentFileUpload.size, id: currentFileUpload.id, type: currentFileUpload.type, name: currentFileUpload.name });
390 + } else {
391 + if (currentFileUpload.size <= currentFileUpload.ptr) {
392 + // If we are done, send the end marker
393 + send({ action: 'fileUploadEnd', size: currentFileUpload.size, id: currentFileUpload.id, type: currentFileUpload.type, name: currentFileUpload.name });
394 + changeFileInfo(currentFileUpload.id, 3, 200);
395 + fileUploads.splice(0, 1);
396 + currentFileUpload = null;
397 + continueFileUpload(); // Send the next file
398 + } else {
399 + // Send the next block
400 + var nextBlockLen = Math.min(4000, currentFileUpload.data.length - currentFileUpload.ptr);
401 + var data = currentFileUpload.data.substring(currentFileUpload.ptr, currentFileUpload.ptr + nextBlockLen);
402 + send({ action: 'fileData', id: currentFileUpload.id, data: data });
403 + currentFileUpload.ptr += nextBlockLen;
404 + changeFileInfo(currentFileUpload.id, 0, (currentFileUpload.ptr * 200 / currentFileUpload.size));
405 + }
406 + }
407 + }
408
409 + function continueFileDownload(msg) {
410 + send({ action: 'fileUploadAck', id: msg.id });
411 }
412
413 // Camera button
@@ -337,9 +495,9 @@
495 socket.onclose = function () { disconnect(); }
496 socket.onmessage = function (msg) {
497 if ((state < 2) && (typeof msg.data == 'string') && (msg.data == 'c')) {
340 - hangUpButtonClick(0);
341 - hangUpButtonClick(1);
342 - hangUpButtonClick(2);
498 + hangUpButtonClick(0, true);
499 + hangUpButtonClick(1, true);
500 + hangUpButtonClick(2, true);
501 displayControl('Connected.');
502 state = 2;
503 updateControls();
public/styles/messenger.css
+39 -2
@@ -1,7 +1,7 @@
1 .topButton {
2 cursor: pointer;
3 border: none;
4 - margin: 3px;
4 + margin: 2px;
5 float: right;
6 border-radius: 3px;
7 height: 32px;
@@ -12,6 +12,19 @@
12 background-color: lightgray;
13 }
14
15 +.topRedButton {
16 + cursor: pointer;
17 + border: none;
18 + margin: 2px;
19 + float: right;
20 + border-radius: 3px;
21 + height: 32px;
22 + width: 32px;
23 +}
24 +
25 + .topRedButton:hover {
26 + background-color: orangered;
27 + }
28
29 .remoteBubble {
30 background-color: #00cc99;
@@ -87,5 +100,29 @@
100
101 .icon11 {
102 background: url(../images/messenger32.png) -320px 0px;
90 - background-color: gray;
103 + background-color: orange;
104 }
105 +
106 +.fileicon {
107 + background: url(../images/messenger32.png) -96px 0px;
108 + height: 32px;
109 + width: 32px;
110 +}
111 +
112 +.fileiconx {
113 + background: url(../images/messenger32.png) -352px 0px;
114 + height: 32px;
115 + width: 32px;
116 +}
117 +
118 +.fileicontransfer {
119 + background: url(../images/messenger32.png) -288px 0px;
120 + height: 32px;
121 + width: 32px;
122 +}
123 +
124 +.fileicondone {
125 + background: url(../images/messenger32.png) -256px 0px;
126 + height: 32px;
127 + width: 32px;
128 +}
\ No newline at end of file