diff --git a/dashboard-ui/bower_components/emby-webcomponents/.bower.json b/dashboard-ui/bower_components/emby-webcomponents/.bower.json index 0f2433c2be..8f3312d93d 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/.bower.json +++ b/dashboard-ui/bower_components/emby-webcomponents/.bower.json @@ -14,12 +14,12 @@ }, "devDependencies": {}, "ignore": [], - "version": "1.4.412", - "_release": "1.4.412", + "version": "1.4.415", + "_release": "1.4.415", "_resolution": { "type": "version", - "tag": "1.4.412", - "commit": "a3f1a92bdff2edcffb16833836c60613fba0e889" + "tag": "1.4.415", + "commit": "ef218c1a08315f961adbbc08515089198e885972" }, "_source": "https://github.com/MediaBrowser/emby-webcomponents.git", "_target": "^1.2.1", diff --git a/dashboard-ui/bower_components/emby-webcomponents/browserdeviceprofile.js b/dashboard-ui/bower_components/emby-webcomponents/browserdeviceprofile.js index c73c9071d4..146be0ce49 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/browserdeviceprofile.js +++ b/dashboard-ui/bower_components/emby-webcomponents/browserdeviceprofile.js @@ -396,7 +396,7 @@ define(['browser'], function (browser) { } // Can't use mkv on mobile because we have to use the native player controls and they won't be able to seek it - if (canPlayMkv && options.supportsCustomSeeking && !browser.tizen && options.enableMkvProgressive !== false) { + if (canPlayMkv && !browser.tizen && options.enableMkvProgressive !== false) { profile.TranscodingProfiles.push({ Container: 'mkv', Type: 'Video', diff --git a/dashboard-ui/bower_components/emby-webcomponents/chromecastplayer.js b/dashboard-ui/bower_components/emby-webcomponents/chromecastplayer.js new file mode 100644 index 0000000000..c6d2bc41ed --- /dev/null +++ b/dashboard-ui/bower_components/emby-webcomponents/chromecastplayer.js @@ -0,0 +1,937 @@ +define(['appSettings', 'playbackManager', 'connectionManager', 'globalize', 'events'], function (appSettings, playbackManager, connectionManager, globalize, events) { + 'use strict'; + + // Based on https://github.com/googlecast/CastVideos-chrome/blob/master/CastVideos.js + var currentResolve; + var currentReject; + + var PlayerName = 'Chromecast'; + + function sendConnectionResult(isOk) { + + var resolve = currentResolve; + var reject = currentReject; + + currentResolve = null; + currentReject = null; + + if (isOk) { + if (resolve) { + resolve(); + } + } else { + if (reject) { + reject(); + } else { + playbackManager.removeActivePlayer(PlayerName); + } + } + } + + /** + * Constants of states for Chromecast device + **/ + var DEVICE_STATE = { + 'IDLE': 0, + 'ACTIVE': 1, + 'WARNING': 2, + 'ERROR': 3 + }; + + /** + * Constants of states for CastPlayer + **/ + var PLAYER_STATE = { + 'IDLE': 'IDLE', + 'LOADING': 'LOADING', + 'LOADED': 'LOADED', + 'PLAYING': 'PLAYING', + 'PAUSED': 'PAUSED', + 'STOPPED': 'STOPPED', + 'SEEKING': 'SEEKING', + 'ERROR': 'ERROR' + }; + + var applicationID = "2D4B1DA3"; + + // This is the beta version used for testing new changes + + //applicationID = '27C4EB5B'; + + var messageNamespace = 'urn:x-cast:com.connectsdk'; + + var CastPlayer = function () { + + /* device variables */ + // @type {DEVICE_STATE} A state for device + this.deviceState = DEVICE_STATE.IDLE; + + /* Cast player variables */ + // @type {Object} a chrome.cast.media.Media object + this.currentMediaSession = null; + + // @type {string} a chrome.cast.Session object + this.session = null; + // @type {PLAYER_STATE} A state for Cast media player + this.castPlayerState = PLAYER_STATE.IDLE; + + this.hasReceivers = false; + + // bind once - commit 2ebffc2271da0bc5e8b13821586aee2a2e3c7753 + this.errorHandler = this.onError.bind(this); + this.mediaStatusUpdateHandler = this.onMediaStatusUpdate.bind(this); + + this.initializeCastPlayer(); + }; + + /** + * Initialize Cast media player + * Initializes the API. Note that either successCallback and errorCallback will be + * invoked once the API has finished initialization. The sessionListener and + * receiverListener may be invoked at any time afterwards, and possibly more than once. + */ + CastPlayer.prototype.initializeCastPlayer = function () { + + var chrome = window.chrome; + + if (!chrome) { + return; + } + + if (!chrome.cast || !chrome.cast.isAvailable) { + + setTimeout(this.initializeCastPlayer.bind(this), 1000); + return; + } + + // request session + var sessionRequest = new chrome.cast.SessionRequest(applicationID); + var apiConfig = new chrome.cast.ApiConfig(sessionRequest, + this.sessionListener.bind(this), + this.receiverListener.bind(this), + "origin_scoped"); + + console.log('chromecast.initialize'); + + chrome.cast.initialize(apiConfig, this.onInitSuccess.bind(this), this.errorHandler); + + }; + + /** + * Callback function for init success + */ + CastPlayer.prototype.onInitSuccess = function () { + this.isInitialized = true; + console.log("chromecast init success"); + }; + + /** + * Generic error callback function + */ + CastPlayer.prototype.onError = function () { + console.log("chromecast error"); + }; + + /** + * @param {!Object} e A new session + * This handles auto-join when a page is reloaded + * When active session is detected, playback will automatically + * join existing session and occur in Cast mode and media + * status gets synced up with current media of the session + */ + CastPlayer.prototype.sessionListener = function (e) { + + this.session = e; + if (this.session) { + + console.log('sessionListener ' + JSON.stringify(e)); + + if (this.session.media[0]) { + this.onMediaDiscovered('activeSession', this.session.media[0]); + } + + this.onSessionConnected(e); + } + }; + + function alertText(text, title) { + require(['alert'], function (alert) { + alert({ + text: text, + title: title + }); + }); + } + + CastPlayer.prototype.messageListener = function (namespace, message) { + + if (typeof (message) === 'string') { + message = JSON.parse(message); + } + + if (message.type == 'playbackerror') { + + var errorCode = message.data; + + setTimeout(function () { + alertText(globalize.translate('MessagePlaybackError' + errorCode), globalize.translate('HeaderPlaybackError')); + }, 300); + + } + else if (message.type == 'connectionerror') { + + setTimeout(function () { + alertText(globalize.translate('MessageChromecastConnectionError'), globalize.translate('HeaderError')); + }, 300); + + } + else if (message.type) { + events.trigger(this, message.type, [message.data]); + } + }; + + /** + * @param {string} e Receiver availability + * This indicates availability of receivers but + * does not provide a list of device IDs + */ + CastPlayer.prototype.receiverListener = function (e) { + + if (e === 'available') { + console.log("chromecast receiver found"); + this.hasReceivers = true; + } + else { + console.log("chromecast receiver list empty"); + this.hasReceivers = false; + } + }; + + /** + * session update listener + */ + CastPlayer.prototype.sessionUpdateListener = function (isAlive) { + + console.log('sessionUpdateListener alive: ' + isAlive); + + if (isAlive) { + } + else { + this.session = null; + this.deviceState = DEVICE_STATE.IDLE; + this.castPlayerState = PLAYER_STATE.IDLE; + + console.log('sessionUpdateListener: setting currentMediaSession to null'); + this.currentMediaSession = null; + + sendConnectionResult(false); + } + }; + + /** + * Requests that a receiver application session be created or joined. By default, the SessionRequest + * passed to the API at initialization time is used; this may be overridden by passing a different + * session request in opt_sessionRequest. + */ + CastPlayer.prototype.launchApp = function () { + console.log("chromecast launching app..."); + chrome.cast.requestSession(this.onRequestSessionSuccess.bind(this), this.onLaunchError.bind(this)); + }; + + /** + * Callback function for request session success + * @param {Object} e A chrome.cast.Session object + */ + CastPlayer.prototype.onRequestSessionSuccess = function (e) { + + console.log("chromecast session success: " + e.sessionId); + this.onSessionConnected(e); + }; + + CastPlayer.prototype.onSessionConnected = function (session) { + + this.session = session; + + this.deviceState = DEVICE_STATE.ACTIVE; + + this.session.addMessageListener(messageNamespace, this.messageListener.bind(this)); + this.session.addMediaListener(this.sessionMediaListener.bind(this)); + this.session.addUpdateListener(this.sessionUpdateListener.bind(this)); + + events.trigger(this, 'connect'); + + this.sendMessage({ + options: {}, + command: 'Identify' + }); + }; + + /** + * session update listener + */ + CastPlayer.prototype.sessionMediaListener = function (e) { + + console.log('sessionMediaListener'); + this.currentMediaSession = e; + this.currentMediaSession.addUpdateListener(this.mediaStatusUpdateHandler); + }; + + /** + * Callback function for launch error + */ + CastPlayer.prototype.onLaunchError = function () { + console.log("chromecast launch error"); + this.deviceState = DEVICE_STATE.ERROR; + + sendConnectionResult(false); + }; + + /** + * Stops the running receiver application associated with the session. + */ + CastPlayer.prototype.stopApp = function () { + + if (this.session) { + this.session.stop(this.onStopAppSuccess.bind(this, 'Session stopped'), + this.errorHandler); + } + + }; + + /** + * Callback function for stop app success + */ + CastPlayer.prototype.onStopAppSuccess = function (message) { + console.log(message); + this.deviceState = DEVICE_STATE.IDLE; + this.castPlayerState = PLAYER_STATE.IDLE; + + console.log('onStopAppSuccess: setting currentMediaSession to null'); + this.currentMediaSession = null; + }; + + /** + * Loads media into a running receiver application + * @param {Number} mediaIndex An index number to indicate current media content + */ + CastPlayer.prototype.loadMedia = function (options, command) { + + if (!this.session) { + console.log("no session"); + return Promise.reject(); + } + + // Convert the items to smaller stubs to send the minimal amount of information + options.items = options.items.map(function (i) { + + return { + Id: i.Id, + Name: i.Name, + Type: i.Type, + MediaType: i.MediaType, + IsFolder: i.IsFolder + }; + }); + + return this.sendMessage({ + options: options, + command: command + }); + }; + + CastPlayer.prototype.sendMessage = function (message) { + + var player = this; + + var receiverName = null; + + var session = player.session; + + if (session && session.receiver && session.receiver.friendlyName) { + receiverName = session.receiver.friendlyName; + } + + message = Object.assign(message, { + userId: ApiClient.getCurrentUserId(), + deviceId: ApiClient.deviceId(), + accessToken: ApiClient.accessToken(), + serverAddress: ApiClient.serverAddress(), + receiverName: receiverName + }); + + var bitrateSetting = appSettings.maxChromecastBitrate(); + if (bitrateSetting) { + message.maxBitrate = bitrateSetting; + } + + return new Promise(function (resolve, reject) { + + require(['chromecasthelpers'], function (chromecasthelpers) { + + chromecasthelpers.getServerAddress(ApiClient).then(function (serverAddress) { + message.serverAddress = serverAddress; + player.sendMessageInternal(message).then(resolve, reject); + + }, reject); + }); + }); + }; + + CastPlayer.prototype.sendMessageInternal = function (message) { + + message = JSON.stringify(message); + //console.log(message); + + this.session.sendMessage(messageNamespace, message, this.onPlayCommandSuccess.bind(this), this.errorHandler); + return Promise.resolve(); + }; + + CastPlayer.prototype.onPlayCommandSuccess = function () { + console.log('Message was sent to receiver ok.'); + }; + + /** + * Callback function for loadMedia success + * @param {Object} mediaSession A new media object. + */ + CastPlayer.prototype.onMediaDiscovered = function (how, mediaSession) { + + console.log("chromecast new media session ID:" + mediaSession.mediaSessionId + ' (' + how + ')'); + this.currentMediaSession = mediaSession; + + if (how == 'loadMedia') { + this.castPlayerState = PLAYER_STATE.PLAYING; + } + + if (how == 'activeSession') { + this.castPlayerState = mediaSession.playerState; + } + + this.currentMediaSession.addUpdateListener(this.mediaStatusUpdateHandler); + }; + + /** + * Callback function for media status update from receiver + * @param {!Boolean} e true/false + */ + CastPlayer.prototype.onMediaStatusUpdate = function (e) { + + if (e == false) { + this.castPlayerState = PLAYER_STATE.IDLE; + } + console.log("chromecast updating media: " + e); + }; + + /** + * Set media volume in Cast mode + * @param {Boolean} mute A boolean + */ + CastPlayer.prototype.setReceiverVolume = function (mute, vol) { + + if (!this.currentMediaSession) { + console.log('this.currentMediaSession is null'); + return; + } + + if (!mute) { + + this.session.setReceiverVolumeLevel((vol || 1), + this.mediaCommandSuccessCallback.bind(this), + this.errorHandler); + } + else { + this.session.setReceiverMuted(true, + this.mediaCommandSuccessCallback.bind(this), + this.errorHandler); + } + }; + + /** + * Mute CC + */ + CastPlayer.prototype.mute = function () { + this.setReceiverVolume(true); + }; + + /** + * Callback function for media command success + */ + CastPlayer.prototype.mediaCommandSuccessCallback = function (info, e) { + console.log(info); + }; + + function chromecastPlayer() { + + var self = this; + // Create Cast Player + var castPlayer; + + // playbackManager needs this + self.name = PlayerName; + self.type = 'mediaplayer'; + self.id = 'chromecast'; + self.isLocalPlayer = false; + + self.getItemsForPlayback = function (query) { + + var userId = ApiClient.getCurrentUserId(); + + if (query.Ids && query.Ids.split(',').length == 1) { + return ApiClient.getItem(userId, query.Ids.split(',')).then(function (item) { + return { + Items: [item], + TotalRecordCount: 1 + }; + }); + } + else { + + query.Limit = query.Limit || 100; + query.ExcludeLocationTypes = "Virtual"; + + return ApiClient.getItems(userId, query); + } + }; + + function initializeChromecast() { + + fileref.loaded = true; + castPlayer = new CastPlayer(); + + // To allow the native android app to override + document.dispatchEvent(new CustomEvent("chromecastloaded", { + detail: { + player: self + } + })); + + events.on(castPlayer, "connect", function (e) { + + if (currentResolve) { + sendConnectionResult(true); + } else { + playbackManager.setActivePlayer(PlayerName, self.getCurrentTargetInfo()); + } + + console.log('cc: connect'); + // Reset this so the next query doesn't make it appear like content is playing. + self.lastPlayerData = {}; + }); + + events.on(castPlayer, "playbackstart", function (e, data) { + + console.log('cc: playbackstart'); + + castPlayer.initializeCastPlayer(); + + var state = self.getPlayerStateInternal(data); + events.trigger(self, "playbackstart", [state]); + }); + + events.on(castPlayer, "playbackstop", function (e, data) { + + console.log('cc: playbackstop'); + var state = self.getPlayerStateInternal(data); + + events.trigger(self, "playbackstop", [state]); + + // Reset this so the next query doesn't make it appear like content is playing. + self.lastPlayerData = {}; + }); + + events.on(castPlayer, "playbackprogress", function (e, data) { + + console.log('cc: positionchange'); + var state = self.getPlayerStateInternal(data); + + events.trigger(self, "timeupdate", [state]); + }); + + events.on(castPlayer, "volumechange", function (e, data) { + + console.log('cc: volumechange'); + var state = self.getPlayerStateInternal(data); + + events.trigger(self, "volumechange", [state]); + }); + + events.on(castPlayer, "playstatechange", function (e, data) { + + console.log('cc: playstatechange'); + var state = self.getPlayerStateInternal(data); + + events.trigger(self, "pause", [state]); + }); + } + + self.play = function (options) { + + return ApiClient.getCurrentUser().then(function (user) { + + if (options.items) { + + return self.playWithCommand(options, 'PlayNow'); + + } else { + + return self.getItemsForPlayback({ + + Ids: options.ids.join(',') + + }).then(function (result) { + + options.items = result.Items; + return self.playWithCommand(options, 'PlayNow'); + + }); + } + + }); + + }; + + self.playWithCommand = function (options, command) { + + if (!options.items) { + var apiClient = connectionManager.getApiClient(options.serverId); + return apiClient.getItem(apiClient.getCurrentUserId(), options.ids[0]).then(function (item) { + + options.items = [item]; + return self.playWithCommand(options, command); + }); + } + + return castPlayer.loadMedia(options, command); + }; + + self.unpause = function () { + castPlayer.sendMessage({ + options: {}, + command: 'Unpause' + }); + }; + + self.pause = function () { + castPlayer.sendMessage({ + options: {}, + command: 'Pause' + }); + }; + + self.shuffle = function (item) { + + var apiClient = connectionManager.getApiClient(item.ServerId); + var userId = apiClient.getCurrentUserId(); + + apiClient.getItem(userId, item.Id).then(function (item) { + + self.playWithCommand({ + + items: [item] + + }, 'Shuffle'); + + }); + + }; + + self.instantMix = function (item) { + + var apiClient = connectionManager.getApiClient(item.ServerId); + var userId = apiClient.getCurrentUserId(); + + apiClient.getItem(userId, item.Id).then(function (item) { + + self.playWithCommand({ + + items: [item] + + }, 'InstantMix'); + + }); + + }; + + self.canPlayMediaType = function (mediaType) { + + mediaType = (mediaType || '').toLowerCase(); + return mediaType === 'audio' || mediaType === 'video'; + }; + + self.canQueueMediaType = function (mediaType) { + return self.canPlayMediaType(mediaType); + }; + + self.queue = function (options) { + self.playWithCommand(options, 'PlayLast'); + }; + + self.queueNext = function (options) { + self.playWithCommand(options, 'PlayNext'); + }; + + self.stop = function () { + castPlayer.sendMessage({ + options: {}, + command: 'Stop' + }); + }; + + self.displayContent = function (options) { + + castPlayer.sendMessage({ + options: options, + command: 'DisplayContent' + }); + }; + + self.currentTime = function (val) { + + if (val != null) { + return self.seek(val); + } + + var state = self.lastPlayerData || {}; + state = state.PlayState || {}; + return state.PositionTicks; + }; + + self.paused = function () { + var state = self.lastPlayerData || {}; + state = state.PlayState || {}; + + return state.IsPaused; + }; + + self.isMuted = function () { + var state = self.lastPlayerData || {}; + state = state.PlayState || {}; + + return state.IsMuted; + }; + + self.setMute = function (isMuted) { + + if (isMuted) { + castPlayer.sendMessage({ + options: {}, + command: 'Mute' + }); + //castPlayer.setMute(true); + } else { + self.setVolume(self.getVolume() + 2); + } + }; + + self.setRepeatMode = function (mode) { + castPlayer.sendMessage({ + options: { + RepeatMode: mode + }, + command: 'SetRepeatMode' + }); + }; + + self.toggleMute = function () { + + if (self.isMuted()) { + self.setMute(false); + } else { + self.setMute(true); + } + }; + + self.getTargets = function () { + + var targets = []; + + if (castPlayer.hasReceivers) { + targets.push(self.getCurrentTargetInfo()); + } + + return Promise.resolve(targets); + }; + + self.getCurrentTargetInfo = function () { + + var appName = null; + + if (castPlayer.session && castPlayer.session.receiver && castPlayer.session.receiver.friendlyName) { + appName = castPlayer.session.receiver.friendlyName; + } + + return { + name: PlayerName, + id: PlayerName, + playerName: PlayerName, + playableMediaTypes: ["Audio", "Video"], + isLocalPlayer: false, + appName: PlayerName, + deviceName: appName, + supportedCommands: ["VolumeUp", + "VolumeDown", + "Mute", + "Unmute", + "ToggleMute", + "SetVolume", + "SetAudioStreamIndex", + "SetSubtitleStreamIndex", + "DisplayContent", + "SetRepeatMode", + "EndSession"] + }; + }; + + self.seek = function (position) { + + position = parseInt(position); + + position = position / 10000000; + + castPlayer.sendMessage({ + options: { + position: position + }, + command: 'Seek' + }); + }; + + self.setAudioStreamIndex = function (index) { + castPlayer.sendMessage({ + options: { + index: index + }, + command: 'SetAudioStreamIndex' + }); + }; + + self.setSubtitleStreamIndex = function (index) { + castPlayer.sendMessage({ + options: { + index: index + }, + command: 'SetSubtitleStreamIndex' + }); + }; + + self.nextTrack = function () { + castPlayer.sendMessage({ + options: {}, + command: 'NextTrack' + }); + }; + + self.previousTrack = function () { + castPlayer.sendMessage({ + options: {}, + command: 'PreviousTrack' + }); + }; + + self.beginPlayerUpdates = function () { + // Setup polling here + }; + + self.endPlayerUpdates = function () { + // Stop polling here + }; + + self.getVolume = function () { + + var state = self.lastPlayerData || {}; + state = state.PlayState || {}; + + return state.VolumeLevel == null ? 100 : state.VolumeLevel; + }; + + self.volumeDown = function () { + + castPlayer.sendMessage({ + options: {}, + command: 'VolumeDown' + }); + }; + + self.endSession = function () { + + self.stop(); + setTimeout(function () { + castPlayer.stopApp(); + }, 1000); + }; + + self.volumeUp = function () { + + castPlayer.sendMessage({ + options: {}, + command: 'VolumeUp' + }); + }; + + self.setVolume = function (vol) { + + vol = Math.min(vol, 100); + vol = Math.max(vol, 0); + + //castPlayer.setReceiverVolume(false, (vol / 100)); + castPlayer.sendMessage({ + options: { + volume: vol + }, + command: 'SetVolume' + }); + }; + + self.getPlayerState = function () { + + var result = self.getPlayerStateInternal(); + return Promise.resolve(result); + }; + + self.lastPlayerData = {}; + + self.getPlayerStateInternal = function (data) { + + data = data || self.lastPlayerData; + self.lastPlayerData = data; + + console.log(JSON.stringify(data)); + return data; + }; + + self.tryPair = function (target) { + + if (castPlayer.deviceState != DEVICE_STATE.ACTIVE && castPlayer.isInitialized) { + + return new Promise(function (resolve, reject) { + currentResolve = resolve; + currentReject = reject; + + castPlayer.launchApp(); + }); + } else { + + currentResolve = null; + currentReject = null; + + return Promise.reject(); + } + }; + + if (fileref.loaded) { + initializeChromecast(); + } else { + fileref.onload = initializeChromecast; + } + } + + var fileref = document.createElement('script'); + fileref.setAttribute("type", "text/javascript"); + fileref.onload = function () { + fileref.loaded = true; + }; + fileref.setAttribute("src", "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js"); + document.querySelector('head').appendChild(fileref); + + return chromecastPlayer; +}); \ No newline at end of file diff --git a/dashboard-ui/bower_components/emby-webcomponents/htmlaudioplayer/plugin.js b/dashboard-ui/bower_components/emby-webcomponents/htmlaudioplayer/plugin.js index 3f6c741b26..d9ce518470 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/htmlaudioplayer/plugin.js +++ b/dashboard-ui/bower_components/emby-webcomponents/htmlaudioplayer/plugin.js @@ -27,7 +27,6 @@ define(['events', 'browser', 'pluginManager', 'apphost'], function (events, brow require(['browserdeviceprofile'], function (profileBuilder) { var profile = profileBuilder({ - supportsCustomSeeking: true }); resolve(profile); }); @@ -110,7 +109,7 @@ define(['events', 'browser', 'pluginManager', 'apphost'], function (events, brow return true; } - self.stop = function (destroyPlayer, reportEnded) { + self.stop = function (destroyPlayer) { cancelFadeTimeout(); @@ -127,7 +126,7 @@ define(['events', 'browser', 'pluginManager', 'apphost'], function (events, brow elem.src = ''; elem.innerHTML = ''; elem.removeAttribute("src"); - onEndedInternal(reportEnded); + onEnded(); return Promise.resolve(); } @@ -144,7 +143,7 @@ define(['events', 'browser', 'pluginManager', 'apphost'], function (events, brow elem.removeAttribute("src"); elem.volume = originalVolume; - onEndedInternal(reportEnded); + onEnded(); }); } return Promise.resolve(); @@ -212,56 +211,47 @@ define(['events', 'browser', 'pluginManager', 'apphost'], function (events, brow return false; }; - self.volume = function (val) { + self.setVolume = function (val) { if (mediaElement) { - if (val != null) { - mediaElement.volume = val / 100; - return; - } + mediaElement.volume = val / 100; + } + }; + self.getVolume = function () { + if (mediaElement) { return mediaElement.volume * 100; } }; self.volumeUp = function () { - self.volume(Math.min(self.volume() + 2, 100)); + self.setVolume(Math.min(self.getVolume() + 2, 100)); }; self.volumeDown = function () { - self.volume(Math.max(self.volume() - 2, 0)); + self.setVolume(Math.max(self.getVolume() - 2, 0)); }; self.setMute = function (mute) { - if (mute) { - self.volume(0); - } else { - - if (self.isMuted()) { - self.volume(50); - } + if (mediaElement) { + mediaElement.muted = mute; } }; self.isMuted = function () { - return self.volume() === 0; + if (mediaElement) { + return mediaElement.muted; + } + return false; }; function onEnded() { - onEndedInternal(true); - } - - function onEndedInternal(triggerEnded) { - - if (triggerEnded) { - var stopInfo = { - src: currentSrc - }; - - events.trigger(self, 'stopped', [stopInfo]); - } + var stopInfo = { + src: currentSrc + }; + events.trigger(self, 'stopped', [stopInfo]); currentSrc = null; } diff --git a/dashboard-ui/bower_components/emby-webcomponents/input/api.js b/dashboard-ui/bower_components/emby-webcomponents/input/api.js index c91f54ae4a..19cda4d7b9 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/input/api.js +++ b/dashboard-ui/bower_components/emby-webcomponents/input/api.js @@ -89,7 +89,7 @@ define(['connectionManager', 'playbackManager', 'events', 'inputManager', 'focus return; case 'SetVolume': notifyApp(); - playbackManager.volume(cmd.Arguments.Volume); + playbackManager.setVolume(cmd.Arguments.Volume); break; case 'SetAudioStreamIndex': notifyApp(); diff --git a/dashboard-ui/bower_components/emby-webcomponents/mediainfo/mediainfo.js b/dashboard-ui/bower_components/emby-webcomponents/mediainfo/mediainfo.js index 8ba0d9e806..f641fe2364 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/mediainfo/mediainfo.js +++ b/dashboard-ui/bower_components/emby-webcomponents/mediainfo/mediainfo.js @@ -527,21 +527,7 @@ define(['datetime', 'globalize', 'embyRouter', 'itemHelper', 'material-icons', ' var list = []; - if (item.DateCreated && itemHelper.enableDateAddedDisplay(item)) { - list.push({ - type: 'added', - text: globalize.translate('sharedcomponents#AddedOnValue', datetime.toLocaleDateString(datetime.parseISO8601Date(item.DateCreated))) - }); - } - - if (!item.MediaSources) { - return list; - } - - var mediaSource = item.MediaSources[0]; - if (!mediaSource) { - return list; - } + var mediaSource = (item.MediaSources || [])[0] || {}; var videoStream = (mediaSource.MediaStreams || []).filter(function (i) { return i.Type === 'Video'; @@ -620,6 +606,16 @@ define(['datetime', 'globalize', 'embyRouter', 'itemHelper', 'material-icons', ' }); } + if (item.DateCreated && itemHelper.enableDateAddedDisplay(item)) { + + var dateCreated = datetime.parseISO8601Date(item.DateCreated); + + list.push({ + type: 'added', + text: globalize.translate('sharedcomponents#AddedOnValue', datetime.toLocaleDateString(dateCreated) + ' ' + datetime.getDisplayTime(dateCreated)) + }); + } + return list; } diff --git a/dashboard-ui/bower_components/emby-webcomponents/playback/nowplayinghelper.js b/dashboard-ui/bower_components/emby-webcomponents/playback/nowplayinghelper.js new file mode 100644 index 0000000000..72f2023d63 --- /dev/null +++ b/dashboard-ui/bower_components/emby-webcomponents/playback/nowplayinghelper.js @@ -0,0 +1,87 @@ +define([], function () { + 'use strict'; + + function getNowPlayingNames(nowPlayingItem, includeNonNameInfo) { + + var topItem = nowPlayingItem; + var bottomItem = null; + var topText = nowPlayingItem.Name; + + if (nowPlayingItem.AlbumId && nowPlayingItem.MediaType == 'Audio') { + topItem = { + Id: nowPlayingItem.AlbumId, + Name: nowPlayingItem.Album, + Type: 'MusicAlbum', + IsFolder: true + }; + } + + if (nowPlayingItem.MediaType == 'Video') { + if (nowPlayingItem.IndexNumber != null) { + topText = nowPlayingItem.IndexNumber + " - " + topText; + } + if (nowPlayingItem.ParentIndexNumber != null) { + topText = nowPlayingItem.ParentIndexNumber + "." + topText; + } + } + + var bottomText = ''; + + if (nowPlayingItem.Artists && nowPlayingItem.Artists.length) { + + if (nowPlayingItem.ArtistItems && nowPlayingItem.ArtistItems.length) { + + bottomItem = { + Id: nowPlayingItem.ArtistItems[0].Id, + Name: nowPlayingItem.ArtistItems[0].Name, + Type: 'MusicArtist', + IsFolder: true + }; + + bottomText = bottomItem.Name; + } else { + bottomText = nowPlayingItem.Artists[0]; + } + } + else if (nowPlayingItem.SeriesName || nowPlayingItem.Album) { + bottomText = topText; + topText = nowPlayingItem.SeriesName || nowPlayingItem.Album; + + bottomItem = topItem; + + if (nowPlayingItem.SeriesId) { + topItem = { + Id: nowPlayingItem.SeriesId, + Name: nowPlayingItem.SeriesName, + Type: 'Series', + IsFolder: true + }; + } else { + topItem = null; + } + } + else if (nowPlayingItem.ProductionYear && includeNonNameInfo !== false) { + bottomText = nowPlayingItem.ProductionYear; + } + + var list = []; + + list.push({ + text: topText, + item: topItem + }); + + if (bottomText) { + list.push({ + text: bottomText, + item: bottomItem + }); + } + + return list; + } + + return { + getNowPlayingNames: getNowPlayingNames + }; +}); diff --git a/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js b/dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js similarity index 81% rename from dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js rename to dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js index 8e2a50caea..893c339c4f 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js +++ b/dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js @@ -11,7 +11,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return false; } - function playbackManager() { + function PlaybackManager() { var self = this; @@ -38,11 +38,34 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return data.streamInfo ? data.streamInfo.mediaSource : null; }; - function triggerPlayerChange(newPlayer, newTarget, previousPlayer) { + function triggerPlayerChange(newPlayer, newTarget, previousPlayer, previousTargetInfo) { + + if (!newPlayer && !previousPlayer) { + return; + } + + if (newTarget && previousTargetInfo) { + + if (newTarget.id === previousTargetInfo.id) { + return; + } + } events.trigger(self, 'playerchange', [newPlayer, newTarget, previousPlayer]); } + self.beginPlayerUpdates = function (player) { + if (player.beginPlayerUpdates) { + player.beginPlayerUpdates(); + } + }; + + self.endPlayerUpdates = function (player) { + if (player.endPlayerUpdates) { + player.endPlayerUpdates(); + } + }; + self.getPlayerInfo = function () { var player = currentPlayer; @@ -66,6 +89,11 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.setActivePlayer = function (player, targetInfo) { + if (player === 'localplayer' || player.name === 'localplayer') { + setCurrentPlayerInternal(null, null); + return; + } + if (typeof (player) === 'string') { player = players.filter(function (p) { return p.name === player; @@ -76,19 +104,21 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g throw new Error('null player'); } - var previousPlayer = currentPlayer; - - currentPairingId = null; - currentPlayer = player; - currentTargetInfo = targetInfo; - - console.log('Active player: ' + JSON.stringify(currentTargetInfo)); - - triggerPlayerChange(player, targetInfo, previousPlayer); + setCurrentPlayerInternal(player, targetInfo); }; + function displayPlayerInLocalGroup(player) { + + return player.isLocalPlayer; + } + self.trySetActivePlayer = function (player, targetInfo) { + if (player === 'localplayer' || player.name === 'localplayer') { + setCurrentPlayerInternal(null, null); + return; + } + if (typeof (player) === 'string') { player = players.filter(function (p) { return p.name === player; @@ -105,20 +135,13 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g currentPairingId = targetInfo.id; - var promise = player.tryPair ? + var promise = player.tryPair ? player.tryPair(targetInfo) : Promise.resolve(); promise.then(function () { - var previousPlayer = currentPlayer; - - currentPlayer = player; - currentTargetInfo = targetInfo; - - console.log('Active player: ' + JSON.stringify(currentTargetInfo)); - - triggerPlayerChange(player, targetInfo, previousPlayer); + setCurrentPlayerInternal(player, targetInfo); }, function () { if (currentPairingId === targetInfo.id) { @@ -151,36 +174,51 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g function getSupportedCommands(player) { if (player.isLocalPlayer) { - return Dashboard.getSupportedRemoteCommands(); + // Full list + // https://github.com/MediaBrowser/MediaBrowser/blob/master/MediaBrowser.Model/Session/GeneralCommand.cs + return [ + "GoHome", + "GoToSettings", + "VolumeUp", + "VolumeDown", + "Mute", + "Unmute", + "ToggleMute", + "SetVolume", + "SetAudioStreamIndex", + "SetSubtitleStreamIndex", + "DisplayContent", + "GoToSearch", + "DisplayMessage", + "SetRepeatMode" + ]; } throw new Error('player must define supported commands'); } + function createTarget(player) { + return { + name: player.name, + id: player.id, + playerName: player.name, + playableMediaTypes: ['Audio', 'Video', 'Game'].map(player.canPlayMediaType), + isLocalPlayer: player.isLocalPlayer, + supportedCommands: getSupportedCommands(player) + }; + } + function getPlayerTargets(player) { if (player.getTargets) { return player.getTargets(); } - return Promise.resolve([{ - - name: player.name, - id: player.id, - playerName: player.name, - playableMediaTypes: ['Audio', 'Video', 'Game'].map(player.canPlayMediaType), - isLocalPlayer: player.isLocalPlayer, - supportedCommands: getSupportedCommands(player) - }]); + return Promise.resolve([createTarget(player)]); } self.setDefaultPlayerActive = function () { - var player = self.getDefaultPlayer(); - - getPlayerTargets(player).then(function (targets) { - - self.setActivePlayer(player, targets[0]); - }); + self.setActivePlayer('localplayer'); }; self.removeActivePlayer = function (name) { @@ -209,18 +247,18 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g var menuItems = []; menuItems.push({ - name: Globalize.translate('ButtonYes'), + name: globalize.translate('ButtonYes'), id: 'yes' }); menuItems.push({ - name: Globalize.translate('ButtonNo'), + name: globalize.translate('ButtonNo'), id: 'no' }); dialog({ buttons: menuItems, //positionTo: positionTo, - text: Globalize.translate('ConfirmEndPlayerSession') + text: globalize.translate('ConfirmEndPlayerSession') }).then(function (id) { switch (id) { @@ -248,12 +286,25 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.getTargets = function () { - var promises = players.map(getPlayerTargets); + var promises = players.filter(function (p) { + return !displayPlayerInLocalGroup(p); + }).map(getPlayerTargets); return Promise.all(promises).then(function (responses) { var targets = []; + targets.push({ + name: globalize.translate('sharedcomponents#MyDevice'), + id: 'localplayer', + playerName: 'localplayer', + playableMediaTypes: ['Audio', 'Video', 'Game'], + isLocalPlayer: true, + supportedCommands: getSupportedCommands({ + isLocalPlayer: true + }) + }); + for (var i = 0; i < responses.length; i++) { var subTargets = responses[i]; @@ -280,6 +331,60 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g }); }; + self.displayContent = function (options, player) { + player = player || currentPlayer; + if (player && player.displayContent) { + player.displayContent(options); + } + } + + self.sendCommand = function (cmd, player) { + + // Full list + // https://github.com/MediaBrowser/MediaBrowser/blob/master/MediaBrowser.Model/Session/GeneralCommand.cs#L23 + console.log('MediaController received command: ' + cmd.Name); + switch (cmd.Name) { + + case 'SetRepeatMode': + self.setRepeatMode(cmd.Arguments.RepeatMode, player); + break; + case 'VolumeUp': + self.volumeUp(player); + break; + case 'VolumeDown': + self.volumeDown(player); + break; + case 'Mute': + self.setMute(true, player); + break; + case 'Unmute': + self.setMute(false, player); + break; + case 'ToggleMute': + self.toggleMute(player); + break; + case 'SetVolume': + self.setVolume(cmd.Arguments.Volume, player); + break; + case 'SetAudioStreamIndex': + self.setAudioStreamIndex(parseInt(cmd.Arguments.Index), player); + break; + case 'SetSubtitleStreamIndex': + self.setSubtitleStreamIndex(parseInt(cmd.Arguments.Index), player); + break; + case 'ToggleFullscreen': + self.toggleFullscreen(player); + break; + default: + { + if (player.sendCommand) { + player.sendCommand(cmd); + } + break; + } + } + }; + function getCurrentSubtitleStream(player) { var index = getPlayerData(player).subtitleStreamIndex; @@ -323,11 +428,32 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return currentPlayer; }; - function setCurrentPlayer(player) { + function setCurrentPlayerInternal(player, targetInfo) { + + var previousPlayer = currentPlayer; + var previousTargetInfo = currentTargetInfo; + + if (player && !targetInfo && player.isLocalPlayer) { + targetInfo = createTarget(player); + } + + if (player && !targetInfo) { + throw new Error('targetInfo cannot be null'); + } + + currentPairingId = null; currentPlayer = player; + currentTargetInfo = targetInfo; + + if (targetInfo) { + console.log('Active player: ' + JSON.stringify(targetInfo)); + } + if (player && player.isLocalPlayer) { lastLocalPlayer = player; } + + triggerPlayerChange(player, targetInfo, previousPlayer, previousTargetInfo); } self.isPlaying = function () { @@ -360,6 +486,16 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return players; }; + function getAutomaticPlayers() { + + var player = currentPlayer; + if (player && !enableLocalPlaylistManagement(player)) { + return [player]; + } + + return self.getPlayers().filter(enableLocalPlaylistManagement); + } + self.canPlay = function (item) { var itemType = item.Type; @@ -382,7 +518,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g } } - return self.getPlayers().filter(function (p) { + return getAutomaticPlayers().filter(function (p) { return p.canPlayMediaType(mediaType); @@ -406,26 +542,30 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return false; }; - self.isMuted = function () { + self.isMuted = function (player) { - if (currentPlayer) { - return currentPlayer.isMuted(); + player = player || currentPlayer; + + if (player) { + return player.isMuted(); } return false; }; - self.setMute = function (mute) { + self.setMute = function (mute, player) { - if (currentPlayer) { - currentPlayer.setMute(mute); + player = player || currentPlayer; + + if (player) { + player.setMute(mute); } }; - self.toggleMute = function (mute) { + self.toggleMute = function (mute, player) { - var player = currentPlayer; - if (currentPlayer) { + player = player || currentPlayer; + if (player) { if (player.toggleMute) { player.toggleMute(); @@ -435,30 +575,48 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g } }; - self.volume = function (val) { + self.setVolume = function (val, player) { - if (currentPlayer) { - return currentPlayer.volume(val); + player = player || currentPlayer; + + if (player) { + player.setVolume(val); } }; - self.volumeUp = function () { + self.getVolume = function (player) { - if (currentPlayer) { - currentPlayer.volumeUp(); + player = player || currentPlayer; + + if (player) { + return player.getVolume(); } }; - self.volumeDown = function () { + self.volumeUp = function (player) { - if (currentPlayer) { - currentPlayer.volumeDown(); + player = player || currentPlayer; + + if (player) { + player.volumeUp(); } }; - self.setAudioStreamIndex = function (index) { + self.volumeDown = function (player) { - var player = currentPlayer; + player = player || currentPlayer; + + if (player) { + player.volumeDown(); + } + }; + + self.setAudioStreamIndex = function (index, player) { + + player = player || currentPlayer; + if (player && !enableLocalPlaylistManagement(player)) { + return player.setAudioStreamIndex(index); + } if (getPlayerData(player).streamInfo.playMethod === 'Transcode' || !player.canSetAudioStreamIndex()) { @@ -471,9 +629,13 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g } }; - self.setSubtitleStreamIndex = function (index) { + self.setSubtitleStreamIndex = function (index, player) { + + player = player || currentPlayer; + if (player && !enableLocalPlaylistManagement(player)) { + return player.setSubtitleStreamIndex(index); + } - var player = currentPlayer; var currentStream = getCurrentSubtitleStream(player); var newStream = getSubtitleStream(player, index); @@ -531,7 +693,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g if (enabled != null) { var val = enabled ? '1' : '0'; - appSettings.set('displaymirror--' + Dashboard.getCurrentUserId(), val); + appSettings.set('displaymirror', val); if (enabled) { mirrorIfEnabled(); @@ -539,13 +701,17 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return; } - return (appSettings.get('displaymirror--' + Dashboard.getCurrentUserId()) || '') !== '0'; + return (appSettings.get('displaymirror') || '') !== '0'; }; - self.stop = function () { - if (currentPlayer) { + self.stop = function (player) { + + player = player || currentPlayer; + + if (player) { playNextAfterEnded = false; - currentPlayer.stop(true, true); + // TODO: remove second param + player.stop(true, true); } }; @@ -579,9 +745,12 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g } }; - self.seek = function (ticks) { + self.seek = function (ticks, player) { - var player = currentPlayer; + player = player || currentPlayer; + if (currentPlayer && !enableLocalPlaylistManagement(player)) { + return player.seek(ticks); + } changeStream(player, ticks); }; @@ -818,19 +987,55 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.play = function (options) { - if (typeof (options) === 'string') { - options = { ids: [options] }; + normalizePlayOptions(options); + + if (currentPlayer) { + if (options.enableRemotePlayers === false && !currentPlayer.isLocalPlayer) { + return Promise.reject(); + } + + if (!enableLocalPlaylistManagement(currentPlayer)) { + return currentPlayer.play(options); + } } - return playItems(options); + if (options.fullscreen) { + loading.show(); + } + + if (options.items) { + + return translateItemsForPlayback(options.items, options).then(function (items) { + + return playWithIntros(items, options); + }); + + } else { + + if (!options.serverId) { + throw new Error('serverId required!'); + } + + return getItemsForPlayback(options.serverId, { + + Ids: options.ids.join(',') + + }).then(function (result) { + + return translateItemsForPlayback(result.Items, options).then(function (items) { + + return playWithIntros(items, options); + }); + + }); + } }; - self.instantMix = function (id, serverId) { + self.instantMix = function (item, player) { - if (typeof id !== 'string') { - var item = id; - id = item.Id; - serverId = item.ServerId; + player = player || currentPlayer; + if (!enableLocalPlaylistManagement(player)) { + return player.instantMix(item); } var apiClient = connectionManager.getApiClient(serverId); @@ -839,24 +1044,23 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g options.UserId = apiClient.getCurrentUserId(); options.Fields = 'MediaSources'; - apiClient.getInstantMixFromItem(id, options).then(function (result) { + apiClient.getInstantMixFromItem(id, item).then(function (result) { self.play({ items: result.Items }); }); }; - self.shuffle = function (id, serverId) { + self.shuffle = function (shuffleItem, player) { - if (typeof id !== 'string') { - var item = id; - id = item.Id; - serverId = item.ServerId; + player = player || currentPlayer; + if (!enableLocalPlaylistManagement(player)) { + return player.shuffle(shuffleItem); } - var apiClient = connectionManager.getApiClient(serverId); + var apiClient = connectionManager.getApiClient(shuffleItem.ServerId); - apiClient.getItem(apiClient.getCurrentUserId(), id).then(function (item) { + apiClient.getItem(apiClient.getCurrentUserId(), shuffleItem.Id).then(function (item) { var query = { Fields: "MediaSources,Chapters", @@ -915,9 +1119,15 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.getPlayerState = function (player) { player = player || currentPlayer; + + if (!enableLocalPlaylistManagement(player)) { + return player.getPlayerState(); + } + var playerData = getPlayerData(player); - var item = playerData.streamInfo.item; - var mediaSource = playerData.streamInfo.mediaSource; + var streamInfo = playerData.streamInfo; + var item = streamInfo ? streamInfo.item : null; + var mediaSource = streamInfo ? streamInfo.mediaSource : null; var state = { PlayState: {} @@ -925,15 +1135,13 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g if (player) { - state.PlayState.VolumeLevel = player.volume(); + state.PlayState.VolumeLevel = player.getVolume(); state.PlayState.IsMuted = player.isMuted(); state.PlayState.IsPaused = player.paused(); - state.PlayState.PositionTicks = getCurrentTicks(player); state.PlayState.RepeatMode = self.getRepeatMode(); - var currentSrc = player.currentSrc(); - - if (currentSrc) { + if (streamInfo) { + state.PlayState.PositionTicks = getCurrentTicks(player); state.PlayState.SubtitleStreamIndex = playerData.subtitleStreamIndex; state.PlayState.AudioStreamIndex = playerData.audioStreamIndex; @@ -963,10 +1171,16 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g state.NowPlayingItem = getNowPlayingItemForReporting(player, item, mediaSource); } - return state; + return Promise.resolve(state); }; self.currentTime = function (player) { + + player = player || currentPlayer; + if (currentPlayer && !enableLocalPlaylistManagement(player)) { + return player.currentTime(); + } + return getCurrentTicks(player); }; @@ -1000,7 +1214,9 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g nowPlayingItem.PremiereDate = item.PremiereDate; nowPlayingItem.SeriesName = item.SeriesName; nowPlayingItem.Album = item.Album; - nowPlayingItem.Artists = item.ArtistItems; + nowPlayingItem.AlbumId = item.AlbumId; + nowPlayingItem.Artists = item.Artists; + nowPlayingItem.ArtistItems = item.ArtistItems; var imageTags = item.ImageTags || {}; @@ -1055,42 +1271,6 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return nowPlayingItem; } - function playItems(options, method) { - - normalizePlayOptions(options); - - if (options.fullscreen) { - loading.show(); - } - - if (options.items) { - - return translateItemsForPlayback(options.items, options).then(function (items) { - - return playWithIntros(items, options); - }); - - } else { - - if (!options.serverId) { - throw new Error(); - } - - return getItemsForPlayback(options.serverId, { - - Ids: options.ids.join(',') - - }).then(function (result) { - - return translateItemsForPlayback(result.Items, options).then(function (items) { - - return playWithIntros(items, options); - }); - - }); - } - } - function translateItemsForPlayback(items, options) { var firstItem = items[0]; @@ -1281,7 +1461,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g if (player) { player.destroy(); } - setCurrentPlayer(null); + setCurrentPlayerInternal(null); events.trigger(self, 'playbackcancelled'); @@ -1874,11 +2054,9 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g function getPlayer(item, playOptions) { - var players = self.getPlayers(); - var serverItem = isServerItem(item); - return self.getPlayers().filter(function (p) { + return getAutomaticPlayers().filter(function (p) { if (p.canPlayMediaType(item.MediaType)) { @@ -1943,12 +2121,24 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g }); }; - self.setRepeatMode = function (value) { + self.setRepeatMode = function (value, player) { + + player = player || currentPlayer; + if (player && !enableLocalPlaylistManagement(player)) { + return player.setRepeatMode(value); + } + repeatMode = value; events.trigger(self, 'repeatmodechange'); }; - self.getRepeatMode = function () { + self.getRepeatMode = function (player) { + + player = player || currentPlayer; + if (player && !enableLocalPlaylistManagement(player)) { + return player.getRepeatMode(); + } + return repeatMode; }; @@ -1989,7 +2179,12 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g }; } - self.nextTrack = function () { + self.nextTrack = function (player) { + + player = player || currentPlayer; + if (player && !enableLocalPlaylistManagement(player)) { + return player.nextTrack(); + } var newItemInfo = getNextItemInfo(); @@ -2007,7 +2202,13 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g } }; - self.previousTrack = function () { + self.previousTrack = function (player) { + + player = player || currentPlayer; + if (player && !enableLocalPlaylistManagement(player)) { + return player.previousTrack(); + } + var newIndex = currentPlaylistIndex - 1; if (newIndex >= 0) { var newItem = playlist[newIndex]; @@ -2025,23 +2226,28 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g } }; - self.queue = function (options) { - queue(options); + self.queue = function (options, player) { + queue(options, '', player); }; - self.queueNext = function (options) { - queue(options, 'next'); + self.queueNext = function (options, player) { + queue(options, 'next', player); }; - function queue(options, mode) { + function queue(options, mode, player) { - if (!currentPlayer) { - self.play(options); - return; + player = player || currentPlayer; + + if (!player) { + return self.play(options); } - if (typeof (options) === 'string') { - options = { ids: [options] }; + if (!enableLocalPlaylistManagement(player)) { + + if (mode === 'next') { + return player.queueNext(item); + } + return player.queue(item); } // TODO @@ -2049,7 +2255,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g function onPlaybackStarted(player, streamInfo, mediaSource) { - setCurrentPlayer(player); + setCurrentPlayerInternal(player); getPlayerData(player).streamInfo = streamInfo; if (mediaSource) { @@ -2062,13 +2268,15 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g playNextAfterEnded = true; - var state = self.getPlayerState(player); + self.getPlayerState(player).then(function (state) { - reportPlayback(state, getPlayerData(player).streamInfo.item.ServerId, 'reportPlaybackStart'); + reportPlayback(state, getPlayerData(player).streamInfo.item.ServerId, 'reportPlaybackStart'); - startProgressInterval(player); + startProgressInterval(player); - events.trigger(self, 'playbackstart', [player]); + events.trigger(player, 'playbackstart', [state]); + events.trigger(self, 'playbackstart', [player]); + }); } function onPlaybackError(e, error) { @@ -2126,78 +2334,102 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g } // User clicked stop or content ended - var state = self.getPlayerState(player); - var streamInfo = getPlayerData(player).streamInfo; + self.getPlayerState(player).then(function (state) { - if (isServerItem(streamInfo.item)) { + var streamInfo = getPlayerData(player).streamInfo; - if (player.supportsProgress === false && state.PlayState && !state.PlayState.PositionTicks) { - state.PlayState.PositionTicks = streamInfo.item.RunTimeTicks; + if (isServerItem(streamInfo.item)) { + + if (player.supportsProgress === false && state.PlayState && !state.PlayState.PositionTicks) { + state.PlayState.PositionTicks = streamInfo.item.RunTimeTicks; + } + + reportPlayback(state, streamInfo.item.ServerId, 'reportPlaybackStopped'); } - reportPlayback(state, streamInfo.item.ServerId, 'reportPlaybackStopped'); - } + clearProgressInterval(player); - clearProgressInterval(player); + var nextItem = playNextAfterEnded ? getNextItemInfo() : null; - var nextItem = playNextAfterEnded ? getNextItemInfo() : null; + var nextMediaType = (nextItem ? nextItem.item.MediaType : null); - var nextMediaType = (nextItem ? nextItem.item.MediaType : null); + var playbackStopInfo = { + player: player, + state: state, + nextItem: (nextItem ? nextItem.item : null), + nextMediaType: nextMediaType + }; - var playbackStopInfo = { - player: player, - state: state, - nextItem: (nextItem ? nextItem.item : null), - nextMediaType: nextMediaType - }; + events.trigger(player, 'playbackstop', [state]); + events.trigger(self, 'playbackstop', [playbackStopInfo]); - events.trigger(self, 'playbackstop', [playbackStopInfo]); + var newPlayer = nextItem ? getPlayer(nextItem.item, currentPlayOptions) : null; - var newPlayer = nextItem ? getPlayer(nextItem.item, currentPlayOptions) : null; + if (newPlayer !== player) { + player.destroy(); + setCurrentPlayerInternal(null); + } - if (newPlayer !== player) { - player.destroy(); - setCurrentPlayer(null); - } - - if (nextItem) { - self.nextTrack(); - } + if (nextItem) { + self.nextTrack(); + } + }); } function onPlaybackChanging(activePlayer, newPlayer, newItem) { - var state = self.getPlayerState(activePlayer); - var serverId = getPlayerData(activePlayer).streamInfo.item.ServerId; + return self.getPlayerState(activePlayer).then(function (state) { + var serverId = getPlayerData(activePlayer).streamInfo.item.ServerId; - // User started playing something new while existing content is playing - var promise; + // User started playing something new while existing content is playing + var promise; - if (activePlayer === newPlayer) { + unbindStopped(activePlayer); - // If we're staying with the same player, stop it - promise = activePlayer.stop(false, false); + if (activePlayer === newPlayer) { - } else { + // If we're staying with the same player, stop it + // TODO: remove second param + promise = activePlayer.stop(false, true); - // If we're switching players, tear down the current one - promise = activePlayer.stop(true, false); - } + } else { - return promise.then(function () { - reportPlayback(state, serverId, 'reportPlaybackStopped'); + // If we're switching players, tear down the current one + // TODO: remove second param + promise = activePlayer.stop(true, true); + } - clearProgressInterval(activePlayer); + return promise.then(function () { - events.trigger(self, 'playbackstop', [{ - player: activePlayer, - state: state, - nextItem: newItem, - nextMediaType: newItem.MediaType - }]); + bindStopped(activePlayer); + + reportPlayback(state, serverId, 'reportPlaybackStopped'); + + clearProgressInterval(activePlayer); + + events.trigger(self, 'playbackstop', [{ + player: activePlayer, + state: state, + nextItem: newItem, + nextMediaType: newItem.MediaType + }]); + }); }); } + function bindStopped(player) { + + if (enableLocalPlaylistManagement(player)) { + events.off(player, 'stopped', onPlaybackStopped); + events.on(player, 'stopped', onPlaybackStopped); + } + } + + function unbindStopped(player) { + + events.off(player, 'stopped', onPlaybackStopped); + } + function initMediaPlayer(player) { players.push(player); @@ -2214,8 +2446,8 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g if (enableLocalPlaylistManagement(player)) { events.on(player, 'error', onPlaybackError); - events.on(player, 'stopped', onPlaybackStopped); } + bindStopped(player); } events.on(pluginManager, 'registered', function (e, plugin) { @@ -2249,9 +2481,10 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g player.lastProgressReport = new Date().getTime(); - var state = self.getPlayerState(player); - var currentItem = getPlayerData(player).streamInfo.item; - reportPlayback(state, currentItem.ServerId, 'reportPlaybackProgress'); + self.getPlayerState(player).then(function (state) { + var currentItem = getPlayerData(player).streamInfo.item; + reportPlayback(state, currentItem.ServerId, 'reportPlaybackProgress'); + }); } function reportPlayback(state, serverId, method) { @@ -2303,5 +2536,5 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g }); } - return new playbackManager(); + return new PlaybackManager(); }); diff --git a/dashboard-ui/bower_components/emby-webcomponents/playmenu.js b/dashboard-ui/bower_components/emby-webcomponents/playmenu.js index 9a708893a9..d3b5dadb1e 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/playmenu.js +++ b/dashboard-ui/bower_components/emby-webcomponents/playmenu.js @@ -98,7 +98,9 @@ define(['actionsheet', 'datetime', 'playbackManager', 'globalize', 'appSettings' }); break; case 'queue': - playbackManager.queue(item); + playbackManager.queue({ + items: [item] + }); break; case 'instantmix': playbackManager.instantMix(item); diff --git a/dashboard-ui/bower_components/emby-webcomponents/recordingcreator/recordingcreator.js b/dashboard-ui/bower_components/emby-webcomponents/recordingcreator/recordingcreator.js index 3106b61a7d..2821946b6a 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/recordingcreator/recordingcreator.js +++ b/dashboard-ui/bower_components/emby-webcomponents/recordingcreator/recordingcreator.js @@ -119,7 +119,10 @@ apiClient.getLiveTvProgram(programId, apiClient.getCurrentUserId()).then(function (item) { - playbackManager.play(item.ChannelId, serverId); + playbackManager.play({ + ids: [item.ChannelId], + serverId: serverId + }); }); }); return; diff --git a/dashboard-ui/bower_components/emby-webcomponents/sessionplayer.js b/dashboard-ui/bower_components/emby-webcomponents/sessionplayer.js new file mode 100644 index 0000000000..82fec2c8a3 --- /dev/null +++ b/dashboard-ui/bower_components/emby-webcomponents/sessionplayer.js @@ -0,0 +1,368 @@ +define(['playbackManager', 'events', 'serverNotifications'], function (playbackManager, events, serverNotifications) { + 'use strict'; + + function sendPlayCommand(options, playType) { + + var sessionId = playbackManager.getPlayerInfo().id; + + var ids = options.ids || options.items.map(function (i) { + return i.Id; + }); + + var remoteOptions = { + ItemIds: ids.join(','), + + PlayCommand: playType + }; + + if (options.startPositionTicks) { + remoteOptions.startPositionTicks = options.startPositionTicks; + } + + return ApiClient.sendPlayCommand(sessionId, remoteOptions); + } + + function sendPlayStateCommand(command, options) { + + var sessionId = playbackManager.getPlayerInfo().id; + + ApiClient.sendPlayStateCommand(sessionId, command, options); + } + + function RemoteControlPlayer() { + + var self = this; + + self.name = 'Remote Control'; + self.type = 'mediaplayer'; + self.isLocalPlayer = false; + self.id = 'remoteplayer'; + + function sendCommandByName(name, options) { + + var command = { + Name: name + }; + + if (options) { + command.Arguments = options; + } + + self.sendCommand(command); + } + + self.sendCommand = function (command) { + + var sessionId = playbackManager.getPlayerInfo().id; + + ApiClient.sendCommand(sessionId, command); + }; + + self.play = function (options) { + + return sendPlayCommand(options, 'PlayNow'); + }; + + self.shuffle = function (id) { + + sendPlayCommand({ ids: [id] }, 'PlayShuffle'); + }; + + self.instantMix = function (id) { + + sendPlayCommand({ ids: [id] }, 'PlayInstantMix'); + }; + + self.queue = function (options) { + + sendPlayCommand(options, 'PlayNext'); + }; + + self.queueNext = function (options) { + + sendPlayCommand(options, 'PlayLast'); + }; + + self.canQueueMediaType = function (mediaType) { + + return mediaType == 'Audio' || mediaType == 'Video'; + }; + + self.stop = function () { + sendPlayStateCommand('stop'); + }; + + self.nextTrack = function () { + sendPlayStateCommand('nextTrack'); + }; + + self.previousTrack = function () { + sendPlayStateCommand('previousTrack'); + }; + + self.seek = function (positionTicks) { + sendPlayStateCommand('seek', + { + SeekPositionTicks: positionTicks + }); + }; + + self.pause = function () { + sendPlayStateCommand('Pause'); + }; + + self.unpause = function () { + sendPlayStateCommand('Unpause'); + }; + + self.setMute = function (isMuted) { + + if (isMuted) { + sendCommandByName('Mute'); + } else { + sendCommandByName('Unmute'); + } + }; + + self.toggleMute = function () { + sendCommandByName('ToggleMute'); + }; + + self.setVolume = function (vol) { + sendCommandByName('SetVolume', { + Volume: vol + }); + }; + + self.volumeUp = function () { + sendCommandByName('VolumeUp'); + }; + + self.volumeDown = function () { + sendCommandByName('VolumeDown'); + }; + + self.toggleFullscreen = function () { + sendCommandByName('ToggleFullscreen'); + }; + + self.setAudioStreamIndex = function (index) { + sendCommandByName('SetAudioStreamIndex', { + Index: index + }); + }; + + self.setSubtitleStreamIndex = function (index) { + sendCommandByName('SetSubtitleStreamIndex', { + Index: index + }); + }; + + self.setRepeatMode = function (mode) { + + sendCommandByName('SetRepeatMode', { + RepeatMode: mode + }); + }; + + self.displayContent = function (options) { + + sendCommandByName('DisplayContent', options); + }; + + self.getPlayerState = function () { + + var apiClient = window.ApiClient; + + if (apiClient) { + return apiClient.getSessions().then(function (sessions) { + + var currentTargetId = playbackManager.getPlayerInfo().id; + + // Update existing data + //updateSessionInfo(popup, msg.Data); + var session = sessions.filter(function (s) { + return s.Id == currentTargetId; + })[0]; + + if (session) { + session = getPlayerState(session); + } + + return session; + }); + } else { + return Promise.resolve({}); + } + }; + + var pollInterval; + + function onPollIntervalFired() { + + if (!ApiClient.isWebSocketOpen()) { + var apiClient = window.ApiClient; + + if (apiClient) { + apiClient.getSessions().then(processUpdatedSessions); + } + } + } + + self.subscribeToPlayerUpdates = function () { + + self.isUpdating = true; + + if (ApiClient.isWebSocketOpen()) { + + ApiClient.sendWebSocketMessage("SessionsStart", "100,800"); + } + if (pollInterval) { + clearInterval(pollInterval); + pollInterval = null; + } + pollInterval = setInterval(onPollIntervalFired, 5000); + }; + + function unsubscribeFromPlayerUpdates() { + + self.isUpdating = true; + + if (ApiClient.isWebSocketOpen()) { + + ApiClient.sendWebSocketMessage("SessionsStop"); + } + if (pollInterval) { + clearInterval(pollInterval); + pollInterval = null; + } + } + + var playerListenerCount = 0; + self.beginPlayerUpdates = function () { + + if (playerListenerCount <= 0) { + + playerListenerCount = 0; + + self.subscribeToPlayerUpdates(); + } + + playerListenerCount++; + }; + + self.endPlayerUpdates = function () { + + playerListenerCount--; + + if (playerListenerCount <= 0) { + + unsubscribeFromPlayerUpdates(); + playerListenerCount = 0; + } + }; + + self.getTargets = function () { + + var apiClient = window.ApiClient; + + var sessionQuery = { + ControllableByUserId: apiClient.getCurrentUserId() + }; + + if (apiClient) { + return apiClient.getSessions(sessionQuery).then(function (sessions) { + + return sessions.filter(function (s) { + return s.DeviceId != apiClient.deviceId(); + + }).map(function (s) { + return { + name: s.DeviceName, + deviceName: s.DeviceName, + id: s.Id, + playerName: self.name, + appName: s.Client, + playableMediaTypes: s.PlayableMediaTypes, + isLocalPlayer: false, + supportedCommands: s.SupportedCommands + }; + }); + + }); + + } else { + return Promise.resolve([]); + } + }; + + self.tryPair = function(target) { + + return Promise.resolve(); + }; + + function getPlayerState(session) { + + return session; + } + + function firePlaybackEvent(name, session) { + + events.trigger(self, name, [getPlayerState(session)]); + } + + function onWebSocketConnectionChange() { + + // Reconnect + if (self.isUpdating) { + self.subscribeToPlayerUpdates(); + } + } + + function processUpdatedSessions(sessions) { + + var currentTargetId = playbackManager.getPlayerInfo().id; + + // Update existing data + //updateSessionInfo(popup, msg.Data); + var session = sessions.filter(function (s) { + return s.Id == currentTargetId; + })[0]; + + if (session) { + firePlaybackEvent('timeupdate', session); + firePlaybackEvent('pause', session); + } + } + + events.on(serverNotifications, 'Sessions', function (e, apiClient, data) { + processUpdatedSessions(data); + }); + + events.on(serverNotifications, 'SessionEnded', function (e, apiClient, data) { + console.log("Server reports another session ended"); + + if (playbackManager.getPlayerInfo().id == data.Id) { + playbackManager.setDefaultPlayerActive(); + } + }); + + events.on(serverNotifications, 'PlaybackStart', function (e, apiClient, data) { + if (data.DeviceId != apiClient.deviceId()) { + if (playbackManager.getPlayerInfo().id == data.Id) { + firePlaybackEvent('playbackstart', data); + } + } + }); + + events.on(serverNotifications, 'PlaybackStopped', function (e, apiClient, data) { + if (data.DeviceId != apiClient.deviceId()) { + if (playbackManager.getPlayerInfo().id == data.Id) { + firePlaybackEvent('playbackstop', data); + } + } + }); + } + + return RemoteControlPlayer; +}); \ No newline at end of file diff --git a/dashboard-ui/bower_components/emby-webcomponents/shortcuts.js b/dashboard-ui/bower_components/emby-webcomponents/shortcuts.js index 885921a40f..ab8eeb330a 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/shortcuts.js +++ b/dashboard-ui/bower_components/emby-webcomponents/shortcuts.js @@ -240,7 +240,10 @@ define(['playbackManager', 'inputManager', 'connectionManager', 'embyRouter', 'g } else if (action === 'instantmix') { - playbackManager.instantMix(playableItemId, serverId); + playbackManager.instantMix({ + Id: playableItemId, + ServerId: serverId + }); } else if (action === 'play') {