UNPKG

unified-video-framework

Version:

Cross-platform video player framework supporting iOS, Android, Web, Smart TVs (Samsung/LG), Roku, and more

158 lines 6.45 kB
import { useState, useEffect, useCallback, useRef } from 'react'; export function useChapters(options = {}) { const { videoElement, chapters: initialChapters, config = { enabled: true }, onSegmentEntered, onSegmentSkipped, onSkipButtonShown, onSkipButtonHidden } = options; const [currentSegment, setCurrentSegment] = useState(null); const [chapters, setChapters] = useState(initialChapters || null); const [isSkipButtonVisible, setIsSkipButtonVisible] = useState(false); const timeUpdateHandlerRef = useRef(null); const previousSegmentRef = useRef(null); const getCurrentSegment = useCallback((currentTime) => { if (!chapters) return null; return chapters.segments.find(segment => currentTime >= segment.startTime && currentTime < segment.endTime) || null; }, [chapters]); const handleTimeUpdate = useCallback(() => { if (!videoElement || !chapters) return; const newSegment = getCurrentSegment(videoElement.currentTime); if (newSegment !== currentSegment) { if (currentSegment) { setIsSkipButtonVisible(false); onSkipButtonHidden?.(currentSegment); } previousSegmentRef.current = currentSegment; setCurrentSegment(newSegment); if (newSegment) { onSegmentEntered?.(newSegment); if (shouldShowSkipButton(newSegment)) { setIsSkipButtonVisible(true); onSkipButtonShown?.(newSegment); } } } }, [videoElement, chapters, currentSegment, onSegmentEntered, onSkipButtonShown, onSkipButtonHidden, getCurrentSegment]); const shouldShowSkipButton = useCallback((segment) => { if (!config.userPreferences?.showSkipButtons) { return false; } if (segment.type === 'content') { return segment.showSkipButton === true; } return segment.showSkipButton !== false; }, [config]); useEffect(() => { if (!videoElement) return; const handler = () => handleTimeUpdate(); timeUpdateHandlerRef.current = handler; videoElement.addEventListener('timeupdate', handler); return () => { videoElement.removeEventListener('timeupdate', handler); }; }, [videoElement, handleTimeUpdate]); const loadChapters = useCallback(async (newChapters) => { if (!newChapters.videoId || !newChapters.duration || !Array.isArray(newChapters.segments)) { throw new Error('Invalid chapters data'); } const sortedChapters = { ...newChapters, segments: [...newChapters.segments].sort((a, b) => a.startTime - b.startTime) }; setChapters(sortedChapters); if (videoElement) { const segment = getCurrentSegment(videoElement.currentTime); setCurrentSegment(segment); } }, [videoElement, getCurrentSegment]); const skipToSegment = useCallback((segmentId) => { if (!chapters || !videoElement) return; const segment = chapters.segments.find(s => s.id === segmentId); if (!segment) return; const fromSegment = currentSegment; if (fromSegment) { onSegmentSkipped?.(fromSegment, segment); } videoElement.currentTime = segment.startTime; }, [chapters, videoElement, currentSegment, onSegmentSkipped]); const skipCurrentSegment = useCallback(() => { if (!currentSegment || !chapters || !videoElement) return; const sortedSegments = [...chapters.segments].sort((a, b) => a.startTime - b.startTime); const currentIndex = sortedSegments.findIndex(s => s.id === currentSegment.id); if (currentIndex === -1) return; let nextSegment; for (let i = currentIndex + 1; i < sortedSegments.length; i++) { if (sortedSegments[i].type === 'content') { nextSegment = sortedSegments[i]; break; } } const targetTime = nextSegment ? nextSegment.startTime : currentSegment.endTime; onSegmentSkipped?.(currentSegment, nextSegment); videoElement.currentTime = targetTime; }, [currentSegment, chapters, videoElement, onSegmentSkipped]); const getSegmentsByType = useCallback((type) => { if (!chapters) return []; return chapters.segments.filter(segment => segment.type === type); }, [chapters]); const hasSegmentType = useCallback((type) => { return getSegmentsByType(type).length > 0; }, [getSegmentsByType]); const getChapterMarkers = useCallback(() => { if (!chapters) return []; const segmentColors = { intro: '#ff5722', recap: '#ffc107', content: '#4caf50', credits: '#9c27b0', ad: '#f44336' }; return chapters.segments .filter(segment => segment.type !== 'content') .map(segment => ({ position: (segment.startTime / chapters.duration) * 100, segment, color: segmentColors[segment.type] })); }, [chapters]); const formatTime = useCallback((seconds) => { if (!seconds || isNaN(seconds)) return '00:00'; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); if (hours > 0) { return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } else { return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } }, []); const isInSegment = useCallback((segmentId) => { return currentSegment?.id === segmentId; }, [currentSegment]); useEffect(() => { if (initialChapters) { loadChapters(initialChapters); } }, [initialChapters, loadChapters]); return { currentSegment, chapters, isSkipButtonVisible, loadChapters, skipToSegment, skipCurrentSegment, getSegmentsByType, hasSegmentType, getChapterMarkers, formatTime, isInSegment }; } //# sourceMappingURL=useChapters.js.map