unified-video-framework 1.4.151 → 1.4.153

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/package.json +1 -1
  2. package/packages/core/dist/chapter-manager.d.ts +39 -0
  3. package/packages/core/dist/chapter-manager.d.ts.map +1 -0
  4. package/packages/core/dist/chapter-manager.js +173 -0
  5. package/packages/core/dist/chapter-manager.js.map +1 -0
  6. package/packages/core/dist/index.d.ts +2 -0
  7. package/packages/core/dist/index.d.ts.map +1 -1
  8. package/packages/core/dist/index.js +1 -0
  9. package/packages/core/dist/index.js.map +1 -1
  10. package/packages/core/dist/interfaces/IVideoPlayer.d.ts +10 -0
  11. package/packages/core/dist/interfaces/IVideoPlayer.d.ts.map +1 -1
  12. package/packages/core/dist/interfaces.d.ts +33 -1
  13. package/packages/core/dist/interfaces.d.ts.map +1 -1
  14. package/packages/core/package.json +2 -2
  15. package/packages/core/src/chapter-manager.ts +290 -0
  16. package/packages/core/src/index.ts +4 -0
  17. package/packages/core/src/interfaces/IVideoPlayer.ts +11 -0
  18. package/packages/core/src/interfaces.ts +47 -1
  19. package/packages/web/dist/WebPlayer.d.ts +24 -1
  20. package/packages/web/dist/WebPlayer.d.ts.map +1 -1
  21. package/packages/web/dist/WebPlayer.js +472 -1
  22. package/packages/web/dist/WebPlayer.js.map +1 -1
  23. package/packages/web/dist/chapters/ChapterManager.d.ts +38 -0
  24. package/packages/web/dist/chapters/ChapterManager.d.ts.map +1 -0
  25. package/packages/web/dist/chapters/ChapterManager.js +291 -0
  26. package/packages/web/dist/chapters/ChapterManager.js.map +1 -0
  27. package/packages/web/dist/chapters/SkipButtonController.d.ts +31 -0
  28. package/packages/web/dist/chapters/SkipButtonController.d.ts.map +1 -0
  29. package/packages/web/dist/chapters/SkipButtonController.js +213 -0
  30. package/packages/web/dist/chapters/SkipButtonController.js.map +1 -0
  31. package/packages/web/dist/chapters/UserPreferencesManager.d.ts +25 -0
  32. package/packages/web/dist/chapters/UserPreferencesManager.d.ts.map +1 -0
  33. package/packages/web/dist/chapters/UserPreferencesManager.js +232 -0
  34. package/packages/web/dist/chapters/UserPreferencesManager.js.map +1 -0
  35. package/packages/web/dist/chapters/index.d.ts +12 -0
  36. package/packages/web/dist/chapters/index.d.ts.map +1 -0
  37. package/packages/web/dist/chapters/index.js +8 -0
  38. package/packages/web/dist/chapters/index.js.map +1 -0
  39. package/packages/web/dist/chapters/types/ChapterTypes.d.ts +98 -0
  40. package/packages/web/dist/chapters/types/ChapterTypes.d.ts.map +1 -0
  41. package/packages/web/dist/chapters/types/ChapterTypes.js +31 -0
  42. package/packages/web/dist/chapters/types/ChapterTypes.js.map +1 -0
  43. package/packages/web/dist/index.d.ts +1 -1
  44. package/packages/web/dist/index.d.ts.map +1 -1
  45. package/packages/web/dist/index.js +1 -1
  46. package/packages/web/dist/index.js.map +1 -1
  47. package/packages/web/dist/paywall/EmailAuthController.d.ts +1 -1
  48. package/packages/web/dist/paywall/EmailAuthController.d.ts.map +1 -1
  49. package/packages/web/dist/paywall/PaywallController.d.ts +1 -1
  50. package/packages/web/dist/paywall/PaywallController.d.ts.map +1 -1
  51. package/packages/web/dist/react/WebPlayerView.d.ts +2 -2
  52. package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
  53. package/packages/web/dist/react/WebPlayerViewWithEPG.d.ts +2 -2
  54. package/packages/web/dist/react/WebPlayerViewWithEPG.d.ts.map +1 -1
  55. package/packages/web/dist/react/components/ChapterProgress.d.ts +22 -0
  56. package/packages/web/dist/react/components/ChapterProgress.d.ts.map +1 -0
  57. package/packages/web/dist/react/components/ChapterProgress.js +101 -0
  58. package/packages/web/dist/react/components/ChapterProgress.js.map +1 -0
  59. package/packages/web/dist/react/components/SkipButton.d.ts +18 -0
  60. package/packages/web/dist/react/components/SkipButton.d.ts.map +1 -0
  61. package/packages/web/dist/react/components/SkipButton.js +156 -0
  62. package/packages/web/dist/react/components/SkipButton.js.map +1 -0
  63. package/packages/web/dist/react/hooks/useChapters.d.ts +29 -0
  64. package/packages/web/dist/react/hooks/useChapters.d.ts.map +1 -0
  65. package/packages/web/dist/react/hooks/useChapters.js +158 -0
  66. package/packages/web/dist/react/hooks/useChapters.js.map +1 -0
  67. package/packages/web/package.json +3 -3
  68. package/packages/web/src/SecureVideoPlayer.ts +1 -1
  69. package/packages/web/src/WebPlayer.ts +587 -3
  70. package/packages/web/src/__tests__/WebPlayer.test.ts +1 -1
  71. package/packages/web/src/__tests__/epg-integration.test.ts +1 -1
  72. package/packages/web/src/chapters/ChapterManager.ts +464 -0
  73. package/packages/web/src/chapters/SkipButtonController.ts +353 -0
  74. package/packages/web/src/chapters/UserPreferencesManager.ts +324 -0
  75. package/packages/web/src/chapters/index.ts +34 -0
  76. package/packages/web/src/chapters/types/ChapterTypes.ts +236 -0
  77. package/packages/web/src/index.ts +1 -1
  78. package/packages/web/src/paywall/EmailAuthController.ts +1 -1
  79. package/packages/web/src/paywall/PaywallController.ts +1 -1
  80. package/packages/web/src/react/EPG.ts +1 -1
  81. package/packages/web/src/react/WebPlayerView.tsx +2 -2
  82. package/packages/web/src/react/WebPlayerViewWithEPG.tsx +3 -3
  83. package/packages/web/src/react/components/ChapterProgress.tsx +207 -0
  84. package/packages/web/src/react/components/EPGNavigationControls.tsx +1 -1
  85. package/packages/web/src/react/components/EPGOverlay-improved-positioning.tsx +1 -1
  86. package/packages/web/src/react/components/EPGOverlay.tsx +1 -1
  87. package/packages/web/src/react/components/EPGProgramGrid.tsx +1 -1
  88. package/packages/web/src/react/components/EPGTimelineHeader.tsx +1 -1
  89. package/packages/web/src/react/components/SkipButton.tsx +278 -0
  90. package/packages/web/src/react/hooks/useChapters.ts +308 -0
  91. package/packages/web/src/react/types/EPGTypes.ts +1 -1
  92. package/packages/web/src/react/utils/EPGUtils.ts +1 -1
  93. package/packages/web/src/test/epg-test.ts +1 -1
