Implement basic SyncPlay settings
This commit is contained in:
parent
4a5051317b
commit
94c405f08e
20 changed files with 618 additions and 17 deletions
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { Events } from 'jellyfin-apiclient';
|
||||
import * as Helper from './Helper';
|
||||
import Settings from './Settings';
|
||||
import TimeSyncCore from './timeSync/TimeSyncCore';
|
||||
import PlaybackCore from './PlaybackCore';
|
||||
import QueueCore from './QueueCore';
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { Events } from 'jellyfin-apiclient';
|
||||
import * as Helper from './Helper';
|
||||
import Settings from './Settings';
|
||||
|
||||
/**
|
||||
* Class that manages the playback of SyncPlay.
|
||||
|
@ -25,6 +26,8 @@ class PlaybackCore {
|
|||
this.lastCommand = null; // Last scheduled playback command, might not be the latest one.
|
||||
this.scheduledCommandTimeout = null;
|
||||
this.syncTimeout = null;
|
||||
|
||||
this.loadPreferences();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,26 +38,35 @@ class PlaybackCore {
|
|||
this.manager = syncPlayManager;
|
||||
this.timeSyncCore = syncPlayManager.getTimeSyncCore();
|
||||
|
||||
Events.on(Settings, 'update', (event) => {
|
||||
this.loadPreferences();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads preferences from saved settings.
|
||||
*/
|
||||
loadPreferences() {
|
||||
// Minimum required delay for SpeedToSync to kick in, in milliseconds.
|
||||
this.minDelaySpeedToSync = 60.0;
|
||||
this.minDelaySpeedToSync = Settings.getFloat('minDelaySpeedToSync', 60.0);
|
||||
|
||||
// Maximum delay after which SkipToSync is used instead of SpeedToSync, in milliseconds.
|
||||
this.maxDelaySpeedToSync = 3000.0;
|
||||
this.maxDelaySpeedToSync = Settings.getFloat('maxDelaySpeedToSync', 3000.0);
|
||||
|
||||
// Time during which the playback is sped up, in milliseconds.
|
||||
this.speedToSyncDuration = 1000.0;
|
||||
this.speedToSyncDuration = Settings.getFloat('speedToSyncDuration', 1000.0);
|
||||
|
||||
// Minimum required delay for SkipToSync to kick in, in milliseconds.
|
||||
this.minDelaySkipToSync = 400.0;
|
||||
this.minDelaySkipToSync = Settings.getFloat('minDelaySkipToSync', 400.0);
|
||||
|
||||
// Whether SpeedToSync should be used.
|
||||
this.useSpeedToSync = true;
|
||||
this.useSpeedToSync = Settings.getBool('useSpeedToSync', true);
|
||||
|
||||
// Whether SkipToSync should be used.
|
||||
this.useSkipToSync = true;
|
||||
this.useSkipToSync = Settings.getBool('useSkipToSync', true);
|
||||
|
||||
// Whether sync correction during playback is active.
|
||||
this.enableSyncCorrection = true;
|
||||
this.enableSyncCorrection = Settings.getBool('enableSyncCorrection', true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -526,7 +538,12 @@ class PlaybackCore {
|
|||
// Diff might be caused by the player internally starting the playback.
|
||||
const diffMillis = (serverPositionTicks - currentPositionTicks) / Helper.TicksPerMillisecond;
|
||||
|
||||
// Adapt playback diff to selected device for time syncing.
|
||||
const targetPlaybackDiff = diffMillis - this.timeSyncCore.getPlaybackDiff();
|
||||
|
||||
// Notify update for playback sync.
|
||||
this.playbackDiffMillis = diffMillis;
|
||||
Events.trigger(this.manager, 'playback-diff', [this.playbackDiffMillis]);
|
||||
|
||||
// Avoid overloading the browser.
|
||||
const elapsed = currentTime - this.lastSyncTime;
|
||||
|
@ -536,22 +553,22 @@ class PlaybackCore {
|
|||
const playerWrapper = this.manager.getPlayerWrapper();
|
||||
|
||||
if (this.syncEnabled && this.enableSyncCorrection) {
|
||||
const absDiffMillis = Math.abs(diffMillis);
|
||||
const absDiffMillis = Math.abs(targetPlaybackDiff);
|
||||
// TODO: SpeedToSync sounds bad on songs.
|
||||
// TODO: SpeedToSync is failing on Safari (Mojave); even if playbackRate is supported, some delay seems to exist.
|
||||
// TODO: both SpeedToSync and SpeedToSync seem to have a hard time keeping up on Android Chrome as well.
|
||||
if (playerWrapper.hasPlaybackRate() && this.useSpeedToSync && absDiffMillis >= this.minDelaySpeedToSync && absDiffMillis < this.maxDelaySpeedToSync) {
|
||||
// Fix negative speed when client is ahead of time more than speedToSyncTime.
|
||||
const MinSpeed = 0.2;
|
||||
if (diffMillis <= -speedToSyncTime * MinSpeed) {
|
||||
speedToSyncTime = Math.abs(diffMillis) / (1.0 - MinSpeed);
|
||||
if (targetPlaybackDiff <= -speedToSyncTime * MinSpeed) {
|
||||
speedToSyncTime = Math.abs(targetPlaybackDiff) / (1.0 - MinSpeed);
|
||||
}
|
||||
|
||||
// SpeedToSync strategy.
|
||||
const speed = 1 + diffMillis / speedToSyncTime;
|
||||
const speed = 1 + targetPlaybackDiff / speedToSyncTime;
|
||||
|
||||
if (speed <= 0) {
|
||||
console.error('SyncPlay error: speed should not be negative!', speed, diffMillis, speedToSyncTime);
|
||||
console.error('SyncPlay error: speed should not be negative!', speed, targetPlaybackDiff, speedToSyncTime);
|
||||
}
|
||||
|
||||
playerWrapper.setPlaybackRate(speed);
|
||||
|
|
84
src/components/syncPlay/core/Settings.js
Normal file
84
src/components/syncPlay/core/Settings.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* Module that manages SyncPlay settings.
|
||||
* @module components/syncPlay/core/Settings
|
||||
*/
|
||||
|
||||
import { Events, AppStorage } from 'jellyfin-apiclient';
|
||||
|
||||
/**
|
||||
* Class that manages SyncPlay settings.
|
||||
*/
|
||||
class SyncPlaySettings {
|
||||
constructor() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the key used to store a setting in the App Storage.
|
||||
* @param {string} name The name of the setting.
|
||||
* @returns {string} The key.
|
||||
*/
|
||||
getKey(name) {
|
||||
return 'syncPlay-' + name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of a setting.
|
||||
* @param {string} name The name of the setting.
|
||||
* @returns {string} The value.
|
||||
*/
|
||||
get(name) {
|
||||
return AppStorage.getItem(this.getKey(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of a setting. Triggers an update if the new value differs from the old one.
|
||||
* @param {string} name The name of the setting.
|
||||
* @param {Object} value The value of the setting.
|
||||
*/
|
||||
set(name, value) {
|
||||
const oldValue = this.get(name);
|
||||
AppStorage.setItem(this.getKey(name), value);
|
||||
const newValue = this.get(name);
|
||||
|
||||
if (oldValue !== newValue) {
|
||||
Events.trigger(this, name, [newValue, oldValue]);
|
||||
}
|
||||
|
||||
console.debug(`SyncPlay Settings set: '${name}' from '${oldValue}' to '${newValue}'.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of a setting as boolean.
|
||||
* @param {string} name The name of the setting.
|
||||
* @param {boolean} defaultValue The default value if the setting does not exist.
|
||||
* @returns {boolean} The value.
|
||||
*/
|
||||
getBool(name, defaultValue = false) {
|
||||
const value = this.get(name);
|
||||
if (value !== 'true' && value !== 'false') {
|
||||
return defaultValue;
|
||||
} else {
|
||||
return this.get(name) !== 'false';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of a setting as float number.
|
||||
* @param {string} name The name of the setting.
|
||||
* @param {number} defaultValue The default value if the setting does not exist.
|
||||
* @returns {number} The value.
|
||||
*/
|
||||
getFloat(name, defaultValue = 0) {
|
||||
const value = this.get(name);
|
||||
if (value === null || value === '' || isNaN(value)) {
|
||||
return defaultValue;
|
||||
} else {
|
||||
return Number.parseFloat(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** SyncPlaySettings singleton. */
|
||||
const Settings = new SyncPlaySettings();
|
||||
export default Settings;
|
|
@ -1,4 +1,5 @@
|
|||
import * as Helper from './Helper';
|
||||
import Settings from './Settings';
|
||||
import ManagerClass from './Manager';
|
||||
import PlayerFactoryClass from './players/PlayerFactory';
|
||||
import GenericPlayer from './players/GenericPlayer';
|
||||
|
@ -8,6 +9,7 @@ const Manager = new ManagerClass(PlayerFactory);
|
|||
|
||||
export default {
|
||||
Helper,
|
||||
Settings,
|
||||
Manager,
|
||||
PlayerFactory,
|
||||
Players: {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
import { Events } from 'jellyfin-apiclient';
|
||||
import Settings from '../Settings';
|
||||
import TimeSyncServer from './TimeSyncServer';
|
||||
|
||||
/**
|
||||
|
@ -13,6 +14,9 @@ class TimeSyncCore {
|
|||
constructor() {
|
||||
this.manager = null;
|
||||
this.timeSyncServer = null;
|
||||
|
||||
this.timeSyncDeviceId = Settings.get('timeSyncDevice') || 'server';
|
||||
this.extraTimeOffset = Settings.getFloat('extraTimeOffset', 0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,6 +35,10 @@ class TimeSyncCore {
|
|||
|
||||
Events.trigger(this, 'time-sync-server-update', [timeOffset, ping]);
|
||||
});
|
||||
|
||||
Events.on(Settings, 'extraTimeOffset', (event, value, oldValue) => {
|
||||
this.extraTimeOffset = Settings.getFloat('extraTimeOffset', 0.0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -40,6 +48,32 @@ class TimeSyncCore {
|
|||
this.timeSyncServer.forceUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of available devices for time sync.
|
||||
* @returns {Array} The list of devices.
|
||||
*/
|
||||
getDevices() {
|
||||
const devices = [{
|
||||
type: 'server',
|
||||
id: 'server',
|
||||
name: 'Server',
|
||||
timeOffset: this.timeSyncServer.getTimeOffset(),
|
||||
ping: this.timeSyncServer.getPing(),
|
||||
peerTimeOffset: 0,
|
||||
peerPing: 0
|
||||
}];
|
||||
|
||||
return devices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the identifier of the selected device for time sync. Default value is 'server'.
|
||||
* @returns {string} The identifier.
|
||||
*/
|
||||
getActiveDevice() {
|
||||
return this.timeSyncDeviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the display name of the selected device for time sync.
|
||||
* @returns {string} The display name.
|
||||
|
@ -54,7 +88,8 @@ class TimeSyncCore {
|
|||
* @returns {Date} Local time.
|
||||
*/
|
||||
remoteDateToLocal(remote) {
|
||||
return this.timeSyncServer.remoteDateToLocal(remote);
|
||||
const date = this.timeSyncServer.remoteDateToLocal(remote);
|
||||
return this.offsetDate(date, -this.extraTimeOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -63,15 +98,35 @@ class TimeSyncCore {
|
|||
* @returns {Date} Server time.
|
||||
*/
|
||||
localDateToRemote(local) {
|
||||
return this.timeSyncServer.localDateToRemote(local);
|
||||
const date = this.timeSyncServer.localDateToRemote(local);
|
||||
return this.offsetDate(date, this.extraTimeOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets time offset that should be used for time syncing, in milliseconds.
|
||||
* Gets time offset that should be used for time syncing, in milliseconds. Takes into account server and active device selected for syncing.
|
||||
* @returns {number} The time offset.
|
||||
*/
|
||||
getTimeOffset() {
|
||||
return this.timeSyncServer.getTimeOffset();
|
||||
return this.timeSyncServer.getTimeOffset() + this.extraTimeOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the playback diff that should be used to offset local playback, in milliseconds.
|
||||
* @returns {number} The time offset.
|
||||
*/
|
||||
getPlaybackDiff() {
|
||||
// TODO: this will use playback data from WebRTC peers.
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Offsets a given date by a given ammount of milliseconds.
|
||||
* @param {Date} date The date.
|
||||
* @param {number} offset The offset, in milliseconds.
|
||||
* @returns {Date} The offset date.
|
||||
*/
|
||||
offsetDate(date, offset) {
|
||||
return new Date(date.getTime() + offset);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue