diff --git a/src/components/nowPlayingBar/nowPlayingBar.js b/src/components/nowPlayingBar/nowPlayingBar.js index fec43db3e8..bb823cb1e6 100644 --- a/src/components/nowPlayingBar/nowPlayingBar.js +++ b/src/components/nowPlayingBar/nowPlayingBar.js @@ -170,17 +170,20 @@ function bindEvents(elem) { elem.querySelector('.previousTrackButton').addEventListener('click', function (e) { if (currentPlayer) { - if (lastPlayerState.NowPlayingItem.MediaType === 'Audio') { + if (playbackManager.isPlayingAudio(currentPlayer)) { // Cancel this event if doubleclick is fired. The actual previousTrack will be processed by the 'dblclick' event if (e.detail > 1 ) { return; } + // Return to start of track, unless we are already (almost) at the beginning. In the latter case, continue and move // to the previous track, unless we are at the first track so no previous track exists. - if (currentPlayer._currentTime >= 5 || playbackManager.getCurrentPlaylistIndex(currentPlayer) <= 1) { + // currentTime is in msec. + + if (playbackManager.currentTime(currentPlayer) >= 5 || playbackManager.getCurrentPlaylistIndex(currentPlayer) <= 1) { playbackManager.seekPercent(0, currentPlayer); // This is done automatically by playbackManager, however, setting this here gives instant visual feedback. - // TODO: Check why seekPercentage doesn't reflect the changes inmmediately, so we can remove this workaround. + // TODO: Check why seekPercent doesn't reflect the changes inmmediately, so we can remove this workaround. positionSlider.value = 0; return; } @@ -574,7 +577,8 @@ function updateNowPlayingInfo(state) { itemContextMenu.show(Object.assign({ item: item, user: user - }, options)); + }, options)) + .catch(() => { /* no-op */ }); }); }); } @@ -642,7 +646,8 @@ function hideNowPlayingBar() { } function onPlaybackStopped(e, state) { - console.debug('nowplaying event: ' + e.type); + console.debug('[nowPlayingBar:onPlaybackStopped] event: ' + e.type); + const player = this; if (player.isLocalPlayer) { @@ -669,7 +674,7 @@ function onStateChanged(event, state) { return; } - console.debug('nowplaying event: ' + event.type); + console.debug('[nowPlayingBar:onStateChanged] event: ' + event.type); const player = this; if (!state.NowPlayingItem || layoutManager.tv || state.IsFullscreen === false) { @@ -792,4 +797,3 @@ document.addEventListener('viewbeforeshow', function (e) { } } }); - diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index f2828ba794..99862a4ce9 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -223,7 +223,8 @@ function updateNowPlayingInfo(context, state, serverId) { itemContextMenu.show(Object.assign({ item: fullItem, user: user - }, options)); + }, options)) + .catch(() => { /* no-op */ }); }); }); }); @@ -773,17 +774,20 @@ export default function () { context.querySelector('.btnPreviousTrack').addEventListener('click', function (e) { if (currentPlayer) { - if (lastPlayerState.NowPlayingItem.MediaType === 'Audio') { + if (playbackManager.isPlayingAudio(currentPlayer)) { // Cancel this event if doubleclick is fired. The actual previousTrack will be processed by the 'dblclick' event if (e.detail > 1 ) { return; } + // Return to start of track, unless we are already (almost) at the beginning. In the latter case, continue and move // to the previous track, unless we are at the first track so no previous track exists. - if (currentPlayer._currentTime >= 5 || playbackManager.getCurrentPlaylistIndex(currentPlayer) <= 1) { + // currentTime is in msec. + + if (playbackManager.currentTime(currentPlayer) >= 5 || playbackManager.getCurrentPlaylistIndex(currentPlayer) <= 1) { playbackManager.seekPercent(0, currentPlayer); // This is done automatically by playbackManager, however, setting this here gives instant visual feedback. - // TODO: Check why seekPercentage doesn't reflect the changes inmmediately, so we can remove this workaround. + // TODO: Check why seekPercent doesn't reflect the changes inmmediately, so we can remove this workaround. positionSlider.value = 0; return; } diff --git a/src/components/shortcuts.js b/src/components/shortcuts.js index 58a7bffb05..03418f3e21 100644 --- a/src/components/shortcuts.js +++ b/src/components/shortcuts.js @@ -121,14 +121,15 @@ function showContextMenu(card, options) { playlistId: playlistId, collectionId: collectionId, user: user - - }, options || {})).then(result => { - if (result.command === 'playallfromhere' || result.command === 'queueallfromhere') { - executeAction(card, options.positionTo, result.command); - } else if (result.updated || result.deleted) { - notifyRefreshNeeded(card, options.itemsContainer); - } - }); + }, options || {})) + .then(result => { + if (result.command === 'playallfromhere' || result.command === 'queueallfromhere') { + executeAction(card, options.positionTo, result.command); + } else if (result.updated || result.deleted) { + notifyRefreshNeeded(card, options.itemsContainer); + } + }) + .catch(() => { /* no-op */ }); }); }); }); diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js index 123b3c3092..da0945ec9c 100644 --- a/src/controllers/itemDetails/index.js +++ b/src/controllers/itemDetails/index.js @@ -1966,13 +1966,15 @@ export default function (view, params) { selectedItem = item; apiClient.getCurrentUser().then(function (user) { - itemContextMenu.show(getContextMenuOptions(selectedItem, user, button)).then(function (result) { - if (result.deleted) { - appRouter.goHome(); - } else if (result.updated) { - reload(self, view, params); - } - }); + itemContextMenu.show(getContextMenuOptions(selectedItem, user, button)) + .then(function (result) { + if (result.deleted) { + appRouter.goHome(); + } else if (result.updated) { + reload(self, view, params); + } + }) + .catch(() => { /* no-op */ }); }); }); } diff --git a/src/plugins/chromecastPlayer/plugin.js b/src/plugins/chromecastPlayer/plugin.js index 59950b60d2..664ebf53e6 100644 --- a/src/plugins/chromecastPlayer/plugin.js +++ b/src/plugins/chromecastPlayer/plugin.js @@ -11,17 +11,20 @@ import { getItems } from '../../utils/jellyfin-apiclient/getItems.ts'; // Based on https://github.com/googlecast/CastVideos-chrome/blob/master/CastVideos.js -let currentResolve; -let currentReject; - const PlayerName = 'Google Cast'; +/* + * Some async CastSDK function are completed with callbacks. + * sendConnectionResult turns this into completion as a promise. + */ +let _currentResolve = null; +let _currentReject = null; function sendConnectionResult(isOk) { - const resolve = currentResolve; - const reject = currentReject; + const resolve = _currentResolve; + const reject = _currentReject; - currentResolve = null; - currentReject = null; + _currentResolve = null; + _currentReject = null; if (isOk) { if (resolve) { @@ -128,14 +131,14 @@ class CastPlayer { */ onInitSuccess() { this.isInitialized = true; - console.debug('chromecast init success'); + console.debug('[chromecastPlayer] init success'); } /** * Generic error callback function */ onError() { - console.debug('chromecast error'); + console.debug('[chromecastPlayer] error'); } /** @@ -156,6 +159,7 @@ class CastPlayer { } } + // messageListener - receive callback messages from the Cast receiver messageListener(namespace, message) { if (typeof (message) === 'string') { message = JSON.parse(message); @@ -182,10 +186,10 @@ class CastPlayer { */ receiverListener(e) { if (e === 'available') { - console.debug('chromecast receiver found'); + console.debug('[chromecastPlayer] receiver found'); this.hasReceivers = true; } else { - console.debug('chromecast receiver list empty'); + console.debug('[chromecastPlayer] receiver list empty'); this.hasReceivers = false; } } @@ -195,7 +199,7 @@ class CastPlayer { */ sessionUpdateListener(isAlive) { if (isAlive) { - console.debug('sessionUpdateListener: already alive'); + console.debug('[chromecastPlayer] sessionUpdateListener: already alive'); } else { this.session = null; this.deviceState = DEVICE_STATE.IDLE; @@ -203,7 +207,7 @@ class CastPlayer { document.removeEventListener('volumeupbutton', onVolumeUpKeyDown, false); document.removeEventListener('volumedownbutton', onVolumeDownKeyDown, false); - console.debug('sessionUpdateListener: setting currentMediaSession to null'); + console.debug('[chromecastPlayer] sessionUpdateListener: setting currentMediaSession to null'); this.currentMediaSession = null; sendConnectionResult(false); @@ -216,7 +220,7 @@ class CastPlayer { * session request in opt_sessionRequest. */ launchApp() { - console.debug('chromecast launching app...'); + console.debug('[chromecastPlayer] launching app...'); chrome.cast.requestSession(this.onRequestSessionSuccess.bind(this), this.onLaunchError.bind(this)); } @@ -225,7 +229,7 @@ class CastPlayer { * @param {Object} e A chrome.cast.Session object */ onRequestSessionSuccess(e) { - console.debug('chromecast session success: ' + e.sessionId); + console.debug('[chromecastPlayer] session success: ' + e.sessionId); this.onSessionConnected(e); } @@ -259,7 +263,7 @@ class CastPlayer { * Callback function for launch error */ onLaunchError() { - console.debug('chromecast launch error'); + console.debug('[chromecastPlayer] launch error'); this.deviceState = DEVICE_STATE.ERROR; sendConnectionResult(false); } @@ -289,11 +293,12 @@ class CastPlayer { /** * Loads media into a running receiver application - * @param {Number} mediaIndex An index number to indicate current media content + * @param {Number} mediaIndex - An index number to indicate current media content + * @returns Promise */ loadMedia(options, command) { if (!this.session) { - console.debug('no session'); + console.debug('[chromecastPlayer] no session'); return Promise.reject(new Error('no session')); } @@ -385,7 +390,7 @@ class CastPlayer { * @param {Object} mediaSession A new media object. */ onMediaDiscovered(how, mediaSession) { - console.debug('chromecast new media session ID:' + mediaSession.mediaSessionId + ' (' + how + ')'); + console.debug('[chromecastPlayer] new media session ID:' + mediaSession.mediaSessionId + ' (' + how + ')'); this.currentMediaSession = mediaSession; if (how === 'loadMedia') { @@ -404,7 +409,7 @@ class CastPlayer { * @param {!Boolean} e true/false */ onMediaStatusUpdate(e) { - console.debug('chromecast updating media: ' + e); + console.debug('[chromecastPlayer] updating media: ' + e); if (e === false) { this.castPlayerState = PLAYER_STATE.IDLE; } @@ -498,12 +503,17 @@ function getItemsForPlayback(apiClient, query) { } } +/* + * relay castPlayer events to ChromecastPlayer events and include state info + */ function bindEventForRelay(instance, eventName) { Events.on(instance._castPlayer, eventName, function (e, data) { - console.debug('cc: ' + eventName); - const state = instance.getPlayerStateInternal(data); - - Events.trigger(instance, eventName, [state]); + console.debug('[chromecastPlayer] ' + eventName); + // skip events without data + if (data?.ItemId) { + const state = instance.getPlayerStateInternal(data); + Events.trigger(instance, eventName, [state]); + } }); } @@ -519,30 +529,39 @@ function initializeChromecast() { })); Events.on(instance._castPlayer, 'connect', function () { - if (currentResolve) { + if (_currentResolve) { sendConnectionResult(true); } else { playbackManager.setActivePlayer(PlayerName, instance.getCurrentTargetInfo()); } - console.debug('cc: connect'); + console.debug('[chromecastPlayer] connect'); // Reset this so that statechange will fire instance.lastPlayerData = null; }); Events.on(instance._castPlayer, 'playbackstart', function (e, data) { - console.debug('cc: playbackstart'); + console.debug('[chromecastPlayer] playbackstart'); instance._castPlayer.initializeCastPlayer(); const state = instance.getPlayerStateInternal(data); Events.trigger(instance, 'playbackstart', [state]); + + // be prepared that after this media item a next one may follow. See playbackManager + instance._playNextAfterEnded = true; }); Events.on(instance._castPlayer, 'playbackstop', function (e, data) { - console.debug('cc: playbackstop'); + console.debug('[chromecastPlayer] playbackstop'); + let state = instance.getPlayerStateInternal(data); + if (!instance._playNextAfterEnded) { + // mark that no next media items are to be processed. + state.nextItem = null; + state.NextMediaType = null; + } Events.trigger(instance, 'playbackstop', [state]); state = instance.lastPlayerData.PlayState || {}; @@ -550,14 +569,16 @@ function initializeChromecast() { const mute = state.IsMuted || false; // Reset this so the next query doesn't make it appear like content is playing. - instance.lastPlayerData = {}; - instance.lastPlayerData.PlayState = {}; - instance.lastPlayerData.PlayState.VolumeLevel = volume; - instance.lastPlayerData.PlayState.IsMuted = mute; + instance.lastPlayerData = { + PlayState: { + VolumeLevel: volume, + IsMuted: mute + } + }; }); Events.on(instance._castPlayer, 'playbackprogress', function (e, data) { - console.debug('cc: positionchange'); + console.debug('[chromecastPlayer] positionchange'); const state = instance.getPlayerStateInternal(data); Events.trigger(instance, 'timeupdate', [state]); @@ -571,9 +592,10 @@ function initializeChromecast() { bindEventForRelay(instance, 'shufflequeuemodechange'); Events.on(instance._castPlayer, 'playstatechange', function (e, data) { - console.debug('cc: playstatechange'); - const state = instance.getPlayerStateInternal(data); + console.debug('[chromecastPlayer] playstatechange'); + // Updates the player and nowPlayingBar state to the current 'pause' state. + const state = instance.getPlayerStateInternal(data); Events.trigger(instance, 'pause', [state]); }); } @@ -598,19 +620,21 @@ class ChromecastPlayer { }); } + /* + * Cast button handling: select and connect to chromecast receiver + */ tryPair() { const castPlayer = this._castPlayer; if (castPlayer.deviceState !== DEVICE_STATE.ACTIVE && castPlayer.isInitialized) { return new Promise(function (resolve, reject) { - currentResolve = resolve; - currentReject = reject; + _currentResolve = resolve; + _currentReject = reject; castPlayer.launchApp(); }); } else { - currentResolve = null; - currentReject = null; - + _currentResolve = null; + _currentReject = null; return Promise.reject(new Error('tryPair failed')); } } @@ -674,8 +698,6 @@ class ChromecastPlayer { normalizeImages(data); - console.debug(JSON.stringify(data)); - if (triggerStateChange) { Events.trigger(this, 'statechange', [data]); } @@ -824,6 +846,8 @@ class ChromecastPlayer { } stop() { + // suppress playing a next media item after this one. See playbackManager + this._playNextAfterEnded = false; return this._castPlayer.sendMessage({ options: {}, command: 'Stop' @@ -1039,6 +1063,10 @@ class ChromecastPlayer { this.playWithCommand(options, 'PlayNext'); } + /* + * play + * options.items[]: Id, IsFolder, MediaType, Name, ServerId, Type, ... + */ play(options) { if (options.items) { return this.playWithCommand(options, 'PlayNow'); @@ -1090,6 +1118,15 @@ class ChromecastPlayer { getPlayerState() { return this.getPlayerStateInternal() || {}; } + + getCurrentPlaylistIndex() { + // tbd: update to support playlists and not only album with tracks + return this.getPlayerStateInternal()?.NowPlayingItem?.IndexNumber; + } + + clearQueue(currentTime) { // eslint-disable-line no-unused-vars + // not supported yet + } } export default ChromecastPlayer;