diff --git a/dashboard-ui/css/librarybrowser.css b/dashboard-ui/css/librarybrowser.css index b248b67c1b..13f6aaf71c 100644 --- a/dashboard-ui/css/librarybrowser.css +++ b/dashboard-ui/css/librarybrowser.css @@ -1,3 +1,64 @@ -body { - -} \ No newline at end of file +/*.libraryContent { +} + +.librarySidebar { + display: none; +} + +@media all and (min-width: 650px) { + + .libraryContent { + float: right; + width: 65%; + } + + .librarySidebar { + display: block; + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: 35%; + border-right: 2px solid #666; + } +} + +@media all and (min-width: 750px) { + .libraryContent { + width: 70%; + } + + .librarySidebar { + width: 30%; + } +} + +@media all and (min-width: 1200px) { + .libraryContent { + width: 75%; + } + + .librarySidebar { + width: 25%; + } +} + +@media all and (min-width: 1440px) { + .libraryContent { + width: 80%; + } + + .librarySidebar { + width: 20%; + } +} + +@media all and (min-width: 1920px) { + .libraryContent { + width: 87.5%; + } + + .librarySidebar { + width: 12.5%; + } +}*/ diff --git a/dashboard-ui/scripts/extensions.js b/dashboard-ui/scripts/extensions.js index 71fbcc88ed..7455c1970d 100644 --- a/dashboard-ui/scripts/extensions.js +++ b/dashboard-ui/scripts/extensions.js @@ -331,19 +331,401 @@ function parseISO8601Date(s, toLocal) { })(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'] = new Array(1500000, 128000, 1920, 1080); + resolutions['medium'] = new Array(750000, 128000, 1280, 720); + resolutions['low'] = new Array(200000, 128000, 720, 480); + + 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.player.tag.src; + var src = parse_src_url(currentSrc); + var newSrc = "/mediabrowser/" + src.Type + "/" + src.item_id + "/stream." + src.stream + "?audioChannels=" + src.audioChannels + "&audioBitrate=" + resolutions[this.options.src[0].res][1] + + "&videoBitrate=" + resolutions[this.options.src[0].res][0] + "&maxWidth=" + resolutions[this.options.src[0].res][2] + "&maxHeight=" + resolutions[this.options.src[0].res][3] + + "&videoCodec=" + src.videoCodec + "&audioCodec=" + src.audioCodec; + + if (this.player.duration() == "Infinity") { + if (currentSrc.indexOf("StartTimeTicks") >= 0) { + var startTimeTicks = currentSrc.match(new RegExp("StartTimeTicks=[0-9]+", "g")); + var start_time = startTimeTicks[0].replace("StartTimeTicks=", ""); + + newSrc += "&StartTimeTicks=" + Math.floor(parseInt(start_time) + (10000000 * current_time)); + } else { + newSrc += "&StartTimeTicks=" + Math.floor(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.player.tag.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 () { + } +}); + +/* + JS for the stop button in video.js player + */ + +/* + Define the base class for the stop button. + */ + +_V_.StopButton = _V_.Button.extend({ + + kind: "stop", + className: "vjs-stop-button", + + init: function (player, options) { + + this._super(player, options); + + }, + + buildCSSClass: function () { + + return this.className + " vjs-menu-button " + this._super(); + }, + + onClick: function () { + MediaPlayer.stop(); + } +}); + + //convert Ticks to human hr:min:sec format function ticks_to_human(str) { var in_seconds = (str / 10000000); - var hours = Math.floor(in_seconds/3600); - var minutes = Math.floor((in_seconds-(hours*3600))/60); - var seconds = '0'+Math.round(in_seconds-(hours*3600)-(minutes*60)); + var hours = Math.floor(in_seconds / 3600); + var minutes = Math.floor((in_seconds - (hours * 3600)) / 60); + var seconds = '0' + Math.round(in_seconds - (hours * 3600) - (minutes * 60)); var time = ''; - if (hours > 0) time += hours+":"; + if (hours > 0) time += hours + ":"; if (minutes < 10 && hours == 0) time += minutes; - else time += ('0'+minutes).substr(-2); + else time += ('0' + minutes).substr(-2); time += ":" + seconds.substr(-2); return time; @@ -351,19 +733,19 @@ function ticks_to_human(str) { //parse video player src URL function parse_src_url(url) { - var src = url.replace("\?","\&"); + var src = url.replace("\?", "\&"); var parts = src.split("/"); - var len = parts.length-1; + var len = parts.length - 1; var query = parts[len].split("&"); var array = new Array(); - array['Type'] = parts[len-2]; - array['item_id'] = parts[len-1]; + array['Type'] = parts[len - 2]; + array['item_id'] = parts[len - 1]; for (i = 0; i < query.length; i++) { if (i == 0) { var pairs = query[i].split("."); - }else { + } else { var pairs = query[i].split("="); } diff --git a/dashboard-ui/scripts/mediaplayer.js b/dashboard-ui/scripts/mediaplayer.js index 2a2936b083..5f78e10fbd 100644 --- a/dashboard-ui/scripts/mediaplayer.js +++ b/dashboard-ui/scripts/mediaplayer.js @@ -11,7 +11,7 @@ if (media.canPlayType) { - return media.canPlayType('video/mp4').replace(/no/, '') || media.canPlayType('video/webm').replace(/no/, '') || media.canPlayType('video/ogv').replace(/no/, ''); + return media.canPlayType('video/mp4').replace(/no/, '') || media.canPlayType('video/mp2t').replace(/no/, '') || media.canPlayType('video/webm').replace(/no/, '') || media.canPlayType('application/x-mpegURL').replace(/no/, '') || media.canPlayType('video/ogv').replace(/no/, ''); } return false; @@ -72,7 +72,7 @@ var html = ''; var url = ""; - + if (item.BackdropImageTags && item.BackdropImageTags.length) { url = ApiClient.getImageUrl(item.Id, { @@ -96,7 +96,7 @@ height: 36, tag: item.ImageTags.Primary }); - }else { + } else { url = "css/images/items/detail/video.png"; } @@ -114,7 +114,7 @@ } html += "
"; - html += '
'+name+'
'+series_name+'
'; + html += '
' + name + '
' + series_name + '
'; $('#mediaInfo', nowPlayingBar).html(html); }, @@ -170,9 +170,6 @@ var volume = localStorage.getItem("volume") || 0.5; - //need to store current play position (reset to 0 on new video load) - MediaPlayer.playingTime = 0; - var baseParams = { audioChannels: 2, audioBitrate: 128000, @@ -182,94 +179,106 @@ StartTimeTicks: 0 }; - if (typeof(startPosition) != "undefined") { + if (typeof (startPosition) != "undefined") { baseParams['StartTimeTicks'] = startPosition; } - var mp4VideoUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.mp4', $.extend({}, baseParams, { - videoCodec: 'h264', - audioCodec: 'aac' - })); - - var webmVideoUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.webm', $.extend({}, baseParams, { - videoCodec: 'vpx', - audioCodec: 'Vorbis' - })); - - var ogvVideoUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.ogv', $.extend({}, baseParams, { - videoCodec: 'theora', - audioCodec: 'Vorbis' - })); - - $("#media_player").jPlayer({ - ready: function () { - $(this).jPlayer("setMedia", { - m4v: mp4VideoUrl, - ogv: ogvVideoUrl, - webm: webmVideoUrl - }).jPlayer("play"); - - $('.jp_duration').html(ticks_to_human(item.RunTimeTicks)); - - $(this).bind($.jPlayer.event.timeupdate,function(event){ - MediaPlayer.playingTime = event.jPlayer.status.currentTime; - }); - - $(this).bind($.jPlayer.event.volumechange,function(event){ - localStorage.setItem("volume", event.jPlayer.options.volume ); - }); - - //add quality selector - var available_res = ['high','medium','low']; - $('.jp_quality').html(''); - $.each(available_res, function(i, value) { - var html = '
  • '+value+'
  • '; - $('.jp_quality').append(html); - }); - - $('.jp_chapters').html(''); - if (item.Chapters && item.Chapters.length) { - // Put together the available chapter list - $.each( item.Chapters, function( i, chapter ) { - var chapter_name = chapter.Name + " (" + ticks_to_human(chapter.StartPositionTicks) + ")"; - var html = '
  • '+chapter_name+'
  • '; - $('.jp_chapters').append(html); - }); - } - - MediaPlayer.updateProgress(); - ApiClient.reportPlaybackStart(Dashboard.getCurrentUserId(), item.Id); - - }, - volume: volume, - supplied: "m4v, ogv, webm", - cssSelectorAncestor: "#media_container", - emulateHtml: true - }); + var html = ''; var nowPlayingBar = $('#nowPlayingBar'); - $('#mediaElement', nowPlayingBar).show(); + $('#mediaElement', nowPlayingBar).html(html).show(); + + _V_("videoWindow", { 'controls': true, 'autoplay': true, 'preload': 'auto' }, function () { + + var mp4VideoUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.mp4', $.extend({}, baseParams, { + videoCodec: 'h264', + audioCodec: 'aac' + })); + + var tsVideoUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.ts', $.extend({}, baseParams, { + videoCodec: 'h264', + audioCodec: 'aac' + })); + + var webmVideoUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.webm', $.extend({}, baseParams, { + videoCodec: 'vpx', + audioCodec: 'Vorbis' + })); + + var hlsVideoUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.m3u8', $.extend({}, baseParams, { + videoCodec: 'h264', + audioCodec: 'aac' + })); + + var ogvVideoUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.ogv', $.extend({}, baseParams, { + videoCodec: 'theora', + audioCodec: 'Vorbis' + })); + + (this).src([{ type: "video/webm", src: webmVideoUrl }, + { type: "video/mp4", src: mp4VideoUrl }, + { type: "video/mp2t; codecs='h264, aac'", src: tsVideoUrl }, + { type: "application/x-mpegURL", src: hlsVideoUrl }, + { type: "video/ogg", src: ogvVideoUrl }] + ).volume(volume); + + videoJSextension.setup_video($('#videoWindow'), item); + + (this).addEvent("loadstart", function () { + $(".vjs-remaining-time-display").hide(); + }); + + (this).addEvent("durationchange", function () { + if ((this).duration() != "Infinity") + $(".vjs-remaining-time-display").show(); + }); + + (this).addEvent("volumechange", function () { + localStorage.setItem("volume", (this).volume()); + }); + + (this).addEvent("play", MediaPlayer.updateProgress); + + ApiClient.reportPlaybackStart(Dashboard.getCurrentUserId(), item.Id); + }); return $('video', nowPlayingBar)[0]; }, stop: function () { - var startTimeTicks = $("#media_player video").attr("src").match(new RegExp("StartTimeTicks=[0-9]+","g")); - var start_time = startTimeTicks[0].replace("StartTimeTicks=",""); + var elem = MediaPlayer.mediaElement; - var item_string = $("#media_player video").attr("src").match(new RegExp("Videos/[0-9a-z\-]+","g")); - var item_id = item_string[0].replace("Videos/",""); + //check if it's a video using VideoJS + if ($(elem).hasClass("vjs-tech")) { + var player = _V_("videoWindow"); - var current_time = MediaPlayer.playingTime; - var positionTicks = parseInt(start_time) + Math.floor(10000000*current_time); + var startTimeTicks = player.tag.src.match(new RegExp("StartTimeTicks=[0-9]+", "g")); + var start_time = startTimeTicks[0].replace("StartTimeTicks=", ""); - ApiClient.reportPlaybackStopped(Dashboard.getCurrentUserId(), item_id, positionTicks); + var item_string = player.tag.src.match(new RegExp("Videos/[0-9a-z\-]+", "g")); + var item_id = item_string[0].replace("Videos/", ""); - clearTimeout(MediaPlayer.progressInterval); + var positionTicks = parseInt(start_time) + Math.floor(10000000 * player.currentTime()); - $("#media_player").jPlayer("destroy"); + ApiClient.reportPlaybackStopped(Dashboard.getCurrentUserId(), item_id, positionTicks); + + clearTimeout(progressInterval); + + if (player.techName == "html5") { + player.tag.src = ""; + player.tech.removeTriggers(); + player.load(); + } + //player.tech.destroy(); + player.destroy(); + } else { + elem.pause(); + elem.src = ""; + } + + $(elem).remove(); $('#nowPlayingBar').hide(); @@ -281,74 +290,89 @@ }, updateProgress: function () { - MediaPlayer.progressInterval = setInterval(function(){ - var current_time = MediaPlayer.playingTime; + progressInterval = setInterval(function () { + var player = _V_("videoWindow"); - var startTimeTicks = $("#media_player video").attr("src").match(new RegExp("StartTimeTicks=[0-9]+","g")); - var start_time = startTimeTicks[0].replace("StartTimeTicks=",""); + var startTimeTicks = player.tag.src.match(new RegExp("StartTimeTicks=[0-9]+", "g")); + var start_time = startTimeTicks[0].replace("StartTimeTicks=", ""); - var item_string = $("#media_player video").attr("src").match(new RegExp("Videos/[0-9a-z\-]+","g")); - var item_id = item_string[0].replace("Videos/",""); + var item_string = player.tag.src.match(new RegExp("Videos/[0-9a-z\-]+", "g")); + var item_id = item_string[0].replace("Videos/", ""); - var positionTicks = parseInt(start_time) + Math.floor(10000000*current_time); + var positionTicks = parseInt(start_time) + Math.floor(10000000 * player.currentTime()); ApiClient.reportPlaybackProgress(Dashboard.getCurrentUserId(), item_id, positionTicks); - },30000); - }, - - setResolution: function (new_res) { - var resolutions = new Array(); - resolutions['high'] = new Array(1500000, 128000, 1920, 1080); - resolutions['medium'] = new Array(750000, 128000, 1280, 720); - resolutions['low'] = new Array(200000, 128000, 720, 480); - - var current_time = MediaPlayer.playingTime; - - // Set the button text to the newly chosen quality - - - // Change the source and make sure we don't start the video over - var currentSrc = $("#media_player video").attr("src"); - var src = parse_src_url(currentSrc); - var newSrc = "/mediabrowser/"+src.Type+"/"+src.item_id+"/stream."+src.stream+"?audioChannels="+src.audioChannels+"&audioBitrate="+resolutions[new_res][1]+ - "&videoBitrate="+resolutions[new_res][0]+"&maxWidth="+resolutions[new_res][2]+"&maxHeight="+resolutions[new_res][3]+ - "&videoCodec="+src.videoCodec+"&audioCodec="+src.audioCodec; - - if (currentSrc.indexOf("StartTimeTicks") >= 0) { - var startTimeTicks = currentSrc.match(new RegExp("StartTimeTicks=[0-9]+","g")); - var start_time = startTimeTicks[0].replace("StartTimeTicks=",""); - - newSrc += "&StartTimeTicks="+Math.floor(parseInt(start_time)+(10000000*current_time)); - }else { - newSrc += "&StartTimeTicks="+Math.floor(10000000*current_time); - } - - //need to store current play position (reset to 0 on new video load) - MediaPlayer.playingTime = 0; - - $("#media_player").jPlayer("setMedia",{ - m4v: newSrc, - ogv: newSrc, - webm: newSrc - }).jPlayer("play"); - - }, - - setChapter: function (chapter_id, new_time) { - - var currentSrc = $("#media_player video").attr("src"); - - if (currentSrc.indexOf("StartTimeTicks") >= 0) { - var newSrc = currentSrc.replace(new RegExp("StartTimeTicks=[0-9]+","g"),"StartTimeTicks="+new_time); - }else { - var newSrc = currentSrc += "&StartTimeTicks="+new_time; - } - - $("#media_player").jPlayer("setMedia",{ - m4v: newSrc, - ogv: newSrc, - webm: newSrc - }).jPlayer("play"); + }, 30000); } }; + +var videoJSextension = { + + /* + Add our video quality selector button to the videojs controls. This takes + a mandatory jQuery object of the