mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Implement syncplay frontend
This commit is contained in:
parent
ed12e7c4f9
commit
6c18b655e0
11 changed files with 915 additions and 4 deletions
|
@ -30,7 +30,7 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.osdHeader .headerButton:not(.headerBackButton):not(.headerCastButton) {
|
.osdHeader .headerButton:not(.headerBackButton):not(.headerCastButton):not(.headerSyncButton) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -171,6 +171,7 @@ define(['events', 'browser', 'require', 'apphost', 'appSettings', 'htmlMediaHelp
|
||||||
elem.addEventListener('pause', onPause);
|
elem.addEventListener('pause', onPause);
|
||||||
elem.addEventListener('playing', onPlaying);
|
elem.addEventListener('playing', onPlaying);
|
||||||
elem.addEventListener('play', onPlay);
|
elem.addEventListener('play', onPlay);
|
||||||
|
elem.addEventListener('waiting', onWaiting);
|
||||||
}
|
}
|
||||||
|
|
||||||
function unBindEvents(elem) {
|
function unBindEvents(elem) {
|
||||||
|
@ -180,6 +181,7 @@ define(['events', 'browser', 'require', 'apphost', 'appSettings', 'htmlMediaHelp
|
||||||
elem.removeEventListener('pause', onPause);
|
elem.removeEventListener('pause', onPause);
|
||||||
elem.removeEventListener('playing', onPlaying);
|
elem.removeEventListener('playing', onPlaying);
|
||||||
elem.removeEventListener('play', onPlay);
|
elem.removeEventListener('play', onPlay);
|
||||||
|
elem.removeEventListener('waiting', onWaiting);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.stop = function (destroyPlayer) {
|
self.stop = function (destroyPlayer) {
|
||||||
|
@ -294,6 +296,10 @@ define(['events', 'browser', 'require', 'apphost', 'appSettings', 'htmlMediaHelp
|
||||||
events.trigger(self, 'pause');
|
events.trigger(self, 'pause');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onWaiting() {
|
||||||
|
events.trigger(self, 'waiting');
|
||||||
|
}
|
||||||
|
|
||||||
function onError() {
|
function onError() {
|
||||||
|
|
||||||
var errorCode = this.error ? (this.error.code || 0) : 0;
|
var errorCode = this.error ? (this.error.code || 0) : 0;
|
||||||
|
@ -450,6 +456,21 @@ define(['events', 'browser', 'require', 'apphost', 'appSettings', 'htmlMediaHelp
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
HtmlAudioPlayer.prototype.setPlaybackRate = function (value) {
|
||||||
|
var mediaElement = this._mediaElement;
|
||||||
|
if (mediaElement) {
|
||||||
|
mediaElement.playbackRate = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
HtmlAudioPlayer.prototype.getPlaybackRate = function () {
|
||||||
|
var mediaElement = this._mediaElement;
|
||||||
|
if (mediaElement) {
|
||||||
|
return mediaElement.playbackRate;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
HtmlAudioPlayer.prototype.setVolume = function (val) {
|
HtmlAudioPlayer.prototype.setVolume = function (val) {
|
||||||
var mediaElement = this._mediaElement;
|
var mediaElement = this._mediaElement;
|
||||||
if (mediaElement) {
|
if (mediaElement) {
|
||||||
|
@ -493,5 +514,26 @@ define(['events', 'browser', 'require', 'apphost', 'appSettings', 'htmlMediaHelp
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var supportedFeatures;
|
||||||
|
|
||||||
|
function getSupportedFeatures() {
|
||||||
|
var list = [];
|
||||||
|
var audio = document.createElement('audio');
|
||||||
|
|
||||||
|
if (typeof audio.playbackRate === "number") {
|
||||||
|
list.push("PlaybackRate");
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
HtmlAudioPlayer.prototype.supports = function (feature) {
|
||||||
|
if (!supportedFeatures) {
|
||||||
|
supportedFeatures = getSupportedFeatures();
|
||||||
|
}
|
||||||
|
|
||||||
|
return supportedFeatures.indexOf(feature) !== -1;
|
||||||
|
};
|
||||||
|
|
||||||
return HtmlAudioPlayer;
|
return HtmlAudioPlayer;
|
||||||
});
|
});
|
||||||
|
|
|
@ -785,6 +785,7 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa
|
||||||
videoElement.removeEventListener('play', onPlay);
|
videoElement.removeEventListener('play', onPlay);
|
||||||
videoElement.removeEventListener('click', onClick);
|
videoElement.removeEventListener('click', onClick);
|
||||||
videoElement.removeEventListener('dblclick', onDblClick);
|
videoElement.removeEventListener('dblclick', onDblClick);
|
||||||
|
videoElement.removeEventListener('waiting', onWaiting);
|
||||||
|
|
||||||
videoElement.parentNode.removeChild(videoElement);
|
videoElement.parentNode.removeChild(videoElement);
|
||||||
}
|
}
|
||||||
|
@ -915,6 +916,10 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa
|
||||||
events.trigger(self, 'pause');
|
events.trigger(self, 'pause');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onWaiting() {
|
||||||
|
events.trigger(self, 'waiting');
|
||||||
|
}
|
||||||
|
|
||||||
function onError() {
|
function onError() {
|
||||||
var errorCode = this.error ? (this.error.code || 0) : 0;
|
var errorCode = this.error ? (this.error.code || 0) : 0;
|
||||||
var errorMessage = this.error ? (this.error.message || '') : '';
|
var errorMessage = this.error ? (this.error.message || '') : '';
|
||||||
|
@ -1348,6 +1353,7 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa
|
||||||
videoElement.addEventListener('play', onPlay);
|
videoElement.addEventListener('play', onPlay);
|
||||||
videoElement.addEventListener('click', onClick);
|
videoElement.addEventListener('click', onClick);
|
||||||
videoElement.addEventListener('dblclick', onDblClick);
|
videoElement.addEventListener('dblclick', onDblClick);
|
||||||
|
videoElement.addEventListener('waiting', onWaiting);
|
||||||
|
|
||||||
document.body.insertBefore(dlg, document.body.firstChild);
|
document.body.insertBefore(dlg, document.body.firstChild);
|
||||||
videoDialog = dlg;
|
videoDialog = dlg;
|
||||||
|
@ -1436,6 +1442,10 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa
|
||||||
list.push('AirPlay');
|
list.push('AirPlay');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof video.playbackRate === "number") {
|
||||||
|
list.push("PlaybackRate");
|
||||||
|
}
|
||||||
|
|
||||||
list.push('SetBrightness');
|
list.push('SetBrightness');
|
||||||
list.push('SetAspectRatio');
|
list.push('SetAspectRatio');
|
||||||
|
|
||||||
|
@ -1656,6 +1666,21 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
HtmlVideoPlayer.prototype.setPlaybackRate = function (value) {
|
||||||
|
var mediaElement = this._mediaElement;
|
||||||
|
if (mediaElement) {
|
||||||
|
mediaElement.playbackRate = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
HtmlVideoPlayer.prototype.getPlaybackRate = function () {
|
||||||
|
var mediaElement = this._mediaElement;
|
||||||
|
if (mediaElement) {
|
||||||
|
return mediaElement.playbackRate;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
HtmlVideoPlayer.prototype.setVolume = function (val) {
|
HtmlVideoPlayer.prototype.setVolume = function (val) {
|
||||||
var mediaElement = this._mediaElement;
|
var mediaElement = this._mediaElement;
|
||||||
if (mediaElement) {
|
if (mediaElement) {
|
||||||
|
|
|
@ -3777,6 +3777,24 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
PlaybackManager.prototype.setPlaybackRate = function (value, player) {
|
||||||
|
player = player || this._currentPlayer;
|
||||||
|
|
||||||
|
if (player) {
|
||||||
|
player.setPlaybackRate(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
PlaybackManager.prototype.getPlaybackRate = function (player) {
|
||||||
|
player = player || this._currentPlayer;
|
||||||
|
|
||||||
|
if (player) {
|
||||||
|
return player.getPlaybackRate(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
PlaybackManager.prototype.instantMix = function (item, player) {
|
PlaybackManager.prototype.instantMix = function (item, player) {
|
||||||
|
|
||||||
player = player || this._currentPlayer;
|
player = player || this._currentPlayer;
|
||||||
|
@ -3887,6 +3905,9 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla
|
||||||
if (player.supports('SetAspectRatio')) {
|
if (player.supports('SetAspectRatio')) {
|
||||||
list.push('SetAspectRatio');
|
list.push('SetAspectRatio');
|
||||||
}
|
}
|
||||||
|
if (player.supports('PlaybackRate')) {
|
||||||
|
list.push('PlaybackRate');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
define(['events', 'globalize', 'playbackManager', 'connectionManager', 'playMethodHelper', 'layoutManager', 'serverNotifications', 'paper-icon-button-light', 'css!./playerstats'], function (events, globalize, playbackManager, connectionManager, playMethodHelper, layoutManager, serverNotifications) {
|
define(['events', 'globalize', 'playbackManager', 'connectionManager', 'syncplayManager', 'playMethodHelper', 'layoutManager', 'serverNotifications', 'paper-icon-button-light', 'css!./playerstats'], function (events, globalize, playbackManager, connectionManager, syncplayManager, playMethodHelper, layoutManager, serverNotifications) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function init(instance) {
|
function init(instance) {
|
||||||
|
@ -327,6 +327,28 @@ define(['events', 'globalize', 'playbackManager', 'connectionManager', 'playMeth
|
||||||
return sessionStats;
|
return sessionStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSyncplayStats() {
|
||||||
|
var syncStats = [];
|
||||||
|
var stats = syncplayManager.getStats();
|
||||||
|
|
||||||
|
syncStats.push({
|
||||||
|
label: globalize.translate("LabelSyncplayTimeDiff"),
|
||||||
|
value: stats.TimeDiff + "ms"
|
||||||
|
});
|
||||||
|
|
||||||
|
syncStats.push({
|
||||||
|
label: globalize.translate("LabelSyncplayPlaybackDiff"),
|
||||||
|
value: stats.PlaybackDiff + "ms"
|
||||||
|
});
|
||||||
|
|
||||||
|
syncStats.push({
|
||||||
|
label: globalize.translate("LabelSyncplaySyncMethod"),
|
||||||
|
value: stats.SyncMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
return syncStats;
|
||||||
|
}
|
||||||
|
|
||||||
function getStats(instance, player) {
|
function getStats(instance, player) {
|
||||||
|
|
||||||
var statsPromise = player.getStats ? player.getStats() : Promise.resolve({});
|
var statsPromise = player.getStats ? player.getStats() : Promise.resolve({});
|
||||||
|
@ -383,6 +405,13 @@ define(['events', 'globalize', 'playbackManager', 'connectionManager', 'playMeth
|
||||||
name: 'Original Media Info'
|
name: 'Original Media Info'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (syncplayManager.isSyncplayEnabled()) {
|
||||||
|
categories.push({
|
||||||
|
stats: getSyncplayStats(),
|
||||||
|
name: 'Syncplay Info'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve(categories);
|
return Promise.resolve(categories);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
define(['connectionManager', 'playbackManager', 'events', 'inputManager', 'focusManager', 'appRouter'], function (connectionManager, playbackManager, events, inputManager, focusManager, appRouter) {
|
define(['connectionManager', 'playbackManager', 'syncplayManager', 'events', 'inputManager', 'focusManager', 'appRouter'], function (connectionManager, playbackManager, syncplayManager, events, inputManager, focusManager, appRouter) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var serverNotifications = {};
|
var serverNotifications = {};
|
||||||
|
@ -187,6 +187,10 @@ define(['connectionManager', 'playbackManager', 'events', 'inputManager', 'focus
|
||||||
events.trigger(serverNotifications, 'UserDataChanged', [apiClient, msg.Data.UserDataList[i]]);
|
events.trigger(serverNotifications, 'UserDataChanged', [apiClient, msg.Data.UserDataList[i]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (msg.MessageType === "SyncplayCommand") {
|
||||||
|
syncplayManager.processCommand(msg.Data, apiClient);
|
||||||
|
} else if (msg.MessageType === "SyncplayGroupUpdate") {
|
||||||
|
syncplayManager.processGroupUpdate(msg.Data, apiClient);
|
||||||
} else {
|
} else {
|
||||||
events.trigger(serverNotifications, msg.MessageType, [apiClient, msg.Data]);
|
events.trigger(serverNotifications, msg.MessageType, [apiClient, msg.Data]);
|
||||||
}
|
}
|
||||||
|
|
140
src/components/syncplay/groupSelectionMenu.js
Normal file
140
src/components/syncplay/groupSelectionMenu.js
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
define(['events', 'loading', 'connectionManager', 'playbackManager', 'syncplayManager', 'globalize', 'datetime'], function (events, loading, connectionManager, playbackManager, syncplayManager, globalize, datetime) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function getActivePlayerId() {
|
||||||
|
var info = playbackManager.getPlayerInfo();
|
||||||
|
return info ? info.id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyCallback() {
|
||||||
|
// avoid console logs about uncaught promises
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNewJoinGroupSelection(button) {
|
||||||
|
var apiClient = connectionManager.currentApiClient();
|
||||||
|
var sessionId = getActivePlayerId();
|
||||||
|
sessionId = sessionId ? sessionId : "none";
|
||||||
|
|
||||||
|
loading.show();
|
||||||
|
|
||||||
|
apiClient.sendSyncplayCommand(sessionId, "ListGroups").then(function (response) {
|
||||||
|
response.json().then(function (groups) {
|
||||||
|
var inSession = sessionId !== "none";
|
||||||
|
|
||||||
|
var menuItems = groups.map(function (group) {
|
||||||
|
var name = datetime.getDisplayRunningTime(group.PositionTicks);
|
||||||
|
if (!inSession) {
|
||||||
|
name = group.PlayingItemName
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: name,
|
||||||
|
icon: "group",
|
||||||
|
id: group.GroupId,
|
||||||
|
selected: false,
|
||||||
|
secondaryText: group.Partecipants.join(", ")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (inSession) {
|
||||||
|
menuItems.push({
|
||||||
|
name: globalize.translate('LabelSyncplayNewGroup'),
|
||||||
|
icon: "add",
|
||||||
|
id: "new-group",
|
||||||
|
selected: true,
|
||||||
|
secondaryText: globalize.translate('LabelSyncplayNewGroupDescription')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuItems.length === 0) {
|
||||||
|
require(['toast'], function (alert) {
|
||||||
|
alert({
|
||||||
|
title: globalize.translate('MessageSyncplayNoGroupsAvailable'),
|
||||||
|
text: globalize.translate('MessageSyncplayNoGroupsAvailable')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
loading.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
require(['actionsheet'], function (actionsheet) {
|
||||||
|
|
||||||
|
loading.hide();
|
||||||
|
|
||||||
|
var menuOptions = {
|
||||||
|
title: globalize.translate('HeaderSyncplaySelectGroup'),
|
||||||
|
items: menuItems,
|
||||||
|
positionTo: button,
|
||||||
|
resolveOnClick: true,
|
||||||
|
border: true
|
||||||
|
};
|
||||||
|
|
||||||
|
actionsheet.show(menuOptions).then(function (id) {
|
||||||
|
|
||||||
|
if (id == "new-group") {
|
||||||
|
apiClient.sendSyncplayCommand(sessionId, "NewGroup");
|
||||||
|
} else {
|
||||||
|
apiClient.sendSyncplayCommand(sessionId, "JoinGroup", {
|
||||||
|
GroupId: id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, emptyCallback);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).catch(function (error) {
|
||||||
|
loading.hide();
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLeaveGroupSelection(button) {
|
||||||
|
var apiClient = connectionManager.currentApiClient();
|
||||||
|
var sessionId = getActivePlayerId();
|
||||||
|
|
||||||
|
loading.show();
|
||||||
|
|
||||||
|
var menuItems = [{
|
||||||
|
name: globalize.translate('LabelSyncplayLeaveGroup'),
|
||||||
|
icon: "meeting_room",
|
||||||
|
id: "leave-group",
|
||||||
|
selected: true,
|
||||||
|
secondaryText: globalize.translate('LabelSyncplayLeaveGroupDescription')
|
||||||
|
}];
|
||||||
|
|
||||||
|
require(['actionsheet'], function (actionsheet) {
|
||||||
|
|
||||||
|
loading.hide();
|
||||||
|
|
||||||
|
var menuOptions = {
|
||||||
|
title: globalize.translate('HeaderSyncplayEnabled'),
|
||||||
|
items: menuItems,
|
||||||
|
positionTo: button,
|
||||||
|
resolveOnClick: true,
|
||||||
|
border: true
|
||||||
|
};
|
||||||
|
|
||||||
|
actionsheet.show(menuOptions).then(function (id) {
|
||||||
|
if (id == "leave-group") {
|
||||||
|
apiClient.sendSyncplayCommand(sessionId, "LeaveGroup");
|
||||||
|
}
|
||||||
|
}, emptyCallback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showGroupSelection(button) {
|
||||||
|
if (syncplayEnabled) {
|
||||||
|
showLeaveGroupSelection(button);
|
||||||
|
} else {
|
||||||
|
showNewJoinGroupSelection(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var syncplayEnabled = false;
|
||||||
|
|
||||||
|
events.on(syncplayManager, 'SyncplayEnabled', function (e, enabled) {
|
||||||
|
syncplayEnabled = enabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: showGroupSelection
|
||||||
|
};
|
||||||
|
});
|
600
src/components/syncplay/syncplaymanager.js
Normal file
600
src/components/syncplay/syncplaymanager.js
Normal file
|
@ -0,0 +1,600 @@
|
||||||
|
define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager'], function (events, globalize, loading, connectionManager, playbackManager) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function waitForEvent(emitter, eventType) {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
var callback = function () {
|
||||||
|
events.off(emitter, eventType, callback);
|
||||||
|
resolve(arguments);
|
||||||
|
}
|
||||||
|
events.on(emitter, eventType, callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displaySyncplayUpdate(message) {
|
||||||
|
require(['toast'], function (alert) {
|
||||||
|
alert({
|
||||||
|
title: message.Header,
|
||||||
|
text: message.Text
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActivePlayerId() {
|
||||||
|
var info = playbackManager.getPlayerInfo();
|
||||||
|
return info ? info.id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function millisecondsToTicks(milliseconds) {
|
||||||
|
return milliseconds * 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ticksToMilliseconds(ticks) {
|
||||||
|
return ticks / 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SyncplayManager() {
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
function onPlayerChange() {
|
||||||
|
bindToPlayer(playbackManager.getCurrentPlayer());
|
||||||
|
events.trigger(self, "PlayerChange", [self.currentPlayer]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPlayPauseStateChanged(e) {
|
||||||
|
events.trigger(self, "PlayPauseStateChange", [self.currentPlayer]);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.playbackRateSupported = false;
|
||||||
|
self.syncEnabled = false;
|
||||||
|
self.maxAcceptedDelaySpeedToSync = 50; // milliseconds
|
||||||
|
self.maxAcceptedDelaySkipToSync = 300; // milliseconds
|
||||||
|
self.syncMethodThreshold = 2000; // milliseconds
|
||||||
|
self.speedUpToSyncTime = 1000; // milliseconds
|
||||||
|
self.playbackDiffMillis = 0; // used for stats
|
||||||
|
self.syncMethod = "None"; // used for stats
|
||||||
|
|
||||||
|
function onTimeUpdate(e) {
|
||||||
|
events.trigger(self, "TimeUpdate", [e]);
|
||||||
|
|
||||||
|
if (self.lastCommand && self.lastCommand.Command === 'Play' && !self.isBuffering()) {
|
||||||
|
var currentTime = new Date();
|
||||||
|
var playAtTime = self.lastCommand.When;
|
||||||
|
|
||||||
|
var state = playbackManager.getPlayerState().PlayState;
|
||||||
|
// Estimate PositionTicks on server
|
||||||
|
var ServerPositionTicks = self.lastCommand.PositionTicks + ((currentTime - playAtTime) - self.timeDiff) * 10000;
|
||||||
|
// Measure delay that needs to be recovered
|
||||||
|
// diff might be caused by the player internally starting the playback
|
||||||
|
var diff = ServerPositionTicks - state.PositionTicks;
|
||||||
|
var diffMillis = diff / 10000;
|
||||||
|
|
||||||
|
self.playbackDiffMillis = diffMillis;
|
||||||
|
|
||||||
|
// console.debug("Syncplay onTimeUpdate", diffMillis, state.PositionTicks, ServerPositionTicks);
|
||||||
|
|
||||||
|
if (self.syncEnabled) {
|
||||||
|
var absDiffMillis = Math.abs(diffMillis);
|
||||||
|
// TODO: SpeedToSync sounds bad on songs
|
||||||
|
if (self.playbackRateSupported && absDiffMillis > self.maxAcceptedDelaySpeedToSync && absDiffMillis < self.syncMethodThreshold) {
|
||||||
|
// SpeedToSync method
|
||||||
|
var speed = 1 + diffMillis / self.speedUpToSyncTime;
|
||||||
|
|
||||||
|
self.currentPlayer.setPlaybackRate(speed);
|
||||||
|
self.syncEnabled = false;
|
||||||
|
self.showSyncIcon("SpeedToSync (x" + speed + ")");
|
||||||
|
|
||||||
|
self.syncTimeout = setTimeout(() => {
|
||||||
|
self.currentPlayer.setPlaybackRate(1);
|
||||||
|
self.syncEnabled = true;
|
||||||
|
self.clearSyncIcon();
|
||||||
|
}, self.speedUpToSyncTime);
|
||||||
|
} else if (absDiffMillis > self.maxAcceptedDelaySkipToSync) {
|
||||||
|
// SkipToSync method
|
||||||
|
playbackManager.syncplay_seek(ServerPositionTicks);
|
||||||
|
self.syncEnabled = false;
|
||||||
|
self.showSyncIcon("SkipToSync");
|
||||||
|
|
||||||
|
self.syncTimeout = setTimeout(() => {
|
||||||
|
self.syncEnabled = true;
|
||||||
|
self.clearSyncIcon();
|
||||||
|
}, self.syncMethodThreshold / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.lastPlaybackWaiting = null; // used to determine if player's buffering
|
||||||
|
self.minBufferingThresholdMillis = 1000;
|
||||||
|
|
||||||
|
// TODO: implement group wait
|
||||||
|
function onPlaying() {
|
||||||
|
self.lastPlaybackWaiting = null;
|
||||||
|
events.trigger(self, "PlayerPlaying");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement group wait
|
||||||
|
function onWaiting() {
|
||||||
|
if (!self.lastPlaybackWaiting) {
|
||||||
|
self.lastPlaybackWaiting = new Date();
|
||||||
|
}
|
||||||
|
events.trigger(self, "PlayerWaiting");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isBuffering = function () {
|
||||||
|
if (self.lastPlaybackWaiting === null) return false;
|
||||||
|
return (new Date() - self.lastPlaybackWaiting) > self.minBufferingThresholdMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindToPlayer(player) {
|
||||||
|
if (player !== self.currentPlayer) {
|
||||||
|
releaseCurrentPlayer();
|
||||||
|
self.currentPlayer = player;
|
||||||
|
if (!player) return;
|
||||||
|
}
|
||||||
|
events.on(player, "pause", onPlayPauseStateChanged);
|
||||||
|
events.on(player, "unpause", onPlayPauseStateChanged);
|
||||||
|
events.on(player, "timeupdate", onTimeUpdate);
|
||||||
|
events.on(player, "playing", onPlaying);
|
||||||
|
events.on(player, "waiting", onWaiting);
|
||||||
|
self.playbackRateSupported = player.supports("PlaybackRate");
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseCurrentPlayer() {
|
||||||
|
var player = self.currentPlayer;
|
||||||
|
if (player) {
|
||||||
|
events.off(player, "pause", onPlayPauseStateChanged);
|
||||||
|
events.off(player, "unpause", onPlayPauseStateChanged);
|
||||||
|
events.off(player, "timeupdate", onTimeUpdate);
|
||||||
|
events.off(player, "playing", onPlaying);
|
||||||
|
events.off(player, "waiting", onWaiting);
|
||||||
|
if (self.playbackRateSupported) {
|
||||||
|
player.setPlaybackRate(1);
|
||||||
|
}
|
||||||
|
self.currentPlayer = null;
|
||||||
|
self.playbackRateSupported = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.currentPlayer = null;
|
||||||
|
|
||||||
|
events.on(playbackManager, "playerchange", onPlayerChange);
|
||||||
|
bindToPlayer(playbackManager.getCurrentPlayer());
|
||||||
|
|
||||||
|
self.syncplayEnabledAt = null; // Server time of when Syncplay has been enabled
|
||||||
|
self.syncplayReady = false; // Syncplay is ready after first ping to server
|
||||||
|
|
||||||
|
self.processGroupUpdate = function (cmd, apiClient) {
|
||||||
|
switch (cmd.Type) {
|
||||||
|
case 'PrepareSession':
|
||||||
|
var serverId = apiClient.serverInfo().Id;
|
||||||
|
playbackManager.play({
|
||||||
|
ids: cmd.Data.ItemIds,
|
||||||
|
startPositionTicks: cmd.Data.StartPositionTicks,
|
||||||
|
mediaSourceId: cmd.Data.MediaSourceId,
|
||||||
|
audioStreamIndex: cmd.Data.AudioStreamIndex,
|
||||||
|
subtitleStreamIndex: cmd.Data.SubtitleStreamIndex,
|
||||||
|
startIndex: cmd.Data.StartIndex,
|
||||||
|
serverId: serverId
|
||||||
|
}).then(function () {
|
||||||
|
waitForEvent(self, "PlayerChange").then(function () {
|
||||||
|
playbackManager.pause();
|
||||||
|
var sessionId = getActivePlayerId();
|
||||||
|
if (!sessionId) {
|
||||||
|
console.error("Missing sessionId!");
|
||||||
|
displaySyncplayUpdate({
|
||||||
|
Text: "Failed to enable Syncplay!"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Sometimes JoinGroup fails, maybe because server hasn't been updated yet
|
||||||
|
setTimeout(() => {
|
||||||
|
apiClient.sendSyncplayCommand(sessionId, "JoinGroup", {
|
||||||
|
GroupId: cmd.GroupId
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'UserJoined':
|
||||||
|
displaySyncplayUpdate({
|
||||||
|
Text: globalize.translate('MessageSyncplayUserJoined', cmd.Data),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'UserLeft':
|
||||||
|
displaySyncplayUpdate({
|
||||||
|
Text: globalize.translate('MessageSyncplayUserLeft', cmd.Data),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'GroupJoined':
|
||||||
|
displaySyncplayUpdate({
|
||||||
|
Text: globalize.translate('MessageSyncplayEnabled'),
|
||||||
|
});
|
||||||
|
// Enable Syncplay
|
||||||
|
self.syncplayEnabledAt = new Date(cmd.Data);
|
||||||
|
self.syncplayReady = false;
|
||||||
|
events.trigger(self, "SyncplayEnabled", [true]);
|
||||||
|
waitForEvent(self, "SyncplayReady").then(function () {
|
||||||
|
self.processCommand(self.queuedCommand, apiClient);
|
||||||
|
self.queuedCommand = null;
|
||||||
|
});
|
||||||
|
self.injectPlaybackManager();
|
||||||
|
self.startPing();
|
||||||
|
break;
|
||||||
|
case 'NotInGroup':
|
||||||
|
case 'GroupLeft':
|
||||||
|
displaySyncplayUpdate({
|
||||||
|
Text: globalize.translate('MessageSyncplayDisabled'),
|
||||||
|
});
|
||||||
|
// Disable Syncplay
|
||||||
|
self.syncplayEnabledAt = null;
|
||||||
|
self.syncplayReady = false;
|
||||||
|
events.trigger(self, "SyncplayEnabled", [false]);
|
||||||
|
self.restorePlaybackManager();
|
||||||
|
self.stopPing();
|
||||||
|
break;
|
||||||
|
case 'GroupWait':
|
||||||
|
displaySyncplayUpdate({
|
||||||
|
Text: globalize.translate('MessageSyncplayGroupWait', cmd.Data),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'KeepAlive':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error('processSyncplayGroupUpdate does not recognize: ' + cmd.Type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.lastCommand = null;
|
||||||
|
self.queuedCommand = null;
|
||||||
|
|
||||||
|
self.processCommand = function (cmd, apiClient) {
|
||||||
|
if (cmd === null) return;
|
||||||
|
|
||||||
|
if (!self.isSyncplayEnabled()) {
|
||||||
|
console.debug("Syncplay processCommand: ignoring command", cmd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self.syncplayReady) {
|
||||||
|
console.debug("Syncplay processCommand: queued command", cmd);
|
||||||
|
self.queuedCommand = cmd;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.When = new Date(cmd.When);
|
||||||
|
|
||||||
|
if (cmd.When < self.syncplayEnabledAt) {
|
||||||
|
console.debug("Syncplay processCommand: ignoring old command", cmd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if new command differs from last one
|
||||||
|
if (self.lastCommand &&
|
||||||
|
self.lastCommand.When === cmd.When &&
|
||||||
|
self.lastCommand.PositionTicks === cmd.PositionTicks &&
|
||||||
|
self.Command === cmd.Command
|
||||||
|
) {
|
||||||
|
console.debug("Syncplay processCommand: ignoring duplicate command", cmd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.lastCommand = cmd;
|
||||||
|
console.log("Syncplay will", cmd.Command, "at", cmd.When, "PositionTicks", cmd.PositionTicks);
|
||||||
|
|
||||||
|
switch (cmd.Command) {
|
||||||
|
case 'Play':
|
||||||
|
self.schedulePlay(cmd.When, cmd.PositionTicks);
|
||||||
|
break;
|
||||||
|
case 'Pause':
|
||||||
|
self.schedulePause(cmd.When, cmd.PositionTicks);
|
||||||
|
break;
|
||||||
|
case 'Seek':
|
||||||
|
self.scheduleSeek(cmd.When, cmd.PositionTicks);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error('processSyncplayCommand does not recognize: ' + cmd.Type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.scheduledCommand = null;
|
||||||
|
self.syncTimeout = null;
|
||||||
|
|
||||||
|
self.schedulePlay = function (playAtTime, positionTicks) {
|
||||||
|
self.clearScheduledCommand();
|
||||||
|
var currentTime = new Date();
|
||||||
|
var playAtTimeLocal = self.serverDateToLocal(playAtTime);
|
||||||
|
|
||||||
|
if (playAtTimeLocal > currentTime) {
|
||||||
|
var playTimeout = (playAtTimeLocal - currentTime) - self.playerDelay;
|
||||||
|
playbackManager.syncplay_seek(positionTicks);
|
||||||
|
|
||||||
|
self.scheduledCommand = setTimeout(() => {
|
||||||
|
playbackManager.syncplay_unpause();
|
||||||
|
|
||||||
|
self.syncTimeout = setTimeout(() => {
|
||||||
|
self.syncEnabled = true
|
||||||
|
}, self.syncMethodThreshold / 2);
|
||||||
|
|
||||||
|
}, playTimeout);
|
||||||
|
|
||||||
|
// console.debug("Syncplay schedulePlay:", playTimeout);
|
||||||
|
} else {
|
||||||
|
// Group playback already started
|
||||||
|
var serverPositionTicks = positionTicks + (currentTime - playAtTimeLocal) * 10000;
|
||||||
|
playbackManager.syncplay_unpause();
|
||||||
|
playbackManager.syncplay_seek(serverPositionTicks);
|
||||||
|
|
||||||
|
self.syncTimeout = setTimeout(() => {
|
||||||
|
self.syncEnabled = true
|
||||||
|
}, self.syncMethodThreshold / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.schedulePause = function (pauseAtTime, positionTicks) {
|
||||||
|
self.clearScheduledCommand();
|
||||||
|
var currentTime = new Date();
|
||||||
|
var pauseAtTimeLocal = self.serverDateToLocal(pauseAtTime);
|
||||||
|
|
||||||
|
if (pauseAtTimeLocal > currentTime) {
|
||||||
|
var pauseTimeout = (pauseAtTimeLocal - currentTime) - self.playerDelay;
|
||||||
|
|
||||||
|
self.scheduledCommand = setTimeout(() => {
|
||||||
|
playbackManager.syncplay_pause();
|
||||||
|
setTimeout(() => {
|
||||||
|
playbackManager.syncplay_seek(positionTicks);
|
||||||
|
}, 800);
|
||||||
|
|
||||||
|
}, pauseTimeout);
|
||||||
|
} else {
|
||||||
|
playbackManager.syncplay_pause();
|
||||||
|
setTimeout(() => {
|
||||||
|
playbackManager.syncplay_seek(positionTicks);
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.scheduleSeek = function (seekAtTime, positionTicks) {
|
||||||
|
self.schedulePause(seekAtTime, positionTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.clearScheduledCommand = function () {
|
||||||
|
clearTimeout(self.scheduledCommand);
|
||||||
|
clearTimeout(self.syncTimeout);
|
||||||
|
|
||||||
|
self.syncEnabled = false;
|
||||||
|
if (self.currentPlayer) {
|
||||||
|
self.currentPlayer.setPlaybackRate(1);
|
||||||
|
}
|
||||||
|
self.clearSyncIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.injectPlaybackManager = function () {
|
||||||
|
if (!self.isSyncplayEnabled()) return;
|
||||||
|
if (playbackManager.syncplayEnabled) return;
|
||||||
|
|
||||||
|
playbackManager.syncplay_unpause = playbackManager.unpause;
|
||||||
|
playbackManager.syncplay_pause = playbackManager.pause;
|
||||||
|
playbackManager.syncplay_seek = playbackManager.seek;
|
||||||
|
|
||||||
|
playbackManager.unpause = self.playRequest;
|
||||||
|
playbackManager.pause = self.pauseRequest;
|
||||||
|
playbackManager.seek = self.seekRequest;
|
||||||
|
playbackManager.syncplayEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.restorePlaybackManager = function () {
|
||||||
|
if (self.isSyncplayEnabled()) return;
|
||||||
|
if (!playbackManager.syncplayEnabled) return;
|
||||||
|
|
||||||
|
playbackManager.unpause = playbackManager.syncplay_unpause;
|
||||||
|
playbackManager.pause = playbackManager.syncplay_pause;
|
||||||
|
playbackManager.seek = playbackManager.syncplay_seek;
|
||||||
|
playbackManager.syncplayEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.playRequest = function (player) {
|
||||||
|
var apiClient = connectionManager.currentApiClient();
|
||||||
|
var sessionId = getActivePlayerId();
|
||||||
|
apiClient.sendSyncplayCommand(sessionId, "PlayRequest");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pauseRequest = function (player) {
|
||||||
|
var apiClient = connectionManager.currentApiClient();
|
||||||
|
var sessionId = getActivePlayerId();
|
||||||
|
apiClient.sendSyncplayCommand(sessionId, "PauseRequest");
|
||||||
|
// Pause locally as well, to give the user some little control
|
||||||
|
playbackManager.syncplay_pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.seekRequest = function (PositionTicks, player) {
|
||||||
|
var apiClient = connectionManager.currentApiClient();
|
||||||
|
var sessionId = getActivePlayerId();
|
||||||
|
apiClient.sendSyncplayCommand(sessionId, "SeekRequest", {
|
||||||
|
PositionTicks: PositionTicks
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pingIntervalTimeoutGreedy = 1000;
|
||||||
|
self.pingIntervalTimeoutLowProfile = 60000;
|
||||||
|
self.greedyPingCount = 3;
|
||||||
|
|
||||||
|
self.pingStop = true;
|
||||||
|
self.pingIntervalTimeout = self.pingIntervalTimeoutGreedy;
|
||||||
|
self.pingInterval = null;
|
||||||
|
self.initTimeDiff = 0; // number of pings
|
||||||
|
self.timeDiff = 0; // local time minus server time
|
||||||
|
self.roundTripDuration = 0;
|
||||||
|
self.notifySyncplayReady = false;
|
||||||
|
|
||||||
|
self.updateTimeDiff = function (pingStartTime, pingEndTime, serverTime) {
|
||||||
|
self.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) + (self.roundTripDuration / 2);
|
||||||
|
|
||||||
|
// Initial setup
|
||||||
|
if (self.initTimeDiff === 0) {
|
||||||
|
self.timeDiff = newTimeDiff;
|
||||||
|
self.initTimeDiff++
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// As response time gets better, absolute value should decrease
|
||||||
|
var distanceFromZero = Math.abs(newTimeDiff);
|
||||||
|
var oldDistanceFromZero = Math.abs(self.timeDiff);
|
||||||
|
if (distanceFromZero < oldDistanceFromZero) {
|
||||||
|
self.timeDiff = newTimeDiff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid overloading server
|
||||||
|
if (self.initTimeDiff >= self.greedyPingCount) {
|
||||||
|
self.pingIntervalTimeout = self.pingIntervalTimeoutLowProfile;
|
||||||
|
} else {
|
||||||
|
self.initTimeDiff++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.debug("Syncplay updateTimeDiff:", serverTime, self.timeDiff, self.roundTripDuration, newTimeDiff);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.requestPing = function () {
|
||||||
|
if (self.pingInterval === null && !self.pingStop) {
|
||||||
|
self.pingInterval = setTimeout(() => {
|
||||||
|
self.pingInterval = null;
|
||||||
|
|
||||||
|
var apiClient = connectionManager.currentApiClient();
|
||||||
|
var sessionId = getActivePlayerId();
|
||||||
|
|
||||||
|
var pingStartTime = new Date();
|
||||||
|
apiClient.sendSyncplayCommand(sessionId, "GetUtcTime").then(function (response) {
|
||||||
|
var pingEndTime = new Date();
|
||||||
|
response.text().then(function (utcTime) {
|
||||||
|
var serverTime = new Date(utcTime);
|
||||||
|
self.updateTimeDiff(pingStartTime, pingEndTime, serverTime);
|
||||||
|
|
||||||
|
// Alert user that ping is high
|
||||||
|
if (Math.abs(self.roundTripDuration) >= 1000) {
|
||||||
|
events.trigger(self, "SyncplayError", [true]);
|
||||||
|
} else {
|
||||||
|
events.trigger(self, "SyncplayError", [false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify server of ping
|
||||||
|
apiClient.sendSyncplayCommand(sessionId, "KeepAlive", {
|
||||||
|
Ping: (self.roundTripDuration / 2) + self.playerDelay
|
||||||
|
});
|
||||||
|
|
||||||
|
if (self.notifySyncplayReady) {
|
||||||
|
self.syncplayReady = true;
|
||||||
|
events.trigger(self, "SyncplayReady");
|
||||||
|
self.notifySyncplayReady = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.requestPing();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}, self.pingIntervalTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.startPing = function () {
|
||||||
|
self.notifySyncplayReady = true;
|
||||||
|
self.pingStop = false;
|
||||||
|
self.initTimeDiff = self.initTimeDiff > self.greedyPingCount ? 1 : self.initTimeDiff;
|
||||||
|
self.pingIntervalTimeout = self.pingIntervalTimeoutGreedy;
|
||||||
|
|
||||||
|
self.requestPing();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.stopPing = function () {
|
||||||
|
self.pingStop = true;
|
||||||
|
if (self.pingInterval !== null) {
|
||||||
|
clearTimeout(self.pingInterval);
|
||||||
|
self.pingInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.serverDateToLocal = function (server) {
|
||||||
|
// local - server = diff
|
||||||
|
return new Date(server.getTime() + self.timeDiff);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.localDateToServer = function (local) {
|
||||||
|
// local - server = diff
|
||||||
|
return new Date(local.getTime() - self.timeDiff);
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FEATURE IS CURRENTLY DISABLED
|
||||||
|
// Mainly because SpeedToSync seems to do the job
|
||||||
|
// Also because the delay is unreliable and different every time
|
||||||
|
self.playerDelay = 0;
|
||||||
|
self.playerDelayMeasured = true; // disable this feature
|
||||||
|
self.measurePlayerDelay = function (positionTicks) {
|
||||||
|
if (self.playerDelayMeasured) {
|
||||||
|
playbackManager.syncplay_seek(positionTicks);
|
||||||
|
} else {
|
||||||
|
// Measure playerDelay by issuing a play command
|
||||||
|
// followed by a pause command after one second
|
||||||
|
// PositionTicks should be at 1 second minus two times the player delay
|
||||||
|
loading.show();
|
||||||
|
self.currentPlayer.setPlaybackRate(1);
|
||||||
|
playbackManager.syncplay_seek(0);
|
||||||
|
// Wait for player to seek
|
||||||
|
setTimeout(() => {
|
||||||
|
playbackManager.syncplay_unpause();
|
||||||
|
// Play one second of media
|
||||||
|
setTimeout(() => {
|
||||||
|
playbackManager.syncplay_pause();
|
||||||
|
// Wait for state to get update
|
||||||
|
setTimeout(() => {
|
||||||
|
var state = playbackManager.getPlayerState().PlayState;
|
||||||
|
var delayTicks = millisecondsToTicks(1000) - state.PositionTicks;
|
||||||
|
var delayMillis = ticksToMilliseconds(delayTicks);
|
||||||
|
self.playerDelay = delayMillis / 2;
|
||||||
|
// Make sure delay is not negative
|
||||||
|
self.playerDelay = self.playerDelay > 0 ? self.playerDelay : 0;
|
||||||
|
self.playerDelayMeasured = true;
|
||||||
|
// console.debug("Syncplay PlayerDelay:", self.playerDelay);
|
||||||
|
// Restore player
|
||||||
|
setTimeout(() => {
|
||||||
|
playbackManager.syncplay_seek(positionTicks);
|
||||||
|
loading.hide();
|
||||||
|
}, 800);
|
||||||
|
}, 1000);
|
||||||
|
}, 1000);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
self.isSyncplayEnabled = function () {
|
||||||
|
return self.syncplayEnabledAt !== null ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.getStats = function () {
|
||||||
|
return {
|
||||||
|
TimeDiff: self.timeDiff,
|
||||||
|
PlaybackDiff: self.playbackDiffMillis,
|
||||||
|
SyncMethod: self.syncMethod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI
|
||||||
|
self.showSyncIcon = function (syncMethod) {
|
||||||
|
self.syncMethod = syncMethod;
|
||||||
|
events.trigger(self, "SyncplayError", [true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.clearSyncIcon = function () {
|
||||||
|
self.syncMethod = "None";
|
||||||
|
events.trigger(self, "SyncplayError", [false]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SyncplayManager();
|
||||||
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', 'viewManager', 'libraryBrowser', 'appRouter', 'apphost', 'playbackManager', 'browser', 'globalize', 'scripts/imagehelper', 'paper-icon-button-light', 'material-icons', 'scrollStyles', 'flexStyles'], function (dom, layoutManager, inputManager, connectionManager, events, viewManager, libraryBrowser, appRouter, appHost, playbackManager, browser, globalize, imageHelper) {
|
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) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function renderHeader() {
|
function renderHeader() {
|
||||||
|
@ -12,6 +12,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', '
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
html += '<div class="headerRight">';
|
html += '<div class="headerRight">';
|
||||||
html += '<span class="headerSelectedPlayer"></span>';
|
html += '<span class="headerSelectedPlayer"></span>';
|
||||||
|
html += '<button is="paper-icon-button-light" class="headerSyncButton syncButton headerButton headerButtonRight hide"><span class="material-icons sync_disabled"></span></button>';
|
||||||
html += '<button is="paper-icon-button-light" class="headerAudioPlayerButton audioPlayerButton headerButton headerButtonRight hide"><span class="material-icons music_note"></span></button>';
|
html += '<button is="paper-icon-button-light" class="headerAudioPlayerButton audioPlayerButton headerButton headerButtonRight hide"><span class="material-icons music_note"></span></button>';
|
||||||
html += '<button is="paper-icon-button-light" class="headerCastButton castButton headerButton headerButtonRight hide"><span class="material-icons cast"></span></button>';
|
html += '<button is="paper-icon-button-light" class="headerCastButton castButton headerButton headerButtonRight hide"><span class="material-icons cast"></span></button>';
|
||||||
html += '<button type="button" is="paper-icon-button-light" class="headerButton headerButtonRight headerSearchButton hide"><span class="material-icons search"></span></button>';
|
html += '<button type="button" is="paper-icon-button-light" class="headerButton headerButtonRight headerSearchButton hide"><span class="material-icons search"></span></button>';
|
||||||
|
@ -30,6 +31,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', '
|
||||||
headerCastButton = skinHeader.querySelector('.headerCastButton');
|
headerCastButton = skinHeader.querySelector('.headerCastButton');
|
||||||
headerAudioPlayerButton = skinHeader.querySelector('.headerAudioPlayerButton');
|
headerAudioPlayerButton = skinHeader.querySelector('.headerAudioPlayerButton');
|
||||||
headerSearchButton = skinHeader.querySelector('.headerSearchButton');
|
headerSearchButton = skinHeader.querySelector('.headerSearchButton');
|
||||||
|
headerSyncButton = skinHeader.querySelector('.headerSyncButton');
|
||||||
|
|
||||||
lazyLoadViewMenuBarImages();
|
lazyLoadViewMenuBarImages();
|
||||||
bindMenuEvents();
|
bindMenuEvents();
|
||||||
|
@ -93,6 +95,8 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', '
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
headerSyncButton.classList.remove("hide");
|
||||||
|
|
||||||
requiresUserRefresh = false;
|
requiresUserRefresh = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,6 +151,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', '
|
||||||
}
|
}
|
||||||
|
|
||||||
headerAudioPlayerButton.addEventListener('click', showAudioPlayer);
|
headerAudioPlayerButton.addEventListener('click', showAudioPlayer);
|
||||||
|
headerSyncButton.addEventListener('click', onSyncButtonClicked);
|
||||||
|
|
||||||
if (layoutManager.mobile) {
|
if (layoutManager.mobile) {
|
||||||
initHeadRoom(skinHeader);
|
initHeadRoom(skinHeader);
|
||||||
|
@ -177,6 +182,32 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', '
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSyncButtonClicked() {
|
||||||
|
var btn = this;
|
||||||
|
|
||||||
|
require(["groupSelectionMenu"], function (groupSelectionMenu) {
|
||||||
|
groupSelectionMenu.show(btn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSyncplayIcon(event, enabled) {
|
||||||
|
var icon = headerSyncButton.querySelector("i");
|
||||||
|
if (enabled) {
|
||||||
|
icon.innerHTML = "sync";
|
||||||
|
} else {
|
||||||
|
icon.innerHTML = "sync_disabled";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSyncplayErrorIcon(event, show_error) {
|
||||||
|
var icon = headerSyncButton.querySelector("i");
|
||||||
|
if (show_error) {
|
||||||
|
icon.innerHTML = "sync_problem";
|
||||||
|
} else {
|
||||||
|
icon.innerHTML = "sync";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getItemHref(item, context) {
|
function getItemHref(item, context) {
|
||||||
return appRouter.getRouteUrl(item, {
|
return appRouter.getRouteUrl(item, {
|
||||||
context: context
|
context: context
|
||||||
|
@ -799,6 +830,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', '
|
||||||
var headerCastButton;
|
var headerCastButton;
|
||||||
var headerSearchButton;
|
var headerSearchButton;
|
||||||
var headerAudioPlayerButton;
|
var headerAudioPlayerButton;
|
||||||
|
var headerSyncButton;
|
||||||
var enableLibraryNavDrawer = layoutManager.desktop;
|
var enableLibraryNavDrawer = layoutManager.desktop;
|
||||||
var skinHeader = document.querySelector('.skinHeader');
|
var skinHeader = document.querySelector('.skinHeader');
|
||||||
var requiresUserRefresh = true;
|
var requiresUserRefresh = true;
|
||||||
|
@ -931,6 +963,8 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', '
|
||||||
updateUserInHeader();
|
updateUserInHeader();
|
||||||
});
|
});
|
||||||
events.on(playbackManager, 'playerchange', updateCastIcon);
|
events.on(playbackManager, 'playerchange', updateCastIcon);
|
||||||
|
events.on(syncplayManager, 'SyncplayEnabled', updateSyncplayIcon);
|
||||||
|
events.on(syncplayManager, 'SyncplayError', updateSyncplayErrorIcon);
|
||||||
loadNavDrawer();
|
loadNavDrawer();
|
||||||
return LibraryMenu;
|
return LibraryMenu;
|
||||||
});
|
});
|
||||||
|
|
|
@ -817,6 +817,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('syncplayManager', [componentsPath + '/syncplay/syncplaymanager'], returnFirstDependency);
|
||||||
define('layoutManager', [componentsPath + '/layoutManager', 'apphost'], getLayoutManager);
|
define('layoutManager', [componentsPath + '/layoutManager', 'apphost'], getLayoutManager);
|
||||||
define('homeSections', [componentsPath + '/homesections/homesections'], returnFirstDependency);
|
define('homeSections', [componentsPath + '/homesections/homesections'], returnFirstDependency);
|
||||||
define('playMenu', [componentsPath + '/playmenu'], returnFirstDependency);
|
define('playMenu', [componentsPath + '/playmenu'], returnFirstDependency);
|
||||||
|
|
|
@ -491,6 +491,8 @@
|
||||||
"HeaderSubtitleProfile": "Subtitle Profile",
|
"HeaderSubtitleProfile": "Subtitle Profile",
|
||||||
"HeaderSubtitleProfiles": "Subtitle Profiles",
|
"HeaderSubtitleProfiles": "Subtitle Profiles",
|
||||||
"HeaderSubtitleProfilesHelp": "Subtitle profiles describe the subtitle formats supported by the device.",
|
"HeaderSubtitleProfilesHelp": "Subtitle profiles describe the subtitle formats supported by the device.",
|
||||||
|
"HeaderSyncplaySelectGroup": "Join a group",
|
||||||
|
"HeaderSyncplayEnabled": "Syncplay enabled",
|
||||||
"HeaderSystemDlnaProfiles": "System Profiles",
|
"HeaderSystemDlnaProfiles": "System Profiles",
|
||||||
"HeaderTags": "Tags",
|
"HeaderTags": "Tags",
|
||||||
"HeaderTaskTriggers": "Task Triggers",
|
"HeaderTaskTriggers": "Task Triggers",
|
||||||
|
@ -853,6 +855,13 @@
|
||||||
"LabelSubtitlePlaybackMode": "Subtitle mode:",
|
"LabelSubtitlePlaybackMode": "Subtitle mode:",
|
||||||
"LabelSubtitles": "Subtitles",
|
"LabelSubtitles": "Subtitles",
|
||||||
"LabelSupportedMediaTypes": "Supported Media Types:",
|
"LabelSupportedMediaTypes": "Supported Media Types:",
|
||||||
|
"LabelSyncplayTimeDiff": "Time difference with server:",
|
||||||
|
"LabelSyncplayPlaybackDiff": "Playback time difference:",
|
||||||
|
"LabelSyncplaySyncMethod": "Sync method:",
|
||||||
|
"LabelSyncplayNewGroup": "New group",
|
||||||
|
"LabelSyncplayNewGroupDescription": "Create a new group",
|
||||||
|
"LabelSyncplayLeaveGroup": "Leave group",
|
||||||
|
"LabelSyncplayLeaveGroupDescription": "Disable Syncplay",
|
||||||
"LabelTVHomeScreen": "TV mode home screen:",
|
"LabelTVHomeScreen": "TV mode home screen:",
|
||||||
"LabelTag": "Tag:",
|
"LabelTag": "Tag:",
|
||||||
"LabelTagline": "Tagline:",
|
"LabelTagline": "Tagline:",
|
||||||
|
@ -1016,6 +1025,12 @@
|
||||||
"MessageUnableToConnectToServer": "We're unable to connect to the selected server right now. Please ensure it is running and try again.",
|
"MessageUnableToConnectToServer": "We're unable to connect to the selected server right now. Please ensure it is running and try again.",
|
||||||
"MessageUnsetContentHelp": "Content will be displayed as plain folders. For best results use the metadata manager to set the content types of sub-folders.",
|
"MessageUnsetContentHelp": "Content will be displayed as plain folders. For best results use the metadata manager to set the content types of sub-folders.",
|
||||||
"MessageYouHaveVersionInstalled": "You currently have version {0} installed.",
|
"MessageYouHaveVersionInstalled": "You currently have version {0} installed.",
|
||||||
|
"MessageSyncplayEnabled": "Syncplay enabled.",
|
||||||
|
"MessageSyncplayDisabled": "Syncplay disabled.",
|
||||||
|
"MessageSyncplayUserJoined": "<b>{0}</b> joined group.",
|
||||||
|
"MessageSyncplayUserLeft": "<b>{0}</b> left group.",
|
||||||
|
"MessageSyncplayGroupWait": "<b>{0}</b> is buffering...",
|
||||||
|
"MessageSyncplayNoGroupsAvailable": "No groups available.",
|
||||||
"Metadata": "Metadata",
|
"Metadata": "Metadata",
|
||||||
"MetadataManager": "Metadata Manager",
|
"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.",
|
"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.",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue