ts-video-player 0.1.0
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/LICENSE.md +21 -0
- package/README.md +438 -0
- package/dist/cdn.d.ts +10 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/cdn.min.js +1046 -0
- package/dist/cdn.min.js.map +54 -0
- package/dist/cdn.test.d.ts +2 -0
- package/dist/cdn.test.d.ts.map +1 -0
- package/dist/chunk-36x4yhjn.js +765 -0
- package/dist/chunk-36x4yhjn.js.map +12 -0
- package/dist/chunk-eajts2jv.js +4243 -0
- package/dist/chunk-eajts2jv.js.map +42 -0
- package/dist/chunk-z1ehbz9f.js +380 -0
- package/dist/chunk-z1ehbz9f.js.map +19 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/core/audio-gain.d.ts +67 -0
- package/dist/core/audio-gain.d.ts.map +1 -0
- package/dist/core/caption-styles.d.ts +121 -0
- package/dist/core/caption-styles.d.ts.map +1 -0
- package/dist/core/chapters.d.ts +150 -0
- package/dist/core/chapters.d.ts.map +1 -0
- package/dist/core/error-overlay.d.ts +71 -0
- package/dist/core/error-overlay.d.ts.map +1 -0
- package/dist/core/events.d.ts +119 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/features.d.ts +64 -0
- package/dist/core/features.d.ts.map +1 -0
- package/dist/core/i18n.d.ts +132 -0
- package/dist/core/i18n.d.ts.map +1 -0
- package/dist/core/index.d.ts +37 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/live.d.ts +130 -0
- package/dist/core/live.d.ts.map +1 -0
- package/dist/core/media-session.d.ts +38 -0
- package/dist/core/media-session.d.ts.map +1 -0
- package/dist/core/orientation.d.ts +22 -0
- package/dist/core/orientation.d.ts.map +1 -0
- package/dist/core/popover.d.ts +42 -0
- package/dist/core/popover.d.ts.map +1 -0
- package/dist/core/remote-playback.d.ts +195 -0
- package/dist/core/remote-playback.d.ts.map +1 -0
- package/dist/core/settings-menu.d.ts +132 -0
- package/dist/core/settings-menu.d.ts.map +1 -0
- package/dist/core/spinner.d.ts +67 -0
- package/dist/core/spinner.d.ts.map +1 -0
- package/dist/core/state.d.ts +142 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/text-tracks.d.ts +162 -0
- package/dist/core/text-tracks.d.ts.map +1 -0
- package/dist/core/themes.d.ts +213 -0
- package/dist/core/themes.d.ts.map +1 -0
- package/dist/core/thumbnails.d.ts +124 -0
- package/dist/core/thumbnails.d.ts.map +1 -0
- package/dist/core/tooltips.d.ts +100 -0
- package/dist/core/tooltips.d.ts.map +1 -0
- package/dist/elements/base.d.ts +3 -0
- package/dist/elements/base.d.ts.map +1 -0
- package/dist/elements/index.d.ts +33 -0
- package/dist/elements/index.d.ts.map +1 -0
- package/dist/elements/index.js +55 -0
- package/dist/elements/index.js.map +9 -0
- package/dist/elements/media-airplay-button.d.ts +13 -0
- package/dist/elements/media-airplay-button.d.ts.map +1 -0
- package/dist/elements/media-error-display.d.ts +13 -0
- package/dist/elements/media-error-display.d.ts.map +1 -0
- package/dist/elements/media-fullscreen-button.d.ts +13 -0
- package/dist/elements/media-fullscreen-button.d.ts.map +1 -0
- package/dist/elements/media-live-button.d.ts +13 -0
- package/dist/elements/media-live-button.d.ts.map +1 -0
- package/dist/elements/media-mute-button.d.ts +13 -0
- package/dist/elements/media-mute-button.d.ts.map +1 -0
- package/dist/elements/media-pip-button.d.ts +13 -0
- package/dist/elements/media-pip-button.d.ts.map +1 -0
- package/dist/elements/media-play-button.d.ts +14 -0
- package/dist/elements/media-play-button.d.ts.map +1 -0
- package/dist/elements/media-playback-rate-button.d.ts +10 -0
- package/dist/elements/media-playback-rate-button.d.ts.map +1 -0
- package/dist/elements/media-preview-time.d.ts +18 -0
- package/dist/elements/media-preview-time.d.ts.map +1 -0
- package/dist/elements/media-progress-bar.d.ts +16 -0
- package/dist/elements/media-progress-bar.d.ts.map +1 -0
- package/dist/elements/media-remote-playback-button.d.ts +16 -0
- package/dist/elements/media-remote-playback-button.d.ts.map +1 -0
- package/dist/elements/media-seek-button.d.ts +9 -0
- package/dist/elements/media-seek-button.d.ts.map +1 -0
- package/dist/elements/media-settings-menu.d.ts +32 -0
- package/dist/elements/media-settings-menu.d.ts.map +1 -0
- package/dist/elements/media-time-display.d.ts +21 -0
- package/dist/elements/media-time-display.d.ts.map +1 -0
- package/dist/elements/media-time-group.d.ts +22 -0
- package/dist/elements/media-time-group.d.ts.map +1 -0
- package/dist/elements/media-time-separator.d.ts +13 -0
- package/dist/elements/media-time-separator.d.ts.map +1 -0
- package/dist/elements/media-volume-slider.d.ts +16 -0
- package/dist/elements/media-volume-slider.d.ts.map +1 -0
- package/dist/elements/utils.d.ts +27 -0
- package/dist/elements/utils.d.ts.map +1 -0
- package/dist/elements/video-player.d.ts +23 -0
- package/dist/elements/video-player.d.ts.map +1 -0
- package/dist/elements/video-skin.d.ts +23 -0
- package/dist/elements/video-skin.d.ts.map +1 -0
- package/dist/features/captions.d.ts +8 -0
- package/dist/features/captions.d.ts.map +1 -0
- package/dist/features/create-player.d.ts +45 -0
- package/dist/features/create-player.d.ts.map +1 -0
- package/dist/features/fullscreen.d.ts +8 -0
- package/dist/features/fullscreen.d.ts.map +1 -0
- package/dist/features/gestures.d.ts +8 -0
- package/dist/features/gestures.d.ts.map +1 -0
- package/dist/features/index.d.ts +26 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +31 -0
- package/dist/features/index.js.map +9 -0
- package/dist/features/keyboard.d.ts +8 -0
- package/dist/features/keyboard.d.ts.map +1 -0
- package/dist/features/pip.d.ts +8 -0
- package/dist/features/pip.d.ts.map +1 -0
- package/dist/features/playback.d.ts +8 -0
- package/dist/features/playback.d.ts.map +1 -0
- package/dist/features/quality.d.ts +8 -0
- package/dist/features/quality.d.ts.map +1 -0
- package/dist/features/types.d.ts +26 -0
- package/dist/features/types.d.ts.map +1 -0
- package/dist/features/volume.d.ts +8 -0
- package/dist/features/volume.d.ts.map +1 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8149 -0
- package/dist/index.js.map +31 -0
- package/dist/layouts/index.d.ts +58 -0
- package/dist/layouts/index.d.ts.map +1 -0
- package/dist/player.d.ts +90 -0
- package/dist/player.d.ts.map +1 -0
- package/dist/plugins/ads.d.ts +311 -0
- package/dist/plugins/ads.d.ts.map +1 -0
- package/dist/plugins/analytics.d.ts +203 -0
- package/dist/plugins/analytics.d.ts.map +1 -0
- package/dist/plugins/end-screen.d.ts +184 -0
- package/dist/plugins/end-screen.d.ts.map +1 -0
- package/dist/plugins/index.d.ts +13 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +20 -0
- package/dist/plugins/index.js.map +9 -0
- package/dist/plugins/skip-segments.d.ts +159 -0
- package/dist/plugins/skip-segments.d.ts.map +1 -0
- package/dist/plugins/watermark.d.ts +157 -0
- package/dist/plugins/watermark.d.ts.map +1 -0
- package/dist/providers/base.d.ts +56 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/dash.d.ts +94 -0
- package/dist/providers/dash.d.ts.map +1 -0
- package/dist/providers/hls.d.ts +182 -0
- package/dist/providers/hls.d.ts.map +1 -0
- package/dist/providers/html5.d.ts +64 -0
- package/dist/providers/html5.d.ts.map +1 -0
- package/dist/providers/index.d.ts +41 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/vimeo.d.ts +157 -0
- package/dist/providers/vimeo.d.ts.map +1 -0
- package/dist/providers/youtube.d.ts +134 -0
- package/dist/providers/youtube.d.ts.map +1 -0
- package/dist/stx/index.d.ts +59 -0
- package/dist/stx/index.d.ts.map +1 -0
- package/dist/stx/index.js +480 -0
- package/dist/stx/index.js.map +11 -0
- package/dist/types.d.ts +724 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/ui/components.d.ts +121 -0
- package/dist/ui/components.d.ts.map +1 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +16 -0
- package/dist/ui/index.js.map +9 -0
- package/package.json +109 -0
- package/src/cdn.test.ts +10 -0
- package/src/cdn.ts +60 -0
- package/src/config.ts +25 -0
- package/src/core/audio-gain.ts +171 -0
- package/src/core/caption-styles.ts +568 -0
- package/src/core/chapters.ts +551 -0
- package/src/core/error-overlay.ts +354 -0
- package/src/core/events.ts +552 -0
- package/src/core/features.ts +287 -0
- package/src/core/i18n.ts +590 -0
- package/src/core/index.ts +249 -0
- package/src/core/live.ts +472 -0
- package/src/core/media-session.ts +137 -0
- package/src/core/orientation.ts +57 -0
- package/src/core/popover.ts +143 -0
- package/src/core/remote-playback.ts +661 -0
- package/src/core/settings-menu.ts +630 -0
- package/src/core/spinner.ts +310 -0
- package/src/core/state.ts +476 -0
- package/src/core/text-tracks.ts +599 -0
- package/src/core/themes.ts +607 -0
- package/src/core/thumbnails.ts +457 -0
- package/src/core/tooltips.ts +370 -0
- package/src/elements/base.ts +2 -0
- package/src/elements/index.ts +82 -0
- package/src/elements/media-airplay-button.ts +78 -0
- package/src/elements/media-error-display.ts +47 -0
- package/src/elements/media-fullscreen-button.ts +72 -0
- package/src/elements/media-live-button.ts +65 -0
- package/src/elements/media-mute-button.ts +69 -0
- package/src/elements/media-pip-button.ts +66 -0
- package/src/elements/media-play-button.ts +69 -0
- package/src/elements/media-playback-rate-button.ts +59 -0
- package/src/elements/media-preview-time.ts +75 -0
- package/src/elements/media-progress-bar.ts +200 -0
- package/src/elements/media-remote-playback-button.ts +108 -0
- package/src/elements/media-seek-button.ts +50 -0
- package/src/elements/media-settings-menu.ts +302 -0
- package/src/elements/media-time-display.ts +99 -0
- package/src/elements/media-time-group.ts +41 -0
- package/src/elements/media-time-separator.ts +35 -0
- package/src/elements/media-volume-slider.ts +140 -0
- package/src/elements/utils.ts +82 -0
- package/src/elements/video-player.ts +175 -0
- package/src/elements/video-skin.ts +127 -0
- package/src/features/captions.ts +56 -0
- package/src/features/create-player.ts +91 -0
- package/src/features/fullscreen.ts +44 -0
- package/src/features/gestures.ts +44 -0
- package/src/features/index.ts +44 -0
- package/src/features/keyboard.ts +89 -0
- package/src/features/pip.ts +47 -0
- package/src/features/playback.ts +68 -0
- package/src/features/quality.ts +19 -0
- package/src/features/types.ts +28 -0
- package/src/features/volume.ts +46 -0
- package/src/index.ts +635 -0
- package/src/layouts/index.ts +672 -0
- package/src/player.ts +821 -0
- package/src/plugins/ads.ts +1045 -0
- package/src/plugins/analytics.ts +673 -0
- package/src/plugins/end-screen.ts +793 -0
- package/src/plugins/index.ts +61 -0
- package/src/plugins/skip-segments.ts +537 -0
- package/src/plugins/watermark.ts +537 -0
- package/src/providers/base.ts +197 -0
- package/src/providers/dash.ts +588 -0
- package/src/providers/hls.ts +624 -0
- package/src/providers/html5.ts +473 -0
- package/src/providers/index.ts +97 -0
- package/src/providers/vimeo.ts +533 -0
- package/src/providers/youtube.ts +542 -0
- package/src/stx/index.ts +606 -0
- package/src/types.ts +874 -0
- package/src/ui/components.ts +910 -0
- package/src/ui/index.ts +29 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Player Plugins
|
|
3
|
+
*
|
|
4
|
+
* Optional plugins for extended functionality.
|
|
5
|
+
*
|
|
6
|
+
* @module plugins
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Analytics
|
|
10
|
+
export {
|
|
11
|
+
VideoAnalytics,
|
|
12
|
+
createVideoAnalytics,
|
|
13
|
+
analyticsPlugin,
|
|
14
|
+
type VideoEventName,
|
|
15
|
+
type VideoAnalyticsConfig,
|
|
16
|
+
} from './analytics'
|
|
17
|
+
|
|
18
|
+
// Ads (VAST/VPAID)
|
|
19
|
+
export {
|
|
20
|
+
VASTParser,
|
|
21
|
+
AdsManager,
|
|
22
|
+
createAdsManager,
|
|
23
|
+
adsPlugin,
|
|
24
|
+
type Ad,
|
|
25
|
+
type AdMediaFile,
|
|
26
|
+
type AdBreak,
|
|
27
|
+
type AdConfig,
|
|
28
|
+
type AdState,
|
|
29
|
+
} from './ads'
|
|
30
|
+
|
|
31
|
+
// Skip Segments
|
|
32
|
+
export {
|
|
33
|
+
SkipSegmentsManager,
|
|
34
|
+
createSkipSegments,
|
|
35
|
+
skipSegmentsPlugin,
|
|
36
|
+
type SegmentType,
|
|
37
|
+
type SkipSegment,
|
|
38
|
+
type SkipSegmentsConfig,
|
|
39
|
+
type SkipSegmentsState,
|
|
40
|
+
} from './skip-segments'
|
|
41
|
+
|
|
42
|
+
// End Screen
|
|
43
|
+
export {
|
|
44
|
+
EndScreenManager,
|
|
45
|
+
createEndScreen,
|
|
46
|
+
endScreenPlugin,
|
|
47
|
+
type EndScreenRecommendation,
|
|
48
|
+
type EndScreenConfig,
|
|
49
|
+
type EndScreenState,
|
|
50
|
+
} from './end-screen'
|
|
51
|
+
|
|
52
|
+
// Watermarks
|
|
53
|
+
export {
|
|
54
|
+
WatermarkManager,
|
|
55
|
+
createWatermarkManager,
|
|
56
|
+
watermarkPlugin,
|
|
57
|
+
type WatermarkPosition,
|
|
58
|
+
type WatermarkConfig,
|
|
59
|
+
type WatermarkAnimation,
|
|
60
|
+
type DynamicWatermark,
|
|
61
|
+
} from './watermark'
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skip Segments Plugin
|
|
3
|
+
*
|
|
4
|
+
* Skip intro, outro, recap, and other segments.
|
|
5
|
+
*
|
|
6
|
+
* @module plugins/skip-segments
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Player } from '../player'
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Types
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export type SegmentType = 'intro' | 'outro' | 'recap' | 'preview' | 'credits' | 'sponsor' | 'custom'
|
|
16
|
+
|
|
17
|
+
export interface SkipSegment {
|
|
18
|
+
/** Segment type */
|
|
19
|
+
type: SegmentType
|
|
20
|
+
/** Start time in seconds */
|
|
21
|
+
startTime: number
|
|
22
|
+
/** End time in seconds */
|
|
23
|
+
endTime: number
|
|
24
|
+
/** Skip to time (defaults to endTime) */
|
|
25
|
+
skipTo?: number
|
|
26
|
+
/** Button text */
|
|
27
|
+
buttonText?: string
|
|
28
|
+
/** Auto-skip after delay (0 = no auto-skip) */
|
|
29
|
+
autoSkipDelay?: number
|
|
30
|
+
/** Show countdown */
|
|
31
|
+
showCountdown?: boolean
|
|
32
|
+
/** Custom data */
|
|
33
|
+
data?: Record<string, unknown>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SkipSegmentsConfig {
|
|
37
|
+
/** Skip segments */
|
|
38
|
+
segments: SkipSegment[]
|
|
39
|
+
/** Default button visibility duration (ms) */
|
|
40
|
+
buttonVisibleDuration?: number
|
|
41
|
+
/** Default auto-skip delay (seconds) */
|
|
42
|
+
defaultAutoSkipDelay?: number
|
|
43
|
+
/** Position of skip button */
|
|
44
|
+
buttonPosition?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'
|
|
45
|
+
/** Show progress bar markers */
|
|
46
|
+
showMarkers?: boolean
|
|
47
|
+
/** Marker color */
|
|
48
|
+
markerColor?: string
|
|
49
|
+
/** Enable keyboard shortcut (Enter to skip) */
|
|
50
|
+
keyboardShortcut?: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SkipSegmentsState {
|
|
54
|
+
/** Current active segment */
|
|
55
|
+
activeSegment: SkipSegment | null
|
|
56
|
+
/** Is button visible */
|
|
57
|
+
buttonVisible: boolean
|
|
58
|
+
/** Auto-skip countdown */
|
|
59
|
+
countdown: number
|
|
60
|
+
/** Is auto-skipping */
|
|
61
|
+
isAutoSkipping: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// Default Text Labels
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
68
|
+
const defaultButtonText: Record<SegmentType, string> = {
|
|
69
|
+
intro: 'Skip Intro',
|
|
70
|
+
outro: 'Skip Outro',
|
|
71
|
+
recap: 'Skip Recap',
|
|
72
|
+
preview: 'Skip Preview',
|
|
73
|
+
credits: 'Skip Credits',
|
|
74
|
+
sponsor: 'Skip',
|
|
75
|
+
custom: 'Skip',
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// =============================================================================
|
|
79
|
+
// Skip Segments Manager
|
|
80
|
+
// =============================================================================
|
|
81
|
+
|
|
82
|
+
export class SkipSegmentsManager {
|
|
83
|
+
private player: Player | null = null
|
|
84
|
+
private config: Required<SkipSegmentsConfig>
|
|
85
|
+
private state: SkipSegmentsState
|
|
86
|
+
private container: HTMLElement | null = null
|
|
87
|
+
private button: HTMLButtonElement | null = null
|
|
88
|
+
private markersContainer: HTMLElement | null = null
|
|
89
|
+
private countdownTimer: ReturnType<typeof setInterval> | null = null
|
|
90
|
+
private hideTimer: ReturnType<typeof setTimeout> | null = null
|
|
91
|
+
private unsubscribers: Array<() => void> = []
|
|
92
|
+
|
|
93
|
+
// Event handlers
|
|
94
|
+
// eslint-disable-next-line pickier/no-unused-vars
|
|
95
|
+
private onSkip: ((_segment: SkipSegment) => void) | null = null
|
|
96
|
+
// eslint-disable-next-line pickier/no-unused-vars
|
|
97
|
+
private onSegmentEnter: ((_segment: SkipSegment) => void) | null = null
|
|
98
|
+
// eslint-disable-next-line pickier/no-unused-vars
|
|
99
|
+
private onSegmentExit: ((_segment: SkipSegment) => void) | null = null
|
|
100
|
+
|
|
101
|
+
constructor(config: SkipSegmentsConfig) {
|
|
102
|
+
this.config = {
|
|
103
|
+
segments: config.segments || [],
|
|
104
|
+
buttonVisibleDuration: config.buttonVisibleDuration ?? 10000,
|
|
105
|
+
defaultAutoSkipDelay: config.defaultAutoSkipDelay ?? 0,
|
|
106
|
+
buttonPosition: config.buttonPosition || 'bottom-right',
|
|
107
|
+
showMarkers: config.showMarkers ?? true,
|
|
108
|
+
markerColor: config.markerColor || 'rgba(255, 255, 0, 0.8)',
|
|
109
|
+
keyboardShortcut: config.keyboardShortcut ?? true,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.state = {
|
|
113
|
+
activeSegment: null,
|
|
114
|
+
buttonVisible: false,
|
|
115
|
+
countdown: 0,
|
|
116
|
+
isAutoSkipping: false,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Attach to player
|
|
122
|
+
*/
|
|
123
|
+
attach(player: Player, container: HTMLElement): void {
|
|
124
|
+
this.player = player
|
|
125
|
+
this.container = container
|
|
126
|
+
|
|
127
|
+
this.createButton()
|
|
128
|
+
this.attachEventListeners()
|
|
129
|
+
|
|
130
|
+
if (this.config.showMarkers) {
|
|
131
|
+
this.createMarkers()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create skip button
|
|
137
|
+
*/
|
|
138
|
+
private createButton(): void {
|
|
139
|
+
if (!this.container) return
|
|
140
|
+
|
|
141
|
+
this.button = document.createElement('button')
|
|
142
|
+
this.button.className = 'tsvp-skip-button'
|
|
143
|
+
this.button.type = 'button'
|
|
144
|
+
|
|
145
|
+
const positionStyles = this.getPositionStyles()
|
|
146
|
+
|
|
147
|
+
this.button.style.cssText = `
|
|
148
|
+
position: absolute;
|
|
149
|
+
${positionStyles}
|
|
150
|
+
padding: 12px 24px;
|
|
151
|
+
background: rgba(0, 0, 0, 0.85);
|
|
152
|
+
color: #fff;
|
|
153
|
+
border: 2px solid rgba(255, 255, 255, 0.8);
|
|
154
|
+
border-radius: 4px;
|
|
155
|
+
font-size: 16px;
|
|
156
|
+
font-weight: 600;
|
|
157
|
+
font-family: var(--tsvp-font-family, sans-serif);
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
z-index: 30;
|
|
160
|
+
display: none;
|
|
161
|
+
transition: transform 150ms ease, background 150ms ease;
|
|
162
|
+
`
|
|
163
|
+
|
|
164
|
+
this.button.addEventListener('mouseenter', () => {
|
|
165
|
+
this.button!.style.background = 'rgba(255, 255, 255, 0.2)'
|
|
166
|
+
this.button!.style.transform = 'scale(1.05)'
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
this.button.addEventListener('mouseleave', () => {
|
|
170
|
+
this.button!.style.background = 'rgba(0, 0, 0, 0.85)'
|
|
171
|
+
this.button!.style.transform = 'scale(1)'
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
this.button.addEventListener('click', () => this.skip())
|
|
175
|
+
|
|
176
|
+
this.container.appendChild(this.button)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get position styles for button
|
|
181
|
+
*/
|
|
182
|
+
private getPositionStyles(): string {
|
|
183
|
+
switch (this.config.buttonPosition) {
|
|
184
|
+
case 'top-left':
|
|
185
|
+
return 'top: 20px; left: 20px;'
|
|
186
|
+
case 'top-right':
|
|
187
|
+
return 'top: 20px; right: 20px;'
|
|
188
|
+
case 'bottom-left':
|
|
189
|
+
return 'bottom: 80px; left: 20px;'
|
|
190
|
+
case 'bottom-right':
|
|
191
|
+
default:
|
|
192
|
+
return 'bottom: 80px; right: 20px;'
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Create progress bar markers
|
|
198
|
+
*/
|
|
199
|
+
private createMarkers(): void {
|
|
200
|
+
// Markers will be created when attached to progress bar
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get markers element for progress bar
|
|
205
|
+
*/
|
|
206
|
+
getMarkersElement(duration: number): HTMLElement {
|
|
207
|
+
this.markersContainer = document.createElement('div')
|
|
208
|
+
this.markersContainer.className = 'tsvp-skip-markers'
|
|
209
|
+
this.markersContainer.style.cssText = `
|
|
210
|
+
position: absolute;
|
|
211
|
+
left: 0;
|
|
212
|
+
right: 0;
|
|
213
|
+
top: 0;
|
|
214
|
+
bottom: 0;
|
|
215
|
+
pointer-events: none;
|
|
216
|
+
`
|
|
217
|
+
|
|
218
|
+
for (const segment of this.config.segments) {
|
|
219
|
+
const marker = document.createElement('div')
|
|
220
|
+
marker.className = 'tsvp-skip-marker'
|
|
221
|
+
|
|
222
|
+
const startPercent = (segment.startTime / duration) * 100
|
|
223
|
+
const widthPercent = ((segment.endTime - segment.startTime) / duration) * 100
|
|
224
|
+
|
|
225
|
+
marker.style.cssText = `
|
|
226
|
+
position: absolute;
|
|
227
|
+
left: ${startPercent}%;
|
|
228
|
+
width: ${widthPercent}%;
|
|
229
|
+
top: 0;
|
|
230
|
+
bottom: 0;
|
|
231
|
+
background: ${this.config.markerColor};
|
|
232
|
+
opacity: 0.6;
|
|
233
|
+
`
|
|
234
|
+
|
|
235
|
+
this.markersContainer.appendChild(marker)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return this.markersContainer
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Attach event listeners
|
|
243
|
+
*/
|
|
244
|
+
private attachEventListeners(): void {
|
|
245
|
+
if (!this.player) return
|
|
246
|
+
|
|
247
|
+
// Time update to check for segments
|
|
248
|
+
const handleTimeUpdate = () => {
|
|
249
|
+
const state = this.player?.state
|
|
250
|
+
if (!state) return
|
|
251
|
+
|
|
252
|
+
this.checkSegments(state.currentTime)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this.player.on('timeupdate', handleTimeUpdate)
|
|
256
|
+
this.unsubscribers.push(() => this.player?.off('timeupdate', handleTimeUpdate))
|
|
257
|
+
|
|
258
|
+
// Keyboard shortcut
|
|
259
|
+
if (this.config.keyboardShortcut) {
|
|
260
|
+
const handleKeydown = (e: KeyboardEvent) => {
|
|
261
|
+
if (e.key === 'Enter' && this.state.activeSegment && this.state.buttonVisible) {
|
|
262
|
+
e.preventDefault()
|
|
263
|
+
this.skip()
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
document.addEventListener('keydown', handleKeydown)
|
|
268
|
+
this.unsubscribers.push(() => document.removeEventListener('keydown', handleKeydown))
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Check if current time is in any segment
|
|
274
|
+
*/
|
|
275
|
+
private checkSegments(currentTime: number): void {
|
|
276
|
+
const previousSegment = this.state.activeSegment
|
|
277
|
+
|
|
278
|
+
// Find active segment
|
|
279
|
+
let activeSegment: SkipSegment | null = null
|
|
280
|
+
for (const segment of this.config.segments) {
|
|
281
|
+
if (currentTime >= segment.startTime && currentTime < segment.endTime) {
|
|
282
|
+
activeSegment = segment
|
|
283
|
+
break
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Segment changed
|
|
288
|
+
if (activeSegment !== previousSegment) {
|
|
289
|
+
if (previousSegment) {
|
|
290
|
+
this.onSegmentExit?.(previousSegment)
|
|
291
|
+
this.hideButton()
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (activeSegment) {
|
|
295
|
+
this.state.activeSegment = activeSegment
|
|
296
|
+
this.onSegmentEnter?.(activeSegment)
|
|
297
|
+
this.showButton(activeSegment)
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
this.state.activeSegment = null
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Show skip button
|
|
307
|
+
*/
|
|
308
|
+
private showButton(segment: SkipSegment): void {
|
|
309
|
+
if (!this.button) return
|
|
310
|
+
|
|
311
|
+
// Set button text
|
|
312
|
+
const text = segment.buttonText || defaultButtonText[segment.type]
|
|
313
|
+
this.button.textContent = text
|
|
314
|
+
|
|
315
|
+
this.state.buttonVisible = true
|
|
316
|
+
this.button.style.display = 'block'
|
|
317
|
+
|
|
318
|
+
// Animate in
|
|
319
|
+
this.button.style.opacity = '0'
|
|
320
|
+
this.button.style.transform = 'translateX(20px)'
|
|
321
|
+
requestAnimationFrame(() => {
|
|
322
|
+
this.button!.style.transition = 'opacity 200ms ease, transform 200ms ease'
|
|
323
|
+
this.button!.style.opacity = '1'
|
|
324
|
+
this.button!.style.transform = 'translateX(0)'
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
// Start auto-skip countdown
|
|
328
|
+
const autoSkipDelay = segment.autoSkipDelay ?? this.config.defaultAutoSkipDelay
|
|
329
|
+
if (autoSkipDelay > 0) {
|
|
330
|
+
this.startAutoSkip(autoSkipDelay, segment)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Auto-hide after duration
|
|
334
|
+
if (this.config.buttonVisibleDuration > 0 && !autoSkipDelay) {
|
|
335
|
+
this.hideTimer = setTimeout(() => {
|
|
336
|
+
this.hideButton()
|
|
337
|
+
}, this.config.buttonVisibleDuration)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Hide skip button
|
|
343
|
+
*/
|
|
344
|
+
private hideButton(): void {
|
|
345
|
+
if (!this.button) return
|
|
346
|
+
|
|
347
|
+
this.state.buttonVisible = false
|
|
348
|
+
this.state.isAutoSkipping = false
|
|
349
|
+
this.state.countdown = 0
|
|
350
|
+
|
|
351
|
+
// Clear timers
|
|
352
|
+
if (this.countdownTimer) {
|
|
353
|
+
clearInterval(this.countdownTimer)
|
|
354
|
+
this.countdownTimer = null
|
|
355
|
+
}
|
|
356
|
+
if (this.hideTimer) {
|
|
357
|
+
clearTimeout(this.hideTimer)
|
|
358
|
+
this.hideTimer = null
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Animate out
|
|
362
|
+
this.button.style.opacity = '0'
|
|
363
|
+
this.button.style.transform = 'translateX(20px)'
|
|
364
|
+
|
|
365
|
+
setTimeout(() => {
|
|
366
|
+
if (this.button && !this.state.buttonVisible) {
|
|
367
|
+
this.button.style.display = 'none'
|
|
368
|
+
}
|
|
369
|
+
}, 200)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Start auto-skip countdown
|
|
374
|
+
*/
|
|
375
|
+
private startAutoSkip(delay: number, segment: SkipSegment): void {
|
|
376
|
+
this.state.isAutoSkipping = true
|
|
377
|
+
this.state.countdown = delay
|
|
378
|
+
|
|
379
|
+
this.updateButtonText(segment)
|
|
380
|
+
|
|
381
|
+
this.countdownTimer = setInterval(() => {
|
|
382
|
+
this.state.countdown--
|
|
383
|
+
this.updateButtonText(segment)
|
|
384
|
+
|
|
385
|
+
if (this.state.countdown <= 0) {
|
|
386
|
+
if (this.countdownTimer) {
|
|
387
|
+
clearInterval(this.countdownTimer)
|
|
388
|
+
this.countdownTimer = null
|
|
389
|
+
}
|
|
390
|
+
this.skip()
|
|
391
|
+
}
|
|
392
|
+
}, 1000)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Update button text with countdown
|
|
397
|
+
*/
|
|
398
|
+
private updateButtonText(segment: SkipSegment): void {
|
|
399
|
+
if (!this.button) return
|
|
400
|
+
|
|
401
|
+
const baseText = segment.buttonText || defaultButtonText[segment.type]
|
|
402
|
+
|
|
403
|
+
if (this.state.isAutoSkipping && this.state.countdown > 0 && segment.showCountdown !== false) {
|
|
404
|
+
this.button.textContent = `${baseText} (${this.state.countdown})`
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
this.button.textContent = baseText
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Skip current segment
|
|
413
|
+
*/
|
|
414
|
+
skip(): void {
|
|
415
|
+
if (!this.state.activeSegment || !this.player) return
|
|
416
|
+
|
|
417
|
+
const segment = this.state.activeSegment
|
|
418
|
+
const skipTo = segment.skipTo ?? segment.endTime
|
|
419
|
+
|
|
420
|
+
this.player.seekTo(skipTo)
|
|
421
|
+
this.onSkip?.(segment)
|
|
422
|
+
this.hideButton()
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Add segment
|
|
427
|
+
*/
|
|
428
|
+
addSegment(segment: SkipSegment): void {
|
|
429
|
+
this.config.segments.push(segment)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Remove segment
|
|
434
|
+
*/
|
|
435
|
+
removeSegment(type: SegmentType): void {
|
|
436
|
+
this.config.segments = this.config.segments.filter(s => s.type !== type)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Clear all segments
|
|
441
|
+
*/
|
|
442
|
+
clearSegments(): void {
|
|
443
|
+
this.config.segments = []
|
|
444
|
+
this.hideButton()
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Set segments
|
|
449
|
+
*/
|
|
450
|
+
setSegments(segments: SkipSegment[]): void {
|
|
451
|
+
this.config.segments = segments
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Get segments
|
|
456
|
+
*/
|
|
457
|
+
getSegments(): SkipSegment[] {
|
|
458
|
+
return [...this.config.segments]
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Get current state
|
|
463
|
+
*/
|
|
464
|
+
getState(): SkipSegmentsState {
|
|
465
|
+
return { ...this.state }
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Set event handlers
|
|
470
|
+
*/
|
|
471
|
+
on(event: 'skip', handler: (segment: SkipSegment) => void): void
|
|
472
|
+
on(event: 'segmentEnter', handler: (segment: SkipSegment) => void): void
|
|
473
|
+
on(event: 'segmentExit', handler: (segment: SkipSegment) => void): void
|
|
474
|
+
on(event: string, handler: (...args: any[]) => void): void {
|
|
475
|
+
switch (event) {
|
|
476
|
+
case 'skip':
|
|
477
|
+
this.onSkip = handler
|
|
478
|
+
break
|
|
479
|
+
case 'segmentEnter':
|
|
480
|
+
this.onSegmentEnter = handler
|
|
481
|
+
break
|
|
482
|
+
case 'segmentExit':
|
|
483
|
+
this.onSegmentExit = handler
|
|
484
|
+
break
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Destroy
|
|
490
|
+
*/
|
|
491
|
+
destroy(): void {
|
|
492
|
+
if (this.countdownTimer) {
|
|
493
|
+
clearInterval(this.countdownTimer)
|
|
494
|
+
}
|
|
495
|
+
if (this.hideTimer) {
|
|
496
|
+
clearTimeout(this.hideTimer)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
this.unsubscribers.forEach(unsub => unsub())
|
|
500
|
+
|
|
501
|
+
this.button?.remove()
|
|
502
|
+
this.markersContainer?.remove()
|
|
503
|
+
|
|
504
|
+
this.player = null
|
|
505
|
+
this.container = null
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// =============================================================================
|
|
510
|
+
// Factory
|
|
511
|
+
// =============================================================================
|
|
512
|
+
|
|
513
|
+
export function createSkipSegments(config: SkipSegmentsConfig): SkipSegmentsManager {
|
|
514
|
+
return new SkipSegmentsManager(config)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Skip segments plugin for Player
|
|
519
|
+
*/
|
|
520
|
+
export function skipSegmentsPlugin(config: SkipSegmentsConfig): { name: string, install: (player: Player, container: HTMLElement) => () => void } {
|
|
521
|
+
return {
|
|
522
|
+
name: 'skipSegments',
|
|
523
|
+
|
|
524
|
+
install(player: Player, container: HTMLElement): () => void {
|
|
525
|
+
const skipSegments = createSkipSegments(config)
|
|
526
|
+
skipSegments.attach(player, container)
|
|
527
|
+
|
|
528
|
+
// Expose on player
|
|
529
|
+
;(player as any).skipSegments = skipSegments
|
|
530
|
+
|
|
531
|
+
return () => {
|
|
532
|
+
skipSegments.destroy()
|
|
533
|
+
delete (player as any).skipSegments
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
}
|
|
537
|
+
}
|