unified-video-framework 1.4.151 → 1.4.154

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 (86) 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/src/chapter-manager.ts +290 -0
  15. package/packages/core/src/index.ts +4 -0
  16. package/packages/core/src/interfaces/IVideoPlayer.ts +11 -0
  17. package/packages/core/src/interfaces.ts +47 -1
  18. package/packages/web/dist/WebPlayer.d.ts +24 -1
  19. package/packages/web/dist/WebPlayer.d.ts.map +1 -1
  20. package/packages/web/dist/WebPlayer.js +472 -1
  21. package/packages/web/dist/WebPlayer.js.map +1 -1
  22. package/packages/web/dist/chapters/ChapterManager.d.ts +38 -0
  23. package/packages/web/dist/chapters/ChapterManager.d.ts.map +1 -0
  24. package/packages/web/dist/chapters/ChapterManager.js +291 -0
  25. package/packages/web/dist/chapters/ChapterManager.js.map +1 -0
  26. package/packages/web/dist/chapters/SkipButtonController.d.ts +31 -0
  27. package/packages/web/dist/chapters/SkipButtonController.d.ts.map +1 -0
  28. package/packages/web/dist/chapters/SkipButtonController.js +213 -0
  29. package/packages/web/dist/chapters/SkipButtonController.js.map +1 -0
  30. package/packages/web/dist/chapters/UserPreferencesManager.d.ts +25 -0
  31. package/packages/web/dist/chapters/UserPreferencesManager.d.ts.map +1 -0
  32. package/packages/web/dist/chapters/UserPreferencesManager.js +232 -0
  33. package/packages/web/dist/chapters/UserPreferencesManager.js.map +1 -0
  34. package/packages/web/dist/chapters/index.d.ts +12 -0
  35. package/packages/web/dist/chapters/index.d.ts.map +1 -0
  36. package/packages/web/dist/chapters/index.js +8 -0
  37. package/packages/web/dist/chapters/index.js.map +1 -0
  38. package/packages/web/dist/chapters/types/ChapterTypes.d.ts +98 -0
  39. package/packages/web/dist/chapters/types/ChapterTypes.d.ts.map +1 -0
  40. package/packages/web/dist/chapters/types/ChapterTypes.js +31 -0
  41. package/packages/web/dist/chapters/types/ChapterTypes.js.map +1 -0
  42. package/packages/web/dist/index.d.ts +1 -1
  43. package/packages/web/dist/index.d.ts.map +1 -1
  44. package/packages/web/dist/index.js +1 -1
  45. package/packages/web/dist/index.js.map +1 -1
  46. package/packages/web/dist/react/WebPlayerView.d.ts +2 -2
  47. package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
  48. package/packages/web/dist/react/WebPlayerViewWithEPG.d.ts +2 -2
  49. package/packages/web/dist/react/WebPlayerViewWithEPG.d.ts.map +1 -1
  50. package/packages/web/dist/react/components/ChapterProgress.d.ts +22 -0
  51. package/packages/web/dist/react/components/ChapterProgress.d.ts.map +1 -0
  52. package/packages/web/dist/react/components/ChapterProgress.js +101 -0
  53. package/packages/web/dist/react/components/ChapterProgress.js.map +1 -0
  54. package/packages/web/dist/react/components/SkipButton.d.ts +18 -0
  55. package/packages/web/dist/react/components/SkipButton.d.ts.map +1 -0
  56. package/packages/web/dist/react/components/SkipButton.js +156 -0
  57. package/packages/web/dist/react/components/SkipButton.js.map +1 -0
  58. package/packages/web/dist/react/hooks/useChapters.d.ts +29 -0
  59. package/packages/web/dist/react/hooks/useChapters.d.ts.map +1 -0
  60. package/packages/web/dist/react/hooks/useChapters.js +158 -0
  61. package/packages/web/dist/react/hooks/useChapters.js.map +1 -0
  62. package/packages/web/package.json +0 -3
  63. package/packages/web/src/SecureVideoPlayer.ts +1 -1
  64. package/packages/web/src/WebPlayer.ts +587 -3
  65. package/packages/web/src/__tests__/WebPlayer.test.ts +1 -1
  66. package/packages/web/src/__tests__/epg-integration.test.ts +1 -1
  67. package/packages/web/src/chapters/ChapterManager.ts +464 -0
  68. package/packages/web/src/chapters/SkipButtonController.ts +353 -0
  69. package/packages/web/src/chapters/UserPreferencesManager.ts +324 -0
  70. package/packages/web/src/chapters/index.ts +34 -0
  71. package/packages/web/src/chapters/types/ChapterTypes.ts +236 -0
  72. package/packages/web/src/index.ts +1 -1
  73. package/packages/web/src/react/EPG.ts +1 -1
  74. package/packages/web/src/react/WebPlayerView.tsx +2 -2
  75. package/packages/web/src/react/WebPlayerViewWithEPG.tsx +3 -3
  76. package/packages/web/src/react/components/ChapterProgress.tsx +207 -0
  77. package/packages/web/src/react/components/EPGNavigationControls.tsx +1 -1
  78. package/packages/web/src/react/components/EPGOverlay-improved-positioning.tsx +1 -1
  79. package/packages/web/src/react/components/EPGOverlay.tsx +1 -1
  80. package/packages/web/src/react/components/EPGProgramGrid.tsx +1 -1
  81. package/packages/web/src/react/components/EPGTimelineHeader.tsx +1 -1
  82. package/packages/web/src/react/components/SkipButton.tsx +278 -0
  83. package/packages/web/src/react/hooks/useChapters.ts +308 -0
  84. package/packages/web/src/react/types/EPGTypes.ts +1 -1
  85. package/packages/web/src/react/utils/EPGUtils.ts +1 -1
  86. package/packages/web/src/test/epg-test.ts +1 -1
@@ -0,0 +1,464 @@
1
+ /**
2
+ * Core chapter management system for video segments and skip functionality
3
+ */
4
+
5
+ import {
6
+ VideoSegment,
7
+ VideoChapters,
8
+ ChapterConfig,
9
+ ChapterEvents,
10
+ ChapterMarker,
11
+ SegmentType,
12
+ DEFAULT_CHAPTER_CONFIG,
13
+ SEGMENT_COLORS
14
+ } from './types/ChapterTypes';
15
+ import { SkipButtonController } from './SkipButtonController';
16
+
17
+ export class ChapterManager {
18
+ private chapters: VideoChapters | null = null;
19
+ private currentSegment: VideoSegment | null = null;
20
+ private previousSegment: VideoSegment | null = null;
21
+ private skipButtonController: SkipButtonController;
22
+ private config: ChapterConfig;
23
+ private eventListeners: Map<keyof ChapterEvents, Function[]> = new Map();
24
+ private isDestroyed = false;
25
+
26
+ constructor(
27
+ private playerContainer: HTMLElement,
28
+ private videoElement: HTMLVideoElement,
29
+ config: ChapterConfig = DEFAULT_CHAPTER_CONFIG
30
+ ) {
31
+ // Merge config with defaults
32
+ this.config = { ...DEFAULT_CHAPTER_CONFIG, ...config };
33
+
34
+ // Initialize skip button controller
35
+ this.skipButtonController = new SkipButtonController(
36
+ playerContainer,
37
+ this.config,
38
+ (segment) => this.skipToNextSegment(segment),
39
+ (segment) => this.emit('skipButtonShown', { segment, currentTime: this.videoElement.currentTime }),
40
+ (segment, reason) => this.emit('skipButtonHidden', {
41
+ segment,
42
+ currentTime: this.videoElement.currentTime,
43
+ reason: reason as any
44
+ })
45
+ );
46
+
47
+ // Set up time update listener
48
+ this.setupTimeUpdateListener();
49
+
50
+ // Load chapters if provided in config
51
+ if (this.config.data) {
52
+ this.loadChapters(this.config.data);
53
+ } else if (this.config.dataUrl) {
54
+ this.loadChaptersFromUrl(this.config.dataUrl);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Load chapters data
60
+ */
61
+ public async loadChapters(chapters: VideoChapters): Promise<void> {
62
+ try {
63
+ // Validate chapters data
64
+ this.validateChapters(chapters);
65
+
66
+ this.chapters = chapters;
67
+ this.sortSegments();
68
+
69
+ // Emit loaded event
70
+ this.emit('chaptersLoaded', {
71
+ chapters: this.chapters,
72
+ segmentCount: this.chapters.segments.length
73
+ });
74
+
75
+ // Update chapter markers if enabled
76
+ if (this.config.showChapterMarkers) {
77
+ this.updateChapterMarkers();
78
+ }
79
+
80
+ // Check current segment
81
+ this.checkCurrentSegment(this.videoElement.currentTime);
82
+
83
+ } catch (error) {
84
+ this.emit('chaptersLoadError', {
85
+ error: error as Error
86
+ });
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Load chapters from URL
93
+ */
94
+ public async loadChaptersFromUrl(url: string): Promise<void> {
95
+ try {
96
+ const response = await fetch(url);
97
+ if (!response.ok) {
98
+ throw new Error(`Failed to load chapters: ${response.statusText}`);
99
+ }
100
+
101
+ const chapters: VideoChapters = await response.json();
102
+ await this.loadChapters(chapters);
103
+
104
+ } catch (error) {
105
+ this.emit('chaptersLoadError', {
106
+ error: error as Error,
107
+ url
108
+ });
109
+ throw error;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Get current segment at given time
115
+ */
116
+ public getCurrentSegment(currentTime: number): VideoSegment | null {
117
+ if (!this.chapters) return null;
118
+
119
+ return this.chapters.segments.find(segment =>
120
+ currentTime >= segment.startTime && currentTime < segment.endTime
121
+ ) || null;
122
+ }
123
+
124
+ /**
125
+ * Skip to next segment after current one
126
+ */
127
+ public skipToNextSegment(currentSegment: VideoSegment): void {
128
+ if (!this.chapters) return;
129
+
130
+ const nextSegment = this.getNextContentSegment(currentSegment);
131
+ const targetTime = nextSegment ? nextSegment.startTime : currentSegment.endTime;
132
+
133
+ // Emit skip event
134
+ this.emit('segmentSkipped', {
135
+ fromSegment: currentSegment,
136
+ toSegment: nextSegment || undefined,
137
+ skipMethod: 'button',
138
+ currentTime: this.videoElement.currentTime
139
+ });
140
+
141
+ // Seek to target time
142
+ this.videoElement.currentTime = targetTime;
143
+ }
144
+
145
+ /**
146
+ * Skip to specific segment by ID
147
+ */
148
+ public skipToSegment(segmentId: string): void {
149
+ if (!this.chapters) return;
150
+
151
+ const segment = this.chapters.segments.find(s => s.id === segmentId);
152
+ if (!segment) return;
153
+
154
+ const fromSegment = this.currentSegment;
155
+
156
+ // Emit skip event
157
+ if (fromSegment) {
158
+ this.emit('segmentSkipped', {
159
+ fromSegment,
160
+ toSegment: segment,
161
+ skipMethod: 'manual',
162
+ currentTime: this.videoElement.currentTime
163
+ });
164
+ }
165
+
166
+ // Seek to segment start
167
+ this.videoElement.currentTime = segment.startTime;
168
+ }
169
+
170
+ /**
171
+ * Get all segments
172
+ */
173
+ public getSegments(): VideoSegment[] {
174
+ return this.chapters?.segments || [];
175
+ }
176
+
177
+ /**
178
+ * Get segment by ID
179
+ */
180
+ public getSegment(segmentId: string): VideoSegment | null {
181
+ if (!this.chapters) return null;
182
+ return this.chapters.segments.find(s => s.id === segmentId) || null;
183
+ }
184
+
185
+ /**
186
+ * Get segments by type
187
+ */
188
+ public getSegmentsByType(type: SegmentType): VideoSegment[] {
189
+ if (!this.chapters) return [];
190
+ return this.chapters.segments.filter(s => s.type === type);
191
+ }
192
+
193
+ /**
194
+ * Get chapter markers for progress bar
195
+ */
196
+ public getChapterMarkers(): ChapterMarker[] {
197
+ if (!this.chapters || !this.config.showChapterMarkers) return [];
198
+
199
+ return this.chapters.segments
200
+ .filter(segment => segment.type !== 'content') // Don't show markers for content segments
201
+ .map(segment => ({
202
+ segment,
203
+ position: (segment.startTime / this.chapters!.duration) * 100,
204
+ color: SEGMENT_COLORS[segment.type],
205
+ label: segment.title || segment.type
206
+ }));
207
+ }
208
+
209
+ /**
210
+ * Update configuration
211
+ */
212
+ public updateConfig(newConfig: Partial<ChapterConfig>): void {
213
+ this.config = { ...this.config, ...newConfig };
214
+
215
+ // Update skip button position if changed
216
+ if (newConfig.skipButtonPosition) {
217
+ this.skipButtonController.updatePosition(newConfig.skipButtonPosition);
218
+ }
219
+
220
+ // Update chapter markers if setting changed
221
+ if ('showChapterMarkers' in newConfig) {
222
+ if (newConfig.showChapterMarkers) {
223
+ this.updateChapterMarkers();
224
+ } else {
225
+ this.removeChapterMarkers();
226
+ }
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Add event listener
232
+ */
233
+ public on<K extends keyof ChapterEvents>(event: K, listener: (data: ChapterEvents[K]) => void): void {
234
+ if (!this.eventListeners.has(event)) {
235
+ this.eventListeners.set(event, []);
236
+ }
237
+ this.eventListeners.get(event)!.push(listener);
238
+ }
239
+
240
+ /**
241
+ * Remove event listener
242
+ */
243
+ public off<K extends keyof ChapterEvents>(event: K, listener: (data: ChapterEvents[K]) => void): void {
244
+ const listeners = this.eventListeners.get(event);
245
+ if (listeners) {
246
+ const index = listeners.indexOf(listener);
247
+ if (index > -1) {
248
+ listeners.splice(index, 1);
249
+ }
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Destroy the chapter manager
255
+ */
256
+ public destroy(): void {
257
+ this.isDestroyed = true;
258
+ this.skipButtonController.destroy();
259
+ this.removeChapterMarkers();
260
+ this.eventListeners.clear();
261
+ this.chapters = null;
262
+ this.currentSegment = null;
263
+ this.previousSegment = null;
264
+ }
265
+
266
+ /**
267
+ * Check if chapters are loaded
268
+ */
269
+ public hasChapters(): boolean {
270
+ return this.chapters !== null && this.chapters.segments.length > 0;
271
+ }
272
+
273
+ /**
274
+ * Get current chapter data
275
+ */
276
+ public getChapters(): VideoChapters | null {
277
+ return this.chapters;
278
+ }
279
+
280
+ /**
281
+ * Set up time update listener
282
+ */
283
+ private setupTimeUpdateListener(): void {
284
+ const handleTimeUpdate = () => {
285
+ if (this.isDestroyed) return;
286
+ this.checkCurrentSegment(this.videoElement.currentTime);
287
+ };
288
+
289
+ this.videoElement.addEventListener('timeupdate', handleTimeUpdate);
290
+ }
291
+
292
+ /**
293
+ * Check and update current segment
294
+ */
295
+ private checkCurrentSegment(currentTime: number): void {
296
+ if (!this.chapters) return;
297
+
298
+ const newSegment = this.getCurrentSegment(currentTime);
299
+
300
+ // Check if segment changed
301
+ if (newSegment !== this.currentSegment) {
302
+ // Handle segment exit
303
+ if (this.currentSegment) {
304
+ this.emit('segmentExited', {
305
+ segment: this.currentSegment,
306
+ currentTime,
307
+ nextSegment: newSegment || undefined
308
+ });
309
+
310
+ // Hide skip button when exiting skippable segments
311
+ if (this.shouldShowSkipButton(this.currentSegment)) {
312
+ this.skipButtonController.hideSkipButton('segment-end');
313
+ }
314
+ }
315
+
316
+ // Update current segment
317
+ this.previousSegment = this.currentSegment;
318
+ this.currentSegment = newSegment;
319
+
320
+ // Handle segment entry
321
+ if (this.currentSegment) {
322
+ this.emit('segmentEntered', {
323
+ segment: this.currentSegment,
324
+ currentTime,
325
+ previousSegment: this.previousSegment || undefined
326
+ });
327
+
328
+ // Show skip button for skippable segments
329
+ if (this.shouldShowSkipButton(this.currentSegment)) {
330
+ this.skipButtonController.showSkipButton(this.currentSegment, currentTime);
331
+ }
332
+ }
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Check if segment should show skip button
338
+ */
339
+ private shouldShowSkipButton(segment: VideoSegment): boolean {
340
+ // Don't show for content segments by default
341
+ if (segment.type === 'content') {
342
+ return segment.showSkipButton === true;
343
+ }
344
+
345
+ // Show for other segment types unless explicitly disabled
346
+ return segment.showSkipButton !== false;
347
+ }
348
+
349
+ /**
350
+ * Get next content segment after current segment
351
+ */
352
+ private getNextContentSegment(currentSegment: VideoSegment): VideoSegment | null {
353
+ if (!this.chapters) return null;
354
+
355
+ const sortedSegments = [...this.chapters.segments].sort((a, b) => a.startTime - b.startTime);
356
+ const currentIndex = sortedSegments.findIndex(s => s.id === currentSegment.id);
357
+
358
+ if (currentIndex === -1) return null;
359
+
360
+ // Find next content segment
361
+ for (let i = currentIndex + 1; i < sortedSegments.length; i++) {
362
+ if (sortedSegments[i].type === 'content') {
363
+ return sortedSegments[i];
364
+ }
365
+ }
366
+
367
+ return null;
368
+ }
369
+
370
+ /**
371
+ * Sort segments by start time
372
+ */
373
+ private sortSegments(): void {
374
+ if (this.chapters) {
375
+ this.chapters.segments.sort((a, b) => a.startTime - b.startTime);
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Validate chapters data
381
+ */
382
+ private validateChapters(chapters: VideoChapters): void {
383
+ if (!chapters.videoId) {
384
+ throw new Error('Chapters must have a videoId');
385
+ }
386
+
387
+ if (!chapters.duration || chapters.duration <= 0) {
388
+ throw new Error('Chapters must have a valid duration');
389
+ }
390
+
391
+ if (!Array.isArray(chapters.segments)) {
392
+ throw new Error('Chapters must have a segments array');
393
+ }
394
+
395
+ // Validate each segment
396
+ chapters.segments.forEach((segment, index) => {
397
+ if (!segment.id) {
398
+ throw new Error(`Segment at index ${index} must have an id`);
399
+ }
400
+
401
+ if (!segment.type) {
402
+ throw new Error(`Segment at index ${index} must have a type`);
403
+ }
404
+
405
+ if (segment.startTime < 0 || segment.endTime <= segment.startTime) {
406
+ throw new Error(`Segment at index ${index} has invalid time range`);
407
+ }
408
+
409
+ if (segment.endTime > chapters.duration) {
410
+ throw new Error(`Segment at index ${index} extends beyond video duration`);
411
+ }
412
+ });
413
+ }
414
+
415
+ /**
416
+ * Update chapter markers on progress bar
417
+ */
418
+ private updateChapterMarkers(): void {
419
+ if (!this.chapters || !this.config.showChapterMarkers) return;
420
+
421
+ const progressBar = this.playerContainer.querySelector('.uvf-progress-bar');
422
+ if (!progressBar) return;
423
+
424
+ // Remove existing markers
425
+ this.removeChapterMarkers();
426
+
427
+ // Add new markers
428
+ const markers = this.getChapterMarkers();
429
+ markers.forEach(marker => {
430
+ const markerElement = document.createElement('div');
431
+ markerElement.className = `uvf-chapter-marker uvf-chapter-marker-${marker.segment.type}`;
432
+ markerElement.style.left = `${marker.position}%`;
433
+ markerElement.style.backgroundColor = marker.color || SEGMENT_COLORS[marker.segment.type];
434
+ markerElement.setAttribute('title', marker.label || '');
435
+ markerElement.setAttribute('data-segment-id', marker.segment.id);
436
+
437
+ progressBar.appendChild(markerElement);
438
+ });
439
+ }
440
+
441
+ /**
442
+ * Remove chapter markers from progress bar
443
+ */
444
+ private removeChapterMarkers(): void {
445
+ const markers = this.playerContainer.querySelectorAll('.uvf-chapter-marker');
446
+ markers.forEach(marker => marker.remove());
447
+ }
448
+
449
+ /**
450
+ * Emit event
451
+ */
452
+ private emit<K extends keyof ChapterEvents>(event: K, data: ChapterEvents[K]): void {
453
+ const listeners = this.eventListeners.get(event);
454
+ if (listeners) {
455
+ listeners.forEach(listener => {
456
+ try {
457
+ listener(data);
458
+ } catch (error) {
459
+ console.error(`Error in chapter event listener for ${event}:`, error);
460
+ }
461
+ });
462
+ }
463
+ }
464
+ }