/** * useVideoBufferingState Hook * * Tracks video buffering state including: * - Initial buffering (waiting for canplay) * - Mid-playback buffering (waiting event) * - Progress-based timeout detection */ import { useState, useRef, useCallback, useEffect, type RefObject } from 'react'; import { logger } from '../../lib/logger'; import { TRANSITION_CONFIG } from '../../config/transition.config'; export interface UseVideoBufferingStateOptions { videoRef: RefObject; enabled?: boolean; /** Custom no-progress timeout in ms (uses config default if not provided) */ noProgressMs?: number; /** Custom check interval in ms (uses config default if not provided) */ checkIntervalMs?: number; /** Callback when buffering state changes */ onBufferingChange?: (isBuffering: boolean) => void; /** Callback when progress timeout occurs */ onProgressTimeout?: () => void; } export interface UseVideoBufferingStateResult { /** True when video is waiting for data (initial load or mid-playback) */ isBuffering: boolean; /** True specifically when waiting event has fired (mid-playback) */ isWaitingForData: boolean; /** True when canplay has fired (enough data to start playing) */ isReady: boolean; /** Reset buffering state (e.g., when loading new source) */ reset: () => void; /** Start progress monitoring (call after video starts loading) */ startProgressMonitor: () => void; /** Stop progress monitoring */ stopProgressMonitor: () => void; /** Update last progress time (call on progress events) */ updateProgressTime: () => void; } /** * Hook for tracking video buffering state. * * Handles both initial buffering (before canplay) and mid-playback * buffering (when video is waiting for more data). * * @example * const { isBuffering, isReady, startProgressMonitor } = useVideoBufferingState({ * videoRef, * enabled: true, * onProgressTimeout: () => console.error('Video timed out'), * }); */ export function useVideoBufferingState({ videoRef, enabled = true, noProgressMs, checkIntervalMs, onBufferingChange, onProgressTimeout, }: UseVideoBufferingStateOptions): UseVideoBufferingStateResult { const [isReady, setIsReady] = useState(false); const [isWaitingForData, setIsWaitingForData] = useState(false); const isWaitingForDataRef = useRef(false); const lastProgressTimeRef = useRef(0); const progressTimeoutRef = useRef | null>(null); const isMonitoringRef = useRef(false); const onProgressTimeoutRef = useRef(onProgressTimeout); const onBufferingChangeRef = useRef(onBufferingChange); // Update refs on each render useEffect(() => { onProgressTimeoutRef.current = onProgressTimeout; onBufferingChangeRef.current = onBufferingChange; }); // Calculate actual timeout values const { progressTimeout } = TRANSITION_CONFIG; const isMobile = typeof navigator !== 'undefined' && /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); const actualNoProgressMs = noProgressMs ?? (isMobile ? progressTimeout.noProgressMs * progressTimeout.mobileMultiplier : progressTimeout.noProgressMs); const actualCheckIntervalMs = checkIntervalMs ?? progressTimeout.checkIntervalMs; const stopProgressMonitor = useCallback(() => { if (progressTimeoutRef.current) { clearTimeout(progressTimeoutRef.current); progressTimeoutRef.current = null; } isMonitoringRef.current = false; }, []); const updateProgressTime = useCallback(() => { lastProgressTimeRef.current = Date.now(); }, []); const startProgressMonitor = useCallback(() => { if (isMonitoringRef.current) return; isMonitoringRef.current = true; lastProgressTimeRef.current = Date.now(); const video = videoRef.current; const checkProgress = () => { if (!isMonitoringRef.current) return; if (!video) return; const timeSinceProgress = Date.now() - lastProgressTimeRef.current; // If playing normally (not waiting), continue monitoring if (!video.paused && !isWaitingForDataRef.current) { progressTimeoutRef.current = setTimeout(checkProgress, actualCheckIntervalMs); return; } // If waiting but received data recently, keep waiting if (timeSinceProgress < actualNoProgressMs) { progressTimeoutRef.current = setTimeout(checkProgress, actualCheckIntervalMs); return; } // No progress for too long while waiting - timeout logger.error('useVideoBufferingState: No network progress - timing out', { timeSinceProgress, currentTime: video.currentTime?.toFixed(2), isWaiting: isWaitingForDataRef.current, actualNoProgressMs, }); onProgressTimeoutRef.current?.(); }; progressTimeoutRef.current = setTimeout(checkProgress, actualCheckIntervalMs); }, [videoRef, actualNoProgressMs, actualCheckIntervalMs]); const reset = useCallback(() => { setIsReady(false); setIsWaitingForData(false); isWaitingForDataRef.current = false; stopProgressMonitor(); }, [stopProgressMonitor]); // Set up video event listeners useEffect(() => { const video = videoRef.current; if (!video || !enabled) return; const onCanPlay = () => { setIsReady(true); }; const onWaiting = () => { const bufferedInfo = video.buffered.length > 0 ? `${video.buffered.start(0).toFixed(2)}-${video.buffered.end(video.buffered.length - 1).toFixed(2)}` : 'none'; logger.info('useVideoBufferingState: Video waiting for data', { currentTime: video.currentTime.toFixed(2), buffered: bufferedInfo, }); setIsWaitingForData(true); isWaitingForDataRef.current = true; }; const onPlaying = () => { // Clear waiting state when playback resumes if (isWaitingForDataRef.current) { logger.info('useVideoBufferingState: Resumed playback after buffering'); setIsWaitingForData(false); isWaitingForDataRef.current = false; } }; const onProgress = () => { updateProgressTime(); }; video.addEventListener('canplay', onCanPlay); video.addEventListener('waiting', onWaiting); video.addEventListener('playing', onPlaying); video.addEventListener('progress', onProgress); return () => { video.removeEventListener('canplay', onCanPlay); video.removeEventListener('waiting', onWaiting); video.removeEventListener('playing', onPlaying); video.removeEventListener('progress', onProgress); stopProgressMonitor(); }; }, [videoRef, enabled, stopProgressMonitor, updateProgressTime]); // Notify on buffering state change const isBuffering = !isReady || isWaitingForData; useEffect(() => { onBufferingChangeRef.current?.(isBuffering); }, [isBuffering]); return { isBuffering, isWaitingForData, isReady, reset, startProgressMonitor, stopProgressMonitor, updateProgressTime, }; } export default useVideoBufferingState;