@@ -0,0 +1,278 @@
1
+ /**
2
+ * React component for skip button
3
+ */
4
+
5
+ import React, { useState, useEffect, useRef } from 'react';
6
+ import {
7
+ VideoSegment,
8
+ SkipButtonPosition,
9
+ DEFAULT_SKIP_LABELS
10
+ } from '../../chapters/types/ChapterTypes';
11
+
12
+ export interface SkipButtonProps {
13
+ /** Current segment to show skip button for */
14
+ segment: VideoSegment | null;
15
+
16
+ /** Whether button is visible */
17
+ visible?: boolean;
18
+
19
+ /** Button position */
20
+ position?: SkipButtonPosition;
21
+
22
+ /** Auto-hide delay in milliseconds */
23
+ autoHideDelay?: number;
24
+
25
+ /** Custom skip button text */
26
+ skipLabel?: string;
27
+
28
+ /** Callback when skip button is clicked */
29
+ onSkip?: (segment: VideoSegment) => void;
30
+
31
+ /** Callback when button is shown */
32
+ onShow?: (segment: VideoSegment) => void;
33
+
34
+ /** Callback when button is hidden */
35
+ onHide?: (segment: VideoSegment, reason: string) => void;
36
+
37
+ /** Custom CSS class name */
38
+ className?: string;
39
+
40
+ /** Custom styles */
41
+ style?: React.CSSProperties;
42
+
43
+ /** Enable auto-skip countdown */
44
+ enableAutoSkip?: boolean;
45
+
46
+ /** Auto-skip delay in seconds */
47
+ autoSkipDelay?: number;
48
+ }
49
+
50
+ export const SkipButton: React.FC<SkipButtonProps> = ({
51
+ segment,
52
+ visible = false,
53
+ position = 'bottom-right',
54
+ autoHideDelay = 5000,
55
+ skipLabel,
56
+ onSkip,
57
+ onShow,
58
+ onHide,
59
+ className = '',
60
+ style = {},
61
+ enableAutoSkip = false,
62
+ autoSkipDelay = 10
63
+ }) => {
64
+ // State
65
+ const [isVisible, setIsVisible] = useState(visible);
66
+ const [isAutoSkip, setIsAutoSkip] = useState(false);
67
+ const [countdown, setCountdown] = useState<number | null>(null);
68
+
69
+ // Refs
70
+ const autoHideTimeoutRef = useRef<NodeJS.Timeout | null>(null);
71
+ const autoSkipTimeoutRef = useRef<NodeJS.Timeout | null>(null);
72
+ const countdownIntervalRef = useRef<NodeJS.Timeout | null>(null);
73
+ const previousSegmentRef = useRef<VideoSegment | null>(null);
74
+
75
+ /**
76
+ * Clear all timeouts
77
+ */
78
+ const clearTimeouts = () => {
79
+ if (autoHideTimeoutRef.current) {
80
+ clearTimeout(autoHideTimeoutRef.current);
81
+ autoHideTimeoutRef.current = null;
82
+ }
83
+ if (autoSkipTimeoutRef.current) {
84
+ clearTimeout(autoSkipTimeoutRef.current);
85
+ autoSkipTimeoutRef.current = null;
86
+ }
87
+ if (countdownIntervalRef.current) {
88
+ clearInterval(countdownIntervalRef.current);
89
+ countdownIntervalRef.current = null;
90
+ }
91
+ };
92
+
93
+ /**
94
+ * Start auto-skip countdown
95
+ */
96
+ const startAutoSkip = (segment: VideoSegment, delay: number) => {
97
+ setIsAutoSkip(true);
98
+ setCountdown(delay);
99
+
100
+ // Start countdown interval
101
+ countdownIntervalRef.current = setInterval(() => {
102
+ setCountdown((prev) => {
103
+ if (prev === null || prev <= 1) {
104
+ // Auto-skip triggered
105
+ clearTimeouts();
106
+ onSkip?.(segment);
107
+ handleHide('timeout');
108
+ return null;
109
+ }
110
+ return prev - 1;
111
+ });
112
+ }, 1000);
113
+
114
+ // Set backup timeout
115
+ autoSkipTimeoutRef.current = setTimeout(() => {
116
+ clearTimeouts();
117
+ onSkip?.(segment);
118
+ handleHide('timeout');
119
+ }, delay * 1000);
120
+ };
121
+
122
+ /**
123
+ * Handle button click
124
+ */
125
+ const handleSkip = () => {
126
+ if (!segment) return;
127
+
128
+ clearTimeouts();
129
+ onSkip?.(segment);
130
+ handleHide('user-action');
131
+ };
132
+
133
+ /**
134
+ * Handle button show
135
+ */
136
+ const handleShow = () => {
137
+ if (!segment) return;
138
+
139
+ setIsVisible(true);
140
+ onShow?.(segment);
141
+
142
+ // Start auto-hide timer
143
+ if (autoHideDelay > 0) {
144
+ autoHideTimeoutRef.current = setTimeout(() => {
145
+ handleHide('timeout');
146
+ }, autoHideDelay);
147
+ }
148
+
149
+ // Start auto-skip if enabled
150
+ if (enableAutoSkip && segment.autoSkip && segment.autoSkipDelay) {
151
+ startAutoSkip(segment, segment.autoSkipDelay);
152
+ }
153
+ };
154
+
155
+ /**
156
+ * Handle button hide
157
+ */
158
+ const handleHide = (reason: string = 'manual') => {
159
+ if (!segment) return;
160
+
161
+ clearTimeouts();
162
+ setIsVisible(false);
163
+ setIsAutoSkip(false);
164
+ setCountdown(null);
165
+ onHide?.(segment, reason);
166
+ };
167
+
168
+ // Effect to handle visibility changes
169
+ useEffect(() => {
170
+ if (visible && segment && segment !== previousSegmentRef.current) {
171
+ previousSegmentRef.current = segment;
172
+ handleShow();
173
+ } else if (!visible || !segment) {
174
+ if (previousSegmentRef.current) {
175
+ handleHide('segment-end');
176
+ }
177
+ previousSegmentRef.current = null;
178
+ }
179
+ }, [visible, segment]);
180
+
181
+ // Cleanup on unmount
182
+ useEffect(() => {
183
+ return () => {
184
+ clearTimeouts();
185
+ };
186
+ }, []);
187
+
188
+ // Don't render if no segment or not visible
189
+ if (!segment || !isVisible) {
190
+ return null;
191
+ }
192
+
193
+ // Get skip label
194
+ const buttonText = skipLabel || segment.skipLabel || DEFAULT_SKIP_LABELS[segment.type];
195
+ const displayText = isAutoSkip && countdown !== null
196
+ ? `${buttonText} (${countdown})`
197
+ : buttonText;
198
+
199
+ // Get position classes
200
+ const positionClass = `uvf-skip-button-${position}`;
201
+ const segmentClass = `uvf-skip-${segment.type}`;
202
+ const autoSkipClass = isAutoSkip ? 'auto-skip' : '';
203
+ const countdownClass = isAutoSkip && countdown !== null ? 'countdown' : '';
204
+
205
+ // Combine classes
206
+ const buttonClasses = [
207
+ 'uvf-skip-button',
208
+ 'visible',
209
+ positionClass,
210
+ segmentClass,
211
+ autoSkipClass,
212
+ countdownClass,
213
+ className
214
+ ].filter(Boolean).join(' ');
215
+
216
+ // Default styles based on position
217
+ const defaultStyles: React.CSSProperties = {
218
+ position: 'absolute',
219
+ zIndex: 1000,
220
+ ...style
221
+ };
222
+
223
+ switch (position) {
224
+ case 'bottom-right':
225
+ Object.assign(defaultStyles, {
226
+ bottom: '100px',
227
+ right: '30px'
228
+ });
229
+ break;
230
+ case 'bottom-left':
231
+ Object.assign(defaultStyles, {
232
+ bottom: '100px',
233
+ left: '30px'
234
+ });
235
+ break;
236
+ case 'top-right':
237
+ Object.assign(defaultStyles, {
238
+ top: '30px',
239
+ right: '30px'
240
+ });
241
+ break;
242
+ case 'top-left':
243
+ Object.assign(defaultStyles, {
244
+ top: '30px',
245
+ left: '30px'
246
+ });
247
+ break;
248
+ }
249
+
250
+ return (
251
+ <button
252
+ type="button"
253
+ className={buttonClasses}
254
+ style={defaultStyles}
255
+ onClick={handleSkip}
256
+ aria-label={`${buttonText} - ${segment.title || segment.type}`}
257
+ >
258
+ {displayText}
259
+
260
+ {/* Progress bar for auto-skip countdown */}
261
+ {isAutoSkip && countdown !== null && (
262
+ <div
263
+ className="uvf-skip-countdown-progress"
264
+ style={{
265
+ position: 'absolute',
266
+ bottom: 0,
267
+ left: 0,
268
+ height: '3px',
269
+ backgroundColor: 'currentColor',
270
+ width: `${((autoSkipDelay - countdown) / autoSkipDelay) * 100}%`,
271
+ transition: 'width 1s linear',
272
+ borderRadius: '0 0 6px 6px'
273
+ }}
274
+ />
275
+ )}
276
+ </button>
277
+ );
278
+ };
@@ -0,0 +1,308 @@
1
+ /**
2
+ * React hook for video chapters and skip functionality
3
+ */
4
+
5
+ import { useState, useEffect, useCallback, useRef } from 'react';
6
+ import {
7
+ VideoSegment,
8
+ VideoChapters,
9
+ ChapterConfig,
10
+ ChapterEvents,
11
+ SegmentType
12
+ } from '../../chapters/types/ChapterTypes';
13
+
14
+ export interface UseChaptersOptions {
15
+ videoElement?: HTMLVideoElement | null;
16
+ chapters?: VideoChapters;
17
+ config?: ChapterConfig;
18
+ onSegmentEntered?: (segment: VideoSegment) => void;
19
+ onSegmentSkipped?: (fromSegment: VideoSegment, toSegment?: VideoSegment) => void;
20
+ onSkipButtonShown?: (segment: VideoSegment) => void;
21
+ onSkipButtonHidden?: (segment: VideoSegment) => void;
22
+ }
23
+
24
+ export interface UseChaptersResult {
25
+ // State
26
+ currentSegment: VideoSegment | null;
27
+ chapters: VideoChapters | null;
28
+ isSkipButtonVisible: boolean;
29
+
30
+ // Actions
31
+ loadChapters: (chapters: VideoChapters) => Promise<void>;
32
+ skipToSegment: (segmentId: string) => void;
33
+ skipCurrentSegment: () => void;
34
+
35
+ // Queries
36
+ getSegmentsByType: (type: SegmentType) => VideoSegment[];
37
+ hasSegmentType: (type: SegmentType) => boolean;
38
+ getChapterMarkers: () => Array<{
39
+ position: number;
40
+ segment: VideoSegment;
41
+ color: string;
42
+ }>;
43
+
44
+ // Utils
45
+ formatTime: (seconds: number) => string;
46
+ isInSegment: (segmentId: string) => boolean;
47
+ }
48
+
49
+ export function useChapters(options: UseChaptersOptions = {}): UseChaptersResult {
50
+ const {
51
+ videoElement,
52
+ chapters: initialChapters,
53
+ config = { enabled: true },
54
+ onSegmentEntered,
55
+ onSegmentSkipped,
56
+ onSkipButtonShown,
57
+ onSkipButtonHidden
58
+ } = options;
59
+
60
+ // State
61
+ const [currentSegment, setCurrentSegment] = useState<VideoSegment | null>(null);
62
+ const [chapters, setChapters] = useState<VideoChapters | null>(initialChapters || null);
63
+ const [isSkipButtonVisible, setIsSkipButtonVisible] = useState(false);
64
+
65
+ // Refs
66
+ const timeUpdateHandlerRef = useRef<(() => void) | null>(null);
67
+ const previousSegmentRef = useRef<VideoSegment | null>(null);
68
+
69
+ /**
70
+ * Get current segment at given time
71
+ */
72
+ const getCurrentSegment = useCallback((currentTime: number): VideoSegment | null => {
73
+ if (!chapters) return null;
74
+
75
+ return chapters.segments.find(segment =>
76
+ currentTime >= segment.startTime && currentTime < segment.endTime
77
+ ) || null;
78
+ }, [chapters]);
79
+
80
+ /**
81
+ * Handle time update
82
+ */
83
+ const handleTimeUpdate = useCallback(() => {
84
+ if (!videoElement || !chapters) return;
85
+
86
+ const newSegment = getCurrentSegment(videoElement.currentTime);
87
+
88
+ if (newSegment !== currentSegment) {
89
+ // Segment changed
90
+ if (currentSegment) {
91
+ // Exiting current segment
92
+ setIsSkipButtonVisible(false);
93
+ onSkipButtonHidden?.(currentSegment);
94
+ }
95
+
96
+ previousSegmentRef.current = currentSegment;
97
+ setCurrentSegment(newSegment);
98
+
99
+ if (newSegment) {
100
+ // Entering new segment
101
+ onSegmentEntered?.(newSegment);
102
+
103
+ // Show skip button for skippable segments
104
+ if (shouldShowSkipButton(newSegment)) {
105
+ setIsSkipButtonVisible(true);
106
+ onSkipButtonShown?.(newSegment);
107
+ }
108
+ }
109
+ }
110
+ }, [videoElement, chapters, currentSegment, onSegmentEntered, onSkipButtonShown, onSkipButtonHidden, getCurrentSegment]);
111
+
112
+ /**
113
+ * Check if segment should show skip button
114
+ */
115
+ const shouldShowSkipButton = useCallback((segment: VideoSegment): boolean => {
116
+ if (!config.userPreferences?.showSkipButtons) {
117
+ return false;
118
+ }
119
+
120
+ // Don't show for content segments by default
121
+ if (segment.type === 'content') {
122
+ return segment.showSkipButton === true;
123
+ }
124
+
125
+ // Show for other segment types unless explicitly disabled
126
+ return segment.showSkipButton !== false;
127
+ }, [config]);
128
+
129
+ /**
130
+ * Set up time update listener
131
+ */
132
+ useEffect(() => {
133
+ if (!videoElement) return;
134
+
135
+ const handler = () => handleTimeUpdate();
136
+ timeUpdateHandlerRef.current = handler;
137
+
138
+ videoElement.addEventListener('timeupdate', handler);
139
+
140
+ return () => {
141
+ videoElement.removeEventListener('timeupdate', handler);
142
+ };
143
+ }, [videoElement, handleTimeUpdate]);
144
+
145
+ /**
146
+ * Load chapters
147
+ */
148
+ const loadChapters = useCallback(async (newChapters: VideoChapters): Promise<void> => {
149
+ // Validate chapters
150
+ if (!newChapters.videoId || !newChapters.duration || !Array.isArray(newChapters.segments)) {
151
+ throw new Error('Invalid chapters data');
152
+ }
153
+
154
+ // Sort segments by start time
155
+ const sortedChapters = {
156
+ ...newChapters,
157
+ segments: [...newChapters.segments].sort((a, b) => a.startTime - b.startTime)
158
+ };
159
+
160
+ setChapters(sortedChapters);
161
+
162
+ // Check current segment if video is playing
163
+ if (videoElement) {
164
+ const segment = getCurrentSegment(videoElement.currentTime);
165
+ setCurrentSegment(segment);
166
+ }
167
+ }, [videoElement, getCurrentSegment]);
168
+
169
+ /**
170
+ * Skip to specific segment
171
+ */
172
+ const skipToSegment = useCallback((segmentId: string) => {
173
+ if (!chapters || !videoElement) return;
174
+
175
+ const segment = chapters.segments.find(s => s.id === segmentId);
176
+ if (!segment) return;
177
+
178
+ const fromSegment = currentSegment;
179
+
180
+ // Emit skip event
181
+ if (fromSegment) {
182
+ onSegmentSkipped?.(fromSegment, segment);
183
+ }
184
+
185
+ // Seek to segment start
186
+ videoElement.currentTime = segment.startTime;
187
+ }, [chapters, videoElement, currentSegment, onSegmentSkipped]);
188
+
189
+ /**
190
+ * Skip current segment
191
+ */
192
+ const skipCurrentSegment = useCallback(() => {
193
+ if (!currentSegment || !chapters || !videoElement) return;
194
+
195
+ // Find next content segment
196
+ const sortedSegments = [...chapters.segments].sort((a, b) => a.startTime - b.startTime);
197
+ const currentIndex = sortedSegments.findIndex(s => s.id === currentSegment.id);
198
+
199
+ if (currentIndex === -1) return;
200
+
201
+ // Find next content segment
202
+ let nextSegment: VideoSegment | undefined;
203
+ for (let i = currentIndex + 1; i < sortedSegments.length; i++) {
204
+ if (sortedSegments[i].type === 'content') {
205
+ nextSegment = sortedSegments[i];
206
+ break;
207
+ }
208
+ }
209
+
210
+ const targetTime = nextSegment ? nextSegment.startTime : currentSegment.endTime;
211
+
212
+ // Emit skip event
213
+ onSegmentSkipped?.(currentSegment, nextSegment);
214
+
215
+ // Seek to target time
216
+ videoElement.currentTime = targetTime;
217
+ }, [currentSegment, chapters, videoElement, onSegmentSkipped]);
218
+
219
+ /**
220
+ * Get segments by type
221
+ */
222
+ const getSegmentsByType = useCallback((type: SegmentType): VideoSegment[] => {
223
+ if (!chapters) return [];
224
+ return chapters.segments.filter(segment => segment.type === type);
225
+ }, [chapters]);
226
+
227
+ /**
228
+ * Check if has segment type
229
+ */
230
+ const hasSegmentType = useCallback((type: SegmentType): boolean => {
231
+ return getSegmentsByType(type).length > 0;
232
+ }, [getSegmentsByType]);
233
+
234
+ /**
235
+ * Get chapter markers for progress bar
236
+ */
237
+ const getChapterMarkers = useCallback(() => {
238
+ if (!chapters) return [];
239
+
240
+ const segmentColors: Record<SegmentType, string> = {
241
+ intro: '#ff5722',
242
+ recap: '#ffc107',
243
+ content: '#4caf50',
244
+ credits: '#9c27b0',
245
+ ad: '#f44336'
246
+ };
247
+
248
+ return chapters.segments
249
+ .filter(segment => segment.type !== 'content')
250
+ .map(segment => ({
251
+ position: (segment.startTime / chapters.duration) * 100,
252
+ segment,
253
+ color: segmentColors[segment.type]
254
+ }));
255
+ }, [chapters]);
256
+
257
+ /**
258
+ * Format time as MM:SS or HH:MM:SS
259
+ */
260
+ const formatTime = useCallback((seconds: number): string => {
261
+ if (!seconds || isNaN(seconds)) return '00:00';
262
+
263
+ const hours = Math.floor(seconds / 3600);
264
+ const minutes = Math.floor((seconds % 3600) / 60);
265
+ const secs = Math.floor(seconds % 60);
266
+
267
+ if (hours > 0) {
268
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
269
+ } else {
270
+ return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
271
+ }
272
+ }, []);
273
+
274
+ /**
275
+ * Check if currently in specific segment
276
+ */
277
+ const isInSegment = useCallback((segmentId: string): boolean => {
278
+ return currentSegment?.id === segmentId;
279
+ }, [currentSegment]);
280
+
281
+ // Load initial chapters
282
+ useEffect(() => {
283
+ if (initialChapters) {
284
+ loadChapters(initialChapters);
285
+ }
286
+ }, [initialChapters, loadChapters]);
287
+
288
+ return {
289
+ // State
290
+ currentSegment,
291
+ chapters,
292
+ isSkipButtonVisible,
293
+
294
+ // Actions
295
+ loadChapters,
296
+ skipToSegment,
297
+ skipCurrentSegment,
298
+
299
+ // Queries
300
+ getSegmentsByType,
301
+ hasSegmentType,
302
+ getChapterMarkers,
303
+
304
+ // Utils
305
+ formatTime,
306
+ isInSegment
307
+ };
308
+ }
@@ -122,4 +122,4 @@ export interface ProgramBlock extends TimeRange {
122
122
 
123
123
  export type EPGViewMode = 'grid' | 'list' | 'compact';
124
124
  export type EPGSortBy = 'time' | 'channel' | 'category' | 'rating';
125
- export type EPGFilterType = 'all' | 'favorites' | 'recordings' | 'reminders' | 'category';
125
+ export type EPGFilterType = 'all' | 'favorites' | 'recordings' | 'reminders' | 'category';
@@ -285,4 +285,4 @@ export const throttle = <T extends (...args: any[]) => any>(
285
285
  setTimeout(() => inThrottle = false, limit);
286
286
  }
287
287
  };
288
- };
288
+ };
@@ -119,4 +119,4 @@ export { runEPGTests };
119
119
  // Run tests if this file is executed directly
120
120
  if (typeof window === 'undefined') {
121
121
  runEPGTests();
122
- }
122
+ }