Much progress on sync logic but there will need to be so much testing

This commit is contained in:
Travis Shivers 2020-06-18 19:32:08 -05:00
parent dd53064ae7
commit 0eef02afa1
10 changed files with 147 additions and 74 deletions

View File

@ -146,57 +146,6 @@ class PlexClient {
}
return this.seekTo(time);
}
async sync(hostTime, SYNCFLEXIBILITY, SYNCMODE, POLLINTERVAL) {
const hostTimeline = hostTime;
if (this.clientIdentifier === 'PTPLAYER9PLUS10') {
await this.getTimeline();
}
const lastCommandTime = Math.abs(this.lastSyncCommand - new Date().getTime());
if (this.lastSyncCommand && this.clientIdentifier !== 'PTPLAYER9PLUS10' && lastCommandTime < POLLINTERVAL) {
throw new Error('too soon for another sync command');
}
const lagTime = Math.abs(hostTimeline.recievedAt - new Date().getTime());
if (lagTime) {
hostTimeline.time += lagTime;
}
const timelineAge = new Date().getTime() - this.lastTimelineObject.recievedAt;
const ourTime = parseInt(this.lastTimelineObject.time, 10) + parseInt(timelineAge, 10);
const difference = Math.abs((parseInt(ourTime, 10)) - parseInt(hostTimeline.time, 10));
// console.log('Difference with host is', difference);
const bothPaused = hostTimeline.playerState === 'paused' && this.lastTimelineObject.state === 'paused';
if (parseInt(difference, 10) > parseInt(SYNCFLEXIBILITY, 10)
|| (bothPaused && difference > 10)) {
// We need to seek!
this.lastSyncCommand = new Date().getTime();
// Decide what seeking method we want to use
if (SYNCMODE === 'cleanseek' || hostTimeline.playerState === 'paused') {
return this.cleanSeek(hostTimeline.time);
}
if (SYNCMODE === 'skipahead') {
return this.skipAhead(hostTimeline.time, 10000);
}
// Fall back to skipahead
return this.skipAhead(hostTimeline.time, 10000);
}
// Calc the average delay of the last 10 host timeline updates
// We do this to avoid any issues with random lag spikes
this.differenceCache.unshift(difference);
if (this.differenceCache.length > 5) {
this.differenceCache.pop();
}
let total = 0;
for (let i = 0; i < this.differenceCache.length; i += 1) {
total += this.differenceCache[i];
}
const avg = total / this.differenceCache.length;
if (this.clientIdentifier === 'PTPLAYER9PLUS10' && avg > 1500) {
console.log('Soft syncing because difference is', difference);
return this.cleanSeek(hostTimeline.time, true);
}
return 'No sync needed';
}
}
export default PlexClient;

View File

