unified-video-framework 1.4.356 → 1.4.357
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/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/dist/chapters/ChapterManager.d.ts +3 -0
- package/packages/web/dist/chapters/ChapterManager.d.ts.map +1 -1
- package/packages/web/dist/chapters/ChapterManager.js +21 -1
- package/packages/web/dist/chapters/ChapterManager.js.map +1 -1
- package/packages/web/dist/chapters/SkipButtonController.d.ts +4 -1
- package/packages/web/dist/chapters/SkipButtonController.d.ts.map +1 -1
- package/packages/web/dist/chapters/SkipButtonController.js +82 -19
- package/packages/web/dist/chapters/SkipButtonController.js.map +1 -1
- package/packages/web/dist/chapters/types/ChapterTypes.d.ts +13 -0
- package/packages/web/dist/chapters/types/ChapterTypes.d.ts.map +1 -1
- package/packages/web/dist/chapters/types/ChapterTypes.js.map +1 -1
- package/packages/web/dist/react/WebPlayerView.d.ts +10 -0
- package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerView.js +5 -1
- package/packages/web/dist/react/WebPlayerView.js.map +1 -1
- package/packages/web/dist/react/components/SkipButton.d.ts +2 -0
- package/packages/web/dist/react/components/SkipButton.d.ts.map +1 -1
- package/packages/web/dist/react/components/SkipButton.js +30 -13
- package/packages/web/dist/react/components/SkipButton.js.map +1 -1
- package/packages/web/src/WebPlayer.ts +8787 -8787
- package/packages/web/src/chapters/ChapterManager.ts +62 -17
- package/packages/web/src/chapters/SkipButtonController.ts +112 -30
- package/packages/web/src/chapters/types/ChapterTypes.ts +53 -32
- package/packages/web/src/react/WebPlayerView.tsx +14 -1
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
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,63 @@ export class SkipButtonController {
|
|
|
163
204
|
if (!this.skipButton) return;
|
|
164
205
|
|
|
165
206
|
// Set button text
|
|
166
|
-
|
|
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
|
+
skipLabel = "Next Episode";
|
|
217
|
+
showSecondary = true;
|
|
218
|
+
}
|
|
219
|
+
|
|
167
220
|
this.skipButton.textContent = skipLabel;
|
|
168
221
|
|
|
222
|
+
// Handle Secondary Button Visibility
|
|
223
|
+
if (this.secondaryButton) {
|
|
224
|
+
if (showSecondary && this.onSecondaryAction) {
|
|
225
|
+
this.secondaryButton.style.display = 'inline-block';
|
|
226
|
+
if (this.config.customStyles?.skipButton) {
|
|
227
|
+
// Inherit some basic styles like font size if available
|
|
228
|
+
if (this.config.customStyles.skipButton.fontSize) {
|
|
229
|
+
this.secondaryButton.style.fontSize = this.config.customStyles.skipButton.fontSize;
|
|
230
|
+
}
|
|
231
|
+
if (this.config.customStyles.skipButton.padding) {
|
|
232
|
+
this.secondaryButton.style.padding = this.config.customStyles.skipButton.padding;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
this.secondaryButton.style.display = 'none';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
169
240
|
// Update aria-label for accessibility
|
|
170
241
|
this.skipButton.setAttribute('aria-label', `${skipLabel} - ${segment.title || segment.type}`);
|
|
171
242
|
|
|
172
243
|
// Add segment type class for styling
|
|
173
244
|
this.skipButton.className = `uvf-skip-button uvf-skip-${segment.type}`;
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
this.
|
|
245
|
+
|
|
246
|
+
// Position class is on container now, so update it just in case position changed
|
|
247
|
+
this.updatePosition(this.state.position);
|
|
177
248
|
}
|
|
178
249
|
|
|
179
250
|
/**
|
|
180
|
-
* Apply position styles to
|
|
251
|
+
* Apply position styles to element (container or button)
|
|
181
252
|
*/
|
|
182
|
-
private applyPositionStyles(
|
|
253
|
+
private applyPositionStyles(element: HTMLElement, position: SkipButtonPosition): void {
|
|
183
254
|
// Reset position classes
|
|
184
|
-
|
|
255
|
+
element.classList.remove(
|
|
185
256
|
'uvf-skip-button-bottom-right',
|
|
186
|
-
'uvf-skip-button-bottom-left',
|
|
257
|
+
'uvf-skip-button-bottom-left',
|
|
187
258
|
'uvf-skip-button-top-right',
|
|
188
259
|
'uvf-skip-button-top-left'
|
|
189
260
|
);
|
|
190
261
|
|
|
191
262
|
// Add new position class
|
|
192
|
-
|
|
263
|
+
element.classList.add(`uvf-skip-button-${position}`);
|
|
193
264
|
|
|
194
265
|
// Apply CSS styles based on position
|
|
195
266
|
const styles: Partial<CSSStyleDeclaration> = {
|
|
@@ -201,39 +272,48 @@ export class SkipButtonController {
|
|
|
201
272
|
case 'bottom-right':
|
|
202
273
|
Object.assign(styles, {
|
|
203
274
|
bottom: '100px',
|
|
204
|
-
right: '30px'
|
|
275
|
+
right: '30px',
|
|
276
|
+
top: 'auto',
|
|
277
|
+
left: 'auto'
|
|
205
278
|
});
|
|
206
279
|
break;
|
|
207
280
|
case 'bottom-left':
|
|
208
281
|
Object.assign(styles, {
|
|
209
282
|
bottom: '100px',
|
|
210
|
-
left: '30px'
|
|
283
|
+
left: '30px',
|
|
284
|
+
top: 'auto',
|
|
285
|
+
right: 'auto'
|
|
211
286
|
});
|
|
212
287
|
break;
|
|
213
288
|
case 'top-right':
|
|
214
289
|
Object.assign(styles, {
|
|
215
290
|
top: '30px',
|
|
216
|
-
right: '30px'
|
|
291
|
+
right: '30px',
|
|
292
|
+
bottom: 'auto',
|
|
293
|
+
left: 'auto'
|
|
217
294
|
});
|
|
218
295
|
break;
|
|
219
296
|
case 'top-left':
|
|
220
297
|
Object.assign(styles, {
|
|
221
298
|
top: '30px',
|
|
222
|
-
left: '30px'
|
|
299
|
+
left: '30px',
|
|
300
|
+
bottom: 'auto',
|
|
301
|
+
right: 'auto'
|
|
223
302
|
});
|
|
224
303
|
break;
|
|
225
304
|
}
|
|
226
305
|
|
|
227
|
-
Object.assign(
|
|
306
|
+
Object.assign(element.style, styles);
|
|
228
307
|
}
|
|
229
308
|
|
|
230
309
|
/**
|
|
231
310
|
* Show the skip button with animation
|
|
232
311
|
*/
|
|
233
312
|
private showButton(): void {
|
|
234
|
-
if (!this.
|
|
313
|
+
if (!this.container) return;
|
|
235
314
|
|
|
236
|
-
this.
|
|
315
|
+
this.container.classList.add('visible');
|
|
316
|
+
// Also ensure buttons are visible (opacity handled by CSS on container usually, but let's be safe)
|
|
237
317
|
this.state.visible = true;
|
|
238
318
|
}
|
|
239
319
|
|
|
@@ -241,10 +321,12 @@ export class SkipButtonController {
|
|
|
241
321
|
* Hide the skip button with animation
|
|
242
322
|
*/
|
|
243
323
|
private hideButton(): void {
|
|
244
|
-
if (!this.
|
|
324
|
+
if (!this.container) return;
|
|
245
325
|
|
|
246
|
-
this.
|
|
247
|
-
this.skipButton
|
|
326
|
+
this.container.classList.remove('visible');
|
|
327
|
+
if (this.skipButton) {
|
|
328
|
+
this.skipButton.classList.remove('auto-skip', 'countdown');
|
|
329
|
+
}
|
|
248
330
|
}
|
|
249
331
|
|
|
250
332
|
/**
|
|
@@ -285,10 +367,10 @@ export class SkipButtonController {
|
|
|
285
367
|
|
|
286
368
|
// Update button text with countdown
|
|
287
369
|
const originalText = this.skipButton.textContent || '';
|
|
288
|
-
|
|
370
|
+
|
|
289
371
|
// Start countdown animation
|
|
290
372
|
this.skipButton.classList.add('countdown');
|
|
291
|
-
|
|
373
|
+
|
|
292
374
|
// Update countdown every second
|
|
293
375
|
this.countdownInterval = setInterval(() => {
|
|
294
376
|
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') {
|