1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Fix code issues

This commit is contained in:
gion 2020-05-05 12:01:43 +02:00
parent a2ba96ab82
commit 11f6217bb2
17 changed files with 340 additions and 242 deletions

View file

@ -520,8 +520,8 @@ define(['events', 'browser', 'require', 'apphost', 'appSettings', 'htmlMediaHelp
var list = [];
var audio = document.createElement('audio');
if (typeof audio.playbackRate === "number") {
list.push("PlaybackRate");
if (typeof audio.playbackRate === 'number') {
list.push('PlaybackRate');
}
return list;

View file

@ -1442,8 +1442,8 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa
list.push('AirPlay');
}
if (typeof video.playbackRate === "number") {
list.push("PlaybackRate");
if (typeof video.playbackRate === 'number') {
list.push('PlaybackRate');
}
list.push('SetBrightness');

View file

@ -54,6 +54,7 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla
if (!serverId) {
// Not a server item
// We can expand on this later and possibly report them
events.trigger(playbackManagerInstance, 'reportplayback', [false]);
return;
}
@ -77,7 +78,11 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla
}
var apiClient = connectionManager.getApiClient(serverId);
apiClient[method](info);
var reportPlaybackPromise = apiClient[method](info);
// Notify that report has been sent
reportPlaybackPromise.then(() => {
events.trigger(playbackManagerInstance, 'reportplayback', [true]);
});
}
function getPlaylistSync(playbackManagerInstance, player) {
@ -3777,18 +3782,14 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla
}
};
PlaybackManager.prototype.setPlaybackRate = function (value, player) {
player = player || this._currentPlayer;
if (player) {
PlaybackManager.prototype.setPlaybackRate = function (value, player = this._currentPlayer) {
if (player && player.setPlaybackRate) {
player.setPlaybackRate(value);
}
};
PlaybackManager.prototype.getPlaybackRate = function (player) {
player = player || this._currentPlayer;
if (player) {
PlaybackManager.prototype.getPlaybackRate = function (player = this._currentPlayer) {
if (player && player.getPlaybackRate) {
return player.getPlaybackRate();
}

View file

@ -332,17 +332,17 @@ define(['events', 'globalize', 'playbackManager', 'connectionManager', 'syncplay
var stats = syncplayManager.getStats();
syncStats.push({
label: globalize.translate("LabelSyncplayTimeOffset"),
value: stats.TimeOffset + "ms"
label: globalize.translate('LabelSyncplayTimeOffset'),
value: stats.TimeOffset + globalize.translate('MillisecondsUnit')
});
syncStats.push({
label: globalize.translate("LabelSyncplayPlaybackDiff"),
value: stats.PlaybackDiff + "ms"
label: globalize.translate('LabelSyncplayPlaybackDiff'),
value: stats.PlaybackDiff + globalize.translate('MillisecondsUnit')
});
syncStats.push({
label: globalize.translate("LabelSyncplaySyncMethod"),
label: globalize.translate('LabelSyncplaySyncMethod'),
value: stats.SyncMethod
});

View file

@ -187,9 +187,9 @@ define(['connectionManager', 'playbackManager', 'syncplayManager', 'events', 'in
events.trigger(serverNotifications, 'UserDataChanged', [apiClient, msg.Data.UserDataList[i]]);
}
}
} else if (msg.MessageType === "SyncplayCommand") {
} else if (msg.MessageType === 'SyncplayCommand') {
syncplayManager.processCommand(msg.Data, apiClient);
} else if (msg.MessageType === "SyncplayGroupUpdate") {
} else if (msg.MessageType === 'SyncplayGroupUpdate') {
syncplayManager.processGroupUpdate(msg.Data, apiClient);
} else {
events.trigger(serverNotifications, msg.MessageType, [apiClient, msg.Data]);

View file

@ -3,7 +3,6 @@ import connectionManager from 'connectionManager';
import playbackManager from 'playbackManager';
import syncplayManager from 'syncplayManager';
import loading from 'loading';
import datetime from 'datetime';
import toast from 'toast';
import actionsheet from 'actionsheet';
import globalize from 'globalize';
@ -18,13 +17,6 @@ function getActivePlayerId () {
return info ? info.id : null;
}
/**
* Used to avoid console logs about uncaught promises
*/
function emptyCallback () {
// avoid console logs about uncaught promises
}
/**
* Used when user needs to join a group.
* @param {HTMLElement} button - Element where to place the menu.
@ -32,47 +24,43 @@ function emptyCallback () {
* @param {Object} apiClient - ApiClient.
*/
function showNewJoinGroupSelection (button, user, apiClient) {
let sessionId = getActivePlayerId();
sessionId = sessionId ? sessionId : "none";
const inSession = sessionId !== "none";
const sessionId = getActivePlayerId() || 'none';
const inSession = sessionId !== 'none';
const policy = user.localUser ? user.localUser.Policy : {};
let playingItemId;
try {
const playState = playbackManager.getPlayerState();
playingItemId = playState.NowPlayingItem.Id;
console.debug('Item', playingItemId, 'is currently playing.');
} catch (error) {
playingItemId = "";
playingItemId = '';
console.debug('No item is currently playing.');
}
apiClient.sendSyncplayCommand(sessionId, "ListGroups").then(function (response) {
response.json().then(function (groups) {
apiClient.sendSyncplayCommand(sessionId, 'ListGroups').then(function (response) {
response.json().then(function (groups) {
var menuItems = groups.map(function (group) {
// TODO: update running time if group is playing?
var name = datetime.getDisplayRunningTime(group.PositionTicks);
if (!inSession) {
name = group.PlayingItemName;
}
return {
name: name,
icon: "group",
name: group.PlayingItemName,
icon: 'group',
id: group.GroupId,
selected: false,
secondaryText: group.Participants.join(", ")
secondaryText: group.Participants.join(', ')
};
});
if (inSession && policy.SyncplayAccess === "CreateAndJoinGroups") {
if (inSession && policy.SyncplayAccess === 'CreateAndJoinGroups') {
menuItems.push({
name: globalize.translate('LabelSyncplayNewGroup'),
icon: "add",
id: "new-group",
icon: 'add',
id: 'new-group',
selected: true,
secondaryText: globalize.translate('LabelSyncplayNewGroupDescription')
});
}
if (menuItems.length === 0) {
if (inSession && policy.SyncplayAccess === "JoinGroups") {
if (inSession && policy.SyncplayAccess === 'JoinGroups') {
toast({
text: globalize.translate('MessageSyncplayCreateGroupDenied')
});
@ -94,15 +82,17 @@ function showNewJoinGroupSelection (button, user, apiClient) {
};
actionsheet.show(menuOptions).then(function (id) {
if (id == "new-group") {
apiClient.sendSyncplayCommand(sessionId, "NewGroup");
if (id == 'new-group') {
apiClient.sendSyncplayCommand(sessionId, 'NewGroup');
} else {
apiClient.sendSyncplayCommand(sessionId, "JoinGroup", {
apiClient.sendSyncplayCommand(sessionId, 'JoinGroup', {
GroupId: id,
PlayingItemId: playingItemId
});
}
}, emptyCallback);
}).catch((error) => {
console.error('Syncplay: unexpected error listing groups:', error);
});
loading.hide();
});
@ -110,7 +100,7 @@ function showNewJoinGroupSelection (button, user, apiClient) {
console.error(error);
loading.hide();
toast({
text: globalize.translate('MessageSyncplayNoGroupsAvailable')
text: globalize.translate('MessageSyncplayErrorAccessingGroups')
});
});
}
@ -126,17 +116,16 @@ function showLeaveGroupSelection (button, user, apiClient) {
if (!sessionId) {
syncplayManager.signalError();
toast({
// TODO: translate
text: "Syncplay error occured."
text: globalize.translate('MessageSyncplayErrorNoActivePlayer')
});
showNewJoinGroupSelection(button, user, apiClient);
return;
}
const menuItems = [{
name: globalize.translate('LabelSyncplayLeaveGroup'),
icon: "meeting_room",
id: "leave-group",
icon: 'meeting_room',
id: 'leave-group',
selected: true,
secondaryText: globalize.translate('LabelSyncplayLeaveGroupDescription')
}];
@ -150,17 +139,19 @@ function showLeaveGroupSelection (button, user, apiClient) {
};
actionsheet.show(menuOptions).then(function (id) {
if (id == "leave-group") {
apiClient.sendSyncplayCommand(sessionId, "LeaveGroup");
if (id == 'leave-group') {
apiClient.sendSyncplayCommand(sessionId, 'LeaveGroup');
}
}, emptyCallback);
}).catch((error) => {
console.error('Syncplay: unexpected error showing group menu:', error);
});
loading.hide();
}
// Register to Syncplay events
let syncplayEnabled = false;
events.on(syncplayManager, 'SyncplayEnabled', function (e, enabled) {
events.on(syncplayManager, 'enabled', function (e, enabled) {
syncplayEnabled = enabled;
});
@ -173,11 +164,11 @@ export function show (button) {
// TODO: should feature be disabled if playback permission is missing?
playbackPermissionManager.check().then(() => {
console.debug("Playback is allowed.");
console.debug('Playback is allowed.');
}).catch((error) => {
console.error("Playback not allowed!", error);
console.error('Playback not allowed!', error);
toast({
text: globalize.translate("MessageSyncplayPlaybackPermissionRequired")
text: globalize.translate('MessageSyncplayPlaybackPermissionRequired')
});
});

View file

@ -11,7 +11,7 @@ function createTestMediaElement () {
document.body.appendChild(elem);
elem.volume = 1; // Volume should not be zero to trigger proper permissions
elem.src = "assets/audio/silence.wav"; // Silent sound
elem.src = 'assets/audio/silence.mp3'; // Silent sound
return elem;
}
@ -30,7 +30,7 @@ function destroyTestMediaElement (elem) {
*/
class PlaybackPermissionManager {
/**
* Tests playback permission. Grabs the permission when called inside a click event (or any other valid user interaction).
* Tests playback permission. Grabs the permission when called inside a click event (or any other valid user interaction).
* @returns {Promise} Promise that resolves succesfully if playback permission is allowed.
*/
check () {

View file

@ -1,5 +1,3 @@
/* eslint-disable indent */
/**
* Module that manages the Syncplay feature.
* @module components/syncplay/syncplayManager
@ -13,15 +11,25 @@ import toast from 'toast';
import globalize from 'globalize';
/**
* Waits for an event to be triggered on an object.
* Waits for an event to be triggered on an object. An optional timeout can specified after which the promise is rejected.
* @param {Object} emitter Object on which to listen for events.
* @param {string} eventType Event name to listen for.
* @param {number} timeout Time in milliseconds before rejecting promise if event does not trigger.
* @returns {Promise} A promise that resolves when the event is triggered.
*/
function waitForEvent(emitter, eventType) {
return new Promise((resolve) => {
var callback = () => {
function waitForEventOnce(emitter, eventType, timeout) {
return new Promise((resolve, reject) => {
let rejectTimeout;
if (timeout) {
rejectTimeout = setTimeout(() => {
reject('Timed out.');
}, timeout);
}
const callback = () => {
events.off(emitter, eventType, callback);
if (rejectTimeout) {
clearTimeout(rejectTimeout);
}
resolve(arguments);
};
events.on(emitter, eventType, callback);
@ -33,7 +41,7 @@ function waitForEvent(emitter, eventType) {
* @returns {string} The player's id.
*/
function getActivePlayerId() {
var info = playbackManager.getPlayerInfo();
var info = playbackManager.getPlayerInfo();
return info ? info.id : null;
}
@ -48,11 +56,10 @@ const MaxAttemptsSpeedToSync = 3; // attempts before disabling SpeedToSync
const MaxAttemptsSync = 5; // attempts before disabling syncing at all
/**
* Time estimation
* Other constants
*/
const PingIntervalTimeoutGreedy = 1000; // milliseconds
const PingIntervalTimeoutLowProfile = 60000; // milliseconds
const GreedyPingCount = 3;
const WaitForEventDefaultTimeout = 30000; // milliseconds
const WaitForPlayerEventTimeout = 500; // milliseconds
/**
* Class that manages the Syncplay feature.
@ -62,7 +69,7 @@ class SyncplayManager {
this.playbackRateSupported = false;
this.syncEnabled = false;
this.playbackDiffMillis = 0; // used for stats
this.syncMethod = "None"; // used for stats
this.syncMethod = 'None'; // used for stats
this.syncAttempts = 0;
this.lastSyncTime = new Date();
this.syncWatcherTimeout = null; // interval that watches playback time and syncs it
@ -71,6 +78,7 @@ class SyncplayManager {
this.minBufferingThresholdMillis = 1000;
this.currentPlayer = null;
this.localPlayerPlaybackRate = 1.0; // used to restore user PlaybackRate
this.syncplayEnabledAt = null; // Server time of when Syncplay has been enabled
this.syncplayReady = false; // Syncplay is ready after first ping to server
@ -84,24 +92,37 @@ class SyncplayManager {
this.timeOffsetWithServer = 0; // server time minus local time
this.roundTripDuration = 0;
this.notifySyncplayReady = false;
events.on(playbackManager, "playerchange", () => {
events.on(playbackManager, 'playbackstart', (player, state) => {
this.onPlaybackStart(player, state);
});
events.on(playbackManager, 'playbackstop', (stopInfo) => {
this.onPlaybackStop(stopInfo);
});
events.on(playbackManager, 'playerchange', () => {
this.onPlayerChange();
});
this.bindToPlayer(playbackManager.getCurrentPlayer());
events.on(this, "TimeUpdate", (event) => {
events.on(this, 'timeupdate', (event) => {
this.syncPlaybackTime();
});
events.on(timeSyncManager, "Update", (event, timeOffset, ping) => {
events.on(timeSyncManager, 'update', (event, error, timeOffset, ping) => {
if (error) {
console.debug('Syncplay, time update issue', error);
return;
}
this.timeOffsetWithServer = timeOffset;
this.roundTripDuration = ping * 2;
if (this.notifySyncplayReady) {
this.syncplayReady = true;
events.trigger(this, "SyncplayReady");
events.trigger(this, 'ready');
this.notifySyncplayReady = false;
}
@ -113,33 +134,55 @@ class SyncplayManager {
if (!sessionId) {
this.signalError();
toast({
// TODO: translate
text: "Syncplay error occured."
text: globalize.translate('MessageSyncplayErrorMissingSession')
});
return;
}
apiClient.sendSyncplayCommand(sessionId, "UpdatePing", {
apiClient.sendSyncplayCommand(sessionId, 'UpdatePing', {
Ping: ping
});
}
});
}
/**
* Called when playback starts.
*/
onPlaybackStart (player, state) {
events.trigger(this, 'playbackstart', [player, state]);
}
/**
* Called when playback stops.
*/
onPlaybackStop (stopInfo) {
events.trigger(this, 'playbackstop', [stopInfo]);
if (this.isSyncplayEnabled()) {
this.disableSyncplay(false);
}
}
/**
* Called when the player changes.
*/
onPlayerChange () {
this.bindToPlayer(playbackManager.getCurrentPlayer());
events.trigger(this, "PlayerChange", [this.currentPlayer]);
events.trigger(this, 'playerchange', [this.currentPlayer]);
}
/**
* Called on playback state changes.
* @param {Object} e The playback state change event.
* Called when playback unpauses.
*/
onPlayPauseStateChanged (e) {
events.trigger(this, "PlayPauseStateChange", [this.currentPlayer]);
onPlayerUnpause () {
events.trigger(this, 'unpause', [this.currentPlayer]);
}
/**
* Called when playback pauses.
*/
onPlayerPause() {
events.trigger(this, 'pause', [this.currentPlayer]);
}
/**
@ -149,7 +192,7 @@ class SyncplayManager {
onTimeUpdate (e) {
// NOTICE: this event is unreliable, at least in Safari
// which just stops firing the event after a while.
events.trigger(this, "TimeUpdate", [e]);
events.trigger(this, 'timeupdate', [e]);
}
/**
@ -158,7 +201,7 @@ class SyncplayManager {
onPlaying () {
// TODO: implement group wait
this.lastPlaybackWaiting = null;
events.trigger(this, "PlayerPlaying");
events.trigger(this, 'playing');
}
/**
@ -169,7 +212,7 @@ class SyncplayManager {
if (!this.lastPlaybackWaiting) {
this.lastPlaybackWaiting = new Date();
}
events.trigger(this, "PlayerWaiting");
events.trigger(this, 'waiting');
}
/**
@ -191,15 +234,18 @@ class SyncplayManager {
this.currentPlayer = player;
if (!player) return;
}
// TODO: remove this extra functions
// 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'
// points to 'player' (the event emitter) instead of pointing to the SyncplayManager singleton.
const self = this;
this._onPlayPauseStateChanged = () => {
self.onPlayPauseStateChanged();
this._onPlayerUnpause = () => {
self.onPlayerUnpause();
};
this._onPlayPauseStateChanged = (e) => {
self.onPlayPauseStateChanged(e);
this._onPlayerPause = () => {
self.onPlayerPause();
};
this._onTimeUpdate = (e) => {
@ -214,12 +260,17 @@ class SyncplayManager {
self.onWaiting();
};
events.on(player, "pause", this._onPlayPauseStateChanged);
events.on(player, "unpause", this._onPlayPauseStateChanged);
events.on(player, "timeupdate", this._onTimeUpdate);
events.on(player, "playing", this._onPlaying);
events.on(player, "waiting", this._onWaiting);
this.playbackRateSupported = player.supports("PlaybackRate");
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();
}
}
/**
@ -228,13 +279,15 @@ class SyncplayManager {
releaseCurrentPlayer () {
var player = this.currentPlayer;
if (player) {
events.off(player, "pause", this._onPlayPauseStateChanged);
events.off(player, "unpause", this._onPlayPauseStateChanged);
events.off(player, "timeupdate", this._onTimeUpdate);
events.off(player, "playing", this._onPlaying);
events.off(player, "waiting", this._onWaiting);
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
if (this.playbackRateSupported) {
player.setPlaybackRate(1);
player.setPlaybackRate(this.localPlayerPlaybackRate);
this.localPlayerPlaybackRate = 1.0;
}
this.currentPlayer = null;
this.playbackRateSupported = false;
@ -262,8 +315,7 @@ class SyncplayManager {
});
break;
case 'GroupJoined':
const enabledAt = new Date(cmd.Data);
this.enableSyncplay(apiClient, enabledAt, true);
this.enableSyncplay(apiClient, new Date(cmd.Data), true);
break;
case 'NotInGroup':
case 'GroupLeft':
@ -274,28 +326,28 @@ class SyncplayManager {
text: globalize.translate('MessageSyncplayGroupWait', cmd.Data)
});
break;
case 'GroupNotJoined':
case 'GroupDoesNotExist':
toast({
text: globalize.translate('MessageSyncplayGroupNotJoined', cmd.Data)
text: globalize.translate('MessageSyncplayGroupDoesNotExist')
});
break;
case 'CreateGroupDenied':
toast({
text: globalize.translate('MessageSyncplayCreateGroupDenied', cmd.Data)
text: globalize.translate('MessageSyncplayCreateGroupDenied')
});
break;
case 'JoinGroupDenied':
toast({
text: globalize.translate('MessageSyncplayJoinGroupDenied', cmd.Data)
text: globalize.translate('MessageSyncplayJoinGroupDenied')
});
break;
case 'LibraryAccessDenied':
toast({
text: globalize.translate('MessageSyncplayLibraryAccessDenied', cmd.Data)
text: globalize.translate('MessageSyncplayLibraryAccessDenied')
});
break;
default:
console.error('processSyncplayGroupUpdate does not recognize: ' + cmd.Type);
console.error('processSyncplayGroupUpdate: command is not recognised: ' + cmd.Type);
break;
}
}
@ -309,12 +361,12 @@ class SyncplayManager {
if (cmd === null) return;
if (!this.isSyncplayEnabled()) {
console.debug("Syncplay processCommand: ignoring command", cmd);
console.debug('Syncplay processCommand: SyncPlay not enabled, ignoring command', cmd);
return;
}
if (!this.syncplayReady) {
console.debug("Syncplay processCommand: queued command", cmd);
console.debug('Syncplay processCommand: SyncPlay not ready, queued command', cmd);
this.queuedCommand = cmd;
return;
}
@ -323,7 +375,7 @@ class SyncplayManager {
cmd.EmittedAt = new Date(cmd.EmitttedAt);
if (cmd.EmitttedAt < this.syncplayEnabledAt) {
console.debug("Syncplay processCommand: ignoring old command", cmd);
console.debug('Syncplay processCommand: ignoring old command', cmd);
return;
}
@ -333,12 +385,12 @@ class SyncplayManager {
this.lastCommand.PositionTicks === cmd.PositionTicks &&
this.Command === cmd.Command
) {
console.debug("Syncplay processCommand: ignoring duplicate command", cmd);
console.debug('Syncplay processCommand: ignoring duplicate command', cmd);
return;
}
this.lastCommand = cmd;
console.log("Syncplay will", cmd.Command, "at", cmd.When, "PositionTicks", cmd.PositionTicks);
console.log('Syncplay will', cmd.Command, 'at', cmd.When, 'PositionTicks', cmd.PositionTicks);
switch (cmd.Command) {
case 'Play':
@ -351,7 +403,7 @@ class SyncplayManager {
this.scheduleSeek(cmd.When, cmd.PositionTicks);
break;
default:
console.error('processSyncplayCommand does not recognize: ' + cmd.Type);
console.error('processCommand: command is not recognised: ' + cmd.Type);
break;
}
}
@ -363,7 +415,7 @@ class SyncplayManager {
* @param {Object} sessionData Info about the content to load.
*/
prepareSession (apiClient, groupId, sessionData) {
var serverId = apiClient.serverInfo().Id;
const serverId = apiClient.serverInfo().Id;
playbackManager.play({
ids: sessionData.ItemIds,
startPositionTicks: sessionData.StartPositionTicks,
@ -373,14 +425,12 @@ class SyncplayManager {
startIndex: sessionData.StartIndex,
serverId: serverId
}).then(() => {
waitForEvent(this, "PlayerChange").then(() => {
playbackManager.pause();
waitForEventOnce(this, 'playbackstart', WaitForEventDefaultTimeout).then(() => {
var sessionId = getActivePlayerId();
if (!sessionId) {
console.error("Missing sessionId!");
console.error('Missing sessionId!');
toast({
// TODO: translate
text: "Failed to enable Syncplay! Missing session id."
text: globalize.translate('MessageSyncplayErrorMissingSession')
});
return;
}
@ -390,21 +440,38 @@ class SyncplayManager {
const playState = playbackManager.getPlayerState();
playingItemId = playState.NowPlayingItem.Id;
} catch (error) {
playingItemId = "";
playingItemId = '';
}
// Sometimes JoinGroup fails, maybe because server hasn't been updated yet
setTimeout(() => {
apiClient.sendSyncplayCommand(sessionId, "JoinGroup", {
// 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.');
}
apiClient.sendSyncplayCommand(sessionId, 'JoinGroup', {
GroupId: groupId,
PlayingItemId: playingItemId
});
}, 500);
}).catch(() => {
console.error('Timed out while waiting for `reportplayback` event!');
toast({
text: globalize.translate('MessageSyncplayErrorMedia')
});
return;
});
}).catch(() => {
console.error('Timed out while waiting for `playbackstart` event!');
if (!this.isSyncplayEnabled()) {
toast({
text: globalize.translate('MessageSyncplayErrorMedia')
});
}
return;
});
}).catch((error) => {
console.error(error);
toast({
// TODO: translate
text: "Failed to enable Syncplay! Media error."
text: globalize.translate('MessageSyncplayErrorMedia')
});
});
}
@ -418,12 +485,13 @@ class SyncplayManager {
enableSyncplay (apiClient, enabledAt, showMessage = false) {
this.syncplayEnabledAt = enabledAt;
this.injectPlaybackManager();
events.trigger(this, "SyncplayEnabled", [true]);
events.trigger(this, 'enabled', [true]);
waitForEvent(this, "SyncplayReady").then(() => {
waitForEventOnce(this, 'ready').then(() => {
this.processCommand(this.queuedCommand, apiClient);
this.queuedCommand = null;
});
this.syncplayReady = false;
this.notifySyncplayReady = true;
@ -446,7 +514,7 @@ class SyncplayManager {
this.lastCommand = null;
this.queuedCommand = null;
this.syncEnabled = false;
events.trigger(this, "SyncplayEnabled", [false]);
events.trigger(this, 'enabled', [false]);
this.restorePlaybackManager();
if (showMessage) {
@ -461,7 +529,7 @@ class SyncplayManager {
* @returns {boolean} _true_ if user joined a group, _false_ otherwise.
*/
isSyncplayEnabled () {
return this.syncplayEnabledAt !== null ? true : false;
return this.syncplayEnabledAt !== null;
}
/**
@ -471,15 +539,15 @@ class SyncplayManager {
*/
schedulePlay (playAtTime, positionTicks) {
this.clearScheduledCommand();
var currentTime = new Date();
var playAtTimeLocal = timeSyncManager.serverDateToLocal(playAtTime);
const currentTime = new Date();
const playAtTimeLocal = timeSyncManager.serverDateToLocal(playAtTime);
if (playAtTimeLocal > currentTime) {
var playTimeout = playAtTimeLocal - currentTime;
playbackManager.syncplay_seek(positionTicks);
const playTimeout = playAtTimeLocal - currentTime;
this.localSeek(positionTicks);
this.scheduledCommand = setTimeout(() => {
playbackManager.syncplay_unpause();
this.localUnpause();
this.syncTimeout = setTimeout(() => {
this.syncEnabled = true;
@ -487,12 +555,14 @@ class SyncplayManager {
}, playTimeout);
// console.debug("Syncplay schedulePlay:", playTimeout);
console.debug('Scheduled play in', playTimeout / 1000.0, 'seconds.');
} else {
// Group playback already started
var serverPositionTicks = positionTicks + (currentTime - playAtTimeLocal) * 10000;
playbackManager.syncplay_unpause();
playbackManager.syncplay_seek(serverPositionTicks);
const serverPositionTicks = positionTicks + (currentTime - playAtTimeLocal) * 10000;
waitForEventOnce(this, 'unpause').then(() => {
this.localSeek(serverPositionTicks);
});
this.localUnpause();
this.syncTimeout = setTimeout(() => {
this.syncEnabled = true;
@ -507,24 +577,26 @@ class SyncplayManager {
*/
schedulePause (pauseAtTime, positionTicks) {
this.clearScheduledCommand();
var currentTime = new Date();
var pauseAtTimeLocal = timeSyncManager.serverDateToLocal(pauseAtTime);
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();
};
if (pauseAtTimeLocal > currentTime) {
var pauseTimeout = pauseAtTimeLocal - currentTime;
const pauseTimeout = pauseAtTimeLocal - currentTime;
this.scheduledCommand = setTimeout(callback, pauseTimeout);
this.scheduledCommand = setTimeout(() => {
playbackManager.syncplay_pause();
setTimeout(() => {
playbackManager.syncplay_seek(positionTicks);
}, 800);
}, pauseTimeout);
console.debug('Scheduled pause in', pauseTimeout / 1000.0, 'seconds.');
} else {
playbackManager.syncplay_pause();
setTimeout(() => {
playbackManager.syncplay_seek(positionTicks);
}, 800);
callback();
}
}
@ -558,10 +630,10 @@ class SyncplayManager {
if (!this.isSyncplayEnabled()) return;
if (playbackManager.syncplayEnabled) return;
// TODO: make this less hacky
playbackManager.syncplay_unpause = playbackManager.unpause;
playbackManager.syncplay_pause = playbackManager.pause;
playbackManager.syncplay_seek = playbackManager.seek;
// TODO: make this less hacky
playbackManager._localUnpause = playbackManager.unpause;
playbackManager._localPause = playbackManager.pause;
playbackManager._localSeek = playbackManager.seek;
playbackManager.unpause = this.playRequest;
playbackManager.pause = this.pauseRequest;
@ -576,9 +648,9 @@ class SyncplayManager {
if (this.isSyncplayEnabled()) return;
if (!playbackManager.syncplayEnabled) return;
playbackManager.unpause = playbackManager.syncplay_unpause;
playbackManager.pause = playbackManager.syncplay_pause;
playbackManager.seek = playbackManager.syncplay_seek;
playbackManager.unpause = playbackManager._localUnpause;
playbackManager.pause = playbackManager._localPause;
playbackManager.seek = playbackManager._localSeek;
playbackManager.syncplayEnabled = false;
}
@ -588,7 +660,7 @@ class SyncplayManager {
playRequest (player) {
var apiClient = connectionManager.currentApiClient();
var sessionId = getActivePlayerId();
apiClient.sendSyncplayCommand(sessionId, "PlayRequest");
apiClient.sendSyncplayCommand(sessionId, 'PlayRequest');
}
/**
@ -597,9 +669,9 @@ class SyncplayManager {
pauseRequest (player) {
var apiClient = connectionManager.currentApiClient();
var sessionId = getActivePlayerId();
apiClient.sendSyncplayCommand(sessionId, "PauseRequest");
apiClient.sendSyncplayCommand(sessionId, 'PauseRequest');
// Pause locally as well, to give the user some little control
playbackManager.syncplay_pause();
playbackManager._localUnpause(player);
}
/**
@ -608,14 +680,47 @@ class SyncplayManager {
seekRequest (PositionTicks, player) {
var apiClient = connectionManager.currentApiClient();
var sessionId = getActivePlayerId();
apiClient.sendSyncplayCommand(sessionId, "SeekRequest", {
apiClient.sendSyncplayCommand(sessionId, 'SeekRequest', {
PositionTicks: PositionTicks
});
}
/**
* Calls original PlaybackManager's unpause method.
*/
localUnpause(player) {
if (playbackManager.syncplayEnabled) {
playbackManager._localUnpause(player);
} else {
playbackManager.unpause(player);
}
}
/**
* Calls original PlaybackManager's pause method.
*/
localPause(player) {
if (playbackManager.syncplayEnabled) {
playbackManager._localPause(player);
} else {
playbackManager.pause(player);
}
}
/**
* Calls original PlaybackManager's seek method.
*/
localSeek(PositionTicks, player) {
if (playbackManager.syncplayEnabled) {
playbackManager._localSeek(PositionTicks, player);
} else {
playbackManager.seek(PositionTicks, player);
}
}
/**
* Attempts to sync playback time with estimated server time.
*
*
* 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.
@ -637,18 +742,15 @@ class SyncplayManager {
const playAtTime = this.lastCommand.When;
const CurrentPositionTicks = playbackManager.currentTime();
const currentPositionTicks = playbackManager.currentTime();
// Estimate PositionTicks on server
const ServerPositionTicks = this.lastCommand.PositionTicks + ((currentTime - playAtTime) + this.timeOffsetWithServer) * 10000;
const serverPositionTicks = this.lastCommand.PositionTicks + ((currentTime - playAtTime) + this.timeOffsetWithServer) * 10000;
// Measure delay that needs to be recovered
// diff might be caused by the player internally starting the playback
const diff = ServerPositionTicks - CurrentPositionTicks;
const diffMillis = diff / 10000;
const diffMillis = (serverPositionTicks - currentPositionTicks) / 10000.0;
this.playbackDiffMillis = diffMillis;
// console.debug("Syncplay onTimeUpdate", diffMillis, CurrentPositionTicks, ServerPositionTicks);
if (this.syncEnabled) {
const absDiffMillis = Math.abs(diffMillis);
// TODO: SpeedToSync sounds bad on songs
@ -664,7 +766,7 @@ class SyncplayManager {
this.currentPlayer.setPlaybackRate(speed);
this.syncEnabled = false;
this.syncAttempts++;
this.showSyncIcon("SpeedToSync (x" + speed + ")");
this.showSyncIcon('SpeedToSync (x' + speed + ')');
this.syncTimeout = setTimeout(() => {
this.currentPlayer.setPlaybackRate(1);
@ -675,13 +777,13 @@ class SyncplayManager {
// Disable SkipToSync if it keeps failing
if (this.syncAttempts > MaxAttemptsSync) {
this.syncEnabled = false;
this.showSyncIcon("Sync disabled (too many attempts)");
this.showSyncIcon('Sync disabled (too many attempts)');
}
// SkipToSync method
playbackManager.syncplay_seek(ServerPositionTicks);
this.localSeek(serverPositionTicks);
this.syncEnabled = false;
this.syncAttempts++;
this.showSyncIcon("SkipToSync (" + this.syncAttempts + ")");
this.showSyncIcon('SkipToSync (' + this.syncAttempts + ')');
this.syncTimeout = setTimeout(() => {
this.syncEnabled = true;
@ -690,7 +792,7 @@ class SyncplayManager {
} else {
// Playback is synced
if (this.syncAttempts > 0) {
// console.debug("Playback has been synced after", this.syncAttempts, "attempts.");
console.debug('Playback has been synced after', this.syncAttempts, 'attempts.');
}
this.syncAttempts = 0;
}
@ -706,7 +808,7 @@ class SyncplayManager {
TimeOffset: this.timeOffsetWithServer,
PlaybackDiff: this.playbackDiffMillis,
SyncMethod: this.syncMethod
}
};
}
/**
@ -714,15 +816,15 @@ class SyncplayManager {
*/
showSyncIcon (syncMethod) {
this.syncMethod = syncMethod;
events.trigger(this, "SyncplayError", [true]);
events.trigger(this, 'syncing', [true, this.syncMethod]);
}
/**
* Emits an event to clear the Syncplay status icon.
*/
clearSyncIcon () {
this.syncMethod = "None";
events.trigger(this, "SyncplayError", [false]);
this.syncMethod = 'None';
events.trigger(this, 'syncing', [false, this.syncMethod]);
}
/**

View file

@ -1,5 +1,3 @@
/* eslint-disable indent */
/**
* Module that manages time syncing with server.
* @module components/syncplay/timeSyncManager
@ -22,30 +20,30 @@ const GreedyPingCount = 3;
class Measurement {
/**
* Creates a new measurement.
* @param {Date} t0 Client's timestamp of the request transmission
* @param {Date} t1 Server's timestamp of the request reception
* @param {Date} t2 Server's timestamp of the response transmission
* @param {Date} t3 Client's timestamp of the response reception
* @param {Date} requestSent Client's timestamp of the request transmission
* @param {Date} requestReceived Server's timestamp of the request reception
* @param {Date} responseSent Server's timestamp of the response transmission
* @param {Date} responseReceived Client's timestamp of the response reception
*/
constructor(t0, t1, t2, t3) {
this.t0 = t0.getTime();
this.t1 = t1.getTime();
this.t2 = t2.getTime();
this.t3 = t3.getTime();
constructor(requestSent, requestReceived, responseSent, responseReceived) {
this.requestSent = requestSent.getTime();
this.requestReceived = requestReceived.getTime();
this.responseSent = responseSent.getTime();
this.responseReceived = responseReceived.getTime();
}
/**
* Time offset from server.
*/
getOffset () {
return ((this.t1 - this.t0) + (this.t2 - this.t3)) / 2;
return ((this.requestReceived - this.requestSent) + (this.responseSent - this.responseReceived)) / 2;
}
/**
* Get round-trip delay.
*/
getDelay () {
return (this.t3 - this.t0) - (this.t2 - this.t1);
return (this.responseReceived - this.requestSent) - (this.responseSent - this.requestReceived);
}
/**
@ -76,7 +74,7 @@ class TimeSyncManager {
* @returns {boolean} _true_ if a measurement has been done, _false_ otherwise.
*/
isReady() {
return this.measurement ? true : false;
return !!this.measurement;
}
/**
@ -119,14 +117,14 @@ class TimeSyncManager {
this.poller = setTimeout(() => {
this.poller = null;
const apiClient = connectionManager.currentApiClient();
const t0 = new Date(); // pingStartTime
const requestSent = new Date();
apiClient.getServerTime().then((response) => {
const t3 = new Date(); // pingEndTime
const responseReceived = new Date();
response.json().then((data) => {
const t1 = new Date(data.RequestReceptionTime); // request received
const t2 = new Date(data.ResponseTransmissionTime); // response sent
const requestReceived = new Date(data.RequestReceptionTime);
const responseSent = new Date(data.ResponseTransmissionTime);
const measurement = new Measurement(t0, t1, t2, t3);
const measurement = new Measurement(requestSent, requestReceived, responseSent, responseReceived);
this.updateTimeOffset(measurement);
// Avoid overloading server
@ -136,11 +134,11 @@ class TimeSyncManager {
this.pings++;
}
events.trigger(this, "Update", [this.getTimeOffset(), this.getPing()]);
events.trigger(this, 'update', [null, this.getTimeOffset(), this.getPing()]);
});
}).catch((error) => {
console.error(error);
events.trigger(this, "Error", [error]);
events.trigger(this, 'update', [error, null, null]);
}).finally(() => {
this.requestPing();
});