From 73819585eb454df44449cf18ff457183515e8e4f Mon Sep 17 00:00:00 2001 From: Marcello Date: Thu, 2 Apr 2026 22:18:58 +0100 Subject: [PATCH] fix: preserve Emby session reporting across stream switches Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/components/player/AndroidVideoPlayer.tsx | 4 +- src/components/player/KSPlayerCore.tsx | 4 +- src/components/player/hooks/useEmbySession.ts | 53 +++++++------------ 3 files changed, 26 insertions(+), 35 deletions(-) diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index dfaf6f50..0e378fea 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -712,7 +712,8 @@ const AndroidVideoPlayer: React.FC = () => { streamProvider: newProvider, streamName: newStreamName, headers: newStream.headers, - availableStreams: availableStreams + availableStreams: availableStreams, + embyItemId: newStream.embyItemId ?? undefined, }); }, 300); }; @@ -751,6 +752,7 @@ const AndroidVideoPlayer: React.FC = () => { backdrop: backdrop || undefined, availableStreams: {}, groupedEpisodes: groupedEpisodes, + embyItemId: stream.embyItemId ?? undefined, }); }, 300); }; diff --git a/src/components/player/KSPlayerCore.tsx b/src/components/player/KSPlayerCore.tsx index 60d61d34..50d48224 100644 --- a/src/components/player/KSPlayerCore.tsx +++ b/src/components/player/KSPlayerCore.tsx @@ -714,7 +714,8 @@ const KSPlayerCore: React.FC = () => { streamProvider: newProvider, streamName: newStreamName, headers: newStream.headers, - availableStreams: availableStreams + availableStreams: availableStreams, + embyItemId: newStream.embyItemId ?? undefined, }); }, 100); }; @@ -767,6 +768,7 @@ const KSPlayerCore: React.FC = () => { episodeId: ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number} `, imdbId: imdbId ?? undefined, backdrop: backdrop || undefined, + embyItemId: stream.embyItemId ?? undefined, }); }, 100); }; diff --git a/src/components/player/hooks/useEmbySession.ts b/src/components/player/hooks/useEmbySession.ts index 329928e2..8f1eaa68 100644 --- a/src/components/player/hooks/useEmbySession.ts +++ b/src/components/player/hooks/useEmbySession.ts @@ -20,28 +20,40 @@ export const useEmbySession = ( currentTime: number, paused: boolean ) => { - const hasStartedRef = useRef(false); const progressTimerRef = useRef | null>(null); // Keep a ref for the latest values to avoid stale closures in the interval const currentTimeRef = useRef(currentTime); const pausedRef = useRef(paused); - const embyItemIdRef = useRef(embyItemId); useEffect(() => { currentTimeRef.current = currentTime; }, [currentTime]); useEffect(() => { pausedRef.current = paused; }, [paused]); - useEffect(() => { embyItemIdRef.current = embyItemId; }, [embyItemId]); - // Report playback start once when embyItemId becomes available + // Session lifecycle: start when embyItemId becomes available, stop when it + // changes or the component unmounts. The cleanup closure captures the + // embyItemId that was active when the effect ran, so the correct session is + // always stopped — even if the ref/prop has already moved to a new value. useEffect(() => { if (!embyItemId) return; - if (hasStartedRef.current) return; - hasStartedRef.current = true; embyService.reportPlaybackStart(embyItemId, currentTimeRef.current).catch((err) => { logger.warn('[useEmbySession] reportPlaybackStart error:', err); }); - // eslint-disable-next-line react-hooks/exhaustive-deps + + return () => { + if (progressTimerRef.current) { + clearInterval(progressTimerRef.current); + progressTimerRef.current = null; + } + + // Deferred so navigation can complete first + const positionAtStop = currentTimeRef.current; + setTimeout(() => { + embyService + .reportPlaybackStopped(embyItemId, positionAtStop) + .catch((err) => logger.warn('[useEmbySession] reportPlaybackStopped error:', err)); + }, 0); + }; }, [embyItemId]); // Send progress every PROGRESS_INTERVAL_MS while playing; pause/resume sends an immediate report @@ -59,10 +71,8 @@ export const useEmbySession = ( if (!paused) { progressTimerRef.current = setInterval(() => { - const itemId = embyItemIdRef.current; - if (!itemId) return; embyService - .reportPlaybackProgress(itemId, currentTimeRef.current, pausedRef.current) + .reportPlaybackProgress(embyItemId, currentTimeRef.current, pausedRef.current) .catch(() => {}); }, PROGRESS_INTERVAL_MS); } @@ -73,28 +83,5 @@ export const useEmbySession = ( progressTimerRef.current = null; } }; - // Re-run when paused state changes; embyItemId already guarded above - // eslint-disable-next-line react-hooks/exhaustive-deps }, [embyItemId, paused]); - - // Report stopped on component unmount (deferred so navigation can complete first) - useEffect(() => { - return () => { - const itemId = embyItemIdRef.current; - if (!itemId) return; - - if (progressTimerRef.current) { - clearInterval(progressTimerRef.current); - progressTimerRef.current = null; - } - - setTimeout(() => { - embyService - .reportPlaybackStopped(itemId, currentTimeRef.current) - .catch((err) => logger.warn('[useEmbySession] reportPlaybackStopped error:', err)); - }, 0); - }; - // Only run cleanup on unmount - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); };