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,4243 @@
1
+ import {
2
+ EventEmitter,
3
+ MediaEventsNormalizer,
4
+ StateStore,
5
+ createActivityDetector,
6
+ createKeyboardHandler,
7
+ createStorageSync,
8
+ detectFullscreenAvailability,
9
+ detectPipAvailability,
10
+ detectVolumeAvailability,
11
+ enterFullscreen,
12
+ enterPiP,
13
+ exitFullscreen,
14
+ exitPiP,
15
+ onFullscreenChange,
16
+ onPiPChange,
17
+ probeVolumeAvailability,
18
+ selectBufferedAmount
19
+ } from "./chunk-36x4yhjn.js";
20
+
21
+ // src/core/media-session.ts
22
+ function isMediaSessionSupported() {
23
+ return typeof navigator !== "undefined" && "mediaSession" in navigator;
24
+ }
25
+ function createMediaSession(store, actions, options) {
26
+ if (!isMediaSessionSupported())
27
+ return () => {};
28
+ const session = navigator.mediaSession;
29
+ if (options) {
30
+ updateMetadata(options);
31
+ }
32
+ const handlers = [
33
+ ["play", () => actions.play()],
34
+ ["pause", () => actions.pause()],
35
+ ["stop", () => actions.stop()],
36
+ ["seekbackward", (details) => {
37
+ const offset = details.seekOffset || -10;
38
+ actions.seekBy(offset);
39
+ }],
40
+ ["seekforward", (details) => {
41
+ const offset = details.seekOffset || 10;
42
+ actions.seekBy(offset);
43
+ }],
44
+ ["seekto", (details) => {
45
+ const seekTime = details.seekTime;
46
+ if (seekTime !== undefined)
47
+ actions.seekTo(seekTime);
48
+ }]
49
+ ];
50
+ for (const [action, handler] of handlers) {
51
+ try {
52
+ session.setActionHandler(action, handler);
53
+ } catch {}
54
+ }
55
+ const unsubPaused = store.subscribe("paused", (state) => {
56
+ session.playbackState = state.paused ? "paused" : "playing";
57
+ });
58
+ let positionTimer = null;
59
+ const updatePosition = () => {
60
+ const state = store.getState();
61
+ if (!isFinite(state.duration) || state.duration <= 0)
62
+ return;
63
+ try {
64
+ session.setPositionState({
65
+ duration: state.duration,
66
+ playbackRate: state.playbackRate,
67
+ position: Math.min(state.currentTime, state.duration)
68
+ });
69
+ } catch {}
70
+ };
71
+ const unsubPlaying = store.subscribe("playing", (state) => {
72
+ if (state.playing) {
73
+ updatePosition();
74
+ if (!positionTimer) {
75
+ positionTimer = setInterval(updatePosition, 1000);
76
+ }
77
+ } else {
78
+ if (positionTimer) {
79
+ clearInterval(positionTimer);
80
+ positionTimer = null;
81
+ }
82
+ updatePosition();
83
+ }
84
+ });
85
+ return () => {
86
+ if (positionTimer)
87
+ clearInterval(positionTimer);
88
+ unsubPaused();
89
+ unsubPlaying();
90
+ for (const [action] of handlers) {
91
+ try {
92
+ session.setActionHandler(action, null);
93
+ } catch {}
94
+ }
95
+ };
96
+ }
97
+ function updateMetadata(options) {
98
+ if (!isMediaSessionSupported())
99
+ return;
100
+ navigator.mediaSession.metadata = new MediaMetadata({
101
+ title: options.title || "",
102
+ artist: options.artist || "",
103
+ album: options.album || "",
104
+ artwork: options.artwork || []
105
+ });
106
+ }
107
+
108
+ // src/core/orientation.ts
109
+ function isOrientationLockSupported() {
110
+ return typeof screen !== "undefined" && "orientation" in screen && typeof screen.orientation.lock === "function";
111
+ }
112
+ function lockOrientation(type) {
113
+ if (!isOrientationLockSupported())
114
+ return;
115
+ const orientationMap = {
116
+ landscape: "landscape",
117
+ portrait: "portrait",
118
+ any: "any"
119
+ };
120
+ screen.orientation.lock(orientationMap[type]).catch(() => {});
121
+ }
122
+ function unlockOrientation() {
123
+ if (!isOrientationLockSupported())
124
+ return;
125
+ try {
126
+ screen.orientation.unlock();
127
+ } catch {}
128
+ }
129
+
130
+ // src/providers/base.ts
131
+ class BaseProvider {
132
+ container = null;
133
+ options = {};
134
+ events = new EventEmitter;
135
+ _ready = false;
136
+ get mediaElement() {
137
+ return null;
138
+ }
139
+ get ready() {
140
+ return this._ready;
141
+ }
142
+ async setup(container, options) {
143
+ this.container = container;
144
+ this.options = options;
145
+ await this.initialize();
146
+ this._ready = true;
147
+ this.events.emit("ready");
148
+ }
149
+ destroy() {
150
+ this._ready = false;
151
+ this.cleanup();
152
+ this.events.removeAllListeners();
153
+ }
154
+ stop() {
155
+ this.pause();
156
+ this.seekTo(0);
157
+ }
158
+ getQualities() {
159
+ return [];
160
+ }
161
+ setQuality(_quality) {}
162
+ getTextTracks() {
163
+ return [];
164
+ }
165
+ setTextTrackMode(_trackId, _mode) {}
166
+ getAudioTracks() {
167
+ return [];
168
+ }
169
+ setAudioTrack(_trackId) {}
170
+ getFeatureAvailability(feature) {
171
+ const el = this.mediaElement;
172
+ const media = el instanceof HTMLMediaElement ? el : null;
173
+ const video = el instanceof HTMLVideoElement ? el : null;
174
+ switch (feature) {
175
+ case "volume":
176
+ return el && !(el instanceof HTMLMediaElement) ? "available" : detectVolumeAvailability(media);
177
+ case "fullscreen":
178
+ return detectFullscreenAvailability(this.container, video);
179
+ case "pip":
180
+ return el && !(el instanceof HTMLMediaElement) ? "unsupported" : detectPipAvailability(video);
181
+ default:
182
+ return "unsupported";
183
+ }
184
+ }
185
+ async enterFullscreen() {
186
+ await enterFullscreen(this.container, this.mediaElement);
187
+ }
188
+ async exitFullscreen() {
189
+ await exitFullscreen(this.mediaElement);
190
+ }
191
+ getFullscreenElement() {
192
+ return this.container;
193
+ }
194
+ async enterPiP() {
195
+ const media = this.mediaElement;
196
+ if (!media)
197
+ return;
198
+ await enterPiP(media);
199
+ }
200
+ async exitPiP() {
201
+ await exitPiP(this.mediaElement);
202
+ }
203
+ on(event, handler) {
204
+ this.events.on(event, handler);
205
+ }
206
+ off(event, handler) {
207
+ this.events.off(event, handler);
208
+ }
209
+ emitError(code, message, details) {
210
+ const error = { code, message, details };
211
+ this.events.emit("error", error);
212
+ }
213
+ }
214
+ // src/providers/html5.ts
215
+ function isHTML5Source(src) {
216
+ if (typeof src === "string") {
217
+ const extension = getExtension(src);
218
+ if (extension === "mp4" || extension === "webm" || extension === "ogg" || extension === "ogv" || extension === "mov" || extension === "mp3" || extension === "wav" || extension === "flac" || extension === "m4a" || extension === "aac" || extension === "oga" || extension === "opus") {
219
+ return true;
220
+ }
221
+ return false;
222
+ }
223
+ if (typeof src === "object" && "type" in src) {
224
+ const type = src.type?.toLowerCase() || "";
225
+ return type.startsWith("video/") || type.startsWith("audio/") || type === "application/ogg";
226
+ }
227
+ return false;
228
+ }
229
+ function getExtension(src) {
230
+ const path = src.split(/[?#]/, 1)[0];
231
+ return path.slice(path.lastIndexOf(".") + 1).toLowerCase();
232
+ }
233
+ function getMimeType(src) {
234
+ const ext = getExtension(src);
235
+ const mimeTypes = {
236
+ mp4: "video/mp4",
237
+ webm: "video/webm",
238
+ ogg: "video/ogg",
239
+ ogv: "video/ogg",
240
+ mov: "video/quicktime",
241
+ mp3: "audio/mpeg",
242
+ wav: "audio/wav",
243
+ flac: "audio/flac",
244
+ m4a: "audio/mp4",
245
+ aac: "audio/aac",
246
+ oga: "audio/ogg",
247
+ opus: "audio/ogg; codecs=opus"
248
+ };
249
+ return mimeTypes[ext || ""] || "video/mp4";
250
+ }
251
+
252
+ class HTML5Provider extends BaseProvider {
253
+ name = "html5";
254
+ type = "video";
255
+ media = null;
256
+ eventsNormalizer = null;
257
+ cleanupFullscreen = null;
258
+ cleanupPiP = null;
259
+ isAudio = false;
260
+ get mediaElement() {
261
+ return this.media;
262
+ }
263
+ async initialize() {
264
+ if (!this.container) {
265
+ throw new Error("Container element is required");
266
+ }
267
+ this.isAudio = this.options.mediaType === "audio";
268
+ this.media = document.createElement(this.isAudio ? "audio" : "video");
269
+ this.media.className = "ts-video-player__media";
270
+ if (this.options.autoplay)
271
+ this.media.autoplay = true;
272
+ if (this.options.loop)
273
+ this.media.loop = true;
274
+ if (this.options.muted)
275
+ this.media.muted = true;
276
+ if (this.options.playsinline && !this.isAudio) {
277
+ this.media.playsInline = true;
278
+ }
279
+ if (this.options.preload)
280
+ this.media.preload = this.options.preload;
281
+ if (this.options.crossorigin)
282
+ this.media.crossOrigin = this.options.crossorigin;
283
+ if (this.options.controlsList?.length)
284
+ this.media.setAttribute("controlslist", this.options.controlsList.join(" "));
285
+ if (!this.isAudio && this.options.disablePictureInPicture) {
286
+ this.media.disablePictureInPicture = true;
287
+ }
288
+ if (this.options.poster && !this.isAudio) {
289
+ this.media.poster = this.options.poster;
290
+ }
291
+ if (this.options.volume !== undefined) {
292
+ this.media.volume = this.options.volume;
293
+ }
294
+ this.container.appendChild(this.media);
295
+ for (const track of this.options.tracks ?? [])
296
+ this.addTextTrack(track);
297
+ this.eventsNormalizer = new MediaEventsNormalizer(this.media, this.events);
298
+ this.cleanupFullscreen = onFullscreenChange((fullscreen) => {
299
+ this.events.emit("fullscreenchange", fullscreen);
300
+ });
301
+ if (!this.isAudio && this.media instanceof HTMLVideoElement) {
302
+ this.cleanupPiP = onPiPChange(this.media, (pip) => {
303
+ this.events.emit("pipchange", pip);
304
+ });
305
+ }
306
+ if (!this.isAudio) {
307
+ this.media.addEventListener("loadedmetadata", () => {
308
+ const video = this.media;
309
+ const fullscreenAvailability = this.getFeatureAvailability("fullscreen");
310
+ const pipAvailability = this.getFeatureAvailability("pip");
311
+ const volumeAvailability = this.getFeatureAvailability("volume");
312
+ this.events.emit("statechange", {
313
+ videoWidth: video.videoWidth,
314
+ videoHeight: video.videoHeight,
315
+ aspectRatio: (video.videoHeight > 0 ? video.videoWidth / video.videoHeight : 0) || 16 / 9,
316
+ canFullscreen: fullscreenAvailability === "available",
317
+ canPictureInPicture: pipAvailability === "available",
318
+ fullscreenAvailability,
319
+ pipAvailability,
320
+ volumeAvailability
321
+ });
322
+ this.events.emit("availabilitychange", "fullscreen", fullscreenAvailability);
323
+ this.events.emit("availabilitychange", "pip", pipAvailability);
324
+ this.events.emit("availabilitychange", "volume", volumeAvailability);
325
+ });
326
+ }
327
+ this.media.addEventListener("loadeddata", () => {
328
+ if (!this.media)
329
+ return;
330
+ probeVolumeAvailability(this.media).then((availability) => {
331
+ this.events.emit("statechange", {
332
+ volumeAvailability: availability
333
+ });
334
+ this.events.emit("availabilitychange", "volume", availability);
335
+ }).catch(() => {});
336
+ });
337
+ if (!this.isAudio && this.media instanceof HTMLVideoElement) {
338
+ this.media.addEventListener("webkitpresentationmodechanged", () => {
339
+ const video = this.media;
340
+ if (video?.webkitPresentationMode) {
341
+ const isPiP = video.webkitPresentationMode === "picture-in-picture";
342
+ this.events.emit("pipchange", isPiP);
343
+ }
344
+ });
345
+ }
346
+ }
347
+ cleanup() {
348
+ this.eventsNormalizer?.destroy();
349
+ this.cleanupFullscreen?.();
350
+ this.cleanupPiP?.();
351
+ if (this.media) {
352
+ this.media.pause();
353
+ this.media.removeAttribute("src");
354
+ this.media.load();
355
+ this.media.remove();
356
+ this.media = null;
357
+ }
358
+ }
359
+ canPlay(src) {
360
+ return isHTML5Source(src);
361
+ }
362
+ async load(src) {
363
+ if (!this.media)
364
+ return;
365
+ this.media.innerHTML = "";
366
+ this.media.removeAttribute("src");
367
+ const sources = Array.isArray(src) ? src : [src];
368
+ for (const source of sources) {
369
+ if (typeof source === "string") {
370
+ const sourceEl = document.createElement("source");
371
+ sourceEl.src = source;
372
+ sourceEl.type = getMimeType(source);
373
+ this.media.appendChild(sourceEl);
374
+ } else if ("src" in source && typeof source.src === "string") {
375
+ const mediaSource = source;
376
+ const sourceEl = document.createElement("source");
377
+ sourceEl.src = mediaSource.src;
378
+ sourceEl.type = mediaSource.type || getMimeType(mediaSource.src);
379
+ this.media.appendChild(sourceEl);
380
+ }
381
+ }
382
+ this.media.load();
383
+ }
384
+ async play() {
385
+ if (!this.media)
386
+ return;
387
+ try {
388
+ await this.media.play();
389
+ } catch (error) {
390
+ if (error.name === "NotAllowedError") {
391
+ this.emitError(1, "Playback was blocked. User interaction required.", error);
392
+ } else {
393
+ throw error;
394
+ }
395
+ }
396
+ }
397
+ pause() {
398
+ this.media?.pause();
399
+ }
400
+ seekTo(time) {
401
+ if (this.media) {
402
+ this.media.currentTime = Math.max(0, Math.min(time, this.getDuration()));
403
+ }
404
+ }
405
+ getCurrentTime() {
406
+ return this.media?.currentTime || 0;
407
+ }
408
+ getDuration() {
409
+ return this.media?.duration || 0;
410
+ }
411
+ setVolume(volume) {
412
+ if (this.media) {
413
+ this.media.volume = Math.max(0, Math.min(1, volume));
414
+ }
415
+ }
416
+ getVolume() {
417
+ return this.media?.volume || 1;
418
+ }
419
+ setMuted(muted) {
420
+ if (this.media) {
421
+ this.media.muted = muted;
422
+ }
423
+ }
424
+ getMuted() {
425
+ return this.media?.muted || false;
426
+ }
427
+ setPlaybackRate(rate) {
428
+ if (this.media) {
429
+ this.media.playbackRate = Math.max(0.25, Math.min(4, rate));
430
+ }
431
+ }
432
+ getPlaybackRate() {
433
+ return this.media?.playbackRate || 1;
434
+ }
435
+ getTextTracks() {
436
+ if (!this.media)
437
+ return [];
438
+ const tracks = [];
439
+ const textTracks = this.media.textTracks;
440
+ for (let i = 0;i < textTracks.length; i++) {
441
+ const track = textTracks[i];
442
+ tracks.push({
443
+ id: String(i),
444
+ kind: track.kind,
445
+ label: track.label || `Track ${i + 1}`,
446
+ language: track.language || "",
447
+ mode: track.mode,
448
+ cues: this.getTrackCues(track)
449
+ });
450
+ }
451
+ return tracks;
452
+ }
453
+ getTrackCues(track) {
454
+ if (!track.cues)
455
+ return [];
456
+ const cues = [];
457
+ for (let i = 0;i < track.cues.length; i++) {
458
+ const cue = track.cues[i];
459
+ cues.push({
460
+ id: cue.id || String(i),
461
+ startTime: cue.startTime,
462
+ endTime: cue.endTime,
463
+ text: cue.text
464
+ });
465
+ }
466
+ return cues;
467
+ }
468
+ setTextTrackMode(trackId, mode) {
469
+ if (!this.media)
470
+ return;
471
+ const index = parseInt(trackId, 10);
472
+ const track = this.media.textTracks[index];
473
+ if (track) {
474
+ track.mode = mode;
475
+ this.events.emit("texttrackchange", mode === "showing" ? this.getTextTracks()[index] : null);
476
+ }
477
+ }
478
+ getAudioTracks() {
479
+ if (!this.media || this.isAudio)
480
+ return [];
481
+ const video = this.media;
482
+ if (!("audioTracks" in video))
483
+ return [];
484
+ const audioTracks = video.audioTracks;
485
+ const tracks = [];
486
+ for (let i = 0;i < audioTracks.length; i++) {
487
+ const track = audioTracks[i];
488
+ tracks.push({
489
+ id: track.id || String(i),
490
+ label: track.label || `Track ${i + 1}`,
491
+ language: track.language || "",
492
+ kind: track.kind || "",
493
+ selected: track.enabled
494
+ });
495
+ }
496
+ return tracks;
497
+ }
498
+ setAudioTrack(trackId) {
499
+ if (!this.media || this.isAudio)
500
+ return;
501
+ const video = this.media;
502
+ if (!("audioTracks" in video))
503
+ return;
504
+ const audioTracks = video.audioTracks;
505
+ for (let i = 0;i < audioTracks.length; i++) {
506
+ const isTarget = audioTracks[i].id === trackId || String(i) === trackId;
507
+ audioTracks[i].enabled = isTarget;
508
+ }
509
+ this.events.emit("audiotrackchange", this.getAudioTracks().find((t) => t.selected) || null);
510
+ }
511
+ addTextTrack(options) {
512
+ if (!this.media)
513
+ return;
514
+ const track = document.createElement("track");
515
+ if (options.src)
516
+ track.src = options.src;
517
+ track.kind = options.kind || "subtitles";
518
+ track.label = options.label || "";
519
+ track.srclang = options.language || "";
520
+ if (options.default)
521
+ track.default = true;
522
+ this.media.appendChild(track);
523
+ }
524
+ }
525
+ var html5Loader = {
526
+ name: "html5",
527
+ type: "video",
528
+ canPlay(src) {
529
+ return isHTML5Source(src);
530
+ },
531
+ mediaType(src) {
532
+ if (typeof src === "string") {
533
+ const extension = getExtension(src);
534
+ if (["mp3", "wav", "flac", "m4a", "aac", "oga", "opus"].includes(extension)) {
535
+ return "audio";
536
+ }
537
+ return "video";
538
+ }
539
+ if (typeof src === "object" && "type" in src) {
540
+ const type = src.type?.toLowerCase() || "";
541
+ if (type.startsWith("audio/"))
542
+ return "audio";
543
+ }
544
+ return "video";
545
+ },
546
+ async load(container, options) {
547
+ const provider = new HTML5Provider;
548
+ await provider.setup(container, options);
549
+ return provider;
550
+ }
551
+ };
552
+ // src/providers/youtube.ts
553
+ var YOUTUBE_PATTERNS = [
554
+ /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/,
555
+ /youtube\.com\/v\/([a-zA-Z0-9_-]{11})/,
556
+ /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/
557
+ ];
558
+ function extractYouTubeId(url) {
559
+ for (const pattern of YOUTUBE_PATTERNS) {
560
+ const match = url.match(pattern);
561
+ if (match)
562
+ return match[1];
563
+ }
564
+ return null;
565
+ }
566
+ function isYouTubeSource(src) {
567
+ if (typeof src === "string") {
568
+ return extractYouTubeId(src) !== null;
569
+ }
570
+ if (typeof src === "object" && "type" in src) {
571
+ return src.type === "youtube";
572
+ }
573
+ return false;
574
+ }
575
+ function parseYouTubeSource(src) {
576
+ if (typeof src === "string") {
577
+ const videoId = extractYouTubeId(src);
578
+ if (!videoId)
579
+ return null;
580
+ const url = new URL(src, "https://youtube.com");
581
+ const start = url.searchParams.get("t") || url.searchParams.get("start");
582
+ const end = url.searchParams.get("end");
583
+ return {
584
+ videoId,
585
+ startTime: start ? parseTime(start) : undefined,
586
+ endTime: end ? parseTime(end) : undefined
587
+ };
588
+ }
589
+ if (typeof src === "object" && "type" in src && src.type === "youtube") {
590
+ const ytSrc = src;
591
+ return {
592
+ videoId: ytSrc.videoId || extractYouTubeId(ytSrc.src) || "",
593
+ startTime: ytSrc.startTime,
594
+ endTime: ytSrc.endTime
595
+ };
596
+ }
597
+ return null;
598
+ }
599
+ function parseTime(time) {
600
+ if (/^\d+$/.test(time))
601
+ return parseInt(time, 10);
602
+ const match1 = time.match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/);
603
+ if (match1) {
604
+ const h = parseInt(match1[1] || "0", 10);
605
+ const m = parseInt(match1[2] || "0", 10);
606
+ const s = parseInt(match1[3] || "0", 10);
607
+ return h * 3600 + m * 60 + s;
608
+ }
609
+ const parts = time.split(":").map((p) => parseInt(p, 10));
610
+ if (parts.length === 2)
611
+ return parts[0] * 60 + parts[1];
612
+ if (parts.length === 3)
613
+ return parts[0] * 3600 + parts[1] * 60 + parts[2];
614
+ return 0;
615
+ }
616
+
617
+ class YouTubeProvider extends BaseProvider {
618
+ name = "youtube";
619
+ type = "youtube";
620
+ player = null;
621
+ iframe = null;
622
+ pendingVideoId = null;
623
+ startTime;
624
+ endTime;
625
+ timeUpdateInterval = null;
626
+ lastState = -1;
627
+ get mediaElement() {
628
+ return this.iframe;
629
+ }
630
+ async initialize() {
631
+ if (!this.container) {
632
+ throw new Error("Container element is required");
633
+ }
634
+ await this.loadYouTubeAPI();
635
+ const playerDiv = document.createElement("div");
636
+ playerDiv.id = `ts-video-player-yt-${Date.now()}`;
637
+ playerDiv.className = "ts-video-player__youtube";
638
+ this.container.appendChild(playerDiv);
639
+ const playerVars = {
640
+ autoplay: this.options.autoplay ? 1 : 0,
641
+ controls: 0,
642
+ disablekb: 1,
643
+ enablejsapi: 1,
644
+ iv_load_policy: 3,
645
+ modestbranding: 1,
646
+ playsinline: this.options.playsinline ? 1 : 0,
647
+ rel: 0,
648
+ origin: window.location.origin
649
+ };
650
+ if (this.options.youtubeParams) {
651
+ Object.assign(playerVars, this.options.youtubeParams);
652
+ }
653
+ playerVars.host = "https://www.youtube-nocookie.com";
654
+ this.player = new window.YT.Player(playerDiv.id, {
655
+ width: "100%",
656
+ height: "100%",
657
+ playerVars,
658
+ events: {
659
+ onReady: () => this.onReady(),
660
+ onStateChange: (e) => this.onStateChange(e),
661
+ onError: (e) => this.onError(e),
662
+ onPlaybackQualityChange: () => this.onQualityChange(),
663
+ onPlaybackRateChange: () => this.onRateChange()
664
+ }
665
+ });
666
+ this.iframe = this.player.getIframe();
667
+ this.iframe.className = "ts-video-player__media";
668
+ }
669
+ async loadYouTubeAPI() {
670
+ if (window.YT?.Player)
671
+ return;
672
+ return new Promise((resolve) => {
673
+ if (document.querySelector('script[src*="youtube.com/iframe_api"]')) {
674
+ const check = () => {
675
+ if (window.YT?.Player) {
676
+ resolve();
677
+ } else {
678
+ setTimeout(check, 100);
679
+ }
680
+ };
681
+ check();
682
+ return;
683
+ }
684
+ const script = document.createElement("script");
685
+ script.src = "https://www.youtube.com/iframe_api";
686
+ script.async = true;
687
+ window.onYouTubeIframeAPIReady = () => {
688
+ resolve();
689
+ };
690
+ document.head.appendChild(script);
691
+ });
692
+ }
693
+ cleanup() {
694
+ if (this.timeUpdateInterval) {
695
+ clearInterval(this.timeUpdateInterval);
696
+ this.timeUpdateInterval = null;
697
+ }
698
+ this.player?.destroy();
699
+ this.player = null;
700
+ this.iframe = null;
701
+ }
702
+ onReady() {
703
+ this.timeUpdateInterval = setInterval(() => {
704
+ if (this.player && this.lastState === 1) {
705
+ this.events.emit("timeupdate", this.getCurrentTime());
706
+ }
707
+ }, 250);
708
+ this.events.emit("loadedmetadata");
709
+ this.events.emit("canplay");
710
+ if (this.pendingVideoId) {
711
+ this.loadVideo(this.pendingVideoId, this.startTime, this.endTime);
712
+ this.pendingVideoId = null;
713
+ }
714
+ }
715
+ onStateChange(event) {
716
+ const state = event.data;
717
+ this.lastState = state;
718
+ const PlayerState = window.YT.PlayerState;
719
+ switch (state) {
720
+ case PlayerState.UNSTARTED:
721
+ this.events.emit("loadstart");
722
+ break;
723
+ case PlayerState.PLAYING:
724
+ this.events.emit("play");
725
+ this.events.emit("playing");
726
+ this.events.emit("statechange", { paused: false, playing: true, waiting: false });
727
+ break;
728
+ case PlayerState.PAUSED:
729
+ this.events.emit("pause");
730
+ this.events.emit("statechange", { paused: true, playing: false });
731
+ break;
732
+ case PlayerState.BUFFERING:
733
+ this.events.emit("waiting");
734
+ this.events.emit("statechange", { waiting: true });
735
+ break;
736
+ case PlayerState.ENDED:
737
+ this.events.emit("ended");
738
+ this.events.emit("statechange", { ended: true, playing: false });
739
+ break;
740
+ case PlayerState.CUED:
741
+ this.events.emit("loadeddata");
742
+ this.events.emit("durationchange", this.getDuration());
743
+ break;
744
+ }
745
+ }
746
+ onError(event) {
747
+ const errorMessages = {
748
+ 2: "Invalid video ID",
749
+ 5: "HTML5 player error",
750
+ 100: "Video not found or removed",
751
+ 101: "Video cannot be embedded",
752
+ 150: "Video cannot be embedded"
753
+ };
754
+ this.emitError(event.data, errorMessages[event.data] || "Unknown YouTube error");
755
+ }
756
+ onQualityChange() {
757
+ const quality = this.player?.getPlaybackQuality() || "auto";
758
+ const qualities = this.getQualities();
759
+ this.events.emit("qualitychange", qualities.find((q) => q.id === quality) || null);
760
+ }
761
+ onRateChange() {
762
+ this.events.emit("ratechange", this.getPlaybackRate());
763
+ }
764
+ canPlay(src) {
765
+ return isYouTubeSource(src);
766
+ }
767
+ async load(src) {
768
+ const parsed = parseYouTubeSource(src);
769
+ if (!parsed) {
770
+ this.emitError(2, "Invalid YouTube source");
771
+ return;
772
+ }
773
+ this.startTime = parsed.startTime;
774
+ this.endTime = parsed.endTime;
775
+ if (!this.player || !this._ready) {
776
+ this.pendingVideoId = parsed.videoId;
777
+ return;
778
+ }
779
+ this.loadVideo(parsed.videoId, parsed.startTime, parsed.endTime);
780
+ }
781
+ loadVideo(videoId, startTime, endTime) {
782
+ if (!this.player)
783
+ return;
784
+ if (this.options.autoplay) {
785
+ this.player.loadVideoById({
786
+ videoId,
787
+ startSeconds: startTime,
788
+ endSeconds: endTime
789
+ });
790
+ } else {
791
+ this.player.cueVideoById({
792
+ videoId,
793
+ startSeconds: startTime,
794
+ endSeconds: endTime
795
+ });
796
+ }
797
+ }
798
+ async play() {
799
+ this.player?.playVideo();
800
+ }
801
+ pause() {
802
+ this.player?.pauseVideo();
803
+ }
804
+ stop() {
805
+ this.player?.stopVideo();
806
+ }
807
+ seekTo(time) {
808
+ this.player?.seekTo(time, true);
809
+ this.events.emit("seeking");
810
+ setTimeout(() => this.events.emit("seeked"), 100);
811
+ }
812
+ getCurrentTime() {
813
+ return this.player?.getCurrentTime() || 0;
814
+ }
815
+ getDuration() {
816
+ return this.player?.getDuration() || 0;
817
+ }
818
+ setVolume(volume) {
819
+ this.player?.setVolume(Math.round(volume * 100));
820
+ this.events.emit("volumechange", volume, this.getMuted());
821
+ }
822
+ getVolume() {
823
+ return (this.player?.getVolume() || 100) / 100;
824
+ }
825
+ setMuted(muted) {
826
+ if (muted) {
827
+ this.player?.mute();
828
+ } else {
829
+ this.player?.unMute();
830
+ }
831
+ this.events.emit("volumechange", this.getVolume(), muted);
832
+ }
833
+ getMuted() {
834
+ return this.player?.isMuted() || false;
835
+ }
836
+ setPlaybackRate(rate) {
837
+ this.player?.setPlaybackRate(rate);
838
+ }
839
+ getPlaybackRate() {
840
+ return this.player?.getPlaybackRate() || 1;
841
+ }
842
+ getQualities() {
843
+ const levels = this.player?.getAvailableQualityLevels() || [];
844
+ const heights = {
845
+ highres: 2160,
846
+ hd2160: 2160,
847
+ hd1440: 1440,
848
+ hd1080: 1080,
849
+ hd720: 720,
850
+ large: 480,
851
+ medium: 360,
852
+ small: 240,
853
+ tiny: 144
854
+ };
855
+ const current = this.player?.getPlaybackQuality();
856
+ return levels.map((level) => ({
857
+ id: level,
858
+ width: Math.round((heights[level] || 480) * (16 / 9)),
859
+ height: heights[level] || 480,
860
+ bitrate: 0,
861
+ selected: level === current
862
+ }));
863
+ }
864
+ setQuality(quality) {
865
+ if (quality === "auto") {
866
+ this.player?.setPlaybackQuality("default");
867
+ } else {
868
+ this.player?.setPlaybackQuality(quality.id);
869
+ }
870
+ }
871
+ }
872
+ var youtubeLoader = {
873
+ name: "youtube",
874
+ type: "youtube",
875
+ canPlay(src) {
876
+ return isYouTubeSource(src);
877
+ },
878
+ mediaType() {
879
+ return "video";
880
+ },
881
+ preconnect() {
882
+ return [
883
+ "https://www.youtube-nocookie.com",
884
+ "https://www.youtube.com",
885
+ "https://i.ytimg.com",
886
+ "https://s.ytimg.com"
887
+ ];
888
+ },
889
+ async load(container, options) {
890
+ const provider = new YouTubeProvider;
891
+ await provider.setup(container, options);
892
+ return provider;
893
+ }
894
+ };
895
+ // src/providers/vimeo.ts
896
+ var VIMEO_PATTERNS = [
897
+ /vimeo\.com\/(\d+)/,
898
+ /vimeo\.com\/video\/(\d+)/,
899
+ /player\.vimeo\.com\/video\/(\d+)/,
900
+ /vimeo\.com\/channels\/[\w]+\/(\d+)/,
901
+ /vimeo\.com\/groups\/[\w]+\/videos\/(\d+)/
902
+ ];
903
+ function extractVimeoId(url) {
904
+ for (const pattern of VIMEO_PATTERNS) {
905
+ const match = url.match(pattern);
906
+ if (match)
907
+ return match[1];
908
+ }
909
+ return null;
910
+ }
911
+ function extractVimeoHash(url) {
912
+ const match = url.match(/vimeo\.com\/\d+\/([a-f0-9]+)/);
913
+ return match ? match[1] : null;
914
+ }
915
+ function isVimeoSource(src) {
916
+ if (typeof src === "string") {
917
+ return extractVimeoId(src) !== null;
918
+ }
919
+ if (typeof src === "object" && "type" in src) {
920
+ return src.type === "vimeo";
921
+ }
922
+ return false;
923
+ }
924
+ function parseVimeoSource(src) {
925
+ if (typeof src === "string") {
926
+ const videoId = extractVimeoId(src);
927
+ if (!videoId)
928
+ return null;
929
+ return { videoId, hash: extractVimeoHash(src) || undefined };
930
+ }
931
+ if (typeof src === "object" && "type" in src && src.type === "vimeo") {
932
+ const vSrc = src;
933
+ return {
934
+ videoId: vSrc.videoId || extractVimeoId(vSrc.src) || "",
935
+ hash: vSrc.hash
936
+ };
937
+ }
938
+ return null;
939
+ }
940
+ var VIMEO_SDK_URL = "https://player.vimeo.com/api/player.js";
941
+
942
+ class VimeoProvider extends BaseProvider {
943
+ name = "vimeo";
944
+ type = "vimeo";
945
+ container_el = null;
946
+ player = null;
947
+ iframe = null;
948
+ timeUpdateInterval = null;
949
+ _qualities = [];
950
+ _duration = 0;
951
+ get mediaElement() {
952
+ return this.iframe;
953
+ }
954
+ async initialize() {
955
+ if (!this.container) {
956
+ throw new Error("Container element is required");
957
+ }
958
+ await this.loadVimeoSDK();
959
+ this.container_el = document.createElement("div");
960
+ this.container_el.className = "ts-video-player__vimeo";
961
+ this.container.appendChild(this.container_el);
962
+ }
963
+ async loadVimeoSDK() {
964
+ if (window.Vimeo)
965
+ return;
966
+ return new Promise((resolve, reject) => {
967
+ if (document.querySelector(`script[src="${VIMEO_SDK_URL}"]`)) {
968
+ const check = () => {
969
+ if (window.Vimeo)
970
+ resolve();
971
+ else
972
+ setTimeout(check, 100);
973
+ };
974
+ check();
975
+ return;
976
+ }
977
+ const script = document.createElement("script");
978
+ script.src = VIMEO_SDK_URL;
979
+ script.async = true;
980
+ script.onload = () => resolve();
981
+ script.onerror = () => reject(new Error("Failed to load Vimeo SDK"));
982
+ document.head.appendChild(script);
983
+ });
984
+ }
985
+ cleanup() {
986
+ if (this.timeUpdateInterval) {
987
+ clearInterval(this.timeUpdateInterval);
988
+ this.timeUpdateInterval = null;
989
+ }
990
+ this.player?.destroy();
991
+ this.player = null;
992
+ this.iframe = null;
993
+ this.container_el?.remove();
994
+ this.container_el = null;
995
+ }
996
+ canPlay(src) {
997
+ return isVimeoSource(src);
998
+ }
999
+ async load(src) {
1000
+ const parsed = parseVimeoSource(src);
1001
+ if (!parsed || !this.container_el) {
1002
+ this.emitError(2, "Invalid Vimeo source");
1003
+ return;
1004
+ }
1005
+ if (this.player) {
1006
+ await this.player.destroy();
1007
+ this.player = null;
1008
+ }
1009
+ const playerOptions = {
1010
+ id: parseInt(parsed.videoId, 10),
1011
+ autopause: false,
1012
+ autoplay: this.options.autoplay || false,
1013
+ controls: false,
1014
+ loop: this.options.loop || false,
1015
+ muted: this.options.muted || false,
1016
+ playsinline: this.options.playsinline !== false,
1017
+ responsive: true,
1018
+ dnt: true
1019
+ };
1020
+ if (this.options.vimeoParams) {
1021
+ Object.assign(playerOptions, this.options.vimeoParams);
1022
+ }
1023
+ this.player = new window.Vimeo.Player(this.container_el, playerOptions);
1024
+ await this.player.ready();
1025
+ this.iframe = this.container_el.querySelector("iframe");
1026
+ if (this.iframe) {
1027
+ this.iframe.className = "ts-video-player__media";
1028
+ }
1029
+ this.attachVimeoEvents();
1030
+ this.events.emit("loadedmetadata");
1031
+ this.events.emit("canplay");
1032
+ this._duration = await this.player.getDuration();
1033
+ this.events.emit("durationchange", this._duration);
1034
+ await this.updateQualities();
1035
+ const [width, height] = await Promise.all([this.player.getVideoWidth(), this.player.getVideoHeight()]);
1036
+ this.events.emit("statechange", {
1037
+ videoWidth: width,
1038
+ videoHeight: height,
1039
+ aspectRatio: width / height || 16 / 9,
1040
+ canFullscreen: true,
1041
+ canPictureInPicture: false
1042
+ });
1043
+ }
1044
+ attachVimeoEvents() {
1045
+ if (!this.player)
1046
+ return;
1047
+ this.player.on("play", () => {
1048
+ this.events.emit("play");
1049
+ this.events.emit("playing");
1050
+ this.events.emit("statechange", { paused: false, playing: true });
1051
+ });
1052
+ this.player.on("pause", () => {
1053
+ this.events.emit("pause");
1054
+ this.events.emit("statechange", { paused: true, playing: false });
1055
+ });
1056
+ this.player.on("ended", () => {
1057
+ this.events.emit("ended");
1058
+ this.events.emit("statechange", { ended: true, playing: false });
1059
+ });
1060
+ this.player.on("timeupdate", (data) => {
1061
+ this.events.emit("timeupdate", data.seconds);
1062
+ });
1063
+ this.player.on("seeking", () => {
1064
+ this.events.emit("seeking");
1065
+ });
1066
+ this.player.on("seeked", () => {
1067
+ this.events.emit("seeked");
1068
+ });
1069
+ this.player.on("bufferstart", () => {
1070
+ this.events.emit("waiting");
1071
+ this.events.emit("statechange", { waiting: true });
1072
+ });
1073
+ this.player.on("bufferend", () => {
1074
+ this.events.emit("statechange", { waiting: false });
1075
+ });
1076
+ this.player.on("progress", (data) => {
1077
+ const buffered = [{ start: 0, end: data.percent * this._duration }];
1078
+ this.events.emit("progress", buffered);
1079
+ });
1080
+ this.player.on("volumechange", async () => {
1081
+ const [volume, muted] = await Promise.all([this.player.getVolume(), this.player.getMuted()]);
1082
+ this.events.emit("volumechange", volume, muted);
1083
+ });
1084
+ this.player.on("playbackratechange", (data) => {
1085
+ this.events.emit("ratechange", data.playbackRate);
1086
+ });
1087
+ this.player.on("qualitychange", async () => {
1088
+ await this.updateQualities();
1089
+ const current = this._qualities.find((q) => q.selected);
1090
+ this.events.emit("qualitychange", current || null);
1091
+ });
1092
+ this.player.on("error", (error) => {
1093
+ this.emitError(5, error.message, error);
1094
+ });
1095
+ }
1096
+ async updateQualities() {
1097
+ if (!this.player)
1098
+ return;
1099
+ try {
1100
+ const qualities = await this.player.getQualities();
1101
+ this._qualities = qualities.map((q) => {
1102
+ const match = q.label.match(/(\d+)p/);
1103
+ const height = match ? parseInt(match[1], 10) : 0;
1104
+ return {
1105
+ id: q.id,
1106
+ width: Math.round(height * (16 / 9)),
1107
+ height,
1108
+ bitrate: 0,
1109
+ selected: q.active
1110
+ };
1111
+ });
1112
+ this.events.emit("statechange", { qualities: this._qualities });
1113
+ } catch {}
1114
+ }
1115
+ async play() {
1116
+ try {
1117
+ await this.player?.play();
1118
+ } catch (error) {
1119
+ if (error.name === "NotAllowedError") {
1120
+ this.emitError(1, "Playback was blocked. User interaction required.", error);
1121
+ } else {
1122
+ throw error;
1123
+ }
1124
+ }
1125
+ }
1126
+ pause() {
1127
+ this.player?.pause();
1128
+ }
1129
+ stop() {
1130
+ this.pause();
1131
+ this.seekTo(0);
1132
+ }
1133
+ seekTo(time) {
1134
+ this.player?.setCurrentTime(time);
1135
+ }
1136
+ getCurrentTime() {
1137
+ return 0;
1138
+ }
1139
+ getDuration() {
1140
+ return this._duration;
1141
+ }
1142
+ setVolume(volume) {
1143
+ this.player?.setVolume(Math.max(0, Math.min(1, volume)));
1144
+ }
1145
+ getVolume() {
1146
+ return 1;
1147
+ }
1148
+ setMuted(muted) {
1149
+ this.player?.setMuted(muted);
1150
+ }
1151
+ getMuted() {
1152
+ return false;
1153
+ }
1154
+ setPlaybackRate(rate) {
1155
+ this.player?.setPlaybackRate(rate);
1156
+ }
1157
+ getPlaybackRate() {
1158
+ return 1;
1159
+ }
1160
+ getQualities() {
1161
+ return this._qualities;
1162
+ }
1163
+ setQuality(quality) {
1164
+ if (!this.player)
1165
+ return;
1166
+ if (quality === "auto") {
1167
+ this.player.setQuality("auto");
1168
+ } else {
1169
+ this.player.setQuality(quality.id);
1170
+ }
1171
+ }
1172
+ async enableTextTrack(language) {
1173
+ await this.player?.enableTextTrack(language);
1174
+ }
1175
+ async disableTextTrack() {
1176
+ await this.player?.disableTextTrack();
1177
+ }
1178
+ async getChapters() {
1179
+ if (!this.player)
1180
+ return [];
1181
+ try {
1182
+ return await this.player.getChapters();
1183
+ } catch {
1184
+ return [];
1185
+ }
1186
+ }
1187
+ }
1188
+ var vimeoLoader = {
1189
+ name: "vimeo",
1190
+ type: "vimeo",
1191
+ canPlay(src) {
1192
+ return isVimeoSource(src);
1193
+ },
1194
+ mediaType() {
1195
+ return "video";
1196
+ },
1197
+ preconnect() {
1198
+ return ["https://player.vimeo.com", "https://i.vimeocdn.com", "https://f.vimeocdn.com"];
1199
+ },
1200
+ async load(container, options) {
1201
+ const provider = new VimeoProvider;
1202
+ await provider.setup(container, options);
1203
+ return provider;
1204
+ }
1205
+ };
1206
+ // src/providers/hls.ts
1207
+ function isHLSSource(src) {
1208
+ if (typeof src === "string") {
1209
+ const url = src.toLowerCase();
1210
+ return url.endsWith(".m3u8") || url.includes("application/x-mpegurl") || url.includes("application/vnd.apple.mpegurl");
1211
+ }
1212
+ if (typeof src === "object" && "type" in src) {
1213
+ const type = src.type?.toLowerCase() || "";
1214
+ return type === "application/x-mpegurl" || type === "application/vnd.apple.mpegurl";
1215
+ }
1216
+ return false;
1217
+ }
1218
+ function isNativeHLSSupported() {
1219
+ const video = document.createElement("video");
1220
+ return video.canPlayType("application/vnd.apple.mpegurl") !== "";
1221
+ }
1222
+ var HLS_CDN_DEFAULT = "https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js";
1223
+
1224
+ class HLSProvider extends BaseProvider {
1225
+ name = "hls";
1226
+ type = "hls";
1227
+ video = null;
1228
+ hls = null;
1229
+ eventsNormalizer = null;
1230
+ cleanupFullscreen = null;
1231
+ cleanupPiP = null;
1232
+ useNativeHLS = false;
1233
+ _qualities = [];
1234
+ _audioTracks = [];
1235
+ _textTracks = [];
1236
+ currentQualityIndex = -1;
1237
+ get mediaElement() {
1238
+ return this.video;
1239
+ }
1240
+ get canLiveSync() {
1241
+ return !this.useNativeHLS && this.hls !== null;
1242
+ }
1243
+ get hlsConfig() {
1244
+ return this.hls?.config || {};
1245
+ }
1246
+ async initialize() {
1247
+ if (!this.container) {
1248
+ throw new Error("Container element is required");
1249
+ }
1250
+ this.video = document.createElement("video");
1251
+ this.video.className = "ts-video-player__media";
1252
+ if (this.options.autoplay)
1253
+ this.video.autoplay = true;
1254
+ if (this.options.loop)
1255
+ this.video.loop = true;
1256
+ if (this.options.muted)
1257
+ this.video.muted = true;
1258
+ if (this.options.playsinline)
1259
+ this.video.playsInline = true;
1260
+ if (this.options.preload)
1261
+ this.video.preload = this.options.preload;
1262
+ if (this.options.crossorigin)
1263
+ this.video.crossOrigin = this.options.crossorigin;
1264
+ if (this.options.poster)
1265
+ this.video.poster = this.options.poster;
1266
+ if (this.options.volume !== undefined)
1267
+ this.video.volume = this.options.volume;
1268
+ this.container.appendChild(this.video);
1269
+ this.eventsNormalizer = new MediaEventsNormalizer(this.video, this.events);
1270
+ this.cleanupFullscreen = onFullscreenChange((fullscreen) => {
1271
+ this.events.emit("fullscreenchange", fullscreen);
1272
+ });
1273
+ this.cleanupPiP = onPiPChange(this.video, (pip) => {
1274
+ this.events.emit("pipchange", pip);
1275
+ });
1276
+ this.useNativeHLS = isNativeHLSSupported();
1277
+ if (!this.useNativeHLS) {
1278
+ await this.loadHLSLibrary();
1279
+ }
1280
+ this.video.addEventListener("loadedmetadata", () => {
1281
+ const fullscreenAvailability = this.getFeatureAvailability("fullscreen");
1282
+ const pipAvailability = this.getFeatureAvailability("pip");
1283
+ const volumeAvailability = this.getFeatureAvailability("volume");
1284
+ this.events.emit("statechange", {
1285
+ videoWidth: this.video.videoWidth,
1286
+ videoHeight: this.video.videoHeight,
1287
+ aspectRatio: (this.video.videoHeight > 0 ? this.video.videoWidth / this.video.videoHeight : 0) || 16 / 9,
1288
+ canFullscreen: fullscreenAvailability === "available",
1289
+ canPictureInPicture: pipAvailability === "available",
1290
+ fullscreenAvailability,
1291
+ pipAvailability,
1292
+ volumeAvailability
1293
+ });
1294
+ this.events.emit("availabilitychange", "fullscreen", fullscreenAvailability);
1295
+ this.events.emit("availabilitychange", "pip", pipAvailability);
1296
+ this.events.emit("availabilitychange", "volume", volumeAvailability);
1297
+ });
1298
+ }
1299
+ async loadHLSLibrary() {
1300
+ if (window.Hls)
1301
+ return;
1302
+ return new Promise((resolve, reject) => {
1303
+ const script = document.createElement("script");
1304
+ script.src = this.options.hlsCdnUrl || HLS_CDN_DEFAULT;
1305
+ script.async = true;
1306
+ script.onload = () => resolve();
1307
+ script.onerror = () => reject(new Error("Failed to load HLS.js"));
1308
+ document.head.appendChild(script);
1309
+ });
1310
+ }
1311
+ cleanup() {
1312
+ this.eventsNormalizer?.destroy();
1313
+ this.cleanupFullscreen?.();
1314
+ this.cleanupPiP?.();
1315
+ if (this.hls) {
1316
+ this.hls.destroy();
1317
+ this.hls = null;
1318
+ }
1319
+ if (this.video) {
1320
+ this.video.pause();
1321
+ this.video.removeAttribute("src");
1322
+ this.video.load();
1323
+ this.video.remove();
1324
+ this.video = null;
1325
+ }
1326
+ this._qualities = [];
1327
+ this._audioTracks = [];
1328
+ this._textTracks = [];
1329
+ }
1330
+ canPlay(src) {
1331
+ return isHLSSource(src);
1332
+ }
1333
+ async load(src) {
1334
+ if (!this.video)
1335
+ return;
1336
+ const url = typeof src === "string" ? src : src.src;
1337
+ if (this.useNativeHLS) {
1338
+ this.video.src = url;
1339
+ this.video.load();
1340
+ return;
1341
+ }
1342
+ if (!window.Hls) {
1343
+ await this.loadHLSLibrary();
1344
+ }
1345
+ if (!window.Hls?.isSupported()) {
1346
+ this.emitError(4, "HLS is not supported in this browser");
1347
+ return;
1348
+ }
1349
+ if (this.hls) {
1350
+ this.hls.destroy();
1351
+ }
1352
+ const config = {
1353
+ enableWorker: true,
1354
+ lowLatencyMode: false,
1355
+ ...this.options.hlsConfig
1356
+ };
1357
+ this.hls = new window.Hls(config);
1358
+ this.attachHLSEvents();
1359
+ this.hls.attachMedia(this.video);
1360
+ this.hls.loadSource(url);
1361
+ }
1362
+ attachHLSEvents() {
1363
+ if (!this.hls || !window.Hls)
1364
+ return;
1365
+ const Hls = window.Hls;
1366
+ this.hls.on(Hls.Events.MANIFEST_PARSED, (_event, data) => {
1367
+ this.updateQualities(data.levels);
1368
+ if (this.options.autoplay) {
1369
+ this.video?.play().catch(() => {});
1370
+ }
1371
+ });
1372
+ this.hls.on(Hls.Events.LEVEL_SWITCHED, (_event, data) => {
1373
+ this.currentQualityIndex = data.level;
1374
+ this.updateQualitySelection();
1375
+ this.events.emit("qualitychange", this._qualities[data.level] || null);
1376
+ });
1377
+ this.hls.on(Hls.Events.AUDIO_TRACK_LOADED, () => {
1378
+ this.updateAudioTracks();
1379
+ });
1380
+ this.hls.on(Hls.Events.AUDIO_TRACK_SWITCHED, () => {
1381
+ this.updateAudioTracks();
1382
+ const current = this._audioTracks.find((t) => t.selected);
1383
+ this.events.emit("audiotrackchange", current || null);
1384
+ });
1385
+ this.hls.on(Hls.Events.SUBTITLE_TRACK_LOADED, () => {
1386
+ this.updateTextTracks();
1387
+ });
1388
+ this.hls.on(Hls.Events.ERROR, (_event, data) => {
1389
+ if (data.fatal) {
1390
+ switch (data.type) {
1391
+ case Hls.ErrorTypes.NETWORK_ERROR:
1392
+ this.hls?.startLoad();
1393
+ break;
1394
+ case Hls.ErrorTypes.MEDIA_ERROR:
1395
+ this.hls?.recoverMediaError();
1396
+ break;
1397
+ default:
1398
+ this.emitError(5, `HLS fatal error: ${data.details}`, data);
1399
+ this.hls?.destroy();
1400
+ break;
1401
+ }
1402
+ }
1403
+ });
1404
+ }
1405
+ updateQualities(levels) {
1406
+ this._qualities = levels.map((level, index) => ({
1407
+ id: String(index),
1408
+ width: level.width,
1409
+ height: level.height,
1410
+ bitrate: level.bitrate,
1411
+ codec: level.codecSet || level.videoCodec || undefined,
1412
+ selected: index === this.currentQualityIndex
1413
+ }));
1414
+ this.events.emit("statechange", { qualities: this._qualities });
1415
+ }
1416
+ updateQualitySelection() {
1417
+ this._qualities = this._qualities.map((q, i) => ({
1418
+ ...q,
1419
+ selected: i === this.currentQualityIndex
1420
+ }));
1421
+ }
1422
+ updateAudioTracks() {
1423
+ if (!this.hls)
1424
+ return;
1425
+ this._audioTracks = this.hls.audioTracks.map((track, index) => ({
1426
+ id: String(track.id),
1427
+ label: track.name || `Track ${index + 1}`,
1428
+ language: track.lang || "",
1429
+ kind: track.default ? "main" : "alternative",
1430
+ selected: index === this.hls.audioTrack
1431
+ }));
1432
+ this.events.emit("statechange", { audioTracks: this._audioTracks });
1433
+ }
1434
+ updateTextTracks() {
1435
+ if (!this.hls)
1436
+ return;
1437
+ this._textTracks = this.hls.subtitleTracks.map((track, index) => ({
1438
+ id: String(track.id),
1439
+ kind: "subtitles",
1440
+ label: track.name || `Subtitle ${index + 1}`,
1441
+ language: track.lang || "",
1442
+ mode: index === this.hls.subtitleTrack ? "showing" : "disabled",
1443
+ cues: []
1444
+ }));
1445
+ this.events.emit("statechange", { textTracks: this._textTracks });
1446
+ }
1447
+ async play() {
1448
+ if (!this.video)
1449
+ return;
1450
+ try {
1451
+ await this.video.play();
1452
+ } catch (error) {
1453
+ if (error.name === "NotAllowedError") {
1454
+ this.emitError(1, "Playback was blocked. User interaction required.", error);
1455
+ } else {
1456
+ throw error;
1457
+ }
1458
+ }
1459
+ }
1460
+ pause() {
1461
+ this.video?.pause();
1462
+ }
1463
+ seekTo(time) {
1464
+ if (this.video) {
1465
+ this.video.currentTime = Math.max(0, Math.min(time, this.getDuration()));
1466
+ }
1467
+ }
1468
+ getCurrentTime() {
1469
+ return this.video?.currentTime || 0;
1470
+ }
1471
+ getDuration() {
1472
+ return this.video?.duration || 0;
1473
+ }
1474
+ syncToLive() {
1475
+ if (this.hls?.liveSyncPosition != null) {
1476
+ this.seekTo(this.hls.liveSyncPosition);
1477
+ }
1478
+ }
1479
+ setVolume(volume) {
1480
+ if (this.video) {
1481
+ this.video.volume = Math.max(0, Math.min(1, volume));
1482
+ }
1483
+ }
1484
+ getVolume() {
1485
+ return this.video?.volume || 1;
1486
+ }
1487
+ setMuted(muted) {
1488
+ if (this.video) {
1489
+ this.video.muted = muted;
1490
+ }
1491
+ }
1492
+ getMuted() {
1493
+ return this.video?.muted || false;
1494
+ }
1495
+ setPlaybackRate(rate) {
1496
+ if (this.video) {
1497
+ this.video.playbackRate = Math.max(0.25, Math.min(4, rate));
1498
+ }
1499
+ }
1500
+ getPlaybackRate() {
1501
+ return this.video?.playbackRate || 1;
1502
+ }
1503
+ getQualities() {
1504
+ return this._qualities;
1505
+ }
1506
+ setQuality(quality) {
1507
+ if (!this.hls)
1508
+ return;
1509
+ if (quality === "auto") {
1510
+ this.hls.currentLevel = -1;
1511
+ this.currentQualityIndex = -1;
1512
+ this.events.emit("statechange", { autoQuality: true });
1513
+ } else {
1514
+ const index = parseInt(quality.id, 10);
1515
+ if (!isNaN(index) && index >= 0 && index < this._qualities.length) {
1516
+ this.hls.currentLevel = index;
1517
+ this.currentQualityIndex = index;
1518
+ this.events.emit("statechange", { autoQuality: false });
1519
+ }
1520
+ }
1521
+ }
1522
+ getAudioTracks() {
1523
+ return this._audioTracks;
1524
+ }
1525
+ setAudioTrack(trackId) {
1526
+ if (!this.hls)
1527
+ return;
1528
+ const index = parseInt(trackId, 10);
1529
+ if (!isNaN(index) && index >= 0 && index < this.hls.audioTracks.length) {
1530
+ this.hls.audioTrack = index;
1531
+ }
1532
+ }
1533
+ getTextTracks() {
1534
+ return this._textTracks;
1535
+ }
1536
+ setTextTrackMode(trackId, mode) {
1537
+ if (!this.hls)
1538
+ return;
1539
+ if (mode === "showing") {
1540
+ const index = parseInt(trackId, 10);
1541
+ if (!isNaN(index)) {
1542
+ this.hls.subtitleTrack = index;
1543
+ }
1544
+ } else {
1545
+ this.hls.subtitleTrack = -1;
1546
+ }
1547
+ this.updateTextTracks();
1548
+ const current = this._textTracks.find((t) => t.mode === "showing");
1549
+ this.events.emit("texttrackchange", current || null);
1550
+ }
1551
+ }
1552
+ var hlsLoader = {
1553
+ name: "hls",
1554
+ type: "hls",
1555
+ canPlay(src) {
1556
+ return isHLSSource(src);
1557
+ },
1558
+ mediaType() {
1559
+ return "video";
1560
+ },
1561
+ preconnect() {
1562
+ return ["https://cdn.jsdelivr.net"];
1563
+ },
1564
+ async load(container, options) {
1565
+ const provider = new HLSProvider;
1566
+ await provider.setup(container, options);
1567
+ return provider;
1568
+ }
1569
+ };
1570
+ // src/providers/dash.ts
1571
+ var dashjs = null;
1572
+ var dashLoading = null;
1573
+ var DASH_CDN_DEFAULT = "https://cdn.dashjs.org/latest/dash.all.min.js";
1574
+ async function loadDashJS(cdnUrl) {
1575
+ if (dashjs)
1576
+ return dashjs;
1577
+ if (dashLoading)
1578
+ return dashLoading;
1579
+ dashLoading = new Promise((resolve, reject) => {
1580
+ if (typeof window !== "undefined" && window.dashjs) {
1581
+ dashjs = window.dashjs;
1582
+ resolve(dashjs);
1583
+ return;
1584
+ }
1585
+ const script = document.createElement("script");
1586
+ script.src = cdnUrl || DASH_CDN_DEFAULT;
1587
+ script.async = true;
1588
+ script.onload = () => {
1589
+ dashjs = window.dashjs;
1590
+ resolve(dashjs);
1591
+ };
1592
+ script.onerror = () => {
1593
+ reject(new Error("Failed to load dash.js"));
1594
+ };
1595
+ document.head.appendChild(script);
1596
+ });
1597
+ return dashLoading;
1598
+ }
1599
+ function isDASHSource(src) {
1600
+ if (typeof src === "object" && "type" in src && src.type === "dash") {
1601
+ return true;
1602
+ }
1603
+ if (typeof src === "string") {
1604
+ return src.includes(".mpd") || src.includes("manifest(format=mpd");
1605
+ }
1606
+ return false;
1607
+ }
1608
+
1609
+ class DASHProvider extends BaseProvider {
1610
+ name = "dash";
1611
+ type = "dash";
1612
+ dash = null;
1613
+ video = null;
1614
+ source = null;
1615
+ config = {};
1616
+ _volume = 1;
1617
+ _muted = false;
1618
+ _playbackRate = 1;
1619
+ get mediaElement() {
1620
+ return this.video;
1621
+ }
1622
+ async initialize() {
1623
+ this.video = document.createElement("video");
1624
+ this.video.style.width = "100%";
1625
+ this.video.style.height = "100%";
1626
+ this.video.style.display = "block";
1627
+ await loadDashJS(this.options.dashCdnUrl);
1628
+ this.dash = dashjs.MediaPlayer().create();
1629
+ if (this.options.dashConfig) {
1630
+ this.config.dashConfig = this.options.dashConfig;
1631
+ }
1632
+ if (this.config.dashConfig) {
1633
+ Object.entries(this.config.dashConfig).forEach(([key, value]) => {
1634
+ this.dash.updateSettings({ [key]: value });
1635
+ });
1636
+ }
1637
+ if (this.config.lowLatency) {
1638
+ this.dash.updateSettings({
1639
+ streaming: {
1640
+ lowLatencyEnabled: true,
1641
+ liveDelay: 3,
1642
+ liveCatchUpMinDrift: 0.05,
1643
+ liveCatchUpPlaybackRate: 0.5
1644
+ }
1645
+ });
1646
+ }
1647
+ this.attachDashEvents();
1648
+ if (this.container) {
1649
+ this.container.appendChild(this.video);
1650
+ }
1651
+ this.attachVideoEvents();
1652
+ }
1653
+ attachVideoEvents() {
1654
+ if (!this.video)
1655
+ return;
1656
+ this.video.addEventListener("play", () => this.events.emit("play"));
1657
+ this.video.addEventListener("pause", () => this.events.emit("pause"));
1658
+ this.video.addEventListener("ended", () => this.events.emit("ended"));
1659
+ this.video.addEventListener("timeupdate", () => this.events.emit("timeupdate", this.getCurrentTime()));
1660
+ this.video.addEventListener("durationchange", () => this.events.emit("durationchange", this.getDuration()));
1661
+ this.video.addEventListener("volumechange", () => {
1662
+ this.events.emit("volumechange", this.getVolume(), this.getMuted());
1663
+ });
1664
+ this.video.addEventListener("waiting", () => this.events.emit("waiting"));
1665
+ this.video.addEventListener("playing", () => this.events.emit("playing"));
1666
+ this.video.addEventListener("seeking", () => this.events.emit("seeking"));
1667
+ this.video.addEventListener("seeked", () => this.events.emit("seeked"));
1668
+ this.video.addEventListener("progress", () => {
1669
+ const ranges = [];
1670
+ for (let i = 0;i < this.video.buffered.length; i++) {
1671
+ ranges.push({
1672
+ start: this.video.buffered.start(i),
1673
+ end: this.video.buffered.end(i)
1674
+ });
1675
+ }
1676
+ this.events.emit("progress", ranges);
1677
+ });
1678
+ this.video.addEventListener("loadedmetadata", () => {
1679
+ this.events.emit("loadedmetadata");
1680
+ const fullscreenAvailability = this.getFeatureAvailability("fullscreen");
1681
+ const pipAvailability = this.getFeatureAvailability("pip");
1682
+ const volumeAvailability = this.getFeatureAvailability("volume");
1683
+ this.events.emit("statechange", {
1684
+ videoWidth: this.video.videoWidth,
1685
+ videoHeight: this.video.videoHeight,
1686
+ aspectRatio: (this.video.videoHeight > 0 ? this.video.videoWidth / this.video.videoHeight : 0) || 16 / 9,
1687
+ canFullscreen: fullscreenAvailability === "available",
1688
+ canPictureInPicture: pipAvailability === "available",
1689
+ fullscreenAvailability,
1690
+ pipAvailability,
1691
+ volumeAvailability
1692
+ });
1693
+ this.events.emit("availabilitychange", "fullscreen", fullscreenAvailability);
1694
+ this.events.emit("availabilitychange", "pip", pipAvailability);
1695
+ this.events.emit("availabilitychange", "volume", volumeAvailability);
1696
+ });
1697
+ this.video.addEventListener("loadeddata", () => this.events.emit("loadeddata"));
1698
+ this.video.addEventListener("canplay", () => this.events.emit("canplay"));
1699
+ this.video.addEventListener("canplaythrough", () => this.events.emit("canplaythrough"));
1700
+ this.video.addEventListener("ratechange", () => this.events.emit("ratechange", this.getPlaybackRate()));
1701
+ }
1702
+ attachDashEvents() {
1703
+ if (!dashjs)
1704
+ return;
1705
+ const MediaPlayer = dashjs.MediaPlayer;
1706
+ this.dash.on(MediaPlayer.events.QUALITY_CHANGE_RENDERED, () => {
1707
+ this.events.emit("qualitychange", this.getQualities().find((q) => q.selected) || null);
1708
+ });
1709
+ this.dash.on(MediaPlayer.events.MANIFEST_LOADED, () => {});
1710
+ this.dash.on(MediaPlayer.events.ERROR, (event) => {
1711
+ this.emitError(event.error?.code || 0, event.error?.message || "DASH playback error", event.error);
1712
+ });
1713
+ }
1714
+ configureDRM(drm) {
1715
+ const protectionData = {};
1716
+ if (drm.widevine) {
1717
+ protectionData["com.widevine.alpha"] = {
1718
+ serverURL: drm.widevine.url,
1719
+ httpRequestHeaders: drm.widevine.headers
1720
+ };
1721
+ }
1722
+ if (drm.playready) {
1723
+ protectionData["com.microsoft.playready"] = {
1724
+ serverURL: drm.playready.url,
1725
+ httpRequestHeaders: drm.playready.headers
1726
+ };
1727
+ }
1728
+ if (drm.fairplay) {
1729
+ protectionData["com.apple.fps.1_0"] = {
1730
+ serverURL: drm.fairplay.url,
1731
+ serverCertificateURL: drm.fairplay.certificateUrl,
1732
+ httpRequestHeaders: drm.fairplay.headers
1733
+ };
1734
+ }
1735
+ this.dash.setProtectionData(protectionData);
1736
+ }
1737
+ async load(src) {
1738
+ if (!this.video || !this.dash)
1739
+ return;
1740
+ if (typeof src === "string") {
1741
+ this.source = { src, type: "dash" };
1742
+ } else if (isDASHSource(src)) {
1743
+ this.source = src;
1744
+ } else {
1745
+ this.emitError(4, "Invalid DASH source");
1746
+ return;
1747
+ }
1748
+ if (this.source.drm) {
1749
+ this.configureDRM(this.source.drm);
1750
+ }
1751
+ this.dash.initialize(this.video, this.source.src, false);
1752
+ }
1753
+ canPlay(src) {
1754
+ return isDASHSource(src);
1755
+ }
1756
+ cleanup() {
1757
+ if (this.dash) {
1758
+ this.dash.reset();
1759
+ this.dash = null;
1760
+ }
1761
+ if (this.video) {
1762
+ this.video.remove();
1763
+ this.video = null;
1764
+ }
1765
+ this.source = null;
1766
+ }
1767
+ async play() {
1768
+ if (!this.video)
1769
+ return;
1770
+ try {
1771
+ await this.video.play();
1772
+ } catch (error) {
1773
+ if (error.name === "NotAllowedError") {
1774
+ this.emitError(1, "Playback was blocked. User interaction required.", error);
1775
+ } else {
1776
+ throw error;
1777
+ }
1778
+ }
1779
+ }
1780
+ pause() {
1781
+ if (this.video) {
1782
+ this.video.pause();
1783
+ }
1784
+ }
1785
+ seekTo(time) {
1786
+ if (this.video) {
1787
+ this.video.currentTime = time;
1788
+ }
1789
+ }
1790
+ getCurrentTime() {
1791
+ return this.video?.currentTime || 0;
1792
+ }
1793
+ getDuration() {
1794
+ return this.video?.duration || 0;
1795
+ }
1796
+ setVolume(volume) {
1797
+ this._volume = Math.max(0, Math.min(1, volume));
1798
+ if (this.video) {
1799
+ this.video.volume = this._volume;
1800
+ }
1801
+ }
1802
+ getVolume() {
1803
+ return this.video?.volume ?? this._volume;
1804
+ }
1805
+ setMuted(muted) {
1806
+ this._muted = muted;
1807
+ if (this.video) {
1808
+ this.video.muted = muted;
1809
+ }
1810
+ }
1811
+ getMuted() {
1812
+ return this.video?.muted ?? this._muted;
1813
+ }
1814
+ setPlaybackRate(rate) {
1815
+ this._playbackRate = rate;
1816
+ if (this.video) {
1817
+ this.video.playbackRate = rate;
1818
+ }
1819
+ }
1820
+ getPlaybackRate() {
1821
+ return this.video?.playbackRate ?? this._playbackRate;
1822
+ }
1823
+ get isLive() {
1824
+ return this.dash?.isDynamic() ?? false;
1825
+ }
1826
+ get liveLatency() {
1827
+ return this.dash?.getCurrentLiveLatency() ?? 0;
1828
+ }
1829
+ get canLiveSync() {
1830
+ return this.isLive;
1831
+ }
1832
+ syncToLive() {
1833
+ if (this.dash && this.isLive) {
1834
+ this.dash.seekToOriginalLive();
1835
+ }
1836
+ }
1837
+ getQualities() {
1838
+ if (!this.dash)
1839
+ return [];
1840
+ const bitrateInfoList = this.dash.getBitrateInfoListFor("video");
1841
+ if (!bitrateInfoList)
1842
+ return [];
1843
+ const currentIndex = this.dash.getQualityFor("video");
1844
+ return bitrateInfoList.map((info, index) => ({
1845
+ id: `${index}`,
1846
+ label: `${info.height}p`,
1847
+ width: info.width,
1848
+ height: info.height,
1849
+ bitrate: info.bitrate,
1850
+ selected: index === currentIndex
1851
+ }));
1852
+ }
1853
+ setQuality(quality) {
1854
+ if (!this.dash)
1855
+ return;
1856
+ if (quality === "auto") {
1857
+ this.dash.updateSettings({
1858
+ streaming: { abr: { autoSwitchBitrate: { video: true } } }
1859
+ });
1860
+ } else {
1861
+ const index = Number.parseInt(quality.id, 10);
1862
+ if (!Number.isNaN(index)) {
1863
+ this.dash.updateSettings({
1864
+ streaming: { abr: { autoSwitchBitrate: { video: false } } }
1865
+ });
1866
+ this.dash.setQualityFor("video", index);
1867
+ }
1868
+ }
1869
+ }
1870
+ get isAutoQuality() {
1871
+ if (!this.dash)
1872
+ return true;
1873
+ const settings = this.dash.getSettings();
1874
+ return settings?.streaming?.abr?.autoSwitchBitrate?.video ?? true;
1875
+ }
1876
+ getAudioTracks() {
1877
+ if (!this.dash)
1878
+ return [];
1879
+ const tracks = this.dash.getTracksFor("audio");
1880
+ if (!tracks)
1881
+ return [];
1882
+ const currentTrack = this.dash.getCurrentTrackFor("audio");
1883
+ return tracks.map((track, index) => ({
1884
+ id: track.id || `${index}`,
1885
+ label: track.labels?.[0]?.text || track.lang || `Track ${index + 1}`,
1886
+ language: track.lang || "",
1887
+ kind: "main",
1888
+ selected: track === currentTrack
1889
+ }));
1890
+ }
1891
+ setAudioTrack(trackId) {
1892
+ if (!this.dash)
1893
+ return;
1894
+ const tracks = this.dash.getTracksFor("audio");
1895
+ const track = tracks.find((t, i) => (t.id || `${i}`) === trackId);
1896
+ if (track) {
1897
+ this.dash.setCurrentTrack(track);
1898
+ }
1899
+ }
1900
+ getTextTracks() {
1901
+ if (!this.dash)
1902
+ return [];
1903
+ const tracks = this.dash.getTracksFor("text");
1904
+ if (!tracks)
1905
+ return [];
1906
+ const currentTrack = this.dash.getCurrentTrackFor("text");
1907
+ return tracks.map((track, index) => ({
1908
+ id: track.id || `${index}`,
1909
+ label: track.labels?.[0]?.text || track.lang || `Subtitle ${index + 1}`,
1910
+ language: track.lang || "",
1911
+ kind: track.kind || "subtitles",
1912
+ mode: track === currentTrack ? "showing" : "disabled",
1913
+ cues: []
1914
+ }));
1915
+ }
1916
+ setTextTrackMode(trackId, mode) {
1917
+ if (!this.dash)
1918
+ return;
1919
+ if (mode === "disabled") {
1920
+ this.dash.enableText(false);
1921
+ return;
1922
+ }
1923
+ const tracks = this.dash.getTracksFor("text");
1924
+ const track = tracks.find((t, i) => (t.id || `${i}`) === trackId);
1925
+ if (track) {
1926
+ this.dash.setCurrentTrack(track);
1927
+ this.dash.enableText(mode === "showing");
1928
+ }
1929
+ }
1930
+ }
1931
+ var dashLoader = {
1932
+ name: "dash",
1933
+ type: "dash",
1934
+ canPlay(src) {
1935
+ return isDASHSource(src);
1936
+ },
1937
+ mediaType() {
1938
+ return "video";
1939
+ },
1940
+ async load(container, options) {
1941
+ const provider = new DASHProvider;
1942
+ await provider.setup(container, options);
1943
+ return provider;
1944
+ },
1945
+ preconnect() {
1946
+ return ["https://cdn.dashjs.org"];
1947
+ }
1948
+ };
1949
+ // src/providers/index.ts
1950
+ var defaultLoaders = [
1951
+ hlsLoader,
1952
+ dashLoader,
1953
+ youtubeLoader,
1954
+ vimeoLoader,
1955
+ html5Loader
1956
+ ];
1957
+ function findLoader(src, loaders = defaultLoaders) {
1958
+ for (const loader of loaders) {
1959
+ if (loader.canPlay(src)) {
1960
+ return loader;
1961
+ }
1962
+ }
1963
+ return null;
1964
+ }
1965
+ function findSourceCandidates(sources, loaders = defaultLoaders) {
1966
+ const candidates = [];
1967
+ sources.forEach((src, index) => {
1968
+ const loader = findLoader(src, loaders);
1969
+ if (loader)
1970
+ candidates.push({ src, index, loader });
1971
+ });
1972
+ return candidates;
1973
+ }
1974
+ function detectMediaType(src, loaders = defaultLoaders) {
1975
+ const loader = findLoader(src, loaders);
1976
+ return loader?.mediaType(src) || "unknown";
1977
+ }
1978
+ function getPreconnectHints(src, loaders = defaultLoaders) {
1979
+ const loader = findLoader(src, loaders);
1980
+ return loader?.preconnect?.() || [];
1981
+ }
1982
+
1983
+ // src/player.ts
1984
+ class Player {
1985
+ _el;
1986
+ _options;
1987
+ _store;
1988
+ _events = new EventEmitter;
1989
+ _provider = null;
1990
+ _loaders;
1991
+ _cleanupFns = [];
1992
+ _destroyed = false;
1993
+ _sourceLoadId = 0;
1994
+ _announcer = null;
1995
+ constructor(container, options = {}) {
1996
+ if (typeof container === "string") {
1997
+ const el = document.querySelector(container);
1998
+ if (!el)
1999
+ throw new Error(`Container element not found: ${container}`);
2000
+ this._el = el;
2001
+ } else {
2002
+ this._el = container;
2003
+ }
2004
+ this._options = { ...defaultOptions, ...options };
2005
+ this._loaders = defaultLoaders;
2006
+ this._store = new StateStore;
2007
+ this.setupElement();
2008
+ this.setupState();
2009
+ this.setupKeyboard();
2010
+ this.setupActivity();
2011
+ this.setupStorage();
2012
+ this.setupMediaSession();
2013
+ this.setupEvents();
2014
+ if (this._options.src) {
2015
+ this.setSrc(this._options.src);
2016
+ }
2017
+ this._events.emit("init");
2018
+ }
2019
+ get el() {
2020
+ return this._el;
2021
+ }
2022
+ get state() {
2023
+ return this._store.getState();
2024
+ }
2025
+ get provider() {
2026
+ return this._provider;
2027
+ }
2028
+ get ready() {
2029
+ return this._provider?.ready || false;
2030
+ }
2031
+ setupElement() {
2032
+ this._el.classList.add("ts-video-player");
2033
+ this._el.setAttribute("tabindex", "0");
2034
+ this._el.setAttribute("role", "region");
2035
+ this._el.setAttribute("aria-roledescription", "video player");
2036
+ this._el.setAttribute("aria-label", this._options.title || "Video player");
2037
+ this._el.setAttribute("aria-keyshortcuts", "Space K M F I C ArrowLeft ArrowRight J L ArrowUp ArrowDown Home End 0 1 2 3 4 5 6 7 8 9");
2038
+ const mediaContainer = document.createElement("div");
2039
+ mediaContainer.className = "ts-video-player__container";
2040
+ this._el.appendChild(mediaContainer);
2041
+ if (this._options.announcements !== false) {
2042
+ const liveRegion = document.createElement("div");
2043
+ liveRegion.className = "ts-video-player__announcer";
2044
+ liveRegion.setAttribute("role", "status");
2045
+ liveRegion.setAttribute("aria-live", "polite");
2046
+ liveRegion.setAttribute("aria-atomic", "true");
2047
+ liveRegion.style.cssText = "position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0";
2048
+ this._el.appendChild(liveRegion);
2049
+ this._announcer = liveRegion;
2050
+ }
2051
+ const _el = this._el;
2052
+ _el.__videoPlayer = this;
2053
+ }
2054
+ setupState() {
2055
+ this._store.batch({
2056
+ volume: this._options.volume ?? 1,
2057
+ muted: this._options.muted ?? false,
2058
+ loop: this._options.loop ?? false,
2059
+ autoplay: this._options.autoplay ?? false,
2060
+ playsinline: this._options.playsinline ?? true,
2061
+ playbackRate: this._options.playbackRate ?? 1,
2062
+ poster: this._options.poster || "",
2063
+ title: this._options.title || "",
2064
+ preload: this._options.preload || "metadata"
2065
+ });
2066
+ const unsubscribe = this._store.subscribe((state) => {
2067
+ this.updateAttributes(state);
2068
+ });
2069
+ this._cleanupFns.push(unsubscribe);
2070
+ }
2071
+ setupKeyboard() {
2072
+ if (this._options.keyboard === false)
2073
+ return;
2074
+ const actions = {
2075
+ togglePlay: () => this.togglePlay(),
2076
+ toggleMute: () => this.toggleMute(),
2077
+ toggleFullscreen: () => this.toggleFullscreen(),
2078
+ togglePiP: () => this.togglePiP(),
2079
+ toggleCaptions: () => this.toggleCaptions(),
2080
+ seekBackward: () => this.seekBy(-5),
2081
+ seekForward: () => this.seekBy(5),
2082
+ volumeUp: () => this.setVolume(Math.min(1, this.state.volume + 0.1)),
2083
+ volumeDown: () => this.setVolume(Math.max(0, this.state.volume - 0.1)),
2084
+ speedUp: () => this.setPlaybackRate(Math.min(4, this.state.playbackRate + 0.25)),
2085
+ speedDown: () => this.setPlaybackRate(Math.max(0.25, this.state.playbackRate - 0.25)),
2086
+ seekStart: () => this.seekTo(0),
2087
+ seekEnd: () => this.seekTo(this.state.duration),
2088
+ seekPercent: (percent) => this.seekTo(percent / 100 * this.state.duration)
2089
+ };
2090
+ const config = typeof this._options.keyboard === "object" ? this._options.keyboard : {};
2091
+ const handler = createKeyboardHandler(actions, {
2092
+ seekStep: config.seekStep,
2093
+ volumeStep: config.volumeStep
2094
+ });
2095
+ const target = config.global ? document : this._el;
2096
+ target.addEventListener("keydown", handler);
2097
+ this._cleanupFns.push(() => target.removeEventListener("keydown", handler));
2098
+ }
2099
+ setupActivity() {
2100
+ const cleanup = createActivityDetector(this._el, {
2101
+ timeout: this._options.controlsTimeout || 3000,
2102
+ onActive: () => {
2103
+ this._store.batch({ userActive: true, controlsVisible: true });
2104
+ this._events.emit("useractivitychange", true);
2105
+ this._events.emit("controlschange", true);
2106
+ },
2107
+ onInactive: () => {
2108
+ if (this.state.playing) {
2109
+ this._store.batch({ userActive: false, controlsVisible: false });
2110
+ this._events.emit("useractivitychange", false);
2111
+ this._events.emit("controlschange", false);
2112
+ }
2113
+ }
2114
+ });
2115
+ this._cleanupFns.push(cleanup);
2116
+ const onMouseEnter = () => this._store.set("pointerOver", true);
2117
+ const onMouseLeave = () => this._store.set("pointerOver", false);
2118
+ this._el.addEventListener("mouseenter", onMouseEnter);
2119
+ this._el.addEventListener("mouseleave", onMouseLeave);
2120
+ this._cleanupFns.push(() => this._el.removeEventListener("mouseenter", onMouseEnter), () => this._el.removeEventListener("mouseleave", onMouseLeave));
2121
+ }
2122
+ setupStorage() {
2123
+ if (!this._options.storage)
2124
+ return;
2125
+ const config = typeof this._options.storage === "object" ? this._options.storage : {};
2126
+ const storage = config.type === "session" ? sessionStorage : localStorage;
2127
+ const cleanup = createStorageSync(this._store, config.persist || ["volume", "muted", "playbackRate"], storage);
2128
+ this._cleanupFns.push(cleanup);
2129
+ }
2130
+ setupMediaSession() {
2131
+ if (this._options.mediaSession === false)
2132
+ return;
2133
+ const sessionOpts = typeof this._options.mediaSession === "object" ? this._options.mediaSession : this._options.title ? { title: this._options.title } : undefined;
2134
+ const cleanup = createMediaSession(this._store, {
2135
+ play: () => this.play(),
2136
+ pause: () => this.pause(),
2137
+ seekTo: (time) => this.seekTo(time),
2138
+ seekBy: (offset) => this.seekBy(offset),
2139
+ stop: () => this.stop()
2140
+ }, sessionOpts);
2141
+ this._cleanupFns.push(cleanup);
2142
+ }
2143
+ setupEvents() {
2144
+ if (this._options.on) {
2145
+ for (const [event, handler] of Object.entries(this._options.on)) {
2146
+ if (handler) {
2147
+ this.on(event, handler);
2148
+ }
2149
+ }
2150
+ }
2151
+ }
2152
+ updateAttributes(state) {
2153
+ const el = this._el;
2154
+ el.dataset.paused = String(state.paused);
2155
+ el.dataset.playing = String(state.playing);
2156
+ el.dataset.started = String(state.started);
2157
+ el.dataset.ended = String(state.ended);
2158
+ el.dataset.seeking = String(state.seeking);
2159
+ el.dataset.waiting = String(state.waiting);
2160
+ el.dataset.loop = String(state.loop);
2161
+ el.dataset.autoplay = String(state.autoplay);
2162
+ el.dataset.loading = String(state.waiting || state.loadingState === "loading");
2163
+ el.dataset.canPlay = String(state.canPlay);
2164
+ el.dataset.loadingState = state.loadingState;
2165
+ el.setAttribute("aria-busy", String(state.waiting || state.loadingState === "loading"));
2166
+ el.dataset.muted = String(state.muted);
2167
+ el.dataset.volumeLevel = state.muted || state.volume === 0 ? "silent" : state.volume < 0.5 ? "low" : "high";
2168
+ el.dataset.fullscreen = String(state.fullscreen);
2169
+ el.dataset.pip = String(state.pictureInPicture);
2170
+ el.dataset.controlsVisible = String(state.controlsVisible);
2171
+ el.dataset.userActive = String(state.userActive);
2172
+ el.dataset.mediaType = state.mediaType;
2173
+ el.dataset.streamType = state.streamType;
2174
+ const hasActiveCaptions = state.textTracks.some((t) => t.mode === "showing");
2175
+ el.dataset.captions = String(hasActiveCaptions);
2176
+ el.dataset.error = String(!!state.error);
2177
+ }
2178
+ announce(message) {
2179
+ if (!this._announcer)
2180
+ return;
2181
+ this._announcer.textContent = "";
2182
+ requestAnimationFrame(() => {
2183
+ if (this._announcer)
2184
+ this._announcer.textContent = message;
2185
+ });
2186
+ }
2187
+ destroy() {
2188
+ if (this._destroyed)
2189
+ return;
2190
+ this._destroyed = true;
2191
+ this._sourceLoadId++;
2192
+ this._events.emit("destroy");
2193
+ this._cleanupFns.forEach((fn) => fn());
2194
+ this._cleanupFns = [];
2195
+ this._provider?.destroy();
2196
+ this._provider = null;
2197
+ this._events.removeAllListeners();
2198
+ this._store.reset();
2199
+ this._el.classList.remove("ts-video-player");
2200
+ this._el.innerHTML = "";
2201
+ delete this._el.__videoPlayer;
2202
+ }
2203
+ async setSrc(src) {
2204
+ if (this._destroyed)
2205
+ return;
2206
+ const loadId = ++this._sourceLoadId;
2207
+ const sources = Array.isArray(src) ? src : [src];
2208
+ if (sources.length === 0) {
2209
+ return;
2210
+ }
2211
+ this._store.batch({
2212
+ sources,
2213
+ src: sources[0],
2214
+ loadingState: "loading",
2215
+ error: null
2216
+ });
2217
+ this._events.emit("sourceschange", sources);
2218
+ const candidates = findSourceCandidates(sources, this._loaders);
2219
+ if (candidates.length === 0) {
2220
+ this._store.batch({
2221
+ loadingState: "error",
2222
+ error: { code: 4, message: "No compatible provider found for source" }
2223
+ });
2224
+ return;
2225
+ }
2226
+ let lastError;
2227
+ for (const candidate of candidates) {
2228
+ if (this._destroyed || loadId !== this._sourceLoadId)
2229
+ return;
2230
+ const { loader } = candidate;
2231
+ if (this._provider && this._provider.type !== loader.type) {
2232
+ this._provider.destroy();
2233
+ this._provider = null;
2234
+ this._events.emit("providerchange", null);
2235
+ }
2236
+ if (!this._provider) {
2237
+ const mediaContainer = this._el.querySelector(".ts-video-player__container");
2238
+ try {
2239
+ this._provider = await loader.load(mediaContainer, {
2240
+ ...this._options,
2241
+ mediaType: loader.mediaType(candidate.src)
2242
+ });
2243
+ if (this._destroyed || loadId !== this._sourceLoadId) {
2244
+ this._provider.destroy();
2245
+ this._provider = null;
2246
+ return;
2247
+ }
2248
+ this._events.emit("providerchange", this._provider);
2249
+ this.attachProviderEvents(this._provider);
2250
+ } catch (error) {
2251
+ lastError = error;
2252
+ this._provider = null;
2253
+ continue;
2254
+ }
2255
+ }
2256
+ try {
2257
+ const compatibleSources = loader.type === "video" ? candidates.filter((item) => item.loader.type === loader.type).map((item) => item.src) : [candidate.src];
2258
+ await this._provider.load(compatibleSources.length > 1 ? compatibleSources : candidate.src);
2259
+ if (this._destroyed || loadId !== this._sourceLoadId)
2260
+ return;
2261
+ this._store.batch({
2262
+ src: candidate.src,
2263
+ currentSourceIndex: candidate.index,
2264
+ mediaType: loader.mediaType(candidate.src),
2265
+ providerType: loader.type
2266
+ });
2267
+ return;
2268
+ } catch (error) {
2269
+ lastError = error;
2270
+ if (this._provider) {
2271
+ this._provider.destroy();
2272
+ this._provider = null;
2273
+ }
2274
+ this._events.emit("providerchange", null);
2275
+ }
2276
+ }
2277
+ if (this._destroyed || loadId !== this._sourceLoadId)
2278
+ return;
2279
+ this._store.batch({
2280
+ loadingState: "error",
2281
+ error: { code: 4, message: "Failed to load all compatible sources", details: lastError }
2282
+ });
2283
+ }
2284
+ getSrc() {
2285
+ return this.state.src;
2286
+ }
2287
+ attachProviderEvents(provider) {
2288
+ provider.on("loadstart", () => {
2289
+ this._store.set("loadingState", "loading");
2290
+ this._events.emit("loadstart");
2291
+ });
2292
+ provider.on("loadedmetadata", () => {
2293
+ this._events.emit("loadedmetadata");
2294
+ });
2295
+ provider.on("loadeddata", () => {
2296
+ this._events.emit("loadeddata");
2297
+ });
2298
+ provider.on("canplay", () => {
2299
+ this._store.batch({ canPlay: true, loadingState: "loaded" });
2300
+ this._events.emit("canplay");
2301
+ });
2302
+ provider.on("canplaythrough", () => {
2303
+ this._store.set("canPlayThrough", true);
2304
+ this._events.emit("canplaythrough");
2305
+ });
2306
+ provider.on("play", () => {
2307
+ this._store.batch({ paused: false, ended: false });
2308
+ this._events.emit("play");
2309
+ this.announce("Playing");
2310
+ });
2311
+ provider.on("pause", () => {
2312
+ this._store.set("paused", true);
2313
+ this._events.emit("pause");
2314
+ this.announce("Paused");
2315
+ });
2316
+ provider.on("playing", () => {
2317
+ this._store.batch({ playing: true, started: true, waiting: false, playbackState: "playing" });
2318
+ this._events.emit("playing");
2319
+ });
2320
+ provider.on("waiting", () => {
2321
+ this._store.batch({ waiting: true, playbackState: "buffering" });
2322
+ this._events.emit("waiting");
2323
+ });
2324
+ provider.on("seeking", () => {
2325
+ this._store.set("seeking", true);
2326
+ this._events.emit("seeking");
2327
+ });
2328
+ provider.on("seeked", () => {
2329
+ this._store.set("seeking", false);
2330
+ this._events.emit("seeked");
2331
+ });
2332
+ provider.on("timeupdate", (time) => {
2333
+ this._store.set("currentTime", time);
2334
+ this._events.emit("timeupdate", time);
2335
+ });
2336
+ provider.on("durationchange", (duration) => {
2337
+ this._store.set("duration", duration);
2338
+ this._events.emit("durationchange", duration);
2339
+ });
2340
+ provider.on("volumechange", (volume, muted) => {
2341
+ this._store.batch({ volume, muted });
2342
+ this._events.emit("volumechange", volume, muted);
2343
+ if (muted) {
2344
+ this.announce("Muted");
2345
+ } else {
2346
+ this.announce(`Volume ${Math.round(volume * 100)}%`);
2347
+ }
2348
+ });
2349
+ provider.on("ratechange", (rate) => {
2350
+ this._store.set("playbackRate", rate);
2351
+ this._events.emit("ratechange", rate);
2352
+ });
2353
+ provider.on("progress", (buffered) => {
2354
+ this._store.batch({
2355
+ buffered,
2356
+ bufferedAmount: selectBufferedAmount({ ...this.state, buffered })
2357
+ });
2358
+ this._events.emit("progress", buffered);
2359
+ });
2360
+ provider.on("ended", () => {
2361
+ this._store.batch({ ended: true, playing: false, playbackState: "ended" });
2362
+ this._events.emit("ended");
2363
+ this.announce("Playback ended");
2364
+ if (this.state.loop) {
2365
+ this.seekTo(0);
2366
+ this.play();
2367
+ }
2368
+ });
2369
+ provider.on("error", (error) => {
2370
+ this._store.batch({ loadingState: "error", error });
2371
+ this._events.emit("error", error);
2372
+ this.announce(`Error: ${error.message}`);
2373
+ });
2374
+ provider.on("statechange", (state) => {
2375
+ this._store.batch(state);
2376
+ this._events.emit("statechange", state);
2377
+ });
2378
+ provider.on("qualitychange", (quality) => {
2379
+ this._store.set("qualities", provider.getQualities());
2380
+ this._events.emit("qualitychange", quality);
2381
+ });
2382
+ provider.on("fullscreenchange", (fullscreen) => {
2383
+ this._store.set("fullscreen", fullscreen);
2384
+ this._events.emit("fullscreenchange", fullscreen);
2385
+ this.announce(fullscreen ? "Entered fullscreen" : "Exited fullscreen");
2386
+ });
2387
+ provider.on("pipchange", (pip) => {
2388
+ this._store.set("pictureInPicture", pip);
2389
+ this._events.emit("pipchange", pip);
2390
+ });
2391
+ provider.on("availabilitychange", (feature, availability) => {
2392
+ if (feature === "volume")
2393
+ this._store.set("volumeAvailability", availability);
2394
+ else if (feature === "fullscreen")
2395
+ this._store.set("fullscreenAvailability", availability);
2396
+ else if (feature === "pip")
2397
+ this._store.set("pipAvailability", availability);
2398
+ this._events.emit("availabilitychange", feature, availability);
2399
+ });
2400
+ }
2401
+ async play() {
2402
+ if (!this._provider)
2403
+ return;
2404
+ await this._provider.play();
2405
+ }
2406
+ pause() {
2407
+ this._provider?.pause();
2408
+ }
2409
+ togglePlay() {
2410
+ if (this.state.paused) {
2411
+ this.play().catch(() => {});
2412
+ } else {
2413
+ this.pause();
2414
+ }
2415
+ }
2416
+ stop() {
2417
+ this._provider?.stop();
2418
+ this._store.batch({ playing: false, paused: true, currentTime: 0, playbackState: "idle" });
2419
+ }
2420
+ seekTo(time) {
2421
+ this._provider?.seekTo(time);
2422
+ }
2423
+ seekBy(offset) {
2424
+ const newTime = Math.max(0, Math.min(this.state.currentTime + offset, this.state.duration));
2425
+ this.seekTo(newTime);
2426
+ }
2427
+ setVolume(volume) {
2428
+ if (this.state.volumeAvailability === "unsupported")
2429
+ return;
2430
+ const clamped = Math.max(0, Math.min(1, volume));
2431
+ this._provider?.setVolume(clamped);
2432
+ this._store.set("volume", clamped);
2433
+ }
2434
+ setMuted(muted) {
2435
+ this._provider?.setMuted(muted);
2436
+ this._store.set("muted", muted);
2437
+ }
2438
+ toggleMute() {
2439
+ this.setMuted(!this.state.muted);
2440
+ }
2441
+ setPlaybackRate(rate) {
2442
+ const clamped = Math.max(0.25, Math.min(4, rate));
2443
+ this._provider?.setPlaybackRate(clamped);
2444
+ this._store.set("playbackRate", clamped);
2445
+ }
2446
+ async enterFullscreen() {
2447
+ if (this.state.fullscreenAvailability === "unsupported")
2448
+ return;
2449
+ if (this.state.pictureInPicture) {
2450
+ await this.exitPiP();
2451
+ }
2452
+ await this._provider?.enterFullscreen();
2453
+ if (this._options.fullscreenOrientationLock !== false) {
2454
+ lockOrientation("landscape");
2455
+ }
2456
+ }
2457
+ async exitFullscreen() {
2458
+ await this._provider?.exitFullscreen();
2459
+ if (this._options.fullscreenOrientationLock !== false) {
2460
+ unlockOrientation();
2461
+ }
2462
+ }
2463
+ async toggleFullscreen() {
2464
+ if (this.state.fullscreen) {
2465
+ await this.exitFullscreen();
2466
+ } else {
2467
+ await this.enterFullscreen();
2468
+ }
2469
+ }
2470
+ async enterPiP() {
2471
+ if (this.state.pipAvailability === "unsupported")
2472
+ return;
2473
+ if (this.state.fullscreen) {
2474
+ await this.exitFullscreen();
2475
+ }
2476
+ await this._provider?.enterPiP();
2477
+ }
2478
+ async exitPiP() {
2479
+ await this._provider?.exitPiP();
2480
+ }
2481
+ async togglePiP() {
2482
+ if (this.state.pictureInPicture) {
2483
+ await this.exitPiP();
2484
+ } else {
2485
+ await this.enterPiP();
2486
+ }
2487
+ }
2488
+ setQuality(quality) {
2489
+ this._provider?.setQuality(quality);
2490
+ if (quality === "auto") {
2491
+ this._store.set("autoQuality", true);
2492
+ } else {
2493
+ this._store.set("autoQuality", false);
2494
+ }
2495
+ }
2496
+ setTextTrack(trackId, mode) {
2497
+ this._provider?.setTextTrackMode(trackId, mode);
2498
+ this._store.set("textTracks", this._provider?.getTextTracks() || []);
2499
+ }
2500
+ setAudioTrack(trackId) {
2501
+ this._provider?.setAudioTrack(trackId);
2502
+ this._store.set("audioTracks", this._provider?.getAudioTracks() || []);
2503
+ }
2504
+ toggleCaptions() {
2505
+ const textTracks = this.state.textTracks;
2506
+ const showing = textTracks.find((t) => t.mode === "showing");
2507
+ if (showing) {
2508
+ this.setTextTrack(showing.id, "disabled");
2509
+ } else {
2510
+ const first = textTracks.find((t) => t.kind === "subtitles" || t.kind === "captions");
2511
+ if (first) {
2512
+ this.setTextTrack(first.id, "showing");
2513
+ }
2514
+ }
2515
+ }
2516
+ on(event, handler) {
2517
+ this._events.on(event, handler);
2518
+ }
2519
+ off(event, handler) {
2520
+ this._events.off(event, handler);
2521
+ }
2522
+ once(event, handler) {
2523
+ this._events.once(event, handler);
2524
+ }
2525
+ subscribe(keyOrListener, listener) {
2526
+ return this._store.subscribe(keyOrListener, listener);
2527
+ }
2528
+ }
2529
+ var defaultOptions = {
2530
+ autoplay: false,
2531
+ loop: false,
2532
+ muted: false,
2533
+ volume: 1,
2534
+ playbackRate: 1,
2535
+ playsinline: true,
2536
+ preload: "metadata",
2537
+ controls: true,
2538
+ keyboard: true,
2539
+ controlsTimeout: 3000,
2540
+ storage: true
2541
+ };
2542
+ function createPlayer(container, options) {
2543
+ return new Player(container, options);
2544
+ }
2545
+ function getPlayer(element) {
2546
+ return element.__videoPlayer || null;
2547
+ }
2548
+
2549
+ // src/elements/base.ts
2550
+ var HTMLElementBase = globalThis.HTMLElement ?? class {
2551
+ };
2552
+
2553
+ // src/elements/video-player.ts
2554
+ var OBSERVED_ATTRS = ["src", "poster", "autoplay", "loop", "muted", "controls", "volume", "playback-rate", "preload", "playsinline", "crossorigin", "controlslist", "disable-picture-in-picture"];
2555
+
2556
+ class VideoPlayerElement extends HTMLElementBase {
2557
+ _player = null;
2558
+ _container = null;
2559
+ _disconnectId = 0;
2560
+ static get observedAttributes() {
2561
+ return [...OBSERVED_ATTRS];
2562
+ }
2563
+ get player() {
2564
+ return this._player;
2565
+ }
2566
+ connectedCallback() {
2567
+ this._disconnectId++;
2568
+ if (this._player)
2569
+ return;
2570
+ const shadow = this.shadowRoot ?? this.attachShadow({ mode: "open" });
2571
+ this._container = document.createElement("div");
2572
+ this._container.setAttribute("part", "container");
2573
+ this._container.style.cssText = "position:relative;width:100%;height:100%;";
2574
+ const slot = document.createElement("slot");
2575
+ this._container.appendChild(slot);
2576
+ shadow.appendChild(this._container);
2577
+ const options = this.buildOptions();
2578
+ this._player = new Player(this._container, options);
2579
+ this.forwardEvents();
2580
+ }
2581
+ disconnectedCallback() {
2582
+ if (this.hasAttribute("keep-alive"))
2583
+ return;
2584
+ const disconnectId = ++this._disconnectId;
2585
+ queueMicrotask(() => {
2586
+ if (this.isConnected || disconnectId !== this._disconnectId)
2587
+ return;
2588
+ this._player?.destroy();
2589
+ this._player = null;
2590
+ this._container = null;
2591
+ this.shadowRoot?.replaceChildren();
2592
+ });
2593
+ }
2594
+ attributeChangedCallback(name, _old, value) {
2595
+ if (!this._player)
2596
+ return;
2597
+ switch (name) {
2598
+ case "src":
2599
+ if (value)
2600
+ this._player.setSrc(value);
2601
+ break;
2602
+ case "muted":
2603
+ this._player.setMuted(value !== null);
2604
+ break;
2605
+ case "volume":
2606
+ if (value !== null)
2607
+ this._player.setVolume(parseFloat(value));
2608
+ break;
2609
+ case "playback-rate":
2610
+ if (value !== null)
2611
+ this._player.setPlaybackRate(parseFloat(value));
2612
+ break;
2613
+ }
2614
+ }
2615
+ buildOptions() {
2616
+ const options = {};
2617
+ const config = this.readSourceConfig();
2618
+ if (config.sources?.length)
2619
+ options.src = config.sources;
2620
+ if (config.tracks?.length)
2621
+ options.tracks = config.tracks;
2622
+ const src = this.getAttribute("src");
2623
+ if (src && !options.src)
2624
+ options.src = config.drm ? { src, type: "application/dash+xml", drm: config.drm } : src;
2625
+ const poster = this.getAttribute("poster");
2626
+ if (poster)
2627
+ options.poster = poster;
2628
+ const title = this.getAttribute("title");
2629
+ if (title)
2630
+ options.title = title;
2631
+ if (this.hasAttribute("autoplay"))
2632
+ options.autoplay = true;
2633
+ if (this.hasAttribute("loop"))
2634
+ options.loop = true;
2635
+ if (this.hasAttribute("muted"))
2636
+ options.muted = true;
2637
+ const volume = this.getAttribute("volume");
2638
+ if (volume)
2639
+ options.volume = parseFloat(volume);
2640
+ const rate = this.getAttribute("playback-rate");
2641
+ if (rate)
2642
+ options.playbackRate = parseFloat(rate);
2643
+ const preload = this.getAttribute("preload");
2644
+ if (preload)
2645
+ options.preload = preload;
2646
+ if (this.hasAttribute("controls"))
2647
+ options.controls = true;
2648
+ options.playsinline = this.getAttribute("playsinline") !== "false";
2649
+ const crossorigin = this.getAttribute("crossorigin");
2650
+ if (crossorigin !== null)
2651
+ options.crossorigin = crossorigin;
2652
+ const controlsList = this.getAttribute("controlslist");
2653
+ if (controlsList)
2654
+ options.controlsList = controlsList.split(/\s+/);
2655
+ options.disablePictureInPicture = this.hasAttribute("disable-picture-in-picture");
2656
+ return options;
2657
+ }
2658
+ readSourceConfig() {
2659
+ const script = this.querySelector('script[type="application/json"][data-media-config]');
2660
+ const value = this.getAttribute("data-media-config")?.trim() || script?.textContent?.trim();
2661
+ if (!value)
2662
+ return {};
2663
+ if (value.length > 65536)
2664
+ throw new TypeError("Media player configuration is too large");
2665
+ try {
2666
+ const parsed = JSON.parse(value);
2667
+ if (parsed.sources !== undefined && (!Array.isArray(parsed.sources) || parsed.sources.some((source) => !source || typeof source !== "object" || typeof source.src !== "string"))) {
2668
+ throw new TypeError("Media player sources are invalid");
2669
+ }
2670
+ const trackKinds = new Set(["subtitles", "captions", "descriptions", "chapters", "metadata"]);
2671
+ if (parsed.tracks !== undefined && (!Array.isArray(parsed.tracks) || parsed.tracks.some((track) => !track || typeof track !== "object" || typeof track.src !== "string" || !trackKinds.has(String(track.kind))))) {
2672
+ throw new TypeError("Media player tracks are invalid");
2673
+ }
2674
+ return { sources: parsed.sources, drm: parsed.drm, tracks: parsed.tracks };
2675
+ } catch (error) {
2676
+ if (error instanceof TypeError)
2677
+ throw error;
2678
+ throw new TypeError("Media player configuration is invalid JSON");
2679
+ }
2680
+ }
2681
+ forwardEvents() {
2682
+ if (!this._player)
2683
+ return;
2684
+ const events = [
2685
+ "play",
2686
+ "pause",
2687
+ "playing",
2688
+ "waiting",
2689
+ "ended",
2690
+ "timeupdate",
2691
+ "durationchange",
2692
+ "volumechange",
2693
+ "fullscreenchange",
2694
+ "pipchange",
2695
+ "error",
2696
+ "loadstart",
2697
+ "loadeddata",
2698
+ "loadedmetadata",
2699
+ "canplay",
2700
+ "canplaythrough",
2701
+ "seeking",
2702
+ "seeked",
2703
+ "ratechange",
2704
+ "qualitychange",
2705
+ "statechange",
2706
+ "availabilitychange"
2707
+ ];
2708
+ for (const event of events) {
2709
+ this._player.on(event, (...args) => {
2710
+ this.dispatchEvent(new CustomEvent(event, {
2711
+ detail: args.length === 1 ? args[0] : args,
2712
+ bubbles: true,
2713
+ composed: true
2714
+ }));
2715
+ });
2716
+ }
2717
+ }
2718
+ }
2719
+ // src/elements/utils.ts
2720
+ function formatTime(seconds, guide = 0) {
2721
+ if (!isFinite(seconds) || isNaN(seconds))
2722
+ return "0:00";
2723
+ const s = Math.floor(seconds);
2724
+ const h = Math.floor(s / 3600);
2725
+ const m = Math.floor(s % 3600 / 60);
2726
+ const sec = s % 60;
2727
+ const showHours = h > 0 || guide >= 3600;
2728
+ if (showHours) {
2729
+ return `${h}:${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}`;
2730
+ }
2731
+ return `${m}:${sec.toString().padStart(2, "0")}`;
2732
+ }
2733
+ function formatTimePhrase(seconds) {
2734
+ if (!isFinite(seconds) || isNaN(seconds))
2735
+ return "0 seconds";
2736
+ const s = Math.floor(seconds);
2737
+ const h = Math.floor(s / 3600);
2738
+ const m = Math.floor(s % 3600 / 60);
2739
+ const sec = s % 60;
2740
+ const parts = [];
2741
+ if (h > 0)
2742
+ parts.push(`${h} ${h === 1 ? "hour" : "hours"}`);
2743
+ if (m > 0)
2744
+ parts.push(`${m} ${m === 1 ? "minute" : "minutes"}`);
2745
+ if (sec > 0 || parts.length === 0)
2746
+ parts.push(`${sec} ${sec === 1 ? "second" : "seconds"}`);
2747
+ return parts.join(", ");
2748
+ }
2749
+ function toISODuration(seconds) {
2750
+ if (!isFinite(seconds) || isNaN(seconds))
2751
+ return "PT0S";
2752
+ const s = Math.floor(seconds);
2753
+ const h = Math.floor(s / 3600);
2754
+ const m = Math.floor(s % 3600 / 60);
2755
+ const sec = s % 60;
2756
+ let result = "PT";
2757
+ if (h > 0)
2758
+ result += `${h}H`;
2759
+ if (m > 0)
2760
+ result += `${m}M`;
2761
+ if (sec > 0 || result === "PT")
2762
+ result += `${sec}S`;
2763
+ return result;
2764
+ }
2765
+ function resolvePlayer(el) {
2766
+ const forId = el.getAttribute("for");
2767
+ if (forId) {
2768
+ const target = document.getElementById(forId);
2769
+ return target?.player ?? null;
2770
+ }
2771
+ const host = el.closest("video-player");
2772
+ return host?.player ?? null;
2773
+ }
2774
+
2775
+ // src/elements/video-skin.ts
2776
+ class VideoSkin extends HTMLElementBase {
2777
+ _cleanups = [];
2778
+ connectedCallback() {
2779
+ if (!this.shadowRoot) {
2780
+ const shadow = this.attachShadow({ mode: "open" });
2781
+ shadow.innerHTML = `
2782
+ <style>
2783
+ :host {
2784
+ display: flex;
2785
+ flex-direction: column;
2786
+ position: absolute;
2787
+ inset: 0;
2788
+ z-index: 1;
2789
+ pointer-events: none;
2790
+ color: white;
2791
+ font-family: system-ui, -apple-system, sans-serif;
2792
+ font-size: 14px;
2793
+ }
2794
+
2795
+ /* Click-to-play overlay */
2796
+ .overlay {
2797
+ flex: 1;
2798
+ display: flex;
2799
+ align-items: center;
2800
+ justify-content: center;
2801
+ pointer-events: auto;
2802
+ }
2803
+
2804
+ /* Bottom controls bar */
2805
+ .controls {
2806
+ display: flex;
2807
+ align-items: center;
2808
+ gap: 4px;
2809
+ padding: 8px 12px;
2810
+ background: linear-gradient(transparent, rgba(0,0,0,0.7));
2811
+ pointer-events: auto;
2812
+ transition: opacity 0.3s, visibility 0.3s;
2813
+ }
2814
+
2815
+ :host([controls-hidden]) .controls {
2816
+ opacity: 0;
2817
+ visibility: hidden;
2818
+ }
2819
+
2820
+ /* Spacer pushes right-aligned controls */
2821
+ .spacer {
2822
+ flex: 1;
2823
+ }
2824
+
2825
+ /* Slots for left/center/right control groups */
2826
+ ::slotted(*) {
2827
+ pointer-events: auto;
2828
+ }
2829
+ </style>
2830
+ <div class="overlay" part="overlay">
2831
+ <slot name="overlay"></slot>
2832
+ </div>
2833
+ <div class="controls" part="controls">
2834
+ <slot name="left"></slot>
2835
+ <slot name="center"></slot>
2836
+ <div class="spacer"></div>
2837
+ <slot name="right"></slot>
2838
+ <slot></slot>
2839
+ </div>
2840
+ `;
2841
+ }
2842
+ queueMicrotask(() => this.attach());
2843
+ }
2844
+ disconnectedCallback() {
2845
+ this._cleanups.forEach((fn) => fn());
2846
+ this._cleanups = [];
2847
+ }
2848
+ attach() {
2849
+ const player = resolvePlayer(this);
2850
+ if (!player)
2851
+ return;
2852
+ const unsub = player.subscribe((state) => {
2853
+ if (state.controlsVisible) {
2854
+ this.removeAttribute("controls-hidden");
2855
+ } else {
2856
+ this.setAttribute("controls-hidden", "");
2857
+ }
2858
+ this.dataset.fullscreen = state.fullscreenAvailability;
2859
+ this.dataset.pip = state.pipAvailability;
2860
+ this.dataset.volume = state.volumeAvailability;
2861
+ this.dataset.captions = String(state.textTracks.length > 0);
2862
+ this.dataset.qualities = String(state.qualities.length > 0);
2863
+ });
2864
+ const overlay = this.shadowRoot.querySelector(".overlay");
2865
+ const onOverlayClick = () => player.togglePlay();
2866
+ overlay.addEventListener("click", onOverlayClick);
2867
+ this._cleanups.push(unsub, () => overlay.removeEventListener("click", onOverlayClick));
2868
+ }
2869
+ }
2870
+ // src/elements/media-play-button.ts
2871
+ var PLAY_ICON = "M8 5v14l11-7z";
2872
+ var PAUSE_ICON = "M6 19h4V5H6v14zm8-14v14h4V5h-4z";
2873
+
2874
+ class MediaPlayButton extends HTMLElementBase {
2875
+ _cleanup = null;
2876
+ _onClick = null;
2877
+ connectedCallback() {
2878
+ if (!this.shadowRoot) {
2879
+ const shadow = this.attachShadow({ mode: "open" });
2880
+ shadow.innerHTML = `
2881
+ <style>
2882
+ :host { display: inline-flex; }
2883
+ button {
2884
+ display: inline-flex; align-items: center; justify-content: center;
2885
+ width: 40px; height: 40px; padding: 8px;
2886
+ background: transparent; border: none; color: inherit; cursor: pointer;
2887
+ }
2888
+ button:hover { opacity: 0.8; }
2889
+ </style>
2890
+ <button part="button" type="button" aria-label="Play">
2891
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
2892
+ <path d="${PLAY_ICON}"/>
2893
+ </svg>
2894
+ </button>
2895
+ `;
2896
+ }
2897
+ queueMicrotask(() => this.attach());
2898
+ }
2899
+ disconnectedCallback() {
2900
+ const button = this.shadowRoot?.querySelector("button");
2901
+ if (button && this._onClick)
2902
+ button.removeEventListener("click", this._onClick);
2903
+ this._cleanup?.();
2904
+ this._cleanup = null;
2905
+ this._onClick = null;
2906
+ }
2907
+ attach() {
2908
+ const player = resolvePlayer(this);
2909
+ if (!player)
2910
+ return;
2911
+ const btn = this.shadowRoot.querySelector("button");
2912
+ this._onClick = () => player.togglePlay();
2913
+ btn.addEventListener("click", this._onClick);
2914
+ const unsub = player.subscribe("paused", (state) => {
2915
+ const paused = state.paused;
2916
+ btn.setAttribute("aria-label", paused ? "Play" : "Pause");
2917
+ btn.setAttribute("aria-pressed", String(!paused));
2918
+ const path = this.shadowRoot.querySelector("path");
2919
+ path.setAttribute("d", paused ? PLAY_ICON : PAUSE_ICON);
2920
+ });
2921
+ this._cleanup = unsub;
2922
+ }
2923
+ }
2924
+ // src/elements/media-mute-button.ts
2925
+ var ICON_MUTED = "M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51A8.796 8.796 0 0 0 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3 3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06a8.99 8.99 0 0 0 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4 9.91 6.09 12 8.18V4z";
2926
+ var ICON_LOW = "M18.5 12A4.5 4.5 0 0 0 16 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z";
2927
+ var ICON_HIGH = "M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z";
2928
+ function getVolumeIcon(muted, volume) {
2929
+ if (muted || volume === 0)
2930
+ return ICON_MUTED;
2931
+ if (volume < 0.5)
2932
+ return ICON_LOW;
2933
+ return ICON_HIGH;
2934
+ }
2935
+
2936
+ class MediaMuteButton extends HTMLElementBase {
2937
+ _cleanup = null;
2938
+ connectedCallback() {
2939
+ if (!this.shadowRoot) {
2940
+ const shadow = this.attachShadow({ mode: "open" });
2941
+ shadow.innerHTML = `
2942
+ <style>
2943
+ :host { display: inline-flex; }
2944
+ button {
2945
+ display: inline-flex; align-items: center; justify-content: center;
2946
+ width: 40px; height: 40px; padding: 8px;
2947
+ background: transparent; border: none; color: inherit; cursor: pointer;
2948
+ }
2949
+ button:hover { opacity: 0.8; }
2950
+ </style>
2951
+ <button part="button" type="button" aria-label="Mute">
2952
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
2953
+ <path d="${ICON_HIGH}"/>
2954
+ </svg>
2955
+ </button>
2956
+ `;
2957
+ }
2958
+ queueMicrotask(() => this.attach());
2959
+ }
2960
+ disconnectedCallback() {
2961
+ this._cleanup?.();
2962
+ this._cleanup = null;
2963
+ }
2964
+ attach() {
2965
+ const player = resolvePlayer(this);
2966
+ if (!player)
2967
+ return;
2968
+ const btn = this.shadowRoot.querySelector("button");
2969
+ btn.addEventListener("click", () => player.toggleMute());
2970
+ const unsub = player.subscribe((state) => {
2971
+ const icon = getVolumeIcon(state.muted, state.volume);
2972
+ btn.setAttribute("aria-label", state.muted ? "Unmute" : "Mute");
2973
+ const path = this.shadowRoot.querySelector("path");
2974
+ path.setAttribute("d", icon);
2975
+ });
2976
+ this._cleanup = unsub;
2977
+ }
2978
+ }
2979
+ // src/elements/media-fullscreen-button.ts
2980
+ var ENTER_FS = "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z";
2981
+ var EXIT_FS = "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z";
2982
+
2983
+ class MediaFullscreenButton extends HTMLElementBase {
2984
+ _cleanup = null;
2985
+ connectedCallback() {
2986
+ if (!this.shadowRoot) {
2987
+ const shadow = this.attachShadow({ mode: "open" });
2988
+ shadow.innerHTML = `
2989
+ <style>
2990
+ :host { display: inline-flex; }
2991
+ :host([hidden]) { display: none; }
2992
+ button {
2993
+ display: inline-flex; align-items: center; justify-content: center;
2994
+ width: 40px; height: 40px; padding: 8px;
2995
+ background: transparent; border: none; color: inherit; cursor: pointer;
2996
+ }
2997
+ button:hover { opacity: 0.8; }
2998
+ button:disabled { opacity: 0.4; cursor: not-allowed; }
2999
+ </style>
3000
+ <button part="button" type="button" aria-label="Enter fullscreen">
3001
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
3002
+ <path d="${ENTER_FS}"/>
3003
+ </svg>
3004
+ </button>
3005
+ `;
3006
+ }
3007
+ queueMicrotask(() => this.attach());
3008
+ }
3009
+ disconnectedCallback() {
3010
+ this._cleanup?.();
3011
+ this._cleanup = null;
3012
+ }
3013
+ attach() {
3014
+ const player = resolvePlayer(this);
3015
+ if (!player)
3016
+ return;
3017
+ const btn = this.shadowRoot.querySelector("button");
3018
+ btn.addEventListener("click", () => player.toggleFullscreen());
3019
+ const unsub = player.subscribe((state) => {
3020
+ const avail = state.fullscreenAvailability;
3021
+ if (avail === "unsupported") {
3022
+ this.setAttribute("hidden", "");
3023
+ } else {
3024
+ this.removeAttribute("hidden");
3025
+ }
3026
+ btn.disabled = avail !== "available";
3027
+ const fs = state.fullscreen;
3028
+ btn.setAttribute("aria-label", fs ? "Exit fullscreen" : "Enter fullscreen");
3029
+ const path = this.shadowRoot.querySelector("path");
3030
+ path.setAttribute("d", fs ? EXIT_FS : ENTER_FS);
3031
+ });
3032
+ this._cleanup = unsub;
3033
+ }
3034
+ }
3035
+ // src/elements/media-pip-button.ts
3036
+ var PIP_ICON = "M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z";
3037
+
3038
+ class MediaPipButton extends HTMLElementBase {
3039
+ _cleanup = null;
3040
+ connectedCallback() {
3041
+ if (!this.shadowRoot) {
3042
+ const shadow = this.attachShadow({ mode: "open" });
3043
+ shadow.innerHTML = `
3044
+ <style>
3045
+ :host { display: inline-flex; }
3046
+ :host([hidden]) { display: none; }
3047
+ button {
3048
+ display: inline-flex; align-items: center; justify-content: center;
3049
+ width: 40px; height: 40px; padding: 8px;
3050
+ background: transparent; border: none; color: inherit; cursor: pointer;
3051
+ }
3052
+ button:hover { opacity: 0.8; }
3053
+ button:disabled { opacity: 0.4; cursor: not-allowed; }
3054
+ </style>
3055
+ <button part="button" type="button" aria-label="Picture in picture">
3056
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
3057
+ <path d="${PIP_ICON}"/>
3058
+ </svg>
3059
+ </button>
3060
+ `;
3061
+ }
3062
+ queueMicrotask(() => this.attach());
3063
+ }
3064
+ disconnectedCallback() {
3065
+ this._cleanup?.();
3066
+ this._cleanup = null;
3067
+ }
3068
+ attach() {
3069
+ const player = resolvePlayer(this);
3070
+ if (!player)
3071
+ return;
3072
+ const btn = this.shadowRoot.querySelector("button");
3073
+ btn.addEventListener("click", () => player.togglePiP());
3074
+ const unsub = player.subscribe((state) => {
3075
+ const avail = state.pipAvailability;
3076
+ if (avail === "unsupported") {
3077
+ this.setAttribute("hidden", "");
3078
+ } else {
3079
+ this.removeAttribute("hidden");
3080
+ }
3081
+ btn.disabled = avail !== "available";
3082
+ });
3083
+ this._cleanup = unsub;
3084
+ }
3085
+ }
3086
+ // src/elements/media-time-display.ts
3087
+ class MediaTimeDisplay extends HTMLElementBase {
3088
+ _cleanup = null;
3089
+ _showRemaining = false;
3090
+ static get observedAttributes() {
3091
+ return ["type"];
3092
+ }
3093
+ get type() {
3094
+ return this.getAttribute("type") || "current";
3095
+ }
3096
+ connectedCallback() {
3097
+ if (!this.shadowRoot) {
3098
+ const shadow = this.attachShadow({ mode: "open" });
3099
+ shadow.innerHTML = `
3100
+ <style>
3101
+ :host {
3102
+ display: inline-flex;
3103
+ font-size: 14px;
3104
+ font-variant-numeric: tabular-nums;
3105
+ user-select: none;
3106
+ cursor: pointer;
3107
+ color: inherit;
3108
+ }
3109
+ time { color: inherit; }
3110
+ .separator { margin: 0 2px; }
3111
+ </style>
3112
+ <time part="time" datetime="PT0S" aria-valuetext="0 seconds">0:00</time>
3113
+ `;
3114
+ }
3115
+ this.addEventListener("click", this.handleClick);
3116
+ queueMicrotask(() => this.attach());
3117
+ }
3118
+ disconnectedCallback() {
3119
+ this._cleanup?.();
3120
+ this._cleanup = null;
3121
+ this.removeEventListener("click", this.handleClick);
3122
+ }
3123
+ handleClick = () => {
3124
+ if (this.type === "duration") {
3125
+ this._showRemaining = !this._showRemaining;
3126
+ const player = resolvePlayer(this);
3127
+ if (player)
3128
+ this.render(player.state);
3129
+ }
3130
+ };
3131
+ attach() {
3132
+ const player = resolvePlayer(this);
3133
+ if (!player)
3134
+ return;
3135
+ const unsub = player.subscribe((state) => {
3136
+ this.render(state);
3137
+ });
3138
+ this._cleanup = unsub;
3139
+ }
3140
+ render(state) {
3141
+ const timeEl = this.shadowRoot?.querySelector("time");
3142
+ if (!timeEl)
3143
+ return;
3144
+ const type = this.type;
3145
+ let seconds;
3146
+ if (type === "remaining" || type === "duration" && this._showRemaining) {
3147
+ seconds = Math.max(state.duration - state.currentTime, 0);
3148
+ const formatted = `-${formatTime(seconds, state.duration)}`;
3149
+ timeEl.textContent = formatted;
3150
+ } else if (type === "duration") {
3151
+ seconds = state.duration;
3152
+ timeEl.textContent = formatTime(seconds);
3153
+ } else {
3154
+ seconds = state.currentTime;
3155
+ timeEl.textContent = formatTime(seconds, state.duration);
3156
+ }
3157
+ timeEl.setAttribute("datetime", toISODuration(seconds));
3158
+ timeEl.setAttribute("aria-valuetext", formatTimePhrase(seconds));
3159
+ }
3160
+ }
3161
+ // src/elements/media-time-group.ts
3162
+ class MediaTimeGroup extends HTMLElementBase {
3163
+ connectedCallback() {
3164
+ if (!this.shadowRoot) {
3165
+ const shadow = this.attachShadow({ mode: "open" });
3166
+ shadow.innerHTML = `
3167
+ <style>
3168
+ :host {
3169
+ display: inline-flex;
3170
+ align-items: center;
3171
+ gap: 2px;
3172
+ font-variant-numeric: tabular-nums;
3173
+ font-family: inherit;
3174
+ font-size: inherit;
3175
+ color: inherit;
3176
+ }
3177
+ </style>
3178
+ <slot></slot>
3179
+ `;
3180
+ }
3181
+ }
3182
+ }
3183
+ // src/elements/media-time-separator.ts
3184
+ class MediaTimeSeparator extends HTMLElementBase {
3185
+ connectedCallback() {
3186
+ if (!this.shadowRoot) {
3187
+ const shadow = this.attachShadow({ mode: "open" });
3188
+ shadow.innerHTML = `
3189
+ <style>
3190
+ :host {
3191
+ display: inline-flex;
3192
+ align-items: center;
3193
+ padding: 0 2px;
3194
+ font-family: inherit;
3195
+ font-size: inherit;
3196
+ color: inherit;
3197
+ opacity: 0.7;
3198
+ }
3199
+ </style>
3200
+ <span aria-hidden="true" part="separator"><slot>/</slot></span>
3201
+ `;
3202
+ }
3203
+ this.setAttribute("role", "separator");
3204
+ this.setAttribute("aria-hidden", "true");
3205
+ }
3206
+ }
3207
+ // src/elements/media-preview-time.ts
3208
+ class MediaPreviewTime extends HTMLElementBase {
3209
+ _cleanups = [];
3210
+ connectedCallback() {
3211
+ if (!this.shadowRoot) {
3212
+ const shadow = this.attachShadow({ mode: "open" });
3213
+ shadow.innerHTML = `
3214
+ <style>
3215
+ :host {
3216
+ display: inline-block;
3217
+ font-variant-numeric: tabular-nums;
3218
+ font-family: monospace;
3219
+ font-size: 12px;
3220
+ color: inherit;
3221
+ pointer-events: none;
3222
+ }
3223
+ </style>
3224
+ <span part="value">0:00</span>
3225
+ `;
3226
+ }
3227
+ queueMicrotask(() => this.attach());
3228
+ }
3229
+ disconnectedCallback() {
3230
+ this._cleanups.forEach((fn) => fn());
3231
+ this._cleanups = [];
3232
+ }
3233
+ attach() {
3234
+ const player = resolvePlayer(this);
3235
+ if (!player)
3236
+ return;
3237
+ const progressBar = this.closest("media-progress-bar") || this.parentElement?.querySelector("media-progress-bar");
3238
+ if (progressBar) {
3239
+ const onPointerMove = (e) => {
3240
+ const pe = e;
3241
+ const track = progressBar.shadowRoot?.querySelector(".track") || progressBar;
3242
+ const rect = track.getBoundingClientRect();
3243
+ const percent = Math.max(0, Math.min(1, (pe.clientX - rect.left) / rect.width));
3244
+ const time = percent * (player.state.duration || 0);
3245
+ this.updateTime(time, player.state.duration);
3246
+ };
3247
+ progressBar.addEventListener("pointermove", onPointerMove);
3248
+ this._cleanups.push(() => progressBar.removeEventListener("pointermove", onPointerMove));
3249
+ }
3250
+ }
3251
+ updateTime(time, duration) {
3252
+ const span = this.shadowRoot?.querySelector("span");
3253
+ if (span) {
3254
+ span.textContent = formatTime(time, duration);
3255
+ }
3256
+ }
3257
+ }
3258
+ // src/elements/media-progress-bar.ts
3259
+ class MediaProgressBar extends HTMLElementBase {
3260
+ _cleanups = [];
3261
+ _isDragging = false;
3262
+ connectedCallback() {
3263
+ if (!this.shadowRoot) {
3264
+ const shadow = this.attachShadow({ mode: "open" });
3265
+ shadow.innerHTML = `
3266
+ <style>
3267
+ :host { display: flex; flex: 1; align-items: center; }
3268
+ .container {
3269
+ position: relative; width: 100%; height: 20px;
3270
+ cursor: pointer; touch-action: none;
3271
+ }
3272
+ .track {
3273
+ position: absolute; top: 50%; left: 0; right: 0;
3274
+ height: 4px; background: rgba(255,255,255,0.3);
3275
+ border-radius: 2px; transform: translateY(-50%);
3276
+ }
3277
+ :host([waveform]) .track { height: 20px; overflow: hidden; background: rgba(255,255,255,0.16); }
3278
+ .waveform { position: absolute; inset: 0; background-position: center; background-repeat: no-repeat; background-size: 100% 100%; opacity: 0.65; }
3279
+ .buffered {
3280
+ position: absolute; top: 0; left: 0;
3281
+ height: 100%; background: rgba(255,255,255,0.4);
3282
+ border-radius: 2px; width: 0%;
3283
+ }
3284
+ .fill {
3285
+ position: absolute; top: 0; left: 0;
3286
+ height: 100%; background: #fff;
3287
+ border-radius: 2px; width: 0%;
3288
+ }
3289
+ .thumb {
3290
+ position: absolute; top: 50%; left: 0%;
3291
+ width: 12px; height: 12px; background: #fff;
3292
+ border-radius: 50%; transform: translate(-50%, -50%);
3293
+ transition: transform 0.1s;
3294
+ }
3295
+ .container:hover .thumb { transform: translate(-50%, -50%) scale(1.2); }
3296
+ .tooltip {
3297
+ position: absolute; bottom: 100%; left: 0;
3298
+ transform: translateX(-50%);
3299
+ background: rgba(0,0,0,0.85); color: #fff;
3300
+ padding: 4px 8px; border-radius: 3px;
3301
+ font-size: 12px; font-family: monospace;
3302
+ white-space: nowrap; pointer-events: none;
3303
+ opacity: 0; transition: opacity 0.15s;
3304
+ margin-bottom: 8px;
3305
+ }
3306
+ .tooltip::after {
3307
+ content: ''; position: absolute;
3308
+ top: 100%; left: 50%; transform: translateX(-50%);
3309
+ border: 4px solid transparent;
3310
+ border-top-color: rgba(0,0,0,0.85);
3311
+ }
3312
+ .container:hover .tooltip { opacity: 1; }
3313
+ </style>
3314
+ <div class="container" part="container" role="slider"
3315
+ aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"
3316
+ aria-label="Seek" tabindex="0">
3317
+ <div class="tooltip" part="tooltip">0:00</div>
3318
+ <div class="track" part="track">
3319
+ <div class="waveform" part="waveform" aria-hidden="true"></div>
3320
+ <div class="buffered" part="buffered"></div>
3321
+ <div class="fill" part="fill"></div>
3322
+ <div class="thumb" part="thumb"></div>
3323
+ </div>
3324
+ </div>
3325
+ `;
3326
+ }
3327
+ queueMicrotask(() => this.attach());
3328
+ }
3329
+ disconnectedCallback() {
3330
+ this._cleanups.forEach((fn) => fn());
3331
+ this._cleanups = [];
3332
+ }
3333
+ attach() {
3334
+ const player = resolvePlayer(this);
3335
+ if (!player)
3336
+ return;
3337
+ const container = this.shadowRoot.querySelector(".container");
3338
+ const track = this.shadowRoot.querySelector(".track");
3339
+ const fill = this.shadowRoot.querySelector(".fill");
3340
+ const thumb = this.shadowRoot.querySelector(".thumb");
3341
+ const buffered = this.shadowRoot.querySelector(".buffered");
3342
+ const tooltip = this.shadowRoot.querySelector(".tooltip");
3343
+ const waveform = this.getAttribute("waveform");
3344
+ if (waveform) {
3345
+ const resolved = new URL(waveform, document.baseURI);
3346
+ if (["http:", "https:", "blob:"].includes(resolved.protocol) || resolved.href.startsWith("data:image/")) {
3347
+ const layer = this.shadowRoot.querySelector(".waveform");
3348
+ layer.style.backgroundImage = `url(${JSON.stringify(resolved.href)})`;
3349
+ }
3350
+ }
3351
+ const updateTooltip = (e) => {
3352
+ const rect = track.getBoundingClientRect();
3353
+ const percent = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
3354
+ const time = percent * (player.state.duration || 0);
3355
+ tooltip.textContent = formatTime(time, player.state.duration);
3356
+ const containerRect = container.getBoundingClientRect();
3357
+ const tooltipWidth = tooltip.offsetWidth;
3358
+ const rawLeft = e.clientX - containerRect.left;
3359
+ const minLeft = tooltipWidth / 2;
3360
+ const maxLeft = containerRect.width - tooltipWidth / 2;
3361
+ const clampedLeft = Math.max(minLeft, Math.min(maxLeft, rawLeft));
3362
+ tooltip.style.left = `${clampedLeft}px`;
3363
+ };
3364
+ const seekFromEvent = (e) => {
3365
+ const rect = track.getBoundingClientRect();
3366
+ const percent = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
3367
+ const time = percent * (player.state.duration || 0);
3368
+ player.seekTo(time);
3369
+ };
3370
+ const onPointerDown = (e) => {
3371
+ this._isDragging = true;
3372
+ container.setPointerCapture(e.pointerId);
3373
+ seekFromEvent(e);
3374
+ };
3375
+ const onPointerMove = (e) => {
3376
+ updateTooltip(e);
3377
+ if (!this._isDragging)
3378
+ return;
3379
+ seekFromEvent(e);
3380
+ };
3381
+ const onPointerUp = (e) => {
3382
+ if (!this._isDragging)
3383
+ return;
3384
+ this._isDragging = false;
3385
+ container.releasePointerCapture(e.pointerId);
3386
+ };
3387
+ const onKeyDown = (e) => {
3388
+ const step = 5;
3389
+ let newTime = player.state.currentTime;
3390
+ switch (e.key) {
3391
+ case "ArrowRight":
3392
+ newTime = Math.min(player.state.duration, newTime + step);
3393
+ break;
3394
+ case "ArrowLeft":
3395
+ newTime = Math.max(0, newTime - step);
3396
+ break;
3397
+ case "Home":
3398
+ newTime = 0;
3399
+ break;
3400
+ case "End":
3401
+ newTime = player.state.duration;
3402
+ break;
3403
+ default:
3404
+ return;
3405
+ }
3406
+ e.preventDefault();
3407
+ player.seekTo(newTime);
3408
+ };
3409
+ container.addEventListener("pointerdown", onPointerDown);
3410
+ container.addEventListener("pointermove", onPointerMove);
3411
+ container.addEventListener("pointerup", onPointerUp);
3412
+ container.addEventListener("keydown", onKeyDown);
3413
+ this._cleanups.push(() => container.removeEventListener("pointerdown", onPointerDown), () => container.removeEventListener("pointermove", onPointerMove), () => container.removeEventListener("pointerup", onPointerUp), () => container.removeEventListener("keydown", onKeyDown));
3414
+ const unsub = player.subscribe((state) => {
3415
+ if (this._isDragging)
3416
+ return;
3417
+ const duration = state.duration || 0;
3418
+ const percent = duration > 0 ? state.currentTime / duration * 100 : 0;
3419
+ fill.style.width = `${percent}%`;
3420
+ thumb.style.left = `${percent}%`;
3421
+ container.setAttribute("aria-valuenow", String(Math.round(percent)));
3422
+ container.setAttribute("aria-valuetext", `${formatTimePhrase(state.currentTime)} of ${formatTimePhrase(duration)}`);
3423
+ const bufferedPct = (state.bufferedAmount || 0) * 100;
3424
+ buffered.style.width = `${bufferedPct}%`;
3425
+ });
3426
+ this._cleanups.push(unsub);
3427
+ }
3428
+ }
3429
+ // src/elements/media-volume-slider.ts
3430
+ class MediaVolumeSlider extends HTMLElementBase {
3431
+ _cleanups = [];
3432
+ _isDragging = false;
3433
+ connectedCallback() {
3434
+ if (!this.shadowRoot) {
3435
+ const shadow = this.attachShadow({ mode: "open" });
3436
+ shadow.innerHTML = `
3437
+ <style>
3438
+ :host { display: inline-flex; align-items: center; width: 80px; }
3439
+ :host([hidden]) { display: none; }
3440
+ .container {
3441
+ position: relative; width: 100%; height: 20px;
3442
+ cursor: pointer; touch-action: none;
3443
+ }
3444
+ .track {
3445
+ position: absolute; top: 50%; left: 0; right: 0;
3446
+ height: 4px; background: rgba(255,255,255,0.3);
3447
+ border-radius: 2px; transform: translateY(-50%);
3448
+ }
3449
+ .fill {
3450
+ position: absolute; top: 0; left: 0;
3451
+ height: 100%; background: #fff;
3452
+ border-radius: 2px; width: 100%;
3453
+ }
3454
+ .thumb {
3455
+ position: absolute; top: 50%; left: 100%;
3456
+ width: 12px; height: 12px; background: #fff;
3457
+ border-radius: 50%; transform: translate(-50%, -50%);
3458
+ transition: transform 0.1s;
3459
+ }
3460
+ .container:hover .thumb { transform: translate(-50%, -50%) scale(1.2); }
3461
+ </style>
3462
+ <div class="container" part="container" role="slider"
3463
+ aria-valuemin="0" aria-valuemax="100" aria-valuenow="100"
3464
+ aria-label="Volume" tabindex="0">
3465
+ <div class="track" part="track">
3466
+ <div class="fill" part="fill"></div>
3467
+ <div class="thumb" part="thumb"></div>
3468
+ </div>
3469
+ </div>
3470
+ `;
3471
+ }
3472
+ queueMicrotask(() => this.attach());
3473
+ }
3474
+ disconnectedCallback() {
3475
+ this._cleanups.forEach((fn) => fn());
3476
+ this._cleanups = [];
3477
+ }
3478
+ attach() {
3479
+ const player = resolvePlayer(this);
3480
+ if (!player)
3481
+ return;
3482
+ const container = this.shadowRoot.querySelector(".container");
3483
+ const track = this.shadowRoot.querySelector(".track");
3484
+ const fill = this.shadowRoot.querySelector(".fill");
3485
+ const thumb = this.shadowRoot.querySelector(".thumb");
3486
+ const setFromEvent = (e) => {
3487
+ const rect = track.getBoundingClientRect();
3488
+ const volume = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
3489
+ player.setVolume(volume);
3490
+ if (volume > 0 && player.state.muted)
3491
+ player.setMuted(false);
3492
+ };
3493
+ const onPointerDown = (e) => {
3494
+ this._isDragging = true;
3495
+ container.setPointerCapture(e.pointerId);
3496
+ setFromEvent(e);
3497
+ };
3498
+ const onPointerMove = (e) => {
3499
+ if (!this._isDragging)
3500
+ return;
3501
+ setFromEvent(e);
3502
+ };
3503
+ const onPointerUp = (e) => {
3504
+ if (!this._isDragging)
3505
+ return;
3506
+ this._isDragging = false;
3507
+ container.releasePointerCapture(e.pointerId);
3508
+ };
3509
+ const onKeyDown = (e) => {
3510
+ let volume = player.state.volume;
3511
+ const step = 0.05;
3512
+ switch (e.key) {
3513
+ case "ArrowRight":
3514
+ case "ArrowUp":
3515
+ volume = Math.min(1, volume + step);
3516
+ break;
3517
+ case "ArrowLeft":
3518
+ case "ArrowDown":
3519
+ volume = Math.max(0, volume - step);
3520
+ break;
3521
+ default:
3522
+ return;
3523
+ }
3524
+ e.preventDefault();
3525
+ player.setVolume(volume);
3526
+ };
3527
+ container.addEventListener("pointerdown", onPointerDown);
3528
+ container.addEventListener("pointermove", onPointerMove);
3529
+ container.addEventListener("pointerup", onPointerUp);
3530
+ container.addEventListener("keydown", onKeyDown);
3531
+ this._cleanups.push(() => container.removeEventListener("pointerdown", onPointerDown), () => container.removeEventListener("pointermove", onPointerMove), () => container.removeEventListener("pointerup", onPointerUp), () => container.removeEventListener("keydown", onKeyDown));
3532
+ const unsub = player.subscribe((state) => {
3533
+ if (state.volumeAvailability === "unsupported") {
3534
+ this.setAttribute("hidden", "");
3535
+ return;
3536
+ }
3537
+ this.removeAttribute("hidden");
3538
+ const vol = state.muted ? 0 : state.volume;
3539
+ const percent = vol * 100;
3540
+ fill.style.width = `${percent}%`;
3541
+ thumb.style.left = `${percent}%`;
3542
+ container.setAttribute("aria-valuenow", String(Math.round(percent)));
3543
+ });
3544
+ this._cleanups.push(unsub);
3545
+ }
3546
+ }
3547
+ // src/core/popover.ts
3548
+ function computePosition(anchor, popover, options = {}) {
3549
+ const {
3550
+ placement = "top",
3551
+ offset = 4,
3552
+ boundary = null,
3553
+ padding = 8
3554
+ } = options;
3555
+ const anchorRect = anchor.getBoundingClientRect();
3556
+ const popoverRect = popover.getBoundingClientRect();
3557
+ const boundaryRect = boundary ? boundary.getBoundingClientRect() : { top: 0, left: 0, bottom: window.innerHeight, right: window.innerWidth, width: window.innerWidth, height: window.innerHeight };
3558
+ const availableTop = anchorRect.top - boundaryRect.top - padding;
3559
+ const availableBottom = boundaryRect.bottom - anchorRect.bottom - padding;
3560
+ let vertical;
3561
+ if (placement.startsWith("top")) {
3562
+ vertical = popoverRect.height + offset <= availableTop ? "top" : "bottom";
3563
+ } else {
3564
+ vertical = popoverRect.height + offset <= availableBottom ? "bottom" : "top";
3565
+ }
3566
+ let top;
3567
+ if (vertical === "top") {
3568
+ top = anchorRect.top - popoverRect.height - offset;
3569
+ } else {
3570
+ top = anchorRect.bottom + offset;
3571
+ }
3572
+ let align = "";
3573
+ if (placement.endsWith("-start"))
3574
+ align = "-start";
3575
+ else if (placement.endsWith("-end"))
3576
+ align = "-end";
3577
+ let left;
3578
+ switch (align) {
3579
+ case "-start":
3580
+ left = anchorRect.left;
3581
+ break;
3582
+ case "-end":
3583
+ left = anchorRect.right - popoverRect.width;
3584
+ break;
3585
+ default:
3586
+ left = anchorRect.left + (anchorRect.width - popoverRect.width) / 2;
3587
+ break;
3588
+ }
3589
+ const minLeft = boundaryRect.left + padding;
3590
+ const maxLeft = boundaryRect.right - popoverRect.width - padding;
3591
+ left = Math.max(minLeft, Math.min(maxLeft, left));
3592
+ const minTop = boundaryRect.top + padding;
3593
+ const maxTop = boundaryRect.bottom - popoverRect.height - padding;
3594
+ top = Math.max(minTop, Math.min(maxTop, top));
3595
+ const finalPlacement = `${vertical}${align}`;
3596
+ return { top, left, placement: finalPlacement };
3597
+ }
3598
+ function applyPosition(anchor, popover, options = {}) {
3599
+ const prevVisibility = popover.style.visibility;
3600
+ popover.style.visibility = "hidden";
3601
+ popover.style.display = "block";
3602
+ const result = computePosition(anchor, popover, options);
3603
+ const offsetParent = popover.offsetParent;
3604
+ if (offsetParent) {
3605
+ const parentRect = offsetParent.getBoundingClientRect();
3606
+ popover.style.top = `${result.top - parentRect.top}px`;
3607
+ popover.style.left = `${result.left - parentRect.left}px`;
3608
+ } else {
3609
+ popover.style.top = `${result.top}px`;
3610
+ popover.style.left = `${result.left}px`;
3611
+ }
3612
+ popover.style.visibility = prevVisibility;
3613
+ return result.placement;
3614
+ }
3615
+
3616
+ // src/elements/media-settings-menu.ts
3617
+ var GEAR_ICON = "M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.488.488 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z";
3618
+ var SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
3619
+
3620
+ class MediaSettingsMenu extends HTMLElementBase {
3621
+ _cleanup = null;
3622
+ _isOpen = false;
3623
+ _currentPanel = "main";
3624
+ connectedCallback() {
3625
+ if (!this.shadowRoot) {
3626
+ const shadow = this.attachShadow({ mode: "open" });
3627
+ shadow.innerHTML = `
3628
+ <style>
3629
+ :host { display: inline-flex; position: relative; }
3630
+ button.trigger {
3631
+ display: inline-flex; align-items: center; justify-content: center;
3632
+ width: 40px; height: 40px; padding: 8px;
3633
+ background: transparent; border: none; color: inherit; cursor: pointer;
3634
+ }
3635
+ button.trigger:hover { opacity: 0.8; }
3636
+ .menu {
3637
+ position: fixed; z-index: 9999;
3638
+ min-width: 160px; max-height: 250px; overflow-y: auto;
3639
+ background: rgba(28,28,28,0.95); border-radius: 4px;
3640
+ padding: 4px 0; opacity: 0; visibility: hidden;
3641
+ transform: translateY(4px);
3642
+ transition: opacity 0.2s, transform 0.2s, visibility 0.2s;
3643
+ }
3644
+ :host([open]) .menu {
3645
+ opacity: 1; visibility: visible; transform: translateY(0);
3646
+ }
3647
+ .item {
3648
+ display: flex; align-items: center; gap: 8px;
3649
+ width: 100%; padding: 8px 16px;
3650
+ background: none; border: none; color: white;
3651
+ text-align: left; cursor: pointer; font-size: 14px;
3652
+ }
3653
+ .item:hover { background: rgba(255,255,255,0.1); }
3654
+ .item[aria-checked="true"]::before { content: '\\2713'; }
3655
+ .item[aria-checked="false"]::before { content: ''; min-width: 12px; display: inline-block; }
3656
+ .back { font-weight: bold; border-bottom: 1px solid rgba(255,255,255,0.2); }
3657
+ </style>
3658
+ <button class="trigger" part="trigger" type="button"
3659
+ aria-label="Settings" aria-haspopup="true" aria-expanded="false">
3660
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
3661
+ <path d="${GEAR_ICON}"/>
3662
+ </svg>
3663
+ </button>
3664
+ <div class="menu" part="menu" role="menu" aria-hidden="true"></div>
3665
+ `;
3666
+ }
3667
+ const trigger = this.shadowRoot.querySelector(".trigger");
3668
+ this._onTriggerClick = () => this.toggle();
3669
+ trigger.addEventListener("click", this._onTriggerClick);
3670
+ this._onDocClick = (e) => {
3671
+ if (this._isOpen && !this.contains(e.target))
3672
+ this.close();
3673
+ };
3674
+ document.addEventListener("click", this._onDocClick);
3675
+ this._onDocKeydown = (e) => {
3676
+ if (e.key === "Escape" && this._isOpen)
3677
+ this.close();
3678
+ };
3679
+ document.addEventListener("keydown", this._onDocKeydown);
3680
+ queueMicrotask(() => this.attach());
3681
+ }
3682
+ _onTriggerClick = null;
3683
+ _onDocClick = null;
3684
+ _onDocKeydown = null;
3685
+ disconnectedCallback() {
3686
+ this._cleanup?.();
3687
+ this._cleanup = null;
3688
+ if (this._onTriggerClick) {
3689
+ this.shadowRoot?.querySelector(".trigger")?.removeEventListener("click", this._onTriggerClick);
3690
+ }
3691
+ if (this._onDocClick)
3692
+ document.removeEventListener("click", this._onDocClick);
3693
+ if (this._onDocKeydown)
3694
+ document.removeEventListener("keydown", this._onDocKeydown);
3695
+ }
3696
+ toggle() {
3697
+ if (this._isOpen)
3698
+ this.close();
3699
+ else
3700
+ this.open();
3701
+ }
3702
+ open() {
3703
+ this._isOpen = true;
3704
+ this._currentPanel = "main";
3705
+ this.setAttribute("open", "");
3706
+ this.shadowRoot.querySelector(".trigger").setAttribute("aria-expanded", "true");
3707
+ this.shadowRoot.querySelector(".menu").setAttribute("aria-hidden", "false");
3708
+ this.renderPanel();
3709
+ this.positionMenu();
3710
+ }
3711
+ close() {
3712
+ this._isOpen = false;
3713
+ this.removeAttribute("open");
3714
+ this.shadowRoot.querySelector(".trigger").setAttribute("aria-expanded", "false");
3715
+ this.shadowRoot.querySelector(".menu").setAttribute("aria-hidden", "true");
3716
+ }
3717
+ positionMenu() {
3718
+ const trigger = this.shadowRoot.querySelector(".trigger");
3719
+ const menu = this.shadowRoot.querySelector(".menu");
3720
+ if (!trigger || !menu)
3721
+ return;
3722
+ const playerEl = this.closest("video-player, .ts-video-player");
3723
+ const result = computePosition(trigger, menu, {
3724
+ placement: "top-end",
3725
+ offset: 4,
3726
+ boundary: playerEl,
3727
+ padding: 8
3728
+ });
3729
+ menu.style.top = `${result.top}px`;
3730
+ menu.style.left = `${result.left}px`;
3731
+ }
3732
+ attach() {
3733
+ const player = resolvePlayer(this);
3734
+ if (!player)
3735
+ return;
3736
+ const unsub = player.subscribe(() => {
3737
+ if (this._isOpen)
3738
+ this.renderPanel();
3739
+ });
3740
+ this._cleanup = unsub;
3741
+ }
3742
+ renderPanel() {
3743
+ const player = resolvePlayer(this);
3744
+ if (!player)
3745
+ return;
3746
+ const menu = this.shadowRoot.querySelector(".menu");
3747
+ menu.innerHTML = "";
3748
+ switch (this._currentPanel) {
3749
+ case "main":
3750
+ this.renderMain(menu, player);
3751
+ break;
3752
+ case "speed":
3753
+ this.renderSpeed(menu, player);
3754
+ break;
3755
+ case "quality":
3756
+ this.renderQuality(menu, player);
3757
+ break;
3758
+ case "captions":
3759
+ this.renderCaptions(menu, player);
3760
+ break;
3761
+ case "audio":
3762
+ this.renderAudio(menu, player);
3763
+ break;
3764
+ }
3765
+ if (this._isOpen) {
3766
+ queueMicrotask(() => this.positionMenu());
3767
+ }
3768
+ }
3769
+ renderMain(menu, player) {
3770
+ const state = player.state;
3771
+ this.addItem(menu, `Speed: ${state.playbackRate}x`, () => {
3772
+ this._currentPanel = "speed";
3773
+ this.renderPanel();
3774
+ });
3775
+ if (state.qualities.length > 0) {
3776
+ const current = state.qualities.find((q) => q.selected);
3777
+ const label = state.autoQuality ? "Auto" : current ? `${current.height}p` : "Auto";
3778
+ this.addItem(menu, `Quality: ${label}`, () => {
3779
+ this._currentPanel = "quality";
3780
+ this.renderPanel();
3781
+ });
3782
+ }
3783
+ if (state.textTracks.length > 0) {
3784
+ const showing = state.textTracks.find((t) => t.mode === "showing");
3785
+ this.addItem(menu, `Captions: ${showing?.label || "Off"}`, () => {
3786
+ this._currentPanel = "captions";
3787
+ this.renderPanel();
3788
+ });
3789
+ }
3790
+ if (state.audioTracks.length > 1) {
3791
+ const selected = state.audioTracks.find((track) => track.selected);
3792
+ this.addItem(menu, `Audio: ${selected?.label || "Default"}`, () => {
3793
+ this._currentPanel = "audio";
3794
+ this.renderPanel();
3795
+ });
3796
+ }
3797
+ }
3798
+ renderSpeed(menu, player) {
3799
+ this.addItem(menu, "Back", () => {
3800
+ this._currentPanel = "main";
3801
+ this.renderPanel();
3802
+ }, false, true);
3803
+ for (const speed of SPEEDS) {
3804
+ const checked = player.state.playbackRate === speed;
3805
+ this.addItem(menu, `${speed}x`, () => {
3806
+ player.setPlaybackRate(speed);
3807
+ this.close();
3808
+ }, checked);
3809
+ }
3810
+ }
3811
+ renderQuality(menu, player) {
3812
+ this.addItem(menu, "Back", () => {
3813
+ this._currentPanel = "main";
3814
+ this.renderPanel();
3815
+ }, false, true);
3816
+ this.addItem(menu, "Auto", () => {
3817
+ player.setQuality("auto");
3818
+ this.close();
3819
+ }, player.state.autoQuality);
3820
+ for (const q of player.state.qualities) {
3821
+ this.addItem(menu, `${q.height}p`, () => {
3822
+ player.setQuality(q);
3823
+ this.close();
3824
+ }, q.selected && !player.state.autoQuality);
3825
+ }
3826
+ }
3827
+ renderCaptions(menu, player) {
3828
+ this.addItem(menu, "Back", () => {
3829
+ this._currentPanel = "main";
3830
+ this.renderPanel();
3831
+ }, false, true);
3832
+ const anyShowing = player.state.textTracks.some((t) => t.mode === "showing");
3833
+ this.addItem(menu, "Off", () => {
3834
+ for (const t of player.state.textTracks) {
3835
+ if (t.mode === "showing")
3836
+ player.setTextTrack(t.id, "disabled");
3837
+ }
3838
+ this.close();
3839
+ }, !anyShowing);
3840
+ for (const track of player.state.textTracks) {
3841
+ if (track.kind === "subtitles" || track.kind === "captions") {
3842
+ this.addItem(menu, track.label, () => {
3843
+ player.setTextTrack(track.id, "showing");
3844
+ this.close();
3845
+ }, track.mode === "showing");
3846
+ }
3847
+ }
3848
+ }
3849
+ renderAudio(menu, player) {
3850
+ this.addItem(menu, "Back", () => {
3851
+ this._currentPanel = "main";
3852
+ this.renderPanel();
3853
+ }, false, true);
3854
+ for (const track of player.state.audioTracks) {
3855
+ this.addItem(menu, track.label || track.language || "Audio track", () => {
3856
+ player.setAudioTrack(track.id);
3857
+ this.close();
3858
+ }, track.selected);
3859
+ }
3860
+ }
3861
+ addItem(menu, label, onClick, checked = false, isBack = false) {
3862
+ const btn = document.createElement("button");
3863
+ btn.className = `item${isBack ? " back" : ""}`;
3864
+ btn.setAttribute("role", "menuitem");
3865
+ if (!isBack)
3866
+ btn.setAttribute("aria-checked", String(checked));
3867
+ btn.textContent = label;
3868
+ btn.addEventListener("click", onClick);
3869
+ menu.appendChild(btn);
3870
+ }
3871
+ }
3872
+ // src/elements/media-seek-button.ts
3873
+ class MediaSeekButton extends HTMLElementBase {
3874
+ cleanup = null;
3875
+ onClick = null;
3876
+ connectedCallback() {
3877
+ if (!this.shadowRoot) {
3878
+ const shadow = this.attachShadow({ mode: "open" });
3879
+ shadow.innerHTML = `
3880
+ <style>
3881
+ :host { display: inline-flex; }
3882
+ button { min-width: 40px; height: 40px; padding: 0 8px; border: 0; background: transparent; color: inherit; cursor: pointer; font: inherit; }
3883
+ button:hover { opacity: .8; }
3884
+ button:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
3885
+ button:disabled { cursor: default; opacity: .45; }
3886
+ </style>
3887
+ <button part="button" type="button"></button>
3888
+ `;
3889
+ }
3890
+ queueMicrotask(() => this.attach());
3891
+ }
3892
+ disconnectedCallback() {
3893
+ const button = this.shadowRoot?.querySelector("button");
3894
+ if (button && this.onClick)
3895
+ button.removeEventListener("click", this.onClick);
3896
+ this.cleanup?.();
3897
+ this.cleanup = null;
3898
+ this.onClick = null;
3899
+ }
3900
+ attach() {
3901
+ const player = resolvePlayer(this);
3902
+ const button = this.shadowRoot?.querySelector("button");
3903
+ if (!player || !button)
3904
+ return;
3905
+ const seconds = Number.parseFloat(this.getAttribute("seconds") ?? "10");
3906
+ const offset = Number.isFinite(seconds) && seconds !== 0 ? seconds : 10;
3907
+ const label = offset > 0 ? `Forward ${offset} seconds` : `Back ${Math.abs(offset)} seconds`;
3908
+ button.textContent = offset > 0 ? `+${offset}` : String(offset);
3909
+ button.setAttribute("aria-label", label);
3910
+ this.onClick = () => player.seekBy(offset);
3911
+ button.addEventListener("click", this.onClick);
3912
+ this.cleanup = player.subscribe((state) => {
3913
+ button.disabled = offset < 0 ? state.currentTime <= 0 : state.duration > 0 && state.currentTime >= state.duration;
3914
+ });
3915
+ }
3916
+ }
3917
+ // src/elements/media-playback-rate-button.ts
3918
+ var defaultRates = [0.5, 0.75, 1, 1.25, 1.5, 2];
3919
+
3920
+ class MediaPlaybackRateButton extends HTMLElementBase {
3921
+ cleanup = null;
3922
+ onClick = null;
3923
+ connectedCallback() {
3924
+ if (!this.shadowRoot) {
3925
+ const shadow = this.attachShadow({ mode: "open" });
3926
+ shadow.innerHTML = `
3927
+ <style>
3928
+ :host { display: inline-flex; }
3929
+ button { min-width: 42px; height: 40px; padding: 0 8px; border: 0; background: transparent; color: inherit; cursor: pointer; font: inherit; font-variant-numeric: tabular-nums; }
3930
+ button:hover { opacity: .8; }
3931
+ button:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
3932
+ </style>
3933
+ <button part="button" type="button" aria-label="Playback speed"></button>
3934
+ `;
3935
+ }
3936
+ queueMicrotask(() => this.attach());
3937
+ }
3938
+ disconnectedCallback() {
3939
+ const button = this.shadowRoot?.querySelector("button");
3940
+ if (button && this.onClick)
3941
+ button.removeEventListener("click", this.onClick);
3942
+ this.cleanup?.();
3943
+ this.cleanup = null;
3944
+ this.onClick = null;
3945
+ }
3946
+ getRates() {
3947
+ const rates = (this.getAttribute("rates") ?? "").split(",").map((value) => Number.parseFloat(value.trim())).filter((value) => Number.isFinite(value) && value >= 0.25 && value <= 4);
3948
+ return [...new Set(rates.length > 0 ? rates : defaultRates)].sort((a, b) => a - b);
3949
+ }
3950
+ attach() {
3951
+ const player = resolvePlayer(this);
3952
+ const button = this.shadowRoot?.querySelector("button");
3953
+ if (!player || !button)
3954
+ return;
3955
+ const rates = this.getRates();
3956
+ this.onClick = () => {
3957
+ const currentIndex = rates.findIndex((rate) => rate > player.state.playbackRate + 0.001);
3958
+ player.setPlaybackRate(rates[currentIndex < 0 ? 0 : currentIndex]);
3959
+ };
3960
+ button.addEventListener("click", this.onClick);
3961
+ this.cleanup = player.subscribe("playbackRate", (state) => {
3962
+ button.textContent = `${state.playbackRate}x`;
3963
+ button.setAttribute("aria-label", `Playback speed ${state.playbackRate}x`);
3964
+ });
3965
+ }
3966
+ }
3967
+ // src/elements/media-airplay-button.ts
3968
+ var AIRPLAY_ICON = "M6.2 19h11.6L12 13.2 6.2 19zM19 3H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h2v-2H5V5h14v10h-2v2h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z";
3969
+
3970
+ class MediaAirplayButton extends HTMLElementBase {
3971
+ cleanup = null;
3972
+ connectedCallback() {
3973
+ if (!this.shadowRoot) {
3974
+ const shadow = this.attachShadow({ mode: "open" });
3975
+ shadow.innerHTML = `
3976
+ <style>
3977
+ :host { display: none; }
3978
+ :host([available]) { display: inline-flex; }
3979
+ button { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; padding: 8px; background: transparent; border: 0; color: inherit; cursor: pointer; }
3980
+ button:hover { opacity: 0.8; }
3981
+ :host([connected]) button { color: var(--media-airplay-active-color, #38bdf8); }
3982
+ </style>
3983
+ <button part="button" type="button" aria-label="Play on AirPlay">
3984
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon"><path d="${AIRPLAY_ICON}"/></svg>
3985
+ </button>
3986
+ `;
3987
+ }
3988
+ queueMicrotask(() => this.attach());
3989
+ }
3990
+ disconnectedCallback() {
3991
+ this.cleanup?.();
3992
+ this.cleanup = null;
3993
+ }
3994
+ attach() {
3995
+ const player = resolvePlayer(this);
3996
+ const button = this.shadowRoot?.querySelector("button");
3997
+ if (!player || !button)
3998
+ return;
3999
+ let video = null;
4000
+ const availability = () => update();
4001
+ const update = () => {
4002
+ const media = player.provider?.mediaElement;
4003
+ const nextVideo = media instanceof HTMLVideoElement ? media : null;
4004
+ if (nextVideo !== video) {
4005
+ video?.removeEventListener("webkitplaybacktargetavailabilitychanged", availability);
4006
+ video?.removeEventListener("webkitcurrentplaybacktargetiswirelesschanged", availability);
4007
+ nextVideo?.addEventListener("webkitplaybacktargetavailabilitychanged", availability);
4008
+ nextVideo?.addEventListener("webkitcurrentplaybacktargetiswirelesschanged", availability);
4009
+ video = nextVideo;
4010
+ }
4011
+ const available = typeof video?.webkitShowPlaybackTargetPicker === "function";
4012
+ this.toggleAttribute("available", available);
4013
+ this.toggleAttribute("connected", video?.webkitCurrentPlaybackTargetIsWireless === true);
4014
+ button.setAttribute("aria-pressed", String(video?.webkitCurrentPlaybackTargetIsWireless === true));
4015
+ };
4016
+ const click = () => video?.webkitShowPlaybackTargetPicker?.();
4017
+ button.addEventListener("click", click);
4018
+ const unsubscribe = player.subscribe(update);
4019
+ update();
4020
+ this.cleanup = () => {
4021
+ unsubscribe();
4022
+ button.removeEventListener("click", click);
4023
+ video?.removeEventListener("webkitplaybacktargetavailabilitychanged", availability);
4024
+ video?.removeEventListener("webkitcurrentplaybacktargetiswirelesschanged", availability);
4025
+ };
4026
+ }
4027
+ }
4028
+ // src/elements/media-live-button.ts
4029
+ class MediaLiveButton extends HTMLElementBase {
4030
+ cleanup = null;
4031
+ connectedCallback() {
4032
+ if (!this.shadowRoot) {
4033
+ const shadow = this.attachShadow({ mode: "open" });
4034
+ shadow.innerHTML = `
4035
+ <style>
4036
+ :host { display: none; }
4037
+ :host([available]) { display: inline-flex; }
4038
+ button { display: inline-flex; align-items: center; gap: 6px; min-height: 40px; padding: 8px; background: transparent; border: 0; color: inherit; cursor: pointer; font: inherit; font-size: 12px; font-weight: 700; }
4039
+ i { width: 8px; height: 8px; border-radius: 9999px; background: #ef4444; }
4040
+ :host(:not([at-live-edge])) button { opacity: 0.72; }
4041
+ </style>
4042
+ <button part="button" type="button" aria-label="Go to live edge"><i aria-hidden="true"></i><span>LIVE</span></button>
4043
+ `;
4044
+ }
4045
+ queueMicrotask(() => this.attach());
4046
+ }
4047
+ disconnectedCallback() {
4048
+ this.cleanup?.();
4049
+ this.cleanup = null;
4050
+ }
4051
+ attach() {
4052
+ const player = resolvePlayer(this);
4053
+ const button = this.shadowRoot?.querySelector("button");
4054
+ if (!player || !button)
4055
+ return;
4056
+ const threshold = Math.max(0, Number(this.getAttribute("threshold") || 10));
4057
+ const update = () => {
4058
+ const state = player.state;
4059
+ const end = state.seekable.at(-1)?.end;
4060
+ const providerLive = player.provider?.isLive === true;
4061
+ const available = end !== undefined && (providerLive || !Number.isFinite(state.duration));
4062
+ const atLiveEdge = available && end - state.currentTime <= threshold;
4063
+ this.toggleAttribute("available", available);
4064
+ this.toggleAttribute("at-live-edge", atLiveEdge);
4065
+ button.setAttribute("aria-pressed", String(atLiveEdge));
4066
+ };
4067
+ const click = () => {
4068
+ const end = player.state.seekable.at(-1)?.end;
4069
+ if (end !== undefined) {
4070
+ player.seekTo(end);
4071
+ player.play();
4072
+ }
4073
+ };
4074
+ button.addEventListener("click", click);
4075
+ const unsubscribe = player.subscribe(update);
4076
+ update();
4077
+ this.cleanup = () => {
4078
+ unsubscribe();
4079
+ button.removeEventListener("click", click);
4080
+ };
4081
+ }
4082
+ }
4083
+ // src/elements/media-error-display.ts
4084
+ class MediaErrorDisplay extends HTMLElementBase {
4085
+ cleanup = null;
4086
+ connectedCallback() {
4087
+ if (!this.shadowRoot) {
4088
+ const shadow = this.attachShadow({ mode: "open" });
4089
+ shadow.innerHTML = `
4090
+ <style>
4091
+ :host { display: none; position: absolute; inset: 0; z-index: 5; place-items: center; padding: 24px; background: rgba(0, 0, 0, 0.76); color: white; text-align: center; }
4092
+ :host([open]) { display: grid; }
4093
+ p { max-width: 42rem; margin: 0; font: 500 14px/1.5 system-ui, sans-serif; }
4094
+ </style>
4095
+ <p part="message" role="alert" aria-live="assertive"></p>
4096
+ `;
4097
+ }
4098
+ queueMicrotask(() => this.attach());
4099
+ }
4100
+ disconnectedCallback() {
4101
+ this.cleanup?.();
4102
+ this.cleanup = null;
4103
+ }
4104
+ attach() {
4105
+ const player = resolvePlayer(this);
4106
+ const message = this.shadowRoot?.querySelector("p");
4107
+ if (!player || !message)
4108
+ return;
4109
+ const update = () => {
4110
+ const error = player.state.error;
4111
+ this.toggleAttribute("open", !!error);
4112
+ message.textContent = error?.message || "";
4113
+ if (error?.code !== undefined)
4114
+ this.setAttribute("data-code", String(error.code));
4115
+ else
4116
+ this.removeAttribute("data-code");
4117
+ };
4118
+ this.cleanup = player.subscribe(update);
4119
+ update();
4120
+ }
4121
+ }
4122
+ // src/elements/media-remote-playback-button.ts
4123
+ var REMOTE_ICON = "M1 18v3h3a3 3 0 0 0-3-3zm0-4v2a5 5 0 0 1 5 5h2a7 7 0 0 0-7-7zm0-4v2c5 0 9 4 9 9h2c0-6.1-4.9-11-11-11zm18-7H5a2 2 0 0 0-2 2v3h2V5h14v10h-5v2h5a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z";
4124
+
4125
+ class MediaRemotePlaybackButton extends HTMLElementBase {
4126
+ cleanup = null;
4127
+ connectedCallback() {
4128
+ if (!this.shadowRoot) {
4129
+ const shadow = this.attachShadow({ mode: "open" });
4130
+ shadow.innerHTML = `
4131
+ <style>
4132
+ :host { display: none; }
4133
+ :host([available]) { display: inline-flex; }
4134
+ button { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; padding: 8px; background: transparent; border: 0; color: inherit; cursor: pointer; }
4135
+ button:hover { opacity: 0.8; }
4136
+ :host([connected]) button { color: var(--media-remote-active-color, #38bdf8); }
4137
+ </style>
4138
+ <button part="button" type="button" aria-label="Play on another device">
4139
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon"><path d="${REMOTE_ICON}"/></svg>
4140
+ </button>
4141
+ `;
4142
+ }
4143
+ queueMicrotask(() => this.attach());
4144
+ }
4145
+ disconnectedCallback() {
4146
+ this.cleanup?.();
4147
+ this.cleanup = null;
4148
+ }
4149
+ attach() {
4150
+ const player = resolvePlayer(this);
4151
+ const button = this.shadowRoot?.querySelector("button");
4152
+ if (!player || !button)
4153
+ return;
4154
+ let remote = null;
4155
+ let watchId = null;
4156
+ let generation = 0;
4157
+ const stateChanged = () => {
4158
+ const connected = remote?.state === "connected";
4159
+ this.toggleAttribute("connected", connected);
4160
+ button.setAttribute("aria-pressed", String(connected));
4161
+ };
4162
+ const detachRemote = () => {
4163
+ generation++;
4164
+ remote?.removeEventListener("connecting", stateChanged);
4165
+ remote?.removeEventListener("connect", stateChanged);
4166
+ remote?.removeEventListener("disconnect", stateChanged);
4167
+ if (remote && watchId !== null)
4168
+ remote.cancelWatchAvailability(watchId).catch(() => {});
4169
+ remote = null;
4170
+ watchId = null;
4171
+ this.removeAttribute("available");
4172
+ this.removeAttribute("connected");
4173
+ };
4174
+ const update = () => {
4175
+ const media = player.provider?.mediaElement;
4176
+ const nextRemote = media instanceof HTMLMediaElement ? media.remote ?? null : null;
4177
+ if (nextRemote === remote)
4178
+ return;
4179
+ detachRemote();
4180
+ if (!nextRemote)
4181
+ return;
4182
+ remote = nextRemote;
4183
+ const currentGeneration = generation;
4184
+ remote.addEventListener("connecting", stateChanged);
4185
+ remote.addEventListener("connect", stateChanged);
4186
+ remote.addEventListener("disconnect", stateChanged);
4187
+ stateChanged();
4188
+ remote.watchAvailability((available) => {
4189
+ if (generation === currentGeneration && remote === nextRemote)
4190
+ this.toggleAttribute("available", available);
4191
+ }).then((id) => {
4192
+ if (generation === currentGeneration && remote === nextRemote)
4193
+ watchId = id;
4194
+ else
4195
+ nextRemote.cancelWatchAvailability(id).catch(() => {});
4196
+ }).catch(() => this.removeAttribute("available"));
4197
+ };
4198
+ const click = () => {
4199
+ remote?.prompt().catch(() => {});
4200
+ };
4201
+ button.addEventListener("click", click);
4202
+ const unsubscribe = player.subscribe(update);
4203
+ update();
4204
+ this.cleanup = () => {
4205
+ unsubscribe();
4206
+ button.removeEventListener("click", click);
4207
+ detachRemote();
4208
+ };
4209
+ }
4210
+ }
4211
+ // src/elements/index.ts
4212
+ var elements = [
4213
+ ["video-player", VideoPlayerElement],
4214
+ ["video-skin", VideoSkin],
4215
+ ["media-play-button", MediaPlayButton],
4216
+ ["media-mute-button", MediaMuteButton],
4217
+ ["media-fullscreen-button", MediaFullscreenButton],
4218
+ ["media-pip-button", MediaPipButton],
4219
+ ["media-time-display", MediaTimeDisplay],
4220
+ ["media-time-group", MediaTimeGroup],
4221
+ ["media-time-separator", MediaTimeSeparator],
4222
+ ["media-preview-time", MediaPreviewTime],
4223
+ ["media-progress-bar", MediaProgressBar],
4224
+ ["media-volume-slider", MediaVolumeSlider],
4225
+ ["media-settings-menu", MediaSettingsMenu],
4226
+ ["media-seek-button", MediaSeekButton],
4227
+ ["media-playback-rate-button", MediaPlaybackRateButton],
4228
+ ["media-airplay-button", MediaAirplayButton],
4229
+ ["media-live-button", MediaLiveButton],
4230
+ ["media-error-display", MediaErrorDisplay],
4231
+ ["media-remote-playback-button", MediaRemotePlaybackButton]
4232
+ ];
4233
+ function registerElements() {
4234
+ for (const [name, ctor] of elements) {
4235
+ if (!customElements.get(name)) {
4236
+ customElements.define(name, ctor);
4237
+ }
4238
+ }
4239
+ }
4240
+
4241
+ export { isMediaSessionSupported, createMediaSession, updateMetadata, isOrientationLockSupported, lockOrientation, unlockOrientation, BaseProvider, isHTML5Source, HTML5Provider, html5Loader, extractYouTubeId, isYouTubeSource, YouTubeProvider, youtubeLoader, extractVimeoId, extractVimeoHash, isVimeoSource, VimeoProvider, vimeoLoader, isHLSSource, isNativeHLSSupported, HLSProvider, hlsLoader, isDASHSource, DASHProvider, dashLoader, defaultLoaders, findLoader, detectMediaType, getPreconnectHints, Player, createPlayer, getPlayer, VideoPlayerElement, formatTime, formatTimePhrase, toISODuration, resolvePlayer, VideoSkin, MediaPlayButton, MediaMuteButton, MediaFullscreenButton, MediaPipButton, MediaTimeDisplay, MediaTimeGroup, MediaTimeSeparator, MediaPreviewTime, MediaProgressBar, MediaVolumeSlider, computePosition, applyPosition, MediaSettingsMenu, MediaSeekButton, MediaPlaybackRateButton, MediaAirplayButton, MediaLiveButton, MediaErrorDisplay, MediaRemotePlaybackButton, registerElements };
4242
+
4243
+ //# debugId=BE17C381D5DC647964756E2164756E21