jellyfish-web/src/plugins/syncPlay/core/QueueCore.js

375 lines
13 KiB
JavaScript
Raw Normal View History

/**
* Module that manages the queue of SyncPlay.
2020-11-23 14:27:54 +01:00
* @module components/syncPlay/core/QueueCore
*/
2021-05-26 14:28:54 -04:00
import globalize from '../../../scripts/globalize';
2022-10-01 02:57:30 -04:00
import toast from '../../../components/toast/toast';
2020-11-23 14:27:54 +01:00
import * as Helper from './Helper';
/**
* Class that manages the queue of SyncPlay.
*/
2020-11-23 14:27:54 +01:00
class QueueCore {
constructor() {
this.manager = null;
this.lastPlayQueueUpdate = null;
this.playlist = [];
}
/**
* Initializes the core.
2020-11-23 14:27:54 +01:00
* @param {Manager} syncPlayManager The SyncPlay manager.
*/
init(syncPlayManager) {
this.manager = syncPlayManager;
}
/**
* Handles the change in the play queue.
* @param {Object} apiClient The ApiClient.
* @param {Object} newPlayQueue The new play queue.
*/
updatePlayQueue(apiClient, newPlayQueue) {
newPlayQueue.LastUpdate = new Date(newPlayQueue.LastUpdate);
if (newPlayQueue.LastUpdate.getTime() <= this.getLastUpdateTime()) {
console.debug('SyncPlay updatePlayQueue: ignoring old update', newPlayQueue);
return;
}
console.debug('SyncPlay updatePlayQueue:', newPlayQueue);
const serverId = apiClient.serverInfo().Id;
this.onPlayQueueUpdate(apiClient, newPlayQueue, serverId).then((previous) => {
if (newPlayQueue.LastUpdate.getTime() < this.getLastUpdateTime()) {
console.warn('SyncPlay updatePlayQueue: trying to apply old update.', newPlayQueue);
throw new Error('Trying to apply old update');
}
// Ignore if remote player is self-managed (has own SyncPlay manager running).
if (this.manager.isRemote()) {
console.warn('SyncPlay updatePlayQueue: remote player has own SyncPlay manager.');
return;
}
const playerWrapper = this.manager.getPlayerWrapper();
switch (newPlayQueue.Reason) {
case 'NewPlaylist': {
if (!this.manager.isFollowingGroupPlayback()) {
this.manager.followGroupPlayback(apiClient).then(() => {
this.startPlayback(apiClient);
});
} else {
this.startPlayback(apiClient);
}
break;
}
case 'SetCurrentItem':
2020-12-03 15:55:28 +01:00
case 'NextItem':
case 'PreviousItem': {
playerWrapper.onQueueUpdate();
const playlistItemId = this.getCurrentPlaylistItemId();
this.setCurrentPlaylistItem(apiClient, playlistItemId);
break;
}
case 'RemoveItems': {
playerWrapper.onQueueUpdate();
const index = previous.playQueueUpdate.PlayingItemIndex;
const oldPlaylistItemId = index === -1 ? null : previous.playlist[index].PlaylistItemId;
const playlistItemId = this.getCurrentPlaylistItemId();
if (oldPlaylistItemId !== playlistItemId) {
this.setCurrentPlaylistItem(apiClient, playlistItemId);
}
break;
}
case 'MoveItem':
case 'Queue':
case 'QueueNext': {
playerWrapper.onQueueUpdate();
break;
}
case 'RepeatMode':
playerWrapper.localSetRepeatMode(this.getRepeatMode());
break;
case 'ShuffleMode':
playerWrapper.localSetQueueShuffleMode(this.getShuffleMode());
break;
default:
console.error('SyncPlay updatePlayQueue: unknown reason for update:', newPlayQueue.Reason);
break;
}
}).catch((error) => {
console.warn('SyncPlay updatePlayQueue:', error);
});
}
/**
* Called when a play queue update needs to be applied.
* @param {Object} apiClient The ApiClient.
* @param {Object} playQueueUpdate The play queue update.
* @param {string} serverId The server identifier.
* @returns {Promise} A promise that gets resolved when update is applied.
*/
onPlayQueueUpdate(apiClient, playQueueUpdate, serverId) {
const oldPlayQueueUpdate = this.lastPlayQueueUpdate;
const oldPlaylist = this.playlist;
const itemIds = playQueueUpdate.Playlist.map(queueItem => queueItem.ItemId);
if (!itemIds.length) {
if (this.lastPlayQueueUpdate && playQueueUpdate.LastUpdate.getTime() <= this.getLastUpdateTime()) {
return Promise.reject('Trying to apply old update');
}
this.lastPlayQueueUpdate = playQueueUpdate;
this.playlist = [];
return Promise.resolve({
playQueueUpdate: oldPlayQueueUpdate,
playlist: oldPlaylist
});
}
return Helper.getItemsForPlayback(apiClient, {
Ids: itemIds.join(',')
}).then((result) => {
return Helper.translateItemsForPlayback(apiClient, result.Items, {
ids: itemIds,
serverId: serverId
}).then((items) => {
if (this.lastPlayQueueUpdate && playQueueUpdate.LastUpdate.getTime() <= this.getLastUpdateTime()) {
throw new Error('Trying to apply old update');
}
for (let i = 0; i < items.length; i++) {
items[i].PlaylistItemId = playQueueUpdate.Playlist[i].PlaylistItemId;
}
this.lastPlayQueueUpdate = playQueueUpdate;
this.playlist = items;
return {
playQueueUpdate: oldPlayQueueUpdate,
playlist: oldPlaylist
};
});
});
}
/**
* Sends a SyncPlayBuffering request on playback start.
* @param {Object} apiClient The ApiClient.
* @param {string} origin The origin of the wait call, used for debug.
*/
scheduleReadyRequestOnPlaybackStart(apiClient, origin) {
Helper.waitForEventOnce(this.manager, 'playbackstart', Helper.WaitForEventDefaultTimeout, ['playbackerror']).then(async () => {
console.debug('SyncPlay scheduleReadyRequestOnPlaybackStart: local pause and notify server.');
const playerWrapper = this.manager.getPlayerWrapper();
playerWrapper.localPause();
const currentTime = new Date();
const now = this.manager.timeSyncCore.localDateToRemote(currentTime);
2023-03-29 00:38:22 -04:00
const currentPosition = (playerWrapper.currentTimeAsync ?
await playerWrapper.currentTimeAsync() :
playerWrapper.currentTime());
const currentPositionTicks = Math.round(currentPosition * Helper.TicksPerMillisecond);
const isPlaying = playerWrapper.isPlaying();
apiClient.requestSyncPlayReady({
When: now.toISOString(),
PositionTicks: currentPositionTicks,
IsPlaying: isPlaying,
PlaylistItemId: this.getCurrentPlaylistItemId()
});
}).catch((error) => {
console.error('Error while waiting for `playbackstart` event!', origin, error);
if (!this.manager.isSyncPlayEnabled()) {
2021-05-26 14:28:54 -04:00
toast(globalize.translate('MessageSyncPlayErrorMedia'));
}
this.manager.haltGroupPlayback(apiClient);
});
}
/**
* Prepares this client for playback by loading the group's content.
* @param {Object} apiClient The ApiClient.
*/
startPlayback(apiClient) {
if (!this.manager.isFollowingGroupPlayback()) {
console.debug('SyncPlay startPlayback: ignoring, not following playback.');
return Promise.reject();
}
if (this.isPlaylistEmpty()) {
console.debug('SyncPlay startPlayback: empty playlist.');
return;
}
// Estimate start position ticks from last playback command, if available.
const playbackCommand = this.manager.getLastPlaybackCommand();
let startPositionTicks = 0;
if (playbackCommand && playbackCommand.EmittedAt.getTime() >= this.getLastUpdateTime()) {
// Prefer playback commands as they're more frequent (and also because playback position is PlaybackCore's concern).
startPositionTicks = this.manager.getPlaybackCore().estimateCurrentTicks(playbackCommand.PositionTicks, playbackCommand.When);
} else {
// A PlayQueueUpdate is emited only on queue changes so it's less reliable for playback position syncing.
const oldStartPositionTicks = this.getStartPositionTicks();
const lastQueueUpdateDate = this.getLastUpdate();
startPositionTicks = this.manager.getPlaybackCore().estimateCurrentTicks(oldStartPositionTicks, lastQueueUpdateDate);
}
const serverId = apiClient.serverInfo().Id;
const playerWrapper = this.manager.getPlayerWrapper();
playerWrapper.localPlay({
ids: this.getPlaylistAsItemIds(),
startPositionTicks: startPositionTicks,
startIndex: this.getCurrentPlaylistIndex(),
serverId: serverId
}).then(() => {
this.scheduleReadyRequestOnPlaybackStart(apiClient, 'startPlayback');
}).catch((error) => {
console.error(error);
2021-05-26 14:28:54 -04:00
toast(globalize.translate('MessageSyncPlayErrorMedia'));
});
}
/**
* Sets the current playing item.
* @param {Object} apiClient The ApiClient.
* @param {string} playlistItemId The playlist id of the item to play.
*/
setCurrentPlaylistItem(apiClient, playlistItemId) {
if (!this.manager.isFollowingGroupPlayback()) {
console.debug('SyncPlay setCurrentPlaylistItem: ignoring, not following playback.');
return;
}
this.scheduleReadyRequestOnPlaybackStart(apiClient, 'setCurrentPlaylistItem');
const playerWrapper = this.manager.getPlayerWrapper();
playerWrapper.localSetCurrentPlaylistItem(playlistItemId);
}
/**
* Gets the index of the current playing item.
* @returns {number} The index of the playing item.
*/
getCurrentPlaylistIndex() {
if (this.lastPlayQueueUpdate) {
return this.lastPlayQueueUpdate.PlayingItemIndex;
} else {
return -1;
}
}
/**
* Gets the playlist item id of the playing item.
* @returns {string} The playlist item id.
*/
getCurrentPlaylistItemId() {
if (this.lastPlayQueueUpdate) {
const index = this.lastPlayQueueUpdate.PlayingItemIndex;
return index === -1 ? null : this.playlist[index].PlaylistItemId;
} else {
return null;
}
}
/**
* Gets a copy of the playlist.
* @returns {Array} The playlist.
*/
getPlaylist() {
return this.playlist.slice(0);
}
/**
* Checks if playlist is empty.
* @returns {boolean} _true_ if playlist is empty, _false_ otherwise.
*/
isPlaylistEmpty() {
return this.playlist.length === 0;
}
/**
* Gets the last update time as date, if any.
* @returns {Date} The date.
*/
getLastUpdate() {
if (this.lastPlayQueueUpdate) {
return this.lastPlayQueueUpdate.LastUpdate;
} else {
return null;
}
}
/**
* Gets the time of when the queue has been updated.
* @returns {number} The last update time.
*/
getLastUpdateTime() {
if (this.lastPlayQueueUpdate) {
return this.lastPlayQueueUpdate.LastUpdate.getTime();
} else {
return 0;
}
}
/**
* Gets the last reported start position ticks of playing item.
* @returns {number} The start position ticks.
*/
getStartPositionTicks() {
if (this.lastPlayQueueUpdate) {
return this.lastPlayQueueUpdate.StartPositionTicks;
} else {
return 0;
}
}
/**
* Gets the list of item identifiers in the playlist.
* @returns {Array} The list of items.
*/
getPlaylistAsItemIds() {
if (this.lastPlayQueueUpdate) {
return this.lastPlayQueueUpdate.Playlist.map(queueItem => queueItem.ItemId);
} else {
return [];
}
}
/**
* Gets the repeat mode.
* @returns {string} The repeat mode.
*/
getRepeatMode() {
if (this.lastPlayQueueUpdate) {
return this.lastPlayQueueUpdate.RepeatMode;
} else {
return 'Sorted';
}
}
/**
* Gets the shuffle mode.
* @returns {string} The shuffle mode.
*/
getShuffleMode() {
if (this.lastPlayQueueUpdate) {
return this.lastPlayQueueUpdate.ShuffleMode;
} else {
return 'RepeatNone';
}
}
}
2020-11-23 14:27:54 +01:00
export default QueueCore;