diff options
Diffstat (limited to 'web/stream.gathering.org/resources/js/video-js-resolutions.js')
-rw-r--r-- | web/stream.gathering.org/resources/js/video-js-resolutions.js | 432 |
1 files changed, 432 insertions, 0 deletions
diff --git a/web/stream.gathering.org/resources/js/video-js-resolutions.js b/web/stream.gathering.org/resources/js/video-js-resolutions.js new file mode 100644 index 0000000..b07a7ac --- /dev/null +++ b/web/stream.gathering.org/resources/js/video-js-resolutions.js @@ -0,0 +1,432 @@ +// Resolution switching support for videojs +// +// In this plugin I'm really going out of my way to *not* override the +// core videojs namespace and to *not* change the core API. As a +// result this plugin is not as efficient as it might be. It +// initializes itself *for each player* as scoped variables inside the +// plugin closure and grafts itself on to *the instance on which it was +// called* rather than on the videojs player prototype. I don't expect +// this to be a big deal for anybody. +videojs.plugin('resolutions', function(options) { + var player = this; + + // 'reduce' utility method + // @param {Array} array to iterate over + // @param {Function} iterator function for collector + // @param {Array|Object|Number|String} initial collector + // @return collector + vjs.reduce = function(arr, fn, init, n) { + if (!arr || arr.length === 0) { return; } + for (var i=0,j=arr.length; i<j; i++) { + init = fn.call(arr, init, arr[i], i); + } + return init; + }; + + this.resolutions_ = { + options_: {}, + + // takes an existing stream and stops the download entirely + // without killing the player or disposing of the tech + stopStream: function(){ + switch(player.techName){ + case "Html5": + break; + case "Flash": + player.tech.el_.vjs_stop(); + break; + } + + // this may cause flash or the native player to emit errors but + // they are harmless + player.src(""); + }, + + // it is necessary to remove the sources from the DOM after + // parsing them because otherwise the native player may be + // inclined to stream both sources + removeSources: function(el){ + var videoEl = player.el_.getElementsByTagName("video")[0]; + + if (player.techName !== "Html5" || !videoEl) return; + + var srcs = videoEl.getElementsByTagName("source"); + for(var i=0;i<srcs.length;i++){ + videoEl.removeChild(srcs[i]); + } + }, + + // buckets all parsed sources by their type ("video/mp4", for example) + // @param {Array} array of sources: + // [ + // { + // "data-res": "HD", + // "type": "video/mp4", + // "src": "http://some_video_url_hd" + // }, + // { + // "data-default": "true", + // "data-res": "SD", + // "type": "video/mp4", + // "src": "http://some_video_url_sd" + // }, + // { + // "data-default": "true", + // "data-res": "SD", + // "type": "video/ogv", + // "src": "http://some_video_url_sd" + // } + // ] + // @return sources grouped by type: + // { + // "video/mp4": [ + // { + // "data-res": "HD", + // "type": "video/mp4", + // "src": "http://some_video_url_hd" + // }, + // { + // "data-default": "true", + // "data-res": "SD", + // "type": "video/mp4", + // "src": "http://some_video_url_sd" + // } + // ] + // "video/ogv": [ + // { + // "data-res": "SD", + // "type": "video/ogv", + // "src": "http://some_video_url_sd" + // } + // ] + // } + bucketByTypes: function(sources){ + return vjs.reduce(sources, function(init, val, i){ + (init[val.type] = init[val.type] || []).push(val); + return init; + }, {}, player); + }, + + // takes parsed sources and selects the most appropriate source + // taking into account resolution, technology support, and the + // user's previous selections. also indexes the sources + // @param {Array} array of sources: + // [ + // { + // "data-res": "HD", + // "type": "video/mp4", + // "src": "http://some_video_url_hd" + // }, + // { + // "data-default": "true", + // "data-res": "SD", + // "type": "video/mp4", + // "src": "http://some_video_url_sd" + // }, + // { + // "data-default": "true", + // "data-res": "SD", + // "type": "video/ogv", + // "src": "http://some_video_url_sd" + // } + // ] + // @return {Object} single source: + // { + // "data-res": "HD", + // "type": "video/mp4", + // "src": "http://some_video_url_jd", + // "index": 0 + // } + selectSource: function(sources){ + this.removeSources(); + + var sourcesByType = this.bucketByTypes(sources); + var typeAndTech = this.selectTypeAndTech(sources); + + if (!typeAndTech) return false; + + // even though we choose the best resolution for the user here, we + // should remember the resolutions so that we can potentially + // change resolution later + this.options_['sourceResolutions'] = sourcesByType[typeAndTech.type]; + + return this.selectResolution(this.options_['sourceResolutions']); + }, + + // takes parsed sources and returns the most appropriate + // technology and video type + // @param {Array} array of sources: + // [ + // { + // "data-res": "HD", + // "type": "video/mp4", + // "src": "http://some_video_url_hd" + // }, + // { + // "data-default": "true", + // "data-res": "SD", + // "type": "video/mp4", + // "src": "http://some_video_url_sd" + // }, + // { + // "data-default": "true", + // "data-res": "SD", + // "type": "video/ogv", + // "src": "http://some_video_url_sd" + // } + // ] + // @return {Object} type/tech: + // { + // "type": "video/ogv", + // "tech": "Html5" + // } + selectTypeAndTech: function(sources) { + var techName; + var tech; + + for (var i=0,j=player.options_['techOrder'];i<j.length;i++) { + techName = videojs.capitalize(j[i]); + tech = window['videojs'][techName]; + + // Check if the browser supports this technology + if (tech.isSupported()) { + // Loop through each source object + for (var a=0,b=sources;a<b.length;a++) { + var source = b[a]; + // Check if source can be played with this technology + //if (tech['canPlaySource'](source)) { + return { type: source.type, tech: techName }; + //} + } + } + } + }, + + // takes an array of sources of homogeneous type (ie. a complete + // "bucket" from the output of bucketByTypes) and returns the best + // source, taking into account the user's previous preferences + // stored in local storage + // @param {Array} homogeneous sources: + // [ + // { + // "data-res": "HD", + // "type": "video/mp4", + // "src": "http://some_video_url_hd" + // }, + // { + // "data-default": "true", + // "data-res": "SD", + // "type": "video/mp4", + // "src": "http://some_video_url_sd" + // } + // ] + // @return {Object} singular best source: + // { + // "data-default": "true", + // "data-res": "SD", + // "type": "video/mp4", + // "src": "http://some_video_url_sd" + // "index": 1 + // } + selectResolution: function(typeSources) { + var defaultRes = 0; + var supportsLocalStorage = !!window.localStorage; + + // check to see if any sources are marked as default + videojs.obj.each(typeSources, function(i, s){ + // add the index here so we can reference it later + s.index = parseInt(i, 10); + + if (s['data-default']) defaultRes = s.index; + }, player); + + // if the user has previously selected a preference, check if + // that preference is available. if not, use the source marked + // default + var preferredRes = defaultRes; + + // trying to follow the videojs code conventions of if statements + if (supportsLocalStorage){ + var storedRes = parseInt(window.localStorage.getItem('videojs_preferred_res'), 10); + + if (!isNaN(storedRes)) + preferredRes = storedRes; + } + + var maxRes = (typeSources.length - 1); + var actualRes = preferredRes > maxRes ? maxRes : preferredRes; + + return typeSources[actualRes]; + } + }; + + // convenience method + // @return {String} cached resolution label: + // "SD" + player.resolution = function(){ + return this.cache_.src.res; + }; + + // takes a source and switches the player's stream to it on the fly + // @param {Object} singular source: + // { + // "data-default": "true", + // "data-res": "SD", + // "type": "video/mp4", + // "src": "http://some_video_url_sd" + // } + player.changeResolution = function(new_source){ + // has the exact same source been chosen? + if (this.cache_.src === new_source.src){ + this.trigger('resolutionchange'); + return this; // basically a no-op + } + + // remember our position and playback state + var curTime = this.currentTime(); + var remainPaused = this.paused(); + + // pause playback + this.pause(); + + // attempts to stop the download of the existing video + this.resolutions_.stopStream(); + + // HTML5 tends to not recover from reloading the tech but it can + // generally handle changing src. Flash generally cannot handle + // changing src but can reload its tech. + if (this.techName === "Html5"){ + this.src(new_source.src); + } else { + this.loadTech(this.techName, {src: new_source.src}); + } + + // when the technology is re-started, kick off the new stream + this.ready(function() { + this.one('loadeddata', vjs.bind(this, function() { + this.currentTime(curTime); + })); + + this.trigger('resolutionchange'); + + if (!remainPaused) { + this.load(); + this.play(); + } + + // remember this selection + vjs.setLocalStorage('videojs_preferred_res', parseInt(new_source.index, 10)); + }); + }; + + /* Resolution Menu Items + ================================================================================ */ + var ResolutionMenuItem = videojs.MenuItem.extend({ + init: function(player, options){ + // Modify options for parent MenuItem class's init. + options['label'] = options.source['data-res']; + videojs.MenuItem.call(this, player, options); + + this.source = options.source; + this.resolution = options.source['data-res']; + + this.player_.one('loadstart', vjs.bind(this, this.update)); + this.player_.on('resolutionchange', vjs.bind(this, this.update)); + } + }); + + ResolutionMenuItem.prototype.onClick = function(){ + videojs.MenuItem.prototype.onClick.call(this); + this.player_.changeResolution(this.source); + }; + + ResolutionMenuItem.prototype.update = function(){ + var player = this.player_; + if ((player.cache_['src'] === this.source.src)) { + this.selected(true); + } else { + this.selected(false); + } + }; + + /* Resolutions Button + ================================================================================ */ + var ResolutionButton = videojs.MenuButton.extend({ + init: function(player, options) { + videojs.MenuButton.call(this, player, options); + + if (this.items.length <= 1) { + this.hide(); + } + } + }); + + ResolutionButton.prototype.sourceResolutions_; + + ResolutionButton.prototype.sourceResolutions = function() { + return this.sourceResolutions_; + }; + + ResolutionButton.prototype.onClick = function(e){ + // Only proceed if the target of the click was a DIV (just the button and its inner div, not the menu) + // This prevents the menu from opening and closing when one of the menu items is clicked. + if (e.target.className.match(/vjs-control-content/)) { + + // Toggle the 'touched' class + this[this.el_.className.match(/touched/) ? "removeClass" : "addClass"]("touched"); + } else { + + // Remove the 'touched' class from all control bar buttons with menus to hide any already visible... + var buttons = document.getElementsByClassName('vjs-menu-button'); + for(var i=0;i<buttons.length;i++){ + videojs.removeClass(buttons[i], 'touched'); + } + + this.removeClass('touched'); + } + }; + + ResolutionButton.prototype.createItems = function(){ + var resolutions = this.sourceResolutions_ = this.player_.resolutions_.options_['sourceResolutions']; + var items = []; + for (var i = 0; i < resolutions.length; i++) { + items.push(new ResolutionMenuItem(this.player_, { + 'source': this.sourceResolutions_[i] + })); + } + return items; + }; + + /** + * @constructor + */ + ResolutionsButton = ResolutionButton.extend({ + /** @constructor */ + init: function(player, options, ready){ + ResolutionButton.call(this, player, options, ready); + this.el_.setAttribute('aria-label','Resolutions Menu'); + this.el_.setAttribute('id',"vjs-resolutions-button"); + } + }); + + ResolutionsButton.prototype.kind_ = 'resolutions'; + ResolutionsButton.prototype.buttonText = 'Resolutions'; + ResolutionsButton.prototype.className = 'vjs-resolutions-button'; + + // Add Button to controlBar + videojs.obj.merge(player.controlBar.options_['children'], { + 'resolutionsButton': {} + }); + + // let's get the party started! + // we have to grab the parsed sources and select the source with our + // resolution-aware source selector + var source = player.resolutions_.selectSource(player.options_['sources']); + + // when the player is ready, add the resolution button to the control bar + player.ready(function(){ + player.changeResolution(source); + var button = new ResolutionsButton(player); + player.controlBar.addChild(button); + }); +}); |