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.
- package/package.json +1 -1
- package/packages/core/dist/chapter-manager.d.ts +39 -0
- package/packages/core/dist/chapter-manager.d.ts.map +1 -0
- package/packages/core/dist/chapter-manager.js +173 -0
- package/packages/core/dist/chapter-manager.js.map +1 -0
- package/packages/core/dist/index.d.ts +2 -0
- package/packages/core/dist/index.d.ts.map +1 -1
- package/packages/core/dist/index.js +1 -0
- package/packages/core/dist/index.js.map +1 -1
- package/packages/core/dist/interfaces/IVideoPlayer.d.ts +10 -0
- package/packages/core/dist/interfaces/IVideoPlayer.d.ts.map +1 -1
- package/packages/core/dist/interfaces.d.ts +33 -1
- package/packages/core/dist/interfaces.d.ts.map +1 -1
- package/packages/core/package.json +2 -2
- package/packages/core/src/chapter-manager.ts +290 -0
- package/packages/core/src/index.ts +4 -0
- package/packages/core/src/interfaces/IVideoPlayer.ts +11 -0
- package/packages/core/src/interfaces.ts +47 -1
- package/packages/web/dist/WebPlayer.d.ts +24 -1
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +472 -1
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/dist/chapters/ChapterManager.d.ts +38 -0
- package/packages/web/dist/chapters/ChapterManager.d.ts.map +1 -0
- package/packages/web/dist/chapters/ChapterManager.js +291 -0
- package/packages/web/dist/chapters/ChapterManager.js.map +1 -0
- package/packages/web/dist/chapters/SkipButtonController.d.ts +31 -0
- package/packages/web/dist/chapters/SkipButtonController.d.ts.map +1 -0
- package/packages/web/dist/chapters/SkipButtonController.js +213 -0
- package/packages/web/dist/chapters/SkipButtonController.js.map +1 -0
- package/packages/web/dist/chapters/UserPreferencesManager.d.ts +25 -0
- package/packages/web/dist/chapters/UserPreferencesManager.d.ts.map +1 -0
- package/packages/web/dist/chapters/UserPreferencesManager.js +232 -0
- package/packages/web/dist/chapters/UserPreferencesManager.js.map +1 -0
- package/packages/web/dist/chapters/index.d.ts +12 -0
- package/packages/web/dist/chapters/index.d.ts.map +1 -0
- package/packages/web/dist/chapters/index.js +8 -0
- package/packages/web/dist/chapters/index.js.map +1 -0
- package/packages/web/dist/chapters/types/ChapterTypes.d.ts +98 -0
- package/packages/web/dist/chapters/types/ChapterTypes.d.ts.map +1 -0
- package/packages/web/dist/chapters/types/ChapterTypes.js +31 -0
- package/packages/web/dist/chapters/types/ChapterTypes.js.map +1 -0
- package/packages/web/dist/index.d.ts +1 -1
- package/packages/web/dist/index.d.ts.map +1 -1
- package/packages/web/dist/index.js +1 -1
- package/packages/web/dist/index.js.map +1 -1
- package/packages/web/dist/paywall/EmailAuthController.d.ts +1 -1
- package/packages/web/dist/paywall/EmailAuthController.d.ts.map +1 -1
- package/packages/web/dist/paywall/PaywallController.d.ts +1 -1
- package/packages/web/dist/paywall/PaywallController.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerView.d.ts +2 -2
- package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerViewWithEPG.d.ts +2 -2
- package/packages/web/dist/react/WebPlayerViewWithEPG.d.ts.map +1 -1
- package/packages/web/dist/react/components/ChapterProgress.d.ts +22 -0
- package/packages/web/dist/react/components/ChapterProgress.d.ts.map +1 -0
- package/packages/web/dist/react/components/ChapterProgress.js +101 -0
- package/packages/web/dist/react/components/ChapterProgress.js.map +1 -0
- package/packages/web/dist/react/components/SkipButton.d.ts +18 -0
- package/packages/web/dist/react/components/SkipButton.d.ts.map +1 -0
- package/packages/web/dist/react/components/SkipButton.js +156 -0
- package/packages/web/dist/react/components/SkipButton.js.map +1 -0
- package/packages/web/dist/react/hooks/useChapters.d.ts +29 -0
- package/packages/web/dist/react/hooks/useChapters.d.ts.map +1 -0
- package/packages/web/dist/react/hooks/useChapters.js +158 -0
- package/packages/web/dist/react/hooks/useChapters.js.map +1 -0
- package/packages/web/package.json +3 -3
- package/packages/web/src/SecureVideoPlayer.ts +1 -1
- package/packages/web/src/WebPlayer.ts +587 -3
- package/packages/web/src/__tests__/WebPlayer.test.ts +1 -1
- package/packages/web/src/__tests__/epg-integration.test.ts +1 -1
- package/packages/web/src/chapters/ChapterManager.ts +464 -0
- package/packages/web/src/chapters/SkipButtonController.ts +353 -0
- package/packages/web/src/chapters/UserPreferencesManager.ts +324 -0
- package/packages/web/src/chapters/index.ts +34 -0
- package/packages/web/src/chapters/types/ChapterTypes.ts +236 -0
- package/packages/web/src/index.ts +1 -1
- package/packages/web/src/paywall/EmailAuthController.ts +1 -1
- package/packages/web/src/paywall/PaywallController.ts +1 -1
- package/packages/web/src/react/EPG.ts +1 -1
- package/packages/web/src/react/WebPlayerView.tsx +2 -2
- package/packages/web/src/react/WebPlayerViewWithEPG.tsx +3 -3
- package/packages/web/src/react/components/ChapterProgress.tsx +207 -0
- package/packages/web/src/react/components/EPGNavigationControls.tsx +1 -1
- package/packages/web/src/react/components/EPGOverlay-improved-positioning.tsx +1 -1
- package/packages/web/src/react/components/EPGOverlay.tsx +1 -1
- package/packages/web/src/react/components/EPGProgramGrid.tsx +1 -1
- package/packages/web/src/react/components/EPGTimelineHeader.tsx +1 -1
- package/packages/web/src/react/components/SkipButton.tsx +278 -0
- package/packages/web/src/react/hooks/useChapters.ts +308 -0
- package/packages/web/src/react/types/EPGTypes.ts +1 -1
- package/packages/web/src/react/utils/EPGUtils.ts +1 -1
- 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
|
+
}
|