Uploading large files via XHR fails with Chrome, works with Firefox - javascript

I am uploading video files to my server. The files are at least 20MB, some over 100MB.
For improved user experience, I upload via JavaScript and XMLHttpRequest, this way I can display upload speed and remaining time.
And to avoid and trouble on the server (such as requests timing out and taking too long to process) I submit the file in little packages on the server, and have a php script re-assemble the file.
My script works great, with one weird catch - and until just now I thought it was because of my ISP.
Using Google chrome I can upload files up to 20MB with no problems. But anything larger gets errors: For example my 100MB file will not send anything to the server - the second package never arrives. On my 50MB file it happens after around 47%, with the 7th package. And another file doesn't even send the first package.
I restarted my computer, and it keeps happening at the same position/package number for each file - though the position has nothing in common compared to the other failed files.
It doesn't matter if you try to start after one of the failed packages, say If I start at #8 if 7 failed - it will continue to fail. If I ignore errors (rather than to try again) it will just send the rest of the file in empty chunks.
I had already tried from a different internet connection, though I had to use firefox there. And it worked fine. So I install firefox on my machiene, and BAM works like a charm, correctly sending the 100MB file.
What could be going wrong on Chrome?
$(document).on('click','#video_upload',function(evt){
uploadProcess('vod_video_file');
});
function toBlob(text)
{
var data = new ArrayBuffer(text.length);
var ui8a = new Uint8Array(data, 0);
for (var i = 0; i < text.length; i++) ui8a[i] = (text.charCodeAt(i) & 0xff);
if(typeof window.Blob == "function")
{
var blob = new Blob([data]);
}else{
var bb = new (window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder)();
bb.append(data);
var blob = bb.getBlob();
}
return blob;
}
function splitFile(dataArray, size) {
blobs = new Array();
for (var i = 0; i < dataArray.size; i += size)
{
var copy = dataArray.slice();
var partial = copy.slice(i, i+size);
blobs.push(partial);
}
return blobs;
}
function uploadProcess(fileInputId)
{
var file = document.getElementById(fileInputId).files[0];
var reader = new FileReader();
reader.readAsBinaryString(file);
reader.onloadend = function(evt)
{
var fr = evt.target.result;
fileUpload( fr );
}
}
function fileUpload(inputDataArray)
{
var since;
var intervalid;
var totalBytes = inputDataArray.length;
var packets = new Array();
var packetNum = 0;
var packetCount = 0;
var packetSize = 0;
function startUpload()
{
intervalid = setInterval(function(){updateUploadStats();},1000);
calculatePaketSize()
createPackets();
submitPacket();
}
function calculatePaketSize()
{
var ideal_size = 3*1024*1024;
var packet_count = Math.ceil( totalBytes/ideal_size);
packetSize = Math.ceil(totalBytes/packet_count);
}
function createPackets()
{
packets = splitFile(toBlob(inputDataArray), packetSize)
packetCount = packets.length;
}
function updateUploadStats(e)
{
//displaying upload progress in GUI
}
function submitPacket()
{
xhr = new XMLHttpRequest();
xhr.open("POST", 'index.php?controller=AdminVodVideo&action=VideoUpload&ajax=1&r='+packetNum+'&token='+token, true);
xhr.setRequestHeader("Content-type","application/octet-stream");
XMLHttpRequest.prototype.mySendAsBinary = function(text){
this.send(text);
}
var eventSource = xhr.upload || xhr;
eventSource.addEventListener("progress", function(e) {
updateUploadStats(e);
});
xhr.onreadystatechange = function()
{
if(xhr.readyState == 4)
{
if(xhr.status == 200)
{
//server will return the string 'upload failed' if the file to be received was empty.
if( xhr.responseText == 'upload failed')
{
console.log('FAILED , trying again in 3 s');
setTimeout(submitPacket,3000);
}
else
{
updateUploadStats();
packetNum++;
if(packetNum == packetCount)
{
processOnServer();
}
else
{
submitPacket();
}
}
}else{
// process error
console.log('we got a 500 error');
}
}
};
since = Date.now();
xhr.mySendAsBinary( packets[packetNum] );
}
function processOnServer()
{
//telling the server to piece the file back together.
}
startUpload();
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

Related

JavaScript - Send ArrayBuffer data to backend over websocket [guacamole]

I need to send data (a file) via websocket to my guacamole backend using the guacamole-common-js library.
The scenario is the following:
Drag and drop area is created
User puts a file in this area
File is read
A guac filestream is created and the file is sent to the guac backend
Steps 1 to 3 are already working, but I do not know, how to send the file to the guacamole backend.
That's my function when a file is dropped: (guac is a global var that initialized the Guacamole-Client function)
function drop(ev){
ev.preventDefault();
if (ev.dataTransfer.items) {
for (var i = 0; i < ev.dataTransfer.items.length; i++) {
if (ev.dataTransfer.items[i].kind === 'file') {
var file = ev.dataTransfer.items[i].getAsFile();
var reader = new FileReader();
reader.onloadend = function fileContentsLoaded (e){
const stream = guac.createFileStream(file.type, file.name);
const bytes = new Uint8Array(reader.result);
stream.sendBlob(bytes.buffer)
stream.sendEnd()
};
console.log(file)
reader.readAsArrayBuffer(file);
}
}
} else {
for (var i = 0; i < ev.dataTransfer.files.length; i++) {
console.log(ev.dataTransfer.files[i].name);
}
}
}
The backend is also receiving the data and I am able to open the file on the remote server to which the file is sent by guacd, but the file does only contain kind of binary data.
Does someone already managed this or has an idea how I could send the data?
If you use Node.js in Backend, try handle it with Buffer.toString or Buffer.from.
I already found a solution...
The guacamole-common-js lib already provides a function for sending the buffer to the backend...
My "drop" functions looks now the following:
function drop(ev){
ev.preventDefault();
if (ev.dataTransfer.items) {
for (var i = 0; i < ev.dataTransfer.items.length; i++) {
if (ev.dataTransfer.items[i].kind === 'file') {
var file = ev.dataTransfer.items[i].getAsFile();
var reader = new FileReader();
reader.onloadend = function fileContentsLoaded (e){
const stream = guac.createFileStream(file.type, file.name);
var bufferWriter = new Guacamole.ArrayBufferWriter(stream)
bufferWriter.sendData(reader.result)
bufferWriter.sendEnd()
};
reader.readAsArrayBuffer(file);
}
}
} else {
for (var i = 0; i < ev.dataTransfer.files.length; i++) {
console.log(ev.dataTransfer.files[i].name);
}
}
}

Migrate FileReader ReadAsBinaryString() to ReadAsArrayBuffer() or ReadAsText()

I realize that the new Mozilla Firefox return allocation size overflow (on FileReader.ReadAsBinaryString()) when the file bigger than 200MB (something like that).
Here's some of my code on test for client web browser:
function upload(fileInputId, fileIndex)
{
var file = document.getElementById(fileInputId).files[fileIndex];
var blob;
var reader = new FileReader();
reader.readAsBinaryString(file);
reader.onloadend = function(evt)
{
xhr = new XMLHttpRequest();
xhr.open("POST", "upload.php", true);
XMLHttpRequest.prototype.mySendAsBinary = function(text){
var data = new ArrayBuffer(text.length);
var ui8a = new Uint8Array(data, 0);
for (var i = 0; i < text.length; i++){
ui8a[i] = (text.charCodeAt(i) & 0xff);
}
if(typeof window.Blob == "function")
{
blob = new Blob([data]);
}else{
var bb = new (window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder)();
bb.append(data);
blob = bb.getBlob();
}
this.send(blob);
}
var eventSource = xhr.upload || xhr;
eventSource.addEventListener("progress", function(e) {
var position = e.position || e.loaded;
var total = e.totalSize || e.total;
var percentage = Math.round((position/total)*100);
});
xhr.onreadystatechange = function()
{
if(xhr.readyState == 4)
{
if(xhr.status == 200)
{
console.log("Done");
}else{
console.log("Fail");
}
}
};
xhr.mySendAsBinary(evt.target.result);
};
}
So I tried change it to FileReader.ReadAsArrayBuffer(), the error has not shown up but the data are not the same (as it's not read as binary string).
Did anyone has any solution to solve this problem? Is there any way that we can upload bigger file from JS to Web Server in raw/string other than FileReader implementation?
I read on Mozilla JS Documentation that said:
This feature is non-standard and is not on a standards track. Do not
use it on production sites facing the Web: it will not work for every
user. There may also be large incompatibilities between
implementations and the behavior may change in the future. - Mozilla
If not ReadAsBinaryString, the how to implement ReadAsArrayBuffer or ReadAsText
To send Files to a web-server, you simply don't need js. HTML alone is well able to do this with the <form> element.
Now if you want to go through js, for e.g catch the different ProgressEvents, then you can send directly your File, no need to read it whatsoever on your side.
To do this, you've got two (or three) solutions.
If your server is able to handle PUT requests, you can simply xhr.send(file);.
Otherwise, you'd have to go through a FormData.
// if you really want to go the XHR way
document.forms[0].onsubmit = function handleSubmit(evt) {
if(!window.FormData) { // old browser use the <form>
return;
}
// now we handle the submit through js
evt.preventDefault();
var fD = new FormData(this);
var xhr = new XMLHttpRequest();
xhr.onprogress = function handleProgress(evt){};
xhr.onload = function handleLoad(evt){};
xhr.onerror = function handleError(evt){};
xhr.open(this.method, this.action);
// xhr.send(fD); // won't work in StackSnippet
log(fD, this.method, this.action); // so we just log its content
};
function log(formData, method, action) {
console.log('would have sent');
for(let [key, val] of formData.entries())
console.log(key, val);
console.log('through', method);
console.log('to', action);
}
<!-- this in itself is enough -->
<form method="POST" action="your_server.page">
<input type="file" name="file_upload">
<input type="submit">
</form>
Now, you sent a comment saying that you can't upload Files bigger than 1GB to your server.
This limitation is only due to your server's config, so the best if you want to accept such big files is to configure it correctly.
But if you really want to send your File by chunks, even then don't get off of the Blob interface.
Indeed Blobs have a slice() method, so use it.
document.forms[0].onsubmit = function handleSubmit(evt) {
evt.preventDefault();
var file = this.elements[0].files[0];
var processed = 0;
if(file) {
// var MAX_CHUNK_SIZE = Math.min(file.size, server_max_size);
// for demo we just split in 10 chunks
var MAX_CHUNK_SIZE = file.size > 10 ? (file.size / 10) | 0 : 1;
loadChunk(0);
}
function loadChunk(start) {
var fD = new FormData();
var sliced = file.slice(start, start+MAX_CHUNK_SIZE);
processed += sliced.size; // only for demo
fD.append('file_upload', sliced, file.name);
fD.append('starting_index', start);
if(start + MAX_CHUNK_SIZE >= file.size) {
fD.append('last_chunk', true);
}
var xhr = new XMLHttpRequest();
xhr.open('POST', 'your_server.page');
xhr.onload = function onchunkposted(evt) {
if(start + MAX_CHUNK_SIZE >= file.size) {
console.log('All done. Original file size: %s, total of chunks sizes %s', file.size, processed);
return;
}
loadChunk(start + MAX_CHUNK_SIZE);
};
// xhr.send(fD);
log(fD);
setTimeout(xhr.onload, 200); // fake XHR onload
}
};
function log(formData, method, action) {
console.log('would have sent');
for(let [key, val] of formData.entries())
console.log(key, val);
}
<form method="POST" action="your_server.page">
<input type="file" name="file_upload">
<input type="submit">
</form>
But you absolutely don't need to go through a FileReader for this operation.
Actually the only case where it could make sense to use a FileReader here would be for some Android browsers that don't support passing Blob into a FormData, even though they don't give a single clue about it.
So in this case, you'd have to set up your server to let you know the request was empty, and then only read the File as a dataURI that you would send in-place of the original File.
after a long week of research and sleepless nights, you can't upload binary strings without breaking it, also base64 doesn't work for all files, only images, the journey from the client-side to the server breaks the bytes being sent
Kaiido statement is correct
To send Files to a web-server, you simply don't need js
But that doesn't answer my question. Using the Simple XMLHttpRequest() can upload the file and track those progress as well. But still, it's not it. The direct upload, either from the <form> or using XMLHttpRequest() will need to increase your upload limit in php setting. This method is not convenience for me. How if the client upload file as 4GB? So I need to increase to 4GB. Then next time, client upload file as 6GB, then I have to increase to 6GB.
Using the slice() method is make sense for bigger file as we can send it part by part to server. But this time I am not using it yet.
Here's some of my test the worked as I want. I hope some expert could correct me if I am wrong.
My Upload.js
function upload(fileInputId, fileIndex)
{
var file = document.getElementById(fileInputId).files[fileIndex];
var blob;
var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onloadend = function(evt)
{
xhr = new XMLHttpRequest();
xhr.open("POST", "upload.php?name=" + base64_encode(file.name), true);
XMLHttpRequest.prototype.mySendAsBinary = function(text){
var ui8a = new Uint8Array(new Int8Array(text));
if(typeof window.Blob == "function")
{
blob = new Blob([ui8a]);
}else{
var bb = new (window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder)();
bb.append(ui8a);
blob = bb.getBlob();
}
this.send(blob);
}
var eventSource = xhr.upload || xhr;
eventSource.addEventListener("progress", function(e) {
var position = e.position || e.loaded;
var total = e.totalSize || e.total;
var percentage = Math.round((position/total)*100);
console.log(percentage);
});
xhr.onreadystatechange = function()
{
if(xhr.readyState == 4)
{
if(xhr.status == 200)
{
console.log("Done");
}else{
console.log("Fail");
}
}
};
xhr.mySendAsBinary(evt.target.result);
};
}
Below is how the PHP server listen to the ArrayBuffer from JS
if(isset($_GET["name"])){
$name = base64_decode($_GET["name"]);
$loc = $name;
$inputHandler = fopen('php://input', "r");
$fileHandler = fopen($loc, "w+");
while(true) {
//$buffer = fgets($inputHandler, 1024);
$buffer = fread($inputHandler, 1000000);
if (strlen($buffer) == 0) {
fclose($inputHandler);
fclose($fileHandler);
return true;
}
//$b = base64_encode($buffer);
fwrite($fileHandler, $buffer);
}
}
The above method works well. The FileReader read the file as ArrayBuffer the upload to server. For me, migrating from ReadAsBinaryString() to ReadAsArrayBuffer() is important and ReadAsArrayBuffer() has some better performance rather than ReadAsBinaryString()
Here's some reason, why some developer relies to FileReader API:
Streaming. Using this method, the file will be stream, so we can avoid setting the php multiple time.
Easy Encrypt. As the file is sending via ArrayBuffer, it is easy for developer to Encrypt the file while upload in progress.
This method also support upload any type of file. I ve done some test and I realize that ReadAsArrayBuffer() method are more faster than ReadAsBinaryString() and direct form upload. You may try it.
Security Notice
The above code is only under test code, to use it in production, you have to consider sending the data in GET or POST under HTTPS.

HTML Rename download link

I have a mp3 link like this :
http://example.com/932937293723.mp3
but i want to rename it when user downloads the file to be like this
http://example.com/Artist - Title.mp3
My code :
DOWNLOAD
The mp3 file stored in remote server. And i'm not the owner of that server.
HTML download attribute seem not good solution. because it's not cross-browser. Any cross-browser solution to solve this ? Javascript maybe :D
If you insist on working from the front end, try working with the following code. The getblob method is depreciated, but you need to update that side. Let me know.
function getBinary(file){
var xhr = new XMLHttpRequest();
xhr.open("GET", file, false);
xhr.overrideMimeType("text/plain; charset=x-user-defined");
xhr.send(null);
return xhr.responseText;
}
function sendBinary(data, url){
var xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
if (typeof XMLHttpRequest.prototype.sendAsBinary == "function") { // Firefox 3 & 4
var tmp = '';
for (var i = 0; i < data.length; i++) tmp += String.fromCharCode(data.charCodeAt(i) & 0xff);
data = tmp;
}
else { // Chrome 9
// http://javascript0.org/wiki/Portable_sendAsBinary
XMLHttpRequest.prototype.sendAsBinary = function(text){
var data = new ArrayBuffer(text.length);
var ui8a = new Uint8Array(data, 0);
for (var i = 0; i < text.length; i++) ui8a[i] = (text.charCodeAt(i) & 0xff);
var bb = new BlobBuilder(); // doesn't exist in Firefox 4
bb.append(data);
var blob = bb.getBlob();
this.send(blob);
}
}
xhr.sendAsBinary(data);
}
var data = getBinary("My music.mp3");
sendBinary(data,'http://www.tonycuffe.com/mp3/tailtoddle_lo.mp3');
In your back end code, you can fetch the file to your server, store it to a variable, rename it from there, define the corresponding headers, and return it. this could happen as an ajax call initiated on the javascript click.
Post further details about your backed and i can help you more.
You can use something like below (ASP.NET)
In ASPX
Download
In ASP.NET
Response.ContentType = "audio/mpeg3";
Response.AddHeader("content-disposition", "attachment;filename=New_file_name.mp3");
Server.Transfer(decoded_URL_of_MP3_file);
Look here for other MIME types
Update#1 - Using Javascript alone, you can try something like this, though I've not tested in different browsers
function Download(url, fancyFileName)
{
var file = document.createElement('a');
file.href = url;
file.target = '_blank';
file.download = fancyFileName;
var event = document.createEvent('Event');
event.initEvent('click', true, true);
file.dispatchEvent(event);
window.URL.revokeObjectURL(file.href);
}
Download('http://server.com/file.mp3','Artist_file.mp3');

Silence when Playing AAC Chunks

I'm attempting to use Aurora.JS to play audio received from a streaming AAC-encoded source. I'm successfully pulling chunked data, and trying to feed it into a custom emitter, but no audio is actually playing.
Maybe I'm missing something very simple. Here's a sample of what I'm trying to do:
http://jsfiddle.net/Rc6Su/4/
(You're almost certainly gonna get a CORS error when hitting "Play" because the source is cross-domain. The only way I can easily get around that is using this plugin: https://chrome.google.com/webstore/detail/allow-control-allow-origi/nlfbmbojpeacfghkpbjhddihlkkiljbi/related?hl=en)
Before you mention it, this is going into a PhoneGap app and so the cross-domain issue isn't going to be a problem.
The problem code is somewhere in here:
var aurora_source = null;
var player = null;
function make_noise(chunk) {
var uarr = (function (chunk) {
var buf = new ArrayBuffer(chunk.length * 2); // 2 bytes for each character
var bufView = new Uint8Array(buf);
for (var i=0, strLen=chunk.length; i<strLen; i++) {
bufView[i] = chunk.charCodeAt(i);
}
return buf;
})(chunk);
var abData = new AV.Buffer(uarr);
if (!aurora_source) {
var MySource = AV.EventEmitter.extend ({
start : function () {
this.emit('data', abData);
},
pause : function () {
},
reset : function () {
}
});
aurora_source = new MySource();
asset = new AV.Asset(aurora_source);
player = new AV.Player(asset);
player.play();
} else {
$("#debug").append("emit data");
$("#debug").append("\n");
aurora_source.emit('data', abData);
}
}
Could not get audio to play, but found at least that
bufView[i] = chunk.charCodeAt(i);
may have to be replaced by
bufView[i] = chunk.charCodeAt(i) & 0xff;
see What does charCodeAt(...) & 0xff accomplish?
hope it helps.

Javascript ReferenceError: Undefined variable: FormData

I'm having trouble with the code below. The script sends 2 variables and a file to a php-script to upload it to a server. In Firefox and Chrome everything runs smoothly but in Opera I get "ReferenceError: Undefined variable: FormData".
Can't test in IE or Safari because I'm using the File API. There's other functions in the script but only these 2 are important because the error is caused here.
datumActiviteit = "testxx";
naamActiviteit = "testyy";
function sendFiles() {
try{
var imgs = document.querySelectorAll(".obj");
for (var i = 0; i < imgs.length; i++) {
new BestandenUploaden(imgs[i],imgs[i].file);
}
}
catch(ex){alert(ex);}
}
function BestandenUploaden(img,file){
try{
var formData = new FormData();
formData.append("activiteit", naamActiviteit);
formData.append("datum", datumActiviteit);
formData.append("bestand", file);
var oXHR = new XMLHttpRequest();
oXHR.open("POST", "launcherV2.php");
oXHR.onreadystatechange = function (oEvent) {
if (oXHR.readyState==4 && oXHR.status==200) {
if (oXHR.responseText == "continue") {
img.parentNode.lastChild.style.opacity = "1.0";
img.parentNode.lastChild.style.backgroundColor = "transparent";
img.parentNode.lastChild.style.backgroundImage = "url(../afbeeldingen/rocket/complete.png)";
}
else {
window.alert(oXHR.responseText);
}
}
else{
window.alert("readyState or status error :", oXHR.statusText);
}
};
oXHR.send(formData);
}
catch(err){alert(err)};
};
Does anyone have a clue why only Opera(v11.62) would throw this error?
FormData is only available in Opera v12 and up:
http://caniuse.com/#search=FormData

Categories

Resources