diff --git a/ApiClient.js b/ApiClient.js index f2b2d6b7b1..cbe815c03a 100644 --- a/ApiClient.js +++ b/ApiClient.js @@ -436,13 +436,26 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi self.getLiveTvPrograms = function (options) { - var url = self.getUrl("LiveTv/Programs", options || {}); + options = options || {}; + + if (options.channelIds) { - return self.ajax({ - type: "GET", - url: url, - dataType: "json" - }); + return self.ajax({ + type: "POST", + url: self.getUrl("LiveTv/Programs"), + data: JSON.stringify(options), + contentType: "application/json", + dataType: "json" + }); + + } else { + + return self.ajax({ + type: "GET", + url: self.getUrl("LiveTv/Programs", options), + dataType: "json" + }); + } }; self.getLiveTvRecordings = function (options) { diff --git a/dashboard-ui/css/livetv.css b/dashboard-ui/css/livetv.css index f71eada52d..f9d1739f21 100644 --- a/dashboard-ui/css/livetv.css +++ b/dashboard-ui/css/livetv.css @@ -133,12 +133,12 @@ .channelTimeslotHeader { position: absolute; - left: 0; + left: 15px; } .timeslotHeaders { position: absolute; - right: 13px; + right: 28px; overflow-y: hidden; overflow-x: hidden; white-space: nowrap; @@ -205,14 +205,14 @@ overflow-y: hidden; overflow-x: hidden; position: absolute; - left: 0; - bottom: 16px; + left: 15px; + bottom: 46px; } .programGrid { position: absolute; - bottom: 0; - right: 0; + bottom: 30px; + right: 15px; overflow-y: scroll; overflow-x: scroll; } @@ -242,15 +242,15 @@ } .timeslotHeaders, .programGrid { - left: 191px; + left: 206px; } .channelTimeslotHeader, .timeslotHeaders { - top: 135px; + top: 150px; } .channelList, .programGrid { - top: 165px; + top: 180px; } .channelHeaderCell, .timeslotCell { @@ -305,10 +305,10 @@ @media (max-width: 750px) { .channelTimeslotHeader, .timeslotHeaders { - top: 100px; + top: 115px; } .channelList, .programGrid { - top: 130px; + top: 145px; } } diff --git a/dashboard-ui/encodingsettings.html b/dashboard-ui/encodingsettings.html index e9532d69c4..42da151cf1 100644 --- a/dashboard-ui/encodingsettings.html +++ b/dashboard-ui/encodingsettings.html @@ -17,15 +17,15 @@
- Transcoding Quality: + Transcoding Quality Preference: - + - + - +

diff --git a/dashboard-ui/itemdetails.html b/dashboard-ui/itemdetails.html index 5b07f70135..36102947de 100644 --- a/dashboard-ui/itemdetails.html +++ b/dashboard-ui/itemdetails.html @@ -141,6 +141,9 @@ + + + diff --git a/dashboard-ui/livetvguide.html b/dashboard-ui/livetvguide.html index 6e0e4b92b6..d0dcec6eeb 100644 --- a/dashboard-ui/livetvguide.html +++ b/dashboard-ui/livetvguide.html @@ -16,10 +16,15 @@
-
+
+
+ Channels +
+
+
diff --git a/dashboard-ui/scripts/itemdetailpage.js b/dashboard-ui/scripts/itemdetailpage.js index 8ca1387f14..e5b4ce852f 100644 --- a/dashboard-ui/scripts/itemdetailpage.js +++ b/dashboard-ui/scripts/itemdetailpage.js @@ -83,6 +83,12 @@ $('#playButtonContainer', page).hide(); $('#playExternalButtonContainer', page).hide(); } + + if (item.LocalTrailerCount && item.LocationType !== "Offline") { + $('#trailerButtonContainer', page).show(); + } else { + $('#trailerButtonContainer', page).hide(); + } $(".autoNumeric").autoNumeric('init'); @@ -189,7 +195,7 @@ $('#scenesCollapsible', page).show(); renderScenes(page, item, 4); } - if (!item.LocalTrailerCount && !item.RemoteTrailers.length) { + if (item.LocalTrailerCount || !item.RemoteTrailers.length) { $('#trailersCollapsible', page).addClass('hide'); } else { $('#trailersCollapsible', page).removeClass('hide'); @@ -1120,6 +1126,24 @@ LibraryBrowser.showPlayMenu(this, currentItem.Id, currentItem.Type, mediaType, userdata.PlaybackPositionTicks); }); + $('#btnPlayTrailer', page).on('click', function () { + + ApiClient.getLocalTrailers(Dashboard.getCurrentUserId(), currentItem.Id).done(function (trailers) { + + var trailer = trailers[0]; + var userdata = trailer.UserData || {}; + + var mediaType = trailer.MediaType; + + if (trailer.Type == "MusicArtist" || trailer.Type == "MusicAlbum") { + mediaType = "Audio"; + } + + LibraryBrowser.showPlayMenu(this, trailer.Id, trailer.Type, mediaType, userdata.PlaybackPositionTicks); + + }); + }); + $('#btnPlayExternal', page).on('click', function () { ApiClient.markPlayed(Dashboard.getCurrentUserId(), currentItem.Id, new Date()); diff --git a/dashboard-ui/scripts/librarybrowser.js b/dashboard-ui/scripts/librarybrowser.js index f1edb05ed2..8c0473b59d 100644 --- a/dashboard-ui/scripts/librarybrowser.js +++ b/dashboard-ui/scripts/librarybrowser.js @@ -1417,7 +1417,7 @@ return html; }, - getPagingHtml: function (query, totalRecordCount, updatePageSizeSetting) { + getPagingHtml: function (query, totalRecordCount, updatePageSizeSetting, pageSizes) { if (query.Limit && updatePageSizeSetting !== false) { localStorage.setItem('pagesize', query.Limit); @@ -1477,13 +1477,11 @@ } } - options += getOption(20); - options += getOption(50); - options += getOption(100); - options += getOption(200); - options += getOption(300); - options += getOption(400); - options += getOption(500); + pageSizes = pageSizes || [20, 50, 100, 200, 300, 400, 500]; + + for (var j = 0, length = pageSizes.length; j < length; j++) { + options += getOption(pageSizes[j]); + } // Add styles to defeat jquery mobile html += ''; diff --git a/dashboard-ui/scripts/livetvchannels.js b/dashboard-ui/scripts/livetvchannels.js index 1fc89c971b..a77cd7871d 100644 --- a/dashboard-ui/scripts/livetvchannels.js +++ b/dashboard-ui/scripts/livetvchannels.js @@ -1,5 +1,10 @@ (function ($, document, apiClient) { + var query = { + + StartIndex: 0 + }; + function getChannelsHtml(channels) { return LibraryBrowser.getPosterViewHtml({ @@ -10,23 +15,64 @@ }); } - function renderChannels(page, channels) { + function renderChannels(page, result) { - $('#items', page).html(getChannelsHtml(channels)).trigger('create'); + $('.listTopPaging', page).html(LibraryBrowser.getPagingHtml(query, result.TotalRecordCount, true)).trigger('create'); + + var html = getChannelsHtml(result.Items); + + html += LibraryBrowser.getPagingHtml(query, result.TotalRecordCount); + + $('#items', page).html(html).trigger('create'); + + $('.selectPage', page).on('change', function () { + query.StartIndex = (parseInt(this.value) - 1) * query.Limit; + reloadItems(page); + }); + + $('.btnNextPage', page).on('click', function () { + query.StartIndex += query.Limit; + reloadItems(page); + }); + + $('.btnPreviousPage', page).on('click', function () { + query.StartIndex -= query.Limit; + reloadItems(page); + }); + + $('.selectPageSize', page).on('change', function () { + query.Limit = parseInt(this.value); + query.StartIndex = 0; + reloadItems(page); + }); + + LibraryBrowser.saveQueryValues('movies', query); + } + + function reloadItems(page) { + apiClient.getLiveTvChannels(query).done(function (result) { + + renderChannels(page, result); + }); } $(document).on('pagebeforeshow', "#liveTvChannelsPage", function () { var page = this; - apiClient.getLiveTvChannels({ - - userId: Dashboard.getCurrentUserId() + var limit = LibraryBrowser.getDefaultPageSize(); - }).done(function (result) { + // If the default page size has changed, the start index will have to be reset + if (limit != query.Limit) { + query.Limit = limit; + query.StartIndex = 0; + } - renderChannels(page, result.Items); - }); + query.UserId = Dashboard.getCurrentUserId(); + + LibraryBrowser.loadSavedQueryValues('movies', query); + + reloadItems(page); }); })(jQuery, document, ApiClient); \ No newline at end of file diff --git a/dashboard-ui/scripts/livetvguide.js b/dashboard-ui/scripts/livetvguide.js index 9ba886d032..d0af393ef0 100644 --- a/dashboard-ui/scripts/livetvguide.js +++ b/dashboard-ui/scripts/livetvguide.js @@ -8,6 +8,12 @@ var gridLocalEndDateMs; var currentDate; + + var channelQuery = { + + StartIndex: 0, + Limit: 20 + }; var channelsPromise; function normalizeDateToTimeslot(date) { @@ -25,16 +31,19 @@ return date; } + + function reloadChannels(page) { + channelsPromise = null; + reloadGuide(page); + } function reloadGuide(page) { Dashboard.showLoadingMsg(); - - channelsPromise = channelsPromise || apiClient.getLiveTvChannels({ - userId: Dashboard.getCurrentUserId() + channelQuery.userId = Dashboard.getCurrentUserId(); - });; + channelsPromise = channelsPromise || apiClient.getLiveTvChannels(channelQuery); var date = currentDate; @@ -42,23 +51,45 @@ nextDay.setHours(0, 0, 0, 0); nextDay.setDate(nextDay.getDate() + 1); - var promise1 = channelsPromise; - var promise2 = apiClient.getLiveTvPrograms({ + channelsPromise.done(function(channelsResult) { - UserId: Dashboard.getCurrentUserId(), - MaxStartDate: nextDay.toISOString(), - MinEndDate: date.toISOString() - - }); - - $.when(promise1, promise2).done(function (response1, response2) { - - var channels = response1[0].Items; - var programs = response2[0].Items; - - renderGuide(page, date, channels, programs); + apiClient.getLiveTvPrograms({ + UserId: Dashboard.getCurrentUserId(), + MaxStartDate: nextDay.toISOString(), + MinEndDate: date.toISOString(), + channelIds: channelsResult.Items.map(function(c) { + return c.Id; + }).join(',') + + }).done(function(programsResult) { + + renderGuide(page, date, channelsResult.Items, programsResult.Items); + Dashboard.hideLoadingMsg(); + }); - Dashboard.hideLoadingMsg(); + var channelPagingHtml = LibraryBrowser.getPagingHtml(channelQuery, channelsResult.TotalRecordCount, false, [10, 20, 30, 50, 100]); + $('.channelPaging', page).html(channelPagingHtml).trigger('create'); + + $('.selectPage', page).on('change', function () { + channelQuery.StartIndex = (parseInt(this.value) - 1) * channelQuery.Limit; + reloadChannels(page); + }); + + $('.btnNextPage', page).on('click', function () { + channelQuery.StartIndex += channelQuery.Limit; + reloadChannels(page); + }); + + $('.btnPreviousPage', page).on('click', function () { + channelQuery.StartIndex -= channelQuery.Limit; + reloadChannels(page); + }); + + $('.selectPageSize', page).on('change', function () { + channelQuery.Limit = parseInt(this.value); + channelQuery.StartIndex = 0; + reloadChannels(page); + }); }); } @@ -222,7 +253,7 @@ html += '
'; html += program.Name; - + if (program.IsRepeat) { html += ' (R)'; } @@ -373,7 +404,7 @@ } var elem = $('#selectDate', page).html(html).selectmenu('refresh'); - + if (currentDate) { elem.val(currentDate.getTime()).selectmenu('refresh'); } @@ -394,8 +425,8 @@ onProgramGridScroll(page, this); }); - $('#selectDate', page).on('change', function() { - + $('#selectDate', page).on('change', function () { + var date = new Date(); date.setTime(parseInt(this.value)); diff --git a/dashboard-ui/scripts/mediaplayer.js b/dashboard-ui/scripts/mediaplayer.js index 528dd4f5ec..d14179a993 100644 --- a/dashboard-ui/scripts/mediaplayer.js +++ b/dashboard-ui/scripts/mediaplayer.js @@ -17,7 +17,7 @@ var unmuteButton; var startTimeTicksOffset; var curentDurationTicks; - var isStaticStream; + var canClientSeek; var culturesPromise; var timeout; var idleState = true; @@ -28,6 +28,11 @@ var channelsListPromise; var channelsListPromiseTime; + function updateCanClientSeek(elem) { + var duration = elem.duration; + canClientSeek = duration && !isNaN(duration) && duration != Number.POSITIVE_INFINITY && duration != Number.NEGATIVE_INFINITY; + } + function getChannelsListPromise() { var lastUpdateTime = channelsListPromiseTime || 0; @@ -176,13 +181,14 @@ currentProgressInterval = null; } } - + function getTranscodingExtension() { var media = testableVideoElement; - + // safari - if (media.canPlayType('application/x-mpegURL').replace(/no/, '')) { + if (media.canPlayType('application/x-mpegURL').replace(/no/, '') || + media.canPlayType('application/vnd.apple.mpegURL').replace(/no/, '')) { return '.m3u8'; } @@ -190,7 +196,7 @@ if (media.canPlayType('video/webm').replace(/no/, '')) { return '.webm'; } - + return '.mp4'; } @@ -198,7 +204,7 @@ var element = currentMediaElement; - if (isStaticStream && params == null) { + if (canClientSeek && params == null) { element.currentTime = ticks / (1000 * 10000); @@ -207,7 +213,6 @@ params = params || {}; var currentSrc = element.currentSrc; - console.log(currentSrc); currentSrc = replaceQueryString(currentSrc, 'starttimeticks', ticks); if (params.AudioStreamIndex != null) { @@ -232,7 +237,7 @@ currentSrc = replaceQueryString(currentSrc, 'VideoCodec', params.VideoCodec); } if (params.Static != null) { - + currentSrc = replaceQueryString(currentSrc, 'Static', params.Static); if (params.Static == 'true') { @@ -246,6 +251,8 @@ $(element).off('ended.playbackstopped').off('ended.playnext').on("play.onceafterseek", function () { + updateCanClientSeek(this); + $(this).off('play.onceafterseek').on('ended.playbackstopped', onPlaybackStopped).on('ended.playnext', playNextAfterEnded); startProgressInterval(currentItem.Id); @@ -256,7 +263,6 @@ ApiClient.stopActiveEncodings().done(function () { startTimeTicksOffset = ticks; - element.src = currentSrc; }); } @@ -333,22 +339,20 @@ var videoBitrate = parseInt(this.getAttribute('data-videobitrate')); var audioBitrate = parseInt(this.getAttribute('data-audiobitrate')); - var mp4AudioCodec = this.getAttribute('data-mp4audio'); - var mp4VideoCodec = this.getAttribute('data-mp4video'); + var audioCodec = this.getAttribute('data-audiocodec'); + var videoCodec = this.getAttribute('data-videocodec'); var isStatic = this.getAttribute('data-static'); localStorage.setItem('preferredVideoBitrate', videoBitrate + audioBitrate); - var isMp4 = currentMediaElement.currentSrc.toLowerCase().indexOf('.mp4') != -1; - changeStream(getCurrentTicks(), { MaxWidth: maxWidth, VideoBitrate: videoBitrate, AudioBitrate: audioBitrate, Static: isStatic, - AudioCodec: isMp4 ? mp4AudioCodec : null, - VideoCodec: isMp4 ? mp4VideoCodec : null + AudioCodec: audioCodec, + VideoCodec: videoCodec }); } @@ -399,15 +403,16 @@ currentTimeElement.html(timeText); } - function playAudio(item, params) { + function playAudio(item, startPositionTicks) { + + startPositionTicks = startPositionTicks || 0; var baseParams = { audioChannels: 2, - audioBitrate: 128000 + audioBitrate: 128000, + StartTimeTicks: startPositionTicks }; - $.extend(baseParams, params); - var mp3Url = ApiClient.getUrl('Audio/' + item.Id + '/stream.mp3', $.extend({}, baseParams, { audioCodec: 'mp3' })); @@ -422,6 +427,9 @@ var mediaStreams = item.MediaStreams || []; + var isStatic = false; + var seekParam = isStatic && startPositionTicks ? '#t=' + (startPositionTicks / 10000000) : ''; + for (var i = 0, length = mediaStreams.length; i < length; i++) { var stream = mediaStreams[i]; @@ -430,16 +438,19 @@ // Stream statically when possible if (endsWith(item.Path, ".aac") && stream.BitRate <= 256000) { - aacUrl += "&static=true"; + aacUrl += "&static=true" + seekParam; + isStatic = true; } else if (endsWith(item.Path, ".mp3") && stream.BitRate <= 256000) { - mp3Url += "&static=true"; + mp3Url += "&static=true" + seekParam; + isStatic = true; } break; } - } + startTimeTicksOffset = isStatic ? 0 : startPositionTicks; + var html = ''; var requiresControls = $.browser.android || ($.browser.webkit && !$.browser.chrome); @@ -498,8 +509,7 @@ audioElement.hide(); } - var duration = this.duration; - isStaticStream = duration && !isNaN(duration) && duration != Number.POSITIVE_INFINITY && duration != Number.NEGATIVE_INFINITY; + updateCanClientSeek(this); audioElement.off("play.once"); @@ -549,7 +559,11 @@ return false; } - var extension = item.Path.substring(item.Path.lastIndexOf('.') + 1); + var extension = item.Path.substring(item.Path.lastIndexOf('.') + 1).toLowerCase(); + + if (extension == 'm4v') { + return $.browser.chrome; + } return extension.toLowerCase() == 'mp4'; } @@ -575,7 +589,7 @@ return true; } - function getVideoQualityOptions(item, audioStreamIndex) { + function getVideoQualityOptions(item, audioStreamIndex, transcodeExtension) { var videoStream = (item.MediaStreams || []).filter(function (stream) { return stream.Type == "Video"; @@ -600,6 +614,13 @@ maxAllowedWidth = videoStream.Width; } + var canPlayVideoCodecDirect = (videoStream ? videoStream.Codec : '').toLowerCase().indexOf('h264') != -1; + var canPlayAudioDirect = audioStream ? canPlayAudioStreamDirect(audioStream) : false; + + var videoBitrate = videoStream ? videoStream.BitRate : null; + var audioBitrate = audioStream ? audioStream.BitRate : null; + var totalBitrate = (videoBitrate || 0) + (audioBitrate || 0); + // Some 1080- videos are reported as 1912? if (maxAllowedWidth >= 1910) { options.push({ name: '1080p - 10Mbps', maxWidth: 1920, bitrate: 10000000 }); @@ -630,13 +651,6 @@ options.push({ name: '360p', maxWidth: 640, bitrate: 400000 }); options.push({ name: '240p', maxWidth: 426, bitrate: 320000 }); - var canPlayVideoCodecDirect = (videoStream ? videoStream.Codec : '').toLowerCase().indexOf('h264') != -1; - var canPlayAudioDirect = audioStream ? canPlayAudioStreamDirect(audioStream) : false; - - var videoBitrate = videoStream ? videoStream.BitRate : null; - var audioBitrate = audioStream ? audioStream.BitRate : null; - var totalBitrate = (videoBitrate || 0) + (audioBitrate || 0); - var videoWidth = videoStream ? videoStream.Width : null; var i, length, option; @@ -660,15 +674,18 @@ if (canPlayDirect) { option.isStatic = true; - option.mp4VideoCodec = 'copy'; - option.mp4AudioCodec = 'copy'; + option.videoCodec = 'copy'; + option.audioCodec = 'copy'; } + //else if (canPlayVideoCodecDirect && transcodeExtension == '.m3u8') { + // option.videoCodec = 'copy'; + //} } option.isStatic = option.isStatic || false; - option.mp4VideoCodec = option.mp4VideoCodec || 'h264'; - option.mp4AudioCodec = option.mp4AudioCodec || 'aac'; + option.videoCodec = option.videoCodec || (transcodeExtension == '.webm' ? 'vpx' : 'h264'); + option.audioCodec = option.audioCodec || (transcodeExtension == '.webm' ? 'Vorbis' : 'aac'); option.videoBitrate = option.bitrate - option.audioBitrate; } @@ -681,7 +698,7 @@ options[selectedIndex].selected = true; var firstOption = options[0]; - + if (firstOption.isStatic) { firstOption.name = 'Direct'; } @@ -703,39 +720,60 @@ Static: false }; - var qualityOption = getVideoQualityOptions(item, baseParams.AudioStreamIndex).filter(function (opt) { + var mp4Quality = getVideoQualityOptions(item, baseParams.AudioStreamIndex, '.mp4').filter(function (opt) { return opt.selected; })[0]; - baseParams.maxWidth = qualityOption.maxWidth; - baseParams.videoBitrate = qualityOption.videoBitrate; - baseParams.audioBitrate = qualityOption.audioBitrate; + var webmQuality = getVideoQualityOptions(item, baseParams.AudioStreamIndex, '.webm').filter(function (opt) { + return opt.selected; + })[0]; + + var m3U8Quality = getVideoQualityOptions(item, baseParams.AudioStreamIndex, '.m3u8').filter(function (opt) { + return opt.selected; + })[0]; // Webm must be ahead of mp4 due to the issue of mp4 playing too fast in chrome var prioritizeWebmOverH264 = $.browser.chrome || $.browser.msie; - var staticMp4 = qualityOption.isStatic; + var isStatic = mp4Quality.isStatic; + + startTimeTicksOffset = isStatic ? 0 : startPosition || 0; + + var seekParam = isStatic && startPosition ? '#t=' + (startPosition / 10000000) : ''; var mp4VideoUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.mp4', $.extend({}, baseParams, { - VideoCodec: qualityOption.mp4VideoCodec, - AudioCodec: qualityOption.mp4AudioCodec, profile: 'baseline', level: 3, - Static: staticMp4 - })); + Static: isStatic, + maxWidth: mp4Quality.maxWidth, + videoBitrate: mp4Quality.videoBitrate, + audioBitrate: mp4Quality.audioBitrate, + VideoCodec: mp4Quality.videoCodec, + AudioCodec: mp4Quality.audioCodec + + })) + seekParam; var webmVideoUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.webm', $.extend({}, baseParams, { + VideoCodec: 'vpx', - AudioCodec: 'Vorbis' - })); + AudioCodec: 'Vorbis', + maxWidth: webmQuality.maxWidth, + videoBitrate: webmQuality.videoBitrate, + audioBitrate: webmQuality.audioBitrate + + })) + seekParam; var hlsVideoUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.m3u8', $.extend({}, baseParams, { - VideoCodec: qualityOption.mp4VideoCodec, - AudioCodec: qualityOption.mp4AudioCodec, profile: 'baseline', level: 3, - timeStampOffsetMs: 0 - })); + timeStampOffsetMs: 0, + maxWidth: m3U8Quality.maxWidth, + videoBitrate: m3U8Quality.videoBitrate, + audioBitrate: m3U8Quality.audioBitrate, + VideoCodec: m3U8Quality.videoCodec, + AudioCodec: m3U8Quality.audioCodec + + })) + seekParam; var html = ''; @@ -748,19 +786,19 @@ html += '