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"> </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"> </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