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,552 @@
1
+ /**
2
+ * ts-video-player Event System
3
+ *
4
+ * Event emitter and media event normalization.
5
+ *
6
+ * @module core/events
7
+ */
8
+
9
+ import type { ProviderEventMap, MediaError, TimeRange } from '../types'
10
+
11
+ /**
12
+ * Type-safe event emitter
13
+ */
14
+ export class EventEmitter<T extends { [K in keyof T]: (...args: any[]) => void }> {
15
+ private listeners = new Map<keyof T, Set<T[keyof T]>>()
16
+ private onceListeners = new Map<keyof T, Set<T[keyof T]>>()
17
+
18
+ /**
19
+ * Add an event listener
20
+ */
21
+ on<K extends keyof T>(event: K, handler: T[K]): () => void {
22
+ if (!this.listeners.has(event)) {
23
+ this.listeners.set(event, new Set())
24
+ }
25
+ this.listeners.get(event)!.add(handler)
26
+
27
+ return () => this.off(event, handler)
28
+ }
29
+
30
+ /**
31
+ * Add a one-time event listener
32
+ */
33
+ once<K extends keyof T>(event: K, handler: T[K]): () => void {
34
+ if (!this.onceListeners.has(event)) {
35
+ this.onceListeners.set(event, new Set())
36
+ }
37
+ this.onceListeners.get(event)!.add(handler)
38
+
39
+ return () => this.onceListeners.get(event)?.delete(handler)
40
+ }
41
+
42
+ /**
43
+ * Remove an event listener
44
+ */
45
+ off<K extends keyof T>(event: K, handler: T[K]): void {
46
+ this.listeners.get(event)?.delete(handler)
47
+ this.onceListeners.get(event)?.delete(handler)
48
+ }
49
+
50
+ /**
51
+ * Emit an event
52
+ */
53
+ emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>): void {
54
+ // Call regular listeners
55
+ this.listeners.get(event)?.forEach((handler) => {
56
+ try {
57
+ ;(handler as (...args: any[]) => void)(...args)
58
+ } catch (error) {
59
+ console.error(`[ts-video-player] Error in event handler for "${String(event)}":`, error)
60
+ }
61
+ })
62
+
63
+ // Call and remove once listeners
64
+ const onceHandlers = this.onceListeners.get(event)
65
+ if (onceHandlers) {
66
+ onceHandlers.forEach((handler) => {
67
+ try {
68
+ ;(handler as (...args: any[]) => void)(...args)
69
+ } catch (error) {
70
+ console.error(`[ts-video-player] Error in once handler for "${String(event)}":`, error)
71
+ }
72
+ })
73
+ onceHandlers.clear()
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Remove all listeners for an event (or all events)
79
+ */
80
+ removeAllListeners(event?: keyof T): void {
81
+ if (event) {
82
+ this.listeners.delete(event)
83
+ this.onceListeners.delete(event)
84
+ } else {
85
+ this.listeners.clear()
86
+ this.onceListeners.clear()
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Get listener count for an event
92
+ */
93
+ listenerCount(event: keyof T): number {
94
+ return (this.listeners.get(event)?.size || 0) + (this.onceListeners.get(event)?.size || 0)
95
+ }
96
+ }
97
+
98
+ // =============================================================================
99
+ // Media Events Normalizer
100
+ // =============================================================================
101
+
102
+ /**
103
+ * Normalize HTMLMediaElement events to our event system
104
+ */
105
+ export class MediaEventsNormalizer {
106
+ private media: HTMLMediaElement
107
+ private emitter: EventEmitter<ProviderEventMap>
108
+ private listeners: Array<() => void> = []
109
+
110
+ constructor(media: HTMLMediaElement, emitter: EventEmitter<ProviderEventMap>) {
111
+ this.media = media
112
+ this.emitter = emitter
113
+ this.attachListeners()
114
+ }
115
+
116
+ private attachListeners(): void {
117
+ const on = (event: string, handler: (e: Event) => void) => {
118
+ this.media.addEventListener(event, handler)
119
+ this.listeners.push(() => this.media.removeEventListener(event, handler))
120
+ }
121
+
122
+ // Loading events
123
+ on('loadstart', () => this.emitter.emit('loadstart'))
124
+ on('loadedmetadata', () => this.emitter.emit('loadedmetadata'))
125
+ on('loadeddata', () => this.emitter.emit('loadeddata'))
126
+ on('canplay', () => this.emitter.emit('canplay'))
127
+ on('canplaythrough', () => this.emitter.emit('canplaythrough'))
128
+
129
+ // Playback events
130
+ on('play', () => this.emitter.emit('play'))
131
+ on('pause', () => this.emitter.emit('pause'))
132
+ on('playing', () => this.emitter.emit('playing'))
133
+ on('waiting', () => this.emitter.emit('waiting'))
134
+ on('ended', () => this.emitter.emit('ended'))
135
+
136
+ // Seeking events
137
+ on('seeking', () => this.emitter.emit('seeking'))
138
+ on('seeked', () => this.emitter.emit('seeked'))
139
+
140
+ // Time/duration events
141
+ on('timeupdate', () => this.emitter.emit('timeupdate', this.media.currentTime))
142
+ on('durationchange', () => this.emitter.emit('durationchange', this.media.duration))
143
+
144
+ // Volume events
145
+ on('volumechange', () => this.emitter.emit('volumechange', this.media.volume, this.media.muted))
146
+
147
+ // Rate events
148
+ on('ratechange', () => this.emitter.emit('ratechange', this.media.playbackRate))
149
+
150
+ // Progress events
151
+ on('progress', () => {
152
+ const buffered = this.getTimeRanges(this.media.buffered)
153
+ this.emitter.emit('progress', buffered)
154
+ })
155
+
156
+ // Error events
157
+ on('error', () => {
158
+ const error = this.normalizeError(this.media.error)
159
+ this.emitter.emit('error', error)
160
+ })
161
+ }
162
+
163
+ private getTimeRanges(ranges: TimeRanges): TimeRange[] {
164
+ const result: TimeRange[] = []
165
+ for (let i = 0; i < ranges.length; i++) {
166
+ result.push({
167
+ start: ranges.start(i),
168
+ end: ranges.end(i),
169
+ })
170
+ }
171
+ return result
172
+ }
173
+
174
+ private normalizeError(error: globalThis.MediaError | null): MediaError {
175
+ if (!error) {
176
+ return { code: 0, message: 'Unknown error' }
177
+ }
178
+
179
+ const messages: Record<number, string> = {
180
+ 1: 'The fetching of the media resource was aborted by the user agent.',
181
+ 2: 'A network error occurred while fetching the media resource.',
182
+ 3: 'An error occurred while decoding the media resource.',
183
+ 4: 'The media resource is not supported.',
184
+ }
185
+
186
+ return {
187
+ code: error.code,
188
+ message: messages[error.code] || error.message || 'Unknown error',
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Destroy the normalizer
194
+ */
195
+ destroy(): void {
196
+ this.listeners.forEach((remove) => remove())
197
+ this.listeners = []
198
+ }
199
+ }
200
+
201
+ // =============================================================================
202
+ // Fullscreen Events
203
+ // =============================================================================
204
+
205
+ /**
206
+ * Normalize fullscreen change events
207
+ */
208
+ export function onFullscreenChange(handler: (fullscreen: boolean) => void): () => void {
209
+ const events = ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange']
210
+
211
+ const listener = () => {
212
+ const fullscreen = !!(
213
+ document.fullscreenElement ||
214
+ (document as any).webkitFullscreenElement ||
215
+ (document as any).mozFullScreenElement ||
216
+ (document as any).msFullscreenElement
217
+ )
218
+ handler(fullscreen)
219
+ }
220
+
221
+ events.forEach((event) => document.addEventListener(event, listener))
222
+
223
+ return () => {
224
+ events.forEach((event) => document.removeEventListener(event, listener))
225
+ }
226
+ }
227
+
228
+ // =============================================================================
229
+ // Picture-in-Picture Events
230
+ // =============================================================================
231
+
232
+ /**
233
+ * Normalize PiP change events
234
+ */
235
+ export function onPiPChange(video: HTMLVideoElement, handler: (pip: boolean) => void): () => void {
236
+ const enterHandler = () => handler(true)
237
+ const leaveHandler = () => handler(false)
238
+
239
+ video.addEventListener('enterpictureinpicture', enterHandler)
240
+ video.addEventListener('leavepictureinpicture', leaveHandler)
241
+
242
+ return () => {
243
+ video.removeEventListener('enterpictureinpicture', enterHandler)
244
+ video.removeEventListener('leavepictureinpicture', leaveHandler)
245
+ }
246
+ }
247
+
248
+ // =============================================================================
249
+ // Keyboard Events
250
+ // =============================================================================
251
+
252
+ /**
253
+ * Default keyboard shortcuts
254
+ */
255
+ export const DEFAULT_KEYBOARD_SHORTCUTS: Record<string, string> = {
256
+ ' ': 'togglePlay',
257
+ k: 'togglePlay',
258
+ m: 'toggleMute',
259
+ f: 'toggleFullscreen',
260
+ i: 'togglePiP',
261
+ c: 'toggleCaptions',
262
+ ArrowLeft: 'seekBackward',
263
+ ArrowRight: 'seekForward',
264
+ j: 'seekBackward',
265
+ l: 'seekForward',
266
+ ArrowUp: 'volumeUp',
267
+ ArrowDown: 'volumeDown',
268
+ '>': 'speedUp',
269
+ '<': 'speedDown',
270
+ Home: 'seekStart',
271
+ End: 'seekEnd',
272
+ '0': 'seek0',
273
+ '1': 'seek10',
274
+ '2': 'seek20',
275
+ '3': 'seek30',
276
+ '4': 'seek40',
277
+ '5': 'seek50',
278
+ '6': 'seek60',
279
+ '7': 'seek70',
280
+ '8': 'seek80',
281
+ '9': 'seek90',
282
+ }
283
+
284
+ /**
285
+ * Action handlers for keyboard shortcuts
286
+ */
287
+ export interface KeyboardActions {
288
+ togglePlay(): void
289
+ toggleMute(): void
290
+ toggleFullscreen(): void
291
+ togglePiP(): void
292
+ toggleCaptions(): void
293
+ seekBackward(): void
294
+ seekForward(): void
295
+ volumeUp(): void
296
+ volumeDown(): void
297
+ speedUp(): void
298
+ speedDown(): void
299
+ seekStart(): void
300
+ seekEnd(): void
301
+ seekPercent(percent: number): void
302
+ }
303
+
304
+ /**
305
+ * Create keyboard event handler
306
+ */
307
+ export function createKeyboardHandler(
308
+ actions: KeyboardActions,
309
+ options: { seekStep?: number; volumeStep?: number; speedStep?: number } = {},
310
+ ): (event: KeyboardEvent) => void {
311
+ const { seekStep: _seekStep = 5, volumeStep: _volumeStep = 0.1, speedStep: _speedStep = 0.25 } = options
312
+
313
+ return (event: KeyboardEvent) => {
314
+ // Ignore if in input field
315
+ if (
316
+ event.target instanceof HTMLInputElement ||
317
+ event.target instanceof HTMLTextAreaElement ||
318
+ event.target instanceof HTMLSelectElement ||
319
+ (event.target as HTMLElement)?.isContentEditable
320
+ ) {
321
+ return
322
+ }
323
+
324
+ const key = event.key
325
+ const action = DEFAULT_KEYBOARD_SHORTCUTS[key]
326
+
327
+ if (!action) return
328
+
329
+ // Prevent default for handled keys
330
+ event.preventDefault()
331
+
332
+ switch (action) {
333
+ case 'togglePlay':
334
+ actions.togglePlay()
335
+ break
336
+ case 'toggleMute':
337
+ actions.toggleMute()
338
+ break
339
+ case 'toggleFullscreen':
340
+ actions.toggleFullscreen()
341
+ break
342
+ case 'togglePiP':
343
+ actions.togglePiP()
344
+ break
345
+ case 'toggleCaptions':
346
+ actions.toggleCaptions()
347
+ break
348
+ case 'seekBackward':
349
+ actions.seekBackward()
350
+ break
351
+ case 'seekForward':
352
+ actions.seekForward()
353
+ break
354
+ case 'volumeUp':
355
+ actions.volumeUp()
356
+ break
357
+ case 'volumeDown':
358
+ actions.volumeDown()
359
+ break
360
+ case 'speedUp':
361
+ actions.speedUp()
362
+ break
363
+ case 'speedDown':
364
+ actions.speedDown()
365
+ break
366
+ case 'seekStart':
367
+ actions.seekStart()
368
+ break
369
+ case 'seekEnd':
370
+ actions.seekEnd()
371
+ break
372
+ default:
373
+ // Handle number keys for percentage seeking
374
+ if (action.startsWith('seek')) {
375
+ const percent = parseInt(action.replace('seek', ''), 10)
376
+ if (!isNaN(percent)) {
377
+ actions.seekPercent(percent)
378
+ }
379
+ }
380
+ }
381
+ }
382
+ }
383
+
384
+ // =============================================================================
385
+ // User Activity Detection
386
+ // =============================================================================
387
+
388
+ /**
389
+ * Create user activity detector
390
+ */
391
+ export function createActivityDetector(
392
+ element: HTMLElement,
393
+ options: { timeout?: number; onActive?: () => void; onInactive?: () => void } = {},
394
+ ): () => void {
395
+ const { timeout = 3000, onActive, onInactive } = options
396
+
397
+ let timer: ReturnType<typeof setTimeout> | null = null
398
+ let active = true
399
+
400
+ const setActive = () => {
401
+ if (!active) {
402
+ active = true
403
+ onActive?.()
404
+ }
405
+ resetTimer()
406
+ }
407
+
408
+ const setInactive = () => {
409
+ if (active) {
410
+ active = false
411
+ onInactive?.()
412
+ }
413
+ }
414
+
415
+ const resetTimer = () => {
416
+ if (timer) clearTimeout(timer)
417
+ timer = setTimeout(setInactive, timeout)
418
+ }
419
+
420
+ // Events that indicate activity
421
+ const events = ['mousemove', 'mouseenter', 'mouseleave', 'mousedown', 'keydown', 'touchstart', 'touchmove']
422
+
423
+ events.forEach((event) => element.addEventListener(event, setActive))
424
+
425
+ // Start timer
426
+ resetTimer()
427
+
428
+ // Return cleanup function
429
+ return () => {
430
+ if (timer) clearTimeout(timer)
431
+ events.forEach((event) => element.removeEventListener(event, setActive))
432
+ }
433
+ }
434
+
435
+ // =============================================================================
436
+ // Gesture Detection
437
+ // =============================================================================
438
+
439
+ export interface GestureHandlers {
440
+ onTap?: (x: number, y: number) => void
441
+ onDoubleTap?: (x: number, y: number) => void
442
+ onSwipeLeft?: () => void
443
+ onSwipeRight?: () => void
444
+ onSwipeUp?: () => void
445
+ onSwipeDown?: () => void
446
+ onScrub?: (progress: number) => void
447
+ onScrubEnd?: () => void
448
+ }
449
+
450
+ /**
451
+ * Create touch gesture detector
452
+ */
453
+ export function createGestureDetector(element: HTMLElement, handlers: GestureHandlers): () => void {
454
+ let startX = 0
455
+ let startY = 0
456
+ let startTime = 0
457
+ let lastTap = 0
458
+ let isScrubbing = false
459
+ let cachedRect: DOMRect | null = null
460
+
461
+ const SWIPE_THRESHOLD = 50
462
+ const TAP_THRESHOLD = 200
463
+ const DOUBLE_TAP_THRESHOLD = 300
464
+
465
+ const onTouchStart = (e: TouchEvent) => {
466
+ const touch = e.touches[0]
467
+ startX = touch.clientX
468
+ startY = touch.clientY
469
+ startTime = Date.now()
470
+ isScrubbing = false
471
+ cachedRect = element.getBoundingClientRect()
472
+ }
473
+
474
+ const onTouchMove = (e: TouchEvent) => {
475
+ if (!startTime) return
476
+
477
+ const touch = e.touches[0]
478
+ const deltaX = touch.clientX - startX
479
+ const deltaY = touch.clientY - startY
480
+
481
+ // Horizontal scrubbing
482
+ if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 20) {
483
+ isScrubbing = true
484
+ const rect = cachedRect || element.getBoundingClientRect()
485
+ const progress = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width))
486
+ handlers.onScrub?.(progress)
487
+ }
488
+ }
489
+
490
+ const onTouchEnd = (e: TouchEvent) => {
491
+ const endTime = Date.now()
492
+ const duration = endTime - startTime
493
+
494
+ if (isScrubbing) {
495
+ handlers.onScrubEnd?.()
496
+ isScrubbing = false
497
+ startTime = 0
498
+ return
499
+ }
500
+
501
+ const touch = e.changedTouches[0]
502
+ const deltaX = touch.clientX - startX
503
+ const deltaY = touch.clientY - startY
504
+
505
+ // Check for swipe
506
+ if (duration < 500) {
507
+ if (Math.abs(deltaX) > SWIPE_THRESHOLD && Math.abs(deltaX) > Math.abs(deltaY)) {
508
+ if (deltaX > 0) {
509
+ handlers.onSwipeRight?.()
510
+ } else {
511
+ handlers.onSwipeLeft?.()
512
+ }
513
+ startTime = 0
514
+ return
515
+ }
516
+
517
+ if (Math.abs(deltaY) > SWIPE_THRESHOLD && Math.abs(deltaY) > Math.abs(deltaX)) {
518
+ if (deltaY > 0) {
519
+ handlers.onSwipeDown?.()
520
+ } else {
521
+ handlers.onSwipeUp?.()
522
+ }
523
+ startTime = 0
524
+ return
525
+ }
526
+ }
527
+
528
+ // Check for tap
529
+ if (duration < TAP_THRESHOLD && Math.abs(deltaX) < 10 && Math.abs(deltaY) < 10) {
530
+ const now = Date.now()
531
+ if (now - lastTap < DOUBLE_TAP_THRESHOLD) {
532
+ handlers.onDoubleTap?.(touch.clientX, touch.clientY)
533
+ lastTap = 0
534
+ } else {
535
+ handlers.onTap?.(touch.clientX, touch.clientY)
536
+ lastTap = now
537
+ }
538
+ }
539
+
540
+ startTime = 0
541
+ }
542
+
543
+ element.addEventListener('touchstart', onTouchStart, { passive: true })
544
+ element.addEventListener('touchmove', onTouchMove, { passive: true })
545
+ element.addEventListener('touchend', onTouchEnd, { passive: true })
546
+
547
+ return () => {
548
+ element.removeEventListener('touchstart', onTouchStart)
549
+ element.removeEventListener('touchmove', onTouchMove)
550
+ element.removeEventListener('touchend', onTouchEnd)
551
+ }
552
+ }