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
JavaScript
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