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.
Files changed (251) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +438 -0
  3. package/dist/cdn.d.ts +10 -0
  4. package/dist/cdn.d.ts.map +1 -0
  5. package/dist/cdn.min.js +1046 -0
  6. package/dist/cdn.min.js.map +54 -0
  7. package/dist/cdn.test.d.ts +2 -0
  8. package/dist/cdn.test.d.ts.map +1 -0
  9. package/dist/chunk-36x4yhjn.js +765 -0
  10. package/dist/chunk-36x4yhjn.js.map +12 -0
  11. package/dist/chunk-eajts2jv.js +4243 -0
  12. package/dist/chunk-eajts2jv.js.map +42 -0
  13. package/dist/chunk-z1ehbz9f.js +380 -0
  14. package/dist/chunk-z1ehbz9f.js.map +19 -0
  15. package/dist/config.d.ts +9 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/core/audio-gain.d.ts +67 -0
  18. package/dist/core/audio-gain.d.ts.map +1 -0
  19. package/dist/core/caption-styles.d.ts +121 -0
  20. package/dist/core/caption-styles.d.ts.map +1 -0
  21. package/dist/core/chapters.d.ts +150 -0
  22. package/dist/core/chapters.d.ts.map +1 -0
  23. package/dist/core/error-overlay.d.ts +71 -0
  24. package/dist/core/error-overlay.d.ts.map +1 -0
  25. package/dist/core/events.d.ts +119 -0
  26. package/dist/core/events.d.ts.map +1 -0
  27. package/dist/core/features.d.ts +64 -0
  28. package/dist/core/features.d.ts.map +1 -0
  29. package/dist/core/i18n.d.ts +132 -0
  30. package/dist/core/i18n.d.ts.map +1 -0
  31. package/dist/core/index.d.ts +37 -0
  32. package/dist/core/index.d.ts.map +1 -0
  33. package/dist/core/live.d.ts +130 -0
  34. package/dist/core/live.d.ts.map +1 -0
  35. package/dist/core/media-session.d.ts +38 -0
  36. package/dist/core/media-session.d.ts.map +1 -0
  37. package/dist/core/orientation.d.ts +22 -0
  38. package/dist/core/orientation.d.ts.map +1 -0
  39. package/dist/core/popover.d.ts +42 -0
  40. package/dist/core/popover.d.ts.map +1 -0
  41. package/dist/core/remote-playback.d.ts +195 -0
  42. package/dist/core/remote-playback.d.ts.map +1 -0
  43. package/dist/core/settings-menu.d.ts +132 -0
  44. package/dist/core/settings-menu.d.ts.map +1 -0
  45. package/dist/core/spinner.d.ts +67 -0
  46. package/dist/core/spinner.d.ts.map +1 -0
  47. package/dist/core/state.d.ts +142 -0
  48. package/dist/core/state.d.ts.map +1 -0
  49. package/dist/core/text-tracks.d.ts +162 -0
  50. package/dist/core/text-tracks.d.ts.map +1 -0
  51. package/dist/core/themes.d.ts +213 -0
  52. package/dist/core/themes.d.ts.map +1 -0
  53. package/dist/core/thumbnails.d.ts +124 -0
  54. package/dist/core/thumbnails.d.ts.map +1 -0
  55. package/dist/core/tooltips.d.ts +100 -0
  56. package/dist/core/tooltips.d.ts.map +1 -0
  57. package/dist/elements/base.d.ts +3 -0
  58. package/dist/elements/base.d.ts.map +1 -0
  59. package/dist/elements/index.d.ts +33 -0
  60. package/dist/elements/index.d.ts.map +1 -0
  61. package/dist/elements/index.js +55 -0
  62. package/dist/elements/index.js.map +9 -0
  63. package/dist/elements/media-airplay-button.d.ts +13 -0
  64. package/dist/elements/media-airplay-button.d.ts.map +1 -0
  65. package/dist/elements/media-error-display.d.ts +13 -0
  66. package/dist/elements/media-error-display.d.ts.map +1 -0
  67. package/dist/elements/media-fullscreen-button.d.ts +13 -0
  68. package/dist/elements/media-fullscreen-button.d.ts.map +1 -0
  69. package/dist/elements/media-live-button.d.ts +13 -0
  70. package/dist/elements/media-live-button.d.ts.map +1 -0
  71. package/dist/elements/media-mute-button.d.ts +13 -0
  72. package/dist/elements/media-mute-button.d.ts.map +1 -0
  73. package/dist/elements/media-pip-button.d.ts +13 -0
  74. package/dist/elements/media-pip-button.d.ts.map +1 -0
  75. package/dist/elements/media-play-button.d.ts +14 -0
  76. package/dist/elements/media-play-button.d.ts.map +1 -0
  77. package/dist/elements/media-playback-rate-button.d.ts +10 -0
  78. package/dist/elements/media-playback-rate-button.d.ts.map +1 -0
  79. package/dist/elements/media-preview-time.d.ts +18 -0
  80. package/dist/elements/media-preview-time.d.ts.map +1 -0
  81. package/dist/elements/media-progress-bar.d.ts +16 -0
  82. package/dist/elements/media-progress-bar.d.ts.map +1 -0
  83. package/dist/elements/media-remote-playback-button.d.ts +16 -0
  84. package/dist/elements/media-remote-playback-button.d.ts.map +1 -0
  85. package/dist/elements/media-seek-button.d.ts +9 -0
  86. package/dist/elements/media-seek-button.d.ts.map +1 -0
  87. package/dist/elements/media-settings-menu.d.ts +32 -0
  88. package/dist/elements/media-settings-menu.d.ts.map +1 -0
  89. package/dist/elements/media-time-display.d.ts +21 -0
  90. package/dist/elements/media-time-display.d.ts.map +1 -0
  91. package/dist/elements/media-time-group.d.ts +22 -0
  92. package/dist/elements/media-time-group.d.ts.map +1 -0
  93. package/dist/elements/media-time-separator.d.ts +13 -0
  94. package/dist/elements/media-time-separator.d.ts.map +1 -0
  95. package/dist/elements/media-volume-slider.d.ts +16 -0
  96. package/dist/elements/media-volume-slider.d.ts.map +1 -0
  97. package/dist/elements/utils.d.ts +27 -0
  98. package/dist/elements/utils.d.ts.map +1 -0
  99. package/dist/elements/video-player.d.ts +23 -0
  100. package/dist/elements/video-player.d.ts.map +1 -0
  101. package/dist/elements/video-skin.d.ts +23 -0
  102. package/dist/elements/video-skin.d.ts.map +1 -0
  103. package/dist/features/captions.d.ts +8 -0
  104. package/dist/features/captions.d.ts.map +1 -0
  105. package/dist/features/create-player.d.ts +45 -0
  106. package/dist/features/create-player.d.ts.map +1 -0
  107. package/dist/features/fullscreen.d.ts +8 -0
  108. package/dist/features/fullscreen.d.ts.map +1 -0
  109. package/dist/features/gestures.d.ts +8 -0
  110. package/dist/features/gestures.d.ts.map +1 -0
  111. package/dist/features/index.d.ts +26 -0
  112. package/dist/features/index.d.ts.map +1 -0
  113. package/dist/features/index.js +31 -0
  114. package/dist/features/index.js.map +9 -0
  115. package/dist/features/keyboard.d.ts +8 -0
  116. package/dist/features/keyboard.d.ts.map +1 -0
  117. package/dist/features/pip.d.ts +8 -0
  118. package/dist/features/pip.d.ts.map +1 -0
  119. package/dist/features/playback.d.ts +8 -0
  120. package/dist/features/playback.d.ts.map +1 -0
  121. package/dist/features/quality.d.ts +8 -0
  122. package/dist/features/quality.d.ts.map +1 -0
  123. package/dist/features/types.d.ts +26 -0
  124. package/dist/features/types.d.ts.map +1 -0
  125. package/dist/features/volume.d.ts +8 -0
  126. package/dist/features/volume.d.ts.map +1 -0
  127. package/dist/index.d.ts +84 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/index.js +8149 -0
  130. package/dist/index.js.map +31 -0
  131. package/dist/layouts/index.d.ts +58 -0
  132. package/dist/layouts/index.d.ts.map +1 -0
  133. package/dist/player.d.ts +90 -0
  134. package/dist/player.d.ts.map +1 -0
  135. package/dist/plugins/ads.d.ts +311 -0
  136. package/dist/plugins/ads.d.ts.map +1 -0
  137. package/dist/plugins/analytics.d.ts +203 -0
  138. package/dist/plugins/analytics.d.ts.map +1 -0
  139. package/dist/plugins/end-screen.d.ts +184 -0
  140. package/dist/plugins/end-screen.d.ts.map +1 -0
  141. package/dist/plugins/index.d.ts +13 -0
  142. package/dist/plugins/index.d.ts.map +1 -0
  143. package/dist/plugins/index.js +20 -0
  144. package/dist/plugins/index.js.map +9 -0
  145. package/dist/plugins/skip-segments.d.ts +159 -0
  146. package/dist/plugins/skip-segments.d.ts.map +1 -0
  147. package/dist/plugins/watermark.d.ts +157 -0
  148. package/dist/plugins/watermark.d.ts.map +1 -0
  149. package/dist/providers/base.d.ts +56 -0
  150. package/dist/providers/base.d.ts.map +1 -0
  151. package/dist/providers/dash.d.ts +94 -0
  152. package/dist/providers/dash.d.ts.map +1 -0
  153. package/dist/providers/hls.d.ts +182 -0
  154. package/dist/providers/hls.d.ts.map +1 -0
  155. package/dist/providers/html5.d.ts +64 -0
  156. package/dist/providers/html5.d.ts.map +1 -0
  157. package/dist/providers/index.d.ts +41 -0
  158. package/dist/providers/index.d.ts.map +1 -0
  159. package/dist/providers/vimeo.d.ts +157 -0
  160. package/dist/providers/vimeo.d.ts.map +1 -0
  161. package/dist/providers/youtube.d.ts +134 -0
  162. package/dist/providers/youtube.d.ts.map +1 -0
  163. package/dist/stx/index.d.ts +59 -0
  164. package/dist/stx/index.d.ts.map +1 -0
  165. package/dist/stx/index.js +480 -0
  166. package/dist/stx/index.js.map +11 -0
  167. package/dist/types.d.ts +724 -0
  168. package/dist/types.d.ts.map +1 -0
  169. package/dist/ui/components.d.ts +121 -0
  170. package/dist/ui/components.d.ts.map +1 -0
  171. package/dist/ui/index.d.ts +9 -0
  172. package/dist/ui/index.d.ts.map +1 -0
  173. package/dist/ui/index.js +16 -0
  174. package/dist/ui/index.js.map +9 -0
  175. package/package.json +109 -0
  176. package/src/cdn.test.ts +10 -0
  177. package/src/cdn.ts +60 -0
  178. package/src/config.ts +25 -0
  179. package/src/core/audio-gain.ts +171 -0
  180. package/src/core/caption-styles.ts +568 -0
  181. package/src/core/chapters.ts +551 -0
  182. package/src/core/error-overlay.ts +354 -0
  183. package/src/core/events.ts +552 -0
  184. package/src/core/features.ts +287 -0
  185. package/src/core/i18n.ts +590 -0
  186. package/src/core/index.ts +249 -0
  187. package/src/core/live.ts +472 -0
  188. package/src/core/media-session.ts +137 -0
  189. package/src/core/orientation.ts +57 -0
  190. package/src/core/popover.ts +143 -0
  191. package/src/core/remote-playback.ts +661 -0
  192. package/src/core/settings-menu.ts +630 -0
  193. package/src/core/spinner.ts +310 -0
  194. package/src/core/state.ts +476 -0
  195. package/src/core/text-tracks.ts +599 -0
  196. package/src/core/themes.ts +607 -0
  197. package/src/core/thumbnails.ts +457 -0
  198. package/src/core/tooltips.ts +370 -0
  199. package/src/elements/base.ts +2 -0
  200. package/src/elements/index.ts +82 -0
  201. package/src/elements/media-airplay-button.ts +78 -0
  202. package/src/elements/media-error-display.ts +47 -0
  203. package/src/elements/media-fullscreen-button.ts +72 -0
  204. package/src/elements/media-live-button.ts +65 -0
  205. package/src/elements/media-mute-button.ts +69 -0
  206. package/src/elements/media-pip-button.ts +66 -0
  207. package/src/elements/media-play-button.ts +69 -0
  208. package/src/elements/media-playback-rate-button.ts +59 -0
  209. package/src/elements/media-preview-time.ts +75 -0
  210. package/src/elements/media-progress-bar.ts +200 -0
  211. package/src/elements/media-remote-playback-button.ts +108 -0
  212. package/src/elements/media-seek-button.ts +50 -0
  213. package/src/elements/media-settings-menu.ts +302 -0
  214. package/src/elements/media-time-display.ts +99 -0
  215. package/src/elements/media-time-group.ts +41 -0
  216. package/src/elements/media-time-separator.ts +35 -0
  217. package/src/elements/media-volume-slider.ts +140 -0
  218. package/src/elements/utils.ts +82 -0
  219. package/src/elements/video-player.ts +175 -0
  220. package/src/elements/video-skin.ts +127 -0
  221. package/src/features/captions.ts +56 -0
  222. package/src/features/create-player.ts +91 -0
  223. package/src/features/fullscreen.ts +44 -0
  224. package/src/features/gestures.ts +44 -0
  225. package/src/features/index.ts +44 -0
  226. package/src/features/keyboard.ts +89 -0
  227. package/src/features/pip.ts +47 -0
  228. package/src/features/playback.ts +68 -0
  229. package/src/features/quality.ts +19 -0
  230. package/src/features/types.ts +28 -0
  231. package/src/features/volume.ts +46 -0
  232. package/src/index.ts +635 -0
  233. package/src/layouts/index.ts +672 -0
  234. package/src/player.ts +821 -0
  235. package/src/plugins/ads.ts +1045 -0
  236. package/src/plugins/analytics.ts +673 -0
  237. package/src/plugins/end-screen.ts +793 -0
  238. package/src/plugins/index.ts +61 -0
  239. package/src/plugins/skip-segments.ts +537 -0
  240. package/src/plugins/watermark.ts +537 -0
  241. package/src/providers/base.ts +197 -0
  242. package/src/providers/dash.ts +588 -0
  243. package/src/providers/hls.ts +624 -0
  244. package/src/providers/html5.ts +473 -0
  245. package/src/providers/index.ts +97 -0
  246. package/src/providers/vimeo.ts +533 -0
  247. package/src/providers/youtube.ts +542 -0
  248. package/src/stx/index.ts +606 -0
  249. package/src/types.ts +874 -0
  250. package/src/ui/components.ts +910 -0
  251. 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
+ }