2020-04-16 16:05:04 +02:00
|
|
|
/**
|
2020-09-25 09:44:30 +02:00
|
|
|
* Module that manages time syncing with another device.
|
|
|
|
* @module components/syncPlay/core/timeSync/timeSync
|
2020-04-16 16:05:04 +02:00
|
|
|
*/
|
|
|
|
|
2020-10-17 19:08:56 +01:00
|
|
|
import { Events } from 'jellyfin-apiclient';
|
2020-04-16 16:05:04 +02:00
|
|
|
|
|
|
|
/**
|
2020-09-25 09:44:30 +02:00
|
|
|
* Time estimation.
|
2020-04-16 16:05:04 +02:00
|
|
|
*/
|
|
|
|
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.
|
2020-05-05 12:01:43 +02:00
|
|
|
* @param {Date} requestSent Client's timestamp of the request transmission
|
2020-09-25 09:44:30 +02:00
|
|
|
* @param {Date} requestReceived Remote's timestamp of the request reception
|
|
|
|
* @param {Date} responseSent Remote's timestamp of the response transmission
|
2020-05-05 12:01:43 +02:00
|
|
|
* @param {Date} responseReceived Client's timestamp of the response reception
|
2020-04-16 16:05:04 +02:00
|
|
|
*/
|
2020-05-05 12:01:43 +02:00
|
|
|
constructor(requestSent, requestReceived, responseSent, responseReceived) {
|
|
|
|
this.requestSent = requestSent.getTime();
|
|
|
|
this.requestReceived = requestReceived.getTime();
|
|
|
|
this.responseSent = responseSent.getTime();
|
|
|
|
this.responseReceived = responseReceived.getTime();
|
2020-04-16 16:05:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-09-25 09:44:30 +02:00
|
|
|
* Time offset from remote entity, in milliseconds.
|
2020-04-16 16:05:04 +02:00
|
|
|
*/
|
2020-09-25 09:44:30 +02:00
|
|
|
getOffset() {
|
2020-05-05 12:01:43 +02:00
|
|
|
return ((this.requestReceived - this.requestSent) + (this.responseSent - this.responseReceived)) / 2;
|
2020-04-16 16:05:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-09-25 09:44:30 +02:00
|
|
|
* Get round-trip delay, in milliseconds.
|
2020-04-16 16:05:04 +02:00
|
|
|
*/
|
2020-09-25 09:44:30 +02:00
|
|
|
getDelay() {
|
2020-05-05 12:01:43 +02:00
|
|
|
return (this.responseReceived - this.requestSent) - (this.responseSent - this.requestReceived);
|
2020-04-16 16:05:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-09-25 09:44:30 +02:00
|
|
|
* Get ping time, in milliseconds.
|
2020-04-16 16:05:04 +02:00
|
|
|
*/
|
2020-09-25 09:44:30 +02:00
|
|
|
getPing() {
|
2020-04-16 16:05:04 +02:00
|
|
|
return this.getDelay() / 2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-09-25 09:44:30 +02:00
|
|
|
* Class that manages time syncing with remote entity.
|
2020-04-16 16:05:04 +02:00
|
|
|
*/
|
2020-09-25 09:44:30 +02:00
|
|
|
class TimeSync {
|
|
|
|
constructor(syncPlayManager) {
|
|
|
|
this.manager = syncPlayManager;
|
2020-04-16 16:05:04 +02:00
|
|
|
this.pingStop = true;
|
|
|
|
this.pollingInterval = PollingIntervalGreedy;
|
|
|
|
this.poller = null;
|
|
|
|
this.pings = 0; // number of pings
|
|
|
|
this.measurement = null; // current time sync
|
|
|
|
this.measurements = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets status of time sync.
|
|
|
|
* @returns {boolean} _true_ if a measurement has been done, _false_ otherwise.
|
|
|
|
*/
|
|
|
|
isReady() {
|
2020-05-05 12:01:43 +02:00
|
|
|
return !!this.measurement;
|
2020-04-16 16:05:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-09-25 09:44:30 +02:00
|
|
|
* Gets time offset with remote entity, in milliseconds.
|
2020-04-16 16:05:04 +02:00
|
|
|
* @returns {number} The time offset.
|
|
|
|
*/
|
2020-09-25 09:44:30 +02:00
|
|
|
getTimeOffset() {
|
2020-04-16 16:05:04 +02:00
|
|
|
return this.measurement ? this.measurement.getOffset() : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-09-25 09:44:30 +02:00
|
|
|
* Gets ping time to remote entity, in milliseconds.
|
2020-04-16 16:05:04 +02:00
|
|
|
* @returns {number} The ping time.
|
|
|
|
*/
|
2020-09-25 09:44:30 +02:00
|
|
|
getPing() {
|
2020-04-16 16:05:04 +02:00
|
|
|
return this.measurement ? this.measurement.getPing() : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-09-25 09:44:30 +02:00
|
|
|
* Updates time offset between remote entity and local entity.
|
2020-04-16 16:05:04 +02:00
|
|
|
* @param {Measurement} measurement The new measurement.
|
|
|
|
*/
|
|
|
|
updateTimeOffset(measurement) {
|
|
|
|
this.measurements.push(measurement);
|
|
|
|
if (this.measurements.length > NumberOfTrackedMeasurements) {
|
|
|
|
this.measurements.shift();
|
|
|
|
}
|
|
|
|
|
2020-09-25 09:44:30 +02:00
|
|
|
// Pick measurement with minimum delay.
|
2020-04-16 16:05:04 +02:00
|
|
|
const sortedMeasurements = this.measurements.slice(0);
|
|
|
|
sortedMeasurements.sort((a, b) => a.getDelay() - b.getDelay());
|
|
|
|
this.measurement = sortedMeasurements[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-09-25 09:44:30 +02:00
|
|
|
* Schedules a ping request to the remote entity. Triggers time offset update.
|
|
|
|
* @returns {Promise} Resolves on request success.
|
2020-04-16 16:05:04 +02:00
|
|
|
*/
|
|
|
|
requestPing() {
|
2020-09-25 09:44:30 +02:00
|
|
|
console.warn('SyncPlay TimeSync requestPing: override this method!');
|
|
|
|
return Promise.reject('Not implemented.');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Poller for ping requests.
|
|
|
|
*/
|
|
|
|
internalRequestPing() {
|
|
|
|
if (!this.poller && !this.pingStop) {
|
2020-04-16 16:05:04 +02:00
|
|
|
this.poller = setTimeout(() => {
|
|
|
|
this.poller = null;
|
2020-09-25 09:44:30 +02:00
|
|
|
this.requestPing()
|
|
|
|
.then((result) => this.onPingResponseCallback(result))
|
|
|
|
.catch((error) => this.onPingRequestErrorCallback(error))
|
|
|
|
.finally(() => this.internalRequestPing());
|
2020-04-16 16:05:04 +02:00
|
|
|
}, this.pollingInterval);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-25 09:44:30 +02:00
|
|
|
/**
|
|
|
|
* Handles a successful ping request.
|
|
|
|
* @param {Object} result The ping result.
|
|
|
|
*/
|
|
|
|
onPingResponseCallback(result) {
|
|
|
|
const { requestSent, requestReceived, responseSent, responseReceived } = result;
|
|
|
|
const measurement = new Measurement(requestSent, requestReceived, responseSent, responseReceived);
|
|
|
|
this.updateTimeOffset(measurement);
|
|
|
|
|
|
|
|
// Avoid overloading network.
|
|
|
|
if (this.pings >= GreedyPingCount) {
|
|
|
|
this.pollingInterval = PollingIntervalLowProfile;
|
|
|
|
} else {
|
|
|
|
this.pings++;
|
|
|
|
}
|
|
|
|
|
|
|
|
Events.trigger(this, 'update', [null, this.getTimeOffset(), this.getPing()]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles a failed ping request.
|
|
|
|
* @param {Object} error The error.
|
|
|
|
*/
|
|
|
|
onPingRequestErrorCallback(error) {
|
|
|
|
console.error(error);
|
|
|
|
Events.trigger(this, 'update', [error, null, null]);
|
|
|
|
}
|
|
|
|
|
2020-04-16 16:05:04 +02:00
|
|
|
/**
|
|
|
|
* Drops accumulated measurements.
|
|
|
|
*/
|
2020-09-25 09:44:30 +02:00
|
|
|
resetMeasurements() {
|
2020-04-16 16:05:04 +02:00
|
|
|
this.measurement = null;
|
|
|
|
this.measurements = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Starts the time poller.
|
|
|
|
*/
|
|
|
|
startPing() {
|
2020-09-25 09:44:30 +02:00
|
|
|
this.pingStop = false;
|
|
|
|
this.internalRequestPing();
|
2020-04-16 16:05:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stops the time poller.
|
|
|
|
*/
|
|
|
|
stopPing() {
|
2020-09-25 09:44:30 +02:00
|
|
|
this.pingStop = true;
|
2020-04-16 16:05:04 +02:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-09-25 09:44:30 +02:00
|
|
|
* Converts remote time to local time.
|
|
|
|
* @param {Date} remote The time to convert.
|
2020-04-16 16:05:04 +02:00
|
|
|
* @returns {Date} Local time.
|
|
|
|
*/
|
2020-09-25 09:44:30 +02:00
|
|
|
remoteDateToLocal(remote) {
|
|
|
|
// remote - local = offset
|
|
|
|
return new Date(remote.getTime() - this.getTimeOffset());
|
2020-04-16 16:05:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-09-25 09:44:30 +02:00
|
|
|
* Converts local time to remote time.
|
2020-04-16 16:05:04 +02:00
|
|
|
* @param {Date} local The time to convert.
|
2020-09-25 09:44:30 +02:00
|
|
|
* @returns {Date} Remote time.
|
2020-04-16 16:05:04 +02:00
|
|
|
*/
|
2020-09-25 09:44:30 +02:00
|
|
|
localDateToRemote(local) {
|
|
|
|
// remote - local = offset
|
2020-04-17 19:41:02 +02:00
|
|
|
return new Date(local.getTime() + this.getTimeOffset());
|
2020-04-16 16:05:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-25 09:44:30 +02:00
|
|
|
export default TimeSync;
|