From 440bfc832ad1c48846b76cfa4bb0a27ecf557519 Mon Sep 17 00:00:00 2001 From: Luke Date: Sun, 7 Jun 2015 16:06:18 -0400 Subject: [PATCH] modularize media player --- dashboard-ui/css/librarymenu.css | 2 +- dashboard-ui/scripts/mediaplayer-video.js | 52 +-- dashboard-ui/scripts/mediaplayer.js | 458 ++++++++++++++++------ 3 files changed, 353 insertions(+), 159 deletions(-) diff --git a/dashboard-ui/css/librarymenu.css b/dashboard-ui/css/librarymenu.css index fdbe317dd0..b4d6d4bf44 100644 --- a/dashboard-ui/css/librarymenu.css +++ b/dashboard-ui/css/librarymenu.css @@ -282,7 +282,7 @@ } } -@media all and (max-width: 1200px) { +@media all and (max-width: 800px) { /* They can use the left menu */ .dashboardEntryHeaderButton { diff --git a/dashboard-ui/scripts/mediaplayer-video.js b/dashboard-ui/scripts/mediaplayer-video.js index 15a7a80bc5..930c684039 100644 --- a/dashboard-ui/scripts/mediaplayer-video.js +++ b/dashboard-ui/scripts/mediaplayer-video.js @@ -271,7 +271,7 @@ var mediaControls = $("#videoPlayer"); - var state = self.getPlayerStateInternal(self.currentMediaElement, item, self.currentMediaSource); + var state = self.getPlayerStateInternal(self.currentMediaRenderer, item, self.currentMediaSource); var url = ""; var imageWidth = 400; @@ -751,7 +751,7 @@ return currentStream.Type == "Audio"; }); - var currentIndex = getParameterByName('AudioStreamIndex', self.getCurrentSrc(self.currentMediaElement)); + var currentIndex = getParameterByName('AudioStreamIndex', self.getCurrentSrc(self.currentMediaRenderer)); var html = ''; html += '
'; @@ -917,7 +917,7 @@ function getQualityFlyoutHtml() { - var currentSrc = self.getCurrentSrc(self.currentMediaElement).toLowerCase(); + var currentSrc = self.getCurrentSrc(self.currentMediaRenderer).toLowerCase(); var isStatic = currentSrc.indexOf('static=true') != -1; var videoStream = self.currentMediaSource.MediaStreams.filter(function (stream) { @@ -1042,13 +1042,11 @@ }; // Replace audio version - self.cleanup = function (playerElement) { + self.cleanup = function (mediaRenderer) { - if (playerElement.tagName.toLowerCase() == 'video') { - currentTimeElement.html('--:--'); + currentTimeElement.html('--:--'); - unbindEventsForPlayback(); - } + unbindEventsForPlayback(); }; self.playVideo = function (item, mediaSource, startPosition) { @@ -1189,41 +1187,18 @@ videoControls.removeClass('hide'); } - var video = $("video", videoElement); + var mediaRenderer = new HtmlMediaRenderer('video'); initialVolume = self.getSavedVolume(); - video.each(function () { - this.volume = initialVolume; - }); + mediaRenderer.volume(initialVolume); volumeSlider.val(initialVolume).slider('refresh'); updateVolumeButtons(initialVolume); - video.one("loadedmetadata.mediaplayerevent", function (e) { + $(mediaRenderer).on("volumechange.mediaplayerevent", function (e) { - // The IE video player won't autoplay without this - if ($.browser.msie) { - this.play(); - } - - }).one("playing.mediaplayerevent", function (e) { - - // TODO: This is not working in chrome. Is it too early? - - // Appending #t=xxx to the query string doesn't seem to work with HLS - if (startPositionInSeekParam && this.currentSrc && this.currentSrc.toLowerCase().indexOf('.m3u8') != -1) { - var element = this; - setTimeout(function () { - element.currentTime = startPositionInSeekParam; - }, 3000); - } - - }).on("volumechange.mediaplayerevent", function (e) { - - var vol = this.volume; - - updateVolumeButtons(vol); + updateVolumeButtons(this.volume()); }).one("playing.mediaplayerevent", function () { @@ -1262,9 +1237,6 @@ self.stop(); - var errorCode = this.error ? this.error.code : ''; - console.log('Html5 Video error code: ' + errorCode); - var errorMsg = Globalize.translate('MessageErrorPlayingVideo'); if (item.Type == "TvChannel") { @@ -1289,7 +1261,7 @@ }).on("click.mediaplayerevent", function (e) { if (!$.browser.mobile) { - if (this.paused) { + if (this.paused()) { self.unpause(); } else { self.pause(); @@ -1311,7 +1283,7 @@ $('body').addClass('bodyWithPopupOpen'); - self.currentMediaElement = video[0]; + self.currentMediaRenderer = mediaRenderer; self.currentDurationTicks = self.currentMediaSource.RunTimeTicks; self.updateNowPlayingInfo(item); diff --git a/dashboard-ui/scripts/mediaplayer.js b/dashboard-ui/scripts/mediaplayer.js index 1d46c21b35..2f45aac62b 100644 --- a/dashboard-ui/scripts/mediaplayer.js +++ b/dashboard-ui/scripts/mediaplayer.js @@ -4,12 +4,11 @@ var self = this; - var testableVideoElement = document.createElement('video'); var currentProgressInterval; var canClientSeek; var currentPlaylistIndex = 0; - self.currentMediaElement = null; + self.currentMediaRenderer = null; self.currentItem = null; self.currentMediaSource = null; @@ -395,20 +394,20 @@ return profile; }; - self.updateCanClientSeek = function (elem) { + self.updateCanClientSeek = function (mediaRenderer) { - var duration = elem.duration; + var duration = mediaRenderer.duration(); canClientSeek = duration && !isNaN(duration) && duration != Number.POSITIVE_INFINITY && duration != Number.NEGATIVE_INFINITY; }; - self.getCurrentSrc = function (mediaElement) { - return mediaElement.currentSrc; + self.getCurrentSrc = function (mediaRenderer) { + return mediaRenderer.currentSrc(); }; - self.getCurrentTicks = function (mediaElement) { + self.getCurrentTicks = function (mediaRenderer) { - var playerTime = Math.floor(10000000 * (mediaElement || self.currentMediaElement).currentTime); + var playerTime = Math.floor(10000000 * (mediaRenderer || self.currentMediaRenderer).currentTime()); playerTime += self.startTimeTicksOffset; @@ -429,7 +428,7 @@ currentProgressInterval = setInterval(function () { - if (self.currentMediaElement) { + if (self.currentMediaRenderer) { if ((new Date().getTime() - self.lastProgressReport) > intervalTime) { @@ -449,7 +448,7 @@ self.canPlayNativeHls = function () { - var media = testableVideoElement; + var media = document.createElement('video'); // safari if (media.canPlayType('application/x-mpegURL').replace(/no/, '') || @@ -472,17 +471,17 @@ self.changeStream = function (ticks, params) { - var element = self.currentMediaElement; + var mediaRenderer = self.currentMediaRenderer; if (canClientSeek && params == null) { - element.currentTime = ticks / (1000 * 10000); + mediaRenderer.currentTime(ticks / (1000 * 10000)); return; } params = params || {}; - var currentSrc = element.currentSrc; + var currentSrc = mediaRenderer.currentSrc(); var playSessionId = getParameterByName('PlaySessionId', currentSrc); var liveStreamId = getParameterByName('LiveStreamId', currentSrc); @@ -490,7 +489,7 @@ if (params.AudioStreamIndex == null && params.SubtitleStreamIndex == null && params.Bitrate == null) { currentSrc = replaceQueryString(currentSrc, 'starttimeticks', ticks || 0); - changeStreamToUrl(element, playSessionId, currentSrc, ticks); + changeStreamToUrl(mediaRenderer, playSessionId, currentSrc, ticks); return; } @@ -514,23 +513,16 @@ self.currentSubtitleStreamIndex = subtitleStreamIndex; currentSrc = ApiClient.getUrl(self.currentMediaSource.TranscodingUrl); - changeStreamToUrl(element, playSessionId, currentSrc, ticks); + changeStreamToUrl(mediaRenderer, playSessionId, currentSrc, ticks); } }); }; - function changeStreamToUrl(element, playSessionId, url, newPositionTicks) { + function changeStreamToUrl(mediaRenderer, playSessionId, url, newPositionTicks) { clearProgressInterval(); - $(element).off('ended.playbackstopped').off('ended.playnext').one("loadedmetadata.mediaplayerevent", function (e) { - - // The IE video player won't autoplay without this - if ($.browser.msie && self.currentItem.MediaType == "Video") { - this.play(); - } - - }).one("play", function () { + $(mediaRenderer).off('ended.playbackstopped').off('ended.playnext').one("play", function () { self.updateCanClientSeek(this); @@ -545,15 +537,14 @@ ApiClient.stopActiveEncodings(playSessionId).done(function () { self.startTimeTicksOffset = newPositionTicks; - element.src = url; + mediaRenderer.setCurrentSrc(url); }); self.updateTextStreamUrls(newPositionTicks || 0); } else { self.startTimeTicksOffset = newPositionTicks; - element.src = url; - element.play(); + mediaRenderer.setCurrentSrc(url); } } @@ -587,7 +578,7 @@ currentTimeElement.html(timeText); } - var state = self.getPlayerStateInternal(self.currentMediaElement, self.currentItem, self.currentMediaSource); + var state = self.getPlayerStateInternal(self.currentMediaRenderer, self.currentItem, self.currentMediaSource); $(self).trigger('positionchange', [state]); }; @@ -902,7 +893,7 @@ } else if (item.MediaType === "Audio") { - self.currentMediaElement = playAudio(item, self.currentMediaSource, startPosition); + self.currentMediaRenderer = playAudio(item, self.currentMediaSource, startPosition); self.currentDurationTicks = self.currentMediaSource.RunTimeTicks; } @@ -1071,7 +1062,7 @@ self.queue = function (options) { - if (!self.currentMediaElement) { + if (!self.playlist.length) { self.play(options); return; } @@ -1105,7 +1096,7 @@ self.queueNext = function (options) { - if (!self.currentMediaElement) { + if (!self.playlist.length) { self.play(options); return; } @@ -1136,11 +1127,11 @@ self.pause = function () { - self.currentMediaElement.pause(); + self.currentMediaRenderer.pause(); }; self.unpause = function () { - self.currentMediaElement.play(); + self.currentMediaRenderer.unpause(); }; self.seek = function (position) { @@ -1159,12 +1150,12 @@ }; self.volume = function () { - return self.currentMediaElement.volume * 100; + return self.currentMediaRenderer.volume() * 100; }; self.toggleMute = function () { - if (self.currentMediaElement) { + if (self.currentMediaRenderer) { console.log('MediaPlayer toggling mute'); @@ -1178,14 +1169,14 @@ self.volumeDown = function () { - if (self.currentMediaElement) { + if (self.currentMediaRenderer) { self.setVolume(Math.max(self.volume() - 2, 0)); } }; self.volumeUp = function () { - if (self.currentMediaElement) { + if (self.currentMediaRenderer) { self.setVolume(Math.min(self.volume() + 2, 100)); } }; @@ -1193,14 +1184,14 @@ // Sets volume using a 0-100 scale self.setVolume = function (val) { - if (self.currentMediaElement) { + if (self.currentMediaRenderer) { console.log('MediaPlayer setting volume to ' + val); - self.currentMediaElement.volume = val / 100; + self.currentMediaRenderer.volume(val / 100); - self.onVolumeChanged(self.currentMediaElement); + self.onVolumeChanged(self.currentMediaRenderer); - self.saveVolume(); + //self.saveVolume(); } }; @@ -1333,36 +1324,25 @@ self.stop = function () { - var elem = self.currentMediaElement; + var mediaRenderer = self.currentMediaRenderer; - if (elem) { + if (mediaRenderer) { - elem.pause(); + mediaRenderer.pause(); - $(elem).off("ended.playnext").one("ended", function () { + $(mediaRenderer).off("ended.playnext").one("ended", function () { $(this).off(); - if (this.tagName.toLowerCase() != 'audio') { - $(this).remove(); - } - - elem.src = null; - elem.src = ""; - self.currentMediaElement = null; + this.destroy(); + self.currentMediaRenderer = null; self.currentItem = null; self.currentMediaSource = null; - // When the browser regains focus it may start auto-playing the last video - if ($.browser.safari) { - elem.src = 'files/dummy.mp4'; - elem.play(); - } - }).trigger("ended"); } else { - self.currentMediaElement = null; + self.currentMediaRenderer = null; self.currentItem = null; self.currentMediaSource = null; } @@ -1374,34 +1354,34 @@ }; self.isPlaying = function () { - return self.currentMediaElement != null; + return self.playlist.length > 0; }; self.getPlayerState = function () { var deferred = $.Deferred(); - var result = self.getPlayerStateInternal(self.currentMediaElement, self.currentItem, self.currentMediaSource); + var result = self.getPlayerStateInternal(self.currentMediaRenderer, self.currentItem, self.currentMediaSource); deferred.resolveWith(null, [result]); return deferred.promise(); }; - self.getPlayerStateInternal = function (playerElement, item, mediaSource) { + self.getPlayerStateInternal = function (mediaRenderer, item, mediaSource) { var state = { PlayState: {} }; - if (playerElement) { + if (mediaRenderer) { - state.PlayState.VolumeLevel = playerElement.volume * 100; - state.PlayState.IsMuted = playerElement.volume == 0; - state.PlayState.IsPaused = playerElement.paused; - state.PlayState.PositionTicks = self.getCurrentTicks(playerElement); + state.PlayState.VolumeLevel = mediaRenderer.volume() * 100; + state.PlayState.IsMuted = mediaRenderer.volume() == 0; + state.PlayState.IsPaused = mediaRenderer.paused(); + state.PlayState.PositionTicks = self.getCurrentTicks(mediaRenderer); - var currentSrc = playerElement.currentSrc; + var currentSrc = mediaRenderer.currentSrc(); if (currentSrc) { @@ -1521,22 +1501,22 @@ // Nothing to setup here }; - self.onPlaybackStart = function (playerElement, item, mediaSource) { + self.onPlaybackStart = function (mediaRenderer, item, mediaSource) { - self.updateCanClientSeek(playerElement); + self.updateCanClientSeek(mediaRenderer); - var state = self.getPlayerStateInternal(playerElement, item, mediaSource); + var state = self.getPlayerStateInternal(mediaRenderer, item, mediaSource); $(self).trigger('playbackstart', [state]); self.startProgressInterval(); }; - self.onVolumeChanged = function (playerElement) { + self.onVolumeChanged = function (mediaRenderer) { - self.saveVolume(playerElement.volume); + self.saveVolume(mediaRenderer.volume()); - var state = self.getPlayerStateInternal(playerElement, self.currentItem, self.currentMediaSource); + var state = self.getPlayerStateInternal(mediaRenderer, self.currentItem, self.currentMediaSource); $(self).trigger('volumechange', [state]); }; @@ -1551,11 +1531,11 @@ $('body').removeClass('bodyWithPopupOpen'); - var playerElement = this; + var mediaRenderer = this; - $(playerElement).off('.mediaplayerevent').off('ended.playbackstopped'); + $(mediaRenderer).off('.mediaplayerevent').off('ended.playbackstopped'); - self.cleanup(playerElement); + self.cleanup(mediaRenderer); clearProgressInterval(); @@ -1570,14 +1550,14 @@ self.resetEnhancements(); } - var state = self.getPlayerStateInternal(playerElement, item, mediaSource); + var state = self.getPlayerStateInternal(mediaRenderer, item, mediaSource); $(self).trigger('playbackstop', [state]); }; - self.onPlaystateChange = function (playerElement) { + self.onPlaystateChange = function (mediaRenderer) { - var state = self.getPlayerStateInternal(playerElement, self.currentItem, self.currentMediaSource); + var state = self.getPlayerStateInternal(mediaRenderer, self.currentItem, self.currentMediaSource); $(self).trigger('playstatechange', [state]); }; @@ -1585,16 +1565,15 @@ $(window).on("beforeunload", function () { // Try to report playback stopped before the browser closes - if (self.currentItem && self.currentMediaElement && currentProgressInterval) { + if (self.currentItem && self.currentMediaRenderer && currentProgressInterval) { - self.onPlaybackStopped.call(self.currentMediaElement); + self.onPlaybackStopped.call(self.currentMediaRenderer); } }); function sendProgressUpdate() { - var element = self.currentMediaElement; - var state = self.getPlayerStateInternal(element, self.currentItem, self.currentMediaSource); + var state = self.getPlayerStateInternal(self.currentMediaRenderer, self.currentItem, self.currentMediaSource); var info = { QueueableMediaTypes: state.NowPlayingItem.MediaType, @@ -1615,9 +1594,13 @@ } } + self._canPlayWebm = null; self.canPlayWebm = function () { - return testableVideoElement.canPlayType('video/webm').replace(/no/, ''); + if (self._canPlayWebm == null) { + self._canPlayWebm = document.createElement('video').canPlayType('video/webm').replace(/no/, ''); + } + return self._canPlayWebm; }; self.canAutoPlayAudio = function () { @@ -1633,30 +1616,9 @@ return true; }; - function getAudioElement() { + function getAudioRenderer() { - var elem = $('.mediaPlayerAudio'); - - if (elem.length) { - return elem; - } - - var html = ''; - - var requiresControls = !self.canAutoPlayAudio(); - - if (requiresControls) { - html += '
';; - } else { - html += ''; - - $(document.body).append(html); - - return $('.mediaPlayerAudio'); + return new HtmlMediaRenderer('audio'); } function onTimeUpdate() { @@ -1673,14 +1635,13 @@ var initialVolume = self.getSavedVolume(); - return getAudioElement().each(function () { + var mediaRenderer = getAudioRenderer(); - this.src = audioUrl; - this.volume = initialVolume; - this.poster = self.getPosterUrl(item); - this.play(); + mediaRenderer.setCurrentSrc(audioUrl); + mediaRenderer.volume(initialVolume); + mediaRenderer.setPoster(self.getPosterUrl(item)); - }).on("volumechange.mediaplayerevent", function () { + $(mediaRenderer).on("volumechange.mediaplayerevent", function () { console.log('audio element event: volumechange'); @@ -1690,8 +1651,6 @@ console.log('audio element event: playing'); - $('.mediaPlayerAudioContainer').hide(); - // For some reason this is firing at the start, so don't bind until playback has begun $(this).on("ended.playbackstopped", self.onPlaybackStopped).one('ended.playnext', self.playNextAfterEnded); @@ -1715,7 +1674,9 @@ // In the event timeupdate isn't firing, at least we can update when this happens self.setCurrentTime(self.getCurrentTicks()); - }).on("timeupdate.mediaplayerevent", onTimeUpdate)[0]; + }).on("timeupdate.mediaplayerevent", onTimeUpdate); + + return mediaRenderer; }; var getItemFields = "MediaSources,Chapters"; @@ -1736,4 +1697,265 @@ }); -})(document, setTimeout, clearTimeout, screen, window.appStorage, $, setInterval, window); \ No newline at end of file +})(document, setTimeout, clearTimeout, screen, window.appStorage, $, setInterval, window); + +(function(){ + + function htmlMediaRenderer(type) { + + var mediaElement; + var self = this; + + function onEnded() { + $(self).trigger('ended'); + } + + function onTimeUpdate() { + $(self).trigger('timeupdate'); + } + + function onVolumeChange() { + $(self).trigger('volumechange'); + } + + function onOneAudioPlaying() { + $('.mediaPlayerAudioContainer').hide(); + } + + function onPlaying() { + $(self).trigger('playing'); + } + + function onPlay() { + $(self).trigger('play'); + } + + function onPause() { + $(self).trigger('pause'); + } + + function onClick() { + $(self).trigger('click'); + } + + function onDblClick() { + $(self).trigger('dblclick'); + } + + function onError() { + + var errorCode = this.error ? this.error.code : ''; + console.log('Media element error code: ' + errorCode); + + $(self).trigger('error'); + } + + function onLoadedMetadata() { + + // The IE video player won't autoplay without this + if ($.browser.msie) { + this.play(); + } + } + + function onOneVideoPlaying() { + + var currentSrc = (this.currentSrc || '').toLowerCase(); + + var parts = currentSrc.split('#'); + + if (parts.length > 1) { + + var parts = parts[parts.length -1].split('='); + + if (parts.length == 2) { + + var startPositionInSeekParam = parseFloat(parts[1]); + + // Appending #t=xxx to the query string doesn't seem to work with HLS + if (startPositionInSeekParam && currentSrc.indexOf('.m3u8') != -1) { + var element = this; + setTimeout(function () { + element.currentTime = startPositionInSeekParam; + }, 3000); + } + } + } + } + + function createAudioElement() { + + var elem = $('.mediaPlayerAudio'); + + if (!elem.length) { + var html = ''; + + var requiresControls = !MediaPlayer.canAutoPlayAudio(); + + if (requiresControls) { + html += '
';; + } else { + html += ''; + + $(document.body).append(html); + + elem = $('.mediaPlayerAudio'); + } + + return $(elem) + .on('timeupdate', onTimeUpdate) + .on('ended', onEnded) + .on('volumechange', onVolumeChange) + .one('playing', onOneAudioPlaying) + .on('play', onPlay) + .on('pause', onPause) + .on('playing', onPlaying) + .on('error', onError)[0]; + } + + function createVideoElement() { + + var elem = $('.itemVideo'); + + return $(elem) + .one('.loadedmetadata') + .one('playing', onOneVideoPlaying) + .on('timeupdate', onTimeUpdate) + .on('ended', onEnded) + .on('volumechange', onVolumeChange) + .on('play', onPlay) + .on('pause', onPause) + .on('playing', onPlaying) + .on('error', onError)[0]; + } + + self.currentTime = function(val) { + + if (mediaElement) { + if (val != null) { + mediaElement.currentTime = val; + return; + } + + return mediaElement.currentTime; + } + }; + + self.duration = function(val) { + + if (mediaElement) { + return mediaElement.duration; + } + + return null; + }; + + self.pause = function() { + if (mediaElement) { + mediaElement.pause(); + } + }; + + self.unpause = function() { + if (mediaElement) { + mediaElement.play(); + } + }; + + self.volume = function(val) { + if (mediaElement) { + if (val != null) { + mediaElement.volume = val; + return; + } + + return mediaElement.volume; + } + }; + + self.setCurrentSrc = function(val) { + + var elem = mediaElement; + + if (!elem) { + return; + } + + if (!val) { + elem.src = null; + elem.src = ""; + + // When the browser regains focus it may start auto-playing the last video + if ($.browser.safari) { + elem.src = 'files/dummy.mp4'; + elem.play(); + } + + return; + } + + elem.src = val; + + if (elem.tagName.toLowerCase() == 'audio') { + elem.play(); + } + else { + + $(elem).one("loadedmetadata.mediaplayerevent", onLoadedMetadata); + } + }; + + self.currentSrc = function() { + if (mediaElement) { + return mediaElement.currentSrc; + } + }; + + self.paused = function() { + + if (mediaElement) { + return mediaElement.paused; + } + + return false; + }; + + self.destroy = function() { + + self.setCurrentSrc(null); + + var elem = mediaElement; + + if (elem) { + + $(elem).off(); + + if (elem.tagName.toLowerCase() != 'audio') { + $(elem).remove(); + } + } + }; + + self.setPoster = function(url) { + var elem = mediaElement; + + if (elem) { + elem.poster = url; + } + }; + + if (type == 'audio') { + mediaElement = createAudioElement(); + } + else { + mediaElement = createVideoElement(); + } + } + + window.HtmlMediaRenderer = htmlMediaRenderer; + +})(); \ No newline at end of file