feat: Add usePlaybackPolling hook for robust playback progress updates and refine history item matching logic.

This commit is contained in:
kuekhaoyang
2025-11-23 23:11:59 +08:00
parent 95e4a733ee
commit f0b19c1144
4 changed files with 66 additions and 17 deletions
+6 -3
View File
@@ -34,11 +34,14 @@ export function VideoPlayer({ playUrl, videoId, currentEpisode, onBack }: VideoP
if (!videoId) return 0;
// Directly check HistoryStore for progress
// This is the single source of truth for playback resumption
// We prioritize a strict match (including source), but fall back to any match for this video/episode
// This fixes issues where the source parameter might be missing or different
const historyItem = viewingHistory.find(item =>
// Loose match for videoId (string vs number)
item.videoId.toString() === videoId?.toString() &&
item.source === source &&
item.episodeIndex === currentEpisode &&
(source ? item.source === source : true)
) || viewingHistory.find(item =>
item.videoId.toString() === videoId?.toString() &&
item.episodeIndex === currentEpisode
);
@@ -1,4 +1,6 @@
import { useCallback, useEffect } from 'react';
import { formatTime } from '@/lib/utils/format-utils';
import { usePlaybackPolling } from '../usePlaybackPolling';
interface UsePlaybackControlsProps {
videoRef: React.RefObject<HTMLVideoElement | null>;
@@ -112,13 +114,14 @@ export function usePlaybackControls({
}
}, [videoRef, setPlaybackRate, setShowSpeedMenu, speedMenuTimeoutRef]);
const formatTime = useCallback((seconds: number) => {
if (isNaN(seconds)) return '0:00:00';
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return `${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}, []);
// Polling fallback for AirPlay and throttled events
usePlaybackPolling({
isPlaying,
videoRef,
isDraggingProgressRef,
setCurrentTime,
setDuration
});
return {
togglePlay,
@@ -1,4 +1,6 @@
import { useCallback, useEffect } from 'react';
import { formatTime } from '@/lib/utils/format-utils';
import { usePlaybackPolling } from '../usePlaybackPolling';
interface UseMobilePlaybackProps {
videoRef: React.RefObject<HTMLVideoElement>;
@@ -124,13 +126,14 @@ export function useMobilePlaybackControls({
setShowSpeedMenu(false);
}, [videoRef, setPlaybackRate, setShowSpeedMenu]);
const formatTime = useCallback((seconds: number) => {
if (isNaN(seconds)) return '0:00:00';
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return `${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}, []);
// Polling fallback for AirPlay and throttled events
usePlaybackPolling({
isPlaying,
videoRef,
isDraggingProgressRef,
setCurrentTime,
setDuration
});
return {
togglePlay,
@@ -0,0 +1,40 @@
import { useEffect } from 'react';
interface UsePlaybackPollingProps {
isPlaying: boolean;
videoRef: React.RefObject<HTMLVideoElement | null>;
isDraggingProgressRef: React.MutableRefObject<boolean>;
setCurrentTime: (time: number) => void;
setDuration: (duration: number) => void;
}
/**
* Polling fallback for AirPlay and throttled events
* Updates playback progress manually when events are suppressed
*/
export function usePlaybackPolling({
isPlaying,
videoRef,
isDraggingProgressRef,
setCurrentTime,
setDuration
}: UsePlaybackPollingProps) {
useEffect(() => {
if (!isPlaying || !videoRef.current) return;
const interval = setInterval(() => {
if (videoRef.current && !isDraggingProgressRef.current) {
const current = videoRef.current.currentTime;
const total = videoRef.current.duration;
// Only update if significantly different to avoid jitter
setCurrentTime(current);
if (!isNaN(total) && total > 0) {
setDuration(total);
}
}
}, 500); // Poll every 500ms
return () => clearInterval(interval);
}, [isPlaying, videoRef, isDraggingProgressRef, setCurrentTime, setDuration]);
}