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,533 @@
1
+ /**
2
+ * ts-video-player Vimeo Provider
3
+ *
4
+ * Vimeo embed provider using the Vimeo Player API.
5
+ *
6
+ * @module providers/vimeo
7
+ */
8
+
9
+ import type { ProviderType, Src, VimeoSource, PlayerOptions, VideoQuality } from '../types'
10
+ import { BaseProvider } from './base'
11
+
12
+ // =============================================================================
13
+ // Vimeo URL Parsing
14
+ // =============================================================================
15
+
16
+ /**
17
+ * Vimeo URL patterns
18
+ */
19
+ const VIMEO_PATTERNS = [
20
+ /vimeo\.com\/(\d+)/,
21
+ /vimeo\.com\/video\/(\d+)/,
22
+ /player\.vimeo\.com\/video\/(\d+)/,
23
+ /vimeo\.com\/channels\/[\w]+\/(\d+)/,
24
+ /vimeo\.com\/groups\/[\w]+\/videos\/(\d+)/,
25
+ ]
26
+
27
+ /**
28
+ * Extract Vimeo video ID from URL
29
+ */
30
+ export function extractVimeoId(url: string): string | null {
31
+ for (const pattern of VIMEO_PATTERNS) {
32
+ const match = url.match(pattern)
33
+ if (match) return match[1]
34
+ }
35
+ return null
36
+ }
37
+
38
+ /**
39
+ * Extract hash for private videos
40
+ */
41
+ export function extractVimeoHash(url: string): string | null {
42
+ // Format: vimeo.com/123456789/abcdef123
43
+ const match = url.match(/vimeo\.com\/\d+\/([a-f0-9]+)/)
44
+ return match ? match[1] : null
45
+ }
46
+
47
+ /**
48
+ * Check if source is a Vimeo source
49
+ */
50
+ export function isVimeoSource(src: Src): boolean {
51
+ if (typeof src === 'string') {
52
+ return extractVimeoId(src) !== null
53
+ }
54
+
55
+ if (typeof src === 'object' && 'type' in src) {
56
+ return (src as VimeoSource).type === 'vimeo'
57
+ }
58
+
59
+ return false
60
+ }
61
+
62
+ /**
63
+ * Parse Vimeo source
64
+ */
65
+ function parseVimeoSource(src: Src): { videoId: string; hash?: string } | null {
66
+ if (typeof src === 'string') {
67
+ const videoId = extractVimeoId(src)
68
+ if (!videoId) return null
69
+ return { videoId, hash: extractVimeoHash(src) || undefined }
70
+ }
71
+
72
+ if (typeof src === 'object' && 'type' in src && (src as VimeoSource).type === 'vimeo') {
73
+ const vSrc = src as VimeoSource
74
+ return {
75
+ videoId: vSrc.videoId || extractVimeoId(vSrc.src) || '',
76
+ hash: vSrc.hash,
77
+ }
78
+ }
79
+
80
+ return null
81
+ }
82
+
83
+ // =============================================================================
84
+ // Vimeo Player Types
85
+ // =============================================================================
86
+
87
+ interface VimeoPlayerOptions {
88
+ id?: number
89
+ url?: string
90
+ autopause?: boolean
91
+ autoplay?: boolean
92
+ background?: boolean
93
+ byline?: boolean
94
+ color?: string
95
+ controls?: boolean
96
+ dnt?: boolean
97
+ height?: number
98
+ loop?: boolean
99
+ maxheight?: number
100
+ maxwidth?: number
101
+ muted?: boolean
102
+ playsinline?: boolean
103
+ portrait?: boolean
104
+ responsive?: boolean
105
+ speed?: boolean
106
+ title?: boolean
107
+ transparent?: boolean
108
+ width?: number
109
+ }
110
+
111
+ interface VimeoPlayer {
112
+ ready(): Promise<void>
113
+ play(): Promise<void>
114
+ pause(): Promise<void>
115
+ unload(): Promise<void>
116
+ destroy(): Promise<void>
117
+ getCurrentTime(): Promise<number>
118
+ setCurrentTime(seconds: number): Promise<number>
119
+ getDuration(): Promise<number>
120
+ getVolume(): Promise<number>
121
+ setVolume(volume: number): Promise<number>
122
+ getMuted(): Promise<boolean>
123
+ setMuted(muted: boolean): Promise<boolean>
124
+ getLoop(): Promise<boolean>
125
+ setLoop(loop: boolean): Promise<boolean>
126
+ getPlaybackRate(): Promise<number>
127
+ setPlaybackRate(rate: number): Promise<number>
128
+ getPaused(): Promise<boolean>
129
+ getEnded(): Promise<boolean>
130
+ getVideoTitle(): Promise<string>
131
+ getVideoWidth(): Promise<number>
132
+ getVideoHeight(): Promise<number>
133
+ getVideoId(): Promise<number>
134
+ getVideoUrl(): Promise<string>
135
+ getQualities(): Promise<Array<{ id: string; label: string; active: boolean }>>
136
+ setQuality(quality: string): Promise<string>
137
+ getTextTracks(): Promise<Array<{ label: string; language: string; kind: string; mode: string }>>
138
+ enableTextTrack(language: string, kind?: string): Promise<void>
139
+ disableTextTrack(): Promise<void>
140
+ on(event: string, callback: (...args: any[]) => void): void
141
+ off(event: string, callback?: (...args: any[]) => void): void
142
+ getBuffered(): Promise<Array<{ start: number; end: number }>>
143
+ getChapters(): Promise<Array<{ title: string; startTime: number; index: number }>>
144
+ }
145
+
146
+ interface VimeoStatic {
147
+ Player: new (element: HTMLElement | HTMLIFrameElement, options?: VimeoPlayerOptions) => VimeoPlayer
148
+ }
149
+
150
+ declare global {
151
+ interface Window {
152
+ Vimeo?: VimeoStatic
153
+ }
154
+ }
155
+
156
+ // =============================================================================
157
+ // Vimeo Provider
158
+ // =============================================================================
159
+
160
+ const VIMEO_SDK_URL = 'https://player.vimeo.com/api/player.js'
161
+
162
+ /**
163
+ * Vimeo embed provider
164
+ */
165
+ export class VimeoProvider extends BaseProvider {
166
+ readonly name = 'vimeo'
167
+ readonly type: ProviderType = 'vimeo'
168
+
169
+ private container_el: HTMLElement | null = null
170
+ private player: VimeoPlayer | null = null
171
+ private iframe: HTMLIFrameElement | null = null
172
+ private timeUpdateInterval: ReturnType<typeof setInterval> | null = null
173
+ private _qualities: VideoQuality[] = []
174
+ private _duration = 0
175
+ get mediaElement(): HTMLIFrameElement | null {
176
+ return this.iframe
177
+ }
178
+
179
+ // === Lifecycle ===
180
+
181
+ protected async initialize(): Promise<void> {
182
+ if (!this.container) {
183
+ throw new Error('Container element is required')
184
+ }
185
+
186
+ // Load Vimeo SDK
187
+ await this.loadVimeoSDK()
188
+
189
+ // Create container for Vimeo player
190
+ this.container_el = document.createElement('div')
191
+ this.container_el.className = 'ts-video-player__vimeo'
192
+ this.container.appendChild(this.container_el)
193
+ }
194
+
195
+ private async loadVimeoSDK(): Promise<void> {
196
+ if (window.Vimeo) return
197
+
198
+ return new Promise((resolve, reject) => {
199
+ if (document.querySelector(`script[src="${VIMEO_SDK_URL}"]`)) {
200
+ const check = () => {
201
+ if (window.Vimeo) resolve()
202
+ else setTimeout(check, 100)
203
+ }
204
+ check()
205
+ return
206
+ }
207
+
208
+ const script = document.createElement('script')
209
+ script.src = VIMEO_SDK_URL
210
+ script.async = true
211
+ script.onload = () => resolve()
212
+ script.onerror = () => reject(new Error('Failed to load Vimeo SDK'))
213
+ document.head.appendChild(script)
214
+ })
215
+ }
216
+
217
+ protected cleanup(): void {
218
+ if (this.timeUpdateInterval) {
219
+ clearInterval(this.timeUpdateInterval)
220
+ this.timeUpdateInterval = null
221
+ }
222
+
223
+ this.player?.destroy()
224
+ this.player = null
225
+ this.iframe = null
226
+ this.container_el?.remove()
227
+ this.container_el = null
228
+ }
229
+
230
+ // === Loading ===
231
+
232
+ canPlay(src: Src): boolean {
233
+ return isVimeoSource(src)
234
+ }
235
+
236
+ async load(src: Src): Promise<void> {
237
+ const parsed = parseVimeoSource(src)
238
+ if (!parsed || !this.container_el) {
239
+ this.emitError(2, 'Invalid Vimeo source')
240
+ return
241
+ }
242
+
243
+ // Destroy existing player
244
+ if (this.player) {
245
+ await this.player.destroy()
246
+ this.player = null
247
+ }
248
+
249
+ // Build player options
250
+ const playerOptions: VimeoPlayerOptions = {
251
+ id: parseInt(parsed.videoId, 10),
252
+ autopause: false,
253
+ autoplay: this.options.autoplay || false,
254
+ controls: false, // We use our own controls
255
+ loop: this.options.loop || false,
256
+ muted: this.options.muted || false,
257
+ playsinline: this.options.playsinline !== false,
258
+ responsive: true,
259
+ dnt: true, // Do Not Track
260
+ }
261
+
262
+ // Apply custom Vimeo params
263
+ if (this.options.vimeoParams) {
264
+ Object.assign(playerOptions, this.options.vimeoParams)
265
+ }
266
+
267
+ // Create player
268
+ this.player = new window.Vimeo!.Player(this.container_el, playerOptions)
269
+
270
+ // Wait for ready
271
+ await this.player.ready()
272
+
273
+ // Get iframe
274
+ this.iframe = this.container_el.querySelector('iframe')
275
+ if (this.iframe) {
276
+ this.iframe.className = 'ts-video-player__media'
277
+ }
278
+
279
+ // Setup event listeners
280
+ this.attachVimeoEvents()
281
+
282
+ // Emit ready
283
+ this.events.emit('loadedmetadata')
284
+ this.events.emit('canplay')
285
+
286
+ // Get duration
287
+ this._duration = await this.player.getDuration()
288
+ this.events.emit('durationchange', this._duration)
289
+
290
+ // Get initial qualities
291
+ await this.updateQualities()
292
+
293
+ // Emit dimensions
294
+ const [width, height] = await Promise.all([this.player.getVideoWidth(), this.player.getVideoHeight()])
295
+ this.events.emit('statechange', {
296
+ videoWidth: width,
297
+ videoHeight: height,
298
+ aspectRatio: width / height || 16 / 9,
299
+ canFullscreen: true,
300
+ canPictureInPicture: false,
301
+ })
302
+ }
303
+
304
+ private attachVimeoEvents(): void {
305
+ if (!this.player) return
306
+
307
+ // Play
308
+ this.player.on('play', () => {
309
+ this.events.emit('play')
310
+ this.events.emit('playing')
311
+ this.events.emit('statechange', { paused: false, playing: true })
312
+ })
313
+
314
+ // Pause
315
+ this.player.on('pause', () => {
316
+ this.events.emit('pause')
317
+ this.events.emit('statechange', { paused: true, playing: false })
318
+ })
319
+
320
+ // Ended
321
+ this.player.on('ended', () => {
322
+ this.events.emit('ended')
323
+ this.events.emit('statechange', { ended: true, playing: false })
324
+ })
325
+
326
+ // Time update
327
+ this.player.on('timeupdate', (data: { seconds: number }) => {
328
+ this.events.emit('timeupdate', data.seconds)
329
+ })
330
+
331
+ // Seeking
332
+ this.player.on('seeking', () => {
333
+ this.events.emit('seeking')
334
+ })
335
+
336
+ this.player.on('seeked', () => {
337
+ this.events.emit('seeked')
338
+ })
339
+
340
+ // Buffering
341
+ this.player.on('bufferstart', () => {
342
+ this.events.emit('waiting')
343
+ this.events.emit('statechange', { waiting: true })
344
+ })
345
+
346
+ this.player.on('bufferend', () => {
347
+ this.events.emit('statechange', { waiting: false })
348
+ })
349
+
350
+ // Progress
351
+ this.player.on('progress', (data: { percent: number }) => {
352
+ const buffered = [{ start: 0, end: data.percent * this._duration }]
353
+ this.events.emit('progress', buffered)
354
+ })
355
+
356
+ // Volume
357
+ this.player.on('volumechange', async () => {
358
+ const [volume, muted] = await Promise.all([this.player!.getVolume(), this.player!.getMuted()])
359
+ this.events.emit('volumechange', volume, muted)
360
+ })
361
+
362
+ // Playback rate
363
+ this.player.on('playbackratechange', (data: { playbackRate: number }) => {
364
+ this.events.emit('ratechange', data.playbackRate)
365
+ })
366
+
367
+ // Quality change
368
+ this.player.on('qualitychange', async () => {
369
+ await this.updateQualities()
370
+ const current = this._qualities.find((q) => q.selected)
371
+ this.events.emit('qualitychange', current || null)
372
+ })
373
+
374
+ // Error
375
+ this.player.on('error', (error: { name: string; message: string }) => {
376
+ this.emitError(5, error.message, error)
377
+ })
378
+ }
379
+
380
+ private async updateQualities(): Promise<void> {
381
+ if (!this.player) return
382
+
383
+ try {
384
+ const qualities = await this.player.getQualities()
385
+ this._qualities = qualities.map((q) => {
386
+ // Parse resolution from label (e.g., "1080p", "720p")
387
+ const match = q.label.match(/(\d+)p/)
388
+ const height = match ? parseInt(match[1], 10) : 0
389
+
390
+ return {
391
+ id: q.id,
392
+ width: Math.round(height * (16 / 9)),
393
+ height,
394
+ bitrate: 0,
395
+ selected: q.active,
396
+ }
397
+ })
398
+
399
+ this.events.emit('statechange', { qualities: this._qualities })
400
+ } catch {
401
+ // Quality API not available for all videos
402
+ }
403
+ }
404
+
405
+ // === Playback ===
406
+
407
+ async play(): Promise<void> {
408
+ try {
409
+ await this.player?.play()
410
+ } catch (error) {
411
+ if ((error as Error).name === 'NotAllowedError') {
412
+ this.emitError(1, 'Playback was blocked. User interaction required.', error)
413
+ } else {
414
+ throw error
415
+ }
416
+ }
417
+ }
418
+
419
+ pause(): void {
420
+ this.player?.pause()
421
+ }
422
+
423
+ stop(): void {
424
+ this.pause()
425
+ this.seekTo(0)
426
+ }
427
+
428
+ // === Seeking ===
429
+
430
+ seekTo(time: number): void {
431
+ this.player?.setCurrentTime(time)
432
+ }
433
+
434
+ getCurrentTime(): number {
435
+ // Sync call not available, return cached or 0
436
+ return 0
437
+ }
438
+
439
+ getDuration(): number {
440
+ return this._duration
441
+ }
442
+
443
+ // === Volume ===
444
+
445
+ setVolume(volume: number): void {
446
+ this.player?.setVolume(Math.max(0, Math.min(1, volume)))
447
+ }
448
+
449
+ getVolume(): number {
450
+ return 1 // Async, return default
451
+ }
452
+
453
+ setMuted(muted: boolean): void {
454
+ this.player?.setMuted(muted)
455
+ }
456
+
457
+ getMuted(): boolean {
458
+ return false // Async, return default
459
+ }
460
+
461
+ // === Playback Rate ===
462
+
463
+ setPlaybackRate(rate: number): void {
464
+ this.player?.setPlaybackRate(rate)
465
+ }
466
+
467
+ getPlaybackRate(): number {
468
+ return 1 // Async, return default
469
+ }
470
+
471
+ // === Quality ===
472
+
473
+ getQualities(): VideoQuality[] {
474
+ return this._qualities
475
+ }
476
+
477
+ setQuality(quality: VideoQuality | 'auto'): void {
478
+ if (!this.player) return
479
+
480
+ if (quality === 'auto') {
481
+ this.player.setQuality('auto')
482
+ } else {
483
+ this.player.setQuality(quality.id)
484
+ }
485
+ }
486
+
487
+ // === Text Tracks ===
488
+
489
+ async enableTextTrack(language: string): Promise<void> {
490
+ await this.player?.enableTextTrack(language)
491
+ }
492
+
493
+ async disableTextTrack(): Promise<void> {
494
+ await this.player?.disableTextTrack()
495
+ }
496
+
497
+ // === Chapters ===
498
+
499
+ async getChapters(): Promise<Array<{ title: string; startTime: number; index: number }>> {
500
+ if (!this.player) return []
501
+ try {
502
+ return await this.player.getChapters()
503
+ } catch {
504
+ return []
505
+ }
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Vimeo Provider Loader
511
+ */
512
+ export const vimeoLoader = {
513
+ name: 'vimeo',
514
+ type: 'vimeo' as ProviderType,
515
+
516
+ canPlay(src: Src): boolean {
517
+ return isVimeoSource(src)
518
+ },
519
+
520
+ mediaType(): 'video' {
521
+ return 'video'
522
+ },
523
+
524
+ preconnect(): string[] {
525
+ return ['https://player.vimeo.com', 'https://i.vimeocdn.com', 'https://f.vimeocdn.com']
526
+ },
527
+
528
+ async load(container: HTMLElement, options: PlayerOptions): Promise<VimeoProvider> {
529
+ const provider = new VimeoProvider()
530
+ await provider.setup(container, options)
531
+ return provider
532
+ },
533
+ }