diff --git a/src/store/modules/plex/helpers/PlexClient.js b/src/store/modules/plex/helpers/PlexClient.js index 4f60860b..3c0dc67c 100644 --- a/src/store/modules/plex/helpers/PlexClient.js +++ b/src/store/modules/plex/helpers/PlexClient.js @@ -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; diff --git a/src/store/modules/plexclients/actions.js b/src/store/modules/plexclients/actions.js index 70de225a..8409d51f 100644 --- a/src/store/modules/plexclients/actions.js +++ b/src/store/modules/plexclients/actions.js @@ -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'; + }, }; diff --git a/src/store/modules/plexclients/getters.js b/src/store/modules/plexclients/getters.js index 6eee8bef..fe832ba2 100644 --- a/src/store/modules/plexclients/getters.js +++ b/src/store/modules/plexclients/getters.js @@ -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, }; diff --git a/src/store/modules/plexclients/mutations.js b/src/store/modules/plexclients/mutations.js index cbe2859e..792fe8e9 100644 --- a/src/store/modules/plexclients/mutations.js +++ b/src/store/modules/plexclients/mutations.js @@ -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; + }, }; diff --git a/src/store/modules/plexclients/state.js b/src/store/modules/plexclients/state.js index 1bcc7c5c..118eba85 100644 --- a/src/store/modules/plexclients/state.js +++ b/src/store/modules/plexclients/state.js @@ -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; diff --git a/src/store/modules/slplayer/actions.js b/src/store/modules/slplayer/actions.js index 098ec8e2..f8c0e779 100644 --- a/src/store/modules/slplayer/actions.js +++ b/src/store/modules/slplayer/actions.js @@ -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; diff --git a/src/store/modules/synclounge/actions.js b/src/store/modules/synclounge/actions.js index 74aa8dfd..ed72df52 100644 --- a/src/store/modules/synclounge/actions.js +++ b/src/store/modules/synclounge/actions.js @@ -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, }, }); diff --git a/src/store/modules/synclounge/eventhandlers.js b/src/store/modules/synclounge/eventhandlers.js index 5ccff1e4..6d45d6fc 100644 --- a/src/store/modules/synclounge/eventhandlers.js +++ b/src/store/modules/synclounge/eventhandlers.js @@ -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'); diff --git a/src/store/modules/synclounge/getters.js b/src/store/modules/synclounge/getters.js index 6d684881..9db2d1c4 100644 --- a/src/store/modules/synclounge/getters.js +++ b/src/store/modules/synclounge/getters.js @@ -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, }; diff --git a/src/store/modules/synclounge/mutations.js b/src/store/modules/synclounge/mutations.js index 93dceaa0..a8492f33 100644 --- a/src/store/modules/synclounge/mutations.js +++ b/src/store/modules/synclounge/mutations.js @@ -70,4 +70,5 @@ export default { SET_HOST_TIMELINE: (state, timeline) => { state.hostTimeline = timeline; }, + };