WebRTC audio/video call and conferencing server.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

449 lines
10 KiB

/*
* Spreed WebRTC.
* Copyright (C) 2013-2015 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, JsSHA) {
var requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem || null;
if (!requestFileSystem) {
console.warn("Browser support for filesystem API not found.");
}
// NOTE(longsleep): A single chunk is 60k - https://code.google.com/p/webrtc/issues/detail?id=2270
// also Firefox does only support 16k maximum sents. See https://code.google.com/p/webrtc/issues/detail?id=2279.
var fileChunkSize = window.webrtcDetectedBrowser === "chrome" ? 60000 : 16000;
// Fake implementation for browsers which do not support FileSystem API.
var FileWriterFake = function(owner) {
this.owner = owner;
this.file = null;
};
FileWriterFake.prototype.flush = function() {
var l = this.owner.writeBuffer.length;
this.owner.e.trigger("written", l, 0);
if (this.owner.complete && !this.file) {
// Sort.
var writeBuffer = this.owner.writeBuffer.slice();
writeBuffer.sort(function(a, b) {
return a[0] - b[0];
});
// Strip sizes.
var dataBuffer = writeBuffer.map(function(d) {
// array contains [size, data]
return d[1];
});
// Create.
var data = new Blob(dataBuffer, {
type: this.owner.info.type || "application/octet-stream"
});
var url = null;
this.file = {
toURL: function() {
if (!url) {
url = URL.createObjectURL(data);
}
return url;
},
remove: function(callback) {
if (url) {
URL.revokeObjectURL(url);
url = null;
}
if (callback) {
callback();
}
}
}
this.owner.file = this.file;
this.owner.e.triggerHandler("complete", [this]);
}
};
FileWriterFake.prototype.stop = function() {
this.file = null;
this.owner = null;
};
// FileSystem API writer.
var FileWriterFileSystem = function(owner) {
this.owner = owner;
this.file = null;
this.writing = false;
this.written = 0;
this.writing_written = 0;
this.writer = null;
this.create()
};
FileWriterFileSystem.prototype.supported = !! requestFileSystem;
FileWriterFileSystem.prototype.filesystem = null;
FileWriterFileSystem.prototype.create = function() {
var generator = _.bind(function() {
FileWriterFileSystem.prototype.filesystem.root.getFile(this.owner.id, {
create: true,
exclusive: true
}, _.bind(function(fileEntry) {
console.log("Generate file", this.owner, fileEntry);
this.setup(fileEntry);
this.flush();
}, this), _.bind(function(e) {
console.warn("Fileystem failed to create file.", e);
this.owner.e.triggerHandler("failed", [e, this.owner]);
}, this));
}, this);
if (!FileWriterFileSystem.prototype.filesystem) {
this.requestFileSystem(null, _.bind(function(fs) {
generator();
}, this), _.bind(function(e) {
this.owner.e.triggerHandler("error", [e, this.owner]);
}, this));
} else {
generator();
}
};
FileWriterFileSystem.prototype.requestFileSystem = function(size, cb, error) {
if (!size) {
// Default size.
size = 5 * 1024 * 1024;
}
requestFileSystem(window.TEMPORARY, size, _.bind(function(fs) {
// Success call.
console.info("Opened file system.", fs.name)
FileWriterFileSystem.prototype.filesystem = fs;
if (cb) {
cb(fs);
}
}, this), _.bind(function(e) {
// Error call.
console.warn("Failed to open file system.", e);
if (error) {
error(e);
}
}, this));
};
FileWriterFileSystem.prototype.setup = function(file) {
this.file = file;
this.file.createWriter(_.bind(function(fileWriter) {
if (this.owner.stopped) {
return;
}
//console.log("Writer created", fileWriter);
fileWriter.onerror = _.bind(function(e) {
console.warn("File write failed: " + e.toString());
if (this.owner) {
this.owner.e.triggerHandler("writeerror", [e, this]);
}
}, this);
fileWriter.onwriteend = _.bind(function(e) {
this.written += this.writing_written;
this.writing_written = 0;
this.writing = false;
//console.log("Done file writing.", e, fileWriter.position, fileWriter.length);
if (this.owner) {
this.owner.e.trigger("written", this.written, this.owner.writeBuffer.length);
this.flush();
}
}, this);
//console.log("created writer for file", this.owner.info.size);
this.writing = true;
this.writer = fileWriter;
this.written = 0;
this.writing_written = 0;
// Create fill with full size.
fileWriter.truncate(this.owner.info.size || 0);
}, this));
};
FileWriterFileSystem.prototype.flush = function() {
if (this.writing || this.owner.stopped || !this.writer) {
return;
}
var buffer = this.owner.writeBuffer;
var length = buffer.length;
if (length) {
this.writing = true;
//console.log("Flushing buffer to file writer", length);
// Grab the current writeBuffer and empty it.
var writeBuffer = buffer.splice(0, length);
// Sort for bytes position.
writeBuffer.sort(function(a, b) {
return a[0] - b[0];
});
//console.log("In flush", writeBuffer.length);
var next;
var start = null;
var pos;
var d;
var s;
var tmp
while (true) {
// Grab next item.
next = writeBuffer.shift();
if (next) {
s = next[0];
d = next[1];
if (start === null) {
// Set write start bytes position and init write data.
start = pos = s;
tmp = [];
}
}
if (pos !== s || !next) {
// We have a gap or nothing more to write, seek and write the shit.
//console.log("eyikes gap or finished", s, pos, next, this.writer.position, tmp.length);
if (this.writer.position !== start) {
//console.log("uhohu - seeking", this.writer.position, start);
this.writer.seek(start);
}
// Write to file system.
this.writing_written = tmp.length;
this.writer.write(new Blob(tmp, {
type: this.owner.info.type || "application/octet-stream"
}));
tmp = null;
if (next) {
// Add rest to buffer for a later flush.
if (writeBuffer.length) {
buffer.unshift.apply(buffer, writeBuffer);
}
buffer.unshift(next);
}
break;
} else {
// Add it to current write set.
//console.log("coolio no gap", s, pos)
tmp.push(d);
pos += d.length;
}
}
}
if (this.owner.complete && !length) {
this.owner.file = this.file;
this.owner.e.triggerHandler("complete", [this]);
}
};
FileWriterFileSystem.prototype.stop = function() {
this.owner = null;
this.writer = null;
this.file = null;
};
// Create a reference to the supported writer.
var FileWriter = FileWriterFileSystem.prototype.supported ? FileWriterFileSystem : FileWriterFake;
// File wrapper.
var File = function(id, file, info) {
this.id = id;
this.file = file;
this.e = $({});
this.scope = {}; // Additional data store.
if (file) {
this.info = {
chunks: this.chunks = Math.ceil(file.size / fileChunkSize),
size: file.size,
name: file.name,
type: file.type
}
this.complete = true;
} else {
this.info = info ? info : {};
this.complete = false;
}
this.stopped = false;
this.writer = null;
this.writeBuffer = [];
};
File.prototype.getChunk = function(chunk_index, cb) {
var start = fileChunkSize * chunk_index;
// Avoid overseeks.
var end = fileChunkSize * (chunk_index + 1);
if (end > this.info.size) {
end = this.info.size;
}
if (start >= end) {
console.warn("Requested file chunk which is out of bound", chunk_index, start, end, this.info);
return;
}
var reader = new FileReader();
var blob = this.file.slice(start, end);
reader.onload = function(event) {
if (reader.readyState == FileReader.DONE) {
cb(event.target.result);
}
};
reader.readAsArrayBuffer(blob);
};
File.prototype.setChunk = function(chunk_index, byte_position, data) {
if (this.stopped) {
return;
}
this.writeBuffer.push([byte_position, data]);
if (this.writer) {
this.writer.flush();
}
};
File.prototype.fileFinished = function() {
if (!this.complete) {
this.complete = true;
this.flushToFilesystem();
}
};
File.prototype.createWriter = function() {
var writer = this.writer = new FileWriter(this);
return writer;
};
File.prototype.flushToFilesystem = function(file) {
if (this.stopped) {
// Cannot do anything.
return;
}
if (!this.writer) {
this.createWriter(file);
} else {
this.writer.flush();
}
};
File.prototype.toURL = function() {
return this.file.toURL();
};
File.prototype.stop = function() {
this.e.triggerHandler("stop", [this]);
if (this.file && this.file.remove) {
var file = this.file;
this.file.remove(_.bind(function() {
console.log("File removed", file);
}, this));
this.file = null;
}
if (this.writer) {
this.writer.stop();
this.writer = null;
}
this.writeBuffer = [];
this.stopped = true;
};
// fileData
return ["randomGen", function(randomGen) {
// NOTE(longsleep): Check if its a good idea to create this new every time when needed.
var randomSecret = randomGen.random();
//console.log("Random secret", [randomSecret]);;
var fileCount = 0;
var filenamePrefix = randomGen.id();
var filesTable = {};
// public API.
var fileData = {
generateFile: function(id, info) {
id = filenamePrefix + id;
var file = filesTable[id] = new File(id, null, info);
file.createWriter();
return file;
},
createFile: function(namespace, data) {
var id = namespace + "_" + (fileCount++);
var shaObj = new JsSHA(id, "TEXT");
var token = shaObj.getHMAC(randomSecret, "TEXT", "SHA-384", "HEX");
var file = filesTable[token] = new File(token, data);
return file;
},
purgeFile: function(id) {
var file = filesTable[id];
if (file) {
file.stop();
delete filesTable[id];
}
},
getFile: function(id) {
return filesTable[id];
}
}
return fileData;
}];
});