@ -79,12 +79,28 @@ export default {
}
},
FETCH_CHOSEN_CLIENT_TIMELINE: async ({ getters }) => {
const { data } = await getters.GET_CHOSEN_PLEX_CLIENT_AXIOS.get('/player/timeline/poll', {
SEND_CLIENT_REQUEST: async ({ getters, commit }, { path, params }) => {
const { data } = await getters.GET_CHOSEN_PLEX_CLIENT_AXIOS.get(path, {
params: {
commandId: getters.GET_COMMAND_ID,
type: 'video',
...params,
},
transformResponse: xmlutils.parseXML,
});
// TODO: maybe potentially increment it even if it fails
commit('INCREMENT_COMMAND_ID');
return data;
},
FETCH_CHOSEN_CLIENT_TIMELINE: async ({ dispatch }) => {
const data = await dispatch('SEND_CLIENT_REQUEST', {
url: '/player/timeline/poll',
params: {
wait: 0,
},
transformResponse: xmlutils.parseXML,
});
const videoTimeline = data.MediaContainer[0].Timeline.find((timeline) => timeline.type === 'video');
@ -94,6 +110,7 @@ export default {
time: parseInt(videoTimeline.time, 10),
duration: parseInt(videoTimeline.duration, 10),
receivedAt: Date.now(),
commandID: parseInt(data.MediaContainer[0].commandID, 10),
};
},
@ -163,4 +180,79 @@ export default {
machineIdentifier: getters.GET_ACTIVE_SERVER_ID,
};
},
SYNC: async ({
getters, dispatch, commit, rootGetters,
}) => {
if (getters.GET_CHOSEN_CLIENT_ID !== 'PTPLAYER9PLUS10'
&& (!getters.GET_PREVIOUS_SYNC_TIMELINE_COMMAND_ID
|| getters.GET_PLEX_CLIENT_TIMELINE.commandID
<= getters.GET_PREVIOUS_SYNC_TIMELINE_COMMAND_ID)) {
// TODO: examine if I should throw error or handle it another way
throw new Error('Already synced with this timeline. Need to wait for new one to sync again');
}
const adjustedHostTime = rootGetters['synclounge/GET_HOST_PLAYER_TIME_ADJUSTED']();
// TODO: only do this if we are playign (but maybe we just did a play command>...)
// TODO: see if i need await
const playerPollData = dispatch('FETCH_TIMELINE_POLL_DATA_CACHE');
// TODO: also assuming 0 rtt between us and player (is this wise)
const timelineAge = playerPollData.recievedAt
? Date.now() - playerPollData.recievedAt
: 0;
const adjustedTime = playerPollData.playerState === 'playing'
? playerPollData + timelineAge
: playerPollData.time;
const difference = Math.abs(adjustedHostTime - adjustedTime);
const bothPaused = rootGetters['synclounge/GET_HOST_TIMELINE'].playerState === 'paused'
&& playerPollData.playerState === 'paused';
if (difference > rootGetters['settings/GET_SYNCFLEXIBILITY'] || (bothPaused && difference > 10)) {
// We need to seek!
// Decide what seeking method we want to use
if (rootGetters['settings/GET_SYNCMODE'] === 'cleanseek'
|| rootGetters['synclounge/GET_HOST_TIMELINE'].playerState === 'paused') {
return this.cleanSeek(adjustedHostTime);
}
if (rootGetters['settings/GET_SYNCMODE'] === 'skipahead') {
return this.skipAhead(adjustedHostTime, 10000);
}
// Fall back to skipahead
return this.skipAhead(adjustedHostTime, 10000);
}
// Calc the average delay of the last 10 host timeline updates
// We do this to avoid any issues with random lag spikes
this.differenceCache.unshift(difference);
if (this.differenceCache.length > 5) {
this.differenceCache.pop();
}
let total = 0;
for (let i = 0; i < this.differenceCache.length; i += 1) {
total += this.differenceCache[i];
}
const avg = total / this.differenceCache.length;
if (this.clientIdentifier === 'PTPLAYER9PLUS10' && avg > 1500) {
console.log('Soft syncing because difference is', difference);
return this.cleanSeek(adjustedHostTime, true);
}
// TODO: make sure all them hit here and fix the id lol
// todo: also store the command id above because it might have changed during the awaits
// TODO: also update this when pausing or playign so we don't have werid stuff
commit('SET_PREVIOUS_SYNC_TIMELINE_COMMAND_ID', null);
return 'No sync needed';
},
};

View File

@ -21,8 +21,12 @@ export default {
return axios.create({
baseURL: client.chosenConnection.uri,
// TODO: examine this timeout...
timeout: 5000,
headers: rootGetters['plex/GET_PLEX_BASE_PARAMS'](client.accessToken),
headers: {
...rootGetters['plex/GET_PLEX_BASE_PARAMS'](client.accessToken),
'X-Plex-Target-Client-Identifier': clientIdentifier,
},
});
},
@ -54,4 +58,8 @@ export default {
playerState: getters.GET_PLEX_CLIENT_TIMELINE.state,
})
: null),
GET_COMMAND_ID: (state) => state.commandId,
GET_PREVIOUS_SYNC_TIMELINE_COMMAND_ID: (state) => state.previousSyncTimelineCommandId,
};

View File

@ -20,4 +20,12 @@ export default {
SET_ACTIVE_SERVER_ID: (state, id) => {
state.activeServerId = id;
},
INCREMENT_COMMAND_ID: (state) => {
state.commandId += 1;
},
SET_PREVIOUS_SYNC_TIMELINE_COMMAND_ID: (state, commandId) => {
state.previousSyncTimelineCommandId = commandId;
},
};

View File

@ -22,6 +22,11 @@ const state = () => ({
// Timeline storage only for plex clients. For slplayer, we query its state directly
plexClientTimeline: null,
commandId: 0,
// Tracks the commandId of the timeline that was used to synchronize last, so it doesn't try and synchronize
// multiple times with the same data and instead waits for a fresh one
previousSyncTimelineCommandId: null,
});
export default state;

View File

@ -222,6 +222,7 @@ export default {
},
NORMAL_SEEK: async ({ getters, commit, rootGetters }, seekToMs) => {
// TODO: check the logic here to make sense if the seek time is in the past ...
if ((Math.abs(seekToMs - getPlayerCurrentTimeMs(getters)) < 3000
&& rootGetters.GET_HOST_PLAYER_STATE === 'playing')) {
let cancelled = false;

View File

@ -196,7 +196,10 @@ export default {
data: {
...data,
commandId: getters.GET_POLL_NUMBER,
latency: getters.GET_SRTT,
// RTT / 2 because this is just the time it takes for a message to get to the server,
// not a complete round trip. The receiver should add this latency as well as 1/2 their srtt
// to the server when calculating delays
latency: getters.GET_SRTT / 2,
},
});

View File

@ -8,11 +8,10 @@ export default {
commit('CLEAR_MESSAGES');
commit('SET_USERS', currentUsers);
// commit('SET_ME', _data.username);
commit('SET_PARTYPAUSING', partyPausing);
commit('SET_ROOM', _data.room);
sendNotification(`Joined room: ${_data.room}`);
dispatch('DISPLAY_NOTIFICATION', `Joined room: ${_data.room}`, { root: true });
// Add this item to our recently-connected list
dispatch(
'settings/ADD_RECENT_ROOM',
@ -85,7 +84,7 @@ export default {
commit('SET_PARTYPAUSING', value);
},
HANDLE_PARTY_PAUSING_PAUSE: ({ commit, rootGetters }, { isPause, user }) => {
HANDLE_PARTY_PAUSING_PAUSE: ({ commit, dispatch, rootGetters }, { isPause, user }) => {
const messageText = `${user.username} pressed ${isPause ? 'pause' : 'play'}`;
commit('ADD_MESSAGE', {
msg: messageText,
@ -93,7 +92,7 @@ export default {
type: 'alert',
});
sendNotification(messageText);
dispatch('DISPLAY_NOTIFICATION', messageText, { root: true });
if (rootGetters.GET_CHOSEN_CLIENT) {
if (isPause) {
rootGetters.GET_CHOSEN_CLIENT.pressPause();
@ -143,6 +142,8 @@ export default {
commit('SET_HOST_TIMELINE', {
...timeline,
recievedAt: Date.now(),
// TODO: think about whether I need this or
srttSnapsnotAtReception: getters.GET_SRTT,
});
return dispatch('SYNCHRONIZE');
@ -170,18 +171,13 @@ export default {
// console.log('Timeline age is', timelineAge);
try {
// if ((timelineAge > 1000 && rootState.chosenClient.clientIdentifier !== 'PTPLAYER9PLUS10') || rootState.chosenClient.clientIdentifier === 'PTPLAYER9PLUS10') {
// await rootState.chosenClient.getTimeline()
// await decisionMaker(0)
// } else {
await decisionMaker();
// }
await dispatch('DECISION_MAKER');
} catch (e) {
console.log('Error caught in sync logic', e);
}
},
DECISION_MAKE: async ({
DECISION_MAKER: async ({
getters, dispatch, rootGetters,
}) => {
// TODO: potentailly don't do anythign if we have no timeline data yet
@ -192,7 +188,9 @@ export default {
dispatch('DISPLAY_NOTIFICATION', 'The host pressed stop', { root: true });
await rootGetters.GET_CHOSEN_CLIENT.pressStop();
return;
} if (rootGetters['settings/GET_AUTOPLAY']
}
if (rootGetters['settings/GET_AUTOPLAY']
&& (getters.GET_HOST_TIMELINE.ratingKey !== getters.GET_HOST_LAST_RATING_KEY
|| timeline.playerState === 'stopped')) {
// If we have autoplay enabled and the host rating key has changed or if we aren't playign anything
@ -208,6 +206,7 @@ export default {
if (getters.GET_HOST_TIMELINE.playerState === 'playing' && timeline.state === 'paused') {
dispatch('DISPLAY_NOTIFICATION', 'Resuming..', { root: true });
await rootGetters.GET_CHOSEN_CLIENT.pressPlay();
return;
}
if ((getters.GET_HOST_TIMELINE.playerState === 'paused'
@ -215,19 +214,22 @@ export default {
&& timeline.state === 'playing') {
dispatch('DISPLAY_NOTIFICATION', 'Pausing..', { root: true });
await rootGetters.GET_CHOSEN_CLIENT.pressPause();
return;
}
// TODO: since we have awaited,
// TODO: potentially update the player state if we paused or played so we know in the sync
if (getters.GET_HOST_TIMELINE.playerState === 'playing') {
// Add on the delay between us and the SLServer plus the delay between the server and the host
hostUpdateData.time = hostUpdateData.time + (getters.GET_SRTT || 0)
+ (hostTimeline.latency || 0);
}
await dispatch('plexclients/SYNC', null, { root: true });
await rootGetters.GET_CHOSEN_CLIENT.sync(
hostUpdateData,
rootGetters['settings/GET_SYNCFLEXIBILITY'],
rootGetters['settings/GET_SYNCMODE'],
rootGetters['settings/GET_CLIENTPOLLINTERVAL'],
);
},
@ -252,8 +254,9 @@ export default {
commit('SET_HOST_LAST_RATING_KEY', getters.GET_HOST_TIMELINE.ratingKey);
},
HANDLE_DISCONNECT: ({ commit }, disconnectData) => {
sendNotification('Disconnected from the SyncLounge server');
HANDLE_DISCONNECT: ({ commit, dispatch }, disconnectData) => {
dispatch('DISPLAY_NOTIFICATION', 'Disconnected from the SyncLounge server', { root: true });
console.log('Disconnect data', disconnectData);
if (disconnectData === 'io client disconnect') {
console.log('We disconnected from the server');

View File

@ -75,8 +75,11 @@ export default {
GET_HOST_PLAYER_TIME_ADJUSTED: (state, getters) => () => {
const hostAge = Date.now() - getters.GET_HOST_TIMELINE.recievedAt;
// TODO: please veyr much examine the latency and maybe see if the server should calc
return getters.GET_HOST_TIMELINE.playerState === 'playing'
? getters.GET_HOST_TIMELINE.time + hostAge
+ (getters.GET_HOST_TIMELINE.latency || 0)
+ (getters.GET_HOST_TIMELINE.srttSnapsnotAtReception || 0) / 2
: getters.GET_HOST_TIMELINE.time;
},
@ -102,6 +105,6 @@ export default {
GET_SERVER: (state) => state.server,
// Used to detect if the host changes
// Used to detect if the host changes
GET_HOST_LAST_RATING_KEY: (state) => state.hostLastRatingKey,
};

View File

@ -70,4 +70,5 @@ export default {
SET_HOST_TIMELINE: (state, timeline) => {
state.hostTimeline = timeline;
},
};