mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #4891 from Sky-High/playback-fixes-for-chromecast
Fix playback control issues with chromecast
This commit is contained in:
commit
34212614bc
5 changed files with 116 additions and 68 deletions
|
@ -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) {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -121,14 +121,15 @@ function showContextMenu(card, options) {
|
|||
playlistId: playlistId,
|
||||
collectionId: collectionId,
|
||||
user: user
|
||||
|
||||
}, options || {})).then(result => {
|
||||
}, 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 */ });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
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 */ });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue