nextav/src/lib/hooks/use-protected-duration.ts

152 lines
5.1 KiB
TypeScript

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<void>;
}
/**
* 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<string | null>(null);
const hasRealDuration = useRef(false);
const lastFetchedVideoId = useRef<string | null>(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<void> => {
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<number> {
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;
}
}