mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-17 01:33:43 +08:00
149 lines
5.2 KiB
TypeScript
149 lines
5.2 KiB
TypeScript
import { useCallback, useEffect, useRef } from 'react';
|
|
import { formatTime } from '@/lib/utils/format-utils';
|
|
import { usePlaybackPolling } from '../usePlaybackPolling';
|
|
import { useMobileTogglePlay } from './useMobileTogglePlay';
|
|
|
|
interface UseMobilePlaybackProps {
|
|
videoRef: React.RefObject<HTMLVideoElement>;
|
|
isPlaying: boolean;
|
|
setIsPlaying: (playing: boolean) => void;
|
|
setIsLoading: (loading: boolean) => void;
|
|
initialTime: number;
|
|
shouldAutoPlay: boolean;
|
|
setDuration: (duration: number) => void;
|
|
setCurrentTime: (time: number) => void;
|
|
setPlaybackRate: (rate: number) => void;
|
|
setShowMoreMenu: (show: boolean) => void;
|
|
setShowVolumeMenu: (show: boolean) => void;
|
|
setShowSpeedMenu: (show: boolean) => void;
|
|
onTimeUpdate?: (currentTime: number, duration: number) => void;
|
|
onError?: (error: string) => void;
|
|
isDraggingProgressRef: React.MutableRefObject<boolean>;
|
|
isTogglingRef: React.MutableRefObject<boolean>;
|
|
}
|
|
|
|
export function useMobilePlaybackControls({
|
|
videoRef,
|
|
isPlaying,
|
|
setIsPlaying,
|
|
setIsLoading,
|
|
initialTime,
|
|
shouldAutoPlay,
|
|
setDuration,
|
|
setCurrentTime,
|
|
setPlaybackRate,
|
|
setShowMoreMenu,
|
|
setShowVolumeMenu,
|
|
setShowSpeedMenu,
|
|
onTimeUpdate,
|
|
onError,
|
|
isDraggingProgressRef,
|
|
isTogglingRef
|
|
}: UseMobilePlaybackProps) {
|
|
const togglePlay = useMobileTogglePlay({
|
|
videoRef,
|
|
isPlaying,
|
|
isTogglingRef,
|
|
setShowMoreMenu,
|
|
setShowVolumeMenu,
|
|
setShowSpeedMenu
|
|
});
|
|
|
|
const handlePlay = useCallback(() => setIsPlaying(true), [setIsPlaying]);
|
|
const handlePause = useCallback(() => setIsPlaying(false), [setIsPlaying]);
|
|
|
|
const handleTimeUpdateEvent = useCallback(() => {
|
|
if (!videoRef.current || isDraggingProgressRef.current) return;
|
|
const current = videoRef.current.currentTime;
|
|
const total = videoRef.current.duration;
|
|
setCurrentTime(current);
|
|
setDuration(total);
|
|
if (onTimeUpdate) {
|
|
onTimeUpdate(current, total);
|
|
}
|
|
}, [videoRef, isDraggingProgressRef, setCurrentTime, setDuration, onTimeUpdate]);
|
|
|
|
const handleLoadedMetadata = useCallback(() => {
|
|
if (!videoRef.current) return;
|
|
setDuration(videoRef.current.duration);
|
|
setIsLoading(false);
|
|
|
|
// Fix for stuck at 00:00:00:
|
|
// Only seek if we are at the very start (to avoid overwriting a previous seek)
|
|
if (videoRef.current.currentTime < 0.5) {
|
|
// If initialTime is 0, we seek to a tiny offset to help the browser/HLS buffer start.
|
|
const startPosition = initialTime > 0 ? initialTime : 0.1;
|
|
videoRef.current.currentTime = startPosition;
|
|
}
|
|
|
|
videoRef.current.play().catch((err: Error) => {
|
|
console.warn('Autoplay was prevented:', err);
|
|
});
|
|
}, [videoRef, setDuration, setIsLoading, initialTime]);
|
|
|
|
const hasInitialSeekHappened = useRef(false);
|
|
|
|
// Handle late initialization of initialTime (e.g. from async storage hydration)
|
|
useEffect(() => {
|
|
if (initialTime > 0 && videoRef.current && !hasInitialSeekHappened.current) {
|
|
// Only seek if we haven't progressed far (e.g. still near start)
|
|
// AND if the target time is significantly different from current time (> 0.5s)
|
|
// This prevents jumping if the user has already started watching and initialTime updates
|
|
if (videoRef.current.currentTime < 2 && Math.abs(videoRef.current.currentTime - initialTime) > 0.5) {
|
|
videoRef.current.currentTime = initialTime;
|
|
hasInitialSeekHappened.current = true;
|
|
} else if (videoRef.current.currentTime >= 2) {
|
|
// If user already watched past 2s, assume they don't want to be reset
|
|
hasInitialSeekHappened.current = true;
|
|
}
|
|
}
|
|
}, [initialTime, videoRef]);
|
|
|
|
// Force autoplay when shouldAutoPlay is true (for proxy retry)
|
|
useEffect(() => {
|
|
if (shouldAutoPlay && videoRef.current) {
|
|
const playPromise = videoRef.current.play();
|
|
if (playPromise !== undefined) {
|
|
playPromise.catch((err: Error) => {
|
|
console.warn('Force autoplay was prevented:', err);
|
|
});
|
|
}
|
|
}
|
|
}, [shouldAutoPlay, videoRef]);
|
|
|
|
const handleVideoError = useCallback(() => {
|
|
setIsLoading(false);
|
|
if (onError) {
|
|
onError('Video failed to load');
|
|
}
|
|
}, [setIsLoading, onError]);
|
|
|
|
const changePlaybackSpeed = useCallback((speed: number) => {
|
|
if (!videoRef.current) return;
|
|
videoRef.current.playbackRate = speed;
|
|
setPlaybackRate(speed);
|
|
setShowSpeedMenu(false);
|
|
}, [videoRef, setPlaybackRate, setShowSpeedMenu]);
|
|
|
|
// Polling fallback for AirPlay and throttled events
|
|
usePlaybackPolling({
|
|
isPlaying,
|
|
videoRef,
|
|
isDraggingProgressRef,
|
|
setCurrentTime,
|
|
setDuration,
|
|
setIsPlaying
|
|
});
|
|
|
|
return {
|
|
togglePlay,
|
|
handlePlay,
|
|
handlePause,
|
|
handleTimeUpdateEvent,
|
|
handleLoadedMetadata,
|
|
handleVideoError,
|
|
changePlaybackSpeed,
|
|
formatTime
|
|
};
|
|
}
|