152 lines
5.1 KiB
TypeScript
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;
|
|
}
|
|
} |