unified-video-framework 1.4.356 → 1.4.358

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 (35) hide show
  1. package/package.json +1 -1
  2. package/packages/core/dist/interfaces/IVideoPlayer.d.ts +1 -0
  3. package/packages/core/dist/interfaces/IVideoPlayer.d.ts.map +1 -1
  4. package/packages/core/dist/interfaces.d.ts +1 -1
  5. package/packages/core/dist/interfaces.d.ts.map +1 -1
  6. package/packages/core/src/interfaces/IVideoPlayer.ts +2 -1
  7. package/packages/core/src/interfaces.ts +3 -2
  8. package/packages/web/dist/WebPlayer.d.ts.map +1 -1
  9. package/packages/web/dist/WebPlayer.js +13 -0
  10. package/packages/web/dist/WebPlayer.js.map +1 -1
  11. package/packages/web/dist/chapters/ChapterManager.d.ts +3 -0
  12. package/packages/web/dist/chapters/ChapterManager.d.ts.map +1 -1
  13. package/packages/web/dist/chapters/ChapterManager.js +21 -1
  14. package/packages/web/dist/chapters/ChapterManager.js.map +1 -1
  15. package/packages/web/dist/chapters/SkipButtonController.d.ts +4 -1
  16. package/packages/web/dist/chapters/SkipButtonController.d.ts.map +1 -1
  17. package/packages/web/dist/chapters/SkipButtonController.js +87 -19
  18. package/packages/web/dist/chapters/SkipButtonController.js.map +1 -1
  19. package/packages/web/dist/chapters/types/ChapterTypes.d.ts +13 -0
  20. package/packages/web/dist/chapters/types/ChapterTypes.d.ts.map +1 -1
  21. package/packages/web/dist/chapters/types/ChapterTypes.js.map +1 -1
  22. package/packages/web/dist/react/WebPlayerView.d.ts +10 -0
  23. package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
  24. package/packages/web/dist/react/WebPlayerView.js +5 -1
  25. package/packages/web/dist/react/WebPlayerView.js.map +1 -1
  26. package/packages/web/dist/react/components/SkipButton.d.ts +2 -0
  27. package/packages/web/dist/react/components/SkipButton.d.ts.map +1 -1
  28. package/packages/web/dist/react/components/SkipButton.js +30 -13
  29. package/packages/web/dist/react/components/SkipButton.js.map +1 -1
  30. package/packages/web/src/WebPlayer.ts +8862 -8847
  31. package/packages/web/src/chapters/ChapterManager.ts +62 -17
  32. package/packages/web/src/chapters/SkipButtonController.ts +116 -30
  33. package/packages/web/src/chapters/types/ChapterTypes.ts +53 -32
  34. package/packages/web/src/react/WebPlayerView.tsx +14 -1
  35. package/packages/web/src/react/components/SkipButton.tsx +82 -24
@@ -30,18 +30,19 @@ export class ChapterManager {
30
30
  ) {
31
31
  // Merge config with defaults
32
32
  this.config = { ...DEFAULT_CHAPTER_CONFIG, ...config };
33
-
33
+
34
34
  // Initialize skip button controller
35
35
  this.skipButtonController = new SkipButtonController(
36
36
  playerContainer,
37
37
  this.config,
38
38
  (segment) => this.skipToNextSegment(segment),
39
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
- })
40
+ (segment, reason) => this.emit('skipButtonHidden', {
41
+ segment,
42
+ currentTime: this.videoElement.currentTime,
43
+ reason: reason as any
44
+ }),
45
+ (segment) => this.handleSecondaryAction(segment)
45
46
  );
46
47
 
47
48
  // Set up time update listener
