mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Implement NTP like time sync
This commit is contained in:
parent
06e6c99c03
commit
5342b90a56
5 changed files with 236 additions and 156 deletions
|
@ -332,8 +332,8 @@ define(['events', 'globalize', 'playbackManager', 'connectionManager', 'syncplay
|
||||||
var stats = syncplayManager.getStats();
|
var stats = syncplayManager.getStats();
|
||||||
|
|
||||||
syncStats.push({
|
syncStats.push({
|
||||||
label: globalize.translate("LabelSyncplayTimeDiff"),
|
label: globalize.translate("LabelSyncplayTimeOffset"),
|
||||||
value: stats.TimeDiff + "ms"
|
value: stats.TimeOffset + "ms"
|
||||||
});
|
});
|
||||||
|
|
||||||
syncStats.push({
|
syncStats.push({
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import events from 'events';
|
import events from 'events';
|
||||||
import connectionManager from 'connectionManager';
|
import connectionManager from 'connectionManager';
|
||||||
import playbackManager from 'playbackManager';
|
import playbackManager from 'playbackManager';
|
||||||
|
import timeSyncManager from 'timeSyncManager';
|
||||||
import toast from 'toast';
|
import toast from 'toast';
|
||||||
import globalize from 'globalize';
|
import globalize from 'globalize';
|
||||||
|
|
||||||
|
@ -80,11 +81,7 @@ class SyncplayManager {
|
||||||
this.scheduledCommand = null;
|
this.scheduledCommand = null;
|
||||||
this.syncTimeout = null;
|
this.syncTimeout = null;
|
||||||
|
|
||||||
this.pingStop = true;
|
this.timeOffsetWithServer = 0; // server time minus local time
|
||||||
this.pingIntervalTimeout = PingIntervalTimeoutGreedy;
|
|
||||||
this.pingInterval = null;
|
|
||||||
this.initTimeDiff = 0; // number of pings
|
|
||||||
this.timeDiff = 0; // local time minus server time
|
|
||||||
this.roundTripDuration = 0;
|
this.roundTripDuration = 0;
|
||||||
this.notifySyncplayReady = false;
|
this.notifySyncplayReady = false;
|
||||||
|
|
||||||
|
@ -101,6 +98,17 @@ class SyncplayManager {
|
||||||
events.on(this, "TimeUpdate", (event) => {
|
events.on(this, "TimeUpdate", (event) => {
|
||||||
this.syncPlaybackTime();
|
this.syncPlaybackTime();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
events.on(timeSyncManager, "Update", (event, timeOffset, ping) => {
|
||||||
|
this.timeOffsetWithServer = timeOffset;
|
||||||
|
this.roundTripDuration = ping * 2;
|
||||||
|
|
||||||
|
if (this.notifySyncplayReady) {
|
||||||
|
this.syncplayReady = true;
|
||||||
|
events.trigger(this, "SyncplayReady");
|
||||||
|
this.notifySyncplayReady = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -377,14 +385,17 @@ class SyncplayManager {
|
||||||
*/
|
*/
|
||||||
enableSyncplay (apiClient, enabledAt, showMessage = false) {
|
enableSyncplay (apiClient, enabledAt, showMessage = false) {
|
||||||
this.syncplayEnabledAt = enabledAt;
|
this.syncplayEnabledAt = enabledAt;
|
||||||
this.syncplayReady = false;
|
this.injectPlaybackManager();
|
||||||
events.trigger(this, "SyncplayEnabled", [true]);
|
events.trigger(this, "SyncplayEnabled", [true]);
|
||||||
|
|
||||||
waitForEvent(this, "SyncplayReady").then(() => {
|
waitForEvent(this, "SyncplayReady").then(() => {
|
||||||
this.processCommand(this.queuedCommand, apiClient);
|
this.processCommand(this.queuedCommand, apiClient);
|
||||||
this.queuedCommand = null;
|
this.queuedCommand = null;
|
||||||
});
|
});
|
||||||
this.injectPlaybackManager();
|
this.syncplayReady = false;
|
||||||
this.startPing();
|
this.notifySyncplayReady = true;
|
||||||
|
|
||||||
|
timeSyncManager.forceUpdate();
|
||||||
|
|
||||||
if (showMessage) {
|
if (showMessage) {
|
||||||
toast({
|
toast({
|
||||||
|
@ -405,7 +416,6 @@ class SyncplayManager {
|
||||||
this.syncEnabled = false;
|
this.syncEnabled = false;
|
||||||
events.trigger(this, "SyncplayEnabled", [false]);
|
events.trigger(this, "SyncplayEnabled", [false]);
|
||||||
this.restorePlaybackManager();
|
this.restorePlaybackManager();
|
||||||
this.stopPing();
|
|
||||||
this.stopSyncWatcher();
|
this.stopSyncWatcher();
|
||||||
|
|
||||||
if (showMessage) {
|
if (showMessage) {
|
||||||
|
@ -431,7 +441,7 @@ class SyncplayManager {
|
||||||
schedulePlay (playAtTime, positionTicks) {
|
schedulePlay (playAtTime, positionTicks) {
|
||||||
this.clearScheduledCommand();
|
this.clearScheduledCommand();
|
||||||
var currentTime = new Date();
|
var currentTime = new Date();
|
||||||
var playAtTimeLocal = this.serverDateToLocal(playAtTime);
|
var playAtTimeLocal = timeSyncManager.serverDateToLocal(playAtTime);
|
||||||
|
|
||||||
if (playAtTimeLocal > currentTime) {
|
if (playAtTimeLocal > currentTime) {
|
||||||
var playTimeout = playAtTimeLocal - currentTime;
|
var playTimeout = playAtTimeLocal - currentTime;
|
||||||
|
@ -469,7 +479,7 @@ class SyncplayManager {
|
||||||
schedulePause (pauseAtTime, positionTicks) {
|
schedulePause (pauseAtTime, positionTicks) {
|
||||||
this.clearScheduledCommand();
|
this.clearScheduledCommand();
|
||||||
var currentTime = new Date();
|
var currentTime = new Date();
|
||||||
var pauseAtTimeLocal = this.serverDateToLocal(pauseAtTime);
|
var pauseAtTimeLocal = timeSyncManager.serverDateToLocal(pauseAtTime);
|
||||||
|
|
||||||
if (pauseAtTimeLocal > currentTime) {
|
if (pauseAtTimeLocal > currentTime) {
|
||||||
var pauseTimeout = pauseAtTimeLocal - currentTime;
|
var pauseTimeout = pauseAtTimeLocal - currentTime;
|
||||||
|
@ -575,126 +585,6 @@ class SyncplayManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes time difference between this client's time and server's time.
|
|
||||||
* @param {Date} pingStartTime Local time when ping request started.
|
|
||||||
* @param {Date} pingEndTime Local time when ping request ended.
|
|
||||||
* @param {Date} serverTime Server UTC time at ping request.
|
|
||||||
*/
|
|
||||||
updateTimeDiff (pingStartTime, pingEndTime, serverTime) {
|
|
||||||
this.roundTripDuration = (pingEndTime - pingStartTime);
|
|
||||||
// The faster the response, the closer we are to the real timeDiff value
|
|
||||||
// localTime = pingStartTime + roundTripDuration / 2
|
|
||||||
// newTimeDiff = localTime - serverTime
|
|
||||||
var newTimeDiff = (pingStartTime - serverTime) + (this.roundTripDuration / 2);
|
|
||||||
|
|
||||||
// Initial setup
|
|
||||||
if (this.initTimeDiff === 0) {
|
|
||||||
this.timeDiff = newTimeDiff;
|
|
||||||
this.initTimeDiff++
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// As response time gets better, absolute value should decrease
|
|
||||||
var distanceFromZero = Math.abs(newTimeDiff);
|
|
||||||
var oldDistanceFromZero = Math.abs(this.timeDiff);
|
|
||||||
if (distanceFromZero < oldDistanceFromZero) {
|
|
||||||
this.timeDiff = newTimeDiff;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid overloading server
|
|
||||||
if (this.initTimeDiff >= GreedyPingCount) {
|
|
||||||
this.pingIntervalTimeout = PingIntervalTimeoutLowProfile;
|
|
||||||
} else {
|
|
||||||
this.initTimeDiff++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.debug("Syncplay updateTimeDiff:", serverTime, this.timeDiff, this.roundTripDuration, newTimeDiff);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schedules a ping request to the server. Used to compute time difference between client and server.
|
|
||||||
*/
|
|
||||||
requestPing () {
|
|
||||||
if (this.pingInterval === null && !this.pingStop) {
|
|
||||||
this.pingInterval = setTimeout(() => {
|
|
||||||
this.pingInterval = null;
|
|
||||||
|
|
||||||
var apiClient = connectionManager.currentApiClient();
|
|
||||||
var sessionId = getActivePlayerId();
|
|
||||||
|
|
||||||
if (!sessionId) {
|
|
||||||
this.signalError();
|
|
||||||
toast({
|
|
||||||
// TODO: translate
|
|
||||||
text: "Syncplay error occured."
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pingStartTime = new Date();
|
|
||||||
apiClient.sendSyncplayCommand(sessionId, "GetUtcTime").then((response) => {
|
|
||||||
var pingEndTime = new Date();
|
|
||||||
response.text().then((utcTime) => {
|
|
||||||
var serverTime = new Date(utcTime);
|
|
||||||
this.updateTimeDiff(pingStartTime, pingEndTime, serverTime);
|
|
||||||
|
|
||||||
// Alert user that ping is high
|
|
||||||
if (Math.abs(this.roundTripDuration) >= 1000) {
|
|
||||||
events.trigger(this, "SyncplayError", [true]);
|
|
||||||
} else {
|
|
||||||
events.trigger(this, "SyncplayError", [false]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify server of ping
|
|
||||||
apiClient.sendSyncplayCommand(sessionId, "KeepAlive", {
|
|
||||||
Ping: this.roundTripDuration / 2
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.notifySyncplayReady) {
|
|
||||||
this.syncplayReady = true;
|
|
||||||
events.trigger(this, "SyncplayReady");
|
|
||||||
this.notifySyncplayReady = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.requestPing();
|
|
||||||
});
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
this.signalError();
|
|
||||||
toast({
|
|
||||||
// TODO: translate
|
|
||||||
text: "Syncplay error occured."
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
}, this.pingIntervalTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the keep alive poller.
|
|
||||||
*/
|
|
||||||
startPing () {
|
|
||||||
this.notifySyncplayReady = true;
|
|
||||||
this.pingStop = false;
|
|
||||||
this.initTimeDiff = this.initTimeDiff > this.greedyPingCount ? 1 : this.initTimeDiff;
|
|
||||||
this.pingIntervalTimeout = PingIntervalTimeoutGreedy;
|
|
||||||
|
|
||||||
this.requestPing();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the keep alive poller.
|
|
||||||
*/
|
|
||||||
stopPing () {
|
|
||||||
this.pingStop = true;
|
|
||||||
if (this.pingInterval !== null) {
|
|
||||||
clearTimeout(this.pingInterval);
|
|
||||||
this.pingInterval = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to sync playback time with estimated server time.
|
* Attempts to sync playback time with estimated server time.
|
||||||
*
|
*
|
||||||
|
@ -722,7 +612,7 @@ class SyncplayManager {
|
||||||
|
|
||||||
const CurrentPositionTicks = playbackManager.currentTime();
|
const CurrentPositionTicks = playbackManager.currentTime();
|
||||||
// Estimate PositionTicks on server
|
// Estimate PositionTicks on server
|
||||||
const ServerPositionTicks = this.lastCommand.PositionTicks + ((currentTime - playAtTime) - this.timeDiff) * 10000;
|
const ServerPositionTicks = this.lastCommand.PositionTicks + ((currentTime - playAtTime) + this.timeOffsetWithServer) * 10000;
|
||||||
// Measure delay that needs to be recovered
|
// Measure delay that needs to be recovered
|
||||||
// diff might be caused by the player internally starting the playback
|
// diff might be caused by the player internally starting the playback
|
||||||
const diff = ServerPositionTicks - CurrentPositionTicks;
|
const diff = ServerPositionTicks - CurrentPositionTicks;
|
||||||
|
@ -848,33 +738,13 @@ class SyncplayManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts server time to local time.
|
|
||||||
* @param {Date} server The time to convert.
|
|
||||||
* @returns {Date} Local time.
|
|
||||||
*/
|
|
||||||
serverDateToLocal (server) {
|
|
||||||
// local - server = diff
|
|
||||||
return new Date(server.getTime() + this.timeDiff);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts local time to server time.
|
|
||||||
* @param {Date} local The time to convert.
|
|
||||||
* @returns {Date} Server time.
|
|
||||||
*/
|
|
||||||
localDateToServer (local) {
|
|
||||||
// local - server = diff
|
|
||||||
return new Date(local.getTime() - this.timeDiff);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets Syncplay stats.
|
* Gets Syncplay stats.
|
||||||
* @returns {Object} The Syncplay stats.
|
* @returns {Object} The Syncplay stats.
|
||||||
*/
|
*/
|
||||||
getStats () {
|
getStats () {
|
||||||
return {
|
return {
|
||||||
TimeDiff: this.timeDiff,
|
TimeOffset: this.timeOffsetWithServer,
|
||||||
PlaybackDiff: this.playbackDiffMillis,
|
PlaybackDiff: this.playbackDiffMillis,
|
||||||
SyncMethod: this.syncMethod
|
SyncMethod: this.syncMethod
|
||||||
}
|
}
|
||||||
|
|
209
src/components/syncplay/timeSyncManager.js
Normal file
209
src/components/syncplay/timeSyncManager.js
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
/* eslint-disable indent */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module that manages time syncing with server.
|
||||||
|
* @module components/syncplay/timeSyncManager
|
||||||
|
*/
|
||||||
|
|
||||||
|
import events from 'events';
|
||||||
|
import connectionManager from 'connectionManager';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time estimation
|
||||||
|
*/
|
||||||
|
const NumberOfTrackedMeasurements = 8;
|
||||||
|
const PollingIntervalGreedy = 1000; // milliseconds
|
||||||
|
const PollingIntervalLowProfile = 60000; // milliseconds
|
||||||
|
const GreedyPingCount = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that stores measurement data.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
constructor(t0, t1, t2, t3) {
|
||||||
|
this.t0 = t0.getTime();
|
||||||
|
this.t1 = t1.getTime();
|
||||||
|
this.t2 = t2.getTime();
|
||||||
|
this.t3 = t3.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time offset from server.
|
||||||
|
*/
|
||||||
|
getOffset () {
|
||||||
|
return ((this.t1 - this.t0) + (this.t2 - this.t3)) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get round-trip delay.
|
||||||
|
*/
|
||||||
|
getDelay () {
|
||||||
|
return (this.t3 - this.t0) - (this.t2 - this.t1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get ping time.
|
||||||
|
*/
|
||||||
|
getPing () {
|
||||||
|
return this.getDelay() / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that manages time syncing with server.
|
||||||
|
*/
|
||||||
|
class TimeSyncManager {
|
||||||
|
constructor() {
|
||||||
|
this.pingStop = true;
|
||||||
|
this.pollingInterval = PollingIntervalGreedy;
|
||||||
|
this.poller = null;
|
||||||
|
this.pings = 0; // number of pings
|
||||||
|
this.measurement = null; // current time sync
|
||||||
|
this.measurements = [];
|
||||||
|
|
||||||
|
this.startPing();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets status of time sync.
|
||||||
|
* @returns {boolean} _true_ if a measurement has been done, _false_ otherwise.
|
||||||
|
*/
|
||||||
|
isReady() {
|
||||||
|
return this.measurement ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets time offset with server.
|
||||||
|
* @returns {number} The time offset.
|
||||||
|
*/
|
||||||
|
getTimeOffset () {
|
||||||
|
return this.measurement ? this.measurement.getOffset() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets ping time to server.
|
||||||
|
* @returns {number} The ping time.
|
||||||
|
*/
|
||||||
|
getPing () {
|
||||||
|
return this.measurement ? this.measurement.getPing() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates time offset between server and client.
|
||||||
|
* @param {Measurement} measurement The new measurement.
|
||||||
|
*/
|
||||||
|
updateTimeOffset(measurement) {
|
||||||
|
this.measurements.push(measurement);
|
||||||
|
if (this.measurements.length > NumberOfTrackedMeasurements) {
|
||||||
|
this.measurements.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick measurement with minimum delay
|
||||||
|
const sortedMeasurements = this.measurements.slice(0);
|
||||||
|
sortedMeasurements.sort((a, b) => a.getDelay() - b.getDelay());
|
||||||
|
this.measurement = sortedMeasurements[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a ping request to the server. Triggers time offset update.
|
||||||
|
*/
|
||||||
|
requestPing() {
|
||||||
|
if (!this.poller) {
|
||||||
|
this.poller = setTimeout(() => {
|
||||||
|
this.poller = null;
|
||||||
|
const apiClient = connectionManager.currentApiClient();
|
||||||
|
const t0 = new Date(); // pingStartTime
|
||||||
|
apiClient.getServerTime().then((response) => {
|
||||||
|
const t3 = new Date(); // pingEndTime
|
||||||
|
response.json().then((data) => {
|
||||||
|
const t1 = new Date(data.RequestReceptionTime); // request received
|
||||||
|
const t2 = new Date(data.ResponseTransmissionTime); // response sent
|
||||||
|
|
||||||
|
const measurement = new Measurement(t0, t1, t2, t3);
|
||||||
|
this.updateTimeOffset(measurement);
|
||||||
|
|
||||||
|
// Avoid overloading server
|
||||||
|
if (this.pings >= GreedyPingCount) {
|
||||||
|
this.pollingInterval = PollingIntervalLowProfile;
|
||||||
|
} else {
|
||||||
|
this.pings++;
|
||||||
|
}
|
||||||
|
|
||||||
|
events.trigger(this, "Update", [this.getTimeOffset(), this.getPing()]);
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
events.trigger(this, "Error", [error]);
|
||||||
|
}).finally(() => {
|
||||||
|
this.requestPing();
|
||||||
|
});
|
||||||
|
|
||||||
|
}, this.pollingInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops accumulated measurements.
|
||||||
|
*/
|
||||||
|
resetMeasurements () {
|
||||||
|
this.measurement = null;
|
||||||
|
this.measurements = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the time poller.
|
||||||
|
*/
|
||||||
|
startPing() {
|
||||||
|
this.requestPing();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the time poller.
|
||||||
|
*/
|
||||||
|
stopPing() {
|
||||||
|
if (this.poller) {
|
||||||
|
clearTimeout(this.poller);
|
||||||
|
this.poller = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets poller into greedy mode.
|
||||||
|
*/
|
||||||
|
forceUpdate() {
|
||||||
|
this.stopPing();
|
||||||
|
this.pollingInterval = PollingIntervalGreedy;
|
||||||
|
this.pings = 0;
|
||||||
|
this.startPing();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts server time to local time.
|
||||||
|
* @param {Date} server The time to convert.
|
||||||
|
* @returns {Date} Local time.
|
||||||
|
*/
|
||||||
|
serverDateToLocal(server) {
|
||||||
|
// server - local = offset
|
||||||
|
return new Date(server.getTime() + this.getTimeOffset());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts local time to server time.
|
||||||
|
* @param {Date} local The time to convert.
|
||||||
|
* @returns {Date} Server time.
|
||||||
|
*/
|
||||||
|
localDateToServer(local) {
|
||||||
|
// server - local = offset
|
||||||
|
return new Date(local.getTime() - this.getTimeOffset());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** TimeSyncManager singleton. */
|
||||||
|
export default new TimeSyncManager();
|
|
@ -824,6 +824,7 @@ var AppInfo = {};
|
||||||
define('playbackSettings', [componentsPath + '/playbacksettings/playbacksettings'], returnFirstDependency);
|
define('playbackSettings', [componentsPath + '/playbacksettings/playbacksettings'], returnFirstDependency);
|
||||||
define('homescreenSettings', [componentsPath + '/homescreensettings/homescreensettings'], returnFirstDependency);
|
define('homescreenSettings', [componentsPath + '/homescreensettings/homescreensettings'], returnFirstDependency);
|
||||||
define('playbackManager', [componentsPath + '/playback/playbackmanager'], getPlaybackManager);
|
define('playbackManager', [componentsPath + '/playback/playbackmanager'], getPlaybackManager);
|
||||||
|
define('timeSyncManager', [componentsPath + '/syncplay/timeSyncManager'], returnDefault);
|
||||||
define('syncplayManager', [componentsPath + '/syncplay/syncplaymanager'], returnDefault);
|
define('syncplayManager', [componentsPath + '/syncplay/syncplaymanager'], returnDefault);
|
||||||
define('playbackPermissionManager', [componentsPath + '/syncplay/playbackPermissionManager'], returnDefault);
|
define('playbackPermissionManager', [componentsPath + '/syncplay/playbackPermissionManager'], returnDefault);
|
||||||
define('layoutManager', [componentsPath + '/layoutManager', 'apphost'], getLayoutManager);
|
define('layoutManager', [componentsPath + '/layoutManager', 'apphost'], getLayoutManager);
|
||||||
|
|
|
@ -855,7 +855,7 @@
|
||||||
"LabelSubtitlePlaybackMode": "Subtitle mode:",
|
"LabelSubtitlePlaybackMode": "Subtitle mode:",
|
||||||
"LabelSubtitles": "Subtitles",
|
"LabelSubtitles": "Subtitles",
|
||||||
"LabelSupportedMediaTypes": "Supported Media Types:",
|
"LabelSupportedMediaTypes": "Supported Media Types:",
|
||||||
"LabelSyncplayTimeDiff": "Time difference with server:",
|
"LabelSyncplayTimeOffset": "Time offset with server:",
|
||||||
"LabelSyncplayPlaybackDiff": "Playback time difference:",
|
"LabelSyncplayPlaybackDiff": "Playback time difference:",
|
||||||
"LabelSyncplaySyncMethod": "Sync method:",
|
"LabelSyncplaySyncMethod": "Sync method:",
|
||||||
"LabelSyncplayNewGroup": "New group",
|
"LabelSyncplayNewGroup": "New group",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue