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,473 @@
1
+ /**
2
+ * ts-video-player HTML5 Provider
3
+ *
4
+ * Native HTML5 video/audio element provider.
5
+ *
6
+ * @module providers/html5
7
+ */
8
+
9
+ import type {
10
+ ProviderType,
11
+ Src,
12
+ MediaSource,
13
+ PlayerOptions,
14
+ AudioTrack,
15
+ TextTrack,
16
+ } from '../types'
17
+ import { BaseProvider } from './base'
18
+ import { MediaEventsNormalizer, onFullscreenChange, onPiPChange } from '../core/events'
19
+ import { probeVolumeAvailability } from '../core/features'
20
+
21
+ /**
22
+ * Check if a source is an HTML5 compatible source
23
+ */
24
+ export function isHTML5Source(src: Src): boolean {
25
+ if (typeof src === 'string') {
26
+ const extension = getExtension(src)
27
+ // Check for supported extensions
28
+ if (
29
+ extension === 'mp4' ||
30
+ extension === 'webm' ||
31
+ extension === 'ogg' ||
32
+ extension === 'ogv' ||
33
+ extension === 'mov' ||
34
+ extension === 'mp3' ||
35
+ extension === 'wav' ||
36
+ extension === 'flac' ||
37
+ extension === 'm4a' ||
38
+ extension === 'aac' ||
39
+ extension === 'oga' ||
40
+ extension === 'opus'
41
+ ) {
42
+ return true
43
+ }
44
+ // Check for MIME types
45
+ return false
46
+ }
47
+
48
+ if (typeof src === 'object' && 'type' in src) {
49
+ const type = (src as MediaSource).type?.toLowerCase() || ''
50
+ return (
51
+ type.startsWith('video/') ||
52
+ type.startsWith('audio/') ||
53
+ type === 'application/ogg'
54
+ )
55
+ }
56
+
57
+ return false
58
+ }
59
+
60
+ function getExtension(src: string): string {
61
+ const path = src.split(/[?#]/, 1)[0]
62
+ return path.slice(path.lastIndexOf('.') + 1).toLowerCase()
63
+ }
64
+
65
+ /**
66
+ * Get MIME type for a source
67
+ */
68
+ function getMimeType(src: string): string {
69
+ const ext = getExtension(src)
70
+ const mimeTypes: Record<string, string> = {
71
+ mp4: 'video/mp4',
72
+ webm: 'video/webm',
73
+ ogg: 'video/ogg',
74
+ ogv: 'video/ogg',
75
+ mov: 'video/quicktime',
76
+ mp3: 'audio/mpeg',
77
+ wav: 'audio/wav',
78
+ flac: 'audio/flac',
79
+ m4a: 'audio/mp4',
80
+ aac: 'audio/aac',
81
+ oga: 'audio/ogg',
82
+ opus: 'audio/ogg; codecs=opus',
83
+ }
84
+ return mimeTypes[ext || ''] || 'video/mp4'
85
+ }
86
+
87
+ /**
88
+ * HTML5 Video/Audio Provider
89
+ */
90
+ export class HTML5Provider extends BaseProvider {
91
+ readonly name = 'html5'
92
+ readonly type: ProviderType = 'video'
93
+
94
+ private media: HTMLVideoElement | HTMLAudioElement | null = null
95
+ private eventsNormalizer: MediaEventsNormalizer | null = null
96
+ private cleanupFullscreen: (() => void) | null = null
97
+ private cleanupPiP: (() => void) | null = null
98
+ private isAudio = false
99
+
100
+ get mediaElement(): HTMLVideoElement | HTMLAudioElement | null {
101
+ return this.media
102
+ }
103
+
104
+ // === Lifecycle ===
105
+
106
+ protected async initialize(): Promise<void> {
107
+ if (!this.container) {
108
+ throw new Error('Container element is required')
109
+ }
110
+
111
+ // Determine if audio or video based on options
112
+ this.isAudio = this.options.mediaType === 'audio'
113
+
114
+ // Create media element
115
+ this.media = document.createElement(this.isAudio ? 'audio' : 'video')
116
+ this.media.className = 'ts-video-player__media'
117
+
118
+ // Set attributes
119
+ if (this.options.autoplay) this.media.autoplay = true
120
+ if (this.options.loop) this.media.loop = true
121
+ if (this.options.muted) this.media.muted = true
122
+ if (this.options.playsinline && !this.isAudio) {
123
+ (this.media as HTMLVideoElement).playsInline = true
124
+ }
125
+ if (this.options.preload) this.media.preload = this.options.preload
126
+ if (this.options.crossorigin) this.media.crossOrigin = this.options.crossorigin
127
+ if (this.options.controlsList?.length) this.media.setAttribute('controlslist', this.options.controlsList.join(' '))
128
+ if (!this.isAudio && this.options.disablePictureInPicture) {
129
+ (this.media as HTMLVideoElement).disablePictureInPicture = true
130
+ }
131
+ if (this.options.poster && !this.isAudio) {
132
+ (this.media as HTMLVideoElement).poster = this.options.poster
133
+ }
134
+
135
+ // Set initial volume
136
+ if (this.options.volume !== undefined) {
137
+ this.media.volume = this.options.volume
138
+ }
139
+
140
+ // Append to container
141
+ this.container.appendChild(this.media)
142
+ for (const track of this.options.tracks ?? []) this.addTextTrack(track)
143
+
144
+ // Setup event normalization
145
+ this.eventsNormalizer = new MediaEventsNormalizer(this.media, this.events)
146
+
147
+ // Setup fullscreen listener
148
+ this.cleanupFullscreen = onFullscreenChange((fullscreen) => {
149
+ this.events.emit('fullscreenchange', fullscreen)
150
+ })
151
+
152
+ // Setup PiP listener for video
153
+ if (!this.isAudio && this.media instanceof HTMLVideoElement) {
154
+ this.cleanupPiP = onPiPChange(this.media, (pip) => {
155
+ this.events.emit('pipchange', pip)
156
+ })
157
+ }
158
+
159
+ // Emit state for dimensions and feature availability
160
+ if (!this.isAudio) {
161
+ this.media.addEventListener('loadedmetadata', () => {
162
+ const video = this.media as HTMLVideoElement
163
+ const fullscreenAvailability = this.getFeatureAvailability('fullscreen')
164
+ const pipAvailability = this.getFeatureAvailability('pip')
165
+ const volumeAvailability = this.getFeatureAvailability('volume')
166
+ this.events.emit('statechange', {
167
+ videoWidth: video.videoWidth,
168
+ videoHeight: video.videoHeight,
169
+ aspectRatio: (video.videoHeight > 0 ? video.videoWidth / video.videoHeight : 0) || 16 / 9,
170
+ canFullscreen: fullscreenAvailability === 'available',
171
+ canPictureInPicture: pipAvailability === 'available',
172
+ fullscreenAvailability,
173
+ pipAvailability,
174
+ volumeAvailability,
175
+ })
176
+ this.events.emit('availabilitychange', 'fullscreen', fullscreenAvailability)
177
+ this.events.emit('availabilitychange', 'pip', pipAvailability)
178
+ this.events.emit('availabilitychange', 'volume', volumeAvailability)
179
+ })
180
+ }
181
+
182
+ // Async volume probe after loadeddata
183
+ this.media.addEventListener('loadeddata', () => {
184
+ if (!this.media) return
185
+ probeVolumeAvailability(this.media).then((availability) => {
186
+ this.events.emit('statechange', {
187
+ volumeAvailability: availability,
188
+ })
189
+ this.events.emit('availabilitychange', 'volume', availability)
190
+ }).catch(() => {
191
+ // Volume probe failed — keep current availability state
192
+ })
193
+ })
194
+
195
+ // iOS Safari PiP via webkitpresentationmodechanged
196
+ if (!this.isAudio && this.media instanceof HTMLVideoElement) {
197
+ this.media.addEventListener('webkitpresentationmodechanged', () => {
198
+ const video = this.media as any
199
+ if (video?.webkitPresentationMode) {
200
+ const isPiP = video.webkitPresentationMode === 'picture-in-picture'
201
+ this.events.emit('pipchange', isPiP)
202
+ }
203
+ })
204
+ }
205
+ }
206
+
207
+ protected cleanup(): void {
208
+ this.eventsNormalizer?.destroy()
209
+ this.cleanupFullscreen?.()
210
+ this.cleanupPiP?.()
211
+
212
+ if (this.media) {
213
+ this.media.pause()
214
+ this.media.removeAttribute('src')
215
+ this.media.load()
216
+ this.media.remove()
217
+ this.media = null
218
+ }
219
+ }
220
+
221
+ // === Loading ===
222
+
223
+ canPlay(src: Src): boolean {
224
+ return isHTML5Source(src)
225
+ }
226
+
227
+ async load(src: Src): Promise<void> {
228
+ if (!this.media) return
229
+
230
+ // Clear existing sources
231
+ this.media.innerHTML = ''
232
+ this.media.removeAttribute('src')
233
+
234
+ // Normalize source
235
+ const sources = Array.isArray(src) ? src : [src]
236
+
237
+ for (const source of sources) {
238
+ if (typeof source === 'string') {
239
+ // Simple URL
240
+ const sourceEl = document.createElement('source')
241
+ sourceEl.src = source
242
+ sourceEl.type = getMimeType(source)
243
+ this.media.appendChild(sourceEl)
244
+ } else if ('src' in source && typeof source.src === 'string') {
245
+ // MediaSource object
246
+ const mediaSource = source as MediaSource
247
+ const sourceEl = document.createElement('source')
248
+ sourceEl.src = mediaSource.src
249
+ sourceEl.type = mediaSource.type || getMimeType(mediaSource.src)
250
+ this.media.appendChild(sourceEl)
251
+ }
252
+ }
253
+
254
+ // Load the media
255
+ this.media.load()
256
+ }
257
+
258
+ // === Playback ===
259
+
260
+ async play(): Promise<void> {
261
+ if (!this.media) return
262
+ try {
263
+ await this.media.play()
264
+ } catch (error) {
265
+ // Handle autoplay blocked
266
+ if ((error as Error).name === 'NotAllowedError') {
267
+ this.emitError(1, 'Playback was blocked. User interaction required.', error)
268
+ } else {
269
+ throw error
270
+ }
271
+ }
272
+ }
273
+
274
+ pause(): void {
275
+ this.media?.pause()
276
+ }
277
+
278
+ // === Seeking ===
279
+
280
+ seekTo(time: number): void {
281
+ if (this.media) {
282
+ this.media.currentTime = Math.max(0, Math.min(time, this.getDuration()))
283
+ }
284
+ }
285
+
286
+ getCurrentTime(): number {
287
+ return this.media?.currentTime || 0
288
+ }
289
+
290
+ getDuration(): number {
291
+ return this.media?.duration || 0
292
+ }
293
+
294
+ // === Volume ===
295
+
296
+ setVolume(volume: number): void {
297
+ if (this.media) {
298
+ this.media.volume = Math.max(0, Math.min(1, volume))
299
+ }
300
+ }
301
+
302
+ getVolume(): number {
303
+ return this.media?.volume || 1
304
+ }
305
+
306
+ setMuted(muted: boolean): void {
307
+ if (this.media) {
308
+ this.media.muted = muted
309
+ }
310
+ }
311
+
312
+ getMuted(): boolean {
313
+ return this.media?.muted || false
314
+ }
315
+
316
+ // === Playback Rate ===
317
+
318
+ setPlaybackRate(rate: number): void {
319
+ if (this.media) {
320
+ this.media.playbackRate = Math.max(0.25, Math.min(4, rate))
321
+ }
322
+ }
323
+
324
+ getPlaybackRate(): number {
325
+ return this.media?.playbackRate || 1
326
+ }
327
+
328
+ // === Text Tracks ===
329
+
330
+ getTextTracks(): TextTrack[] {
331
+ if (!this.media) return []
332
+
333
+ const tracks: TextTrack[] = []
334
+ const textTracks = this.media.textTracks
335
+
336
+ for (let i = 0; i < textTracks.length; i++) {
337
+ const track = textTracks[i]
338
+ tracks.push({
339
+ id: String(i),
340
+ kind: track.kind as TextTrack['kind'],
341
+ label: track.label || `Track ${i + 1}`,
342
+ language: track.language || '',
343
+ mode: track.mode as TextTrack['mode'],
344
+ cues: this.getTrackCues(track),
345
+ })
346
+ }
347
+
348
+ return tracks
349
+ }
350
+
351
+ private getTrackCues(track: globalThis.TextTrack): TextTrack['cues'] {
352
+ if (!track.cues) return []
353
+
354
+ const cues: TextTrack['cues'] = []
355
+ for (let i = 0; i < track.cues.length; i++) {
356
+ const cue = track.cues[i] as VTTCue
357
+ cues.push({
358
+ id: cue.id || String(i),
359
+ startTime: cue.startTime,
360
+ endTime: cue.endTime,
361
+ text: cue.text,
362
+ })
363
+ }
364
+ return cues
365
+ }
366
+
367
+ setTextTrackMode(trackId: string, mode: 'disabled' | 'hidden' | 'showing'): void {
368
+ if (!this.media) return
369
+
370
+ const index = parseInt(trackId, 10)
371
+ const track = this.media.textTracks[index]
372
+ if (track) {
373
+ track.mode = mode
374
+ this.events.emit('texttrackchange', mode === 'showing' ? this.getTextTracks()[index] : null)
375
+ }
376
+ }
377
+
378
+ // === Audio Tracks ===
379
+
380
+ getAudioTracks(): AudioTrack[] {
381
+ if (!this.media || this.isAudio) return []
382
+
383
+ const video = this.media as HTMLVideoElement
384
+ if (!('audioTracks' in video)) return []
385
+
386
+ const audioTracks = (video as any).audioTracks
387
+ const tracks: AudioTrack[] = []
388
+
389
+ for (let i = 0; i < audioTracks.length; i++) {
390
+ const track = audioTracks[i]
391
+ tracks.push({
392
+ id: track.id || String(i),
393
+ label: track.label || `Track ${i + 1}`,
394
+ language: track.language || '',
395
+ kind: track.kind || '',
396
+ selected: track.enabled,
397
+ })
398
+ }
399
+
400
+ return tracks
401
+ }
402
+
403
+ setAudioTrack(trackId: string): void {
404
+ if (!this.media || this.isAudio) return
405
+
406
+ const video = this.media as HTMLVideoElement
407
+ if (!('audioTracks' in video)) return
408
+
409
+ const audioTracks = (video as any).audioTracks
410
+ for (let i = 0; i < audioTracks.length; i++) {
411
+ const isTarget = audioTracks[i].id === trackId || String(i) === trackId
412
+ audioTracks[i].enabled = isTarget
413
+ }
414
+
415
+ this.events.emit('audiotrackchange', this.getAudioTracks().find((t) => t.selected) || null)
416
+ }
417
+
418
+ // === Add Text Track ===
419
+
420
+ addTextTrack(options: {
421
+ src?: string
422
+ kind?: TextTrack['kind']
423
+ label?: string
424
+ language?: string
425
+ default?: boolean
426
+ }): void {
427
+ if (!this.media) return
428
+
429
+ const track = document.createElement('track')
430
+ if (options.src) track.src = options.src
431
+ track.kind = options.kind || 'subtitles'
432
+ track.label = options.label || ''
433
+ track.srclang = options.language || ''
434
+ if (options.default) track.default = true
435
+
436
+ this.media.appendChild(track)
437
+ }
438
+ }
439
+
440
+ /**
441
+ * HTML5 Provider Loader
442
+ */
443
+ export const html5Loader = {
444
+ name: 'html5',
445
+ type: 'video' as ProviderType,
446
+
447
+ canPlay(src: Src): boolean {
448
+ return isHTML5Source(src)
449
+ },
450
+
451
+ mediaType(src: Src): 'video' | 'audio' | 'unknown' {
452
+ if (typeof src === 'string') {
453
+ const extension = getExtension(src)
454
+ if (['mp3', 'wav', 'flac', 'm4a', 'aac', 'oga', 'opus'].includes(extension)) {
455
+ return 'audio'
456
+ }
457
+ return 'video'
458
+ }
459
+
460
+ if (typeof src === 'object' && 'type' in src) {
461
+ const type = (src as MediaSource).type?.toLowerCase() || ''
462
+ if (type.startsWith('audio/')) return 'audio'
463
+ }
464
+
465
+ return 'video'
466
+ },
467
+
468
+ async load(container: HTMLElement, options: PlayerOptions): Promise<HTML5Provider> {
469
+ const provider = new HTML5Provider()
470
+ await provider.setup(container, options)
471
+ return provider
472
+ },
473
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * ts-video-player Providers
3
+ *
4
+ * Export all media providers.
5
+ *
6
+ * @module providers
7
+ */
8
+
9
+ // Base
10
+ export { BaseProvider } from './base'
11
+
12
+ // HTML5 Video/Audio
13
+ export { HTML5Provider, html5Loader, isHTML5Source } from './html5'
14
+
15
+ // YouTube
16
+ export { YouTubeProvider, youtubeLoader, isYouTubeSource, extractYouTubeId } from './youtube'
17
+
18
+ // Vimeo
19
+ export { VimeoProvider, vimeoLoader, isVimeoSource, extractVimeoId, extractVimeoHash } from './vimeo'
20
+
21
+ // HLS
22
+ export { HLSProvider, hlsLoader, isHLSSource, isNativeHLSSupported } from './hls'
23
+
24
+ // DASH
25
+ export { DASHProvider, dashLoader, isDASHSource } from './dash'
26
+
27
+ export type { DASHSource, DASHProviderConfig } from './dash'
28
+ export type { DRMConfig } from '../types'
29
+
30
+ // Re-export types
31
+ export type { Provider, ProviderLoader, ProviderType } from '../types'
32
+
33
+ // =============================================================================
34
+ // Provider Registry
35
+ // =============================================================================
36
+
37
+ import type { ProviderLoader, Src } from '../types'
38
+ import { html5Loader } from './html5'
39
+ import { youtubeLoader } from './youtube'
40
+ import { vimeoLoader } from './vimeo'
41
+ import { hlsLoader } from './hls'
42
+ import { dashLoader } from './dash'
43
+
44
+ /**
45
+ * Default provider loaders in priority order
46
+ */
47
+ export const defaultLoaders: ProviderLoader[] = [
48
+ hlsLoader, // Check HLS first
49
+ dashLoader, // Then DASH
50
+ youtubeLoader, // Then YouTube
51
+ vimeoLoader, // Then Vimeo
52
+ html5Loader, // HTML5 as fallback
53
+ ]
54
+
55
+ /**
56
+ * Find the appropriate provider loader for a source
57
+ */
58
+ export function findLoader(src: Src, loaders: ProviderLoader[] = defaultLoaders): ProviderLoader | null {
59
+ for (const loader of loaders) {
60
+ if (loader.canPlay(src)) {
61
+ return loader
62
+ }
63
+ }
64
+ return null
65
+ }
66
+
67
+ export interface SourceCandidate {
68
+ src: Src
69
+ index: number
70
+ loader: ProviderLoader
71
+ }
72
+
73
+ /** Resolve every playable source without discarding ordered fallbacks. */
74
+ export function findSourceCandidates(sources: readonly Src[], loaders: ProviderLoader[] = defaultLoaders): SourceCandidate[] {
75
+ const candidates: SourceCandidate[] = []
76
+ sources.forEach((src, index) => {
77
+ const loader = findLoader(src, loaders)
78
+ if (loader) candidates.push({ src, index, loader })
79
+ })
80
+ return candidates
81
+ }
82
+
83
+ /**
84
+ * Detect media type from source
85
+ */
86
+ export function detectMediaType(src: Src, loaders: ProviderLoader[] = defaultLoaders): 'video' | 'audio' | 'unknown' {
87
+ const loader = findLoader(src, loaders)
88
+ return loader?.mediaType(src) || 'unknown'
89
+ }
90
+
91
+ /**
92
+ * Get preconnect hints for a source
93
+ */
94
+ export function getPreconnectHints(src: Src, loaders: ProviderLoader[] = defaultLoaders): string[] {
95
+ const loader = findLoader(src, loaders)
96
+ return loader?.preconnect?.() || []
97
+ }