@@ -65,7 +66,7 @@ export class ChapterManager {
65
66
 
66
67
  this.chapters = chapters;
67
68
  this.sortSegments();
68
-
69
+
69
70
  // Emit loaded event
70
71
  this.emit('chaptersLoaded', {
71
72
  chapters: this.chapters,
@@ -81,8 +82,8 @@ export class ChapterManager {
81
82
  this.checkCurrentSegment(this.videoElement.currentTime);
82
83
 
83
84
  } catch (error) {
84
- this.emit('chaptersLoadError', {
85
- error: error as Error
85
+ this.emit('chaptersLoadError', {
86
+ error: error as Error
86
87
  });
87
88
  throw error;
88
89
  }
@@ -102,9 +103,9 @@ export class ChapterManager {
102
103
  await this.loadChapters(chapters);
103
104
 
104
105
  } catch (error) {
105
- this.emit('chaptersLoadError', {
106
+ this.emit('chaptersLoadError', {
106
107
  error: error as Error,
107
- url
108
+ url
108
109
  });
109
110
  throw error;
110
111
  }
@@ -116,7 +117,7 @@ export class ChapterManager {
116
117
  public getCurrentSegment(currentTime: number): VideoSegment | null {
117
118
  if (!this.chapters) return null;
118
119
 
119
- return this.chapters.segments.find(segment =>
120
+ return this.chapters.segments.find(segment =>
120
121
  currentTime >= segment.startTime && currentTime < segment.endTime
121
122
  ) || null;
122
123
  }
@@ -128,6 +129,14 @@ export class ChapterManager {
128
129
  if (!this.chapters) return;
129
130
 
130
131
  const nextSegment = this.getNextContentSegment(currentSegment);
132
+
133
+ // Check for Next Episode trigger if no next segment or if we are skipping credits
134
+ if ((!nextSegment || currentSegment.type === 'credits') &&
135
+ this.config.nextEpisode?.enabled) {
136
+ this.triggerNextEpisode();
137
+ return;
138
+ }
139
+
131
140
  const targetTime = nextSegment ? nextSegment.startTime : currentSegment.endTime;
132
141
 
133
142
  // Store current playback state
@@ -158,6 +167,28 @@ export class ChapterManager {
158
167
  }
159
168
  }
160
169
 
170
+ /**
171
+ * Handle secondary action (Watch Credits)
172
+ */
173
+ public handleSecondaryAction(segment: VideoSegment): void {
174
+ // For "Watch Credits", we imply that the user wants to stay on the credits.
175
+ // So we just emit an event or log it, but importantly the SkipButton will have
176
+ // cancelled the auto-skip.
177
+ console.log('[ChapterManager] User chose to watch credits');
178
+ }
179
+
180
+ /**
181
+ * Trigger next episode directly
182
+ */
183
+ public triggerNextEpisode(): void {
184
+ if (this.config.nextEpisode?.enabled) {
185
+ this.emit('nextEpisode', {
186
+ config: this.config.nextEpisode,
187
+ autoPlay: true
188
+ });
189
+ }
190
+ }
191
+
161
192
  /**
162
193
  * Skip to specific segment by ID
163
194
  */
@@ -168,10 +199,10 @@ export class ChapterManager {
168
199
  if (!segment) return;
169
200
 
170
201
  const fromSegment = this.currentSegment;
171
-
202
+
172
203
  // Store current playback state
173
204
  const wasPlaying = !this.videoElement.paused;
174
-
205
+
175
206
  // Emit skip event
176
207
  if (fromSegment) {
177
208
  this.emit('segmentSkipped', {
@@ -234,7 +265,7 @@ export class ChapterManager {
234
265
  // Use custom color if provided, otherwise fallback to default
235
266
  const customColor = this.config.customStyles?.progressMarkers?.[segment.type];
236
267
  const color = customColor || SEGMENT_COLORS[segment.type];
237
-
268
+
238
269
  return {
239
270
  segment,
240
271
  position: (segment.startTime / this.chapters!.duration) * 100,
@@ -249,7 +280,7 @@ export class ChapterManager {
249
280
  */
250
281
  public updateConfig(newConfig: Partial<ChapterConfig>): void {
251
282
  this.config = { ...this.config, ...newConfig };
252
-
283
+
253
284
  // Update skip button position if changed
254
285
  if (newConfig.skipButtonPosition) {
255
286
  this.skipButtonController.updatePosition(newConfig.skipButtonPosition);
@@ -384,6 +415,20 @@ export class ChapterManager {
384
415
  return segment.showSkipButton !== false;
385
416
  }
386
417
 
418
+ /**
419
+ * Check if we should override skip button label/behavior for Next Episode
420
+ */
421
+ private checkNextEpisodeOverride(segment: VideoSegment) {
422
+ if (segment.type === 'credits' && this.config.nextEpisode?.enabled) {
423
+ // If we have a next episode, modifying the segment on the fly to include secondary actions
424
+ // might be tricky since segment objects are shared.
425
+ // Ideally, the SkipButtonController should ask for this info.
426
+ // But for now, we rely on the generic SkipButton rendering secondaryLabel if passed.
427
+ // The SkipButtonController needs to know to pass 'secondaryLabel' to the SkipButton.
428
+ // We might need to handle this in SkipButtonController.updateSkipButton.
429
+ }
430
+ }
431
+
387
432
  /**
388
433
  * Get next content segment after current segment
389
434
  */
@@ -392,7 +437,7 @@ export class ChapterManager {
392
437
 
393
438
  const sortedSegments = [...this.chapters.segments].sort((a, b) => a.startTime - b.startTime);
394
439
  const currentIndex = sortedSegments.findIndex(s => s.id === currentSegment.id);
395
-
440
+
396
441
  if (currentIndex === -1) return null;
397
442
 
398
443
  // Find next content segment
@@ -12,18 +12,21 @@ import {
12
12
 
13
13
  export class SkipButtonController {
14
14
  private skipButton: HTMLElement | null = null;
15
+ private secondaryButton: HTMLElement | null = null;
16
+ private container: HTMLElement | null = null;
15
17
  private currentSegment: VideoSegment | null = null;
16
18
  private autoSkipTimeout: NodeJS.Timeout | null = null;
17
19
  private hideTimeout: NodeJS.Timeout | null = null;
18
20
  private countdownInterval: NodeJS.Timeout | null = null;
19
21
  private state: SkipButtonState;
20
-
22
+
21
23
  constructor(
22
24
  private playerContainer: HTMLElement,
23
25
  private config: ChapterConfig,
24
26
  private onSkip: (segment: VideoSegment) => void,
25
27
  private onButtonShown: (segment: VideoSegment) => void,
26
- private onButtonHidden: (segment: VideoSegment, reason: string) => void
28
+ private onButtonHidden: (segment: VideoSegment, reason: string) => void,
29
+ private onSecondaryAction?: (segment: VideoSegment) => void
27
30
  ) {
28
31
  this.state = {
29
32
  visible: false,
@@ -119,7 +122,13 @@ export class SkipButtonController {
119
122
  */
120
123
  public destroy(): void {
121
124
  this.clearTimeouts();
122
- if (this.skipButton) {
125
+ if (this.container) {
126
+ this.container.remove();
127
+ this.container = null;
128
+ this.skipButton = null;
129
+ this.secondaryButton = null;
130
+ } else if (this.skipButton) {
131
+ // Fallback if no container (shouldn't happen with new logic but safe to keep)
123
132
  this.skipButton.remove();
124
133
  this.skipButton = null;
125
134
  }
@@ -129,17 +138,43 @@ export class SkipButtonController {
129
138
  }
130
139
 
131
140
  /**
132
- * Create the skip button DOM element
141
+ * Create the skip button DOM element (container and buttons)
133
142
  */
134
143
  private createSkipButton(): HTMLElement {
144
+ // Create container
145
+ const container = document.createElement('div');
146
+ container.className = 'uvf-skip-container';
147
+ this.container = container;
148
+
149
+ // Create secondary button (Watch Credits) - initially hidden
150
+ const secondaryBtn = document.createElement('button');
151
+ secondaryBtn.className = 'uvf-skip-button uvf-skip-secondary';
152
+ secondaryBtn.setAttribute('type', 'button');
153
+ secondaryBtn.textContent = 'Watch Credits';
154
+ secondaryBtn.style.display = 'none'; // Hidden by default
155
+ secondaryBtn.style.marginRight = '10px';
156
+ secondaryBtn.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
157
+ secondaryBtn.style.border = '1px solid rgba(255, 255, 255, 0.5)';
158
+
159
+ // Explicit click listener for secondary button
160
+ secondaryBtn.addEventListener('click', (e) => {
161
+ e.stopPropagation(); // prevent triggering other things
162
+ if (this.currentSegment && this.onSecondaryAction) {
163
+ this.clearTimeouts(); // Stop auto-skip
164
+ this.onSecondaryAction(this.currentSegment);
165
+ // Hide the container after action
166
+ this.hideSkipButton('user-action');
167
+ }
168
+ });
169
+ this.secondaryButton = secondaryBtn;
170
+ container.appendChild(secondaryBtn);
171
+
172
+ // Create primary button
135
173
  const button = document.createElement('button');
136
174
  button.className = 'uvf-skip-button';
137
175
  button.setAttribute('type', 'button');
138
176
  button.setAttribute('aria-label', 'Skip segment');
139
177
 
140
- // Apply position styles
141
- this.applyPositionStyles(button, this.state.position);
142
-
143
178
  // Add click handler
144
179
  button.addEventListener('click', () => {
145
180
  if (this.currentSegment) {
@@ -148,12 +183,18 @@ export class SkipButtonController {
148
183
  }
149
184
  });
150
185
 
151
- // Apply custom styles if provided
186
+ this.skipButton = button;
187
+ container.appendChild(button);
188
+
189
+ // Apply custom styles if provided (to the primary button mostly)
152
190
  if (this.config.customStyles?.skipButton) {
153
191
  Object.assign(button.style, this.config.customStyles.skipButton);
154
192
  }
155
193
 
156
- return button;
194
+ // Apply position styles to CONTAINER
195
+ this.applyPositionStyles(container, this.state.position);
196
+
197
+ return container;
157
198
  }
158
199
 
159
200
  /**
@@ -163,33 +204,67 @@ export class SkipButtonController {
163
204
  if (!this.skipButton) return;
164
205
 
165
206
  // Set button text
166
- const skipLabel = segment.skipLabel || DEFAULT_SKIP_LABELS[segment.type];
207
+ let skipLabel = segment.skipLabel || DEFAULT_SKIP_LABELS[segment.type];
208
+
209
+ // Check for Next Episode override
210
+ let showSecondary = false;
211
+ // We infer it's next episode scenario if it's credits and we have a config for it
212
+ // AND secondary action is available.
213
+ // Ideally we checked for `nextEpisode` config in ChapterManager, but here we only know if onSecondaryAction exists.
214
+ // Let's assume onSecondaryAction implies we support it.
215
+ if (segment.type === 'credits' && this.onSecondaryAction) {
216
+ if (this.config.nextEpisode?.title) {
217
+ skipLabel = `Next Episode: ${this.config.nextEpisode.title}`;
218
+ } else {
219
+ skipLabel = "Next Episode";
220
+ }
221
+ showSecondary = true;
222
+ }
223
+
167
224
  this.skipButton.textContent = skipLabel;
168
225
 
226
+ // Handle Secondary Button Visibility
227
+ if (this.secondaryButton) {
228
+ if (showSecondary && this.onSecondaryAction) {
229
+ this.secondaryButton.style.display = 'inline-block';
230
+ if (this.config.customStyles?.skipButton) {
231
+ // Inherit some basic styles like font size if available
232
+ if (this.config.customStyles.skipButton.fontSize) {
233
+ this.secondaryButton.style.fontSize = this.config.customStyles.skipButton.fontSize;
234
+ }
235
+ if (this.config.customStyles.skipButton.padding) {
236
+ this.secondaryButton.style.padding = this.config.customStyles.skipButton.padding;
237
+ }
238
+ }
239
+ } else {
240
+ this.secondaryButton.style.display = 'none';
241
+ }
242
+ }
243
+
169
244
  // Update aria-label for accessibility
170
245
  this.skipButton.setAttribute('aria-label', `${skipLabel} - ${segment.title || segment.type}`);
171
246
 
172
247
  // Add segment type class for styling
173
248
  this.skipButton.className = `uvf-skip-button uvf-skip-${segment.type}`;
174
-
175
- // Apply position class
176
- this.skipButton.classList.add(`uvf-skip-button-${this.state.position}`);
249
+
250
+ // Position class is on container now, so update it just in case position changed
251
+ this.updatePosition(this.state.position);
177
252
  }
178
253
 
179
254
  /**
180
- * Apply position styles to skip button
255
+ * Apply position styles to element (container or button)
181
256
  */
182
- private applyPositionStyles(button: HTMLElement, position: SkipButtonPosition): void {
257
+ private applyPositionStyles(element: HTMLElement, position: SkipButtonPosition): void {
183
258
  // Reset position classes
184
- button.classList.remove(
259
+ element.classList.remove(
185
260
  'uvf-skip-button-bottom-right',
186
- 'uvf-skip-button-bottom-left',
261
+ 'uvf-skip-button-bottom-left',
187
262
  'uvf-skip-button-top-right',
188
263
  'uvf-skip-button-top-left'
189
264
  );
190
265
 
191
266
  // Add new position class
192
- button.classList.add(`uvf-skip-button-${position}`);
267
+ element.classList.add(`uvf-skip-button-${position}`);
193
268
 
194
269
  // Apply CSS styles based on position
195
270
  const styles: Partial<CSSStyleDeclaration> = {
@@ -201,39 +276,48 @@ export class SkipButtonController {
201
276
  case 'bottom-right':
202
277
  Object.assign(styles, {
203
278
  bottom: '100px',
204
- right: '30px'
279
+ right: '30px',
280
+ top: 'auto',
281
+ left: 'auto'
205
282
  });
206
283
  break;
207
284
  case 'bottom-left':
208
285
  Object.assign(styles, {
209
286
  bottom: '100px',
210
- left: '30px'
287
+ left: '30px',
288
+ top: 'auto',
289
+ right: 'auto'
211
290
  });
212
291
  break;
213
292
  case 'top-right':
214
293
  Object.assign(styles, {
215
294
  top: '30px',
216
- right: '30px'
295
+ right: '30px',
296
+ bottom: 'auto',
297
+ left: 'auto'
217
298
  });
218
299
  break;
219
300
  case 'top-left':
220
301
  Object.assign(styles, {
221
302
  top: '30px',
222
- left: '30px'
303
+ left: '30px',
304
+ bottom: 'auto',
305
+ right: 'auto'
223
306
  });
224
307
  break;
225
308
  }
226
309
 
227
- Object.assign(button.style, styles);
310
+ Object.assign(element.style, styles);
228
311
  }
229
312
 
230
313
  /**
231
314
  * Show the skip button with animation
232
315
  */
233
316
  private showButton(): void {
234
- if (!this.skipButton) return;
317
+ if (!this.container) return;
235
318
 
236
- this.skipButton.classList.add('visible');
319
+ this.container.classList.add('visible');
320
+ // Also ensure buttons are visible (opacity handled by CSS on container usually, but let's be safe)
237
321
  this.state.visible = true;
238
322
  }
239
323
 
@@ -241,10 +325,12 @@ export class SkipButtonController {
241
325
  * Hide the skip button with animation
242
326
  */
243
327
  private hideButton(): void {
244
- if (!this.skipButton) return;
328
+ if (!this.container) return;
245
329
 
246
- this.skipButton.classList.remove('visible');
247
- this.skipButton.classList.remove('auto-skip', 'countdown');
330
+ this.container.classList.remove('visible');
331
+ if (this.skipButton) {
332
+ this.skipButton.classList.remove('auto-skip', 'countdown');
333
+ }
248
334
  }
249
335
 
250
336
  /**
@@ -285,10 +371,10 @@ export class SkipButtonController {
285
371
 
286
372
  // Update button text with countdown
287
373
  const originalText = this.skipButton.textContent || '';
288
-
374
+
289
375
  // Start countdown animation
290
376
  this.skipButton.classList.add('countdown');
291
-
377
+
292
378
  // Update countdown every second
293
379
  this.countdownInterval = setInterval(() => {
294
380
  remainingTime -= 1;
@@ -12,31 +12,31 @@ export type SkipButtonPosition = 'bottom-right' | 'bottom-left' | 'top-right' |
12
12
  export interface VideoSegment {
13
13
  /** Unique identifier for the segment */
14
14
  id: string;
15
-
15
+
16
16
  /** Type of segment */
17
17
  type: SegmentType;
18
-
18
+
19
19
  /** Start time in seconds */
20
20
  startTime: number;
21
-
21
+
22
22
  /** End time in seconds */
23
23
  endTime: number;
24
-
24
+
25
25
  /** Display title for the segment */
26
26
  title?: string;
27
-
27
+
28
28
  /** Description of the segment */
29
29
  description?: string;
30
-
30
+
31
31
  /** Custom text for the skip button */
32
32
  skipLabel?: string;
33
-
33
+
34
34
  /** Whether to auto-skip after a delay */
35
35
  autoSkip?: boolean;
36
-
36
+
37
37
  /** Delay in seconds before auto-skip */
38
38
  autoSkipDelay?: number;
39
-
39
+
40
40
  /** Whether to show skip button for this segment */
41
41
  showSkipButton?: boolean;
42
42
  }
@@ -47,13 +47,13 @@ export interface VideoSegment {
47
47
  export interface VideoChapters {
48
48
  /** Video identifier */
49
49
  videoId: string;
50
-
50
+
51
51
  /** Total video duration in seconds */
52
52
  duration: number;
53
-
53
+
54
54
  /** Array of video segments */
55
55
  segments: VideoSegment[];
56
-
56
+
57
57
  /** Metadata about the chapters */
58
58
  metadata?: {
59
59
  version?: string;
@@ -69,25 +69,25 @@ export interface VideoChapters {
69
69
  export interface ChapterConfig {
70
70
  /** Enable/disable chapter functionality */
71
71
  enabled: boolean;
72
-
72
+
73
73
  /** Chapter data object */
74
74
  data?: VideoChapters;
75
-
75
+
76
76
  /** URL to fetch chapter data from */
77
77
  dataUrl?: string;
78
-
78
+
79
79
  /** Auto-hide skip button after showing */
80
80
  autoHide?: boolean;
81
-
81
+
82
82
  /** Delay before auto-hiding skip button (ms) */
83
83
  autoHideDelay?: number;
84
-
84
+
85
85
  /** Show chapter markers on progress bar */
86
86
  showChapterMarkers?: boolean;
87
-
87
+
88
88
  /** Position of skip button */
89
89
  skipButtonPosition?: SkipButtonPosition;
90
-
90
+
91
91
  /** Custom CSS styles */
92
92
  customStyles?: {
93
93
  skipButton?: Partial<CSSStyleDeclaration>;
@@ -100,9 +100,24 @@ export interface ChapterConfig {
100
100
  ad?: string;
101
101
  };
102
102
  };
103
-
103
+
104
104
  /** User preferences */
105
105
  userPreferences?: ChapterPreferences;
106
+
107
+ /** Next Episode Configuration */
108
+ nextEpisode?: NextEpisodeConfig;
109
+ }
110
+
111
+ /**
112
+ * Configuration for the next episode
113
+ */
114
+ export interface NextEpisodeConfig {
115
+ enabled: boolean;
116
+ url: string;
117
+ title?: string;
118
+ thumbnail?: string;
119
+ autoPlayDelay?: number; // seconds, overrides segment autoSkipDelay if present
120
+ description?: string;
106
121
  }
107
122
 
108
123
  /**
@@ -111,22 +126,22 @@ export interface ChapterConfig {
111
126
  export interface ChapterPreferences {
112
127
  /** Auto-skip intro segments */
113
128
  autoSkipIntro?: boolean;
114
-
129
+
115
130
  /** Auto-skip recap segments */
116
131
  autoSkipRecap?: boolean;
117
-
132
+
118
133
  /** Auto-skip credits segments */
119
134
  autoSkipCredits?: boolean;
120
-
135
+
121
136
  /** Show skip buttons */
122
137
  showSkipButtons?: boolean;
123
-
138
+
124
139
  /** Skip button timeout in milliseconds */
125
140
  skipButtonTimeout?: number;
126
-
141
+
127
142
  /** Remember user choices */
128
143
  rememberChoices?: boolean;
129
-
144
+
130
145
  /** Resume playback after skip (default: true for better UX) */
131
146
  resumePlaybackAfterSkip?: boolean;
132
147
  }
@@ -141,14 +156,14 @@ export interface ChapterEvents {
141
156
  currentTime: number;
142
157
  previousSegment?: VideoSegment;
143
158
  };
144
-
159
+
145
160
  /** When exiting a segment */
146
161
  segmentExited: {
147
162
  segment: VideoSegment;
148
163
  currentTime: number;
149
164
  nextSegment?: VideoSegment;
150
165
  };
151
-
166
+
152
167
  /** When a segment is skipped */
153
168
  segmentSkipped: {
154
169
  fromSegment: VideoSegment;
@@ -156,31 +171,37 @@ export interface ChapterEvents {
156
171
  skipMethod: 'button' | 'auto' | 'manual';
157
172
  currentTime: number;
158
173
  };
159
-
174
+
160
175
  /** When skip button is shown */
161
176
  skipButtonShown: {
162
177
  segment: VideoSegment;
163
178
  currentTime: number;
164
179
  };
165
-
180
+
166
181
  /** When skip button is hidden */
167
182
  skipButtonHidden: {
168
183
  segment: VideoSegment;
169
184
  currentTime: number;
170
185
  reason: 'timeout' | 'segment-end' | 'user-action' | 'manual';
171
186
  };
172
-
187
+
173
188
  /** When chapters are loaded */
174
189
  chaptersLoaded: {
175
190
  chapters: VideoChapters;
176
191
  segmentCount: number;
177
192
  };
178
-
193
+
179
194
  /** When chapter loading fails */
180
195
  chaptersLoadError: {
181
196
  error: Error;
182
197
  url?: string;
183
198
  };
199
+
200
+ /** When next episode should be played */
201
+ nextEpisode: {
202
+ config: NextEpisodeConfig;
203
+ autoPlay: boolean;
204
+ };
184
205
  }
185
206
 
186
207
  /**
@@ -370,9 +370,16 @@ export type WebPlayerViewProps = {
370
370
  autoSkipCredits?: boolean; // Auto-skip credits segments (default: false)
371
371
  showSkipButtons?: boolean; // Show skip buttons (default: true)
372
372
  skipButtonTimeout?: number; // Button timeout in milliseconds (default: 5000)
373
+
373
374
  rememberChoices?: boolean; // Remember user preferences (default: true)
374
375
  resumePlaybackAfterSkip?: boolean; // Resume playback after skipping (default: true)
375
376
  };
377
+ nextEpisode?: { // Next episode configuration
378
+ title: string;
379
+ url: string;
380
+ thumbnail?: string;
381
+ autoPlayDelay?: number;
382
+ };
376
383
  };
377
384
 
378
385
  // Navigation Configuration
@@ -429,6 +436,7 @@ export type WebPlayerViewProps = {
429
436
  onChapterSkipButtonHidden?: (data: { segment: any; reason: string }) => void; // Skip button hidden
430
437
  onChaptersLoaded?: (data: { segmentCount: number; chapters: any[] }) => void; // Chapters loaded
431
438
  onChaptersLoadError?: (data: { error: Error; url?: string }) => void; // Chapters load error
439
+ onNextEpisode?: (data: { config: any; autoPlay: boolean }) => void; // Next episode triggered
432
440
 
433
441
  // Flash News Ticker
434
442
  flashNewsTicker?: FlashNewsTickerConfig; // Flash news ticker configuration
@@ -985,7 +993,9 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
985
993
  skipButtonTimeout: chaptersConfig.userPreferences?.skipButtonTimeout ?? 5000,
986
994
  rememberChoices: chaptersConfig.userPreferences?.rememberChoices ?? true,
987
995
  resumePlaybackAfterSkip: chaptersConfig.userPreferences?.resumePlaybackAfterSkip ?? true,
988
- }
996
+
997
+ },
998
+ nextEpisode: chaptersConfig.nextEpisode
989
999
  } : { enabled: false }
990
1000
  };
991
1001
 
@@ -1121,6 +1131,9 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1121
1131
  if (props.onChaptersLoadError && typeof (player as any).on === 'function') {
1122
1132
  (player as any).on('chaptersLoadError', props.onChaptersLoadError);
1123
1133
  }
1134
+ if (props.onNextEpisode && typeof (player as any).on === 'function') {
1135
+ (player as any).on('nextEpisode', props.onNextEpisode);
1136
+ }
1124
1137
 
1125
1138
  // Navigation event listeners
1126
1139
  if (props.onNavigationBackClicked && typeof (player as any).on === 'function') {