1
0
Fork 0
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:
Bill Thornton 2023-11-08 16:54:35 -05:00 committed by GitHub
commit 34212614bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 116 additions and 68 deletions

View file

@ -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) {
}
}
});

View file

@ -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;
}

View file

@ -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 */ });
});
});
});

View file

@ -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 */ });
});
});
}

View file

@ -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;