-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(watchhistory): improve watch history storage calls and fix bugs
* fix(watchhistory): wrong item saved when clicking the next video * fix(watchhistory): only the last item being saved * chore: fix prettier * refactor(watchhistory): use position and duration from time event * refactor(watchhistory): separate useWatchHistory hook and add progressive save interval * refactor(watchhistory): pass the series item instead of fetching each save * chore(watchhistory): enable keepalive to update personal shelves request
- Loading branch information
1 parent
d8b6988
commit 9fd1774
Showing
9 changed files
with
168 additions
and
113 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { useMemo } from 'react'; | ||
|
||
import type { PlaylistItem } from '#types/playlist'; | ||
import { useConfigStore } from '#src/stores/ConfigStore'; | ||
import { useWatchHistoryStore } from '#src/stores/WatchHistoryStore'; | ||
import { VideoProgressMinMax } from '#src/config'; | ||
import { useWatchHistoryListener } from '#src/hooks/useWatchHistoryListener'; | ||
import type { JWPlayer } from '#types/jwplayer'; | ||
|
||
export const useWatchHistory = (player: JWPlayer | undefined, item: PlaylistItem, seriesItem?: PlaylistItem) => { | ||
// config | ||
const { features } = useConfigStore((s) => s.config); | ||
const continueWatchingList = features?.continueWatchingList; | ||
const watchHistoryEnabled = !!continueWatchingList; | ||
|
||
// watch history listener | ||
useWatchHistoryListener(player, item, seriesItem); | ||
|
||
// watch History | ||
const watchHistoryItem = useWatchHistoryStore((state) => (!!item && watchHistoryEnabled ? state.getItem(item) : undefined)); | ||
|
||
// calculate the `startTime` of the current item based on the watch progress | ||
return useMemo(() => { | ||
const videoProgress = watchHistoryItem?.progress; | ||
|
||
if (videoProgress && videoProgress > VideoProgressMinMax.Min && videoProgress < VideoProgressMinMax.Max) { | ||
return videoProgress * item.duration; | ||
} | ||
|
||
// start at the beginning of the video (only for VOD content) | ||
return 0; | ||
}, [item.duration, watchHistoryItem?.progress]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,112 @@ | ||
import { useEffect } from 'react'; | ||
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react'; | ||
|
||
import type { JWPlayer } from '#types/jwplayer'; | ||
import type { PlaylistItem } from '#types/playlist'; | ||
import useEventCallback from '#src/hooks/useEventCallback'; | ||
import { saveItem } from '#src/stores/WatchHistoryController'; | ||
import { useConfigStore } from '#src/stores/ConfigStore'; | ||
|
||
export const useWatchHistoryListener = (saveItem: () => void): void => { | ||
const saveItemEvent = useEventCallback(saveItem); | ||
type QueuedProgress = { | ||
item: PlaylistItem; | ||
seriesItem?: PlaylistItem; | ||
progress: number; | ||
timestamp: number; | ||
}; | ||
|
||
const PROGRESSIVE_SAVE_INTERVAL = 300_000; // 5 minutes | ||
|
||
/** | ||
* The `useWatchHistoryListener` hook has the responsibility to save the players watch progress on key moments. | ||
* | ||
* __The problem:__ | ||
* | ||
* There are multiple events that trigger a save. This results in duplicate (unnecessary) saves and API calls. Another | ||
* problem is that some events are triggered when the `item` to update has changed. For example, when clicking a media | ||
* item in the "Related shelf". This causes the wrong item to be saved in the watch history. | ||
* | ||
* __The solution:__ | ||
* | ||
* This hook listens to the player time update event and queues a watch history entry with the current progress and | ||
* item. When this needs to be saved, the queue is used to look up the last progress and item and save it when | ||
* necessary. The queue is then cleared to prevent duplicate saves and API calls. | ||
*/ | ||
export const useWatchHistoryListener = (player: JWPlayer | undefined, item: PlaylistItem, seriesItem?: PlaylistItem) => { | ||
const queuedWatchProgress = useRef<QueuedProgress | null>(null); | ||
|
||
// config | ||
const { features } = useConfigStore((s) => s.config); | ||
const continueWatchingList = features?.continueWatchingList; | ||
const watchHistoryEnabled = !!continueWatchingList; | ||
|
||
// maybe store the watch progress when we have a queued watch progress | ||
const maybeSaveWatchProgress = useCallback(() => { | ||
if (!watchHistoryEnabled || !queuedWatchProgress.current) return; | ||
|
||
const { item, seriesItem, progress } = queuedWatchProgress.current; | ||
|
||
// save the queued watch progress | ||
saveItem(item, seriesItem, progress); | ||
|
||
// clear the queue | ||
queuedWatchProgress.current = null; | ||
}, [watchHistoryEnabled]); | ||
|
||
// update the queued watch progress on each time update event | ||
const handleTimeUpdate = useEventCallback((event: jwplayer.TimeParam) => { | ||
// live streams have a negative duration, we ignore these for now | ||
if (event.duration < 0) return; | ||
|
||
const progress = event.position / event.duration; | ||
|
||
if (!isNaN(progress) && isFinite(progress)) { | ||
queuedWatchProgress.current = { | ||
item, | ||
seriesItem, | ||
progress, | ||
timestamp: queuedWatchProgress.current?.timestamp || Date.now(), | ||
}; | ||
|
||
// save the progress when we haven't done so in the last X minutes | ||
if (Date.now() - queuedWatchProgress.current.timestamp > PROGRESSIVE_SAVE_INTERVAL) { | ||
maybeSaveWatchProgress(); | ||
} | ||
} | ||
}); | ||
|
||
// listen for certain player events | ||
useEffect(() => { | ||
const visibilityListener = () => document.visibilityState === 'hidden' && saveItemEvent(); | ||
window.addEventListener('beforeunload', saveItemEvent); | ||
if (!player || !watchHistoryEnabled) return; | ||
|
||
player.on('time', handleTimeUpdate); | ||
player.on('pause', maybeSaveWatchProgress); | ||
player.on('complete', maybeSaveWatchProgress); | ||
player.on('remove', maybeSaveWatchProgress); | ||
|
||
return () => { | ||
player.off('time', handleTimeUpdate); | ||
player.off('pause', maybeSaveWatchProgress); | ||
player.off('complete', maybeSaveWatchProgress); | ||
player.off('remove', maybeSaveWatchProgress); | ||
}; | ||
}, [player, watchHistoryEnabled, maybeSaveWatchProgress, handleTimeUpdate]); | ||
|
||
useEffect(() => { | ||
return () => { | ||
// store watch progress on unmount and when the media item changes | ||
maybeSaveWatchProgress(); | ||
}; | ||
}, [item?.mediaid, maybeSaveWatchProgress]); | ||
|
||
// add event listeners for unload and visibility change to ensure the latest watch progress is saved | ||
useLayoutEffect(() => { | ||
const visibilityListener = () => document.visibilityState === 'hidden' && maybeSaveWatchProgress(); | ||
|
||
window.addEventListener('pagehide', maybeSaveWatchProgress); | ||
window.addEventListener('visibilitychange', visibilityListener); | ||
|
||
return () => { | ||
saveItemEvent(); | ||
window.removeEventListener('beforeunload', saveItemEvent); | ||
window.removeEventListener('pagehide', maybeSaveWatchProgress); | ||
window.removeEventListener('visibilitychange', visibilityListener); | ||
}; | ||
}, [saveItemEvent]); | ||
}, [maybeSaveWatchProgress]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.