From 166268a30ccb73a87357d11ec14babee4d45e82d Mon Sep 17 00:00:00 2001 From: j <0x006A@0x2620.org> Date: Fri, 13 Aug 2010 18:55:28 +0200 Subject: [PATCH] add firefogg.jsm, this should move back to firefogg later --- OxFF/modules/firefogg.jsm | 299 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 OxFF/modules/firefogg.jsm diff --git a/OxFF/modules/firefogg.jsm b/OxFF/modules/firefogg.jsm new file mode 100644 index 0000000..31c6ba0 --- /dev/null +++ b/OxFF/modules/firefogg.jsm @@ -0,0 +1,299 @@ +// -*- coding: utf-8 -*- +// vi:si:et:sw=2:sts=2:ts=2 + +let EXPORTED_SYMBOLS = [ "FirefoggUploader" ]; + +const Cc = Components.classes; +const Ci = Components.interfaces; + +function FirefoggUploader(parent, path, url, data) { + this._parent = parent; + this._path = path; + this._url = url; + this._chunkUrl = false; + this._data = data; + this._status = 'initiated'; + this.encodingStatus = 'encoding'; + this.progress = 0.0; + this.chunkSize = 1024*1024; //1MB, is there a need to make this variable? + this.chunksUploaded = 0; + this.chunkStatus = {}; + this._done_cb = function(upload) {}; + this._onProgress = function(progress) {}; + this.filename = 'video.ogv'; + this.canceled = false; + this._retries = 0; + this._maxRetry = -1; + this.ready = false; +} + +/* + Internal helper object dealing with chunked upload as defined at + http://firefogg.org/dev/chunk_post.html +*/ +FirefoggUploader.prototype = { + start: function() { + var boundary = "--------XX" + Math.random(); + + this._status = 'requesting chunk upload'; + + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); + + //XMLHttpRequest status callbacks + var upload = this; + function transferComplete(evt) { + var response = {}; + upload.responseText = evt.target.responseText; + try { + response = JSON.parse(evt.target.responseText); + //dump(evt.target.responseText); + } catch(e) { + dump('FAILED to parse response:\n\n'); + dump(evt.target.responseText); + response = {}; + } + if (response.maxRetry) { + upload._maxRetry = response.maxRetry; + } + upload._chunkUrl = response.uploadUrl; + if (upload._chunkUrl) { + //dump('now tracking chunk uploads to ' + upload._chunkUrl + '\n'); + upload._status = 'uploading'; + upload._progress = 0.0; + upload._uploadChunkId = 0; + upload.uploadChunk(0, false); + } else { + upload._status = 'upload failed, no upload url provided'; + upload._progress = -1; + upload._parent.cancel(); + return; + } + } + function transferFailed(evt) { + upload._status = 'uplaod failed'; + upload._progress = -1; + upload.responseText = evt.target.responseText; + //dump(evt.target.responseText); + } + req.addEventListener("load", transferComplete, false); + req.addEventListener("error", transferFailed, false); + + req.open("POST", this._url); + + var formData = ""; + for (key in this._data) { + formData += "--" + boundary + "\r\n" + + "Content-Disposition: form-data; name=\""+key+"\"\r\n\r\n" + this._data[key] + "\r\n"; + } + formData += "--" + boundary + "--\r\n"; + + var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + var formStream = converter.convertToInputStream(formData); + + req.setRequestHeader("Content-type", "multipart/form-data; boundary=" + boundary); + req.setRequestHeader("Content-length", formStream.available()); + req.send(formStream); + }, + onProgress: function(cb) { + this._onProgress = cb; + }, + done: function(done_cb) { + //used by Firefogg to indicate that encoding is done + //provies callback that is called once upload is done too + this.encodingStatus = 'done'; + this._done_cb = done_cb; + }, + uploadChunk: function(chunkId, last) { + /* + upload chunk with given chunkId, last indicated if this is the last chunk. + once upload is done next chunk is queued + */ + var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + file.initWithPath(this._path); + this.filename = file.leafName; + this.filename = this.filename.replace(/\(\.\d+\).ogv/, '.ogv'); + this.filename = this.filename.replace(/\(\.\d+\).webm/, '.webm'); + + var fileStream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); + fileStream.init(file, -1, -1, false); + fileStream.QueryInterface(Ci.nsISeekableStream); + var f = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); + f.setInputStream(fileStream); + + var bytesAvailable = fileStream.available(); + var chunk = false; + var chunkOffset = chunkId * this.chunkSize; + this.progress = parseFloat(chunkOffset)/bytesAvailable; + + if (last) { + //last chunk, read add remaining data + fileStream.seek(fileStream.NS_SEEK_SET, chunkOffset); + chunk = f.readBytes(bytesAvailable-chunkOffset); + } + else if (this.ready && bytesAvailable >= chunkOffset + this.chunkSize) { + //read next chunk + fileStream.seek(fileStream.NS_SEEK_SET, chunkOffset); + chunk = f.readBytes(this.chunkSize); + } else { + if (this.encodingStatus == 'done' && this._status == 'uploading') { + //uploading is done and last chunk is < chunkSize, upload remaining data + this._status = 'success'; + this.uploadChunk(chunkId, true); + } else { + //encoding not ready. wait for 2 seconds and try again + var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback( + function(obj, chunkId, last) { + return function() { obj.uploadChunk(chunkId, last) } + }(this, chunkId, last), + 2000, Ci.nsITimer.TYPE_ONE_SHOT); + } + f.close(); + fileStream.close(); + return; + } + f.close(); + fileStream.close(); + + //FIXME: should this be checked so it does not get called again? + this.chunkStatus[chunkId] = {}; + + //dump('POST ' + this._chunkUrl + ' uploading chunk ' + chunkId +' ('+chunk.length+' bytes)\n'); + + var boundary = "--------XX" + Math.random(); + + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); + + var upload = this; + function transferComplete(evt) { + //if server resonds with valid {result=1} and status was is still 'uploading' go on to next chunk + upload.responseText = evt.target.responseText; + try { + var response = JSON.parse(evt.target.responseText); + } catch(e) { + dump('FAILED to parse response:\n\n'); + dump(evt.target.responseText); + var response = {}; + } + /* + if (upload.canceled) { + //FIXME: should we let the server know that upload was canceled? + } + else */ + + /* + dump('\n\n'); + dump(evt.target.responseText); + dump('\n************\n\n'); + */ + if (response.done == 1) { + //upload finished, update state and expose result + upload.resultUrl = response.resultUrl; + upload.progress = 1; + if (upload._done_cb) + upload._done_cb(upload); + //reset retry counter + upload._retries = 0; + } + else if (response.result == 1) { + //start uploading next chunk + upload._uploadChunkId = chunkId + 1; + upload.uploadChunk(upload._uploadChunkId, false); + //upload status + upload.chunkStatus[chunkId].progress = 1; + upload.chunkStatus[chunkId].loaded = evt.loaded; + upload.chunksUploaded++; + //reset retry counter + upload._retries = 0; + } else { + //failed to upload, try again in 3 second + //dump('could not parse response, failed to upload, will try again in 3 second\n'); + //dump(evt.target.responseText); + if (upload.max_retry > 0 && upload._retries > upload.max_retry) { + upload.cancel(); + upload._status = 'uplaod failed'; + upload._progress = -1; + } else { + upload._retries++; + var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback( + function(obj, chunkId, last) { + return function() { obj.uploadChunk(chunkId, last); }; + }(upload, chunkId, last), 3000, Ci.nsITimer.TYPE_ONE_SHOT); + } + } + } + function transferFailed(evt) { + //failed to upload, try again in 3 second + //dump('transferFailed, will try again in 3 second\n'); + if (upload.max_retry > 0 && upload._retries > upload.max_retry) { + upload.cancel(); + upload._status = 'uplaod failed'; + upload._progress = -1; + } else { + upload._retries++; + var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback( + function(obj, chunkId, last) { + return function() { obj.uploadChunk(chunkId, last); }; + }(upload, chunkId, last), 3000, Ci.nsITimer.TYPE_ONE_SHOT); + } + } + function updateProgress(evt) { + if (evt.lengthComputable) { + upload.progress = parseFloat(chunkOffset + evt.loaded)/bytesAvailable; + upload.chunkStatus[chunkId].loaded = evt.loaded; + upload.chunkStatus[chunkId].loaded = evt.total; + //dump('progress: chunk ' + chunkId + ' uploaded ' + evt.loaded + '\n'); + upload._onProgress(upload.progress); + } + } + req.upload.addEventListener("progress", updateProgress, false); + + req.addEventListener("load", transferComplete, false); + req.addEventListener("error", transferFailed, false); + + req.open("POST", this._chunkUrl); + + var chunk = "--" + boundary + "\r\n" + + "Content-Disposition: form-data; name=\"chunk\"; filename=\"" + this.filename + "\"\r\n" + + "Content-type: video/ogg\r\n\r\n" + chunk; + var chunkStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); + chunkStream.setData(chunk, chunk.length); + + // write done flag into stream + var formData = "\r\n"; + var _data = { + chunkId: chunkId + }; + if (last) { + _data['done'] = 1; + } + for (key in _data) { + formData += "--" + boundary + "\r\n" + + "Content-Disposition: form-data; name=\""+key+"\"\r\n\r\n" + _data[key] + "\r\n"; + } + formData += "--" + boundary + "--\r\n"; + var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + var formStream = converter.convertToInputStream(formData); + + var multiStream = Cc["@mozilla.org/io/multiplex-input-stream;1"].createInstance(Ci.nsIMultiplexInputStream); + multiStream.appendStream(chunkStream); + multiStream.appendStream(formStream); + + //send mupltiplrex stream + req.setRequestHeader("Content-type", "multipart/form-data; boundary=" + boundary); + req.setRequestHeader("Content-length", multiStream.available()); + //this._current_req = req; + req.send(multiStream); + }, + cancel: function() { + this.canceled = true; + if (this._current_req) { + this._current_req.abort(); + this._current_req = null; + } + }, +}