2020-04-03 18:49:19 +02:00
|
|
|
/**
|
2020-05-06 23:41:54 +02:00
|
|
|
* Module that manages the SyncPlay feature.
|
|
|
|
* @module components/syncplay/syncPlayManager
|
2020-04-03 18:49:19 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
import events from 'events';
|
|
|
|
import connectionManager from 'connectionManager';
|
|
|
|
import playbackManager from 'playbackManager';
|
2020-04-16 16:05:04 +02:00
|
|
|
import timeSyncManager from 'timeSyncManager';
|
2020-04-03 18:49:19 +02:00
|
|
|
import toast from 'toast';
|
|
|
|
import globalize from 'globalize';
|
|
|
|
|
|
|
|
/**
|
2020-05-05 12:01:43 +02:00
|
|
|
* Waits for an event to be triggered on an object. An optional timeout can specified after which the promise is rejected.
|
2020-04-03 18:49:19 +02:00
|
|
|
* @param {Object} emitter Object on which to listen for events.
|
|
|
|
* @param {string} eventType Event name to listen for.
|
2020-05-05 12:01:43 +02:00
|
|
|
* @param {number} timeout Time in milliseconds before rejecting promise if event does not trigger.
|
2020-04-03 18:49:19 +02:00
|
|
|
* @returns {Promise} A promise that resolves when the event is triggered.
|
|
|
|
*/
|
2020-05-05 12:01:43 +02:00
|
|
|
function waitForEventOnce(emitter, eventType, timeout) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
let rejectTimeout;
|
|
|
|
if (timeout) {
|
|
|
|
rejectTimeout = setTimeout(() => {
|
|
|
|
reject('Timed out.');
|
|
|
|
}, timeout);
|
|
|
|
}
|
|
|
|
const callback = () => {
|
2020-04-03 18:49:19 +02:00
|
|
|
events.off(emitter, eventType, callback);
|
2020-05-05 12:01:43 +02:00
|
|
|
if (rejectTimeout) {
|
|
|
|
clearTimeout(rejectTimeout);
|
|
|
|
}
|
2020-04-03 18:49:19 +02:00
|
|
|
resolve(arguments);
|
|
|
|
};
|
|
|
|
events.on(emitter, eventType, callback);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets active player id.
|
|
|
|
* @returns {string} The player's id.
|
|
|
|
*/
|
|
|
|
function getActivePlayerId() {
|
2020-05-05 12:01:43 +02:00
|
|
|
var info = playbackManager.getPlayerInfo();
|
2020-04-03 18:49:19 +02:00
|
|
|
return info ? info.id : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Playback synchronization
|
|
|
|
*/
|
|
|
|
const MaxAcceptedDelaySpeedToSync = 50; // milliseconds, delay after which SpeedToSync is enabled
|
|
|
|
const MaxAcceptedDelaySkipToSync = 300; // milliseconds, delay after which SkipToSync is enabled
|
|
|
|
const SyncMethodThreshold = 2000; // milliseconds, switches between SpeedToSync or SkipToSync
|
2020-04-15 18:15:28 +02:00
|
|
|
const SpeedToSyncTime = 1000; // milliseconds, duration in which the playback is sped up
|
|
|
|
const MaxAttemptsSpeedToSync = 3; // attempts before disabling SpeedToSync
|
|
|
|
const MaxAttemptsSync = 5; // attempts before disabling syncing at all
|
2020-04-03 18:49:19 +02:00
|
|
|
|
|
|
|
/**
|
2020-05-05 12:01:43 +02:00
|
|
|
* Other constants
|
2020-04-03 18:49:19 +02:00
|
|
|
*/
|
2020-05-05 12:01:43 +02:00
|
|
|
const WaitForEventDefaultTimeout = 30000; // milliseconds
|
|
|
|
const WaitForPlayerEventTimeout = 500; // milliseconds
|
2020-04-03 18:49:19 +02:00
|
|
|
|
|
|
|
/**
|
2020-05-06 23:41:54 +02:00
|
|
|
* Class that manages the SyncPlay feature.
|
2020-04-03 18:49:19 +02:00
|
|
|
*/
|
2020-05-06 23:41:54 +02:00
|
|
|
class SyncPlayManager {
|
2020-04-03 18:49:19 +02:00
|
|
|
constructor() {
|
|
|
|
this.playbackRateSupported = false;
|
|
|
|
this.syncEnabled = false;
|
|
|
|
this.playbackDiffMillis = 0; // used for stats
|
2020-05-05 12:01:43 +02:00
|
|
|
this.syncMethod = 'None'; // used for stats
|
2020-04-15 18:15:28 +02:00
|
|
|
this.syncAttempts = 0;
|
|
|
|
this.lastSyncTime = new Date();
|
|
|
|
this.syncWatcherTimeout = null; // interval that watches playback time and syncs it
|
2020-04-03 18:49:19 +02:00
|
|
|
|
|
|
|
this.lastPlaybackWaiting = null; // used to determine if player's buffering
|
|
|
|
this.minBufferingThresholdMillis = 1000;
|
|
|
|
|
|
|
|
this.currentPlayer = null;
|
2020-05-05 12:01:43 +02:00
|
|
|
this.localPlayerPlaybackRate = 1.0; // used to restore user PlaybackRate
|
2020-04-03 18:49:19 +02:00
|
|
|
|
2020-05-06 23:41:54 +02:00
|
|
|
this.syncPlayEnabledAt = null; // Server time of when SyncPlay has been enabled
|
|
|
|
this.syncPlayReady = false; // SyncPlay is ready after first ping to server
|
2020-04-03 18:49:19 +02:00
|
|
|
|
|
|
|
this.lastCommand = null;
|
|
|
|
this.queuedCommand = null;
|
|
|
|
|
|
|
|
this.scheduledCommand = null;
|
|
|
|
this.syncTimeout = null;
|
|
|
|
|
2020-04-16 16:05:04 +02:00
|
|
|
this.timeOffsetWithServer = 0; // server time minus local time
|
2020-04-03 18:49:19 +02:00
|
|
|
this.roundTripDuration = 0;
|
2020-05-06 23:41:54 +02:00
|
|
|
this.notifySyncPlayReady = false;
|
2020-05-05 12:01:43 +02:00
|
|
|
|
|
|
|
events.on(playbackManager, 'playbackstart', (player, state) => {
|
|
|
|
this.onPlaybackStart(player, state);
|
|
|
|
});
|
|
|
|
|
|
|
|
events.on(playbackManager, 'playbackstop', (stopInfo) => {
|
|
|
|
this.onPlaybackStop(stopInfo);
|
|
|
|
});
|
|
|
|
|
|
|
|
events.on(playbackManager, 'playerchange', () => {
|
2020-04-03 18:49:19 +02:00
|
|
|
this.onPlayerChange();
|
|
|
|
});
|
2020-04-15 18:15:28 +02:00
|
|
|
|
2020-04-03 18:49:19 +02:00
|
|
|
this.bindToPlayer(playbackManager.getCurrentPlayer());
|
2020-04-15 18:15:28 +02:00
|
|
|
|
2020-05-05 12:01:43 +02:00
|
|
|
events.on(this, 'timeupdate', (event) => {
|
2020-04-15 18:15:28 +02:00
|
|
|
this.syncPlaybackTime();
|
|
|
|
});
|
2020-04-16 16:05:04 +02:00
|
|
|
|
2020-05-05 12:01:43 +02:00
|
|
|
events.on(timeSyncManager, 'update', (event, error, timeOffset, ping) => {
|
|
|
|
if (error) {
|
2020-05-06 23:41:54 +02:00
|
|
|
console.debug('SyncPlay, time update issue', error);
|
2020-05-05 12:01:43 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-04-16 16:05:04 +02:00
|
|
|
this.timeOffsetWithServer = timeOffset;
|
|
|
|
this.roundTripDuration = ping * 2;
|
|
|
|
|
2020-05-06 23:41:54 +02:00
|
|
|
if (this.notifySyncPlayReady) {
|
|
|
|
this.syncPlayReady = true;
|
2020-05-05 12:01:43 +02:00
|
|
|
events.trigger(this, 'ready');
|
2020-05-06 23:41:54 +02:00
|
|
|
this.notifySyncPlayReady = false;
|
2020-04-16 16:05:04 +02:00
|
|
|
}
|
2020-04-17 13:42:46 +02:00
|
|
|
|
|
|
|
// Report ping
|
|
|
|
if (this.syncEnabled) {
|
|
|
|
const apiClient = connectionManager.currentApiClient();
|
|
|
|
const sessionId = getActivePlayerId();
|
|
|
|
|
|
|
|
if (!sessionId) {
|
|
|
|
this.signalError();
|
|
|
|
toast({
|
2020-05-06 23:41:54 +02:00
|
|
|
text: globalize.translate('MessageSyncPlayErrorMissingSession')
|
2020-04-17 13:42:46 +02:00
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-05-06 23:41:54 +02:00
|
|
|
apiClient.sendSyncPlayCommand(sessionId, 'UpdatePing', {
|
2020-04-17 13:42:46 +02:00
|
|
|
Ping: ping
|
|
|
|
});
|
|
|
|
}
|
2020-04-16 16:05:04 +02:00
|
|
|
});
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
|
2020-05-05 12:01:43 +02:00
|
|
|
/**
|
|
|
|
* Called when playback starts.
|
|
|
|
*/
|
|
|
|
onPlaybackStart (player, state) {
|
|
|
|
events.trigger(this, 'playbackstart', [player, state]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called when playback stops.
|
|
|
|
*/
|
|
|
|
onPlaybackStop (stopInfo) {
|
|
|
|
events.trigger(this, 'playbackstop', [stopInfo]);
|
2020-05-06 23:41:54 +02:00
|
|
|
if (this.isSyncPlayEnabled()) {
|
|
|
|
this.disableSyncPlay(false);
|
2020-05-05 12:01:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-03 18:49:19 +02:00
|
|
|
/**
|
|
|
|
* Called when the player changes.
|
|
|
|
*/
|
|
|
|
onPlayerChange () {
|
|
|
|
this.bindToPlayer(playbackManager.getCurrentPlayer());
|
2020-05-05 12:01:43 +02:00
|
|
|
events.trigger(this, 'playerchange', [this.currentPlayer]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called when playback unpauses.
|
|
|
|
*/
|
|
|
|
onPlayerUnpause () {
|
|
|
|
events.trigger(this, 'unpause', [this.currentPlayer]);
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-05-05 12:01:43 +02:00
|
|
|
* Called when playback pauses.
|
2020-04-03 18:49:19 +02:00
|
|
|
*/
|
2020-05-05 12:01:43 +02:00
|
|
|
onPlayerPause() {
|
|
|
|
events.trigger(this, 'pause', [this.currentPlayer]);
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called on playback progress.
|
|
|
|
* @param {Object} e The time update event.
|
|
|
|
*/
|
|
|
|
onTimeUpdate (e) {
|
2020-04-15 18:15:28 +02:00
|
|
|
// NOTICE: this event is unreliable, at least in Safari
|
|
|
|
// which just stops firing the event after a while.
|
2020-05-05 12:01:43 +02:00
|
|
|
events.trigger(this, 'timeupdate', [e]);
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called when playback is resumed.
|
|
|
|
*/
|
|
|
|
onPlaying () {
|
|
|
|
// TODO: implement group wait
|
|
|
|
this.lastPlaybackWaiting = null;
|
2020-05-05 12:01:43 +02:00
|
|
|
events.trigger(this, 'playing');
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called when playback is buffering.
|
|
|
|
*/
|
|
|
|
onWaiting () {
|
|
|
|
// TODO: implement group wait
|
|
|
|
if (!this.lastPlaybackWaiting) {
|
|
|
|
this.lastPlaybackWaiting = new Date();
|
|
|
|
}
|
2020-05-05 12:01:43 +02:00
|
|
|
events.trigger(this, 'waiting');
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets playback buffering status.
|
|
|
|
* @returns {boolean} _true_ if player is buffering, _false_ otherwise.
|
|
|
|
*/
|
|
|
|
isBuffering () {
|
|
|
|
if (this.lastPlaybackWaiting === null) return false;
|
|
|
|
return (new Date() - this.lastPlaybackWaiting) > this.minBufferingThresholdMillis;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Binds to the player's events.
|
|
|
|
* @param {Object} player The player.
|
|
|
|
*/
|
|
|
|
bindToPlayer (player) {
|
|
|
|
if (player !== this.currentPlayer) {
|
|
|
|
this.releaseCurrentPlayer();
|
|
|
|
this.currentPlayer = player;
|
|
|
|
if (!player) return;
|
|
|
|
}
|
2020-05-05 12:01:43 +02:00
|
|
|
|
|
|
|
// FIXME: the following are needed because the 'events' module
|
|
|
|
// is changing the scope when executing the callbacks.
|
|
|
|
// For instance, calling 'onPlayerUnpause' from the wrong scope breaks things because 'this'
|
2020-05-06 23:41:54 +02:00
|
|
|
// points to 'player' (the event emitter) instead of pointing to the SyncPlayManager singleton.
|
2020-04-03 18:49:19 +02:00
|
|
|
const self = this;
|
2020-05-05 12:01:43 +02:00
|
|
|
this._onPlayerUnpause = () => {
|
|
|
|
self.onPlayerUnpause();
|
2020-04-03 18:49:19 +02:00
|
|
|
};
|
|
|
|
|
2020-05-05 12:01:43 +02:00
|
|
|
this._onPlayerPause = () => {
|
|
|
|
self.onPlayerPause();
|
2020-04-03 18:49:19 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
this._onTimeUpdate = (e) => {
|
|
|
|
self.onTimeUpdate(e);
|
|
|
|
};
|
|
|
|
|
|
|
|
this._onPlaying = () => {
|
|
|
|
self.onPlaying();
|
|
|
|
};
|
|
|
|
|
|
|
|
this._onWaiting = () => {
|
|
|
|
self.onWaiting();
|
|
|
|
};
|
|
|
|
|
2020-05-05 12:01:43 +02:00
|
|
|
events.on(player, 'unpause', this._onPlayerUnpause);
|
|
|
|
events.on(player, 'pause', this._onPlayerPause);
|
|
|
|
events.on(player, 'timeupdate', this._onTimeUpdate);
|
|
|
|
events.on(player, 'playing', this._onPlaying);
|
|
|
|
events.on(player, 'waiting', this._onWaiting);
|
|
|
|
this.playbackRateSupported = player.supports('PlaybackRate');
|
|
|
|
|
|
|
|
// Save player current PlaybackRate value
|
|
|
|
if (this.playbackRateSupported) {
|
|
|
|
this.localPlayerPlaybackRate = player.getPlaybackRate();
|
|
|
|
}
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes the bindings to the current player's events.
|
|
|
|
*/
|
|
|
|
releaseCurrentPlayer () {
|
|
|
|
var player = this.currentPlayer;
|
|
|
|
if (player) {
|
2020-05-05 12:01:43 +02:00
|
|
|
events.off(player, 'unpause', this._onPlayerUnpause);
|
|
|
|
events.off(player, 'pause', this._onPlayerPause);
|
|
|
|
events.off(player, 'timeupdate', this._onTimeUpdate);
|
|
|
|
events.off(player, 'playing', this._onPlaying);
|
|
|
|
events.off(player, 'waiting', this._onWaiting);
|
|
|
|
// Restore player original PlaybackRate value
|
2020-04-03 18:49:19 +02:00
|
|
|
if (this.playbackRateSupported) {
|
2020-05-05 12:01:43 +02:00
|
|
|
player.setPlaybackRate(this.localPlayerPlaybackRate);
|
|
|
|
this.localPlayerPlaybackRate = 1.0;
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
this.currentPlayer = null;
|
|
|
|
this.playbackRateSupported = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles a group update from the server.
|
|
|
|
* @param {Object} cmd The group update.
|
|
|
|
* @param {Object} apiClient The ApiClient.
|
|
|
|
*/
|
|
|
|
processGroupUpdate (cmd, apiClient) {
|
|
|
|
switch (cmd.Type) {
|
|
|
|
case 'PrepareSession':
|
2020-04-15 18:15:28 +02:00
|
|
|
this.prepareSession(apiClient, cmd.GroupId, cmd.Data);
|
2020-04-03 18:49:19 +02:00
|
|
|
break;
|
|
|
|
case 'UserJoined':
|
|
|
|
toast({
|
2020-05-06 23:41:54 +02:00
|
|
|
text: globalize.translate('MessageSyncPlayUserJoined', cmd.Data)
|
2020-04-03 18:49:19 +02:00
|
|
|
});
|
|
|
|
break;
|
|
|
|
case 'UserLeft':
|
|
|
|
toast({
|
2020-05-06 23:41:54 +02:00
|
|
|
text: globalize.translate('MessageSyncPlayUserLeft', cmd.Data)
|
2020-04-03 18:49:19 +02:00
|
|
|
});
|
|
|
|
break;
|
|
|
|
case 'GroupJoined':
|
2020-05-06 23:41:54 +02:00
|
|
|
this.enableSyncPlay(apiClient, new Date(cmd.Data), true);
|
2020-04-03 18:49:19 +02:00
|
|
|
break;
|
|
|
|
case 'NotInGroup':
|
|
|
|
case 'GroupLeft':
|
2020-05-06 23:41:54 +02:00
|
|
|
this.disableSyncPlay(true);
|
2020-04-03 18:49:19 +02:00
|
|
|
break;
|
|
|
|
case 'GroupWait':
|
|
|
|
toast({
|
2020-05-06 23:41:54 +02:00
|
|
|
text: globalize.translate('MessageSyncPlayGroupWait', cmd.Data)
|
2020-04-03 18:49:19 +02:00
|
|
|
});
|
|
|
|
break;
|
2020-05-05 12:01:43 +02:00
|
|
|
case 'GroupDoesNotExist':
|
2020-04-22 22:48:26 +02:00
|
|
|
toast({
|
2020-05-06 23:41:54 +02:00
|
|
|
text: globalize.translate('MessageSyncPlayGroupDoesNotExist')
|
2020-04-22 22:48:26 +02:00
|
|
|
});
|
|
|
|
break;
|
|
|
|
case 'CreateGroupDenied':
|
|
|
|
toast({
|
2020-05-06 23:41:54 +02:00
|
|
|
text: globalize.translate('MessageSyncPlayCreateGroupDenied')
|
2020-04-22 22:48:26 +02:00
|
|
|
});
|
|
|
|
break;
|
|
|
|
case 'JoinGroupDenied':
|
|
|
|
toast({
|
2020-05-06 23:41:54 +02:00
|
|
|
text: globalize.translate('MessageSyncPlayJoinGroupDenied')
|
2020-04-22 22:48:26 +02:00
|
|
|
});
|
|
|
|
break;
|
|
|
|
case 'LibraryAccessDenied':
|
|
|
|
toast({
|
2020-05-06 23:41:54 +02:00
|
|
|
text: globalize.translate('MessageSyncPlayLibraryAccessDenied')
|
2020-04-22 22:48:26 +02:00
|
|
|
});
|
2020-04-03 18:49:19 +02:00
|
|
|
break;
|
|
|
|
default:
|
2020-05-06 23:41:54 +02:00
|
|
|
console.error('processSyncPlayGroupUpdate: command is not recognised: ' + cmd.Type);
|
2020-04-03 18:49:19 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles a playback command from the server.
|
|
|
|
* @param {Object} cmd The playback command.
|
|
|
|
* @param {Object} apiClient The ApiClient.
|
|
|
|
*/
|
|
|
|
processCommand (cmd, apiClient) {
|
|
|
|
if (cmd === null) return;
|
|
|
|
|
2020-05-06 23:41:54 +02:00
|
|
|
if (!this.isSyncPlayEnabled()) {
|
|
|
|
console.debug('SyncPlay processCommand: SyncPlay not enabled, ignoring command', cmd);
|
2020-04-03 18:49:19 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-05-06 23:41:54 +02:00
|
|
|
if (!this.syncPlayReady) {
|
|
|
|
console.debug('SyncPlay processCommand: SyncPlay not ready, queued command', cmd);
|
2020-04-03 18:49:19 +02:00
|
|
|
this.queuedCommand = cmd;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd.When = new Date(cmd.When);
|
2020-04-15 18:15:28 +02:00
|
|
|
cmd.EmittedAt = new Date(cmd.EmitttedAt);
|
2020-04-03 18:49:19 +02:00
|
|
|
|
2020-05-06 23:41:54 +02:00
|
|
|
if (cmd.EmitttedAt < this.syncPlayEnabledAt) {
|
|
|
|
console.debug('SyncPlay processCommand: ignoring old command', cmd);
|
2020-04-03 18:49:19 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if new command differs from last one
|
|
|
|
if (this.lastCommand &&
|
|
|
|
this.lastCommand.When === cmd.When &&
|
|
|
|
this.lastCommand.PositionTicks === cmd.PositionTicks &&
|
|
|
|
this.Command === cmd.Command
|
|
|
|
) {
|
2020-05-06 23:41:54 +02:00
|
|
|
console.debug('SyncPlay processCommand: ignoring duplicate command', cmd);
|
2020-04-03 18:49:19 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.lastCommand = cmd;
|
2020-05-06 23:41:54 +02:00
|
|
|
console.log('SyncPlay will', cmd.Command, 'at', cmd.When, 'PositionTicks', cmd.PositionTicks);
|
2020-04-03 18:49:19 +02:00
|
|
|
|
|
|
|
switch (cmd.Command) {
|
|
|
|
case 'Play':
|
|
|
|
this.schedulePlay(cmd.When, cmd.PositionTicks);
|
|
|
|
break;
|
|
|
|
case 'Pause':
|
|
|
|
this.schedulePause(cmd.When, cmd.PositionTicks);
|
|
|
|
break;
|
|
|
|
case 'Seek':
|
|
|
|
this.scheduleSeek(cmd.When, cmd.PositionTicks);
|
|
|
|
break;
|
|
|
|
default:
|
2020-05-05 12:01:43 +02:00
|
|
|
console.error('processCommand: command is not recognised: ' + cmd.Type);
|
2020-04-03 18:49:19 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-15 18:15:28 +02:00
|
|
|
/**
|
|
|
|
* Prepares this client to join a group by loading the required content.
|
|
|
|
* @param {Object} apiClient The ApiClient.
|
|
|
|
* @param {string} groupId The group to join.
|
|
|
|
* @param {Object} sessionData Info about the content to load.
|
|
|
|
*/
|
|
|
|
prepareSession (apiClient, groupId, sessionData) {
|
2020-05-05 12:01:43 +02:00
|
|
|
const serverId = apiClient.serverInfo().Id;
|
2020-04-15 18:15:28 +02:00
|
|
|
playbackManager.play({
|
|
|
|
ids: sessionData.ItemIds,
|
|
|
|
startPositionTicks: sessionData.StartPositionTicks,
|
|
|
|
mediaSourceId: sessionData.MediaSourceId,
|
|
|
|
audioStreamIndex: sessionData.AudioStreamIndex,
|
|
|
|
subtitleStreamIndex: sessionData.SubtitleStreamIndex,
|
|
|
|
startIndex: sessionData.StartIndex,
|
|
|
|
serverId: serverId
|
|
|
|
}).then(() => {
|
2020-05-05 12:01:43 +02:00
|
|
|
waitForEventOnce(this, 'playbackstart', WaitForEventDefaultTimeout).then(() => {
|
2020-04-15 18:15:28 +02:00
|
|
|
var sessionId = getActivePlayerId();
|
|
|
|
if (!sessionId) {
|
2020-05-05 12:01:43 +02:00
|
|
|
console.error('Missing sessionId!');
|
2020-04-15 18:15:28 +02:00
|
|
|
toast({
|
2020-05-06 23:41:54 +02:00
|
|
|
text: globalize.translate('MessageSyncPlayErrorMissingSession')
|
2020-04-15 18:15:28 +02:00
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Get playing item id
|
|
|
|
let playingItemId;
|
|
|
|
try {
|
|
|
|
const playState = playbackManager.getPlayerState();
|
|
|
|
playingItemId = playState.NowPlayingItem.Id;
|
|
|
|
} catch (error) {
|
2020-05-05 12:01:43 +02:00
|
|
|
playingItemId = '';
|
2020-04-15 18:15:28 +02:00
|
|
|
}
|
2020-05-05 12:01:43 +02:00
|
|
|
// Make sure the server has received the player state
|
|
|
|
waitForEventOnce(playbackManager, 'reportplayback', WaitForEventDefaultTimeout).then((success) => {
|
|
|
|
this.localPause();
|
|
|
|
if (!success) {
|
|
|
|
console.warning('Error reporting playback state to server. Joining group will fail.');
|
|
|
|
}
|
2020-05-06 23:41:54 +02:00
|
|
|
apiClient.sendSyncPlayCommand(sessionId, 'JoinGroup', {
|
2020-04-15 18:15:28 +02:00
|
|
|
GroupId: groupId,
|
|
|
|
PlayingItemId: playingItemId
|
|
|
|
});
|
2020-05-05 12:01:43 +02:00
|
|
|
}).catch(() => {
|
|
|
|
console.error('Timed out while waiting for `reportplayback` event!');
|
|
|
|
toast({
|
2020-05-06 23:41:54 +02:00
|
|
|
text: globalize.translate('MessageSyncPlayErrorMedia')
|
2020-05-05 12:01:43 +02:00
|
|
|
});
|
|
|
|
return;
|
|
|
|
});
|
|
|
|
}).catch(() => {
|
|
|
|
console.error('Timed out while waiting for `playbackstart` event!');
|
2020-05-06 23:41:54 +02:00
|
|
|
if (!this.isSyncPlayEnabled()) {
|
2020-05-05 12:01:43 +02:00
|
|
|
toast({
|
2020-05-06 23:41:54 +02:00
|
|
|
text: globalize.translate('MessageSyncPlayErrorMedia')
|
2020-05-05 12:01:43 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return;
|
2020-04-15 18:15:28 +02:00
|
|
|
});
|
|
|
|
}).catch((error) => {
|
|
|
|
console.error(error);
|
|
|
|
toast({
|
2020-05-06 23:41:54 +02:00
|
|
|
text: globalize.translate('MessageSyncPlayErrorMedia')
|
2020-04-15 18:15:28 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-05-06 23:41:54 +02:00
|
|
|
* Enables SyncPlay.
|
2020-04-15 18:15:28 +02:00
|
|
|
* @param {Object} apiClient The ApiClient.
|
2020-05-06 23:41:54 +02:00
|
|
|
* @param {Date} enabledAt When SyncPlay has been enabled. Server side date.
|
2020-04-15 18:15:28 +02:00
|
|
|
* @param {boolean} showMessage Display message.
|
|
|
|
*/
|
2020-05-06 23:41:54 +02:00
|
|
|
enableSyncPlay (apiClient, enabledAt, showMessage = false) {
|
|
|
|
this.syncPlayEnabledAt = enabledAt;
|
2020-04-16 16:05:04 +02:00
|
|
|
this.injectPlaybackManager();
|
2020-05-05 12:01:43 +02:00
|
|
|
events.trigger(this, 'enabled', [true]);
|
2020-04-16 16:05:04 +02:00
|
|
|
|
2020-05-05 12:01:43 +02:00
|
|
|
waitForEventOnce(this, 'ready').then(() => {
|
2020-04-15 18:15:28 +02:00
|
|
|
this.processCommand(this.queuedCommand, apiClient);
|
|
|
|
this.queuedCommand = null;
|
|
|
|
});
|
2020-05-05 12:01:43 +02:00
|
|
|
|
2020-05-06 23:41:54 +02:00
|
|
|
this.syncPlayReady = false;
|
|
|
|
this.notifySyncPlayReady = true;
|
2020-04-16 16:05:04 +02:00
|
|
|
|
|
|
|
timeSyncManager.forceUpdate();
|
2020-04-15 18:15:28 +02:00
|
|
|
|
|
|
|
if (showMessage) {
|
|
|
|
toast({
|
2020-05-06 23:41:54 +02:00
|
|
|
text: globalize.translate('MessageSyncPlayEnabled')
|
2020-04-15 18:15:28 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-05-06 23:41:54 +02:00
|
|
|
* Disables SyncPlay.
|
2020-04-15 18:15:28 +02:00
|
|
|
* @param {boolean} showMessage Display message.
|
|
|
|
*/
|
2020-05-06 23:41:54 +02:00
|
|
|
disableSyncPlay (showMessage = false) {
|
|
|
|
this.syncPlayEnabledAt = null;
|
|
|
|
this.syncPlayReady = false;
|
2020-04-15 18:15:28 +02:00
|
|
|
this.lastCommand = null;
|
|
|
|
this.queuedCommand = null;
|
|
|
|
this.syncEnabled = false;
|
2020-05-05 12:01:43 +02:00
|
|
|
events.trigger(this, 'enabled', [false]);
|
2020-04-15 18:15:28 +02:00
|
|
|
this.restorePlaybackManager();
|
|
|
|
|
|
|
|
if (showMessage) {
|
|
|
|
toast({
|
2020-05-06 23:41:54 +02:00
|
|
|
text: globalize.translate('MessageSyncPlayDisabled')
|
2020-04-15 18:15:28 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-05-06 23:41:54 +02:00
|
|
|
* Gets SyncPlay status.
|
2020-04-15 18:15:28 +02:00
|
|
|
* @returns {boolean} _true_ if user joined a group, _false_ otherwise.
|
|
|
|
*/
|
2020-05-06 23:41:54 +02:00
|
|
|
isSyncPlayEnabled () {
|
|
|
|
return this.syncPlayEnabledAt !== null;
|
2020-04-15 18:15:28 +02:00
|
|
|
}
|
|
|
|
|
2020-04-03 18:49:19 +02:00
|
|
|
/**
|
|
|
|
* Schedules a resume playback on the player at the specified clock time.
|
|
|
|
* @param {Date} playAtTime The server's UTC time at which to resume playback.
|
|
|
|
* @param {number} positionTicks The PositionTicks from where to resume.
|
|
|
|
*/
|
|
|
|
schedulePlay (playAtTime, positionTicks) {
|
|
|
|
this.clearScheduledCommand();
|
2020-05-05 12:01:43 +02:00
|
|
|
const currentTime = new Date();
|
|
|
|
const playAtTimeLocal = timeSyncManager.serverDateToLocal(playAtTime);
|
2020-04-03 18:49:19 +02:00
|
|
|
|
|
|
|
if (playAtTimeLocal > currentTime) {
|
2020-05-05 12:01:43 +02:00
|
|
|
const playTimeout = playAtTimeLocal - currentTime;
|
|
|
|
this.localSeek(positionTicks);
|
2020-04-03 18:49:19 +02:00
|
|
|
|
|
|
|
this.scheduledCommand = setTimeout(() => {
|
2020-05-05 12:01:43 +02:00
|
|
|
this.localUnpause();
|
2020-04-03 18:49:19 +02:00
|
|
|
|
|
|
|
this.syncTimeout = setTimeout(() => {
|
2020-04-15 18:15:28 +02:00
|
|
|
this.syncEnabled = true;
|
|
|
|
}, SyncMethodThreshold / 2);
|
2020-04-03 18:49:19 +02:00
|
|
|
|
|
|
|
}, playTimeout);
|
|
|
|
|
2020-05-05 12:01:43 +02:00
|
|
|
console.debug('Scheduled play in', playTimeout / 1000.0, 'seconds.');
|
2020-04-03 18:49:19 +02:00
|
|
|
} else {
|
|
|
|
// Group playback already started
|
2020-05-05 12:01:43 +02:00
|
|
|
const serverPositionTicks = positionTicks + (currentTime - playAtTimeLocal) * 10000;
|
|
|
|
waitForEventOnce(this, 'unpause').then(() => {
|
|
|
|
this.localSeek(serverPositionTicks);
|
|
|
|
});
|
|
|
|
this.localUnpause();
|
2020-04-03 18:49:19 +02:00
|
|
|
|
|
|
|
this.syncTimeout = setTimeout(() => {
|
2020-04-15 18:15:28 +02:00
|
|
|
this.syncEnabled = true;
|
|
|
|
}, SyncMethodThreshold / 2);
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Schedules a pause playback on the player at the specified clock time.
|
|
|
|
* @param {Date} pauseAtTime The server's UTC time at which to pause playback.
|
|
|
|
* @param {number} positionTicks The PositionTicks where player will be paused.
|
|
|
|
*/
|
|
|
|
schedulePause (pauseAtTime, positionTicks) {
|
|
|
|
this.clearScheduledCommand();
|
2020-05-05 12:01:43 +02:00
|
|
|
const currentTime = new Date();
|
|
|
|
const pauseAtTimeLocal = timeSyncManager.serverDateToLocal(pauseAtTime);
|
|
|
|
|
|
|
|
const callback = () => {
|
|
|
|
waitForEventOnce(this, 'pause', WaitForPlayerEventTimeout).then(() => {
|
|
|
|
this.localSeek(positionTicks);
|
|
|
|
}).catch(() => {
|
|
|
|
// Player was already paused, seeking
|
|
|
|
this.localSeek(positionTicks);
|
|
|
|
});
|
|
|
|
this.localPause();
|
|
|
|
};
|
2020-04-03 18:49:19 +02:00
|
|
|
|
|
|
|
if (pauseAtTimeLocal > currentTime) {
|
2020-05-05 12:01:43 +02:00
|
|
|
const pauseTimeout = pauseAtTimeLocal - currentTime;
|
|
|
|
this.scheduledCommand = setTimeout(callback, pauseTimeout);
|
2020-04-03 18:49:19 +02:00
|
|
|
|
2020-05-05 12:01:43 +02:00
|
|
|
console.debug('Scheduled pause in', pauseTimeout / 1000.0, 'seconds.');
|
2020-04-03 18:49:19 +02:00
|
|
|
} else {
|
2020-05-05 12:01:43 +02:00
|
|
|
callback();
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Schedules a seek playback on the player at the specified clock time.
|
|
|
|
* @param {Date} pauseAtTime The server's UTC time at which to seek playback.
|
|
|
|
* @param {number} positionTicks The PositionTicks where player will be seeked.
|
|
|
|
*/
|
|
|
|
scheduleSeek (seekAtTime, positionTicks) {
|
|
|
|
this.schedulePause(seekAtTime, positionTicks);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clears the current scheduled command.
|
|
|
|
*/
|
|
|
|
clearScheduledCommand () {
|
|
|
|
clearTimeout(this.scheduledCommand);
|
|
|
|
clearTimeout(this.syncTimeout);
|
|
|
|
|
|
|
|
this.syncEnabled = false;
|
|
|
|
if (this.currentPlayer) {
|
|
|
|
this.currentPlayer.setPlaybackRate(1);
|
|
|
|
}
|
|
|
|
this.clearSyncIcon();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Overrides some PlaybackManager's methods to intercept playback commands.
|
|
|
|
*/
|
|
|
|
injectPlaybackManager () {
|
2020-05-06 23:41:54 +02:00
|
|
|
if (!this.isSyncPlayEnabled()) return;
|
|
|
|
if (playbackManager.syncPlayEnabled) return;
|
2020-04-03 18:49:19 +02:00
|
|
|
|
2020-05-05 12:01:43 +02:00
|
|
|
// TODO: make this less hacky
|
|
|
|
playbackManager._localUnpause = playbackManager.unpause;
|
|
|
|
playbackManager._localPause = playbackManager.pause;
|
|
|
|
playbackManager._localSeek = playbackManager.seek;
|
2020-04-03 18:49:19 +02:00
|
|
|
|
|
|
|
playbackManager.unpause = this.playRequest;
|
|
|
|
playbackManager.pause = this.pauseRequest;
|
|
|
|
playbackManager.seek = this.seekRequest;
|
2020-05-06 23:41:54 +02:00
|
|
|
playbackManager.syncPlayEnabled = true;
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Restores original PlaybackManager's methods.
|
|
|
|
*/
|
|
|
|
restorePlaybackManager () {
|
2020-05-06 23:41:54 +02:00
|
|
|
if (this.isSyncPlayEnabled()) return;
|
|
|
|
if (!playbackManager.syncPlayEnabled) return;
|
2020-04-03 18:49:19 +02:00
|
|
|
|
2020-05-05 12:01:43 +02:00
|
|
|
playbackManager.unpause = playbackManager._localUnpause;
|
|
|
|
playbackManager.pause = playbackManager._localPause;
|
|
|
|
playbackManager.seek = playbackManager._localSeek;
|
2020-05-06 23:41:54 +02:00
|
|
|
playbackManager.syncPlayEnabled = false;
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Overrides PlaybackManager's unpause method.
|
|
|
|
*/
|
|
|
|
playRequest (player) {
|
|
|
|
var apiClient = connectionManager.currentApiClient();
|
|
|
|
var sessionId = getActivePlayerId();
|
2020-05-06 23:41:54 +02:00
|
|
|
apiClient.sendSyncPlayCommand(sessionId, 'PlayRequest');
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Overrides PlaybackManager's pause method.
|
|
|
|
*/
|
|
|
|
pauseRequest (player) {
|
|
|
|
var apiClient = connectionManager.currentApiClient();
|
|
|
|
var sessionId = getActivePlayerId();
|
2020-05-06 23:41:54 +02:00
|
|
|
apiClient.sendSyncPlayCommand(sessionId, 'PauseRequest');
|
2020-04-03 18:49:19 +02:00
|
|
|
// Pause locally as well, to give the user some little control
|
2020-05-05 12:01:43 +02:00
|
|
|
playbackManager._localUnpause(player);
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Overrides PlaybackManager's seek method.
|
|
|
|
*/
|
|
|
|
seekRequest (PositionTicks, player) {
|
|
|
|
var apiClient = connectionManager.currentApiClient();
|
|
|
|
var sessionId = getActivePlayerId();
|
2020-05-06 23:41:54 +02:00
|
|
|
apiClient.sendSyncPlayCommand(sessionId, 'SeekRequest', {
|
2020-04-03 18:49:19 +02:00
|
|
|
PositionTicks: PositionTicks
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-05-05 12:01:43 +02:00
|
|
|
/**
|
|
|
|
* Calls original PlaybackManager's unpause method.
|
|
|
|
*/
|
|
|
|
localUnpause(player) {
|
2020-05-06 23:41:54 +02:00
|
|
|
if (playbackManager.syncPlayEnabled) {
|
2020-05-05 12:01:43 +02:00
|
|
|
playbackManager._localUnpause(player);
|
|
|
|
} else {
|
|
|
|
playbackManager.unpause(player);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Calls original PlaybackManager's pause method.
|
|
|
|
*/
|
|
|
|
localPause(player) {
|
2020-05-06 23:41:54 +02:00
|
|
|
if (playbackManager.syncPlayEnabled) {
|
2020-05-05 12:01:43 +02:00
|
|
|
playbackManager._localPause(player);
|
|
|
|
} else {
|
|
|
|
playbackManager.pause(player);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Calls original PlaybackManager's seek method.
|
|
|
|
*/
|
|
|
|
localSeek(PositionTicks, player) {
|
2020-05-06 23:41:54 +02:00
|
|
|
if (playbackManager.syncPlayEnabled) {
|
2020-05-05 12:01:43 +02:00
|
|
|
playbackManager._localSeek(PositionTicks, player);
|
|
|
|
} else {
|
|
|
|
playbackManager.seek(PositionTicks, player);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-15 18:15:28 +02:00
|
|
|
/**
|
|
|
|
* Attempts to sync playback time with estimated server time.
|
2020-05-05 12:01:43 +02:00
|
|
|
*
|
2020-04-15 18:15:28 +02:00
|
|
|
* When sync is enabled, the following will be checked:
|
|
|
|
* - check if local playback time is close enough to the server playback time
|
|
|
|
* If it is not, then a playback time sync will be attempted.
|
|
|
|
* Two methods of syncing are available:
|
|
|
|
* - SpeedToSync: speeds up the media for some time to catch up (default is one second)
|
|
|
|
* - SkipToSync: seeks the media to the estimated correct time
|
|
|
|
* SpeedToSync aims to reduce the delay as much as possible, whereas SkipToSync is less pretentious.
|
|
|
|
*/
|
|
|
|
syncPlaybackTime () {
|
|
|
|
// Attempt to sync only when media is playing.
|
|
|
|
if (!this.lastCommand || this.lastCommand.Command !== 'Play' || this.isBuffering()) return;
|
|
|
|
|
|
|
|
const currentTime = new Date();
|
|
|
|
|
|
|
|
// Avoid overloading the browser
|
|
|
|
const elapsed = currentTime - this.lastSyncTime;
|
|
|
|
if (elapsed < SyncMethodThreshold / 2) return;
|
|
|
|
this.lastSyncTime = currentTime;
|
|
|
|
|
|
|
|
const playAtTime = this.lastCommand.When;
|
|
|
|
|
2020-05-05 12:01:43 +02:00
|
|
|
const currentPositionTicks = playbackManager.currentTime();
|
2020-04-15 18:15:28 +02:00
|
|
|
// Estimate PositionTicks on server
|
2020-05-05 12:01:43 +02:00
|
|
|
const serverPositionTicks = this.lastCommand.PositionTicks + ((currentTime - playAtTime) + this.timeOffsetWithServer) * 10000;
|
2020-04-15 18:15:28 +02:00
|
|
|
// Measure delay that needs to be recovered
|
|
|
|
// diff might be caused by the player internally starting the playback
|
2020-05-05 12:01:43 +02:00
|
|
|
const diffMillis = (serverPositionTicks - currentPositionTicks) / 10000.0;
|
2020-04-15 18:15:28 +02:00
|
|
|
|
|
|
|
this.playbackDiffMillis = diffMillis;
|
|
|
|
|
|
|
|
if (this.syncEnabled) {
|
|
|
|
const absDiffMillis = Math.abs(diffMillis);
|
|
|
|
// TODO: SpeedToSync sounds bad on songs
|
|
|
|
// TODO: SpeedToSync is failing on Safari (Mojave); even if playbackRate is supported, some delay seems to exist
|
|
|
|
if (this.playbackRateSupported && absDiffMillis > MaxAcceptedDelaySpeedToSync && absDiffMillis < SyncMethodThreshold) {
|
|
|
|
// Disable SpeedToSync if it keeps failing
|
|
|
|
if (this.syncAttempts > MaxAttemptsSpeedToSync) {
|
|
|
|
this.playbackRateSupported = false;
|
|
|
|
}
|
|
|
|
// SpeedToSync method
|
|
|
|
const speed = 1 + diffMillis / SpeedToSyncTime;
|
|
|
|
|
|
|
|
this.currentPlayer.setPlaybackRate(speed);
|
|
|
|
this.syncEnabled = false;
|
|
|
|
this.syncAttempts++;
|
2020-05-05 12:01:43 +02:00
|
|
|
this.showSyncIcon('SpeedToSync (x' + speed + ')');
|
2020-04-15 18:15:28 +02:00
|
|
|
|
|
|
|
this.syncTimeout = setTimeout(() => {
|
|
|
|
this.currentPlayer.setPlaybackRate(1);
|
|
|
|
this.syncEnabled = true;
|
|
|
|
this.clearSyncIcon();
|
|
|
|
}, SpeedToSyncTime);
|
|
|
|
} else if (absDiffMillis > MaxAcceptedDelaySkipToSync) {
|
|
|
|
// Disable SkipToSync if it keeps failing
|
|
|
|
if (this.syncAttempts > MaxAttemptsSync) {
|
|
|
|
this.syncEnabled = false;
|
2020-05-05 12:01:43 +02:00
|
|
|
this.showSyncIcon('Sync disabled (too many attempts)');
|
2020-04-15 18:15:28 +02:00
|
|
|
}
|
|
|
|
// SkipToSync method
|
2020-05-05 12:01:43 +02:00
|
|
|
this.localSeek(serverPositionTicks);
|
2020-04-15 18:15:28 +02:00
|
|
|
this.syncEnabled = false;
|
|
|
|
this.syncAttempts++;
|
2020-05-05 12:01:43 +02:00
|
|
|
this.showSyncIcon('SkipToSync (' + this.syncAttempts + ')');
|
2020-04-15 18:15:28 +02:00
|
|
|
|
|
|
|
this.syncTimeout = setTimeout(() => {
|
|
|
|
this.syncEnabled = true;
|
|
|
|
this.clearSyncIcon();
|
|
|
|
}, SyncMethodThreshold / 2);
|
|
|
|
} else {
|
|
|
|
// Playback is synced
|
|
|
|
if (this.syncAttempts > 0) {
|
2020-05-05 12:01:43 +02:00
|
|
|
console.debug('Playback has been synced after', this.syncAttempts, 'attempts.');
|
2020-04-15 18:15:28 +02:00
|
|
|
}
|
|
|
|
this.syncAttempts = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-03 18:49:19 +02:00
|
|
|
/**
|
2020-05-06 23:41:54 +02:00
|
|
|
* Gets SyncPlay stats.
|
|
|
|
* @returns {Object} The SyncPlay stats.
|
2020-04-03 18:49:19 +02:00
|
|
|
*/
|
|
|
|
getStats () {
|
|
|
|
return {
|
2020-04-16 16:05:04 +02:00
|
|
|
TimeOffset: this.timeOffsetWithServer,
|
2020-04-03 18:49:19 +02:00
|
|
|
PlaybackDiff: this.playbackDiffMillis,
|
|
|
|
SyncMethod: this.syncMethod
|
2020-05-05 12:01:43 +02:00
|
|
|
};
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-05-06 23:41:54 +02:00
|
|
|
* Emits an event to update the SyncPlay status icon.
|
2020-04-03 18:49:19 +02:00
|
|
|
*/
|
|
|
|
showSyncIcon (syncMethod) {
|
|
|
|
this.syncMethod = syncMethod;
|
2020-05-05 12:01:43 +02:00
|
|
|
events.trigger(this, 'syncing', [true, this.syncMethod]);
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-05-06 23:41:54 +02:00
|
|
|
* Emits an event to clear the SyncPlay status icon.
|
2020-04-03 18:49:19 +02:00
|
|
|
*/
|
|
|
|
clearSyncIcon () {
|
2020-05-05 12:01:43 +02:00
|
|
|
this.syncMethod = 'None';
|
|
|
|
events.trigger(this, 'syncing', [false, this.syncMethod]);
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
2020-04-15 18:15:28 +02:00
|
|
|
|
|
|
|
/**
|
2020-05-06 23:41:54 +02:00
|
|
|
* Signals an error state, which disables and resets SyncPlay for a new session.
|
2020-04-15 18:15:28 +02:00
|
|
|
*/
|
|
|
|
signalError () {
|
2020-05-06 23:41:54 +02:00
|
|
|
this.disableSyncPlay();
|
2020-04-15 18:15:28 +02:00
|
|
|
}
|
2020-04-03 18:49:19 +02:00
|
|
|
}
|
|
|
|
|
2020-05-06 23:41:54 +02:00
|
|
|
/** SyncPlayManager singleton. */
|
|
|
|
export default new SyncPlayManager();
|