diff --git a/dashboard-ui/css/site.css b/dashboard-ui/css/site.css index 3a837e0c08..c66fee35d2 100644 --- a/dashboard-ui/css/site.css +++ b/dashboard-ui/css/site.css @@ -814,19 +814,28 @@ progress { margin-right: 20px; display: inline-block; position: relative; + width: 270px; } .mediaButton img { height: 28px; } -.itemVideo { +.itemVideo, .itemVideo.video-js { position: absolute; z-index: 99998; height: auto; - width: 180px; + /*width: 180px;*/ bottom: -5px; } +.vjs-quality-button { + padding: 0 0.6em !important; + width: auto !important; +} +.vjs-quality-button div { + width: auto !important; + background: none !important; +} @media all and (min-width: 650px) { diff --git a/dashboard-ui/scripts/Extensions.js b/dashboard-ui/scripts/Extensions.js index 3621e341bf..6d8756c3c9 100644 --- a/dashboard-ui/scripts/Extensions.js +++ b/dashboard-ui/scripts/Extensions.js @@ -328,4 +328,355 @@ function parseISO8601Date(s, toLocal) { } }); -})(jQuery, window); \ No newline at end of file +})(jQuery, window); + + +/* + JS for the quality selector in video.js player + */ + +/* + Define the base class for the quality selector button. + Most of this code is copied from the _V_.TextTrackButton + class. + + https://github.com/zencoder/video-js/blob/master/src/tracks.js#L560) + */ +_V_.ResolutionSelector = _V_.Button.extend({ + + kind: "quality", + className: "vjs-quality-button", + + init: function(player, options) { + + this._super(player, options); + + // Save the starting resolution as a property of the player object + player.options.currentResolution = this.buttonText; + + this.menu = this.createMenu(); + + if (this.items.length === 0) { + this.hide(); + } + }, + + createMenu: function() { + + var menu = new _V_.Menu(this.player); + + // Add a title list item to the top + menu.el.appendChild(_V_.createElement("li", { + className: "vjs-menu-title", + innerHTML: _V_.uc(this.kind) + })); + + this.items = this.createItems(); + + // Add menu items to the menu + this.each(this.items, function(item){ + menu.addItem(item); + }); + + // Add list to element + this.addComponent(menu); + + return menu; + }, + + // Override the default _V_.Button createElement so the button text isn't hidden + createElement: function(type, attrs) { + + // Add standard Aria and Tabindex info + attrs = _V_.merge({ + className: this.buildCSSClass(), + innerHTML: '
' + this.buttonText + '
', + role: "button", + tabIndex: 0 + }, attrs); + + return this._super(type, attrs); + }, + + // Create a menu item for each text track + createItems: function() { + + var items = []; + + this.each( this.availableRes, function( res ) { + + items.push( new _V_.ResolutionMenuItem( this.player, { + + label: res[0].res, + src: res + })); + }); + + return items; + }, + + buildCSSClass: function() { + + return this.className + " vjs-menu-button " + this._super(); + }, + + // Focus - Add keyboard functionality to element + onFocus: function() { + + // Show the menu, and keep showing when the menu items are in focus + this.menu.lockShowing(); + this.menu.el.style.display = "block"; + + // When tabbing through, the menu should hide when focus goes from the last menu item to the next tabbed element. + _V_.one(this.menu.el.childNodes[this.menu.el.childNodes.length - 1], "blur", this.proxy(function() { + + this.menu.unlockShowing(); + })); + }, + + // Can't turn off list display that we turned on with focus, because list would go away. + onBlur: function(){}, + + onClick: function() { + + /* + When you click the button it adds focus, which will show the menu indefinitely. + So we'll remove focus when the mouse leaves the button. + Focus is needed for tab navigation. + */ + this.one( 'mouseout', this.proxy(function() { + + this.menu.unlockShowing(); + this.el.blur(); + })); + } +}); + +/* + Define the base class for the quality menu items + */ +_V_.ResolutionMenuItem = _V_.MenuItem.extend({ + + init: function(player, options){ + + // Modify options for parent MenuItem class's init. + options.selected = ( options.label === player.options.currentResolution ); + this._super( player, options ); + + this.player.addEvent( 'changeRes', _V_.proxy( this, this.update ) ); + }, + + onClick: function() { + + // Check that we are changing to a new quality (not the one we are already on) + if ( this.options.label === this.player.options.currentResolution ) + return; + + var resolutions = new Array(); + resolutions['high'] = 500000; + resolutions['medium'] = 250000; + resolutions['low'] = 50000; + + var current_time = this.player.currentTime(); + + // Set the button text to the newly chosen quality + jQuery( this.player.controlBar.el ).find( '.vjs-quality-text' ).html( this.options.label ); + + // Change the source and make sure we don't start the video over + var currentSrc = $("#"+this.options.src[0].vid_id).find('video').attr("src"); + var newSrc = currentSrc.replace("videoBitrate="+resolutions[this.player.options.currentResolution],"videoBitrate="+resolutions[this.options.src[0].res]); + + if (this.player.duration() == "Infinity") { + if (currentSrc.indexOf("StartTimeTicks") >= 0) { + var startTimeTicks = newSrc.match(new RegExp("StartTimeTicks=[0-9]+","g")); + var start_time = startTimeTicks[0].replace("StartTimeTicks=",""); + + newSrc = newSrc.replace(new RegExp("StartTimeTicks=[0-9]+","g"),"StartTimeTicks="+(parseInt(start_time)+(10000000*current_time))); + }else { + newSrc += "&StartTimeTicks="+10000000*current_time; + } + + this.player.src( newSrc ).one( 'loadedmetadata', function() { + this.play(); + }); + }else { + this.player.src( newSrc ).one( 'loadedmetadata', function() { + this.currentTime( current_time ); + this.play(); + }); + } + + // Save the newly selected resolution in our player options property + this.player.options.currentResolution = this.options.label; + + // Update the classes to reflect the currently selected resolution + this.player.triggerEvent( 'changeRes' ); + }, + + update: function() { + + if ( this.options.label === this.player.options.currentResolution ) { + this.selected( true ); + } else { + this.selected( false ); + } + } +}); + + +/* + JS for the chapter selector in video.js player + */ + +/* + Define the base class for the chapter selector button. + */ +_V_.ChapterSelector = _V_.Button.extend({ + + kind: "chapter", + className: "vjs-chapter-button", + + init: function(player, options) { + + this._super(player, options); + + this.menu = this.createMenu(); + + if (this.items.length === 0) { + this.hide(); + } + }, + + createMenu: function() { + + var menu = new _V_.Menu(this.player); + + // Add a title list item to the top + menu.el.appendChild(_V_.createElement("li", { + className: "vjs-menu-title", + innerHTML: _V_.uc(this.kind) + })); + + this.items = this.createItems(); + + // Add menu items to the menu + this.each(this.items, function(item){ + menu.addItem(item); + }); + + // Add list to element + this.addComponent(menu); + + return menu; + }, + + // Override the default _V_.Button createElement so the button text isn't hidden + createElement: function(type, attrs) { + + // Add standard Aria and Tabindex info + attrs = _V_.merge({ + className: this.buildCSSClass(), + innerHTML: '
' + this.buttonText + '
', + role: "button", + tabIndex: 0 + }, attrs); + + return this._super(type, attrs); + }, + + // Create a menu item for each chapter + createItems: function() { + + var items = []; + + this.each( this.Chapters, function( chapter ) { + + items.push( new _V_.ChapterMenuItem( this.player, { + label: chapter[0].Name, + src: chapter + })); + }); + + return items; + }, + + buildCSSClass: function() { + + return this.className + " vjs-menu-button " + this._super(); + }, + + // Focus - Add keyboard functionality to element + onFocus: function() { + + // Show the menu, and keep showing when the menu items are in focus + this.menu.lockShowing(); + this.menu.el.style.display = "block"; + + // When tabbing through, the menu should hide when focus goes from the last menu item to the next tabbed element. + _V_.one(this.menu.el.childNodes[this.menu.el.childNodes.length - 1], "blur", this.proxy(function() { + + this.menu.unlockShowing(); + })); + }, + + // Can't turn off list display that we turned on with focus, because list would go away. + onBlur: function(){}, + + onClick: function() { + + /* + When you click the button it adds focus, which will show the menu indefinitely. + So we'll remove focus when the mouse leaves the button. + Focus is needed for tab navigation. + */ + this.one( 'mouseout', this.proxy(function() { + + this.menu.unlockShowing(); + this.el.blur(); + })); + } +}); + +/* + Define the base class for the chapter menu items + */ +_V_.ChapterMenuItem = _V_.MenuItem.extend({ + + init: function(player, options){ + + // Modify options for parent MenuItem class's init. + //options.selected = ( options.label === player.options.currentResolution ); + this._super( player, options ); + + this.player.addEvent( 'changeChapter', _V_.proxy( this, this.update ) ); + }, + + onClick: function() { + + // Set the button text to the newly chosen chapter + //jQuery( this.player.controlBar.el ).find( '.vjs-chapter-text' ).html( this.options.label ); + + if (this.player.duration() == "Infinity") { + var currentSrc = $("#"+this.options.src[0].vid_id).find('video').attr("src"); + + if (currentSrc.indexOf("StartTimeTicks") >= 0) { + var newSrc = currentSrc.replace(new RegExp("StartTimeTicks=[0-9]+","g"),"StartTimeTicks="+this.options.src[0].StartPositionTicks); + }else { + var newSrc = currentSrc += "&StartTimeTicks="+this.options.src[0].StartPositionTicks; + } + + this.player.src( newSrc ).one( 'loadedmetadata', function() { + this.play(); + }); + }else { + //figure out the time from ticks + var current_time = parseFloat(this.options.src[0].StartPositionTicks)/10000000; + + this.player.currentTime( current_time ); + } + }, + + update: function() { + } +}); + diff --git a/dashboard-ui/scripts/MediaPlayer.js b/dashboard-ui/scripts/MediaPlayer.js index 8b3043ba5c..4bdda3b535 100644 --- a/dashboard-ui/scripts/MediaPlayer.js +++ b/dashboard-ui/scripts/MediaPlayer.js @@ -68,7 +68,7 @@ } }, - playAudio: function (items) { + playAudio: function (items, params) { var item = items[0]; var baseParams = { @@ -76,6 +76,8 @@ audioBitrate: 128000 }; + $.extend(baseParams, params); + var mp3Url = ApiClient.getUrl('Audio/' + item.Id + '/stream.mp3', $.extend({}, baseParams, { audioCodec: 'mp3' })); @@ -123,44 +125,51 @@ maxHeight: screenHeight }; - var mp4VideoUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.mp4', $.extend({}, baseParams, { - videoCodec: 'h264', - audioCodec: 'aac' - })); + //TODO if you press "stop" button on the nowPlayingBar and restart the same video without refreshing the page + //there is an issue since VideoJS is still loaded. - var tsVideoUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.ts', $.extend({}, baseParams, { - videoCodec: 'h264', - audioCodec: 'aac' - })); + var html = ''; - var webmVideoUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.webm', $.extend({}, baseParams, { - videoCodec: 'vpx', - audioCodec: 'Vorbis' - })); + var nowPlayingBar = $('#nowPlayingBar'); - var hlsVideoUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.m3u8', $.extend({}, baseParams, { - videoCodec: 'h264', - audioCodec: 'aac' - })); + $('#mediaElement', nowPlayingBar).html(html).show(); - var ogvVideoUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.ogv', $.extend({}, baseParams, { - videoCodec: 'theora', - audioCodec: 'Vorbis' - })); + _V_("videoWindow", {'controls': true, 'autoplay': true, 'preload': 'auto'}, function(){ - var html = ''; - html += '