From cff56528536d564f8c0c073fac8763f5504b36d6 Mon Sep 17 00:00:00 2001 From: j <0x006A@0x2620.org> Date: Sat, 17 Mar 2012 01:15:06 +0100 Subject: [PATCH] first round of browser based upload --- pandora/urls.py | 2 +- static/js/pandora.js | 5 +- static/js/pandora/menu.js | 4 + static/js/pandora/upload.js | 171 ++++++++++++++ static/js/pandora/uploadDialog.js | 364 ++++++++++++++++++++++++++++++ static/js/pandora/utils.js | 5 + 6 files changed, 547 insertions(+), 4 deletions(-) create mode 100644 static/js/pandora/upload.js create mode 100644 static/js/pandora/uploadDialog.js diff --git a/pandora/urls.py b/pandora/urls.py index 31c7c795..db4d0b74 100644 --- a/pandora/urls.py +++ b/pandora/urls.py @@ -20,7 +20,7 @@ def serve_static_file(path, location, content_type): urlpatterns = patterns('', (r'^admin/', include(admin.site.urls)), - (r'^api/upload/$', 'archive.views.firefogg_upload'), + (r'^api/upload/?$', 'archive.views.firefogg_upload'), (r'^url=(?P.*)$', 'app.views.redirect_url'), (r'^file/(?P.*)$', 'archive.views.lookup_file'), (r'^api/?$', include(ox.django.api.urls)), diff --git a/static/js/pandora.js b/static/js/pandora.js index 9d170afd..3782c718 100644 --- a/static/js/pandora.js +++ b/static/js/pandora.js @@ -208,12 +208,11 @@ appPanel document: $(document), window: $(window) .bind({ + beforeunload: pandora.beforeunloadWindow, resize: function() { pandora.resizeWindow(); }, - unload: function() { - pandora.unloadWindow(); - } + unload: pandora.unloadWindow }) }, site: data.site, diff --git a/static/js/pandora/menu.js b/static/js/pandora/menu.js index 191f7a86..e7827aa9 100644 --- a/static/js/pandora/menu.js +++ b/static/js/pandora/menu.js @@ -31,6 +31,8 @@ pandora.ui.mainMenu = function() { {}, { id: 'preferences', title: 'Preferences...', disabled: isGuest, keyboard: 'control ,' }, { id: 'archives', title: 'Archives...', disabled: /*isGuest*/ true }, + { id: 'upload', title: 'Upload...', + disabled: !pandora.site.capabilities.canUploadVideo[pandora.user.level]}, {}, { id: 'signup', title: 'Sign Up...', disabled: !isGuest }, isGuest ? { id: 'signin', title: 'Sign In...' } @@ -243,6 +245,8 @@ pandora.ui.mainMenu = function() { ['signup', 'signin', 'signout', 'preferences', 'tv', 'help'] ).indexOf(data.id) > -1) { pandora.UI.set({page: data.id}); + } else if (data.id == 'upload') { + pandora.$ui.uploadDialog = pandora.ui.uploadDialog().open(); } else if ([ 'newlist', 'newlistfromselection', 'newsmartlist', 'newsmartlistfromresults' ].indexOf(data.id) > -1) { diff --git a/static/js/pandora/upload.js b/static/js/pandora/upload.js new file mode 100644 index 00000000..58ca3e81 --- /dev/null +++ b/static/js/pandora/upload.js @@ -0,0 +1,171 @@ +// vi:si:et:sw=4:sts=4:ts=4 +'use strict'; + +pandora.ui.upload = function(oshash, file) { + var self = {}, + chunkSize = 1024*1024, + chunkUrl, + format = pandora.site.video.formats[0], + maxRetry = -1, + resolution = Math.max(pandora.site.video.resolutions), + retries = 0, + that = Ox.Element(), + uploadData = {}, + uploadUrl = '/api/upload/?profile='+resolution+'p.'+format+'&id=' + oshash; + + initUpload(); + + function done() { + that.triggerEvent('done', { + status: that.status, + progress: that.progress, + responseText: that.responseText + }); + } + + function initUpload() { + //request upload slot from server + that.status = 'requesting chunk upload'; + that.progress = 0; + self.req = new XMLHttpRequest(); + self.req.addEventListener("load", function (evt) { + var response = {}; + that.responseText = evt.target.responseText; + try { + response = JSON.parse(evt.target.responseText); + } catch(e) { + response = {}; + that.status = "failed to parse response"; + that.progress = -1; + done(); + } + if (response.maxRetry) { + maxRetry = response.maxRetry; + } + chunkUrl = response.uploadUrl; + if (chunkUrl) { + that.status = 'uploading'; + that.progress = 0.0; + //start upload + uploadChunk(0); + } else { + that.status = 'upload failed, no upload url provided'; + that.progress = -1; + done(); + } + }, false); + self.req.addEventListener("error", function (evt) { + that.status = 'uplaod failed'; + that.progress = -1; + that.responseText = evt.target.responseText; + that.triggerEvent('done', tat); + }, false); + self.req.addEventListener("abort", function (evt) { + that.status = 'aborted'; + that.progress = -1; + that.triggerEvent('done', tat); + }, false); + var formData = new FormData(); + Ox.forEach(uploadData, function(value, key) { + formData.append(key, value); + }); + self.req.open("POST", uploadUrl); + self.req.send(formData); + } + + function progress(p) { + that.progress = p; + that.triggerEvent('progress', { + progress: that.progress + }); + } + + function uploadChunk(chunkId) { + var bytesAvailable = file.size, + chunk, + chunkOffset = chunkId * chunkSize; + + if(file.mozSlice) { + chunk = file.mozSlice(chunkOffset, chunkOffset+chunkSize, file.type); + } else if(file.webkitSlice) { + chunk = file.webkitSlice(chunkOffset, chunkOffset+chunkSize, file.type); + } + + progress(parseFloat(chunkOffset)/bytesAvailable); + + self.req = new XMLHttpRequest(); + self.req.addEventListener("load", function (evt) { + var response; + that.responseText = evt.target.responseText; + try { + response = JSON.parse(evt.target.responseText); + } catch(e) { + response = {}; + } + if (response.done == 1) { + //upload finished + that.resultUrl = response.resultUrl; + that.progress = 1; + that.status = 'done'; + done(); + } else if (response.result == 1) { + //reset retry counter + retries = 0; + //start uploading next chunk + uploadChunk(chunkId + 1); + } else { + //failed to upload, try again in 5 second + retries++; + if (maxRetry > 0 && retries > maxRetry) { + that.status = 'uplaod failed'; + that.progress = -1; + done(); + } else { + setTimeout(function() { + uploadChunk(chunkId); + }, 5000); + } + } + }, false); + self.req.addEventListener("error", function (evt) { + //failed to upload, try again in 3 second + retries++; + if (maxRetry > 0 && retries > maxRetry) { + that.status = 'uplaod failed'; + that.progress = -1; + done(); + } else { + setTimeout(function() { + uploadChunk(chunkId); + }, 3000); + } + }, false); + self.req.upload.addEventListener("progress", function (evt) { + if (evt.lengthComputable) { + progress(parseFloat(chunkOffset + evt.loaded) / bytesAvailable); + } + }, false); + self.req.addEventListener("abort", function (evt) { + that.status = 'aborted'; + that.progress = -1; + done(); + }, false); + + var formData = new FormData(); + formData.append('chunkId', chunkId); + if (bytesAvailable <= chunkOffset + chunkSize) { + formData.append('done', 1); + } + formData.append('chunk', chunk); + self.req.open("POST", chunkUrl, true); + self.req.send(formData); + } + + that.abort = function() { + if (self.req) { + self.req.abort(); + self.req = null; + } + }; + return that; +}; diff --git a/static/js/pandora/uploadDialog.js b/static/js/pandora/uploadDialog.js new file mode 100644 index 00000000..01923956 --- /dev/null +++ b/static/js/pandora/uploadDialog.js @@ -0,0 +1,364 @@ +// vim: et:ts=4:sw=4:sts=4:ft=javascript +'use strict'; + +pandora.ui.uploadDialog = function(data) { + var content = Ox.Element().css({margin: '16px'}), + canceled = false, + file, + closeButton, + actionButton, + selectFile, + that = Ox.Dialog({ + buttons: [ + closeButton = Ox.Button({ + id: 'close', + title: 'Close' + }).bindEvent({ + click: function() { + that.triggerEvent('close'); + } + }), + actionButton = Ox.Button({ + id: 'action', + title: 'Select Video' + }).bindEvent({ + click: function() { + if(actionButton.options('title') == 'Select Video') { + if(selectVideo()) { + actionButton.options('title', 'Upload'); + } + } else if(actionButton.options('title') == 'Close') { + that.close(); + } else if(actionButton.options('title') == 'Cancel') { + canceled = true; + pandora.firefogg && pandora.firefogg.cancel(); + pandora.$ui.upload && pandora.$ui.upload.abort(); + actionButton.options('title', 'Select Video'); + } else { + actionButton.options('title', 'Cancel'); + closeButton.hide(); + encode(); + } + } + }) + ], + content: content, + height: 128, + removeOnClose: true, + width: 368, + title: 'Upload Video', + }) + .bindEvent({ + close: function(data) { + if (pandora.firefogg) { + pandora.firefogg.cancel(); + delete pandora.firefogg; + } + that.close(); + } + }), + $info= $('
').css({ + padding: '4px' + }) + .html('Please select the video file you want to upload.'), + $status = $('
').css({ + padding: '4px', + paddingTop: '8px' + }), + $progress; + + pandora._status = $status; + pandora._info = $info; + if(typeof(Firefogg) == 'undefined') { + /* + selectFile = $('') + .attr({ + type: 'file' + }) + .css({ + padding: '8px' + }) + .bind({ + change: function(event) { + if(this.files.length) { + file = this.files[0]; + if(file.type == 'video/webm') { + $status.html(''); + uploadButton.options({ + disabled: false + }); + } else { + $status.html('Currently only WebM files are supported. (Help encoding video)'); + } + } else { + uploadButton.options({ + disabled: true + }); + } + } + }) + .appendTo(content); + */ + actionButton.options({ + title: 'Close' + }); + $info.css({ + paddingTop: '16px', + paddingBottom: '16px' + }).html( + 'Sorry, right now upload is only supported in ' + + 'Firefox ' + + 'with Firefogg installed.' + + '

You can also use pandora_client to upload videos.' + ); + } + content.append($info); + content.append($status); + + + function aspectratio(ratio) { + var numerator, + denominator; + ratio = ratio.split(':'); + numerator = ratio[0]; + if(ratio.length == 2) { + denominator = ratio[1]; + } + if (Math.abs(numerator/denominator - 4/3) < 0.03) { + numerator = 4; + denominator = 3; + } else if (Math.abs(numerator/denominator - 16/9) < 0.02) { + numerator = 16; + denominator = 9; + } + return { + numerator: numerator, + denominator: denominator, + ratio: numerator + ':' + denominator, + 'float': numerator/denominator + }; + } + + function resetProgress() { + $progress = Ox.Progressbar({ + progress: 0, + showPercent: true, + showTime: true, + width: 304 + }); + $status.html('').append($progress); + } + function encode() { + var info = JSON.parse(pandora.firefogg.sourceInfo), + oshash = info.oshash, + filename = pandora.firefogg.sourceFilename, + item; + resetProgress(); + pandora.api.addFile({ + id: oshash, + filename: filename, + info: info + }, function(result) { + item = result.data.item; + pandora.firefogg.encode( + getEncodingOptions(info), + function(result, file) { + result = JSON.parse(result); + if (result.progress != 1) { + if(canceled) { + $status.html('Encoding canceled.'); + } else { + $status.html('Encoding failed.'); + } + delete pandora.firefogg; + return; + } + setTimeout(function() { + //$status.html('uploading... '); + pandora.$ui.upload = pandora.ui.upload(oshash, file) + .bindEvent({ + progress: function(data) { + var progress = data.progress || 0; + $progress.options({progress: 0.5 + progress/2}); + }, + done: function(data) { + pandora.UI.set({ + item: item, + itemView: 'files' + }); + delete pandora.firefogg; + that.close(); + } + }); + }); + }, + function(progress) { + progress = JSON.parse(progress).progress || 0; + $progress.options({progress: progress / 2}); + } + ); + }); + } + + function getEncodingOptions(info) { + var format = pandora.site.video.formats[0], + resolution = Math.max(pandora.site.video.resolutions), + bpp = 0.17, + fps, + dar, + options = {}; + + if(format == 'webm') { + options.videoCodec = 'vp8'; + options.audioCodec = 'vorbis'; + } else if (format == 'ogv') { + options.videoCodec = 'theora'; + options.audioCodec = 'vorbis'; + } + + if (resolution == 720) { + options.height = 720; + options.samplerate = 48000; + options.audioQuality = 5; + } else if (resolution == 480) { + options.height = 480; + options.samplerate = 44100; + options.audioQuality = 3; + options.channels = 2; + } else if (resolution == 360) { + options.height = 320; + options.samplerate = 44100; + options.audioQuality = 1; + options.channels = 1; + } else if (resolution == 240) { + options.height = 240; + options.samplerate = 44100; + options.audioQuality = 0; + options.channels = 1; + } else if (resolution == 96) { + options.height = 96; + options.samplerate = 22050; + options.audioQuality = -1; + options.audioBitrate = 22; + options.channels = 1; + } + if (info.video && info.video[0].display_aspect_ratio) { + dar = aspectratio(info.video[0].display_aspect_ratio); + fps = aspectratio(info.video[0].framerate).float; + options.width = parseInt(dar.float * options.height, 10); + options.width += options.width % 2; + + //interlaced hdv material is detected with double framerates + if (fps == 50) { + options.framerate = 25; + } else if (fps == 60) { + options.framerate = 30; + } + + if (Math.abs(options.width/options.height - dar.float) < 0.02) { + options.aspect = options.width + ':' + options.height; + } else { + options.aspect = dar.ratio; + } + options.videoBitrate = Math.round(options.height*options.width*fps*bpp/1000); + options.denoise = true; + options.deinterlace = true; + } else { + options.noVideo = true; + } + if (info.audio) { + if (options.cannels && info.audio[0].channels < options.channels) { + delete options.channels; + } + } else { + options.noAudio = true; + delete options.samplerate; + delete options.audioQuality; + delete options.channels; + } + + options.noUpscaling = true; + + if((!info.video.length || (info.video[0].codec == options.videoCodec + && info.video[0].height == options.height)) + && (!info.audio.length || info.audio[0].codec == options.audioCodec)) { + options = { + passthrough: true + }; + } + return JSON.stringify(options); + } + + function formatInfo(info) { + var html = ''; + html += '' + info.path + ''; + html += '
'; + if(info.video && info.video.length>0) { + var video = info.video[0]; + html += video.width + 'x' + video.height + ' (' + video.codec + ')'; + } + if(info.video && info.video.length>0 && info.audio && info.audio.length>0) { + html += ' / '; + } + if(info.audio && info.audio.length>0) { + var audio= info.audio[0]; + html += '' + { + 1: 'mono', + 2: 'stereo', + 6: '5.1' + }[audio.channels]; + html += ' ' + audio.samplerate/1000 + ' kHz '; + html += '(' + audio.codec + ')'; + } + html += '
'; + html += '' + Ox.formatValue(info.size, 'B'); + html += ' / ' + Ox.formatDuration(info.duration); + + return html; + } + + function selectVideo() { + canceled = false; + pandora.firefogg = new Firefogg(); + pandora.firefogg.setFormat(pandora.site.video.formats[0]); + if(pandora.firefogg.selectVideo()) { + var info = JSON.parse(pandora.firefogg.sourceInfo), + options = JSON.parse(getEncodingOptions(info)), + oshash = info.oshash, + filename = pandora.firefogg.sourceFilename, + item; + pandora.api.findFiles({ + query: { + conditions: [{key: 'oshash', value: oshash}] + }, + keys: ['id', 'available'] + }, function(result) { + if(result.data.items.length === 0 || !result.data.items[0].available) { + $info.html(formatInfo(info)); + $status.html( + options.passthrough + ? 'Your video will be uploaded directly.' + : 'Your video will be transcoded before upload.'); + } else { + pandora.api.find({ + query: { + conditions: [{key: 'oshash', value: oshash}] + }, + keys: ['id'] + }, function(result) { + pandora.UI.set({ + item: result.data.items[0].id, + itemView: 'files' + }); + delete pandora.firefogg; + that.close(); + }); + } + }); + return true; + } + return false; + } + + return that; +}; diff --git a/static/js/pandora/utils.js b/static/js/pandora/utils.js index 133d6518..d72a8fdb 100644 --- a/static/js/pandora/utils.js +++ b/static/js/pandora/utils.js @@ -1109,6 +1109,11 @@ pandora.selectList = function() { } }; +pandora.beforeunloadWindow = function() { + if (pandora.firefogg) + return "Encoding is currently running\nDo you want to leave this page?"; +} + pandora.unloadWindow = function() { //prevent errors on unload pandora.isUnloading = true;