EasyRTC Documentation

Documentation

Source: easyrtc_ft.js

1
/* global define, module, require, console */
2
/*!
3
Script: easyrtc_ft.js
4
5
Provides support file and data transfer support to easyrtc.
6
7
About: License
8
9
Copyright (c) 2016, Priologic Software Inc.
10
All rights reserved.
11
12
Redistribution and use in source and binary forms, with or without
13
modification, are permitted provided that the following conditions are met:
14
15
* Redistributions of source code must retain the above copyright notice,
16
this list of conditions and the following disclaimer.
17
* Redistributions in binary form must reproduce the above copyright
18
notice, this list of conditions and the following disclaimer in the
19
documentation and/or other materials provided with the distribution.
20
21
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
25
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31
POSSIBILITY OF SUCH DAMAGE.
32
*/
33
34
(function (root, factory) {
35
if (typeof define === 'function' && define.amd) {
36
//RequireJS (AMD) build system
37
define(['easyrtc'], factory);
38
} else if (typeof module === 'object' && module.exports) {
39
//CommonJS build system
40
module.exports = factory(require('easyrtc'));
41
} else {
42
//Vanilla JS, ensure dependencies are loaded correctly
43
if (typeof window.easyrtc !== 'object' || !window.easyrtc) {
44
throw new Error("easyrtc_ft requires easyrtc");
45
}
46
root.easyrtc_ft = factory(window.easyrtc);
47
}
48
}(this, function (easyrtc, undefined) {
49
50
"use strict";
51
52
/**
53
* @class Easyrtc_ft.
54
*
55
* @returns {Easyrtc_ft} the new easyrtc instance.
56
*
57
* @constructs Easyrtc_ft
58
*/
59
60
var easyrtc_ft = {};
61
62
/**
63
* Error codes that the EasyRTC will use in the errorCode field of error object passed
64
* to error handler set by easyrtc.setOnError. The error codes are short printable strings.
65
* @type Object
66
*/
67
easyrtc_ft.errCodes = {
68
DATA_LOST: "DATA_LOST",
69
INVALID_DATA: "INVALID_DATA",
70
DROP_FILE: "DROP_FILE"
71
};
72
73
/**
74
* Establish an area as a drag-n-drop drop site for files.
75
* @param {DOMString} droptargetName - the id of the drag-and-drop site or the actual DOM object.
76
* @param {Function} filesHandler - function that accepts an array of File's.
77
*/
78
easyrtc_ft.buildDragNDropRegion = function(droptargetName, filesHandler) {
79
var droptarget;
80
if (typeof droptargetName === 'string') {
81
droptarget = document.getElementById(droptargetName);
82
if (!droptarget) {
83
throw ("unknown object " + droptargetName);
84
}
85
}
86
else {
87
droptarget = droptargetName;
88
}
89
90
function addClass(target, classname) {
91
if (target.className) {
92
if (target.className.indexOf(classname, 0) >= 0) {
93
return;
94
}
95
else {
96
target.className = target.className + " " + classname;
97
}
98
}
99
else {
100
target.className = classname;
101
}
102
target.className = target.className.replace("  ", " ");
103
}
104
105
function removeClass(target, classname) {
106
if (!target.className) {
107
return;
108
}
109
target.className = target.className.replace(classname, "").replace("  ", " ");
110
}
111
112
function ignore(e) {
113
e.stopPropagation();
114
e.preventDefault();
115
return false;
116
}
117
118
function drageventcancel(e) {
119
if (e.preventDefault) {
120
e.preventDefault(); // required by FF + Safari
121
}
122
e.dataTransfer.dropEffect = 'copy'; // tells the browser what drop effect is allowed here
123
return false; // required by IE
124
}
125
126
var dropCueClass = "easyrtcfiledrop";
127
128
function dropHandler(e) {
129
removeClass(droptarget, dropCueClass);
130
var dt = e.dataTransfer;
131
var files = dt.files;
132
if (dt.files.length > 0) {
133
try {
134
filesHandler(files);
135
} catch (errorEvent) {
136
easyrtc.showError(easyrtc_ft.errCodes.DROP_FILE, errorEvent);
137
}
138
}
139
return ignore(e);
140
}
141
142
function dragEnterHandler(e) {
143
addClass(droptarget, dropCueClass);
144
return drageventcancel(e);
145
}
146
147
function dragLeaveHandler(e) {
148
removeClass(droptarget, dropCueClass);
149
return drageventcancel(e);
150
}
151
152
var addEvent = (function() {
153
if (document.addEventListener) {
154
return function(el, type, fn) {
155
if (el && el.nodeName || el === window) {
156
el.addEventListener(type, fn, false);
157
} else if (el && el.length) {
158
for (var i = 0; i < el.length; i++) {
159
addEvent(el[i], type, fn);
160
}
161
}
162
};
163
} else {
164
return function(el, type, fn) {
165
if (el && el.nodeName || el === window) {
166
el.attachEvent('on' + type, function() {
167
return fn.call(el, window.event);
168
});
169
} else if (el && el.length) {
170
for (var i = 0; i < el.length; i++) {
171
addEvent(el[i], type, fn);
172
}
173
}
174
};
175
}
176
})();
177
178
droptarget.ondrop = dropHandler;
179
droptarget.ondragenter = dragEnterHandler;
180
droptarget.ondragleave = dragLeaveHandler;
181
droptarget.ondragover = drageventcancel;
182
};
183
184
/**
185
* Builds a function that can be used to send a group of files to a peer.
186
* @param {String} destUser easyrtcid of the person being sent to.
187
* @param {Function} progressListener - if provided, is called with the following objects:
188
*    {status:"waiting"}  // once a file offer has been sent but not accepted or rejected yet
189
*    {status:"started_file", name: filename}
190
*    {status:"working", name:filename, position:position_in_file, size:size_of_current_file, numFiles:number_of_files_left}
191
*    {status:"cancelled"}  // if the remote user cancels the sending
192
*    {status:"done"}       // when the file is done
193
*    the progressListener should always return true for normal operation, false to cancel a filetransfer.
194
* @return {Function} an object that accepts an array of File (the Files to be sent), and a boolean
195
* @param {Object} options - overide default file transfer settings
196
*    maxPacketSize is the size (before base64 encoding) that is sent in a
197
*               single data channel message, in bytes.
198
*    maxChunkSize is the amount read from a file at a time, in bytes.
199
*    ackThreshold is the amount of data that can be sent before an ack is
200
*              received from the party we're sending to, bytes.
201
*    maxChunkSize should be a multiple of maxPacketSize.
202
*    ackThreshold should be several times larger than maxChunkSize. For
203
*              network paths that have greater latency, increase
204
*              ackThreshold further.
205
*/
206
easyrtc_ft.buildFileSender = function(destUser, progressListener, options) {
207
options = options || {};
208
var droptarget;
209
var seq = 0;
210
var positionAcked = 0;
211
var filePosition = 0;
212
var filesOffered = []; // TODO ARray vs Object look weird here but seq is Number 
213
var filesBeingSent = [];
214
var curFile = null;
215
var curSeq = null;
216
var curFileSize;
217
var filesAreBinary;
218
var maxPacketSize = options.maxPacketSize || (10 * 1024); // max bytes per packet, before base64 encoding
219
var maxChunkSize = options.maxPacketSize || (maxPacketSize * 10); // max binary bytes read at a time.
220
var ackThreshold = options.maxPacketSize || (maxChunkSize * 4); // send is allowed to be 400KB ahead of receiver
221
222
var waitingForAck = false;
223
var offersWaiting = [];
224
var outseq = 0;
225
226
function fileCancelReceived(sender, msgType, msgData, targeting) {
227
228
if (!msgData.seq || !filesOffered[msgData.seq]){
229
return;
230
}
231
232
progressListener({
233
seq: msgData.seq,
234
status: "cancelled"
235
});
236
237
// Offer can be offered only once
238
delete filesOffered[msgData.seq];
239
}
240
241
function cancelFilesOffer(offerSeq) {
242
243
// Clear from of
244
if (filesOffered[offerSeq]) {
245
246
fileCancelReceived(destUser, 'filesCancel', filesOffered[offerSeq]);  
247
248
delete filesOffered[offerSeq]; 
249
} else {
250
251
// Clear from waiting queue
252
offersWaiting = offersWaiting.filter(function(offersWaiting) {
253
var isOfferToCancel = offersWaiting.seq === offerSeq;
254
if (isOfferToCancel) {
255
fileCancelReceived(destUser, 'filesCancel', offersWaiting);  
256
}
257
return !isOfferToCancel;
258
}); 
259
}
260
261
easyrtc.sendData(destUser, "filesChunk", {
262
seq: offerSeq,
263
done: "cancelled"
264
});
265
}
266
267
function sendFilesOffer(files, areBinary) {
268
269
var fileNameList = [];
270
for (var i = 0, l = files.length; i < l; i++) {
271
fileNameList[i] = {
272
name: files[i].name, 
273
size: files[i].size
274
};
275
}
276
277
seq++;
278
filesOffered[seq] = {
279
seq: seq,
280
files: files,
281
areBinary: areBinary
282
};
283
284
easyrtc.sendDataWS(destUser, "filesOffer", {
285
seq: seq, 
286
fileNameList: fileNameList
287
});
288
289
progressListener({
290
seq: seq,
291
status: "waiting"
292
});
293
294
return cancelFilesOffer.bind(null, seq);
295
}
296
297
function addOfferToWaitingList(offer) {
298
offersWaiting.push(offer);
299
}
300
301
function processOfferWaiting() {
302
if (offersWaiting.length > 0) {
303
setTimeout(function() {
304
var fileset = offersWaiting.shift();
305
sendOffer(fileset);
306
}, 240);
307
}
308
}
309
310
function sendChunk() {
311
312
if (!curSeq) {
313
return;
314
}
315
316
if (!curFile) {
317
if (filesBeingSent.length === 0) {
318
319
outseq = 0;
320
easyrtc.sendData(destUser, "filesChunk", {
321
seq: curSeq,
322
done: "all"
323
});
324
325
progressListener({
326
seq: curSeq,
327
status: "done"
328
});
329
330
curSeq = null;
331
processOfferWaiting();
332
return;
333
}
334
else {
335
curFile = filesBeingSent.shift();
336
curFileSize = curFile.size;
337
positionAcked = 0;
338
waitingForAck = false;
339
easyrtc.sendData(destUser, "filesChunk", {
340
seq: curSeq,
341
name: curFile.name, 
342
type: curFile.type, 
343
outseq: outseq, 
344
size: curFile.size
345
});
346
outseq++;
347
348
progressListener({
349
seq: curSeq,
350
status: "started_file", 
351
name: curFile.name
352
});
353
}
354
}
355
356
var amountToRead = Math.min(maxChunkSize, curFileSize - filePosition);
357
var progressAck = progressListener({
358
seq: curSeq,
359
status: "working", 
360
name: curFile.name, 
361
position: filePosition, 
362
size: curFileSize, 
363
numFiles: filesBeingSent.length + 1
364
});
365
366
if (!progressAck) {
367
curSeq = null;
368
curFile = null;
369
filePosition = 0;
370
cancelFilesOffer(curSeq);
371
processOfferWaiting();
372
return;
373
}
374
375
var nextLocation = filePosition + amountToRead;
376
var blobSlice = curFile.slice(filePosition, nextLocation);
377
378
var reader = new FileReader();
379
reader.onloadend = function(evt) {
380
if (evt.target.readyState === FileReader.DONE) { // DONE == 2
381
382
var binaryString = "";
383
var bytes = new Uint8Array(evt.target.result);
384
var length = bytes.length;
385
386
for( var i = 0; i < length; i++ ) {
387
binaryString += String.fromCharCode(bytes[i]);
388
}
389
390
for (var pp = 0; pp < binaryString.length; pp++) {
391
var oneChar = binaryString.charCodeAt(pp);
392
}
393
394
for (var pos = 0; pos < binaryString.length; pos += maxPacketSize) {
395
396
var packetLen = Math.min(maxPacketSize, amountToRead - pos);
397
var packetData = binaryString.substring(pos, pos + packetLen);
398
var packetObject = {
399
seq: curSeq,
400
outseq: outseq
401
};
402
403
if (filesAreBinary) {
404
packetObject.data64 = btoa(packetData);
405
} else {
406
packetObject.datatxt = packetData;
407
}
408
409
easyrtc.sendData(destUser, "filesChunk", packetObject);
410
outseq++;
411
}
412
413
if (nextLocation >= curFileSize) {
414
easyrtc.sendData(destUser, "filesChunk", {
415
seq: curSeq,
416
done: "file"
417
});
418
}
419
420
if (filePosition < positionAcked + ackThreshold) {
421
sendChunk();
422
} else {
423
waitingForAck = true;
424
}
425
}
426
};
427
428
reader.readAsArrayBuffer(blobSlice);
429
filePosition = nextLocation;
430
431
//  advance to the next file if we've read all of this file
432
if (nextLocation >= curFileSize) {
433
curFile = null;
434
filePosition = 0;
435
}
436
}
437
438
if (!progressListener) {
439
progressListener = function() {
440
return true;
441
};
442
}
443
444
var roomOccupantListener = function(eventType, eventData) {
445
var roomName;
446
var foundUser = false;
447
for (roomName in eventData) {
448
if (eventData[roomName][destUser]) {
449
foundUser = true;
450
}
451
}
452
if (!foundUser) {
453
easyrtc.removeEventListener("roomOccupant", roomOccupantListener);
454
455
if (filesBeingSent.length > 0) {
456
cancelFilesOffer(curSeq);
457
}
458
459
if (filesOffered.length > 0) {
460
filesOffered.forEach(function (filesOffered, seq) {
461
cancelFilesOffer(seq);
462
});
463
}
464
}
465
};
466
467
easyrtc.addEventListener("roomOccupant", roomOccupantListener);
468
469
function sendOffer(offer) {
470
471
curSeq = offer.seq;
472
for (var i = 0, l = offer.files.length; i < l; i++) {
473
filesBeingSent.push(offer.files[i]);
474
}
475
filesAreBinary = offer.filesAreBinary;
476
filePosition = 0;
477
478
progressListener({
479
seq: curSeq,
480
status: "started"
481
});
482
483
sendChunk(); // this starts the file reading
484
}
485
486
//
487
// if a file offer is rejected, we delete references to it.
488
//
489
function fileOfferRejected(sender, msgType, msgData, targeting) {
490
491
if (!msgData.seq || !filesOffered[msgData.seq]){
492
return;
493
}
494
495
progressListener({
496
seq: msgData.seq,
497
status: "rejected"
498
});
499
500
delete filesOffered[msgData.seq];
501
}
502
//
503
// if a file offer is accepted, initiate sending of files.
504
//
505
function fileOfferAccepted(sender, msgType, msgData, targeting) {
506
507
if (!msgData.seq || !filesOffered[msgData.seq]){
508
return;
509
}
510
511
var alreadySending = filesBeingSent.length > 0;
512
var offer = filesOffered[msgData.seq];
513
514
// Offer can be offered only once
515
delete filesOffered[msgData.seq];
516
517
if (!alreadySending && !curFile) {
518
sendOffer(offer);
519
} else {
520
addOfferToWaitingList(offer);
521
}
522
}
523
524
function packageAckReceived(sender, msgType, msgData) {
525
positionAcked = msgData.positionAck;
526
if (waitingForAck && filePosition < positionAcked + ackThreshold) {
527
waitingForAck = false;
528
sendChunk();
529
}
530
}
531
532
easyrtc.setPeerListener(fileOfferRejected, "filesReject", destUser);
533
easyrtc.setPeerListener(fileOfferAccepted, "filesAccept", destUser);
534
easyrtc.setPeerListener(fileCancelReceived, "filesCancel", destUser);
535
easyrtc.setPeerListener(packageAckReceived, "filesAck", destUser);
536
537
return sendFilesOffer;
538
};
539
540
541
/**
542
* Enable datachannel based file receiving. The received blobs get passed to the statusCB in the 'eof' typed message.
543
* @param {Function(otherGuy,fileNameList, wasAccepted)} acceptRejectCB - this function is called when another peer
544
* (otherGuy) offers to send you a list of files. this function should call it's wasAccepted function with true to
545
* allow those files to be sent, or false to disallow them.
546
* @param {Function} blobAcceptor - this function is called three arguments arguments: the suppliers easyrtcid, a blob and a filename. It is responsible for
547
* saving the blob to the file, usually using easyrtc_ft.saveAs.
548
* @param {type} statusCB  - this function is called with the current state of file receiving. It is passed two arguments:
549
* otherGuy - the easyrtcid of the person sending the files. *
550
* msg - one of the following structures:
551
* {status:"done", reason:"accept_failed"}
552
* {status:"done", reason:"success"}
553
* {status:"done", reason:"cancelled"}
554
* {status:"eof"},
555
* {status:"started_file, name:"filename"}
556
* {status:"progress", name:filename,
557
*    received:received_size_in_bytes,
558
*    size:file_size_in_bytes }
559
*  @example
560
*
561
*    easyrtc_ft(
562
*       function(otherGuy, filenamelist, wasAccepted) {  wasAccepted(true);},
563
*       function(otherGuy, blob, filename) { easyrtc_ft(blob, filename);},
564
*       function(otherGuy, status) {  console.log("status:" + JSON.stringify(status))}
565
*     );
566
*/
567
easyrtc_ft.buildFileReceiver = function(acceptRejectCB, blobAcceptor, statusCB, options) {
568
options = options || {};
569
570
var usersOffers = {};
571
var positionAcked = 0;
572
var ackThreshold = options.ackThreshold || 10000; // receiver is allowed to be 10KB behind of sender
573
574
var roomOccupantListener = function(eventType, eventData) {
575
var destUser;
576
var foundUser;
577
var roomName;
578
var destOffer;
579
for (destUser in usersOffers) {
580
if (usersOffers.hasOwnProperty(destUser)) {
581
582
foundUser = false;
583
for (roomName in eventData) {
584
if (eventData[roomName][destUser]) {
585
foundUser = true;
586
}
587
}
588
589
if (!foundUser) {   
590
var userOffers = usersOffers[destUser];
591
for (var userOffer in userOffers[destUser]) {
592
if (userOffers.hasOwnProperty(userOffer)) {
593
delete userOffers[userOffer];
594
statusCB(destUser, {
595
seq: destOffer,
596
status: "done", 
597
reason: "cancelled"
598
});
599
}
600
}
601
602
delete usersOffers[destUser];
603
}
604
}
605
}
606
};
607
608
easyrtc.addEventListener("roomOccupant", roomOccupantListener);
609
610
function fileOfferHandler(otherGuy, msgType, msgData) {
611
var destOffer = msgData.seq;
612
if (!destOffer){
613
return;
614
}
615
var userOffers = usersOffers[otherGuy] = usersOffers[otherGuy] || {};
616
var userOffer = userOffers[destOffer] = {
617
seq: destOffer,
618
status: 'pending'
619
};
620
acceptRejectCB(otherGuy, msgData.fileNameList, function(wasAccepted) {
621
var ackHandler = function(ackMesg) {
622
623
if (ackMesg.msgType === "error") {
624
statusCB(otherGuy, {
625
seq: destOffer,
626
status: "done", 
627
reason: "accept_failed"
628
});
629
delete userOffers[destOffer];
630
}
631
else {
632
statusCB(otherGuy, {
633
seq: destOffer,
634
status: "started"
635
});
636
}
637
};
638
if (wasAccepted) {
639
userOffers[destOffer] = {
640
seq: destOffer,
641
status: "accepted",
642
nextPacketSeq: 0
643
};
644
645
easyrtc.sendDataWS(otherGuy, "filesAccept", {
646
seq: destOffer
647
}, ackHandler);
648
}
649
else {
650
easyrtc.sendDataWS(otherGuy, "filesReject", {
651
seq: destOffer
652
});
653
654
statusCB(otherGuy, {
655
seq: destOffer,
656
status: "rejected"
657
});
658
659
delete userOffers[destOffer];
660
}
661
});
662
}
663
664
function fileChunkHandler(otherGuy, msgType, msgData) {
665
var destOffer = msgData.seq;
666
if (!destOffer){
667
return;
668
}
669
var userOffers = usersOffers[otherGuy];
670
if (!userOffers) {
671
return;
672
}
673
var userOffer = userOffers[destOffer];
674
if (!userOffer) {
675
return;
676
}
677
if (msgData.done) {
678
switch (msgData.done) {
679
case "file":
680
var blob = new Blob(userOffer.currentData, {
681
type: userOffer.currentFileType
682
});
683
blobAcceptor(otherGuy, blob, userOffer.currentFileName);
684
statusCB(otherGuy, {
685
seq: destOffer,
686
status: "eof", 
687
name: userOffer.currentFileName
688
});
689
690
blob = null;
691
positionAcked = 0;
692
userOffer.currentData = [];
693
break;
694
case "all":
695
statusCB(otherGuy, {
696
seq: destOffer,
697
status: "done", 
698
reason: "success"
699
});
700
break;
701
case "cancelled":
702
delete userOffers[destOffer];
703
statusCB(otherGuy, {
704
seq: destOffer,
705
status: "done", 
706
reason: "cancelled"
707
});
708
break;
709
}
710
}
711
else if (msgData.name) {
712
statusCB(otherGuy, {
713
seq: destOffer,
714
status: "started_file", 
715
name: msgData.name
716
});
717
userOffer.currentFileName = msgData.name;
718
userOffer.currentFileType = msgData.type;
719
userOffer.lengthReceived = 0;
720
userOffer.lengthExpected = msgData.size;
721
userOffer.currentData = [];
722
}
723
else if (msgData.data64 || msgData.datatxt) {
724
var binData;
725
if (msgData.data64) {
726
binData = atob(msgData.data64);
727
}
728
else {
729
binData = msgData.datatxt;
730
}
731
var i;
732
var n = binData.length;
733
var binheap = new Uint8Array(n);
734
for (i = 0; i < n; i += 1) {
735
binheap[i] = binData.charCodeAt(i);
736
}
737
userOffer.lengthReceived += n;
738
739
if (!userOffer.currentData) {
740
easyrtc.showError(easyrtc_ft.errCodes.DATA_LOST, "file tranfert data lost");
741
}
742
743
userOffer.currentData.push(binheap);
744
745
statusCB(otherGuy, {
746
seq: destOffer,
747
status: "progress",
748
name: userOffer.currentFileName,
749
received: userOffer.lengthReceived,
750
size: userOffer.lengthExpected});
751
752
if (userOffer.lengthReceived > positionAcked + ackThreshold) {
753
positionAcked = userOffer.lengthReceived;
754
easyrtc.sendData(otherGuy, "filesAck", {
755
seq: destOffer,
756
positionAck: positionAcked
757
});
758
}
759
}
760
else {
761
easyrtc.showError(easyrtc.errCodes.INVALID_DATA, "Unexpected data structure in filesChunk");
762
}
763
}
764
765
easyrtc.setPeerListener(fileOfferHandler, "filesOffer");
766
easyrtc.setPeerListener(fileChunkHandler, "filesChunk");
767
};
768
769
/** This is a wrapper around Eli Grey's saveAs function. This saves to the browser's downloads directory.
770
* @param {Blob} Blob - the data to be saved.
771
* @param {String} filename - the name of the file the blob should be written to.
772
*/
773
easyrtc_ft.saveAs = (function() {
774
775
/* FileSaver.js
776
* A saveAs() FileSaver implementation.
777
* 2013-01-23
778
*
779
* By Eli Grey, http://eligrey.com
780
* License: X11/MIT
781
*   See LICENSE.md
782
*/
783
784
/*global self */
785
/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
786
plusplus: true */
787
788
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
789
var saveAs = window.saveAs || (navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator)) || (function(view) {
790
791
var doc = view.document,
792
// only get URL when necessary in case BlobBuilder.js hasn't overridden it yet
793
get_URL = function () {
794
return view.URL || view.webkitURL || view;
795
},
796
URL = view.URL || view.webkitURL || view,
797
save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a"),
798
can_use_save_link = !view.externalHost && "download" in save_link,
799
click = function(node) {
800
var event = doc.createEvent("MouseEvents");
801
event.initMouseEvent(
802
"click", true, false, view, 0, 0, 0, 0, 0,
803
false, false, false, false, 0, null
804
);
805
node.dispatchEvent(event);
806
},
807
webkit_req_fs = view.webkitRequestFileSystem,
808
req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem,
809
throw_outside = function(ex) {
810
(view.setImmediate || view.setTimeout)(function() {
811
throw ex;
812
}, 0);
813
},
814
force_saveable_type = "application/octet-stream",
815
fs_min_size = 0,
816
deletion_queue = [];
817
818
function process_deletion_queue() {
819
var i = deletion_queue.length;
820
while (i--) {
821
var file = deletion_queue[i];
822
if (typeof file === "string") { // file is an object URL
823
URL.revokeObjectURL(file);
824
} else { // file is a File
825
file.remove();
826
}
827
}
828
deletion_queue.length = 0; // clear queue
829
}
830
831
function dispatch(filesaver, event_types, event) {
832
event_types = [].concat(event_types);
833
var i = event_types.length;
834
while (i--) {
835
var listener = filesaver["on" + event_types[i]];
836
if (typeof listener === "function") {
837
try {
838
listener.call(filesaver, event || filesaver);
839
} catch (ex) {
840
throw_outside(ex);
841
}
842
}
843
}
844
}
845
846
function FileSaver(blob, name) {
847
// First try a.download, then web filesystem, then object URLs
848
var filesaver = this,
849
type = blob.type,
850
blob_changed = false,
851
object_url,
852
target_view,
853
get_object_url = function() {
854
var object_url = get_URL().createObjectURL(blob);
855
deletion_queue.push(object_url);
856
return object_url;
857
},
858
dispatch_all = function() {
859
dispatch(filesaver, "writestart progress write writeend".split(" "));
860
},
861
// on any filesys errors revert to saving with object URLs
862
fs_error = function() {
863
// don't create more object URLs than needed
864
if (blob_changed || !object_url) {
865
object_url = get_object_url(blob);
866
}
867
if (target_view) {
868
target_view.location.href = object_url;
869
} else {
870
window.open(object_url, "_blank");
871
}
872
filesaver.readyState = filesaver.DONE;
873
dispatch_all();
874
},
875
abortable = function(func) {
876
return function() {
877
if (filesaver.readyState !== filesaver.DONE) {
878
return func.apply(this, arguments);
879
}
880
else {
881
return null;
882
}
883
};
884
},
885
create_if_not_found = {create: true, exclusive: false},
886
slice;
887
888
filesaver.readyState = filesaver.INIT;
889
890
if (!name) {
891
name = "download";
892
}
893
894
if (can_use_save_link) {
895
object_url = get_object_url(blob);
896
save_link.href = object_url;
897
save_link.download = name;
898
click(save_link);
899
filesaver.readyState = filesaver.DONE;
900
dispatch_all();
901
return;
902
}
903
// Object and web filesystem URLs have a problem saving in Google Chrome when
904
// viewed in a tab, so I force save with application/octet-stream
905
// http://code.google.com/p/chromium/issues/detail?id=91158
906
if (view.chrome && type && type !== force_saveable_type) {
907
slice = blob.slice || blob.webkitSlice;
908
blob = slice.call(blob, 0, blob.size, force_saveable_type);
909
blob_changed = true;
910
}
911
// Since I can't be sure that the guessed media type will trigger a download
912
// in WebKit, I append .download to the filename.
913
// https://bugs.webkit.org/show_bug.cgi?id=65440
914
if (webkit_req_fs && name !== "download") {
915
name += ".download";
916
}
917
if (type === force_saveable_type || webkit_req_fs) {
918
target_view = view;
919
}
920
if (!req_fs) {
921
fs_error();
922
return;
923
}
924
fs_min_size += blob.size;
925
req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
926
fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
927
var save = function() {
928
dir.getFile(name, create_if_not_found, abortable(function(file) {
929
file.createWriter(abortable(function(writer) {
930
writer.onwriteend = function(event) {
931
target_view.location.href = file.toURL();
932
deletion_queue.push(file);
933
filesaver.readyState = filesaver.DONE;
934
dispatch(filesaver, "writeend", event);
935
};
936
writer.onerror = function() {
937
var error = writer.error;
938
if (error.code !== error.ABORT_ERR) {
939
fs_error();
940
}
941
};
942
"writestart progress write abort".split(" ").forEach(function(event) {
943
writer["on" + event] = filesaver["on" + event];
944
});
945
writer.write(blob);
946
filesaver.abort = function() {
947
writer.abort();
948
filesaver.readyState = filesaver.DONE;
949
};
950
filesaver.readyState = filesaver.WRITING;
951
}), fs_error);
952
}), fs_error);
953
};
954
dir.getFile(name, {create: false}, abortable(function(file) {
955
// delete file if it already exists
956
file.remove();
957
save();
958
}), abortable(function(ex) {
959
if (ex.code === ex.NOT_FOUND_ERR) {
960
save();
961
} else {
962
fs_error();
963
}
964
}));
965
}), fs_error);
966
}), fs_error);
967
}
968
969
function saveAs(blob, name) {
970
return new FileSaver(blob, name);
971
}
972
973
var FS_proto = FileSaver.prototype;
974
975
FS_proto.abort = function() {
976
var filesaver = this;
977
filesaver.readyState = filesaver.DONE;
978
dispatch(filesaver, "abort");
979
};
980
981
FS_proto.readyState = FS_proto.INIT = 0;
982
FS_proto.WRITING = 1;
983
FS_proto.DONE = 2;
984
FS_proto.error = null;
985
FS_proto.onwritestart = null;
986
FS_proto.onprogress = null;
987
FS_proto.onwrite = null;
988
FS_proto.onabort = null;
989
FS_proto.onerror = null;
990
FS_proto.onwriteend = null;
991
992
view.addEventListener("unload", process_deletion_queue, false);
993
994
return saveAs;
995
996
}(self));
997
998
return saveAs;
999
})();
1000
1001
return easyrtc_ft;
1002
1003
}));