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,624 @@
1
+ /**
2
+ * ts-video-player HLS Provider
3
+ *
4
+ * HLS streaming provider using hls.js library.
5
+ * Supports adaptive bitrate streaming, quality selection, and live streams.
6
+ *
7
+ * @module providers/hls
8
+ */
9
+
10
+ import type {
11
+ ProviderType,
12
+ Src,
13
+ HLSSource,
14
+ PlayerOptions,
15
+ VideoQuality,
16
+ AudioTrack,
17
+ TextTrack,
18
+ } from '../types'
19
+ import { BaseProvider } from './base'
20
+ import { MediaEventsNormalizer, onFullscreenChange, onPiPChange } from '../core/events'
21
+
22
+ // =============================================================================
23
+ // HLS Detection
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Check if source is an HLS source
28
+ */
29
+ export function isHLSSource(src: Src): boolean {
30
+ if (typeof src === 'string') {
31
+ const url = src.toLowerCase()
32
+ return url.endsWith('.m3u8') || url.includes('application/x-mpegurl') || url.includes('application/vnd.apple.mpegurl')
33
+ }
34
+
35
+ if (typeof src === 'object' && 'type' in src) {
36
+ const type = (src as HLSSource).type?.toLowerCase() || ''
37
+ return type === 'application/x-mpegurl' || type === 'application/vnd.apple.mpegurl'
38
+ }
39
+
40
+ return false
41
+ }
42
+
43
+ /**
44
+ * Check if native HLS is supported (Safari, iOS)
45
+ */
46
+ export function isNativeHLSSupported(): boolean {
47
+ const video = document.createElement('video')
48
+ return video.canPlayType('application/vnd.apple.mpegurl') !== ''
49
+ }
50
+
51
+ // =============================================================================
52
+ // HLS.js Types
53
+ // =============================================================================
54
+
55
+ interface HlsConfig {
56
+ enableWorker?: boolean
57
+ lowLatencyMode?: boolean
58
+ backBufferLength?: number
59
+ maxBufferLength?: number
60
+ maxMaxBufferLength?: number
61
+ startLevel?: number
62
+ autoStartLoad?: boolean
63
+ debug?: boolean
64
+ [key: string]: unknown
65
+ }
66
+
67
+ interface HlsLevel {
68
+ bitrate: number
69
+ width: number
70
+ height: number
71
+ codecSet: string
72
+ url: string[]
73
+ audioCodec?: string
74
+ videoCodec?: string
75
+ }
76
+
77
+ interface HlsAudioTrack {
78
+ id: number
79
+ name: string
80
+ lang?: string
81
+ default: boolean
82
+ autoselect: boolean
83
+ forced: boolean
84
+ url: string
85
+ }
86
+
87
+ interface HlsSubtitleTrack {
88
+ id: number
89
+ name: string
90
+ lang?: string
91
+ default: boolean
92
+ autoselect: boolean
93
+ forced: boolean
94
+ url: string
95
+ }
96
+
97
+ interface HlsEvents {
98
+ MEDIA_ATTACHED: string
99
+ MEDIA_DETACHED: string
100
+ MANIFEST_LOADED: string
101
+ MANIFEST_PARSED: string
102
+ LEVEL_LOADED: string
103
+ LEVEL_SWITCHED: string
104
+ AUDIO_TRACK_LOADED: string
105
+ AUDIO_TRACK_SWITCHED: string
106
+ SUBTITLE_TRACK_LOADED: string
107
+ SUBTITLE_TRACK_SWITCH: string
108
+ FRAG_LOADED: string
109
+ FRAG_BUFFERED: string
110
+ ERROR: string
111
+ }
112
+
113
+ interface Hls {
114
+ config: HlsConfig
115
+ media: HTMLVideoElement | null
116
+ currentLevel: number
117
+ nextLevel: number
118
+ loadLevel: number
119
+ autoLevelEnabled: boolean
120
+ levels: HlsLevel[]
121
+ audioTracks: HlsAudioTrack[]
122
+ audioTrack: number
123
+ subtitleTracks: HlsSubtitleTrack[]
124
+ subtitleTrack: number
125
+ liveSyncPosition: number | null
126
+
127
+ loadSource(src: string): void
128
+ attachMedia(media: HTMLVideoElement): void
129
+ detachMedia(): void
130
+ startLoad(startPosition?: number): void
131
+ stopLoad(): void
132
+ recoverMediaError(): void
133
+ destroy(): void
134
+ on(event: string, handler: (...args: any[]) => void): void
135
+ off(event: string, handler: (...args: any[]) => void): void
136
+ }
137
+
138
+ interface HlsStatic {
139
+ new (config?: HlsConfig): Hls
140
+ isSupported(): boolean
141
+ Events: HlsEvents
142
+ ErrorTypes: {
143
+ NETWORK_ERROR: string
144
+ MEDIA_ERROR: string
145
+ KEY_SYSTEM_ERROR: string
146
+ MUX_ERROR: string
147
+ OTHER_ERROR: string
148
+ }
149
+ ErrorDetails: Record<string, string>
150
+ }
151
+
152
+ declare global {
153
+ interface Window {
154
+ Hls?: HlsStatic
155
+ }
156
+ }
157
+
158
+ // =============================================================================
159
+ // HLS Provider
160
+ // =============================================================================
161
+
162
+ const HLS_CDN_DEFAULT = 'https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js'
163
+
164
+ /**
165
+ * HLS streaming provider
166
+ */
167
+ export class HLSProvider extends BaseProvider {
168
+ readonly name = 'hls'
169
+ readonly type: ProviderType = 'hls'
170
+
171
+ private video: HTMLVideoElement | null = null
172
+ private hls: Hls | null = null
173
+ private eventsNormalizer: MediaEventsNormalizer | null = null
174
+ private cleanupFullscreen: (() => void) | null = null
175
+ private cleanupPiP: (() => void) | null = null
176
+ private useNativeHLS = false
177
+ private _qualities: VideoQuality[] = []
178
+ private _audioTracks: AudioTrack[] = []
179
+ private _textTracks: TextTrack[] = []
180
+ private currentQualityIndex = -1 // -1 = auto
181
+
182
+ get mediaElement(): HTMLVideoElement | null {
183
+ return this.video
184
+ }
185
+
186
+ /**
187
+ * Whether live sync is supported
188
+ */
189
+ get canLiveSync(): boolean {
190
+ return !this.useNativeHLS && this.hls !== null
191
+ }
192
+
193
+ /**
194
+ * Get HLS.js config
195
+ */
196
+ get hlsConfig(): HlsConfig {
197
+ return this.hls?.config || {}
198
+ }
199
+
200
+ // === Lifecycle ===
201
+
202
+ protected async initialize(): Promise<void> {
203
+ if (!this.container) {
204
+ throw new Error('Container element is required')
205
+ }
206
+
207
+ // Create video element
208
+ this.video = document.createElement('video')
209
+ this.video.className = 'ts-video-player__media'
210
+
211
+ // Set attributes
212
+ if (this.options.autoplay) this.video.autoplay = true
213
+ if (this.options.loop) this.video.loop = true
214
+ if (this.options.muted) this.video.muted = true
215
+ if (this.options.playsinline) this.video.playsInline = true
216
+ if (this.options.preload) this.video.preload = this.options.preload
217
+ if (this.options.crossorigin) this.video.crossOrigin = this.options.crossorigin
218
+ if (this.options.poster) this.video.poster = this.options.poster
219
+ if (this.options.volume !== undefined) this.video.volume = this.options.volume
220
+
221
+ this.container.appendChild(this.video)
222
+
223
+ // Setup event normalization
224
+ this.eventsNormalizer = new MediaEventsNormalizer(this.video, this.events)
225
+
226
+ // Setup fullscreen/PiP listeners
227
+ this.cleanupFullscreen = onFullscreenChange((fullscreen) => {
228
+ this.events.emit('fullscreenchange', fullscreen)
229
+ })
230
+
231
+ this.cleanupPiP = onPiPChange(this.video, (pip) => {
232
+ this.events.emit('pipchange', pip)
233
+ })
234
+
235
+ // Check for native HLS support
236
+ this.useNativeHLS = isNativeHLSSupported()
237
+
238
+ // Load HLS.js if needed
239
+ if (!this.useNativeHLS) {
240
+ await this.loadHLSLibrary()
241
+ }
242
+
243
+ // Emit dimensions and feature availability on load
244
+ this.video.addEventListener('loadedmetadata', () => {
245
+ const fullscreenAvailability = this.getFeatureAvailability('fullscreen')
246
+ const pipAvailability = this.getFeatureAvailability('pip')
247
+ const volumeAvailability = this.getFeatureAvailability('volume')
248
+ this.events.emit('statechange', {
249
+ videoWidth: this.video!.videoWidth,
250
+ videoHeight: this.video!.videoHeight,
251
+ aspectRatio: (this.video!.videoHeight > 0 ? this.video!.videoWidth / this.video!.videoHeight : 0) || 16 / 9,
252
+ canFullscreen: fullscreenAvailability === 'available',
253
+ canPictureInPicture: pipAvailability === 'available',
254
+ fullscreenAvailability,
255
+ pipAvailability,
256
+ volumeAvailability,
257
+ })
258
+ this.events.emit('availabilitychange', 'fullscreen', fullscreenAvailability)
259
+ this.events.emit('availabilitychange', 'pip', pipAvailability)
260
+ this.events.emit('availabilitychange', 'volume', volumeAvailability)
261
+ })
262
+ }
263
+
264
+ private async loadHLSLibrary(): Promise<void> {
265
+ if (window.Hls) return
266
+
267
+ return new Promise((resolve, reject) => {
268
+ const script = document.createElement('script')
269
+ script.src = this.options.hlsCdnUrl || HLS_CDN_DEFAULT
270
+ script.async = true
271
+ script.onload = () => resolve()
272
+ script.onerror = () => reject(new Error('Failed to load HLS.js'))
273
+ document.head.appendChild(script)
274
+ })
275
+ }
276
+
277
+ protected cleanup(): void {
278
+ this.eventsNormalizer?.destroy()
279
+ this.cleanupFullscreen?.()
280
+ this.cleanupPiP?.()
281
+
282
+ if (this.hls) {
283
+ this.hls.destroy()
284
+ this.hls = null
285
+ }
286
+
287
+ if (this.video) {
288
+ this.video.pause()
289
+ this.video.removeAttribute('src')
290
+ this.video.load()
291
+ this.video.remove()
292
+ this.video = null
293
+ }
294
+
295
+ this._qualities = []
296
+ this._audioTracks = []
297
+ this._textTracks = []
298
+ }
299
+
300
+ // === Loading ===
301
+
302
+ canPlay(src: Src): boolean {
303
+ return isHLSSource(src)
304
+ }
305
+
306
+ async load(src: Src): Promise<void> {
307
+ if (!this.video) return
308
+
309
+ const url = typeof src === 'string' ? src : (src as HLSSource).src
310
+
311
+ if (this.useNativeHLS) {
312
+ // Use native HLS (Safari/iOS)
313
+ this.video.src = url
314
+ this.video.load()
315
+ return
316
+ }
317
+
318
+ // Use HLS.js
319
+ if (!window.Hls) {
320
+ await this.loadHLSLibrary()
321
+ }
322
+
323
+ if (!window.Hls?.isSupported()) {
324
+ this.emitError(4, 'HLS is not supported in this browser')
325
+ return
326
+ }
327
+
328
+ // Destroy existing instance
329
+ if (this.hls) {
330
+ this.hls.destroy()
331
+ }
332
+
333
+ // Build config
334
+ const config: HlsConfig = {
335
+ enableWorker: true,
336
+ lowLatencyMode: false,
337
+ ...this.options.hlsConfig,
338
+ }
339
+
340
+ // Create new HLS instance
341
+ this.hls = new window.Hls(config)
342
+ this.attachHLSEvents()
343
+
344
+ // Attach and load
345
+ this.hls.attachMedia(this.video)
346
+ this.hls.loadSource(url)
347
+ }
348
+
349
+ private attachHLSEvents(): void {
350
+ if (!this.hls || !window.Hls) return
351
+
352
+ const Hls = window.Hls
353
+
354
+ // Manifest loaded
355
+ this.hls.on(Hls.Events.MANIFEST_PARSED, (_event: string, data: { levels: HlsLevel[] }) => {
356
+ this.updateQualities(data.levels)
357
+
358
+ // Auto-start load
359
+ if (this.options.autoplay) {
360
+ this.video?.play().catch(() => {})
361
+ }
362
+ })
363
+
364
+ // Level switched
365
+ this.hls.on(Hls.Events.LEVEL_SWITCHED, (_event: string, data: { level: number }) => {
366
+ this.currentQualityIndex = data.level
367
+ this.updateQualitySelection()
368
+ this.events.emit('qualitychange', this._qualities[data.level] || null)
369
+ })
370
+
371
+ // Audio tracks loaded
372
+ this.hls.on(Hls.Events.AUDIO_TRACK_LOADED, () => {
373
+ this.updateAudioTracks()
374
+ })
375
+
376
+ // Audio track switched
377
+ this.hls.on(Hls.Events.AUDIO_TRACK_SWITCHED, () => {
378
+ this.updateAudioTracks()
379
+ const current = this._audioTracks.find((t) => t.selected)
380
+ this.events.emit('audiotrackchange', current || null)
381
+ })
382
+
383
+ // Subtitle tracks loaded
384
+ this.hls.on(Hls.Events.SUBTITLE_TRACK_LOADED, () => {
385
+ this.updateTextTracks()
386
+ })
387
+
388
+ // Error handling
389
+ this.hls.on(Hls.Events.ERROR, (_event: string, data: { type: string; details: string; fatal: boolean }) => {
390
+ if (data.fatal) {
391
+ switch (data.type) {
392
+ case Hls.ErrorTypes.NETWORK_ERROR:
393
+ // Try to recover from network error
394
+ this.hls?.startLoad()
395
+ break
396
+ case Hls.ErrorTypes.MEDIA_ERROR:
397
+ // Try to recover from media error
398
+ this.hls?.recoverMediaError()
399
+ break
400
+ default:
401
+ // Unrecoverable error
402
+ this.emitError(5, `HLS fatal error: ${data.details}`, data)
403
+ this.hls?.destroy()
404
+ break
405
+ }
406
+ }
407
+ })
408
+ }
409
+
410
+ private updateQualities(levels: HlsLevel[]): void {
411
+ this._qualities = levels.map((level, index) => ({
412
+ id: String(index),
413
+ width: level.width,
414
+ height: level.height,
415
+ bitrate: level.bitrate,
416
+ codec: level.codecSet || level.videoCodec || undefined,
417
+ selected: index === this.currentQualityIndex,
418
+ }))
419
+
420
+ this.events.emit('statechange', { qualities: this._qualities })
421
+ }
422
+
423
+ private updateQualitySelection(): void {
424
+ this._qualities = this._qualities.map((q, i) => ({
425
+ ...q,
426
+ selected: i === this.currentQualityIndex,
427
+ }))
428
+ }
429
+
430
+ private updateAudioTracks(): void {
431
+ if (!this.hls) return
432
+
433
+ this._audioTracks = this.hls.audioTracks.map((track, index) => ({
434
+ id: String(track.id),
435
+ label: track.name || `Track ${index + 1}`,
436
+ language: track.lang || '',
437
+ kind: track.default ? 'main' : 'alternative',
438
+ selected: index === this.hls!.audioTrack,
439
+ }))
440
+
441
+ this.events.emit('statechange', { audioTracks: this._audioTracks })
442
+ }
443
+
444
+ private updateTextTracks(): void {
445
+ if (!this.hls) return
446
+
447
+ this._textTracks = this.hls.subtitleTracks.map((track, index) => ({
448
+ id: String(track.id),
449
+ kind: 'subtitles' as const,
450
+ label: track.name || `Subtitle ${index + 1}`,
451
+ language: track.lang || '',
452
+ mode: (index === this.hls!.subtitleTrack ? 'showing' : 'disabled') as TextTrack['mode'],
453
+ cues: [],
454
+ }))
455
+
456
+ this.events.emit('statechange', { textTracks: this._textTracks })
457
+ }
458
+
459
+ // === Playback ===
460
+
461
+ async play(): Promise<void> {
462
+ if (!this.video) return
463
+ try {
464
+ await this.video.play()
465
+ } catch (error) {
466
+ if ((error as Error).name === 'NotAllowedError') {
467
+ this.emitError(1, 'Playback was blocked. User interaction required.', error)
468
+ } else {
469
+ throw error
470
+ }
471
+ }
472
+ }
473
+
474
+ pause(): void {
475
+ this.video?.pause()
476
+ }
477
+
478
+ // === Seeking ===
479
+
480
+ seekTo(time: number): void {
481
+ if (this.video) {
482
+ this.video.currentTime = Math.max(0, Math.min(time, this.getDuration()))
483
+ }
484
+ }
485
+
486
+ getCurrentTime(): number {
487
+ return this.video?.currentTime || 0
488
+ }
489
+
490
+ getDuration(): number {
491
+ return this.video?.duration || 0
492
+ }
493
+
494
+ /**
495
+ * Sync to live edge (for live streams)
496
+ */
497
+ syncToLive(): void {
498
+ if (this.hls?.liveSyncPosition != null) {
499
+ this.seekTo(this.hls.liveSyncPosition)
500
+ }
501
+ }
502
+
503
+ // === Volume ===
504
+
505
+ setVolume(volume: number): void {
506
+ if (this.video) {
507
+ this.video.volume = Math.max(0, Math.min(1, volume))
508
+ }
509
+ }
510
+
511
+ getVolume(): number {
512
+ return this.video?.volume || 1
513
+ }
514
+
515
+ setMuted(muted: boolean): void {
516
+ if (this.video) {
517
+ this.video.muted = muted
518
+ }
519
+ }
520
+
521
+ getMuted(): boolean {
522
+ return this.video?.muted || false
523
+ }
524
+
525
+ // === Playback Rate ===
526
+
527
+ setPlaybackRate(rate: number): void {
528
+ if (this.video) {
529
+ this.video.playbackRate = Math.max(0.25, Math.min(4, rate))
530
+ }
531
+ }
532
+
533
+ getPlaybackRate(): number {
534
+ return this.video?.playbackRate || 1
535
+ }
536
+
537
+ // === Quality ===
538
+
539
+ getQualities(): VideoQuality[] {
540
+ return this._qualities
541
+ }
542
+
543
+ setQuality(quality: VideoQuality | 'auto'): void {
544
+ if (!this.hls) return
545
+
546
+ if (quality === 'auto') {
547
+ this.hls.currentLevel = -1 // Auto
548
+ this.currentQualityIndex = -1
549
+ this.events.emit('statechange', { autoQuality: true })
550
+ } else {
551
+ const index = parseInt(quality.id, 10)
552
+ if (!isNaN(index) && index >= 0 && index < this._qualities.length) {
553
+ this.hls.currentLevel = index
554
+ this.currentQualityIndex = index
555
+ this.events.emit('statechange', { autoQuality: false })
556
+ }
557
+ }
558
+ }
559
+
560
+ // === Audio Tracks ===
561
+
562
+ getAudioTracks(): AudioTrack[] {
563
+ return this._audioTracks
564
+ }
565
+
566
+ setAudioTrack(trackId: string): void {
567
+ if (!this.hls) return
568
+
569
+ const index = parseInt(trackId, 10)
570
+ if (!isNaN(index) && index >= 0 && index < this.hls.audioTracks.length) {
571
+ this.hls.audioTrack = index
572
+ }
573
+ }
574
+
575
+ // === Text Tracks ===
576
+
577
+ getTextTracks(): TextTrack[] {
578
+ return this._textTracks
579
+ }
580
+
581
+ setTextTrackMode(trackId: string, mode: 'disabled' | 'hidden' | 'showing'): void {
582
+ if (!this.hls) return
583
+
584
+ if (mode === 'showing') {
585
+ const index = parseInt(trackId, 10)
586
+ if (!isNaN(index)) {
587
+ this.hls.subtitleTrack = index
588
+ }
589
+ } else {
590
+ this.hls.subtitleTrack = -1
591
+ }
592
+
593
+ this.updateTextTracks()
594
+ const current = this._textTracks.find((t) => t.mode === 'showing')
595
+ this.events.emit('texttrackchange', current || null)
596
+ }
597
+
598
+ }
599
+
600
+ /**
601
+ * HLS Provider Loader
602
+ */
603
+ export const hlsLoader = {
604
+ name: 'hls',
605
+ type: 'hls' as ProviderType,
606
+
607
+ canPlay(src: Src): boolean {
608
+ return isHLSSource(src)
609
+ },
610
+
611
+ mediaType(): 'video' {
612
+ return 'video'
613
+ },
614
+
615
+ preconnect(): string[] {
616
+ return ['https://cdn.jsdelivr.net']
617
+ },
618
+
619
+ async load(container: HTMLElement, options: PlayerOptions): Promise<HLSProvider> {
620
+ const provider = new HLSProvider()
621
+ await provider.setup(container, options)
622
+ return provider
623
+ },
624
+ }