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.
- package/package.json +1 -1
- package/packages/core/dist/interfaces/IVideoPlayer.d.ts +1 -0
- package/packages/core/dist/interfaces/IVideoPlayer.d.ts.map +1 -1
- package/packages/core/dist/interfaces.d.ts +1 -1
- package/packages/core/dist/interfaces.d.ts.map +1 -1
- package/packages/core/src/interfaces/IVideoPlayer.ts +2 -1
- package/packages/core/src/interfaces.ts +3 -2
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +13 -0
- 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 +87 -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 +8862 -8847
- package/packages/web/src/chapters/ChapterManager.ts +62 -17
- package/packages/web/src/chapters/SkipButtonController.ts +116 -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,67 @@ 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
|
+
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
|
-
//
|
|
176
|
-
this.
|
|
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
|
|
255
|
+
* Apply position styles to element (container or button)
|
|
181
256
|
*/
|
|
182
|
-
private applyPositionStyles(
|
|
257
|
+
private applyPositionStyles(element: HTMLElement, position: SkipButtonPosition): void {
|
|
183
258
|
// Reset position classes
|
|
184
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
317
|
+
if (!this.container) return;
|
|
235
318
|
|
|
236
|
-
this.
|
|
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.
|
|
328
|
+
if (!this.container) return;
|
|
245
329
|
|
|
246
|
-
this.
|
|
247
|
-
this.skipButton
|
|
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') {
|