import { useState, useEffect, useRef, useCallback } from 'react'; interface UseProtectedDurationOptions { videoId?: string; fallbackDuration?: number; } interface UseProtectedDurationReturn { duration: number; isLoading: boolean; error: string | null; handleDurationChange: (newDuration: number) => void; refreshDuration: () => Promise; } /** * Duration protection hook that ensures real video duration is displayed * instead of buffered duration. Uses existing codec_info from database. * * Priority order: * 1. Database-stored duration (codec_info.duration) - most reliable * 2. HTTP headers from transcoding endpoint * 3. Video element metadata (fallback, can be buffered duration) */ export function useProtectedDuration({ videoId, fallbackDuration = 0 }: UseProtectedDurationOptions): UseProtectedDurationReturn { const [duration, setDuration] = useState(fallbackDuration); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const hasRealDuration = useRef(false); const lastFetchedVideoId = useRef(null); /** * Handle duration changes from video element * Only accept significant changes if we don't have real duration */ const handleDurationChange = useCallback((newDuration: number): void => { // If we have real duration from database/headers, ignore metadata changes if (hasRealDuration.current) { console.log(`[DURATION] Blocked metadata duration: ${newDuration}s (using stored: ${duration}s)`); return; } // Only accept significantly larger durations (not buffered durations) if (newDuration > duration * 2.0 && newDuration > 0) { console.log(`[DURATION] Accepted metadata duration: ${newDuration}s`); setDuration(newDuration); hasRealDuration.current = true; // Mark as having real duration } else if (newDuration <= 0) { console.log(`[DURATION] Ignored invalid duration: ${newDuration}s`); } else { console.log(`[DURATION] Ignored buffered duration: ${newDuration}s (current: ${duration}s)`); } }, [duration]); // Only depend on duration /** * Refresh duration from database */ const refreshDuration = useCallback(async (): Promise => { hasRealDuration.current = false; // Reset to allow re-fetch lastFetchedVideoId.current = null; // Force re-fetch by clearing the cache // Re-run the effect by updating a dummy state setError(null); }, []); // No dependencies to prevent re-creation // Fetch duration when videoId changes useEffect(() => { if (videoId && videoId !== lastFetchedVideoId.current) { hasRealDuration.current = false; // Reset for new video const fetchDuration = async () => { if (!videoId) { setDuration(fallbackDuration); return; } setIsLoading(true); setError(null); try { // 1. Try database first (codec_info has real duration) const response = await fetch(`/api/videos/${videoId}`); if (response.ok) { const videoData = await response.json(); console.log(`[DURATION] Video data:`, videoData); if (videoData.codec_info) { const codecInfo = JSON.parse(videoData.codec_info); console.log(`[DURATION] Codec info:`, codecInfo); if (codecInfo.duration && codecInfo.duration > 0) { setDuration(codecInfo.duration); hasRealDuration.current = true; console.log(`[DURATION] Using database duration: ${codecInfo.duration}s`); return; } } } else { console.log(`[DURATION] API response not ok:`, response.status, response.statusText); } // 2. Use fallback when database duration is unavailable. console.log(`[DURATION] Using fallback duration: ${fallbackDuration}s`); setDuration(fallbackDuration); } catch (error) { console.error('[DURATION] Error fetching duration:', error); setError(error instanceof Error ? error.message : 'Unknown error'); setDuration(fallbackDuration); } finally { setIsLoading(false); } }; fetchDuration(); lastFetchedVideoId.current = videoId; } }, [videoId, fallbackDuration]); // Simple dependencies return { duration, isLoading, error, handleDurationChange, refreshDuration }; } /** * Utility function to get duration directly from API */ export async function getRealDuration(videoId: string): Promise { try { // 1. Try database first const response = await fetch(`/api/videos/${videoId}`); if (response.ok) { const videoData = await response.json(); if (videoData.codec_info) { const codecInfo = JSON.parse(videoData.codec_info); if (codecInfo.duration && codecInfo.duration > 0) { return codecInfo.duration; } } } return 0; } catch (error) { console.error('[DURATION] Error getting real duration:', error); return 0; } }