/** * useMediaDurationProbe Hook * * Probes media durations with caching and deduplication. * Used in constructor.tsx for displaying video/audio duration info. */ import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { formatDurationNote, resolveDurationWithFallback, } from '../lib/mediaHelpers'; import { logger } from '../lib/logger'; interface DurationProbeTarget { source: string; mediaType: 'video' | 'audio'; } interface UseMediaDurationProbeOptions { /** Array of media sources to probe */ targets: DurationProbeTarget[]; } interface UseMediaDurationProbeResult { /** Map of source URL to resolved duration (seconds) or null */ durationBySource: Record; /** Get known duration for a source (or null if unknown/pending) */ getDuration: (source: string) => number | null; /** Get formatted duration note for a source */ getDurationNote: (source: string) => string; /** Whether any probes are in progress */ isProbing: boolean; } /** * Hook for probing and caching media durations. * Automatically deduplicates in-flight requests and caches results. * * @example * const { getDurationNote, getDuration } = useMediaDurationProbe({ * targets: [ * { source: backgroundVideoUrl, mediaType: 'video' }, * { source: backgroundAudioUrl, mediaType: 'audio' }, * ], * }); * * return

{getDurationNote(backgroundVideoUrl)}

; */ export function useMediaDurationProbe({ targets, }: UseMediaDurationProbeOptions): UseMediaDurationProbeResult { const [durationBySource, setDurationBySource] = useState< Record >({}); const [isProbing, setIsProbing] = useState(false); const inFlightRef = useRef>(new Set()); // Probe targets for duration useEffect(() => { if (typeof window === 'undefined') return; let isCancelled = false; let activeProbes = 0; targets.forEach(({ source, mediaType }) => { const normalizedSource = String(source || '').trim(); if (!normalizedSource) return; // Skip if already resolved if (durationBySource[normalizedSource] !== undefined) return; // Skip if already in flight const probeKey = `${mediaType}:${normalizedSource}`; if (inFlightRef.current.has(probeKey)) return; inFlightRef.current.add(probeKey); activeProbes++; setIsProbing(true); resolveDurationWithFallback(normalizedSource, mediaType) .then((duration) => { if (isCancelled) return; setDurationBySource((prev) => ({ ...prev, [normalizedSource]: Number.isFinite(duration) && Number(duration) > 0 ? Number(duration) : null, })); }) .catch((error) => { logger.error( 'Failed to resolve media duration:', error instanceof Error ? error : { error }, ); if (isCancelled) return; setDurationBySource((prev) => ({ ...prev, [normalizedSource]: null, })); }) .finally(() => { inFlightRef.current.delete(probeKey); activeProbes--; if (activeProbes === 0) { setIsProbing(false); } }); }); return () => { isCancelled = true; }; }, [targets, durationBySource]); const getDuration = useCallback( (source: string): number | null => { const normalizedSource = String(source || '').trim(); if (!normalizedSource) return null; const duration = durationBySource[normalizedSource]; if (Number.isFinite(duration) && Number(duration) > 0) { return Number(duration); } return null; }, [durationBySource], ); const getDurationNote = useCallback( (source: string): string => { return formatDurationNote(getDuration(source)); }, [getDuration], ); return { durationBySource, getDuration, getDurationNote, isProbing, }; } /** * Build duration probe targets from constructor state. * Utility to simplify target array creation in constructor.tsx. */ export function buildDurationProbeTargets({ backgroundVideoUrl, backgroundAudioUrl, selectedElement, newTransitionVideoUrl, elements, isMediaElementType, isVideoPlayerElementType, isNavigationElementType, }: { backgroundVideoUrl?: string; backgroundAudioUrl?: string; selectedElement?: { type: string; mediaUrl?: string; } | null; newTransitionVideoUrl?: string; elements?: Array<{ type: string; transitionVideoUrl?: string; reverseVideoUrl?: string; }>; isMediaElementType: (type: string) => boolean; isVideoPlayerElementType: (type: string) => boolean; isNavigationElementType: (type: string) => boolean; }): DurationProbeTarget[] { const targets: DurationProbeTarget[] = []; if (backgroundVideoUrl) { targets.push({ source: backgroundVideoUrl, mediaType: 'video' }); } if (backgroundAudioUrl) { targets.push({ source: backgroundAudioUrl, mediaType: 'audio' }); } if ( selectedElement && isMediaElementType(selectedElement.type) && selectedElement.mediaUrl ) { targets.push({ source: selectedElement.mediaUrl, mediaType: isVideoPlayerElementType(selectedElement.type) ? 'video' : 'audio', }); } if (newTransitionVideoUrl) { targets.push({ source: newTransitionVideoUrl, mediaType: 'video' }); } elements?.forEach((element) => { if (!isNavigationElementType(element.type)) return; if (element.transitionVideoUrl) { targets.push({ source: element.transitionVideoUrl, mediaType: 'video' }); } if (element.reverseVideoUrl) { targets.push({ source: element.reverseVideoUrl, mediaType: 'video' }); } }); return targets; } export default useMediaDurationProbe;