374 lines
13 KiB
JavaScript
374 lines
13 KiB
JavaScript
/**
|
|
* Module that manages the queue of SyncPlay.
|
|
* @module components/syncPlay/core/QueueCore
|
|
*/
|
|
|
|
import globalize from '../../../scripts/globalize';
|
|
import toast from '../../../components/toast/toast';
|
|
import * as Helper from './Helper';
|
|
|
|
/**
|
|
* Class that manages the queue of SyncPlay.
|
|
*/
|
|
class QueueCore {
|
|
constructor() {
|
|
this.manager = null;
|
|
this.lastPlayQueueUpdate = null;
|
|
this.playlist = [];
|
|
}
|
|
|
|
/**
|
|
* Initializes the core.
|
|
* @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':
|
|
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);
|
|
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()) {
|
|
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);
|
|
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';
|
|
}
|
|
}
|
|
}
|
|
|
|
export default QueueCore;
|