diff --git a/gulpfile.js b/gulpfile.js
index ad77d9a67..538497d4d 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -45,7 +45,7 @@ const options = {
query: ['src/**/*.png', 'src/**/*.jpg', 'src/**/*.gif', 'src/**/*.svg']
},
copy: {
- query: ['src/**/*.json', 'src/**/*.ico', 'src/**/*.wav']
+ query: ['src/**/*.json', 'src/**/*.ico', 'src/**/*.mp3']
},
injectBundle: {
query: 'src/index.html'
diff --git a/src/assets/audio/silence.mp3 b/src/assets/audio/silence.mp3
new file mode 100644
index 000000000..29dbef218
Binary files /dev/null and b/src/assets/audio/silence.mp3 differ
diff --git a/src/assets/audio/silence.wav b/src/assets/audio/silence.wav
deleted file mode 100644
index 63f253da8..000000000
Binary files a/src/assets/audio/silence.wav and /dev/null differ
diff --git a/src/components/htmlaudioplayer/plugin.js b/src/components/htmlaudioplayer/plugin.js
index 258048105..672bd06b8 100644
--- a/src/components/htmlaudioplayer/plugin.js
+++ b/src/components/htmlaudioplayer/plugin.js
@@ -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;
diff --git a/src/components/htmlvideoplayer/plugin.js b/src/components/htmlvideoplayer/plugin.js
index 064e4155e..60f39c5ec 100644
--- a/src/components/htmlvideoplayer/plugin.js
+++ b/src/components/htmlvideoplayer/plugin.js
@@ -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');
diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js
index e4ce40cf4..ec0ee4140 100644
--- a/src/components/playback/playbackmanager.js
+++ b/src/components/playback/playbackmanager.js
@@ -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();
}
diff --git a/src/components/playerstats/playerstats.js b/src/components/playerstats/playerstats.js
index 404baab7e..07fcd7070 100644
--- a/src/components/playerstats/playerstats.js
+++ b/src/components/playerstats/playerstats.js
@@ -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
});
diff --git a/src/components/serverNotifications.js b/src/components/serverNotifications.js
index 9776c88bd..876c3f7e7 100644
--- a/src/components/serverNotifications.js
+++ b/src/components/serverNotifications.js
@@ -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]);
diff --git a/src/components/syncplay/groupSelectionMenu.js b/src/components/syncplay/groupSelectionMenu.js
index be54d9221..af08f9277 100644
--- a/src/components/syncplay/groupSelectionMenu.js
+++ b/src/components/syncplay/groupSelectionMenu.js
@@ -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')
});
});
diff --git a/src/components/syncplay/playbackPermissionManager.js b/src/components/syncplay/playbackPermissionManager.js
index df16545b3..3c258ad18 100644
--- a/src/components/syncplay/playbackPermissionManager.js
+++ b/src/components/syncplay/playbackPermissionManager.js
@@ -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 () {
diff --git a/src/components/syncplay/syncplayManager.js b/src/components/syncplay/syncplayManager.js
index 0de232108..b5694c88f 100644
--- a/src/components/syncplay/syncplayManager.js
+++ b/src/components/syncplay/syncplayManager.js
@@ -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]);
}
/**
diff --git a/src/components/syncplay/timeSyncManager.js b/src/components/syncplay/timeSyncManager.js
index 74c98820c..ca9293957 100644
--- a/src/components/syncplay/timeSyncManager.js
+++ b/src/components/syncplay/timeSyncManager.js
@@ -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();
});
diff --git a/src/scripts/librarymenu.js b/src/scripts/librarymenu.js
index 1daa200b8..3113031ea 100644
--- a/src/scripts/librarymenu.js
+++ b/src/scripts/librarymenu.js
@@ -1,4 +1,4 @@
-define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', 'viewManager', 'libraryBrowser', 'appRouter', 'apphost', 'playbackManager', 'syncplayManager', 'browser', 'globalize', 'scripts/imagehelper', 'paper-icon-button-light', 'material-icons', 'scrollStyles', 'flexStyles'], function (dom, layoutManager, inputManager, connectionManager, events, viewManager, libraryBrowser, appRouter, appHost, playbackManager, syncplayManager, browser, globalize, imageHelper) {
+define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', 'viewManager', 'libraryBrowser', 'appRouter', 'apphost', 'playbackManager', 'syncplayManager', 'groupSelectionMenu', 'browser', 'globalize', 'scripts/imagehelper', 'paper-icon-button-light', 'material-icons', 'scrollStyles', 'flexStyles'], function (dom, layoutManager, inputManager, connectionManager, events, viewManager, libraryBrowser, appRouter, appHost, playbackManager, syncplayManager, groupSelectionMenu, browser, globalize, imageHelper) {
'use strict';
function renderHeader() {
@@ -89,12 +89,13 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', '
var policy = user.Policy ? user.Policy : user.localUser.Policy;
- if (headerSyncButton && policy && policy.SyncplayAccess !== "None") {
- headerSyncButton.classList.remove("hide");
+ if (headerSyncButton && policy && policy.SyncplayAccess !== 'None') {
+ headerSyncButton.classList.remove('hide');
}
} else {
headerHomeButton.classList.add('hide');
headerCastButton.classList.add('hide');
+ headerSyncButton.classList.add('hide');
if (headerSearchButton) {
headerSearchButton.classList.add('hide');
@@ -188,27 +189,26 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', '
function onSyncButtonClicked() {
var btn = this;
-
- require(["groupSelectionMenu"], function (groupSelectionMenu) {
- groupSelectionMenu.show(btn);
- });
+ groupSelectionMenu.show(btn);
}
- function updateSyncplayIcon(event, enabled) {
- var icon = headerSyncButton.querySelector("i");
+ function onSyncplayEnabled(event, enabled) {
+ var icon = headerSyncButton.querySelector('span');
+ icon.classList.remove('sync', 'sync_disabled', 'sync_problem');
if (enabled) {
- icon.innerHTML = "sync";
+ icon.classList.add('sync');
} else {
- icon.innerHTML = "sync_disabled";
+ icon.classList.add('sync_disabled');
}
}
- function updateSyncplayErrorIcon(event, show_error) {
- var icon = headerSyncButton.querySelector("i");
- if (show_error) {
- icon.innerHTML = "sync_problem";
+ function onSyncplaySyncing(event, is_syncing, syncMethod) {
+ var icon = headerSyncButton.querySelector('span');
+ icon.classList.remove('sync', 'sync_disabled', 'sync_problem');
+ if (is_syncing) {
+ icon.classList.add('sync_problem');
} else {
- icon.innerHTML = "sync";
+ icon.classList.add('sync');
}
}
@@ -967,8 +967,8 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', '
updateUserInHeader();
});
events.on(playbackManager, 'playerchange', updateCastIcon);
- events.on(syncplayManager, 'SyncplayEnabled', updateSyncplayIcon);
- events.on(syncplayManager, 'SyncplayError', updateSyncplayErrorIcon);
+ events.on(syncplayManager, 'enabled', onSyncplayEnabled);
+ events.on(syncplayManager, 'syncing', onSyncplaySyncing);
loadNavDrawer();
return LibraryMenu;
});
diff --git a/src/scripts/site.js b/src/scripts/site.js
index ecfeb2734..a07062ab3 100644
--- a/src/scripts/site.js
+++ b/src/scripts/site.js
@@ -316,7 +316,7 @@ var AppInfo = {};
function returnDefault(obj) {
if (obj.default === null) {
- throw new Error("Object has no default!");
+ throw new Error('Object has no default!');
}
return obj.default;
}
@@ -825,7 +825,8 @@ var AppInfo = {};
define('homescreenSettings', [componentsPath + '/homescreensettings/homescreensettings'], returnFirstDependency);
define('playbackManager', [componentsPath + '/playback/playbackmanager'], getPlaybackManager);
define('timeSyncManager', [componentsPath + '/syncplay/timeSyncManager'], returnDefault);
- define('syncplayManager', [componentsPath + '/syncplay/syncplaymanager'], returnDefault);
+ define('groupSelectionMenu', [componentsPath + '/syncplay/groupSelectionMenu'], returnFirstDependency);
+ define('syncplayManager', [componentsPath + '/syncplay/syncplayManager'], returnDefault);
define('playbackPermissionManager', [componentsPath + '/syncplay/playbackPermissionManager'], returnDefault);
define('layoutManager', [componentsPath + '/layoutManager', 'apphost'], getLayoutManager);
define('homeSections', [componentsPath + '/homesections/homesections'], returnFirstDependency);
diff --git a/src/strings/en-us.json b/src/strings/en-us.json
index fb72e8881..4759b670d 100644
--- a/src/strings/en-us.json
+++ b/src/strings/en-us.json
@@ -855,7 +855,8 @@
"LabelSubtitlePlaybackMode": "Subtitle mode:",
"LabelSubtitles": "Subtitles",
"LabelSupportedMediaTypes": "Supported Media Types:",
- "LabelSyncplayTimeOffset": "Time offset with server:",
+ "LabelSyncplayTimeOffset": "Time offset with the server:",
+ "MillisecondsUnit": "ms",
"LabelSyncplayPlaybackDiff": "Playback time difference:",
"LabelSyncplaySyncMethod": "Sync method:",
"LabelSyncplayNewGroup": "New group",
@@ -1031,15 +1032,19 @@
"MessageYouHaveVersionInstalled": "You currently have version {0} installed.",
"MessageSyncplayEnabled": "Syncplay enabled.",
"MessageSyncplayDisabled": "Syncplay disabled.",
- "MessageSyncplayUserJoined": "{0} joined group.",
- "MessageSyncplayUserLeft": "{0} left group.",
+ "MessageSyncplayUserJoined": "{0} has joined the group.",
+ "MessageSyncplayUserLeft": "{0} has left the group.",
"MessageSyncplayGroupWait": "{0} is buffering...",
"MessageSyncplayNoGroupsAvailable": "No groups available. Start playing something first.",
"MessageSyncplayPlaybackPermissionRequired": "Playback permission required.",
- "MessageSyncplayGroupNotJoined": "Failed to join requested group.",
+ "MessageSyncplayGroupDoesNotExist": "Failed to join group because it does not exist.",
"MessageSyncplayCreateGroupDenied": "Permission required to create a group.",
"MessageSyncplayJoinGroupDenied": "Permission required to use Syncplay.",
"MessageSyncplayLibraryAccessDenied": "Access to this content is restricted.",
+ "MessageSyncplayErrorAccessingGroups": "An error occurred while accessing groups list.",
+ "MessageSyncplayErrorNoActivePlayer": "No active player found. Syncplay has been disabled.",
+ "MessageSyncplayErrorMissingSession": "Failed to enable Syncplay! Missing session.",
+ "MessageSyncplayErrorMedia": "Failed to enable Syncplay! Media error.",
"Metadata": "Metadata",
"MetadataManager": "Metadata Manager",
"MetadataSettingChangeHelp": "Changing metadata settings will affect new content that is added going forward. To refresh existing content, open the detail screen and click the refresh button, or perform bulk refreshes using the metadata manager.",
diff --git a/webpack.dev.js b/webpack.dev.js
index d8879fe80..b86914775 100644
--- a/webpack.dev.js
+++ b/webpack.dev.js
@@ -46,8 +46,8 @@ module.exports = merge(common, {
]
},
{
- test: /\.(wav)$/i,
- use: ["file-loader"]
+ test: /\.(mp3)$/i,
+ use: ['file-loader']
}
]
}
diff --git a/webpack.prod.js b/webpack.prod.js
index cc4c57b9f..2f5315ea7 100644
--- a/webpack.prod.js
+++ b/webpack.prod.js
@@ -39,8 +39,8 @@ module.exports = merge(common, {
]
},
{
- test: /\.(wav)$/i,
- use: ["file-loader"]
+ test: /\.(mp3)$/i,
+ use: ['file-loader']
}
]
}