@locpd/vidstack 1.12.14

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 (242) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -0
  3. package/analyze.json.d.ts +8 -0
  4. package/bundle.d.ts +1 -0
  5. package/cdn/chunks/vidstack-2f5gzOW6.js +1 -0
  6. package/cdn/chunks/vidstack-BYgY9wmd.js +1 -0
  7. package/cdn/chunks/vidstack-BfBBPhXV.js +1 -0
  8. package/cdn/chunks/vidstack-Bjo5esRp.js +1 -0
  9. package/cdn/chunks/vidstack-BuL67v3q.js +1 -0
  10. package/cdn/chunks/vidstack-C0msPRTd.js +3 -0
  11. package/cdn/chunks/vidstack-CJNLoJPa.js +1 -0
  12. package/cdn/chunks/vidstack-CQSpZ7X8.js +16 -0
  13. package/cdn/chunks/vidstack-C_AxqLKV.js +1 -0
  14. package/cdn/chunks/vidstack-CioT3Yw2.js +1 -0
  15. package/cdn/chunks/vidstack-CrqkytHl.js +1 -0
  16. package/cdn/chunks/vidstack-D0M8R0ZU.js +1 -0
  17. package/cdn/chunks/vidstack-D40FSa5B.js +3 -0
  18. package/cdn/chunks/vidstack-DD2JwFVU.js +1 -0
  19. package/cdn/chunks/vidstack-DRH_1tFW.js +1 -0
  20. package/cdn/chunks/vidstack-DfDZuHNP.js +1 -0
  21. package/cdn/chunks/vidstack-DiNS2Vx5.js +1 -0
  22. package/cdn/chunks/vidstack-xjJ-ui_l.js +1 -0
  23. package/cdn/providers/vidstack-audio-2Dt_Ivbp.js +1 -0
  24. package/cdn/providers/vidstack-dash-CUtD4e6q.js +1 -0
  25. package/cdn/providers/vidstack-google-cast-BdORATUX.js +1 -0
  26. package/cdn/providers/vidstack-hls-R25Kb6DP.js +1 -0
  27. package/cdn/providers/vidstack-html-DaAUJYsD.js +1 -0
  28. package/cdn/providers/vidstack-video-Csvox7SO.js +1 -0
  29. package/cdn/providers/vidstack-vimeo-D4Z96kg2.js +1 -0
  30. package/cdn/providers/vidstack-youtube-DiND6h3s.js +1 -0
  31. package/cdn/vidstack.js +1 -0
  32. package/cdn/with-layouts/chunks/vidstack-2f5gzOW6.js +1 -0
  33. package/cdn/with-layouts/chunks/vidstack-45yH5los.js +1 -0
  34. package/cdn/with-layouts/chunks/vidstack-BBVMdOnf.js +1 -0
  35. package/cdn/with-layouts/chunks/vidstack-BB_ulI_T.js +1 -0
  36. package/cdn/with-layouts/chunks/vidstack-BcAewM33.js +1 -0
  37. package/cdn/with-layouts/chunks/vidstack-BfBBPhXV.js +1 -0
  38. package/cdn/with-layouts/chunks/vidstack-Bxv1Qnxe.js +1 -0
  39. package/cdn/with-layouts/chunks/vidstack-C2ZbG62f.js +3 -0
  40. package/cdn/with-layouts/chunks/vidstack-CCYIOKgL.js +1 -0
  41. package/cdn/with-layouts/chunks/vidstack-CL6PeIO1.js +1 -0
  42. package/cdn/with-layouts/chunks/vidstack-C_AxqLKV.js +1 -0
  43. package/cdn/with-layouts/chunks/vidstack-CifDkwDH.js +795 -0
  44. package/cdn/with-layouts/chunks/vidstack-Cry7aD59.js +3 -0
  45. package/cdn/with-layouts/chunks/vidstack-D065okCn.js +1 -0
  46. package/cdn/with-layouts/chunks/vidstack-DGuMoXmI.js +1 -0
  47. package/cdn/with-layouts/chunks/vidstack-DRH_1tFW.js +1 -0
  48. package/cdn/with-layouts/chunks/vidstack-DVBs1XoQ.js +1 -0
  49. package/cdn/with-layouts/chunks/vidstack-Dge3KT8k.js +1 -0
  50. package/cdn/with-layouts/chunks/vidstack-DiNS2Vx5.js +1 -0
  51. package/cdn/with-layouts/chunks/vidstack-HvYfJoen.js +1 -0
  52. package/cdn/with-layouts/providers/vidstack-audio-DE5vKIzW.js +1 -0
  53. package/cdn/with-layouts/providers/vidstack-dash-CA2agUuZ.js +1 -0
  54. package/cdn/with-layouts/providers/vidstack-google-cast-CGs-t8HM.js +1 -0
  55. package/cdn/with-layouts/providers/vidstack-hls-BHMbMFFR.js +1 -0
  56. package/cdn/with-layouts/providers/vidstack-html-Dm9gmNk6.js +1 -0
  57. package/cdn/with-layouts/providers/vidstack-video-C5it_Lbl.js +1 -0
  58. package/cdn/with-layouts/providers/vidstack-vimeo-BabLn9sy.js +1 -0
  59. package/cdn/with-layouts/providers/vidstack-youtube-D8UlccUL.js +1 -0
  60. package/cdn/with-layouts/vidstack.js +1 -0
  61. package/dev/chunks/vidstack-B7Zi3v_O.js +104 -0
  62. package/dev/chunks/vidstack-BFg1ZqiG.js +91 -0
  63. package/dev/chunks/vidstack-BGB2pa9s.js +58 -0
  64. package/dev/chunks/vidstack-BaIbHZE3.js +1519 -0
  65. package/dev/chunks/vidstack-Bb2rASIc.js +5188 -0
  66. package/dev/chunks/vidstack-Bcmx8pmK.js +224 -0
  67. package/dev/chunks/vidstack-Bl4b0Nen.js +29 -0
  68. package/dev/chunks/vidstack-Bo5OTJ06.js +58 -0
  69. package/dev/chunks/vidstack-BoAGnlRt.js +58 -0
  70. package/dev/chunks/vidstack-Bpr4fI4n.js +7 -0
  71. package/dev/chunks/vidstack-Bt8MP2DK.js +204 -0
  72. package/dev/chunks/vidstack-Bu2kfzUd.js +1637 -0
  73. package/dev/chunks/vidstack-C-ffXlSV.js +2995 -0
  74. package/dev/chunks/vidstack-C-ztJq-f.js +109 -0
  75. package/dev/chunks/vidstack-CFNlaVTR.js +55 -0
  76. package/dev/chunks/vidstack-C_l97D5j.js +254 -0
  77. package/dev/chunks/vidstack-CjhKISI0.js +114 -0
  78. package/dev/chunks/vidstack-CofXIJAy.js +57 -0
  79. package/dev/chunks/vidstack-CwTj4H1w.js +18 -0
  80. package/dev/chunks/vidstack-DDwbYVHV.js +66 -0
  81. package/dev/chunks/vidstack-DFImIcIL.js +11 -0
  82. package/dev/chunks/vidstack-DGDvUbvO.js +33 -0
  83. package/dev/chunks/vidstack-DO0kqA99.js +107 -0
  84. package/dev/chunks/vidstack-DXxIKXmd.js +50 -0
  85. package/dev/chunks/vidstack-DajrMUR0.js +297 -0
  86. package/dev/chunks/vidstack-DbBJlz7I.js +10 -0
  87. package/dev/chunks/vidstack-Dihypf8P.js +11 -0
  88. package/dev/chunks/vidstack-DlAhl87f.js +1193 -0
  89. package/dev/chunks/vidstack-Dm1xEU9Q.js +34 -0
  90. package/dev/chunks/vidstack-Dv_LIPFu.js +14 -0
  91. package/dev/chunks/vidstack-igYn0Apa.js +254 -0
  92. package/dev/chunks/vidstack-krOAtKMi.js +32 -0
  93. package/dev/chunks/vidstack-qh1N5_f_.js +26 -0
  94. package/dev/chunks/vidstack-rB-wqXw1.js +107 -0
  95. package/dev/chunks/vidstack-zG6PIeGg.js +66 -0
  96. package/dev/define/plyr-layout.js +51 -0
  97. package/dev/define/templates/plyr-layout.js +571 -0
  98. package/dev/define/templates/vidstack-audio-layout.js +167 -0
  99. package/dev/define/templates/vidstack-video-layout.js +390 -0
  100. package/dev/define/vidstack-icons.js +1 -0
  101. package/dev/define/vidstack-player-default-layout.js +21 -0
  102. package/dev/define/vidstack-player-layouts.js +25 -0
  103. package/dev/define/vidstack-player-ui.js +70 -0
  104. package/dev/define/vidstack-player.js +19 -0
  105. package/dev/global/plyr.js +501 -0
  106. package/dev/global/vidstack-player.js +129 -0
  107. package/dev/providers/vidstack-audio.js +35 -0
  108. package/dev/providers/vidstack-dash.js +516 -0
  109. package/dev/providers/vidstack-google-cast.js +474 -0
  110. package/dev/providers/vidstack-hls.js +408 -0
  111. package/dev/providers/vidstack-html.js +567 -0
  112. package/dev/providers/vidstack-video.js +207 -0
  113. package/dev/providers/vidstack-vimeo.js +554 -0
  114. package/dev/providers/vidstack-youtube.js +286 -0
  115. package/dev/vidstack-elements.js +36 -0
  116. package/dev/vidstack.js +91 -0
  117. package/dom.d.ts +91 -0
  118. package/elements.d.ts +1433 -0
  119. package/empty.vtt +1 -0
  120. package/global/player.d.ts +52 -0
  121. package/global/plyr.d.ts +343 -0
  122. package/google-cast.d.ts +1422 -0
  123. package/icons.d.ts +1 -0
  124. package/index.d.ts +402 -0
  125. package/package.json +199 -0
  126. package/player/index.d.ts +3 -0
  127. package/player/layouts/default.d.ts +3 -0
  128. package/player/layouts/index.d.ts +3 -0
  129. package/player/layouts/plyr.d.ts +3 -0
  130. package/player/styles/base.css +153 -0
  131. package/player/styles/default/buffering.css +55 -0
  132. package/player/styles/default/buttons.css +175 -0
  133. package/player/styles/default/captions.css +181 -0
  134. package/player/styles/default/chapter-title.css +26 -0
  135. package/player/styles/default/controls.css +56 -0
  136. package/player/styles/default/gestures.css +19 -0
  137. package/player/styles/default/icons.css +6 -0
  138. package/player/styles/default/keyboard.css +148 -0
  139. package/player/styles/default/layouts/audio.css +417 -0
  140. package/player/styles/default/layouts/video.css +590 -0
  141. package/player/styles/default/menus.css +959 -0
  142. package/player/styles/default/poster.css +52 -0
  143. package/player/styles/default/sliders.css +391 -0
  144. package/player/styles/default/theme.css +2461 -0
  145. package/player/styles/default/thumbnail.css +40 -0
  146. package/player/styles/default/time.css +45 -0
  147. package/player/styles/default/tooltips.css +141 -0
  148. package/player/styles/plyr/theme.css +1237 -0
  149. package/player/ui.d.ts +3 -0
  150. package/plugins.d.ts +19 -0
  151. package/plugins.js +13 -0
  152. package/prod/chunks/vidstack-B01xzxC4.js +7 -0
  153. package/prod/chunks/vidstack-BCeb7ryV.js +201 -0
  154. package/prod/chunks/vidstack-BGSTndAW.js +1590 -0
  155. package/prod/chunks/vidstack-BPitBBjh.js +1519 -0
  156. package/prod/chunks/vidstack-BQlOPwOu.js +45 -0
  157. package/prod/chunks/vidstack-BSDzlwxO.js +4778 -0
  158. package/prod/chunks/vidstack-BT0m6zEi.js +109 -0
  159. package/prod/chunks/vidstack-BTigPj2h.js +55 -0
  160. package/prod/chunks/vidstack-BiyXcJ_M.js +107 -0
  161. package/prod/chunks/vidstack-BoVf5n1M.js +2985 -0
  162. package/prod/chunks/vidstack-Bq6c3Bam.js +58 -0
  163. package/prod/chunks/vidstack-ByLCIBtB.js +297 -0
  164. package/prod/chunks/vidstack-C2US-gSO.js +248 -0
  165. package/prod/chunks/vidstack-C9vIqaYT.js +10 -0
  166. package/prod/chunks/vidstack-CF6fixCQ.js +1193 -0
  167. package/prod/chunks/vidstack-CTojmhKq.js +66 -0
  168. package/prod/chunks/vidstack-ChQTHmIQ.js +77 -0
  169. package/prod/chunks/vidstack-Cm6_unwd.js +246 -0
  170. package/prod/chunks/vidstack-CwTj4H1w.js +18 -0
  171. package/prod/chunks/vidstack-D3ltXc3a.js +33 -0
  172. package/prod/chunks/vidstack-D5EzK014.js +14 -0
  173. package/prod/chunks/vidstack-DDXt6fpN.js +58 -0
  174. package/prod/chunks/vidstack-DJDnh4xT.js +11 -0
  175. package/prod/chunks/vidstack-DXxIKXmd.js +50 -0
  176. package/prod/chunks/vidstack-D_-9AA6_.js +29 -0
  177. package/prod/chunks/vidstack-DbkZGjSn.js +107 -0
  178. package/prod/chunks/vidstack-Dihypf8P.js +11 -0
  179. package/prod/chunks/vidstack-Dm1xEU9Q.js +34 -0
  180. package/prod/chunks/vidstack-Dq5Yu0Vr.js +205 -0
  181. package/prod/chunks/vidstack-DqAw8m9J.js +26 -0
  182. package/prod/chunks/vidstack-DsPOyKtl.js +57 -0
  183. package/prod/chunks/vidstack-krOAtKMi.js +32 -0
  184. package/prod/chunks/vidstack-nLyr4NEP.js +58 -0
  185. package/prod/chunks/vidstack-xMS8dnYq.js +114 -0
  186. package/prod/chunks/vidstack-yEGTpgeA.js +104 -0
  187. package/prod/define/plyr-layout.js +51 -0
  188. package/prod/define/templates/plyr-layout.js +571 -0
  189. package/prod/define/templates/vidstack-audio-layout.js +167 -0
  190. package/prod/define/templates/vidstack-video-layout.js +390 -0
  191. package/prod/define/vidstack-icons.js +1 -0
  192. package/prod/define/vidstack-player-default-layout.js +21 -0
  193. package/prod/define/vidstack-player-layouts.js +25 -0
  194. package/prod/define/vidstack-player-ui.js +70 -0
  195. package/prod/define/vidstack-player.js +19 -0
  196. package/prod/global/plyr.js +493 -0
  197. package/prod/global/vidstack-player.js +129 -0
  198. package/prod/providers/vidstack-audio.js +35 -0
  199. package/prod/providers/vidstack-dash.js +501 -0
  200. package/prod/providers/vidstack-google-cast.js +468 -0
  201. package/prod/providers/vidstack-hls.js +393 -0
  202. package/prod/providers/vidstack-html.js +555 -0
  203. package/prod/providers/vidstack-video.js +204 -0
  204. package/prod/providers/vidstack-vimeo.js +548 -0
  205. package/prod/providers/vidstack-youtube.js +286 -0
  206. package/prod/vidstack-elements.js +36 -0
  207. package/prod/vidstack.js +158 -0
  208. package/server/chunks/vidstack-6juFdkKy.js +29 -0
  209. package/server/chunks/vidstack-B7iHmv7_.js +307 -0
  210. package/server/chunks/vidstack-BmxyML9v.js +1619 -0
  211. package/server/chunks/vidstack-BskfxwD3.js +566 -0
  212. package/server/chunks/vidstack-BvLV0SMz.js +4642 -0
  213. package/server/chunks/vidstack-BvWwluXZ.js +205 -0
  214. package/server/chunks/vidstack-C-413dj2.js +8 -0
  215. package/server/chunks/vidstack-C26K8z_-.js +55 -0
  216. package/server/chunks/vidstack-CJJiksDz.js +107 -0
  217. package/server/chunks/vidstack-CUNv52x1.js +141 -0
  218. package/server/chunks/vidstack-CqyBCODe.js +295 -0
  219. package/server/chunks/vidstack-CwTj4H1w.js +18 -0
  220. package/server/chunks/vidstack-DHAyGSOl.js +1502 -0
  221. package/server/chunks/vidstack-DLU3cjcp.js +381 -0
  222. package/server/chunks/vidstack-DXxIKXmd.js +50 -0
  223. package/server/chunks/vidstack-DbtDXDS2.js +104 -0
  224. package/server/chunks/vidstack-Dm1xEU9Q.js +34 -0
  225. package/server/chunks/vidstack-DzTHw_bw.js +207 -0
  226. package/server/chunks/vidstack-Wn3NH5Sg.js +1566 -0
  227. package/server/chunks/vidstack-krOAtKMi.js +32 -0
  228. package/server/chunks/vidstack-wNViAkr4.js +3045 -0
  229. package/server/define/plyr-layout.js +16 -0
  230. package/server/define/vidstack-icons.js +1 -0
  231. package/server/define/vidstack-player-default-layout.js +13 -0
  232. package/server/define/vidstack-player-layouts.js +16 -0
  233. package/server/define/vidstack-player-ui.js +11 -0
  234. package/server/define/vidstack-player.js +6 -0
  235. package/server/global/plyr.js +322 -0
  236. package/server/global/vidstack-player.js +58 -0
  237. package/server/vidstack-elements.js +46 -0
  238. package/server/vidstack.js +148 -0
  239. package/tailwind.cjs +101 -0
  240. package/tailwind.d.cts +15 -0
  241. package/types/vidstack-BOvzfZjK.d.ts +1269 -0
  242. package/types/vidstack-Cttpg6GU.d.ts +7474 -0
@@ -0,0 +1,4642 @@
1
+ import { EventsTarget, DOMEvent, fscreen, ViewController, EventsController, onDispose, waitTimeout, signal, listenEvent, peek, isString, isNumber, isArray, isUndefined, State, tick, Component, functionThrottle, effect, untrack, functionDebounce, isKeyboardClick, isKeyboardEvent, deferredPromise, prop, method, provideContext, animationFrameThrottle, uppercaseFirstChar, setAttribute, camelToKebabCase, computed, scoped, noop, setStyle } from './vidstack-Wn3NH5Sg.js';
2
+ import { isHTMLElement, isTouchPinchEvent, isAudioSrc, isVideoSrc, isHLSSrc, isDASHSrc, preconnect, mediaContext, setAttributeIfEmpty, getRequestCredentials, useMediaContext } from './vidstack-DLU3cjcp.js';
3
+ import { isTrackCaptionKind, TextTrackSymbol, TextTrack, coerceToError, FocusVisibleController, clampNumber } from './vidstack-B7iHmv7_.js';
4
+
5
+ const ADD = Symbol(0), REMOVE = Symbol(0), RESET = Symbol(0), SELECT = Symbol(0), READONLY = Symbol(0), SET_READONLY = Symbol(0), ON_RESET = Symbol(0), ON_REMOVE = Symbol(0), ON_USER_SELECT = Symbol(0);
6
+ const ListSymbol = {
7
+ add: ADD,
8
+ remove: REMOVE,
9
+ reset: RESET,
10
+ select: SELECT,
11
+ readonly: READONLY,
12
+ setReadonly: SET_READONLY,
13
+ onReset: ON_RESET,
14
+ onRemove: ON_REMOVE,
15
+ onUserSelect: ON_USER_SELECT
16
+ };
17
+
18
+ class List extends EventsTarget {
19
+ items = [];
20
+ /** @internal */
21
+ [ListSymbol.readonly] = false;
22
+ get length() {
23
+ return this.items.length;
24
+ }
25
+ get readonly() {
26
+ return this[ListSymbol.readonly];
27
+ }
28
+ /**
29
+ * Returns the index of the first occurrence of the given item, or -1 if it is not present.
30
+ */
31
+ indexOf(item) {
32
+ return this.items.indexOf(item);
33
+ }
34
+ /**
35
+ * Returns an item matching the given `id`, or `null` if not present.
36
+ */
37
+ getById(id) {
38
+ if (id === "") return null;
39
+ return this.items.find((item) => item.id === id) ?? null;
40
+ }
41
+ /**
42
+ * Transform list to an array.
43
+ */
44
+ toArray() {
45
+ return [...this.items];
46
+ }
47
+ [Symbol.iterator]() {
48
+ return this.items.values();
49
+ }
50
+ /** @internal */
51
+ [ListSymbol.add](item, trigger) {
52
+ const index = this.items.length;
53
+ if (!("" + index in this)) {
54
+ Object.defineProperty(this, index, {
55
+ get() {
56
+ return this.items[index];
57
+ }
58
+ });
59
+ }
60
+ if (this.items.includes(item)) return;
61
+ this.items.push(item);
62
+ this.dispatchEvent(new DOMEvent("add", { detail: item, trigger }));
63
+ }
64
+ /** @internal */
65
+ [ListSymbol.remove](item, trigger) {
66
+ const index = this.items.indexOf(item);
67
+ if (index >= 0) {
68
+ this[ListSymbol.onRemove]?.(item, trigger);
69
+ this.items.splice(index, 1);
70
+ this.dispatchEvent(new DOMEvent("remove", { detail: item, trigger }));
71
+ }
72
+ }
73
+ /** @internal */
74
+ [ListSymbol.reset](trigger) {
75
+ for (const item of [...this.items]) this[ListSymbol.remove](item, trigger);
76
+ this.items = [];
77
+ this[ListSymbol.setReadonly](false, trigger);
78
+ this[ListSymbol.onReset]?.();
79
+ }
80
+ /** @internal */
81
+ [ListSymbol.setReadonly](readonly, trigger) {
82
+ if (this[ListSymbol.readonly] === readonly) return;
83
+ this[ListSymbol.readonly] = readonly;
84
+ this.dispatchEvent(new DOMEvent("readonly-change", { detail: readonly, trigger }));
85
+ }
86
+ }
87
+
88
+ const CAN_FULLSCREEN = fscreen.fullscreenEnabled;
89
+ class FullscreenController extends ViewController {
90
+ /**
91
+ * Tracks whether we're the active fullscreen event listener. Fullscreen events can only be
92
+ * listened to globally on the document so we need to know if they relate to the current host
93
+ * element or not.
94
+ */
95
+ #listening = false;
96
+ #active = false;
97
+ get active() {
98
+ return this.#active;
99
+ }
100
+ get supported() {
101
+ return CAN_FULLSCREEN;
102
+ }
103
+ onConnect() {
104
+ new EventsController(fscreen).add("fullscreenchange", this.#onChange.bind(this)).add("fullscreenerror", this.#onError.bind(this));
105
+ onDispose(this.#onDisconnect.bind(this));
106
+ }
107
+ async #onDisconnect() {
108
+ if (CAN_FULLSCREEN) await this.exit();
109
+ }
110
+ #onChange(event) {
111
+ const active = isFullscreen(this.el);
112
+ if (active === this.#active) return;
113
+ if (!active) this.#listening = false;
114
+ this.#active = active;
115
+ this.dispatch("fullscreen-change", { detail: active, trigger: event });
116
+ }
117
+ #onError(event) {
118
+ if (!this.#listening) return;
119
+ this.dispatch("fullscreen-error", { detail: null, trigger: event });
120
+ this.#listening = false;
121
+ }
122
+ async enter() {
123
+ try {
124
+ this.#listening = true;
125
+ if (!this.el || isFullscreen(this.el)) return;
126
+ assertFullscreenAPI();
127
+ return fscreen.requestFullscreen(this.el);
128
+ } catch (error) {
129
+ this.#listening = false;
130
+ throw error;
131
+ }
132
+ }
133
+ async exit() {
134
+ if (!this.el || !isFullscreen(this.el)) return;
135
+ assertFullscreenAPI();
136
+ return fscreen.exitFullscreen();
137
+ }
138
+ }
139
+ function canFullscreen() {
140
+ return CAN_FULLSCREEN;
141
+ }
142
+ function isFullscreen(host) {
143
+ if (fscreen.fullscreenElement === host) return true;
144
+ try {
145
+ return host.matches(
146
+ // @ts-expect-error - `fullscreenPseudoClass` is missing from `@types/fscreen`.
147
+ fscreen.fullscreenPseudoClass
148
+ );
149
+ } catch (error) {
150
+ return false;
151
+ }
152
+ }
153
+ function assertFullscreenAPI() {
154
+ if (CAN_FULLSCREEN) return;
155
+ throw Error(
156
+ "[vidstack] no fullscreen API"
157
+ );
158
+ }
159
+
160
+ const IS_IPHONE = false;
161
+ const IS_CHROME = false;
162
+ function canOrientScreen() {
163
+ return canRotateScreen();
164
+ }
165
+ function canRotateScreen() {
166
+ return false;
167
+ }
168
+ function canPlayVideoType(video, type) {
169
+ return false;
170
+ }
171
+ function canPlayHLSNatively(video) {
172
+ return false;
173
+ }
174
+ function canUsePictureInPicture(video) {
175
+ return false;
176
+ }
177
+ function canUseVideoPresentation(video) {
178
+ return false;
179
+ }
180
+ async function canChangeVolume() {
181
+ const video = document.createElement("video");
182
+ video.volume = 0.5;
183
+ await waitTimeout(0);
184
+ return video.volume === 0.5;
185
+ }
186
+ function isHLSSupported() {
187
+ return false;
188
+ }
189
+ function isDASHSupported() {
190
+ return isHLSSupported();
191
+ }
192
+
193
+ class ScreenOrientationController extends ViewController {
194
+ #type = signal(this.#getScreenOrientation());
195
+ #locked = signal(false);
196
+ #currentLock;
197
+ /**
198
+ * The current screen orientation type.
199
+ *
200
+ * @signal
201
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation}
202
+ * @see https://w3c.github.io/screen-orientation/#screen-orientation-types-and-locks
203
+ */
204
+ get type() {
205
+ return this.#type();
206
+ }
207
+ /**
208
+ * Whether the screen orientation is currently locked.
209
+ *
210
+ * @signal
211
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation}
212
+ * @see https://w3c.github.io/screen-orientation/#screen-orientation-types-and-locks
213
+ */
214
+ get locked() {
215
+ return this.#locked();
216
+ }
217
+ /**
218
+ * Whether the viewport is in a portrait orientation.
219
+ *
220
+ * @signal
221
+ */
222
+ get portrait() {
223
+ return this.#type().startsWith("portrait");
224
+ }
225
+ /**
226
+ * Whether the viewport is in a landscape orientation.
227
+ *
228
+ * @signal
229
+ */
230
+ get landscape() {
231
+ return this.#type().startsWith("landscape");
232
+ }
233
+ /**
234
+ * Whether the native Screen Orientation API is available.
235
+ */
236
+ static supported = canOrientScreen();
237
+ /**
238
+ * Whether the native Screen Orientation API is available.
239
+ */
240
+ get supported() {
241
+ return ScreenOrientationController.supported;
242
+ }
243
+ onConnect() {
244
+ if (this.supported) {
245
+ listenEvent(screen.orientation, "change", this.#onOrientationChange.bind(this));
246
+ } else {
247
+ const query = window.matchMedia("(orientation: landscape)");
248
+ query.onchange = this.#onOrientationChange.bind(this);
249
+ onDispose(() => query.onchange = null);
250
+ }
251
+ onDispose(this.#onDisconnect.bind(this));
252
+ }
253
+ async #onDisconnect() {
254
+ if (this.supported && this.#locked()) await this.unlock();
255
+ }
256
+ #onOrientationChange(event) {
257
+ this.#type.set(this.#getScreenOrientation());
258
+ this.dispatch("orientation-change", {
259
+ detail: {
260
+ orientation: peek(this.#type),
261
+ lock: this.#currentLock
262
+ },
263
+ trigger: event
264
+ });
265
+ }
266
+ /**
267
+ * Locks the orientation of the screen to the desired orientation type using the
268
+ * Screen Orientation API.
269
+ *
270
+ * @param lockType - The screen lock orientation type.
271
+ * @throws Error - If screen orientation API is unavailable.
272
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Screen/orientation}
273
+ * @see {@link https://w3c.github.io/screen-orientation}
274
+ */
275
+ async lock(lockType) {
276
+ if (peek(this.#locked) || this.#currentLock === lockType) return;
277
+ this.#assertScreenOrientationAPI();
278
+ await screen.orientation.lock(lockType);
279
+ this.#locked.set(true);
280
+ this.#currentLock = lockType;
281
+ }
282
+ /**
283
+ * Unlocks the orientation of the screen to it's default state using the Screen Orientation
284
+ * API. This method will throw an error if the API is unavailable.
285
+ *
286
+ * @throws Error - If screen orientation API is unavailable.
287
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Screen/orientation}
288
+ * @see {@link https://w3c.github.io/screen-orientation}
289
+ */
290
+ async unlock() {
291
+ if (!peek(this.#locked)) return;
292
+ this.#assertScreenOrientationAPI();
293
+ this.#currentLock = void 0;
294
+ await screen.orientation.unlock();
295
+ this.#locked.set(false);
296
+ }
297
+ #assertScreenOrientationAPI() {
298
+ if (this.supported) return;
299
+ throw Error(
300
+ "[vidstack] no orientation API"
301
+ );
302
+ }
303
+ #getScreenOrientation() {
304
+ return "portrait-primary";
305
+ }
306
+ }
307
+
308
+ function isVideoQualitySrc(src) {
309
+ return !isString(src) && "width" in src && "height" in src && isNumber(src.width) && isNumber(src.height);
310
+ }
311
+
312
+ class TimeRange {
313
+ #ranges;
314
+ get length() {
315
+ return this.#ranges.length;
316
+ }
317
+ constructor(start, end) {
318
+ if (isArray(start)) {
319
+ this.#ranges = start;
320
+ } else if (!isUndefined(start) && !isUndefined(end)) {
321
+ this.#ranges = [[start, end]];
322
+ } else {
323
+ this.#ranges = [];
324
+ }
325
+ }
326
+ start(index) {
327
+ return this.#ranges[index][0] ?? Infinity;
328
+ }
329
+ end(index) {
330
+ return this.#ranges[index][1] ?? Infinity;
331
+ }
332
+ }
333
+ function getTimeRangesStart(range) {
334
+ if (!range.length) return null;
335
+ let min = range.start(0);
336
+ for (let i = 1; i < range.length; i++) {
337
+ const value = range.start(i);
338
+ if (value < min) min = value;
339
+ }
340
+ return min;
341
+ }
342
+ function getTimeRangesEnd(range) {
343
+ if (!range.length) return null;
344
+ let max = range.end(0);
345
+ for (let i = 1; i < range.length; i++) {
346
+ const value = range.end(i);
347
+ if (value > max) max = value;
348
+ }
349
+ return max;
350
+ }
351
+ function normalizeTimeIntervals(intervals) {
352
+ if (intervals.length <= 1) {
353
+ return intervals;
354
+ }
355
+ intervals.sort((a, b) => a[0] - b[0]);
356
+ let normalized = [], current = intervals[0];
357
+ for (let i = 1; i < intervals.length; i++) {
358
+ const next = intervals[i];
359
+ if (current[1] >= next[0] - 1) {
360
+ current = [current[0], Math.max(current[1], next[1])];
361
+ } else {
362
+ normalized.push(current);
363
+ current = next;
364
+ }
365
+ }
366
+ normalized.push(current);
367
+ return normalized;
368
+ }
369
+ function updateTimeIntervals(intervals, interval, value) {
370
+ let start = interval[0], end = interval[1];
371
+ if (value < start) {
372
+ return [value, -1];
373
+ } else if (value === start) {
374
+ return interval;
375
+ } else if (start === -1) {
376
+ interval[0] = value;
377
+ return interval;
378
+ } else if (value > start) {
379
+ interval[1] = value;
380
+ if (end === -1) intervals.push(interval);
381
+ }
382
+ normalizeTimeIntervals(intervals);
383
+ return interval;
384
+ }
385
+
386
+ const mediaState = new State({
387
+ artist: "",
388
+ artwork: null,
389
+ audioTrack: null,
390
+ audioTracks: [],
391
+ autoPlay: false,
392
+ autoPlayError: null,
393
+ audioGain: null,
394
+ buffered: new TimeRange(),
395
+ canLoad: false,
396
+ canLoadPoster: false,
397
+ canFullscreen: false,
398
+ canOrientScreen: canOrientScreen(),
399
+ canPictureInPicture: false,
400
+ canPlay: false,
401
+ clipStartTime: 0,
402
+ clipEndTime: 0,
403
+ controls: false,
404
+ get iOSControls() {
405
+ return IS_IPHONE;
406
+ },
407
+ get nativeControls() {
408
+ return this.controls || this.iOSControls;
409
+ },
410
+ controlsVisible: false,
411
+ get controlsHidden() {
412
+ return !this.controlsVisible;
413
+ },
414
+ crossOrigin: null,
415
+ ended: false,
416
+ error: null,
417
+ fullscreen: false,
418
+ get loop() {
419
+ return this.providedLoop || this.userPrefersLoop;
420
+ },
421
+ logLevel: "silent",
422
+ mediaType: "unknown",
423
+ muted: false,
424
+ paused: true,
425
+ played: new TimeRange(),
426
+ playing: false,
427
+ playsInline: false,
428
+ pictureInPicture: false,
429
+ preload: "metadata",
430
+ playbackRate: 1,
431
+ qualities: [],
432
+ quality: null,
433
+ autoQuality: false,
434
+ canSetQuality: true,
435
+ canSetPlaybackRate: true,
436
+ canSetVolume: false,
437
+ canSetAudioGain: false,
438
+ seekable: new TimeRange(),
439
+ seeking: false,
440
+ source: { src: "", type: "" },
441
+ sources: [],
442
+ started: false,
443
+ textTracks: [],
444
+ textTrack: null,
445
+ get hasCaptions() {
446
+ return this.textTracks.filter(isTrackCaptionKind).length > 0;
447
+ },
448
+ volume: 1,
449
+ waiting: false,
450
+ realCurrentTime: 0,
451
+ get currentTime() {
452
+ return this.ended ? this.duration : this.clipStartTime > 0 ? Math.max(0, Math.min(this.realCurrentTime - this.clipStartTime, this.duration)) : this.realCurrentTime;
453
+ },
454
+ providedDuration: -1,
455
+ intrinsicDuration: 0,
456
+ get duration() {
457
+ return this.seekableWindow;
458
+ },
459
+ get title() {
460
+ return this.providedTitle || this.inferredTitle;
461
+ },
462
+ get poster() {
463
+ return this.providedPoster || this.inferredPoster;
464
+ },
465
+ get viewType() {
466
+ return this.providedViewType !== "unknown" ? this.providedViewType : this.inferredViewType;
467
+ },
468
+ get streamType() {
469
+ return this.providedStreamType !== "unknown" ? this.providedStreamType : this.inferredStreamType;
470
+ },
471
+ get currentSrc() {
472
+ return this.source;
473
+ },
474
+ get bufferedStart() {
475
+ const start = getTimeRangesStart(this.buffered) ?? 0;
476
+ return Math.max(start, this.clipStartTime);
477
+ },
478
+ get bufferedEnd() {
479
+ const end = getTimeRangesEnd(this.buffered) ?? 0;
480
+ return Math.min(this.seekableEnd, Math.max(0, end - this.clipStartTime));
481
+ },
482
+ get bufferedWindow() {
483
+ return Math.max(0, this.bufferedEnd - this.bufferedStart);
484
+ },
485
+ get seekableStart() {
486
+ if (this.isLiveDVR && this.liveDVRWindow > 0) {
487
+ return Math.max(0, this.seekableEnd - this.liveDVRWindow);
488
+ }
489
+ const start = getTimeRangesStart(this.seekable) ?? 0;
490
+ return Math.max(start, this.clipStartTime);
491
+ },
492
+ get seekableEnd() {
493
+ if (this.providedDuration > 0) return this.providedDuration;
494
+ const end = this.liveSyncPosition > 0 ? this.liveSyncPosition : this.canPlay ? getTimeRangesEnd(this.seekable) ?? Infinity : 0;
495
+ return this.clipEndTime > 0 ? Math.min(this.clipEndTime, end) : end;
496
+ },
497
+ get seekableWindow() {
498
+ const window = this.seekableEnd - this.seekableStart;
499
+ return !isNaN(window) ? Math.max(0, window) : Infinity;
500
+ },
501
+ // ~~ remote playback ~~
502
+ canAirPlay: false,
503
+ canGoogleCast: false,
504
+ remotePlaybackState: "disconnected",
505
+ remotePlaybackType: "none",
506
+ remotePlaybackLoader: null,
507
+ remotePlaybackInfo: null,
508
+ get isAirPlayConnected() {
509
+ return this.remotePlaybackType === "airplay" && this.remotePlaybackState === "connected";
510
+ },
511
+ get isGoogleCastConnected() {
512
+ return this.remotePlaybackType === "google-cast" && this.remotePlaybackState === "connected";
513
+ },
514
+ // ~~ responsive design ~~
515
+ pointer: "fine",
516
+ orientation: "landscape",
517
+ width: 0,
518
+ height: 0,
519
+ mediaWidth: 0,
520
+ mediaHeight: 0,
521
+ lastKeyboardAction: null,
522
+ // ~~ user props ~~
523
+ userBehindLiveEdge: false,
524
+ // ~~ live props ~~
525
+ liveEdgeTolerance: 10,
526
+ minLiveDVRWindow: 60,
527
+ get canSeek() {
528
+ return /unknown|on-demand|:dvr/.test(this.streamType) && Number.isFinite(this.duration) && (!this.isLiveDVR || this.duration >= this.liveDVRWindow);
529
+ },
530
+ get live() {
531
+ return this.streamType.includes("live") || !Number.isFinite(this.duration);
532
+ },
533
+ get liveEdgeStart() {
534
+ return this.live && Number.isFinite(this.seekableEnd) ? Math.max(0, this.seekableEnd - this.liveEdgeTolerance) : 0;
535
+ },
536
+ get liveEdge() {
537
+ return this.live && (!this.canSeek || !this.userBehindLiveEdge && this.currentTime >= this.liveEdgeStart);
538
+ },
539
+ get liveEdgeWindow() {
540
+ return this.live && Number.isFinite(this.seekableEnd) ? this.seekableEnd - this.liveEdgeStart : 0;
541
+ },
542
+ get isLiveDVR() {
543
+ return /:dvr/.test(this.streamType);
544
+ },
545
+ get liveDVRWindow() {
546
+ return Math.max(this.inferredLiveDVRWindow, this.minLiveDVRWindow);
547
+ },
548
+ // ~~ internal props ~~
549
+ autoPlaying: false,
550
+ providedTitle: "",
551
+ inferredTitle: "",
552
+ providedLoop: false,
553
+ userPrefersLoop: false,
554
+ userPrefersDualCaptionSeparation: false,
555
+ providedPoster: "",
556
+ inferredPoster: "",
557
+ inferredViewType: "unknown",
558
+ providedViewType: "unknown",
559
+ providedStreamType: "unknown",
560
+ inferredStreamType: "unknown",
561
+ liveSyncPosition: null,
562
+ inferredLiveDVRWindow: 0,
563
+ savedState: null
564
+ });
565
+ const RESET_ON_SRC_QUALITY_CHANGE = /* @__PURE__ */ new Set([
566
+ "autoPlayError",
567
+ "autoPlaying",
568
+ "buffered",
569
+ "canPlay",
570
+ "error",
571
+ "paused",
572
+ "played",
573
+ "playing",
574
+ "seekable",
575
+ "seeking",
576
+ "waiting"
577
+ ]);
578
+ const RESET_ON_SRC_CHANGE = /* @__PURE__ */ new Set([
579
+ ...RESET_ON_SRC_QUALITY_CHANGE,
580
+ "ended",
581
+ "inferredPoster",
582
+ "inferredStreamType",
583
+ "inferredTitle",
584
+ "intrinsicDuration",
585
+ "inferredLiveDVRWindow",
586
+ "liveSyncPosition",
587
+ "realCurrentTime",
588
+ "savedState",
589
+ "started",
590
+ "userBehindLiveEdge"
591
+ ]);
592
+ function softResetMediaState($media, isSourceQualityChange = false) {
593
+ const filter = isSourceQualityChange ? RESET_ON_SRC_QUALITY_CHANGE : RESET_ON_SRC_CHANGE;
594
+ mediaState.reset($media, (prop) => filter.has(prop));
595
+ tick();
596
+ }
597
+ function boundTime(time, store) {
598
+ const clippedTime = time + store.clipStartTime(), isStart = Math.floor(time) === Math.floor(store.seekableStart()), isEnd = Math.floor(clippedTime) === Math.floor(store.seekableEnd());
599
+ if (isStart) {
600
+ return store.seekableStart();
601
+ }
602
+ if (isEnd) {
603
+ return store.seekableEnd();
604
+ }
605
+ if (store.isLiveDVR() && store.liveDVRWindow() > 0 && clippedTime < store.seekableEnd() - store.liveDVRWindow()) {
606
+ return store.bufferedStart();
607
+ }
608
+ return Math.min(Math.max(store.seekableStart() + 0.1, clippedTime), store.seekableEnd() - 0.1);
609
+ }
610
+
611
+ class MediaRemoteControl {
612
+ #target = null;
613
+ #player = null;
614
+ #prevTrackIndex = -1;
615
+ #logger;
616
+ constructor(logger = void 0) {
617
+ this.#logger = logger;
618
+ }
619
+ /**
620
+ * Set the target from which to dispatch media requests events from. The events should bubble
621
+ * up from this target to the player element.
622
+ *
623
+ * @example
624
+ * ```ts
625
+ * const button = document.querySelector('button');
626
+ * remote.setTarget(button);
627
+ * ```
628
+ */
629
+ setTarget(target) {
630
+ this.#target = target;
631
+ }
632
+ /**
633
+ * Returns the current player element. This method will attempt to find the player by
634
+ * searching up from either the given `target` or default target set via `remote.setTarget`.
635
+ *
636
+ * @example
637
+ * ```ts
638
+ * const player = remote.getPlayer();
639
+ * ```
640
+ */
641
+ getPlayer(target) {
642
+ if (this.#player) return this.#player;
643
+ (target ?? this.#target)?.dispatchEvent(
644
+ new DOMEvent("find-media-player", {
645
+ detail: (player) => void (this.#player = player),
646
+ bubbles: true,
647
+ composed: true
648
+ })
649
+ );
650
+ return this.#player;
651
+ }
652
+ /**
653
+ * Set the current player element so the remote can support toggle methods such as
654
+ * `togglePaused` as they rely on the current media state.
655
+ */
656
+ setPlayer(player) {
657
+ this.#player = player;
658
+ }
659
+ /**
660
+ * Dispatch a request to start the media loading process. This will only work if the media
661
+ * player has been initialized with a custom loading strategy `load="custom">`.
662
+ *
663
+ * @docs {@link https://www.vidstack.io/docs/player/core-concepts/loading#load-strategies}
664
+ */
665
+ startLoading(trigger) {
666
+ this.#dispatchRequest("media-start-loading", trigger);
667
+ }
668
+ /**
669
+ * Dispatch a request to start the poster loading process. This will only work if the media
670
+ * player has been initialized with a custom poster loading strategy `posterLoad="custom">`.
671
+ *
672
+ * @docs {@link https://www.vidstack.io/docs/player/core-concepts/loading#load-strategies}
673
+ */
674
+ startLoadingPoster(trigger) {
675
+ this.#dispatchRequest("media-poster-start-loading", trigger);
676
+ }
677
+ /**
678
+ * Dispatch a request to connect to AirPlay.
679
+ *
680
+ * @see {@link https://www.apple.com/au/airplay}
681
+ */
682
+ requestAirPlay(trigger) {
683
+ this.#dispatchRequest("media-airplay-request", trigger);
684
+ }
685
+ /**
686
+ * Dispatch a request to connect to Google Cast.
687
+ *
688
+ * @see {@link https://developers.google.com/cast/docs/overview}
689
+ */
690
+ requestGoogleCast(trigger) {
691
+ this.#dispatchRequest("media-google-cast-request", trigger);
692
+ }
693
+ /**
694
+ * Dispatch a request to begin/resume media playback.
695
+ */
696
+ play(trigger) {
697
+ this.#dispatchRequest("media-play-request", trigger);
698
+ }
699
+ /**
700
+ * Dispatch a request to pause media playback.
701
+ */
702
+ pause(trigger) {
703
+ this.#dispatchRequest("media-pause-request", trigger);
704
+ }
705
+ /**
706
+ * Dispatch a request to set the media volume to mute (0).
707
+ */
708
+ mute(trigger) {
709
+ this.#dispatchRequest("media-mute-request", trigger);
710
+ }
711
+ /**
712
+ * Dispatch a request to unmute the media volume and set it back to it's previous state.
713
+ */
714
+ unmute(trigger) {
715
+ this.#dispatchRequest("media-unmute-request", trigger);
716
+ }
717
+ /**
718
+ * Dispatch a request to enter fullscreen.
719
+ *
720
+ * @docs {@link https://www.vidstack.io/docs/player/api/fullscreen#remote-control}
721
+ */
722
+ enterFullscreen(target, trigger) {
723
+ this.#dispatchRequest("media-enter-fullscreen-request", trigger, target);
724
+ }
725
+ /**
726
+ * Dispatch a request to exit fullscreen.
727
+ *
728
+ * @docs {@link https://www.vidstack.io/docs/player/api/fullscreen#remote-control}
729
+ */
730
+ exitFullscreen(target, trigger) {
731
+ this.#dispatchRequest("media-exit-fullscreen-request", trigger, target);
732
+ }
733
+ /**
734
+ * Dispatch a request to lock the screen orientation.
735
+ *
736
+ * @docs {@link https://www.vidstack.io/docs/player/screen-orientation#remote-control}
737
+ */
738
+ lockScreenOrientation(lockType, trigger) {
739
+ this.#dispatchRequest("media-orientation-lock-request", trigger, lockType);
740
+ }
741
+ /**
742
+ * Dispatch a request to unlock the screen orientation.
743
+ *
744
+ * @docs {@link https://www.vidstack.io/docs/player/api/screen-orientation#remote-control}
745
+ */
746
+ unlockScreenOrientation(trigger) {
747
+ this.#dispatchRequest("media-orientation-unlock-request", trigger);
748
+ }
749
+ /**
750
+ * Dispatch a request to enter picture-in-picture mode.
751
+ *
752
+ * @docs {@link https://www.vidstack.io/docs/player/api/picture-in-picture#remote-control}
753
+ */
754
+ enterPictureInPicture(trigger) {
755
+ this.#dispatchRequest("media-enter-pip-request", trigger);
756
+ }
757
+ /**
758
+ * Dispatch a request to exit picture-in-picture mode.
759
+ *
760
+ * @docs {@link https://www.vidstack.io/docs/player/api/picture-in-picture#remote-control}
761
+ */
762
+ exitPictureInPicture(trigger) {
763
+ this.#dispatchRequest("media-exit-pip-request", trigger);
764
+ }
765
+ /**
766
+ * Notify the media player that a seeking process is happening and to seek to the given `time`.
767
+ */
768
+ seeking(time, trigger) {
769
+ this.#dispatchRequest("media-seeking-request", trigger, time);
770
+ }
771
+ /**
772
+ * Notify the media player that a seeking operation has completed and to seek to the given `time`.
773
+ * This is generally called after a series of `remote.seeking()` calls.
774
+ */
775
+ seek(time, trigger) {
776
+ this.#dispatchRequest("media-seek-request", trigger, time);
777
+ }
778
+ seekToLiveEdge(trigger) {
779
+ this.#dispatchRequest("media-live-edge-request", trigger);
780
+ }
781
+ /**
782
+ * Dispatch a request to update the length of the media in seconds.
783
+ *
784
+ * @example
785
+ * ```ts
786
+ * remote.changeDuration(100); // 100 seconds
787
+ * ```
788
+ */
789
+ changeDuration(duration, trigger) {
790
+ this.#dispatchRequest("media-duration-change-request", trigger, duration);
791
+ }
792
+ /**
793
+ * Dispatch a request to update the clip start time. This is the time at which media playback
794
+ * should start at.
795
+ *
796
+ * @example
797
+ * ```ts
798
+ * remote.changeClipStart(100); // start at 100 seconds
799
+ * ```
800
+ */
801
+ changeClipStart(startTime, trigger) {
802
+ this.#dispatchRequest("media-clip-start-change-request", trigger, startTime);
803
+ }
804
+ /**
805
+ * Dispatch a request to update the clip end time. This is the time at which media playback
806
+ * should end at.
807
+ *
808
+ * @example
809
+ * ```ts
810
+ * remote.changeClipEnd(100); // end at 100 seconds
811
+ * ```
812
+ */
813
+ changeClipEnd(endTime, trigger) {
814
+ this.#dispatchRequest("media-clip-end-change-request", trigger, endTime);
815
+ }
816
+ /**
817
+ * Dispatch a request to update the media volume to the given `volume` level which is a value
818
+ * between 0 and 1.
819
+ *
820
+ * @docs {@link https://www.vidstack.io/docs/player/api/audio-gain#remote-control}
821
+ * @example
822
+ * ```ts
823
+ * remote.changeVolume(0); // 0%
824
+ * remote.changeVolume(0.05); // 5%
825
+ * remote.changeVolume(0.5); // 50%
826
+ * remote.changeVolume(0.75); // 70%
827
+ * remote.changeVolume(1); // 100%
828
+ * ```
829
+ */
830
+ changeVolume(volume, trigger) {
831
+ this.#dispatchRequest("media-volume-change-request", trigger, Math.max(0, Math.min(1, volume)));
832
+ }
833
+ /**
834
+ * Dispatch a request to change the current audio track.
835
+ *
836
+ * @example
837
+ * ```ts
838
+ * remote.changeAudioTrack(1); // track at index 1
839
+ * ```
840
+ */
841
+ changeAudioTrack(index, trigger) {
842
+ this.#dispatchRequest("media-audio-track-change-request", trigger, index);
843
+ }
844
+ /**
845
+ * Dispatch a request to change the video quality. The special value `-1` represents auto quality
846
+ * selection.
847
+ *
848
+ * @example
849
+ * ```ts
850
+ * remote.changeQuality(-1); // auto
851
+ * remote.changeQuality(1); // quality at index 1
852
+ * ```
853
+ */
854
+ changeQuality(index, trigger) {
855
+ this.#dispatchRequest("media-quality-change-request", trigger, index);
856
+ }
857
+ /**
858
+ * Request auto quality selection.
859
+ */
860
+ requestAutoQuality(trigger) {
861
+ this.changeQuality(-1, trigger);
862
+ }
863
+ /**
864
+ * Dispatch a request to change the mode of the text track at the given index.
865
+ *
866
+ * @example
867
+ * ```ts
868
+ * remote.changeTextTrackMode(1, 'showing'); // track at index 1
869
+ * ```
870
+ */
871
+ changeTextTrackMode(index, mode, trigger) {
872
+ this.#dispatchRequest("media-text-track-change-request", trigger, {
873
+ index,
874
+ mode
875
+ });
876
+ }
877
+ /**
878
+ * Dispatch a request to change the media playback rate.
879
+ *
880
+ * @example
881
+ * ```ts
882
+ * remote.changePlaybackRate(0.5); // Half the normal speed
883
+ * remote.changePlaybackRate(1); // Normal speed
884
+ * remote.changePlaybackRate(1.5); // 50% faster than normal
885
+ * remote.changePlaybackRate(2); // Double the normal speed
886
+ * ```
887
+ */
888
+ changePlaybackRate(rate, trigger) {
889
+ this.#dispatchRequest("media-rate-change-request", trigger, rate);
890
+ }
891
+ /**
892
+ * Dispatch a request to change the media audio gain.
893
+ *
894
+ * @example
895
+ * ```ts
896
+ * remote.changeAudioGain(1); // Disable audio gain
897
+ * remote.changeAudioGain(1.5); // 50% louder
898
+ * remote.changeAudioGain(2); // 100% louder
899
+ * ```
900
+ */
901
+ changeAudioGain(gain, trigger) {
902
+ this.#dispatchRequest("media-audio-gain-change-request", trigger, gain);
903
+ }
904
+ /**
905
+ * Dispatch a request to resume idle tracking on controls.
906
+ */
907
+ resumeControls(trigger) {
908
+ this.#dispatchRequest("media-resume-controls-request", trigger);
909
+ }
910
+ /**
911
+ * Dispatch a request to pause controls idle tracking. Pausing tracking will result in the
912
+ * controls being visible until `remote.resumeControls()` is called. This method
913
+ * is generally used when building custom controls and you'd like to prevent the UI from
914
+ * disappearing.
915
+ *
916
+ * @example
917
+ * ```ts
918
+ * // Prevent controls hiding while menu is being interacted with.
919
+ * function onSettingsOpen() {
920
+ * remote.pauseControls();
921
+ * }
922
+ *
923
+ * function onSettingsClose() {
924
+ * remote.resumeControls();
925
+ * }
926
+ * ```
927
+ */
928
+ pauseControls(trigger) {
929
+ this.#dispatchRequest("media-pause-controls-request", trigger);
930
+ }
931
+ /**
932
+ * Dispatch a request to toggle the media playback state.
933
+ */
934
+ togglePaused(trigger) {
935
+ const player = this.getPlayer(trigger?.target);
936
+ if (!player) {
937
+ return;
938
+ }
939
+ if (player.state.paused) this.play(trigger);
940
+ else this.pause(trigger);
941
+ }
942
+ /**
943
+ * Dispatch a request to toggle the controls visibility.
944
+ */
945
+ toggleControls(trigger) {
946
+ const player = this.getPlayer(trigger?.target);
947
+ if (!player) {
948
+ return;
949
+ }
950
+ if (!player.controls.showing) {
951
+ player.controls.show(0, trigger);
952
+ } else {
953
+ player.controls.hide(0, trigger);
954
+ }
955
+ }
956
+ /**
957
+ * Dispatch a request to toggle the media muted state.
958
+ */
959
+ toggleMuted(trigger) {
960
+ const player = this.getPlayer(trigger?.target);
961
+ if (!player) {
962
+ return;
963
+ }
964
+ if (player.state.muted) this.unmute(trigger);
965
+ else this.mute(trigger);
966
+ }
967
+ /**
968
+ * Dispatch a request to toggle the media fullscreen state.
969
+ *
970
+ * @docs {@link https://www.vidstack.io/docs/player/api/fullscreen#remote-control}
971
+ */
972
+ toggleFullscreen(target, trigger) {
973
+ const player = this.getPlayer(trigger?.target);
974
+ if (!player) {
975
+ return;
976
+ }
977
+ if (player.state.fullscreen) this.exitFullscreen(target, trigger);
978
+ else this.enterFullscreen(target, trigger);
979
+ }
980
+ /**
981
+ * Dispatch a request to toggle the media picture-in-picture mode.
982
+ *
983
+ * @docs {@link https://www.vidstack.io/docs/player/api/picture-in-picture#remote-control}
984
+ */
985
+ togglePictureInPicture(trigger) {
986
+ const player = this.getPlayer(trigger?.target);
987
+ if (!player) {
988
+ return;
989
+ }
990
+ if (player.state.pictureInPicture) this.exitPictureInPicture(trigger);
991
+ else this.enterPictureInPicture(trigger);
992
+ }
993
+ /**
994
+ * Show captions.
995
+ */
996
+ showCaptions(trigger) {
997
+ const player = this.getPlayer(trigger?.target);
998
+ if (!player) {
999
+ return;
1000
+ }
1001
+ let tracks = player.state.textTracks, index = this.#prevTrackIndex;
1002
+ if (!tracks[index] || !isTrackCaptionKind(tracks[index])) {
1003
+ index = -1;
1004
+ }
1005
+ if (index === -1) {
1006
+ index = tracks.findIndex((track) => isTrackCaptionKind(track) && track.default);
1007
+ }
1008
+ if (index === -1) {
1009
+ index = tracks.findIndex((track) => isTrackCaptionKind(track));
1010
+ }
1011
+ if (index >= 0) this.changeTextTrackMode(index, "showing", trigger);
1012
+ this.#prevTrackIndex = -1;
1013
+ }
1014
+ /**
1015
+ * Turn captions off.
1016
+ */
1017
+ disableCaptions(trigger) {
1018
+ const player = this.getPlayer(trigger?.target);
1019
+ if (!player) {
1020
+ return;
1021
+ }
1022
+ const tracks = player.state.textTracks, track = player.state.textTrack;
1023
+ if (track) {
1024
+ const index = tracks.indexOf(track);
1025
+ this.changeTextTrackMode(index, "disabled", trigger);
1026
+ this.#prevTrackIndex = index;
1027
+ }
1028
+ }
1029
+ /**
1030
+ * Dispatch a request to toggle the current captions mode.
1031
+ */
1032
+ toggleCaptions(trigger) {
1033
+ const player = this.getPlayer(trigger?.target);
1034
+ if (!player) {
1035
+ return;
1036
+ }
1037
+ if (player.state.textTrack) {
1038
+ this.disableCaptions();
1039
+ } else {
1040
+ this.showCaptions();
1041
+ }
1042
+ }
1043
+ userPrefersLoopChange(prefersLoop, trigger) {
1044
+ this.#dispatchRequest("media-user-loop-change-request", trigger, prefersLoop);
1045
+ }
1046
+ userDualCaptionChange(prefersSeparation, trigger) {
1047
+ this.#dispatchRequest("media-user-dual-captions-change-request", trigger, prefersSeparation);
1048
+ }
1049
+ #dispatchRequest(type, trigger, detail) {
1050
+ const request = new DOMEvent(type, {
1051
+ bubbles: true,
1052
+ composed: true,
1053
+ cancelable: true,
1054
+ detail,
1055
+ trigger
1056
+ });
1057
+ let target = trigger?.target || null;
1058
+ if (target && target instanceof Component) target = target.el;
1059
+ const shouldUsePlayer = !target || target === document || target === window || target === document.body || this.#player?.el && target instanceof Node && !this.#player.el.contains(target);
1060
+ target = shouldUsePlayer ? this.#target ?? this.getPlayer()?.el : target ?? this.#target;
1061
+ if (this.#player) {
1062
+ if (type === "media-play-request" && !this.#player.state.canLoad) {
1063
+ target?.dispatchEvent(request);
1064
+ } else {
1065
+ this.#player.canPlayQueue.enqueue(type, () => target?.dispatchEvent(request));
1066
+ }
1067
+ } else {
1068
+ target?.dispatchEvent(request);
1069
+ }
1070
+ }
1071
+ #noPlayerWarning(method) {
1072
+ }
1073
+ }
1074
+
1075
+ class LocalMediaStorage {
1076
+ playerId = "vds-player";
1077
+ mediaId = null;
1078
+ #data = {
1079
+ volume: null,
1080
+ muted: null,
1081
+ audioGain: null,
1082
+ time: null,
1083
+ lang: null,
1084
+ captions: null,
1085
+ rate: null,
1086
+ quality: null
1087
+ };
1088
+ async getVolume() {
1089
+ return this.#data.volume;
1090
+ }
1091
+ async setVolume(volume) {
1092
+ this.#data.volume = volume;
1093
+ this.save();
1094
+ }
1095
+ async getMuted() {
1096
+ return this.#data.muted;
1097
+ }
1098
+ async setMuted(muted) {
1099
+ this.#data.muted = muted;
1100
+ this.save();
1101
+ }
1102
+ async getTime() {
1103
+ return this.#data.time;
1104
+ }
1105
+ async setTime(time, ended) {
1106
+ const shouldClear = time < 0;
1107
+ this.#data.time = !shouldClear ? time : null;
1108
+ if (shouldClear || ended) this.saveTime();
1109
+ else this.saveTimeThrottled();
1110
+ }
1111
+ async getLang() {
1112
+ return this.#data.lang;
1113
+ }
1114
+ async setLang(lang) {
1115
+ this.#data.lang = lang;
1116
+ this.save();
1117
+ }
1118
+ async getCaptions() {
1119
+ return this.#data.captions;
1120
+ }
1121
+ async setCaptions(enabled) {
1122
+ this.#data.captions = enabled;
1123
+ this.save();
1124
+ }
1125
+ async getPlaybackRate() {
1126
+ return this.#data.rate;
1127
+ }
1128
+ async setPlaybackRate(rate) {
1129
+ this.#data.rate = rate;
1130
+ this.save();
1131
+ }
1132
+ async getAudioGain() {
1133
+ return this.#data.audioGain;
1134
+ }
1135
+ async setAudioGain(gain) {
1136
+ this.#data.audioGain = gain;
1137
+ this.save();
1138
+ }
1139
+ async getVideoQuality() {
1140
+ return this.#data.quality;
1141
+ }
1142
+ async setVideoQuality(quality) {
1143
+ this.#data.quality = quality;
1144
+ this.save();
1145
+ }
1146
+ onChange(src, mediaId, playerId = "vds-player") {
1147
+ const savedData = playerId ? localStorage.getItem(playerId) : null, savedTime = mediaId ? localStorage.getItem(mediaId) : null;
1148
+ this.playerId = playerId;
1149
+ this.mediaId = mediaId;
1150
+ this.#data = {
1151
+ volume: null,
1152
+ muted: null,
1153
+ audioGain: null,
1154
+ lang: null,
1155
+ captions: null,
1156
+ rate: null,
1157
+ quality: null,
1158
+ ...savedData ? JSON.parse(savedData) : {},
1159
+ time: savedTime ? +savedTime : null
1160
+ };
1161
+ }
1162
+ save() {
1163
+ return;
1164
+ }
1165
+ saveTimeThrottled = functionThrottle(this.saveTime.bind(this), 1e3);
1166
+ saveTime() {
1167
+ return;
1168
+ }
1169
+ }
1170
+
1171
+ const SELECTED = Symbol(0);
1172
+ class SelectList extends List {
1173
+ get selected() {
1174
+ return this.items.find((item) => item.selected) ?? null;
1175
+ }
1176
+ get selectedIndex() {
1177
+ return this.items.findIndex((item) => item.selected);
1178
+ }
1179
+ /** @internal */
1180
+ [ListSymbol.onRemove](item, trigger) {
1181
+ this[ListSymbol.select](item, false, trigger);
1182
+ }
1183
+ /** @internal */
1184
+ [ListSymbol.add](item, trigger) {
1185
+ item[SELECTED] = false;
1186
+ Object.defineProperty(item, "selected", {
1187
+ get() {
1188
+ return this[SELECTED];
1189
+ },
1190
+ set: (selected) => {
1191
+ if (this.readonly) return;
1192
+ this[ListSymbol.onUserSelect]?.();
1193
+ this[ListSymbol.select](item, selected);
1194
+ }
1195
+ });
1196
+ super[ListSymbol.add](item, trigger);
1197
+ }
1198
+ /** @internal */
1199
+ [ListSymbol.select](item, selected, trigger) {
1200
+ if (selected === item?.[SELECTED]) return;
1201
+ const prev = this.selected;
1202
+ if (item) item[SELECTED] = selected;
1203
+ const changed = !selected ? prev === item : prev !== item;
1204
+ if (changed) {
1205
+ if (prev) prev[SELECTED] = false;
1206
+ this.dispatchEvent(
1207
+ new DOMEvent("change", {
1208
+ detail: {
1209
+ prev,
1210
+ current: this.selected
1211
+ },
1212
+ trigger
1213
+ })
1214
+ );
1215
+ }
1216
+ }
1217
+ }
1218
+
1219
+ class AudioTrackList extends SelectList {
1220
+ }
1221
+
1222
+ class NativeTextRenderer {
1223
+ priority = 0;
1224
+ #display = true;
1225
+ #video = null;
1226
+ #track = null;
1227
+ #tracks = /* @__PURE__ */ new Set();
1228
+ canRender(_, video) {
1229
+ return !!video;
1230
+ }
1231
+ attach(video) {
1232
+ this.#video = video;
1233
+ if (video) video.textTracks.onchange = this.#onChange.bind(this);
1234
+ }
1235
+ addTrack(track) {
1236
+ this.#tracks.add(track);
1237
+ this.#attachTrack(track);
1238
+ }
1239
+ removeTrack(track) {
1240
+ track[TextTrackSymbol.native]?.remove?.();
1241
+ track[TextTrackSymbol.native] = null;
1242
+ this.#tracks.delete(track);
1243
+ }
1244
+ changeTrack(track) {
1245
+ const current = track?.[TextTrackSymbol.native];
1246
+ if (current && current.track.mode !== "showing") {
1247
+ current.track.mode = "showing";
1248
+ }
1249
+ this.#track = track;
1250
+ }
1251
+ setDisplay(display) {
1252
+ this.#display = display;
1253
+ this.#onChange();
1254
+ }
1255
+ detach() {
1256
+ if (this.#video) this.#video.textTracks.onchange = null;
1257
+ for (const track of this.#tracks) this.removeTrack(track);
1258
+ this.#tracks.clear();
1259
+ this.#video = null;
1260
+ this.#track = null;
1261
+ }
1262
+ #attachTrack(track) {
1263
+ if (!this.#video) return;
1264
+ const el = track[TextTrackSymbol.native] ??= this.#createTrackElement(track);
1265
+ if (isHTMLElement(el)) {
1266
+ this.#video.append(el);
1267
+ el.track.mode = el.default ? "showing" : "disabled";
1268
+ }
1269
+ }
1270
+ #createTrackElement(track) {
1271
+ const el = document.createElement("track"), isDefault = track.default || track.mode === "showing", isSupported = track.src && track.type === "vtt";
1272
+ el.id = track.id;
1273
+ el.src = isSupported ? track.src : "";
1274
+ el.label = track.label;
1275
+ el.kind = track.kind;
1276
+ el.default = isDefault;
1277
+ track.language && (el.srclang = track.language);
1278
+ if (isDefault && !isSupported) {
1279
+ this.#copyCues(track, el.track);
1280
+ }
1281
+ return el;
1282
+ }
1283
+ #copyCues(track, native) {
1284
+ if (track.src && track.type === "vtt" || native.cues?.length) return;
1285
+ for (const cue of track.cues) native.addCue(cue);
1286
+ }
1287
+ #onChange(event) {
1288
+ for (const track of this.#tracks) {
1289
+ const native = track[TextTrackSymbol.native];
1290
+ if (!native) continue;
1291
+ if (!this.#display) {
1292
+ native.track.mode = native.managed ? "hidden" : "disabled";
1293
+ continue;
1294
+ }
1295
+ const isShowing = native.track.mode === "showing";
1296
+ if (isShowing) this.#copyCues(track, native.track);
1297
+ track.setMode(isShowing ? "showing" : "disabled", event);
1298
+ }
1299
+ }
1300
+ }
1301
+
1302
+ class TextRenderers {
1303
+ #video = null;
1304
+ #textTracks;
1305
+ #renderers = [];
1306
+ #media;
1307
+ #nativeDisplay = false;
1308
+ #nativeRenderer = null;
1309
+ #customRenderer = null;
1310
+ constructor(media) {
1311
+ this.#media = media;
1312
+ const textTracks = media.textTracks;
1313
+ this.#textTracks = textTracks;
1314
+ effect(this.#watchControls.bind(this));
1315
+ onDispose(this.#detach.bind(this));
1316
+ new EventsController(textTracks).add("add", this.#onAddTrack.bind(this)).add("remove", this.#onRemoveTrack.bind(this)).add("mode-change", this.#update.bind(this));
1317
+ }
1318
+ #watchControls() {
1319
+ const { nativeControls } = this.#media.$state;
1320
+ this.#nativeDisplay = nativeControls();
1321
+ this.#update();
1322
+ }
1323
+ add(renderer) {
1324
+ this.#renderers.push(renderer);
1325
+ untrack(this.#update.bind(this));
1326
+ }
1327
+ remove(renderer) {
1328
+ renderer.detach();
1329
+ this.#renderers.splice(this.#renderers.indexOf(renderer), 1);
1330
+ untrack(this.#update.bind(this));
1331
+ }
1332
+ /** @internal */
1333
+ attachVideo(video) {
1334
+ requestAnimationFrame(() => {
1335
+ this.#video = video;
1336
+ if (video) {
1337
+ this.#nativeRenderer = new NativeTextRenderer();
1338
+ this.#nativeRenderer.attach(video);
1339
+ for (const track of this.#textTracks) this.#addNativeTrack(track);
1340
+ }
1341
+ this.#update();
1342
+ });
1343
+ }
1344
+ #addNativeTrack(track) {
1345
+ if (!isTrackCaptionKind(track)) return;
1346
+ this.#nativeRenderer?.addTrack(track);
1347
+ }
1348
+ #removeNativeTrack(track) {
1349
+ if (!isTrackCaptionKind(track)) return;
1350
+ this.#nativeRenderer?.removeTrack(track);
1351
+ }
1352
+ #onAddTrack(event) {
1353
+ this.#addNativeTrack(event.detail);
1354
+ }
1355
+ #onRemoveTrack(event) {
1356
+ this.#removeNativeTrack(event.detail);
1357
+ }
1358
+ #update() {
1359
+ const currentTrack = this.#textTracks.selected;
1360
+ if (this.#video && (this.#nativeDisplay || currentTrack?.[TextTrackSymbol.nativeHLS])) {
1361
+ this.#customRenderer?.changeTrack(null);
1362
+ this.#nativeRenderer?.setDisplay(true);
1363
+ this.#nativeRenderer?.changeTrack(currentTrack);
1364
+ return;
1365
+ }
1366
+ this.#nativeRenderer?.setDisplay(false);
1367
+ this.#nativeRenderer?.changeTrack(null);
1368
+ if (!currentTrack) {
1369
+ this.#customRenderer?.changeTrack(null);
1370
+ return;
1371
+ }
1372
+ const customRenderer = this.#renderers.sort((a, b) => a.priority - b.priority).find((renderer) => renderer.canRender(currentTrack, this.#video));
1373
+ if (this.#customRenderer !== customRenderer) {
1374
+ this.#customRenderer?.detach();
1375
+ customRenderer?.attach(this.#video);
1376
+ this.#customRenderer = customRenderer ?? null;
1377
+ }
1378
+ customRenderer?.changeTrack(currentTrack);
1379
+ }
1380
+ #detach() {
1381
+ this.#nativeRenderer?.detach();
1382
+ this.#nativeRenderer = null;
1383
+ this.#customRenderer?.detach();
1384
+ this.#customRenderer = null;
1385
+ }
1386
+ }
1387
+
1388
+ class TextTrackList extends List {
1389
+ #canLoad = false;
1390
+ #defaults = {};
1391
+ #storage = null;
1392
+ #preferredLang = null;
1393
+ /** @internal */
1394
+ [TextTrackSymbol.crossOrigin];
1395
+ constructor() {
1396
+ super();
1397
+ }
1398
+ get selected() {
1399
+ const track = this.items.find((t) => t.mode === "showing" && isTrackCaptionKind(t));
1400
+ return track ?? null;
1401
+ }
1402
+ get selectedIndex() {
1403
+ const selected = this.selected;
1404
+ return selected ? this.indexOf(selected) : -1;
1405
+ }
1406
+ get preferredLang() {
1407
+ return this.#preferredLang;
1408
+ }
1409
+ set preferredLang(lang) {
1410
+ this.#preferredLang = lang;
1411
+ this.#saveLang(lang);
1412
+ }
1413
+ add(init, trigger) {
1414
+ const isTrack = init instanceof TextTrack, track = isTrack ? init : new TextTrack(init), kind = init.kind === "captions" || init.kind === "subtitles" ? "captions" : init.kind;
1415
+ if (this.#defaults[kind] && init.default) delete init.default;
1416
+ track.addEventListener("mode-change", this.#onTrackModeChangeBind);
1417
+ this[ListSymbol.add](track, trigger);
1418
+ track[TextTrackSymbol.crossOrigin] = this[TextTrackSymbol.crossOrigin];
1419
+ if (this.#canLoad) track[TextTrackSymbol.canLoad]();
1420
+ if (init.default) this.#defaults[kind] = track;
1421
+ this.#selectTracks();
1422
+ return this;
1423
+ }
1424
+ remove(track, trigger) {
1425
+ this.#pendingRemoval = track;
1426
+ if (!this.items.includes(track)) return;
1427
+ if (track === this.#defaults[track.kind]) delete this.#defaults[track.kind];
1428
+ track.mode = "disabled";
1429
+ track[TextTrackSymbol.onModeChange] = null;
1430
+ track.removeEventListener("mode-change", this.#onTrackModeChangeBind);
1431
+ this[ListSymbol.remove](track, trigger);
1432
+ this.#pendingRemoval = null;
1433
+ return this;
1434
+ }
1435
+ clear(trigger) {
1436
+ for (const track of [...this.items]) {
1437
+ this.remove(track, trigger);
1438
+ }
1439
+ return this;
1440
+ }
1441
+ getByKind(kind) {
1442
+ const kinds = Array.isArray(kind) ? kind : [kind];
1443
+ return this.items.filter((track) => kinds.includes(track.kind));
1444
+ }
1445
+ /** @internal */
1446
+ [TextTrackSymbol.canLoad]() {
1447
+ if (this.#canLoad) return;
1448
+ for (const track of this.items) track[TextTrackSymbol.canLoad]();
1449
+ this.#canLoad = true;
1450
+ this.#selectTracks();
1451
+ }
1452
+ #selectTracks = functionDebounce(async () => {
1453
+ if (!this.#canLoad) return;
1454
+ if (!this.#preferredLang && this.#storage) {
1455
+ this.#preferredLang = await this.#storage.getLang();
1456
+ }
1457
+ const showCaptions = await this.#storage?.getCaptions(), kinds = [
1458
+ ["captions", "subtitles"],
1459
+ "chapters",
1460
+ "descriptions",
1461
+ "metadata"
1462
+ ];
1463
+ for (const kind of kinds) {
1464
+ const tracks = this.getByKind(kind);
1465
+ if (tracks.find((t) => t.mode === "showing")) continue;
1466
+ const preferredTrack = this.#preferredLang ? tracks.find((track2) => track2.language === this.#preferredLang) : null;
1467
+ const defaultTrack = isArray(kind) ? this.#defaults[kind.find((kind2) => this.#defaults[kind2]) || ""] : this.#defaults[kind];
1468
+ const track = preferredTrack ?? defaultTrack, isCaptionsKind = track && isTrackCaptionKind(track);
1469
+ if (track && (!isCaptionsKind || showCaptions !== false)) {
1470
+ track.mode = "showing";
1471
+ if (isCaptionsKind) this.#saveCaptionsTrack(track);
1472
+ }
1473
+ }
1474
+ }, 300);
1475
+ #pendingRemoval = null;
1476
+ #onTrackModeChangeBind = this.#onTrackModeChange.bind(this);
1477
+ #onTrackModeChange(event) {
1478
+ const track = event.detail;
1479
+ if (this.#storage && isTrackCaptionKind(track) && track !== this.#pendingRemoval) {
1480
+ this.#saveCaptionsTrack(track);
1481
+ }
1482
+ if (track.mode === "showing") {
1483
+ const kinds = isTrackCaptionKind(track) ? ["captions", "subtitles"] : [track.kind];
1484
+ for (const t of this.items) {
1485
+ if (t.mode === "showing" && t != track && kinds.includes(t.kind)) {
1486
+ t.mode = "disabled";
1487
+ }
1488
+ }
1489
+ }
1490
+ this.dispatchEvent(
1491
+ new DOMEvent("mode-change", {
1492
+ detail: event.detail,
1493
+ trigger: event
1494
+ })
1495
+ );
1496
+ }
1497
+ #saveCaptionsTrack(track) {
1498
+ if (track.mode !== "disabled") {
1499
+ this.#saveLang(track.language);
1500
+ }
1501
+ this.#storage?.setCaptions?.(track.mode === "showing");
1502
+ }
1503
+ #saveLang(lang) {
1504
+ this.#storage?.setLang?.(this.#preferredLang = lang);
1505
+ }
1506
+ setStorage(storage) {
1507
+ this.#storage = storage;
1508
+ }
1509
+ }
1510
+
1511
+ const SET_AUTO = Symbol(0), ENABLE_AUTO = Symbol(0);
1512
+ const QualitySymbol = {
1513
+ setAuto: SET_AUTO,
1514
+ enableAuto: ENABLE_AUTO
1515
+ };
1516
+
1517
+ class VideoQualityList extends SelectList {
1518
+ #auto = false;
1519
+ /**
1520
+ * Configures quality switching:
1521
+ *
1522
+ * - `current`: Trigger an immediate quality level switch. This will abort the current fragment
1523
+ * request if any, flush the whole buffer, and fetch fragment matching with current position
1524
+ * and requested quality level.
1525
+ *
1526
+ * - `next`: Trigger a quality level switch for next fragment. This could eventually flush
1527
+ * already buffered next fragment.
1528
+ *
1529
+ * - `load`: Set quality level for next loaded fragment.
1530
+ *
1531
+ * @see {@link https://www.vidstack.io/docs/player/api/video-quality#switch}
1532
+ * @see {@link https://github.com/video-dev/hls.js/blob/master/docs/API.md#quality-switch-control-api}
1533
+ */
1534
+ switch = "current";
1535
+ /**
1536
+ * Whether automatic quality selection is enabled.
1537
+ */
1538
+ get auto() {
1539
+ return this.#auto || this.readonly;
1540
+ }
1541
+ /** @internal */
1542
+ [QualitySymbol.enableAuto];
1543
+ /** @internal */
1544
+ [ListSymbol.onUserSelect]() {
1545
+ this[QualitySymbol.setAuto](false);
1546
+ }
1547
+ /** @internal */
1548
+ [ListSymbol.onReset](trigger) {
1549
+ this[QualitySymbol.enableAuto] = void 0;
1550
+ this[QualitySymbol.setAuto](false, trigger);
1551
+ }
1552
+ /**
1553
+ * Request automatic quality selection (if supported). This will be a no-op if the list is
1554
+ * `readonly` as that already implies auto-selection.
1555
+ */
1556
+ autoSelect(trigger) {
1557
+ if (this.readonly || this.#auto || !this[QualitySymbol.enableAuto]) return;
1558
+ this[QualitySymbol.enableAuto]?.(trigger);
1559
+ this[QualitySymbol.setAuto](true, trigger);
1560
+ }
1561
+ getBySrc(src) {
1562
+ return this.items.find((quality) => quality.src === src);
1563
+ }
1564
+ /** @internal */
1565
+ [QualitySymbol.setAuto](auto, trigger) {
1566
+ if (this.#auto === auto) return;
1567
+ this.#auto = auto;
1568
+ this.dispatchEvent(
1569
+ new DOMEvent("auto-change", {
1570
+ detail: auto,
1571
+ trigger
1572
+ })
1573
+ );
1574
+ }
1575
+ }
1576
+
1577
+ function isAudioProvider(provider) {
1578
+ return provider?.$$PROVIDER_TYPE === "AUDIO";
1579
+ }
1580
+ function isVideoProvider(provider) {
1581
+ return provider?.$$PROVIDER_TYPE === "VIDEO";
1582
+ }
1583
+ function isHLSProvider(provider) {
1584
+ return provider?.$$PROVIDER_TYPE === "HLS";
1585
+ }
1586
+ function isDASHProvider(provider) {
1587
+ return provider?.$$PROVIDER_TYPE === "DASH";
1588
+ }
1589
+ function isYouTubeProvider(provider) {
1590
+ return provider?.$$PROVIDER_TYPE === "YOUTUBE";
1591
+ }
1592
+ function isVimeoProvider(provider) {
1593
+ return provider?.$$PROVIDER_TYPE === "VIMEO";
1594
+ }
1595
+ function isGoogleCastProvider(provider) {
1596
+ return provider?.$$PROVIDER_TYPE === "GOOGLE_CAST";
1597
+ }
1598
+ function isHTMLAudioElement(element) {
1599
+ return false;
1600
+ }
1601
+ function isHTMLVideoElement(element) {
1602
+ return false;
1603
+ }
1604
+ function isHTMLMediaElement(element) {
1605
+ return isHTMLVideoElement();
1606
+ }
1607
+ function isHTMLIFrameElement(element) {
1608
+ return false;
1609
+ }
1610
+
1611
+ class MediaPlayerController extends ViewController {
1612
+ }
1613
+
1614
+ const MEDIA_KEY_SHORTCUTS = {
1615
+ togglePaused: "k Space",
1616
+ toggleMuted: "m",
1617
+ toggleFullscreen: "f",
1618
+ togglePictureInPicture: "i",
1619
+ toggleCaptions: "c",
1620
+ seekBackward: "j J ArrowLeft",
1621
+ seekForward: "l L ArrowRight",
1622
+ volumeUp: "ArrowUp",
1623
+ volumeDown: "ArrowDown",
1624
+ speedUp: ">",
1625
+ slowDown: "<"
1626
+ };
1627
+ const MODIFIER_KEYS = /* @__PURE__ */ new Set(["Shift", "Alt", "Meta", "Ctrl"]), BUTTON_SELECTORS = 'button, [role="button"]', IGNORE_SELECTORS = 'input, textarea, select, [contenteditable], [role^="menuitem"], [role="timer"]';
1628
+ class MediaKeyboardController extends MediaPlayerController {
1629
+ #media;
1630
+ constructor(media) {
1631
+ super();
1632
+ this.#media = media;
1633
+ }
1634
+ onConnect() {
1635
+ effect(this.#onTargetChange.bind(this));
1636
+ }
1637
+ #onTargetChange() {
1638
+ const { keyDisabled, keyTarget } = this.$props;
1639
+ if (keyDisabled()) return;
1640
+ const target = keyTarget() === "player" ? this.el : document, $active = signal(false);
1641
+ if (target === this.el) {
1642
+ new EventsController(this.el).add("focusin", () => $active.set(true)).add("focusout", (event) => {
1643
+ if (!this.el.contains(event.target)) $active.set(false);
1644
+ });
1645
+ } else {
1646
+ if (!peek($active)) $active.set(document.querySelector("[data-media-player]") === this.el);
1647
+ }
1648
+ effect(() => {
1649
+ if (!$active()) return;
1650
+ new EventsController(target).add("keyup", this.#onKeyUp.bind(this)).add("keydown", this.#onKeyDown.bind(this)).add("keydown", this.#onPreventVideoKeys.bind(this), { capture: true });
1651
+ });
1652
+ }
1653
+ #onKeyUp(event) {
1654
+ const focusedEl = document.activeElement;
1655
+ if (!event.key || !this.$state.canSeek() || focusedEl?.matches(IGNORE_SELECTORS)) {
1656
+ return;
1657
+ }
1658
+ let { method, value } = this.#getMatchingMethod(event);
1659
+ if (!isString(value) && !isArray(value)) {
1660
+ value?.onKeyUp?.({
1661
+ event,
1662
+ player: this.#media.player,
1663
+ remote: this.#media.remote
1664
+ });
1665
+ value?.callback?.(event, this.#media.remote);
1666
+ return;
1667
+ }
1668
+ if (method?.startsWith("seek")) {
1669
+ event.preventDefault();
1670
+ event.stopPropagation();
1671
+ if (this.#timeSlider) {
1672
+ this.#forwardTimeKeyboardEvent(event, method === "seekForward");
1673
+ this.#timeSlider = null;
1674
+ } else {
1675
+ this.#media.remote.seek(this.#seekTotal, event);
1676
+ this.#seekTotal = void 0;
1677
+ }
1678
+ }
1679
+ if (method?.startsWith("volume")) {
1680
+ const volumeSlider = this.el.querySelector("[data-media-volume-slider]");
1681
+ volumeSlider?.dispatchEvent(
1682
+ new KeyboardEvent("keyup", {
1683
+ key: method === "volumeUp" ? "Up" : "Down",
1684
+ shiftKey: event.shiftKey,
1685
+ trigger: event
1686
+ })
1687
+ );
1688
+ }
1689
+ }
1690
+ #onKeyDown(event) {
1691
+ if (!event.key || MODIFIER_KEYS.has(event.key)) return;
1692
+ const focusedEl = document.activeElement;
1693
+ if (focusedEl?.matches(IGNORE_SELECTORS) || isKeyboardClick(event) && focusedEl?.matches(BUTTON_SELECTORS)) {
1694
+ return;
1695
+ }
1696
+ let { method, value } = this.#getMatchingMethod(event), isNumberPress = !event.metaKey && /^[0-9]$/.test(event.key);
1697
+ if (!isString(value) && !isArray(value) && !isNumberPress) {
1698
+ value?.onKeyDown?.({
1699
+ event,
1700
+ player: this.#media.player,
1701
+ remote: this.#media.remote
1702
+ });
1703
+ value?.callback?.(event, this.#media.remote);
1704
+ return;
1705
+ }
1706
+ if (!method && isNumberPress && !modifierKeyPressed(event)) {
1707
+ event.preventDefault();
1708
+ event.stopPropagation();
1709
+ this.#media.remote.seek(this.$state.duration() / 10 * Number(event.key), event);
1710
+ return;
1711
+ }
1712
+ if (!method) return;
1713
+ event.preventDefault();
1714
+ event.stopPropagation();
1715
+ switch (method) {
1716
+ case "seekForward":
1717
+ case "seekBackward":
1718
+ this.#seeking(event, method, method === "seekForward");
1719
+ break;
1720
+ case "volumeUp":
1721
+ case "volumeDown":
1722
+ const volumeSlider = this.el.querySelector("[data-media-volume-slider]");
1723
+ if (volumeSlider) {
1724
+ volumeSlider.dispatchEvent(
1725
+ new KeyboardEvent("keydown", {
1726
+ key: method === "volumeUp" ? "Up" : "Down",
1727
+ shiftKey: event.shiftKey,
1728
+ trigger: event
1729
+ })
1730
+ );
1731
+ } else {
1732
+ const value2 = event.shiftKey ? 0.1 : 0.05;
1733
+ this.#media.remote.changeVolume(
1734
+ this.$state.volume() + (method === "volumeUp" ? +value2 : -value2),
1735
+ event
1736
+ );
1737
+ }
1738
+ break;
1739
+ case "toggleFullscreen":
1740
+ this.#media.remote.toggleFullscreen("prefer-media", event);
1741
+ break;
1742
+ case "speedUp":
1743
+ case "slowDown":
1744
+ const playbackRate = this.$state.playbackRate();
1745
+ this.#media.remote.changePlaybackRate(
1746
+ Math.max(0.25, Math.min(2, playbackRate + (method === "speedUp" ? 0.25 : -0.25))),
1747
+ event
1748
+ );
1749
+ break;
1750
+ default:
1751
+ this.#media.remote[method]?.(event);
1752
+ }
1753
+ this.$state.lastKeyboardAction.set({
1754
+ action: method,
1755
+ event
1756
+ });
1757
+ }
1758
+ #onPreventVideoKeys(event) {
1759
+ if (isHTMLMediaElement(event.target)) ;
1760
+ }
1761
+ #getMatchingMethod(event) {
1762
+ const keyShortcuts = {
1763
+ ...this.$props.keyShortcuts(),
1764
+ ...this.#media.ariaKeys
1765
+ };
1766
+ const method = Object.keys(keyShortcuts).find((method2) => {
1767
+ const value = keyShortcuts[method2], keys = isArray(value) ? value.join(" ") : isString(value) ? value : value?.keys;
1768
+ const combinations = (isArray(keys) ? keys : keys?.split(" "))?.map(
1769
+ (key) => replaceSymbolKeys(key).replace(/Control/g, "Ctrl").split("+")
1770
+ );
1771
+ return combinations?.some((combo) => {
1772
+ const modifierKeys = new Set(combo.filter((key) => MODIFIER_KEYS.has(key)));
1773
+ if ("<>".includes(event.key)) {
1774
+ modifierKeys.add("Shift");
1775
+ }
1776
+ for (const modKey of MODIFIER_KEYS) {
1777
+ const modKeyProp = modKey.toLowerCase() + "Key";
1778
+ if (!modifierKeys.has(modKey) && event[modKeyProp]) {
1779
+ return false;
1780
+ }
1781
+ }
1782
+ return combo.every((key) => {
1783
+ return MODIFIER_KEYS.has(key) ? event[key.toLowerCase() + "Key"] : event.key === key.replace("Space", " ");
1784
+ });
1785
+ });
1786
+ });
1787
+ return {
1788
+ method,
1789
+ value: method ? keyShortcuts[method] : null
1790
+ };
1791
+ }
1792
+ #seekTotal;
1793
+ #calcSeekAmount(event, type) {
1794
+ const seekBy = event.shiftKey ? 10 : 5;
1795
+ return this.#seekTotal = Math.max(
1796
+ 0,
1797
+ Math.min(
1798
+ (this.#seekTotal ?? this.$state.currentTime()) + (type === "seekForward" ? +seekBy : -seekBy),
1799
+ this.$state.duration()
1800
+ )
1801
+ );
1802
+ }
1803
+ #timeSlider = null;
1804
+ #forwardTimeKeyboardEvent(event, forward) {
1805
+ this.#timeSlider?.dispatchEvent(
1806
+ new KeyboardEvent(event.type, {
1807
+ key: !forward ? "Left" : "Right",
1808
+ shiftKey: event.shiftKey,
1809
+ trigger: event
1810
+ })
1811
+ );
1812
+ }
1813
+ #seeking(event, type, forward) {
1814
+ if (!this.$state.canSeek()) return;
1815
+ if (!this.#timeSlider) {
1816
+ this.#timeSlider = this.el.querySelector("[data-media-time-slider]");
1817
+ }
1818
+ if (this.#timeSlider) {
1819
+ this.#forwardTimeKeyboardEvent(event, forward);
1820
+ } else {
1821
+ this.#media.remote.seeking(this.#calcSeekAmount(event, type), event);
1822
+ }
1823
+ }
1824
+ }
1825
+ const SYMBOL_KEY_MAP = ["!", "@", "#", "$", "%", "^", "&", "*", "(", ")"];
1826
+ function replaceSymbolKeys(key) {
1827
+ return key.replace(/Shift\+(\d)/g, (_, num) => SYMBOL_KEY_MAP[num - 1]);
1828
+ }
1829
+ function modifierKeyPressed(event) {
1830
+ for (const key of MODIFIER_KEYS) {
1831
+ if (event[key.toLowerCase() + "Key"]) {
1832
+ return true;
1833
+ }
1834
+ }
1835
+ return false;
1836
+ }
1837
+
1838
+ class MediaControls extends MediaPlayerController {
1839
+ #idleTimer = -2;
1840
+ #pausedTracking = false;
1841
+ #hideOnMouseLeave = signal(false);
1842
+ #isMouseOutside = signal(false);
1843
+ #focusedItem = null;
1844
+ #canIdle = signal(true);
1845
+ /**
1846
+ * The default amount of delay in milliseconds while media playback is progressing without user
1847
+ * activity to indicate an idle state (i.e., hide controls).
1848
+ *
1849
+ * @defaultValue 2000
1850
+ */
1851
+ defaultDelay = 2e3;
1852
+ /**
1853
+ * Whether controls can hide after a delay in user interaction. If this is false, controls will
1854
+ * not hide and be user controlled.
1855
+ */
1856
+ get canIdle() {
1857
+ return this.#canIdle();
1858
+ }
1859
+ set canIdle(canIdle) {
1860
+ this.#canIdle.set(canIdle);
1861
+ }
1862
+ /**
1863
+ * Whether controls visibility should be toggled when the mouse enters and leaves the player
1864
+ * container.
1865
+ *
1866
+ * @defaultValue false
1867
+ */
1868
+ get hideOnMouseLeave() {
1869
+ const { hideControlsOnMouseLeave } = this.$props;
1870
+ return this.#hideOnMouseLeave() || hideControlsOnMouseLeave();
1871
+ }
1872
+ set hideOnMouseLeave(hide) {
1873
+ this.#hideOnMouseLeave.set(hide);
1874
+ }
1875
+ /**
1876
+ * Whether media controls are currently visible.
1877
+ */
1878
+ get showing() {
1879
+ return this.$state.controlsVisible();
1880
+ }
1881
+ /**
1882
+ * Show controls.
1883
+ */
1884
+ show(delay = 0, trigger) {
1885
+ this.#clearIdleTimer();
1886
+ if (!this.#pausedTracking) {
1887
+ this.#changeVisibility(true, delay, trigger);
1888
+ }
1889
+ }
1890
+ /**
1891
+ * Hide controls.
1892
+ */
1893
+ hide(delay = this.defaultDelay, trigger) {
1894
+ this.#clearIdleTimer();
1895
+ if (!this.#pausedTracking) {
1896
+ this.#changeVisibility(false, delay, trigger);
1897
+ }
1898
+ }
1899
+ /**
1900
+ * Whether all idle tracking on controls should be paused until resumed again.
1901
+ */
1902
+ pause(trigger) {
1903
+ this.#pausedTracking = true;
1904
+ this.#clearIdleTimer();
1905
+ this.#changeVisibility(true, 0, trigger);
1906
+ }
1907
+ resume(trigger) {
1908
+ this.#pausedTracking = false;
1909
+ if (this.$state.paused()) return;
1910
+ this.#changeVisibility(false, this.defaultDelay, trigger);
1911
+ }
1912
+ onConnect() {
1913
+ effect(this.#init.bind(this));
1914
+ }
1915
+ #init() {
1916
+ const { viewType } = this.$state;
1917
+ if (!this.el || !this.#canIdle()) return;
1918
+ if (viewType() === "audio") {
1919
+ this.show();
1920
+ return;
1921
+ }
1922
+ effect(this.#watchMouse.bind(this));
1923
+ effect(this.#watchPaused.bind(this));
1924
+ const onPlay = this.#onPlay.bind(this), onPause = this.#onPause.bind(this), onEnd = this.#onEnd.bind(this);
1925
+ new EventsController(this.el).add("can-play", (event) => this.show(0, event)).add("play", onPlay).add("pause", onPause).add("end", onEnd).add("auto-play-fail", onPause);
1926
+ }
1927
+ #watchMouse() {
1928
+ if (!this.el) return;
1929
+ const { started, pointer, paused } = this.$state;
1930
+ if (!started() || pointer() !== "fine") return;
1931
+ const events = new EventsController(this.el), shouldHideOnMouseLeave = this.hideOnMouseLeave;
1932
+ if (!shouldHideOnMouseLeave || !this.#isMouseOutside()) {
1933
+ effect(() => {
1934
+ if (!paused()) events.add("pointermove", this.#onStopIdle.bind(this));
1935
+ });
1936
+ }
1937
+ if (shouldHideOnMouseLeave) {
1938
+ events.add("mouseenter", this.#onMouseEnter.bind(this)).add("mouseleave", this.#onMouseLeave.bind(this));
1939
+ }
1940
+ }
1941
+ #watchPaused() {
1942
+ const { paused, started, autoPlayError } = this.$state;
1943
+ if (paused() || autoPlayError() && !started()) return;
1944
+ const onStopIdle = this.#onStopIdle.bind(this);
1945
+ effect(() => {
1946
+ if (!this.el) return;
1947
+ const pointer = this.$state.pointer(), isTouch = pointer === "coarse", events = new EventsController(this.el), eventTypes = [isTouch ? "touchend" : "pointerup", "keydown"];
1948
+ for (const eventType of eventTypes) {
1949
+ events.add(eventType, onStopIdle, { passive: false });
1950
+ }
1951
+ });
1952
+ }
1953
+ #onPlay(event) {
1954
+ if (event.triggers.hasType("ended")) return;
1955
+ this.show(0, event);
1956
+ this.hide(void 0, event);
1957
+ }
1958
+ #onPause(event) {
1959
+ this.show(0, event);
1960
+ }
1961
+ #onEnd(event) {
1962
+ const { loop } = this.$state;
1963
+ if (loop()) this.hide(0, event);
1964
+ }
1965
+ #onMouseEnter(event) {
1966
+ this.#isMouseOutside.set(false);
1967
+ this.show(0, event);
1968
+ this.hide(void 0, event);
1969
+ }
1970
+ #onMouseLeave(event) {
1971
+ this.#isMouseOutside.set(true);
1972
+ this.hide(0, event);
1973
+ }
1974
+ #clearIdleTimer() {
1975
+ window.clearTimeout(this.#idleTimer);
1976
+ this.#idleTimer = -1;
1977
+ }
1978
+ #onStopIdle(event) {
1979
+ if (
1980
+ // @ts-expect-error
1981
+ event.MEDIA_GESTURE || this.#pausedTracking || isTouchPinchEvent(event)
1982
+ ) {
1983
+ return;
1984
+ }
1985
+ if (isKeyboardEvent(event)) {
1986
+ if (event.key === "Escape") {
1987
+ this.el?.focus();
1988
+ this.#focusedItem = null;
1989
+ } else if (this.#focusedItem) {
1990
+ event.preventDefault();
1991
+ requestAnimationFrame(() => {
1992
+ this.#focusedItem?.focus();
1993
+ this.#focusedItem = null;
1994
+ });
1995
+ }
1996
+ }
1997
+ this.show(0, event);
1998
+ this.hide(this.defaultDelay, event);
1999
+ }
2000
+ #changeVisibility(visible, delay, trigger) {
2001
+ if (delay === 0) {
2002
+ this.#onChange(visible, trigger);
2003
+ return;
2004
+ }
2005
+ this.#idleTimer = window.setTimeout(() => {
2006
+ if (!this.scope) return;
2007
+ this.#onChange(visible && !this.#pausedTracking, trigger);
2008
+ }, delay);
2009
+ }
2010
+ #onChange(visible, trigger) {
2011
+ if (this.$state.controlsVisible() === visible) return;
2012
+ this.$state.controlsVisible.set(visible);
2013
+ if (!visible && document.activeElement && this.el?.contains(document.activeElement)) {
2014
+ this.#focusedItem = document.activeElement;
2015
+ requestAnimationFrame(() => {
2016
+ this.el?.focus({ preventScroll: true });
2017
+ });
2018
+ }
2019
+ this.dispatch("controls-change", {
2020
+ detail: visible,
2021
+ trigger
2022
+ });
2023
+ }
2024
+ }
2025
+
2026
+ class AudioProviderLoader {
2027
+ name = "audio";
2028
+ target;
2029
+ canPlay(src) {
2030
+ if (!isAudioSrc(src)) return false;
2031
+ return true;
2032
+ }
2033
+ mediaType() {
2034
+ return "audio";
2035
+ }
2036
+ async load(ctx) {
2037
+ {
2038
+ throw Error("[vidstack] can not load audio provider server-side");
2039
+ }
2040
+ }
2041
+ }
2042
+
2043
+ class VideoProviderLoader {
2044
+ name = "video";
2045
+ target;
2046
+ canPlay(src) {
2047
+ if (!isVideoSrc(src)) return false;
2048
+ return true;
2049
+ }
2050
+ mediaType() {
2051
+ return "video";
2052
+ }
2053
+ async load(ctx) {
2054
+ {
2055
+ throw Error("[vidstack] can not load video provider server-side");
2056
+ }
2057
+ }
2058
+ }
2059
+
2060
+ class HLSProviderLoader extends VideoProviderLoader {
2061
+ static supported = isHLSSupported();
2062
+ name = "hls";
2063
+ canPlay(src) {
2064
+ return HLSProviderLoader.supported && isHLSSrc(src);
2065
+ }
2066
+ async load(context) {
2067
+ {
2068
+ throw Error("[vidstack] can not load hls provider server-side");
2069
+ }
2070
+ }
2071
+ }
2072
+
2073
+ class DASHProviderLoader extends VideoProviderLoader {
2074
+ static supported = isDASHSupported();
2075
+ name = "dash";
2076
+ canPlay(src) {
2077
+ return DASHProviderLoader.supported && isDASHSrc(src);
2078
+ }
2079
+ async load(context) {
2080
+ {
2081
+ throw Error("[vidstack] can not load dash provider server-side");
2082
+ }
2083
+ }
2084
+ }
2085
+
2086
+ class VimeoProviderLoader {
2087
+ name = "vimeo";
2088
+ target;
2089
+ preconnect() {
2090
+ const connections = [
2091
+ "https://i.vimeocdn.com",
2092
+ "https://f.vimeocdn.com",
2093
+ "https://fresnel.vimeocdn.com"
2094
+ ];
2095
+ for (const url of connections) {
2096
+ }
2097
+ }
2098
+ canPlay(src) {
2099
+ return isString(src.src) && src.type === "video/vimeo";
2100
+ }
2101
+ mediaType() {
2102
+ return "video";
2103
+ }
2104
+ async load(ctx) {
2105
+ {
2106
+ throw Error("[vidstack] can not load vimeo provider server-side");
2107
+ }
2108
+ }
2109
+ async loadPoster(src, ctx, abort) {
2110
+ const { resolveVimeoVideoId, getVimeoVideoInfo } = await import('./vidstack-krOAtKMi.js');
2111
+ if (!isString(src.src)) return null;
2112
+ const { videoId, hash } = resolveVimeoVideoId(src.src);
2113
+ if (videoId) {
2114
+ return getVimeoVideoInfo(videoId, abort, hash).then((info) => info ? info.poster : null);
2115
+ }
2116
+ return null;
2117
+ }
2118
+ }
2119
+
2120
+ class YouTubeProviderLoader {
2121
+ name = "youtube";
2122
+ target;
2123
+ preconnect() {
2124
+ const connections = [
2125
+ // Botguard script.
2126
+ "https://www.google.com",
2127
+ // Posters.
2128
+ "https://i.ytimg.com",
2129
+ // Ads.
2130
+ "https://googleads.g.doubleclick.net",
2131
+ "https://static.doubleclick.net"
2132
+ ];
2133
+ for (const url of connections) {
2134
+ }
2135
+ }
2136
+ canPlay(src) {
2137
+ return isString(src.src) && src.type === "video/youtube";
2138
+ }
2139
+ mediaType() {
2140
+ return "video";
2141
+ }
2142
+ async load(ctx) {
2143
+ {
2144
+ throw Error("[vidstack] can not load youtube provider server-side");
2145
+ }
2146
+ }
2147
+ async loadPoster(src, ctx, abort) {
2148
+ const { findYouTubePoster, resolveYouTubeVideoId } = await import('./vidstack-Dm1xEU9Q.js');
2149
+ const videoId = isString(src.src) && resolveYouTubeVideoId(src.src);
2150
+ if (videoId) return findYouTubePoster(videoId, abort);
2151
+ return null;
2152
+ }
2153
+ }
2154
+
2155
+ const MEDIA_ATTRIBUTES = Symbol(0);
2156
+ const mediaAttributes = [
2157
+ "autoPlay",
2158
+ "canAirPlay",
2159
+ "canFullscreen",
2160
+ "canGoogleCast",
2161
+ "canLoad",
2162
+ "canLoadPoster",
2163
+ "canPictureInPicture",
2164
+ "canPlay",
2165
+ "canSeek",
2166
+ "ended",
2167
+ "fullscreen",
2168
+ "isAirPlayConnected",
2169
+ "isGoogleCastConnected",
2170
+ "live",
2171
+ "liveEdge",
2172
+ "loop",
2173
+ "mediaType",
2174
+ "muted",
2175
+ "paused",
2176
+ "pictureInPicture",
2177
+ "playing",
2178
+ "playsInline",
2179
+ "remotePlaybackState",
2180
+ "remotePlaybackType",
2181
+ "seeking",
2182
+ "started",
2183
+ "streamType",
2184
+ "viewType",
2185
+ "waiting"
2186
+ ];
2187
+
2188
+ const mediaPlayerProps = {
2189
+ artist: "",
2190
+ artwork: null,
2191
+ autoplay: false,
2192
+ autoPlay: false,
2193
+ clipStartTime: 0,
2194
+ clipEndTime: 0,
2195
+ controls: false,
2196
+ currentTime: 0,
2197
+ crossorigin: null,
2198
+ crossOrigin: null,
2199
+ duration: -1,
2200
+ fullscreenOrientation: "landscape",
2201
+ googleCast: {},
2202
+ load: "visible",
2203
+ posterLoad: "visible",
2204
+ logLevel: "silent",
2205
+ loop: false,
2206
+ muted: false,
2207
+ paused: true,
2208
+ playsinline: false,
2209
+ playsInline: false,
2210
+ playbackRate: 1,
2211
+ poster: "",
2212
+ preload: "metadata",
2213
+ preferNativeHLS: false,
2214
+ src: "",
2215
+ title: "",
2216
+ controlsDelay: 2e3,
2217
+ hideControlsOnMouseLeave: false,
2218
+ viewType: "unknown",
2219
+ streamType: "unknown",
2220
+ volume: 1,
2221
+ liveEdgeTolerance: 10,
2222
+ minLiveDVRWindow: 60,
2223
+ keyDisabled: false,
2224
+ keyTarget: "player",
2225
+ keyShortcuts: MEDIA_KEY_SHORTCUTS,
2226
+ storage: null
2227
+ };
2228
+
2229
+ class MediaLoadController extends MediaPlayerController {
2230
+ #type;
2231
+ #callback;
2232
+ constructor(type, callback) {
2233
+ super();
2234
+ this.#type = type;
2235
+ this.#callback = callback;
2236
+ }
2237
+ async onAttach(el) {
2238
+ return;
2239
+ }
2240
+ }
2241
+
2242
+ class MediaPlayerDelegate {
2243
+ #handle;
2244
+ #media;
2245
+ constructor(handle, media) {
2246
+ this.#handle = handle;
2247
+ this.#media = media;
2248
+ }
2249
+ notify(type, ...init) {
2250
+ return;
2251
+ }
2252
+ async ready(info, trigger) {
2253
+ return;
2254
+ }
2255
+ async #attemptAutoplay(trigger) {
2256
+ const {
2257
+ player,
2258
+ $state: { autoPlaying, muted }
2259
+ } = this.#media;
2260
+ autoPlaying.set(true);
2261
+ const attemptEvent = new DOMEvent("auto-play-attempt", { trigger });
2262
+ try {
2263
+ await player.play(attemptEvent);
2264
+ } catch (error) {
2265
+ }
2266
+ }
2267
+ }
2268
+
2269
+ class Queue {
2270
+ #queue = /* @__PURE__ */ new Map();
2271
+ /**
2272
+ * Queue the given `item` under the given `key` to be processed at a later time by calling
2273
+ * `serve(key)`.
2274
+ */
2275
+ enqueue(key, item) {
2276
+ this.#queue.set(key, item);
2277
+ }
2278
+ /**
2279
+ * Process item in queue for the given `key`.
2280
+ */
2281
+ serve(key) {
2282
+ const value = this.peek(key);
2283
+ this.#queue.delete(key);
2284
+ return value;
2285
+ }
2286
+ /**
2287
+ * Peek at item in queue for the given `key`.
2288
+ */
2289
+ peek(key) {
2290
+ return this.#queue.get(key);
2291
+ }
2292
+ /**
2293
+ * Removes queued item under the given `key`.
2294
+ */
2295
+ delete(key) {
2296
+ this.#queue.delete(key);
2297
+ }
2298
+ /**
2299
+ * Clear all items in the queue.
2300
+ */
2301
+ clear() {
2302
+ this.#queue.clear();
2303
+ }
2304
+ }
2305
+
2306
+ class RequestQueue {
2307
+ #serving = false;
2308
+ #pending = deferredPromise();
2309
+ #queue = /* @__PURE__ */ new Map();
2310
+ /**
2311
+ * The number of callbacks that are currently in queue.
2312
+ */
2313
+ get size() {
2314
+ return this.#queue.size;
2315
+ }
2316
+ /**
2317
+ * Whether items in the queue are being served immediately, otherwise they're queued to
2318
+ * be processed later.
2319
+ */
2320
+ get isServing() {
2321
+ return this.#serving;
2322
+ }
2323
+ /**
2324
+ * Waits for the queue to be flushed (ie: start serving).
2325
+ */
2326
+ async waitForFlush() {
2327
+ if (this.#serving) return;
2328
+ await this.#pending.promise;
2329
+ }
2330
+ /**
2331
+ * Queue the given `callback` to be invoked at a later time by either calling the `serve()` or
2332
+ * `start()` methods. If the queue has started serving (i.e., `start()` was already called),
2333
+ * then the callback will be invoked immediately.
2334
+ *
2335
+ * @param key - Uniquely identifies this callback so duplicates are ignored.
2336
+ * @param callback - The function to call when this item in the queue is being served.
2337
+ */
2338
+ enqueue(key, callback) {
2339
+ if (this.#serving) {
2340
+ callback();
2341
+ return;
2342
+ }
2343
+ this.#queue.delete(key);
2344
+ this.#queue.set(key, callback);
2345
+ }
2346
+ /**
2347
+ * Invokes the callback with the given `key` in the queue (if it exists).
2348
+ */
2349
+ serve(key) {
2350
+ this.#queue.get(key)?.();
2351
+ this.#queue.delete(key);
2352
+ }
2353
+ /**
2354
+ * Flush all queued items and start serving future requests immediately until `stop()` is called.
2355
+ */
2356
+ start() {
2357
+ this.#flush();
2358
+ this.#serving = true;
2359
+ if (this.#queue.size > 0) this.#flush();
2360
+ }
2361
+ /**
2362
+ * Stop serving requests, they'll be queued until you begin processing again by calling `start()`.
2363
+ */
2364
+ stop() {
2365
+ this.#serving = false;
2366
+ }
2367
+ /**
2368
+ * Stop serving requests, empty the request queue, and release any promises waiting for the
2369
+ * queue to flush.
2370
+ */
2371
+ reset() {
2372
+ this.stop();
2373
+ this.#queue.clear();
2374
+ this.#release();
2375
+ }
2376
+ #flush() {
2377
+ for (const key of this.#queue.keys()) this.serve(key);
2378
+ this.#release();
2379
+ }
2380
+ #release() {
2381
+ this.#pending.resolve();
2382
+ this.#pending = deferredPromise();
2383
+ }
2384
+ }
2385
+
2386
+ class MediaRequestManager extends MediaPlayerController {
2387
+ #stateMgr;
2388
+ #request;
2389
+ #media;
2390
+ controls;
2391
+ #fullscreen;
2392
+ #orientation;
2393
+ #$provider;
2394
+ #providerQueue = new RequestQueue();
2395
+ constructor(stateMgr, request, media) {
2396
+ super();
2397
+ this.#stateMgr = stateMgr;
2398
+ this.#request = request;
2399
+ this.#media = media;
2400
+ this.#$provider = media.$provider;
2401
+ this.controls = new MediaControls();
2402
+ this.#fullscreen = new FullscreenController();
2403
+ this.#orientation = new ScreenOrientationController();
2404
+ }
2405
+ onAttach() {
2406
+ this.listen("fullscreen-change", this.#onFullscreenChange.bind(this));
2407
+ }
2408
+ onConnect(el) {
2409
+ const names = Object.getOwnPropertyNames(Object.getPrototypeOf(this)), events = new EventsController(el), handleRequest = this.#handleRequest.bind(this);
2410
+ for (const name of names) {
2411
+ if (name.startsWith("media-")) {
2412
+ events.add(name, handleRequest);
2413
+ }
2414
+ }
2415
+ this.#attachLoadPlayListener();
2416
+ effect(this.#watchProvider.bind(this));
2417
+ effect(this.#watchControlsDelayChange.bind(this));
2418
+ effect(this.#watchAudioGainSupport.bind(this));
2419
+ effect(this.#watchAirPlaySupport.bind(this));
2420
+ effect(this.#watchGoogleCastSupport.bind(this));
2421
+ effect(this.#watchFullscreenSupport.bind(this));
2422
+ effect(this.#watchPiPSupport.bind(this));
2423
+ }
2424
+ onDestroy() {
2425
+ try {
2426
+ const destroyEvent = this.createEvent("destroy"), { pictureInPicture, fullscreen } = this.$state;
2427
+ if (fullscreen()) this.exitFullscreen("prefer-media", destroyEvent);
2428
+ if (pictureInPicture()) this.exitPictureInPicture(destroyEvent);
2429
+ } catch (e) {
2430
+ }
2431
+ this.#providerQueue.reset();
2432
+ }
2433
+ #attachLoadPlayListener() {
2434
+ const { load } = this.$props, { canLoad } = this.$state;
2435
+ if (load() !== "play" || canLoad()) return;
2436
+ const off = this.listen("media-play-request", (event) => {
2437
+ this.#handleLoadPlayStrategy(event);
2438
+ off();
2439
+ });
2440
+ }
2441
+ #watchProvider() {
2442
+ const provider = this.#$provider(), canPlay = this.$state.canPlay();
2443
+ if (provider && canPlay) {
2444
+ this.#providerQueue.start();
2445
+ }
2446
+ return () => {
2447
+ this.#providerQueue.stop();
2448
+ };
2449
+ }
2450
+ #handleRequest(event) {
2451
+ event.stopPropagation();
2452
+ if (event.defaultPrevented) return;
2453
+ if (!this[event.type]) return;
2454
+ if (peek(this.#$provider)) {
2455
+ this[event.type](event);
2456
+ } else {
2457
+ this.#providerQueue.enqueue(event.type, () => {
2458
+ if (peek(this.#$provider)) this[event.type](event);
2459
+ });
2460
+ }
2461
+ }
2462
+ async play(trigger) {
2463
+ return;
2464
+ }
2465
+ #handleLoadPlayStrategy(trigger) {
2466
+ const { load } = this.$props, { canLoad } = this.$state;
2467
+ if (load() === "play" && !canLoad()) {
2468
+ const event = this.createEvent("media-start-loading", { trigger });
2469
+ this.dispatchEvent(event);
2470
+ this.#providerQueue.enqueue("media-play-request", async () => {
2471
+ try {
2472
+ await this.play(event);
2473
+ } catch (error) {
2474
+ }
2475
+ });
2476
+ return true;
2477
+ }
2478
+ return false;
2479
+ }
2480
+ async pause(trigger) {
2481
+ return;
2482
+ }
2483
+ setAudioGain(gain, trigger) {
2484
+ const { audioGain, canSetAudioGain } = this.$state;
2485
+ if (audioGain() === gain) return;
2486
+ const provider = this.#$provider();
2487
+ if (!provider?.audioGain || !canSetAudioGain()) {
2488
+ throw Error("[vidstack] audio gain api not available");
2489
+ }
2490
+ if (trigger) {
2491
+ this.#request.queue.enqueue("media-audio-gain-change-request", trigger);
2492
+ }
2493
+ provider.audioGain.setGain(gain);
2494
+ }
2495
+ seekToLiveEdge(trigger) {
2496
+ return;
2497
+ }
2498
+ #wasPIPActive = false;
2499
+ async enterFullscreen(target = "prefer-media", trigger) {
2500
+ return;
2501
+ }
2502
+ async exitFullscreen(target = "prefer-media", trigger) {
2503
+ return;
2504
+ }
2505
+ #getFullscreenAdapter(target) {
2506
+ const provider = peek(this.#$provider);
2507
+ return target === "prefer-media" && this.#fullscreen.supported || target === "media" ? this.#fullscreen : provider?.fullscreen;
2508
+ }
2509
+ async enterPictureInPicture(trigger) {
2510
+ return;
2511
+ }
2512
+ async exitPictureInPicture(trigger) {
2513
+ return;
2514
+ }
2515
+ #throwIfPIPNotSupported() {
2516
+ if (this.$state.canPictureInPicture()) return;
2517
+ throw Error(
2518
+ "[vidstack] no pip support"
2519
+ );
2520
+ }
2521
+ #watchControlsDelayChange() {
2522
+ this.controls.defaultDelay = this.$props.controlsDelay();
2523
+ }
2524
+ #watchAudioGainSupport() {
2525
+ const { canSetAudioGain } = this.$state, supported = !!this.#$provider()?.audioGain?.supported;
2526
+ canSetAudioGain.set(supported);
2527
+ }
2528
+ #watchAirPlaySupport() {
2529
+ const { canAirPlay } = this.$state, supported = !!this.#$provider()?.airPlay?.supported;
2530
+ canAirPlay.set(supported);
2531
+ }
2532
+ #watchGoogleCastSupport() {
2533
+ const { canGoogleCast, source } = this.$state, supported = IS_CHROME;
2534
+ canGoogleCast.set(supported);
2535
+ }
2536
+ #watchFullscreenSupport() {
2537
+ const { canFullscreen } = this.$state, supported = this.#fullscreen.supported || !!this.#$provider()?.fullscreen?.supported;
2538
+ canFullscreen.set(supported);
2539
+ }
2540
+ #watchPiPSupport() {
2541
+ const { canPictureInPicture } = this.$state, supported = !!this.#$provider()?.pictureInPicture?.supported;
2542
+ canPictureInPicture.set(supported);
2543
+ }
2544
+ async ["media-airplay-request"](event) {
2545
+ try {
2546
+ await this.requestAirPlay(event);
2547
+ } catch (error) {
2548
+ }
2549
+ }
2550
+ async requestAirPlay(trigger) {
2551
+ try {
2552
+ const adapter = this.#$provider()?.airPlay;
2553
+ if (!adapter?.supported) {
2554
+ throw Error(false ? "AirPlay adapter not available on provider." : "No AirPlay adapter.");
2555
+ }
2556
+ if (trigger) {
2557
+ this.#request.queue.enqueue("media-airplay-request", trigger);
2558
+ }
2559
+ return await adapter.prompt();
2560
+ } catch (error) {
2561
+ this.#request.queue.delete("media-airplay-request");
2562
+ throw error;
2563
+ }
2564
+ }
2565
+ async ["media-google-cast-request"](event) {
2566
+ try {
2567
+ await this.requestGoogleCast(event);
2568
+ } catch (error) {
2569
+ }
2570
+ }
2571
+ #googleCastLoader;
2572
+ async requestGoogleCast(trigger) {
2573
+ try {
2574
+ const { canGoogleCast } = this.$state;
2575
+ if (!peek(canGoogleCast)) {
2576
+ const error = Error(
2577
+ false ? "Google Cast not available on this platform." : "Cast not available."
2578
+ );
2579
+ error.code = "CAST_NOT_AVAILABLE";
2580
+ throw error;
2581
+ }
2582
+ preconnect("https://www.gstatic.com");
2583
+ if (!this.#googleCastLoader) {
2584
+ const $module = await import('./vidstack-CUNv52x1.js');
2585
+ this.#googleCastLoader = new $module.GoogleCastLoader();
2586
+ }
2587
+ await this.#googleCastLoader.prompt(this.#media);
2588
+ if (trigger) {
2589
+ this.#request.queue.enqueue("media-google-cast-request", trigger);
2590
+ }
2591
+ const isConnecting = peek(this.$state.remotePlaybackState) !== "disconnected";
2592
+ if (isConnecting) {
2593
+ this.$state.savedState.set({
2594
+ paused: peek(this.$state.paused),
2595
+ currentTime: peek(this.$state.currentTime)
2596
+ });
2597
+ }
2598
+ this.$state.remotePlaybackLoader.set(isConnecting ? this.#googleCastLoader : null);
2599
+ } catch (error) {
2600
+ this.#request.queue.delete("media-google-cast-request");
2601
+ throw error;
2602
+ }
2603
+ }
2604
+ ["media-clip-start-change-request"](event) {
2605
+ const { clipStartTime } = this.$state;
2606
+ clipStartTime.set(event.detail);
2607
+ }
2608
+ ["media-clip-end-change-request"](event) {
2609
+ const { clipEndTime } = this.$state;
2610
+ clipEndTime.set(event.detail);
2611
+ this.dispatch("duration-change", {
2612
+ detail: event.detail,
2613
+ trigger: event
2614
+ });
2615
+ }
2616
+ ["media-duration-change-request"](event) {
2617
+ const { providedDuration, clipEndTime } = this.$state;
2618
+ providedDuration.set(event.detail);
2619
+ if (clipEndTime() <= 0) {
2620
+ this.dispatch("duration-change", {
2621
+ detail: event.detail,
2622
+ trigger: event
2623
+ });
2624
+ }
2625
+ }
2626
+ ["media-audio-track-change-request"](event) {
2627
+ const { logger, audioTracks } = this.#media;
2628
+ if (audioTracks.readonly) {
2629
+ return;
2630
+ }
2631
+ const index = event.detail, track = audioTracks[index];
2632
+ if (track) {
2633
+ const key = event.type;
2634
+ this.#request.queue.enqueue(key, event);
2635
+ track.selected = true;
2636
+ }
2637
+ }
2638
+ async ["media-enter-fullscreen-request"](event) {
2639
+ try {
2640
+ await this.enterFullscreen(event.detail, event);
2641
+ } catch (error) {
2642
+ this.#onFullscreenError(error, event);
2643
+ }
2644
+ }
2645
+ async ["media-exit-fullscreen-request"](event) {
2646
+ try {
2647
+ await this.exitFullscreen(event.detail, event);
2648
+ } catch (error) {
2649
+ this.#onFullscreenError(error, event);
2650
+ }
2651
+ }
2652
+ async #onFullscreenChange(event) {
2653
+ const lockType = peek(this.$props.fullscreenOrientation), isFullscreen = event.detail;
2654
+ if (isUndefined(lockType) || lockType === "none" || !this.#orientation.supported) return;
2655
+ if (isFullscreen) {
2656
+ if (this.#orientation.locked) return;
2657
+ this.dispatch("media-orientation-lock-request", {
2658
+ detail: lockType,
2659
+ trigger: event
2660
+ });
2661
+ } else if (this.#orientation.locked) {
2662
+ this.dispatch("media-orientation-unlock-request", {
2663
+ trigger: event
2664
+ });
2665
+ }
2666
+ }
2667
+ #onFullscreenError(error, request) {
2668
+ this.#stateMgr.handle(
2669
+ this.createEvent("fullscreen-error", {
2670
+ detail: coerceToError(error)
2671
+ })
2672
+ );
2673
+ }
2674
+ async ["media-orientation-lock-request"](event) {
2675
+ const key = event.type;
2676
+ try {
2677
+ this.#request.queue.enqueue(key, event);
2678
+ await this.#orientation.lock(event.detail);
2679
+ } catch (error) {
2680
+ this.#request.queue.delete(key);
2681
+ }
2682
+ }
2683
+ async ["media-orientation-unlock-request"](event) {
2684
+ const key = event.type;
2685
+ try {
2686
+ this.#request.queue.enqueue(key, event);
2687
+ await this.#orientation.unlock();
2688
+ } catch (error) {
2689
+ this.#request.queue.delete(key);
2690
+ }
2691
+ }
2692
+ async ["media-enter-pip-request"](event) {
2693
+ try {
2694
+ await this.enterPictureInPicture(event);
2695
+ } catch (error) {
2696
+ this.#onPictureInPictureError(error, event);
2697
+ }
2698
+ }
2699
+ async ["media-exit-pip-request"](event) {
2700
+ try {
2701
+ await this.exitPictureInPicture(event);
2702
+ } catch (error) {
2703
+ this.#onPictureInPictureError(error, event);
2704
+ }
2705
+ }
2706
+ #onPictureInPictureError(error, request) {
2707
+ this.#stateMgr.handle(
2708
+ this.createEvent("picture-in-picture-error", {
2709
+ detail: coerceToError(error)
2710
+ })
2711
+ );
2712
+ }
2713
+ ["media-live-edge-request"](event) {
2714
+ const { live, liveEdge, canSeek } = this.$state;
2715
+ if (!live() || liveEdge() || !canSeek()) return;
2716
+ this.#request.queue.enqueue("media-seek-request", event);
2717
+ try {
2718
+ this.seekToLiveEdge();
2719
+ } catch (error) {
2720
+ this.#request.queue.delete("media-seek-request");
2721
+ }
2722
+ }
2723
+ async ["media-loop-request"](event) {
2724
+ try {
2725
+ this.#request.looping = true;
2726
+ this.#request.replaying = true;
2727
+ await this.play(event);
2728
+ } catch (error) {
2729
+ this.#request.looping = false;
2730
+ }
2731
+ }
2732
+ ["media-user-loop-change-request"](event) {
2733
+ this.$state.userPrefersLoop.set(event.detail);
2734
+ }
2735
+ ["media-user-dual-captions-change-request"](event) {
2736
+ this.$state.userPrefersDualCaptionSeparation.set(event.detail);
2737
+ }
2738
+ async ["media-pause-request"](event) {
2739
+ if (this.$state.paused()) return;
2740
+ try {
2741
+ await this.pause(event);
2742
+ } catch (error) {
2743
+ }
2744
+ }
2745
+ async ["media-play-request"](event) {
2746
+ if (!this.$state.paused()) return;
2747
+ try {
2748
+ await this.play(event);
2749
+ } catch (e) {
2750
+ }
2751
+ }
2752
+ ["media-rate-change-request"](event) {
2753
+ const { playbackRate, canSetPlaybackRate } = this.$state;
2754
+ if (playbackRate() === event.detail || !canSetPlaybackRate()) return;
2755
+ const provider = this.#$provider();
2756
+ if (!provider?.setPlaybackRate) return;
2757
+ this.#request.queue.enqueue("media-rate-change-request", event);
2758
+ provider.setPlaybackRate(event.detail);
2759
+ }
2760
+ ["media-audio-gain-change-request"](event) {
2761
+ try {
2762
+ this.setAudioGain(event.detail, event);
2763
+ } catch (e) {
2764
+ }
2765
+ }
2766
+ ["media-quality-change-request"](event) {
2767
+ const { qualities, storage, logger } = this.#media;
2768
+ if (qualities.readonly) {
2769
+ return;
2770
+ }
2771
+ this.#request.queue.enqueue("media-quality-change-request", event);
2772
+ const index = event.detail;
2773
+ if (index < 0) {
2774
+ qualities.autoSelect(event);
2775
+ if (event.isOriginTrusted) storage?.setVideoQuality?.(null);
2776
+ } else {
2777
+ const quality = qualities[index];
2778
+ if (quality) {
2779
+ quality.selected = true;
2780
+ if (event.isOriginTrusted) {
2781
+ storage?.setVideoQuality?.({
2782
+ id: quality.id,
2783
+ width: quality.width,
2784
+ height: quality.height,
2785
+ bitrate: quality.bitrate
2786
+ });
2787
+ }
2788
+ }
2789
+ }
2790
+ }
2791
+ ["media-pause-controls-request"](event) {
2792
+ const key = event.type;
2793
+ this.#request.queue.enqueue(key, event);
2794
+ this.controls.pause(event);
2795
+ }
2796
+ ["media-resume-controls-request"](event) {
2797
+ const key = event.type;
2798
+ this.#request.queue.enqueue(key, event);
2799
+ this.controls.resume(event);
2800
+ }
2801
+ ["media-seek-request"](event) {
2802
+ const { canSeek, ended, live, seekableEnd, userBehindLiveEdge } = this.$state, seekTime = event.detail;
2803
+ if (ended()) this.#request.replaying = true;
2804
+ const key = event.type;
2805
+ this.#request.seeking = false;
2806
+ this.#request.queue.delete(key);
2807
+ const boundedTime = boundTime(seekTime, this.$state);
2808
+ if (!Number.isFinite(boundedTime) || !canSeek()) return;
2809
+ this.#request.queue.enqueue(key, event);
2810
+ this.#$provider().setCurrentTime(boundedTime);
2811
+ if (live() && event.isOriginTrusted && Math.abs(seekableEnd() - boundedTime) >= 2) {
2812
+ userBehindLiveEdge.set(true);
2813
+ }
2814
+ }
2815
+ ["media-seeking-request"](event) {
2816
+ const key = event.type;
2817
+ this.#request.queue.enqueue(key, event);
2818
+ this.$state.seeking.set(true);
2819
+ this.#request.seeking = true;
2820
+ }
2821
+ ["media-start-loading"](event) {
2822
+ if (this.$state.canLoad()) return;
2823
+ const key = event.type;
2824
+ this.#request.queue.enqueue(key, event);
2825
+ this.#stateMgr.handle(this.createEvent("can-load"));
2826
+ }
2827
+ ["media-poster-start-loading"](event) {
2828
+ if (this.$state.canLoadPoster()) return;
2829
+ const key = event.type;
2830
+ this.#request.queue.enqueue(key, event);
2831
+ this.#stateMgr.handle(this.createEvent("can-load-poster"));
2832
+ }
2833
+ ["media-text-track-change-request"](event) {
2834
+ const { index, mode } = event.detail, track = this.#media.textTracks[index];
2835
+ if (track) {
2836
+ const key = event.type;
2837
+ this.#request.queue.enqueue(key, event);
2838
+ track.setMode(mode, event);
2839
+ }
2840
+ }
2841
+ ["media-mute-request"](event) {
2842
+ if (this.$state.muted()) return;
2843
+ const key = event.type;
2844
+ this.#request.queue.enqueue(key, event);
2845
+ this.#$provider().setMuted(true);
2846
+ }
2847
+ ["media-unmute-request"](event) {
2848
+ const { muted, volume } = this.$state;
2849
+ if (!muted()) return;
2850
+ const key = event.type;
2851
+ this.#request.queue.enqueue(key, event);
2852
+ this.#media.$provider().setMuted(false);
2853
+ if (volume() === 0) {
2854
+ this.#request.queue.enqueue(key, event);
2855
+ this.#$provider().setVolume(0.25);
2856
+ }
2857
+ }
2858
+ ["media-volume-change-request"](event) {
2859
+ const { muted, volume } = this.$state;
2860
+ const newVolume = event.detail;
2861
+ if (volume() === newVolume) return;
2862
+ const key = event.type;
2863
+ this.#request.queue.enqueue(key, event);
2864
+ this.#$provider().setVolume(newVolume);
2865
+ if (newVolume > 0 && muted()) {
2866
+ this.#request.queue.enqueue(key, event);
2867
+ this.#$provider().setMuted(false);
2868
+ }
2869
+ }
2870
+ #logError(title, error, request) {
2871
+ return;
2872
+ }
2873
+ }
2874
+ class MediaRequestContext {
2875
+ seeking = false;
2876
+ looping = false;
2877
+ replaying = false;
2878
+ queue = new Queue();
2879
+ }
2880
+
2881
+ class MediaStateManager extends MediaPlayerController {
2882
+ #request;
2883
+ #media;
2884
+ #trackedEvents = /* @__PURE__ */ new Map();
2885
+ #clipEnded = false;
2886
+ #playedIntervals = [];
2887
+ #playedInterval = [-1, -1];
2888
+ #firingWaiting = false;
2889
+ #waitingTrigger;
2890
+ constructor(request, media) {
2891
+ super();
2892
+ this.#request = request;
2893
+ this.#media = media;
2894
+ }
2895
+ onAttach(el) {
2896
+ el.setAttribute("aria-busy", "true");
2897
+ new EventsController(this).add("fullscreen-change", this["fullscreen-change"].bind(this)).add("fullscreen-error", this["fullscreen-error"].bind(this)).add("orientation-change", this["orientation-change"].bind(this));
2898
+ }
2899
+ onConnect(el) {
2900
+ effect(this.#watchCanSetVolume.bind(this));
2901
+ this.#addTextTrackListeners();
2902
+ this.#addQualityListeners();
2903
+ this.#addAudioTrackListeners();
2904
+ this.#resumePlaybackOnConnect();
2905
+ onDispose(this.#pausePlaybackOnDisconnect.bind(this));
2906
+ }
2907
+ onDestroy() {
2908
+ const { audioTracks, qualities, textTracks } = this.#media;
2909
+ audioTracks[ListSymbol.reset]();
2910
+ qualities[ListSymbol.reset]();
2911
+ textTracks[ListSymbol.reset]();
2912
+ this.#stopWatchingQualityResize();
2913
+ }
2914
+ handle(event) {
2915
+ if (!this.scope) return;
2916
+ event.type;
2917
+ untrack(() => this[event.type]?.(event));
2918
+ }
2919
+ #isPlayingOnDisconnect = false;
2920
+ #resumePlaybackOnConnect() {
2921
+ if (!this.#isPlayingOnDisconnect) return;
2922
+ requestAnimationFrame(() => {
2923
+ if (!this.scope) return;
2924
+ this.#media.remote.play(new DOMEvent("dom-connect"));
2925
+ });
2926
+ this.#isPlayingOnDisconnect = false;
2927
+ }
2928
+ #pausePlaybackOnDisconnect() {
2929
+ if (this.#isPlayingOnDisconnect) return;
2930
+ this.#isPlayingOnDisconnect = !this.$state.paused();
2931
+ this.#media.$provider()?.pause();
2932
+ }
2933
+ #resetTracking() {
2934
+ this.#stopWaiting();
2935
+ this.#clipEnded = false;
2936
+ this.#request.replaying = false;
2937
+ this.#request.looping = false;
2938
+ this.#firingWaiting = false;
2939
+ this.#waitingTrigger = void 0;
2940
+ this.#trackedEvents.clear();
2941
+ }
2942
+ #satisfyRequest(request, event) {
2943
+ const requestEvent = this.#request.queue.serve(request);
2944
+ if (!requestEvent) return;
2945
+ event.request = requestEvent;
2946
+ event.triggers.add(requestEvent);
2947
+ }
2948
+ #addTextTrackListeners() {
2949
+ this.#onTextTracksChange();
2950
+ this.#onTextTrackModeChange();
2951
+ const textTracks = this.#media.textTracks;
2952
+ new EventsController(textTracks).add("add", this.#onTextTracksChange.bind(this)).add("remove", this.#onTextTracksChange.bind(this)).add("mode-change", this.#onTextTrackModeChange.bind(this));
2953
+ }
2954
+ #addQualityListeners() {
2955
+ const qualities = this.#media.qualities;
2956
+ new EventsController(qualities).add("add", this.#onQualitiesChange.bind(this)).add("remove", this.#onQualitiesChange.bind(this)).add("change", this.#onQualityChange.bind(this)).add("auto-change", this.#onAutoQualityChange.bind(this)).add("readonly-change", this.#onCanSetQualityChange.bind(this));
2957
+ }
2958
+ #addAudioTrackListeners() {
2959
+ const audioTracks = this.#media.audioTracks;
2960
+ new EventsController(audioTracks).add("add", this.#onAudioTracksChange.bind(this)).add("remove", this.#onAudioTracksChange.bind(this)).add("change", this.#onAudioTrackChange.bind(this));
2961
+ }
2962
+ #onTextTracksChange(event) {
2963
+ const { textTracks } = this.$state;
2964
+ textTracks.set(this.#media.textTracks.toArray());
2965
+ this.dispatch("text-tracks-change", {
2966
+ detail: textTracks(),
2967
+ trigger: event
2968
+ });
2969
+ }
2970
+ #onTextTrackModeChange(event) {
2971
+ if (event) this.#satisfyRequest("media-text-track-change-request", event);
2972
+ const current = this.#media.textTracks.selected, { textTrack } = this.$state;
2973
+ if (textTrack() !== current) {
2974
+ textTrack.set(current);
2975
+ this.dispatch("text-track-change", {
2976
+ detail: current,
2977
+ trigger: event
2978
+ });
2979
+ }
2980
+ }
2981
+ #onAudioTracksChange(event) {
2982
+ const { audioTracks } = this.$state;
2983
+ audioTracks.set(this.#media.audioTracks.toArray());
2984
+ this.dispatch("audio-tracks-change", {
2985
+ detail: audioTracks(),
2986
+ trigger: event
2987
+ });
2988
+ }
2989
+ #onAudioTrackChange(event) {
2990
+ const { audioTrack } = this.$state;
2991
+ audioTrack.set(this.#media.audioTracks.selected);
2992
+ if (event) this.#satisfyRequest("media-audio-track-change-request", event);
2993
+ this.dispatch("audio-track-change", {
2994
+ detail: audioTrack(),
2995
+ trigger: event
2996
+ });
2997
+ }
2998
+ #onQualitiesChange(event) {
2999
+ const { qualities } = this.$state;
3000
+ qualities.set(this.#media.qualities.toArray());
3001
+ this.dispatch("qualities-change", {
3002
+ detail: qualities(),
3003
+ trigger: event
3004
+ });
3005
+ }
3006
+ #onQualityChange(event) {
3007
+ const { quality } = this.$state;
3008
+ quality.set(this.#media.qualities.selected);
3009
+ if (event) this.#satisfyRequest("media-quality-change-request", event);
3010
+ this.dispatch("quality-change", {
3011
+ detail: quality(),
3012
+ trigger: event
3013
+ });
3014
+ }
3015
+ #onAutoQualityChange() {
3016
+ const { qualities } = this.#media, isAuto = qualities.auto;
3017
+ this.$state.autoQuality.set(isAuto);
3018
+ if (!isAuto) this.#stopWatchingQualityResize();
3019
+ }
3020
+ #stopQualityResizeEffect = null;
3021
+ #watchQualityResize() {
3022
+ this.#stopWatchingQualityResize();
3023
+ this.#stopQualityResizeEffect = effect(() => {
3024
+ const { qualities } = this.#media, { mediaWidth, mediaHeight } = this.$state, w = mediaWidth(), h = mediaHeight();
3025
+ if (w === 0 || h === 0) return;
3026
+ let selectedQuality = null, minScore = Infinity;
3027
+ for (const quality of qualities) {
3028
+ const score = Math.abs(quality.width - w) + Math.abs(quality.height - h);
3029
+ if (score < minScore) {
3030
+ minScore = score;
3031
+ selectedQuality = quality;
3032
+ }
3033
+ }
3034
+ if (selectedQuality) {
3035
+ qualities[ListSymbol.select](
3036
+ selectedQuality,
3037
+ true,
3038
+ new DOMEvent("resize", { detail: { width: w, height: h } })
3039
+ );
3040
+ }
3041
+ });
3042
+ }
3043
+ #stopWatchingQualityResize() {
3044
+ this.#stopQualityResizeEffect?.();
3045
+ this.#stopQualityResizeEffect = null;
3046
+ }
3047
+ #onCanSetQualityChange() {
3048
+ this.$state.canSetQuality.set(!this.#media.qualities.readonly);
3049
+ }
3050
+ #watchCanSetVolume() {
3051
+ const { canSetVolume, isGoogleCastConnected } = this.$state;
3052
+ if (isGoogleCastConnected()) {
3053
+ canSetVolume.set(false);
3054
+ return;
3055
+ }
3056
+ canChangeVolume().then(canSetVolume.set);
3057
+ }
3058
+ ["provider-change"](event) {
3059
+ const prevProvider = this.#media.$provider(), newProvider = event.detail;
3060
+ if (prevProvider?.type === newProvider?.type) return;
3061
+ prevProvider?.destroy?.();
3062
+ prevProvider?.scope?.dispose();
3063
+ this.#media.$provider.set(event.detail);
3064
+ if (prevProvider && event.detail === null) {
3065
+ this.#resetMediaState(event);
3066
+ }
3067
+ }
3068
+ ["provider-loader-change"](event) {
3069
+ }
3070
+ ["auto-play"](event) {
3071
+ this.$state.autoPlayError.set(null);
3072
+ }
3073
+ ["auto-play-fail"](event) {
3074
+ this.$state.autoPlayError.set(event.detail);
3075
+ this.#resetTracking();
3076
+ }
3077
+ ["can-load"](event) {
3078
+ this.$state.canLoad.set(true);
3079
+ this.#trackedEvents.set("can-load", event);
3080
+ this.#media.textTracks[TextTrackSymbol.canLoad]();
3081
+ this.#satisfyRequest("media-start-loading", event);
3082
+ }
3083
+ ["can-load-poster"](event) {
3084
+ this.$state.canLoadPoster.set(true);
3085
+ this.#trackedEvents.set("can-load-poster", event);
3086
+ this.#satisfyRequest("media-poster-start-loading", event);
3087
+ }
3088
+ ["media-type-change"](event) {
3089
+ const sourceChangeEvent = this.#trackedEvents.get("source-change");
3090
+ if (sourceChangeEvent) event.triggers.add(sourceChangeEvent);
3091
+ const viewType = this.$state.viewType();
3092
+ this.$state.mediaType.set(event.detail);
3093
+ const providedViewType = this.$state.providedViewType(), currentViewType = providedViewType === "unknown" ? event.detail : providedViewType;
3094
+ if (viewType !== currentViewType) {
3095
+ {
3096
+ this.$state.inferredViewType.set(currentViewType);
3097
+ }
3098
+ }
3099
+ }
3100
+ ["stream-type-change"](event) {
3101
+ const sourceChangeEvent = this.#trackedEvents.get("source-change");
3102
+ if (sourceChangeEvent) event.triggers.add(sourceChangeEvent);
3103
+ const { streamType, inferredStreamType } = this.$state;
3104
+ inferredStreamType.set(event.detail);
3105
+ event.detail = streamType();
3106
+ }
3107
+ ["rate-change"](event) {
3108
+ const { storage } = this.#media, { canPlay } = this.$state;
3109
+ this.$state.playbackRate.set(event.detail);
3110
+ this.#satisfyRequest("media-rate-change-request", event);
3111
+ if (canPlay()) {
3112
+ storage?.setPlaybackRate?.(event.detail);
3113
+ }
3114
+ }
3115
+ ["remote-playback-change"](event) {
3116
+ const { remotePlaybackState, remotePlaybackType } = this.$state, { type, state } = event.detail, isConnected = state === "connected";
3117
+ remotePlaybackType.set(type);
3118
+ remotePlaybackState.set(state);
3119
+ const key = type === "airplay" ? "media-airplay-request" : "media-google-cast-request";
3120
+ if (isConnected) {
3121
+ this.#satisfyRequest(key, event);
3122
+ } else {
3123
+ const requestEvent = this.#request.queue.peek(key);
3124
+ if (requestEvent) {
3125
+ event.request = requestEvent;
3126
+ event.triggers.add(requestEvent);
3127
+ }
3128
+ }
3129
+ }
3130
+ ["sources-change"](event) {
3131
+ const prevSources = this.$state.sources(), newSources = event.detail;
3132
+ this.$state.sources.set(newSources);
3133
+ this.#onSourceQualitiesChange(prevSources, newSources, event);
3134
+ }
3135
+ #onSourceQualitiesChange(prevSources, newSources, trigger) {
3136
+ let { qualities } = this.#media, added = false, removed = false;
3137
+ for (const prevSrc of prevSources) {
3138
+ if (!isVideoQualitySrc(prevSrc)) continue;
3139
+ const exists = newSources.some((s) => s.src === prevSrc.src);
3140
+ if (!exists) {
3141
+ const quality = qualities.getBySrc(prevSrc.src);
3142
+ if (quality) {
3143
+ qualities[ListSymbol.remove](quality, trigger);
3144
+ removed = true;
3145
+ }
3146
+ }
3147
+ }
3148
+ if (removed && !qualities.length) {
3149
+ this.$state.savedState.set(null);
3150
+ qualities[ListSymbol.reset](trigger);
3151
+ }
3152
+ for (const src of newSources) {
3153
+ if (!isVideoQualitySrc(src) || qualities.getBySrc(src.src)) continue;
3154
+ const quality = {
3155
+ id: src.id ?? src.height + "p",
3156
+ bitrate: null,
3157
+ codec: null,
3158
+ ...src,
3159
+ selected: false
3160
+ };
3161
+ qualities[ListSymbol.add](quality, trigger);
3162
+ added = true;
3163
+ }
3164
+ if (added && !qualities[QualitySymbol.enableAuto]) {
3165
+ this.#watchQualityResize();
3166
+ qualities[QualitySymbol.enableAuto] = this.#watchQualityResize.bind(this);
3167
+ qualities[QualitySymbol.setAuto](true, trigger);
3168
+ }
3169
+ }
3170
+ ["source-change"](event) {
3171
+ event.isQualityChange = event.originEvent?.type === "quality-change";
3172
+ const source = event.detail;
3173
+ this.#resetMediaState(event, event.isQualityChange);
3174
+ this.#trackedEvents.set(event.type, event);
3175
+ this.$state.source.set(source);
3176
+ this.el?.setAttribute("aria-busy", "true");
3177
+ }
3178
+ #resetMediaState(event, isSourceQualityChange = false) {
3179
+ const { audioTracks, qualities } = this.#media;
3180
+ if (!isSourceQualityChange) {
3181
+ this.#playedIntervals = [];
3182
+ this.#playedInterval = [-1, -1];
3183
+ audioTracks[ListSymbol.reset](event);
3184
+ qualities[ListSymbol.reset](event);
3185
+ softResetMediaState(this.$state, isSourceQualityChange);
3186
+ this.#resetTracking();
3187
+ return;
3188
+ }
3189
+ softResetMediaState(this.$state, isSourceQualityChange);
3190
+ this.#resetTracking();
3191
+ }
3192
+ ["abort"](event) {
3193
+ const sourceChangeEvent = this.#trackedEvents.get("source-change");
3194
+ if (sourceChangeEvent) event.triggers.add(sourceChangeEvent);
3195
+ const canLoadEvent = this.#trackedEvents.get("can-load");
3196
+ if (canLoadEvent && !event.triggers.hasType("can-load")) {
3197
+ event.triggers.add(canLoadEvent);
3198
+ }
3199
+ }
3200
+ ["load-start"](event) {
3201
+ const sourceChangeEvent = this.#trackedEvents.get("source-change");
3202
+ if (sourceChangeEvent) event.triggers.add(sourceChangeEvent);
3203
+ }
3204
+ ["error"](event) {
3205
+ this.$state.error.set(event.detail);
3206
+ const abortEvent = this.#trackedEvents.get("abort");
3207
+ if (abortEvent) event.triggers.add(abortEvent);
3208
+ }
3209
+ ["loaded-metadata"](event) {
3210
+ const loadStartEvent = this.#trackedEvents.get("load-start");
3211
+ if (loadStartEvent) event.triggers.add(loadStartEvent);
3212
+ }
3213
+ ["loaded-data"](event) {
3214
+ const loadStartEvent = this.#trackedEvents.get("load-start");
3215
+ if (loadStartEvent) event.triggers.add(loadStartEvent);
3216
+ }
3217
+ ["can-play"](event) {
3218
+ const loadedMetadata = this.#trackedEvents.get("loaded-metadata");
3219
+ if (loadedMetadata) event.triggers.add(loadedMetadata);
3220
+ this.#onCanPlayDetail(event.detail);
3221
+ this.el?.setAttribute("aria-busy", "false");
3222
+ }
3223
+ ["can-play-through"](event) {
3224
+ this.#onCanPlayDetail(event.detail);
3225
+ const canPlay = this.#trackedEvents.get("can-play");
3226
+ if (canPlay) event.triggers.add(canPlay);
3227
+ }
3228
+ #onCanPlayDetail(detail) {
3229
+ const { seekable, buffered, intrinsicDuration, canPlay } = this.$state;
3230
+ canPlay.set(true);
3231
+ buffered.set(detail.buffered);
3232
+ seekable.set(detail.seekable);
3233
+ const seekableEnd = getTimeRangesEnd(detail.seekable) ?? Infinity;
3234
+ intrinsicDuration.set(seekableEnd);
3235
+ }
3236
+ ["duration-change"](event) {
3237
+ const { live, intrinsicDuration, providedDuration, clipEndTime, ended } = this.$state, time = event.detail;
3238
+ if (!live()) {
3239
+ const duration = !Number.isNaN(time) ? time : 0;
3240
+ intrinsicDuration.set(duration);
3241
+ if (ended()) this.#onEndPrecisionChange(event);
3242
+ }
3243
+ if (providedDuration() > 0 || clipEndTime() > 0) {
3244
+ event.stopImmediatePropagation();
3245
+ }
3246
+ }
3247
+ ["progress"](event) {
3248
+ const { buffered, seekable } = this.$state, { buffered: newBuffered, seekable: newSeekable } = event.detail, newBufferedEnd = getTimeRangesEnd(newBuffered), hasBufferedLengthChanged = newBuffered.length !== buffered().length, hasBufferedEndChanged = newBufferedEnd !== getTimeRangesEnd(buffered()), newSeekableEnd = getTimeRangesEnd(newSeekable), hasSeekableLengthChanged = newSeekable.length !== seekable().length, hasSeekableEndChanged = newSeekableEnd !== getTimeRangesEnd(seekable());
3249
+ if (hasBufferedLengthChanged || hasBufferedEndChanged) {
3250
+ buffered.set(newBuffered);
3251
+ }
3252
+ if (hasSeekableLengthChanged || hasSeekableEndChanged) {
3253
+ seekable.set(newSeekable);
3254
+ }
3255
+ }
3256
+ ["play"](event) {
3257
+ const {
3258
+ paused,
3259
+ autoPlayError,
3260
+ ended,
3261
+ autoPlaying,
3262
+ playsInline,
3263
+ pointer,
3264
+ muted,
3265
+ viewType,
3266
+ live,
3267
+ userBehindLiveEdge
3268
+ } = this.$state;
3269
+ this.#resetPlaybackIfNeeded();
3270
+ if (!paused()) {
3271
+ event.stopImmediatePropagation();
3272
+ return;
3273
+ }
3274
+ event.autoPlay = autoPlaying();
3275
+ const waitingEvent = this.#trackedEvents.get("waiting");
3276
+ if (waitingEvent) event.triggers.add(waitingEvent);
3277
+ this.#satisfyRequest("media-play-request", event);
3278
+ this.#trackedEvents.set("play", event);
3279
+ paused.set(false);
3280
+ autoPlayError.set(null);
3281
+ if (event.autoPlay) {
3282
+ this.handle(
3283
+ this.createEvent("auto-play", {
3284
+ detail: { muted: muted() },
3285
+ trigger: event
3286
+ })
3287
+ );
3288
+ autoPlaying.set(false);
3289
+ }
3290
+ if (ended() || this.#request.replaying) {
3291
+ this.#request.replaying = false;
3292
+ ended.set(false);
3293
+ this.handle(this.createEvent("replay", { trigger: event }));
3294
+ }
3295
+ if (!playsInline() && viewType() === "video" && pointer() === "coarse") {
3296
+ this.#media.remote.enterFullscreen("prefer-media", event);
3297
+ }
3298
+ if (live() && !userBehindLiveEdge()) {
3299
+ this.#media.remote.seekToLiveEdge(event);
3300
+ }
3301
+ }
3302
+ #resetPlaybackIfNeeded(trigger) {
3303
+ const provider = peek(this.#media.$provider);
3304
+ if (!provider) return;
3305
+ const { ended, seekableStart, clipEndTime, currentTime, realCurrentTime, duration } = this.$state;
3306
+ const shouldReset = ended() || realCurrentTime() < seekableStart() || clipEndTime() > 0 && realCurrentTime() >= clipEndTime() || Math.abs(currentTime() - duration()) < 0.1;
3307
+ if (shouldReset) {
3308
+ this.dispatch("media-seek-request", {
3309
+ detail: seekableStart(),
3310
+ trigger
3311
+ });
3312
+ }
3313
+ return shouldReset;
3314
+ }
3315
+ ["play-fail"](event) {
3316
+ const { muted, autoPlaying } = this.$state;
3317
+ const playEvent = this.#trackedEvents.get("play");
3318
+ if (playEvent) event.triggers.add(playEvent);
3319
+ this.#satisfyRequest("media-play-request", event);
3320
+ const { paused, playing } = this.$state;
3321
+ paused.set(true);
3322
+ playing.set(false);
3323
+ this.#resetTracking();
3324
+ this.#trackedEvents.set("play-fail", event);
3325
+ if (event.autoPlay) {
3326
+ this.handle(
3327
+ this.createEvent("auto-play-fail", {
3328
+ detail: {
3329
+ muted: muted(),
3330
+ error: event.detail
3331
+ },
3332
+ trigger: event
3333
+ })
3334
+ );
3335
+ autoPlaying.set(false);
3336
+ }
3337
+ }
3338
+ ["playing"](event) {
3339
+ const playEvent = this.#trackedEvents.get("play"), seekedEvent = this.#trackedEvents.get("seeked");
3340
+ if (playEvent) event.triggers.add(playEvent);
3341
+ else if (seekedEvent) event.triggers.add(seekedEvent);
3342
+ setTimeout(() => this.#resetTracking(), 0);
3343
+ const {
3344
+ paused,
3345
+ playing,
3346
+ live,
3347
+ liveSyncPosition,
3348
+ seekableEnd,
3349
+ started,
3350
+ currentTime,
3351
+ seeking,
3352
+ ended
3353
+ } = this.$state;
3354
+ paused.set(false);
3355
+ playing.set(true);
3356
+ seeking.set(false);
3357
+ ended.set(false);
3358
+ if (this.#request.looping) {
3359
+ this.#request.looping = false;
3360
+ return;
3361
+ }
3362
+ if (live() && !started() && currentTime() === 0) {
3363
+ const end = liveSyncPosition() ?? seekableEnd() - 2;
3364
+ if (Number.isFinite(end)) this.#media.$provider().setCurrentTime(end);
3365
+ }
3366
+ this["started"](event);
3367
+ }
3368
+ ["started"](event) {
3369
+ const { started } = this.$state;
3370
+ if (!started()) {
3371
+ started.set(true);
3372
+ this.handle(this.createEvent("started", { trigger: event }));
3373
+ }
3374
+ }
3375
+ ["pause"](event) {
3376
+ if (!this.el?.isConnected) {
3377
+ this.#isPlayingOnDisconnect = true;
3378
+ }
3379
+ this.#satisfyRequest("media-pause-request", event);
3380
+ const seekedEvent = this.#trackedEvents.get("seeked");
3381
+ if (seekedEvent) event.triggers.add(seekedEvent);
3382
+ const { paused, playing } = this.$state;
3383
+ paused.set(true);
3384
+ playing.set(false);
3385
+ if (this.#clipEnded) {
3386
+ setTimeout(() => {
3387
+ this.handle(this.createEvent("end", { trigger: event }));
3388
+ this.#clipEnded = false;
3389
+ }, 0);
3390
+ }
3391
+ this.#resetTracking();
3392
+ }
3393
+ ["time-change"](event) {
3394
+ if (this.#request.looping) {
3395
+ event.stopImmediatePropagation();
3396
+ return;
3397
+ }
3398
+ let { waiting, played, clipEndTime, realCurrentTime, currentTime } = this.$state, newTime = event.detail, endTime = clipEndTime();
3399
+ realCurrentTime.set(newTime);
3400
+ this.#updatePlayed();
3401
+ waiting.set(false);
3402
+ for (const track of this.#media.textTracks) {
3403
+ track[TextTrackSymbol.updateActiveCues](newTime, event);
3404
+ }
3405
+ if (endTime > 0 && newTime >= endTime) {
3406
+ this.#clipEnded = true;
3407
+ this.dispatch("media-pause-request", { trigger: event });
3408
+ }
3409
+ this.#saveTime();
3410
+ this.dispatch("time-update", {
3411
+ detail: { currentTime: currentTime(), played: played() },
3412
+ trigger: event
3413
+ });
3414
+ }
3415
+ #updatePlayed() {
3416
+ const { currentTime, played, paused } = this.$state;
3417
+ if (paused()) return;
3418
+ this.#playedInterval = updateTimeIntervals(
3419
+ this.#playedIntervals,
3420
+ this.#playedInterval,
3421
+ currentTime()
3422
+ );
3423
+ played.set(new TimeRange(this.#playedIntervals));
3424
+ }
3425
+ // Called to update time again incase duration precision has changed.
3426
+ #onEndPrecisionChange(trigger) {
3427
+ const { clipStartTime, clipEndTime, duration } = this.$state, isClipped = clipStartTime() > 0 || clipEndTime() > 0;
3428
+ if (isClipped) return;
3429
+ this.handle(
3430
+ this.createEvent("time-change", {
3431
+ detail: duration(),
3432
+ trigger
3433
+ })
3434
+ );
3435
+ }
3436
+ #saveTime() {
3437
+ const { storage } = this.#media, { canPlay, realCurrentTime } = this.$state;
3438
+ if (canPlay()) {
3439
+ storage?.setTime?.(realCurrentTime());
3440
+ }
3441
+ }
3442
+ ["audio-gain-change"](event) {
3443
+ const { storage } = this.#media, { canPlay, audioGain } = this.$state;
3444
+ audioGain.set(event.detail);
3445
+ this.#satisfyRequest("media-audio-gain-change-request", event);
3446
+ if (canPlay()) storage?.setAudioGain?.(audioGain());
3447
+ }
3448
+ ["volume-change"](event) {
3449
+ const { storage } = this.#media, { volume, muted, canPlay } = this.$state, detail = event.detail;
3450
+ volume.set(detail.volume);
3451
+ muted.set(detail.muted || detail.volume === 0);
3452
+ this.#satisfyRequest("media-volume-change-request", event);
3453
+ this.#satisfyRequest(detail.muted ? "media-mute-request" : "media-unmute-request", event);
3454
+ if (canPlay()) {
3455
+ storage?.setVolume?.(volume());
3456
+ storage?.setMuted?.(muted());
3457
+ }
3458
+ }
3459
+ ["seeking"] = functionThrottle(
3460
+ (event) => {
3461
+ const { seeking, realCurrentTime, paused } = this.$state;
3462
+ seeking.set(true);
3463
+ realCurrentTime.set(event.detail);
3464
+ this.#satisfyRequest("media-seeking-request", event);
3465
+ if (paused()) {
3466
+ this.#waitingTrigger = event;
3467
+ this.#fireWaiting();
3468
+ }
3469
+ this.#playedInterval = [-1, -1];
3470
+ },
3471
+ 150,
3472
+ { leading: true }
3473
+ );
3474
+ ["seeked"](event) {
3475
+ const { seeking, currentTime, realCurrentTime, paused, seekableEnd, ended, live } = this.$state;
3476
+ if (this.#request.seeking) {
3477
+ seeking.set(true);
3478
+ event.stopImmediatePropagation();
3479
+ } else if (seeking()) {
3480
+ const waitingEvent = this.#trackedEvents.get("waiting");
3481
+ if (waitingEvent) event.triggers.add(waitingEvent);
3482
+ const seekingEvent = this.#trackedEvents.get("seeking");
3483
+ if (seekingEvent && !event.triggers.has(seekingEvent)) {
3484
+ event.triggers.add(seekingEvent);
3485
+ }
3486
+ if (paused()) this.#stopWaiting();
3487
+ seeking.set(false);
3488
+ realCurrentTime.set(event.detail);
3489
+ this.#satisfyRequest("media-seek-request", event);
3490
+ const origin = event?.originEvent;
3491
+ if (origin?.isTrusted && !(origin instanceof MessageEvent) && !/seek/.test(origin.type)) {
3492
+ this["started"](event);
3493
+ }
3494
+ }
3495
+ if (!live()) {
3496
+ if (Math.floor(currentTime()) !== Math.floor(seekableEnd())) {
3497
+ ended.set(false);
3498
+ } else {
3499
+ this.end(event);
3500
+ }
3501
+ }
3502
+ }
3503
+ ["waiting"](event) {
3504
+ if (this.#firingWaiting || this.#request.seeking) return;
3505
+ event.stopImmediatePropagation();
3506
+ this.#waitingTrigger = event;
3507
+ this.#fireWaiting();
3508
+ }
3509
+ #fireWaiting = functionDebounce(() => {
3510
+ if (!this.#waitingTrigger) return;
3511
+ this.#firingWaiting = true;
3512
+ const { waiting, playing } = this.$state;
3513
+ waiting.set(true);
3514
+ playing.set(false);
3515
+ const event = this.createEvent("waiting", { trigger: this.#waitingTrigger });
3516
+ this.#trackedEvents.set("waiting", event);
3517
+ this.dispatch(event);
3518
+ this.#waitingTrigger = void 0;
3519
+ this.#firingWaiting = false;
3520
+ }, 300);
3521
+ ["end"](event) {
3522
+ const { loop, ended } = this.$state;
3523
+ if (!loop() && ended()) return;
3524
+ if (loop()) {
3525
+ setTimeout(() => {
3526
+ requestAnimationFrame(() => {
3527
+ this.#resetPlaybackIfNeeded(event);
3528
+ this.dispatch("media-loop-request", { trigger: event });
3529
+ });
3530
+ }, 10);
3531
+ return;
3532
+ }
3533
+ setTimeout(() => this.#onEnded(event), 0);
3534
+ }
3535
+ #onEnded(event) {
3536
+ const { storage } = this.#media, { paused, seeking, ended, duration } = this.$state;
3537
+ this.#onEndPrecisionChange(event);
3538
+ if (!paused()) {
3539
+ this.dispatch("pause", { trigger: event });
3540
+ }
3541
+ if (seeking()) {
3542
+ this.dispatch("seeked", {
3543
+ detail: duration(),
3544
+ trigger: event
3545
+ });
3546
+ }
3547
+ ended.set(true);
3548
+ this.#resetTracking();
3549
+ storage?.setTime?.(duration(), true);
3550
+ this.dispatch("ended", {
3551
+ trigger: event
3552
+ });
3553
+ }
3554
+ #stopWaiting() {
3555
+ this.#fireWaiting.cancel();
3556
+ this.$state.waiting.set(false);
3557
+ }
3558
+ ["fullscreen-change"](event) {
3559
+ const isFullscreen = event.detail;
3560
+ this.$state.fullscreen.set(isFullscreen);
3561
+ this.#satisfyRequest(
3562
+ isFullscreen ? "media-enter-fullscreen-request" : "media-exit-fullscreen-request",
3563
+ event
3564
+ );
3565
+ }
3566
+ ["fullscreen-error"](event) {
3567
+ this.#satisfyRequest("media-enter-fullscreen-request", event);
3568
+ this.#satisfyRequest("media-exit-fullscreen-request", event);
3569
+ }
3570
+ ["orientation-change"](event) {
3571
+ const isLocked = event.detail.lock;
3572
+ this.#satisfyRequest(
3573
+ isLocked ? "media-orientation-lock-request" : "media-orientation-unlock-request",
3574
+ event
3575
+ );
3576
+ }
3577
+ ["picture-in-picture-change"](event) {
3578
+ const isPiP = event.detail;
3579
+ this.$state.pictureInPicture.set(isPiP);
3580
+ this.#satisfyRequest(isPiP ? "media-enter-pip-request" : "media-exit-pip-request", event);
3581
+ }
3582
+ ["picture-in-picture-error"](event) {
3583
+ this.#satisfyRequest("media-enter-pip-request", event);
3584
+ this.#satisfyRequest("media-exit-pip-request", event);
3585
+ }
3586
+ ["title-change"](event) {
3587
+ if (!event.trigger) return;
3588
+ event.stopImmediatePropagation();
3589
+ this.$state.inferredTitle.set(event.detail);
3590
+ }
3591
+ ["poster-change"](event) {
3592
+ if (!event.trigger) return;
3593
+ event.stopImmediatePropagation();
3594
+ this.$state.inferredPoster.set(event.detail);
3595
+ }
3596
+ }
3597
+
3598
+ class MediaStateSync extends MediaPlayerController {
3599
+ onSetup() {
3600
+ this.#init();
3601
+ return;
3602
+ }
3603
+ #init() {
3604
+ const providedProps = {
3605
+ duration: "providedDuration",
3606
+ loop: "providedLoop",
3607
+ poster: "providedPoster",
3608
+ streamType: "providedStreamType",
3609
+ title: "providedTitle",
3610
+ viewType: "providedViewType"
3611
+ };
3612
+ const skip = /* @__PURE__ */ new Set([
3613
+ "currentTime",
3614
+ "paused",
3615
+ "playbackRate",
3616
+ "volume"
3617
+ ]);
3618
+ for (const prop of Object.keys(this.$props)) {
3619
+ if (skip.has(prop)) continue;
3620
+ this.$state[providedProps[prop] ?? prop]?.set(this.$props[prop]());
3621
+ }
3622
+ this.$state.muted.set(this.$props.muted() || this.$props.volume() === 0);
3623
+ }
3624
+ // Sync "provided" props with internal state. Provided props are used to differentiate from
3625
+ // provider inferred values.
3626
+ #watchProvidedTypes() {
3627
+ const { viewType, streamType, title, poster, loop } = this.$props, $state = this.$state;
3628
+ $state.providedPoster.set(poster());
3629
+ $state.providedStreamType.set(streamType());
3630
+ $state.providedViewType.set(viewType());
3631
+ $state.providedTitle.set(title());
3632
+ $state.providedLoop.set(loop());
3633
+ }
3634
+ #watchLogLevel() {
3635
+ return;
3636
+ }
3637
+ #watchMetadata() {
3638
+ const { artist, artwork } = this.$props;
3639
+ this.$state.artist.set(artist());
3640
+ this.$state.artwork.set(artwork());
3641
+ }
3642
+ #watchTitle() {
3643
+ const { title } = this.$state;
3644
+ this.dispatch("title-change", { detail: title() });
3645
+ }
3646
+ #watchAutoplay() {
3647
+ const autoPlay = this.$props.autoPlay() || this.$props.autoplay();
3648
+ this.$state.autoPlay.set(autoPlay);
3649
+ this.dispatch("auto-play-change", { detail: autoPlay });
3650
+ }
3651
+ #watchLoop() {
3652
+ const loop = this.$state.loop();
3653
+ this.dispatch("loop-change", { detail: loop });
3654
+ }
3655
+ #watchControls() {
3656
+ const controls = this.$props.controls();
3657
+ this.$state.controls.set(controls);
3658
+ }
3659
+ #watchPoster() {
3660
+ const { poster } = this.$state;
3661
+ this.dispatch("poster-change", { detail: poster() });
3662
+ }
3663
+ #watchCrossOrigin() {
3664
+ const crossOrigin = this.$props.crossOrigin() ?? this.$props.crossorigin(), value = crossOrigin === true ? "" : crossOrigin;
3665
+ this.$state.crossOrigin.set(value);
3666
+ }
3667
+ #watchDuration() {
3668
+ const { duration } = this.$props;
3669
+ this.dispatch("media-duration-change-request", {
3670
+ detail: duration()
3671
+ });
3672
+ }
3673
+ #watchPlaysInline() {
3674
+ const inline = this.$props.playsInline() || this.$props.playsinline();
3675
+ this.$state.playsInline.set(inline);
3676
+ this.dispatch("plays-inline-change", { detail: inline });
3677
+ }
3678
+ #watchClipStartTime() {
3679
+ const { clipStartTime } = this.$props;
3680
+ this.dispatch("media-clip-start-change-request", {
3681
+ detail: clipStartTime()
3682
+ });
3683
+ }
3684
+ #watchClipEndTime() {
3685
+ const { clipEndTime } = this.$props;
3686
+ this.dispatch("media-clip-end-change-request", {
3687
+ detail: clipEndTime()
3688
+ });
3689
+ }
3690
+ #watchLive() {
3691
+ this.dispatch("live-change", { detail: this.$state.live() });
3692
+ }
3693
+ #watchLiveTolerance() {
3694
+ this.$state.liveEdgeTolerance.set(this.$props.liveEdgeTolerance());
3695
+ this.$state.minLiveDVRWindow.set(this.$props.minLiveDVRWindow());
3696
+ }
3697
+ #watchLiveEdge() {
3698
+ this.dispatch("live-edge-change", { detail: this.$state.liveEdge() });
3699
+ }
3700
+ }
3701
+
3702
+ const actions = ["play", "pause", "seekforward", "seekbackward", "seekto"];
3703
+ class NavigatorMediaSession extends MediaPlayerController {
3704
+ onConnect() {
3705
+ effect(this.#onMetadataChange.bind(this));
3706
+ effect(this.#onPlaybackStateChange.bind(this));
3707
+ const handleAction = this.#handleAction.bind(this);
3708
+ for (const action of actions) {
3709
+ navigator.mediaSession.setActionHandler(action, handleAction);
3710
+ }
3711
+ onDispose(this.#onDisconnect.bind(this));
3712
+ }
3713
+ #onDisconnect() {
3714
+ for (const action of actions) {
3715
+ navigator.mediaSession.setActionHandler(action, null);
3716
+ }
3717
+ }
3718
+ #onMetadataChange() {
3719
+ const { title, artist, artwork, poster } = this.$state;
3720
+ navigator.mediaSession.metadata = new MediaMetadata({
3721
+ title: title(),
3722
+ artist: artist(),
3723
+ artwork: artwork() ?? [{ src: poster() }]
3724
+ });
3725
+ }
3726
+ #onPlaybackStateChange() {
3727
+ const { canPlay, paused } = this.$state;
3728
+ navigator.mediaSession.playbackState = !canPlay() ? "none" : paused() ? "paused" : "playing";
3729
+ }
3730
+ #handleAction(details) {
3731
+ const trigger = new DOMEvent(`media-session-action`, { detail: details });
3732
+ switch (details.action) {
3733
+ case "play":
3734
+ this.dispatch("media-play-request", { trigger });
3735
+ break;
3736
+ case "pause":
3737
+ this.dispatch("media-pause-request", { trigger });
3738
+ break;
3739
+ case "seekto":
3740
+ case "seekforward":
3741
+ case "seekbackward":
3742
+ this.dispatch("media-seek-request", {
3743
+ detail: isNumber(details.seekTime) ? details.seekTime : this.$state.currentTime() + (details.seekOffset ?? (details.action === "seekforward" ? 10 : -10)),
3744
+ trigger
3745
+ });
3746
+ break;
3747
+ }
3748
+ }
3749
+ }
3750
+
3751
+ class MediaPlayer extends Component {
3752
+ static props = mediaPlayerProps;
3753
+ static state = mediaState;
3754
+ #media;
3755
+ #stateMgr;
3756
+ #requestMgr;
3757
+ canPlayQueue = new RequestQueue();
3758
+ remoteControl;
3759
+ get #provider() {
3760
+ return this.#media.$provider();
3761
+ }
3762
+ get #props() {
3763
+ return this.$props;
3764
+ }
3765
+ constructor() {
3766
+ super();
3767
+ new MediaStateSync();
3768
+ const context = {
3769
+ player: this,
3770
+ qualities: new VideoQualityList(),
3771
+ audioTracks: new AudioTrackList(),
3772
+ storage: null,
3773
+ $provider: signal(null),
3774
+ $providerSetup: signal(false),
3775
+ $props: this.$props,
3776
+ $state: this.$state
3777
+ };
3778
+ context.remote = this.remoteControl = new MediaRemoteControl(
3779
+ void 0
3780
+ );
3781
+ context.remote.setPlayer(this);
3782
+ context.textTracks = new TextTrackList();
3783
+ context.textTracks[TextTrackSymbol.crossOrigin] = this.$state.crossOrigin;
3784
+ context.textRenderers = new TextRenderers(context);
3785
+ context.ariaKeys = {};
3786
+ this.#media = context;
3787
+ provideContext(mediaContext, context);
3788
+ this.orientation = new ScreenOrientationController();
3789
+ new FocusVisibleController();
3790
+ new MediaKeyboardController(context);
3791
+ const request = new MediaRequestContext();
3792
+ this.#stateMgr = new MediaStateManager(request, context);
3793
+ this.#requestMgr = new MediaRequestManager(this.#stateMgr, request, context);
3794
+ context.delegate = new MediaPlayerDelegate(this.#stateMgr.handle.bind(this.#stateMgr), context);
3795
+ context.notify = context.delegate.notify.bind(context.delegate);
3796
+ if (typeof navigator !== "undefined" && "mediaSession" in navigator) {
3797
+ new NavigatorMediaSession();
3798
+ }
3799
+ new MediaLoadController("load", this.startLoading.bind(this));
3800
+ new MediaLoadController("posterLoad", this.startLoadingPoster.bind(this));
3801
+ }
3802
+ onSetup() {
3803
+ this.#setupMediaAttributes();
3804
+ effect(this.#watchCanPlay.bind(this));
3805
+ effect(this.#watchMuted.bind(this));
3806
+ effect(this.#watchPaused.bind(this));
3807
+ effect(this.#watchVolume.bind(this));
3808
+ effect(this.#watchCurrentTime.bind(this));
3809
+ effect(this.#watchPlaysInline.bind(this));
3810
+ effect(this.#watchPlaybackRate.bind(this));
3811
+ }
3812
+ onAttach(el) {
3813
+ el.setAttribute("data-media-player", "");
3814
+ setAttributeIfEmpty(el, "tabindex", "0");
3815
+ setAttributeIfEmpty(el, "role", "region");
3816
+ effect(this.#watchStorage.bind(this));
3817
+ this.#watchTitle();
3818
+ this.#watchOrientation();
3819
+ listenEvent(el, "find-media-player", this.#onFindPlayer.bind(this));
3820
+ }
3821
+ onConnect(el) {
3822
+ const pointerQuery = window.matchMedia("(pointer: coarse)");
3823
+ this.#onPointerChange(pointerQuery);
3824
+ pointerQuery.onchange = this.#onPointerChange.bind(this);
3825
+ const resize = new ResizeObserver(animationFrameThrottle(this.#onResize.bind(this)));
3826
+ resize.observe(el);
3827
+ effect(this.#onResize.bind(this));
3828
+ this.dispatch("media-player-connect", {
3829
+ detail: this,
3830
+ bubbles: true,
3831
+ composed: true
3832
+ });
3833
+ onDispose(() => {
3834
+ resize.disconnect();
3835
+ pointerQuery.onchange = null;
3836
+ });
3837
+ }
3838
+ onDestroy() {
3839
+ this.#media.player = null;
3840
+ this.canPlayQueue.reset();
3841
+ }
3842
+ #skipTitleUpdate = false;
3843
+ #watchTitle() {
3844
+ this.$el; const { title, live, viewType, providedTitle } = this.$state, isLive = live(), type = uppercaseFirstChar(viewType()), typeText = type !== "Unknown" ? `${isLive ? "Live " : ""}${type}` : isLive ? "Live" : "Media", currentTitle = title();
3845
+ setAttribute(
3846
+ this.el,
3847
+ "aria-label",
3848
+ `${typeText} Player` + (currentTitle ? ` - ${currentTitle}` : "")
3849
+ );
3850
+ }
3851
+ #watchOrientation() {
3852
+ const orientation = this.orientation.landscape ? "landscape" : "portrait";
3853
+ this.$state.orientation.set(orientation);
3854
+ setAttribute(this.el, "data-orientation", orientation);
3855
+ this.#onResize();
3856
+ }
3857
+ #watchCanPlay() {
3858
+ if (this.$state.canPlay() && this.#provider) this.canPlayQueue.start();
3859
+ else this.canPlayQueue.stop();
3860
+ }
3861
+ #setupMediaAttributes() {
3862
+ if (MediaPlayer[MEDIA_ATTRIBUTES]) {
3863
+ this.setAttributes(MediaPlayer[MEDIA_ATTRIBUTES]);
3864
+ return;
3865
+ }
3866
+ const $attrs = {
3867
+ "data-load": function() {
3868
+ return this.$props.load();
3869
+ },
3870
+ "data-captions": function() {
3871
+ const track = this.$state.textTrack();
3872
+ return !!track && isTrackCaptionKind(track);
3873
+ },
3874
+ "data-ios-controls": function() {
3875
+ return this.$state.iOSControls();
3876
+ },
3877
+ "data-controls": function() {
3878
+ return this.controls.showing;
3879
+ },
3880
+ "data-buffering": function() {
3881
+ const { canLoad, canPlay, waiting } = this.$state;
3882
+ return canLoad() && (!canPlay() || waiting());
3883
+ },
3884
+ "data-error": function() {
3885
+ const { error } = this.$state;
3886
+ return !!error();
3887
+ },
3888
+ "data-autoplay-error": function() {
3889
+ const { autoPlayError } = this.$state;
3890
+ return !!autoPlayError();
3891
+ }
3892
+ };
3893
+ const alias = {
3894
+ autoPlay: "autoplay",
3895
+ canAirPlay: "can-airplay",
3896
+ canPictureInPicture: "can-pip",
3897
+ pictureInPicture: "pip",
3898
+ playsInline: "playsinline",
3899
+ remotePlaybackState: "remote-state",
3900
+ remotePlaybackType: "remote-type",
3901
+ isAirPlayConnected: "airplay",
3902
+ isGoogleCastConnected: "google-cast"
3903
+ };
3904
+ for (const prop2 of mediaAttributes) {
3905
+ const attrName = "data-" + (alias[prop2] ?? camelToKebabCase(prop2));
3906
+ $attrs[attrName] = function() {
3907
+ return this.$state[prop2]();
3908
+ };
3909
+ }
3910
+ delete $attrs.title;
3911
+ MediaPlayer[MEDIA_ATTRIBUTES] = $attrs;
3912
+ this.setAttributes($attrs);
3913
+ }
3914
+ #onFindPlayer(event) {
3915
+ event.detail(this);
3916
+ }
3917
+ #onResize() {
3918
+ return;
3919
+ }
3920
+ #onPointerChange(queryList) {
3921
+ return;
3922
+ }
3923
+ /**
3924
+ * The current media provider.
3925
+ */
3926
+ get provider() {
3927
+ return this.#provider;
3928
+ }
3929
+ /**
3930
+ * Media controls settings.
3931
+ */
3932
+ get controls() {
3933
+ return this.#requestMgr.controls;
3934
+ }
3935
+ set controls(controls) {
3936
+ this.#props.controls.set(controls);
3937
+ }
3938
+ /**
3939
+ * Controls the screen orientation of the current browser window and dispatches orientation
3940
+ * change events on the player.
3941
+ */
3942
+ orientation;
3943
+ /**
3944
+ * The title of the current media.
3945
+ */
3946
+ get title() {
3947
+ return peek(this.$state.title);
3948
+ }
3949
+ set title(newTitle) {
3950
+ if (this.#skipTitleUpdate) {
3951
+ this.#skipTitleUpdate = false;
3952
+ return;
3953
+ }
3954
+ this.#props.title.set(newTitle);
3955
+ }
3956
+ /**
3957
+ * A list of all `VideoQuality` objects representing the set of available video renditions.
3958
+ *
3959
+ * @see {@link https://vidstack.io/docs/player/api/video-quality}
3960
+ */
3961
+ get qualities() {
3962
+ return this.#media.qualities;
3963
+ }
3964
+ /**
3965
+ * A list of all `AudioTrack` objects representing the set of available audio tracks.
3966
+ *
3967
+ * @see {@link https://vidstack.io/docs/player/api/audio-tracks}
3968
+ */
3969
+ get audioTracks() {
3970
+ return this.#media.audioTracks;
3971
+ }
3972
+ /**
3973
+ * A list of all `TextTrack` objects representing the set of available text tracks.
3974
+ *
3975
+ * @see {@link https://vidstack.io/docs/player/api/text-tracks}
3976
+ */
3977
+ get textTracks() {
3978
+ return this.#media.textTracks;
3979
+ }
3980
+ /**
3981
+ * Contains text renderers which are responsible for loading, parsing, and rendering text
3982
+ * tracks.
3983
+ */
3984
+ get textRenderers() {
3985
+ return this.#media.textRenderers;
3986
+ }
3987
+ get duration() {
3988
+ return this.$state.duration();
3989
+ }
3990
+ set duration(duration) {
3991
+ this.#props.duration.set(duration);
3992
+ }
3993
+ get paused() {
3994
+ return peek(this.$state.paused);
3995
+ }
3996
+ set paused(paused) {
3997
+ this.#queuePausedUpdate(paused);
3998
+ }
3999
+ #watchPaused() {
4000
+ this.#queuePausedUpdate(this.$props.paused());
4001
+ }
4002
+ #queuePausedUpdate(paused) {
4003
+ if (paused) {
4004
+ this.canPlayQueue.enqueue("paused", () => this.#requestMgr.pause());
4005
+ } else this.canPlayQueue.enqueue("paused", () => this.#requestMgr.play());
4006
+ }
4007
+ get muted() {
4008
+ return peek(this.$state.muted);
4009
+ }
4010
+ set muted(muted) {
4011
+ this.#queueMutedUpdate(muted);
4012
+ }
4013
+ #watchMuted() {
4014
+ this.#queueMutedUpdate(this.$props.muted());
4015
+ }
4016
+ #queueMutedUpdate(muted) {
4017
+ this.canPlayQueue.enqueue("muted", () => {
4018
+ if (this.#provider) this.#provider.setMuted(muted);
4019
+ });
4020
+ }
4021
+ get currentTime() {
4022
+ return peek(this.$state.currentTime);
4023
+ }
4024
+ set currentTime(time) {
4025
+ this.#queueCurrentTimeUpdate(time);
4026
+ }
4027
+ #watchCurrentTime() {
4028
+ this.#queueCurrentTimeUpdate(this.$props.currentTime());
4029
+ }
4030
+ #queueCurrentTimeUpdate(time) {
4031
+ this.canPlayQueue.enqueue("currentTime", () => {
4032
+ const { currentTime } = this.$state;
4033
+ if (time === peek(currentTime)) return;
4034
+ peek(() => {
4035
+ if (!this.#provider) return;
4036
+ const boundedTime = boundTime(time, this.$state);
4037
+ if (Number.isFinite(boundedTime)) {
4038
+ this.#provider.setCurrentTime(boundedTime);
4039
+ }
4040
+ });
4041
+ });
4042
+ }
4043
+ get volume() {
4044
+ return peek(this.$state.volume);
4045
+ }
4046
+ set volume(volume) {
4047
+ this.#queueVolumeUpdate(volume);
4048
+ }
4049
+ #watchVolume() {
4050
+ this.#queueVolumeUpdate(this.$props.volume());
4051
+ }
4052
+ #queueVolumeUpdate(volume) {
4053
+ const clampedVolume = clampNumber(0, volume, 1);
4054
+ this.canPlayQueue.enqueue("volume", () => {
4055
+ if (this.#provider) this.#provider.setVolume(clampedVolume);
4056
+ });
4057
+ }
4058
+ get playbackRate() {
4059
+ return peek(this.$state.playbackRate);
4060
+ }
4061
+ set playbackRate(rate) {
4062
+ this.#queuePlaybackRateUpdate(rate);
4063
+ }
4064
+ #watchPlaybackRate() {
4065
+ this.#queuePlaybackRateUpdate(this.$props.playbackRate());
4066
+ }
4067
+ #queuePlaybackRateUpdate(rate) {
4068
+ this.canPlayQueue.enqueue("rate", () => {
4069
+ if (this.#provider) this.#provider.setPlaybackRate?.(rate);
4070
+ });
4071
+ }
4072
+ #watchPlaysInline() {
4073
+ this.#queuePlaysInlineUpdate(this.$props.playsInline());
4074
+ }
4075
+ #queuePlaysInlineUpdate(inline) {
4076
+ this.canPlayQueue.enqueue("playsinline", () => {
4077
+ if (this.#provider) this.#provider.setPlaysInline?.(inline);
4078
+ });
4079
+ }
4080
+ #watchStorage() {
4081
+ let storageValue = this.$props.storage(), storage = isString(storageValue) ? new LocalMediaStorage() : storageValue;
4082
+ if (storage?.onChange) {
4083
+ const { source } = this.$state, playerId = isString(storageValue) ? storageValue : this.el?.id, mediaId = computed(this.#computeMediaId.bind(this));
4084
+ effect(() => storage.onChange(source(), mediaId(), playerId || void 0));
4085
+ }
4086
+ this.#media.storage = storage;
4087
+ this.#media.textTracks.setStorage(storage);
4088
+ onDispose(() => {
4089
+ storage?.onDestroy?.();
4090
+ this.#media.storage = null;
4091
+ this.#media.textTracks.setStorage(null);
4092
+ });
4093
+ }
4094
+ #computeMediaId() {
4095
+ const { clipStartTime, clipEndTime } = this.$props, { source } = this.$state, src = source();
4096
+ return src.src ? `${src.src}:${clipStartTime()}:${clipEndTime()}` : null;
4097
+ }
4098
+ /**
4099
+ * Begins/resumes playback of the media. If this method is called programmatically before the
4100
+ * user has interacted with the player, the promise may be rejected subject to the browser's
4101
+ * autoplay policies. This method will throw if called before media is ready for playback.
4102
+ *
4103
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play}
4104
+ */
4105
+ async play(trigger) {
4106
+ return this.#requestMgr.play(trigger);
4107
+ }
4108
+ /**
4109
+ * Pauses playback of the media. This method will throw if called before media is ready for
4110
+ * playback.
4111
+ *
4112
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/pause}
4113
+ */
4114
+ async pause(trigger) {
4115
+ return this.#requestMgr.pause(trigger);
4116
+ }
4117
+ /**
4118
+ * Attempts to display the player in fullscreen. The promise will resolve if successful, and
4119
+ * reject if not. This method will throw if any fullscreen API is _not_ currently available.
4120
+ *
4121
+ * @see {@link https://vidstack.io/docs/player/api/fullscreen}
4122
+ */
4123
+ async enterFullscreen(target, trigger) {
4124
+ return this.#requestMgr.enterFullscreen(target, trigger);
4125
+ }
4126
+ /**
4127
+ * Attempts to display the player inline by exiting fullscreen. This method will throw if any
4128
+ * fullscreen API is _not_ currently available.
4129
+ *
4130
+ * @see {@link https://vidstack.io/docs/player/api/fullscreen}
4131
+ */
4132
+ async exitFullscreen(target, trigger) {
4133
+ return this.#requestMgr.exitFullscreen(target, trigger);
4134
+ }
4135
+ /**
4136
+ * Attempts to display the player in picture-in-picture mode. This method will throw if PIP is
4137
+ * not supported. This method will also return a `PictureInPictureWindow` if the current
4138
+ * provider supports it.
4139
+ *
4140
+ * @see {@link https://vidstack.io/docs/player/api/picture-in-picture}
4141
+ */
4142
+ enterPictureInPicture(trigger) {
4143
+ return this.#requestMgr.enterPictureInPicture(trigger);
4144
+ }
4145
+ /**
4146
+ * Attempts to display the player in inline by exiting picture-in-picture mode. This method
4147
+ * will throw if not supported.
4148
+ *
4149
+ * @see {@link https://vidstack.io/docs/player/api/picture-in-picture}
4150
+ */
4151
+ exitPictureInPicture(trigger) {
4152
+ return this.#requestMgr.exitPictureInPicture(trigger);
4153
+ }
4154
+ /**
4155
+ * Sets the current time to the live edge (i.e., `duration`). This is a no-op for non-live
4156
+ * streams and will throw if called before media is ready for playback.
4157
+ *
4158
+ * @see {@link https://vidstack.io/docs/player/api/live}
4159
+ */
4160
+ seekToLiveEdge(trigger) {
4161
+ this.#requestMgr.seekToLiveEdge(trigger);
4162
+ }
4163
+ /**
4164
+ * Called when media can begin loading. Calling this method will trigger the initial provider
4165
+ * loading process. Calling it more than once has no effect.
4166
+ *
4167
+ * @see {@link https://vidstack.io/docs/player/core-concepts/loading#load-strategies}
4168
+ */
4169
+ startLoading(trigger) {
4170
+ this.#media.notify("can-load", void 0, trigger);
4171
+ }
4172
+ /**
4173
+ * Called when the poster image can begin loading. Calling it more than once has no effect.
4174
+ *
4175
+ * @see {@link https://vidstack.io/docs/player/core-concepts/loading#load-strategies}
4176
+ */
4177
+ startLoadingPoster(trigger) {
4178
+ this.#media.notify("can-load-poster", void 0, trigger);
4179
+ }
4180
+ /**
4181
+ * Request Apple AirPlay picker to open.
4182
+ */
4183
+ requestAirPlay(trigger) {
4184
+ return this.#requestMgr.requestAirPlay(trigger);
4185
+ }
4186
+ /**
4187
+ * Request Google Cast device picker to open. The Google Cast framework will be loaded if it
4188
+ * hasn't yet.
4189
+ */
4190
+ requestGoogleCast(trigger) {
4191
+ return this.#requestMgr.requestGoogleCast(trigger);
4192
+ }
4193
+ /**
4194
+ * Set the audio gain, amplifying volume and enabling a maximum volume above 100%.
4195
+ *
4196
+ * @see {@link https://vidstack.io/docs/player/api/audio-gain}
4197
+ */
4198
+ setAudioGain(gain, trigger) {
4199
+ return this.#requestMgr.setAudioGain(gain, trigger);
4200
+ }
4201
+ destroy() {
4202
+ super.destroy();
4203
+ this.#media.remote.setPlayer(null);
4204
+ this.dispatch("destroy");
4205
+ }
4206
+ }
4207
+ const mediaplayer__proto = MediaPlayer.prototype;
4208
+ prop(mediaplayer__proto, "canPlayQueue");
4209
+ prop(mediaplayer__proto, "remoteControl");
4210
+ prop(mediaplayer__proto, "provider");
4211
+ prop(mediaplayer__proto, "controls");
4212
+ prop(mediaplayer__proto, "orientation");
4213
+ prop(mediaplayer__proto, "title");
4214
+ prop(mediaplayer__proto, "qualities");
4215
+ prop(mediaplayer__proto, "audioTracks");
4216
+ prop(mediaplayer__proto, "textTracks");
4217
+ prop(mediaplayer__proto, "textRenderers");
4218
+ prop(mediaplayer__proto, "duration");
4219
+ prop(mediaplayer__proto, "paused");
4220
+ prop(mediaplayer__proto, "muted");
4221
+ prop(mediaplayer__proto, "currentTime");
4222
+ prop(mediaplayer__proto, "volume");
4223
+ prop(mediaplayer__proto, "playbackRate");
4224
+ method(mediaplayer__proto, "play");
4225
+ method(mediaplayer__proto, "pause");
4226
+ method(mediaplayer__proto, "enterFullscreen");
4227
+ method(mediaplayer__proto, "exitFullscreen");
4228
+ method(mediaplayer__proto, "enterPictureInPicture");
4229
+ method(mediaplayer__proto, "exitPictureInPicture");
4230
+ method(mediaplayer__proto, "seekToLiveEdge");
4231
+ method(mediaplayer__proto, "startLoading");
4232
+ method(mediaplayer__proto, "startLoadingPoster");
4233
+ method(mediaplayer__proto, "requestAirPlay");
4234
+ method(mediaplayer__proto, "requestGoogleCast");
4235
+ method(mediaplayer__proto, "setAudioGain");
4236
+
4237
+ function resolveStreamTypeFromDASHManifest(manifestSrc, requestInit) {
4238
+ return fetch(manifestSrc, requestInit).then((res) => res.text()).then((manifest) => {
4239
+ return /type="static"/.test(manifest) ? "on-demand" : "live";
4240
+ });
4241
+ }
4242
+ function resolveStreamTypeFromHLSManifest(manifestSrc, requestInit) {
4243
+ return fetch(manifestSrc, requestInit).then((res) => res.text()).then((manifest) => {
4244
+ const renditionURI = resolveHLSRenditionURI(manifest);
4245
+ if (renditionURI) {
4246
+ return resolveStreamTypeFromHLSManifest(
4247
+ /^https?:/.test(renditionURI) ? renditionURI : new URL(renditionURI, manifestSrc).href,
4248
+ requestInit
4249
+ );
4250
+ }
4251
+ const streamType = /EXT-X-PLAYLIST-TYPE:\s*VOD/.test(manifest) ? "on-demand" : "live";
4252
+ if (streamType === "live" && resolveTargetDuration(manifest) >= 10 && (/#EXT-X-DVR-ENABLED:\s*true/.test(manifest) || manifest.includes("#EXT-X-DISCONTINUITY"))) {
4253
+ return "live:dvr";
4254
+ }
4255
+ return streamType;
4256
+ });
4257
+ }
4258
+ function resolveHLSRenditionURI(manifest) {
4259
+ const matches = manifest.match(/#EXT-X-STREAM-INF:[^\n]+(\n[^\n]+)*/g);
4260
+ return matches ? matches[0].split("\n")[1].trim() : null;
4261
+ }
4262
+ function resolveTargetDuration(manifest) {
4263
+ const lines = manifest.split("\n");
4264
+ for (const line of lines) {
4265
+ if (line.startsWith("#EXT-X-TARGETDURATION")) {
4266
+ const duration = parseFloat(line.split(":")[1]);
4267
+ if (!isNaN(duration)) {
4268
+ return duration;
4269
+ }
4270
+ }
4271
+ }
4272
+ return -1;
4273
+ }
4274
+
4275
+ const sourceTypes = /* @__PURE__ */ new Map();
4276
+ class SourceSelection {
4277
+ #initialize = false;
4278
+ #loaders;
4279
+ #domSources;
4280
+ #media;
4281
+ #loader;
4282
+ constructor(domSources, media, loader, customLoaders = []) {
4283
+ this.#domSources = domSources;
4284
+ this.#media = media;
4285
+ this.#loader = loader;
4286
+ const DASH_LOADER = new DASHProviderLoader(), HLS_LOADER = new HLSProviderLoader(), VIDEO_LOADER = new VideoProviderLoader(), AUDIO_LOADER = new AudioProviderLoader(), YOUTUBE_LOADER = new YouTubeProviderLoader(), VIMEO_LOADER = new VimeoProviderLoader(), EMBED_LOADERS = [YOUTUBE_LOADER, VIMEO_LOADER];
4287
+ this.#loaders = computed(() => {
4288
+ const remoteLoader = media.$state.remotePlaybackLoader();
4289
+ const loaders = media.$props.preferNativeHLS() ? [VIDEO_LOADER, AUDIO_LOADER, DASH_LOADER, HLS_LOADER, ...EMBED_LOADERS, ...customLoaders] : [HLS_LOADER, VIDEO_LOADER, AUDIO_LOADER, DASH_LOADER, ...EMBED_LOADERS, ...customLoaders];
4290
+ return remoteLoader ? [remoteLoader, ...loaders] : loaders;
4291
+ });
4292
+ const { $state } = media;
4293
+ $state.sources.set(normalizeSrc(media.$props.src()));
4294
+ for (const src of $state.sources()) {
4295
+ const loader2 = this.#loaders().find((loader3) => loader3.canPlay(src));
4296
+ if (!loader2) continue;
4297
+ const mediaType = loader2.mediaType(src);
4298
+ media.$state.source.set(src);
4299
+ media.$state.mediaType.set(mediaType);
4300
+ media.$state.inferredViewType.set(mediaType);
4301
+ this.#loader.set(loader2);
4302
+ this.#initialize = true;
4303
+ break;
4304
+ }
4305
+ }
4306
+ connect() {
4307
+ const loader = this.#loader();
4308
+ if (this.#initialize) {
4309
+ this.#notifySourceChange(this.#media.$state.source(), loader);
4310
+ this.#notifyLoaderChange(loader);
4311
+ this.#initialize = false;
4312
+ }
4313
+ effect(this.#onSourcesChange.bind(this));
4314
+ effect(this.#onSourceChange.bind(this));
4315
+ effect(this.#onSetup.bind(this));
4316
+ effect(this.#onLoadSource.bind(this));
4317
+ effect(this.#onLoadPoster.bind(this));
4318
+ }
4319
+ #onSourcesChange() {
4320
+ this.#media.notify("sources-change", [
4321
+ ...normalizeSrc(this.#media.$props.src()),
4322
+ ...this.#domSources()
4323
+ ]);
4324
+ }
4325
+ #onSourceChange() {
4326
+ const { $state } = this.#media;
4327
+ const sources = $state.sources(), currentSource = peek($state.source), newSource = this.#findNewSource(currentSource, sources), noMatch = sources[0]?.src && !newSource.src && !newSource.type;
4328
+ if (noMatch) {
4329
+ const { crossOrigin } = $state, credentials = getRequestCredentials(crossOrigin()), abort = new AbortController();
4330
+ Promise.all(
4331
+ sources.map(
4332
+ (source) => isString(source.src) && source.type === "?" ? fetch(source.src, {
4333
+ method: "HEAD",
4334
+ credentials,
4335
+ signal: abort.signal
4336
+ }).then((res) => {
4337
+ source.type = res.headers.get("content-type") || "??";
4338
+ sourceTypes.set(source.src, source.type);
4339
+ return source;
4340
+ }).catch(() => source) : source
4341
+ )
4342
+ ).then((sources2) => {
4343
+ if (abort.signal.aborted) return;
4344
+ const newSource2 = this.#findNewSource(peek($state.source), sources2);
4345
+ tick();
4346
+ if (!newSource2.src) {
4347
+ this.#media.notify("error", {
4348
+ message: "Failed to load resource.",
4349
+ code: 4
4350
+ });
4351
+ }
4352
+ });
4353
+ return () => abort.abort();
4354
+ }
4355
+ tick();
4356
+ }
4357
+ #findNewSource(currentSource, sources) {
4358
+ let newSource = { src: "", type: "" }, newLoader = null, triggerEvent = new DOMEvent("sources-change", { detail: { sources } }), loaders = this.#loaders(), { started, paused, currentTime, quality, savedState } = this.#media.$state;
4359
+ for (const src of sources) {
4360
+ const loader = loaders.find((loader2) => loader2.canPlay(src));
4361
+ if (loader) {
4362
+ newSource = src;
4363
+ newLoader = loader;
4364
+ break;
4365
+ }
4366
+ }
4367
+ if (isVideoQualitySrc(newSource)) {
4368
+ const currentQuality = quality(), sourceQuality = sources.find((s) => s.src === currentQuality?.src);
4369
+ if (peek(started)) {
4370
+ savedState.set({
4371
+ paused: peek(paused),
4372
+ currentTime: peek(currentTime)
4373
+ });
4374
+ } else {
4375
+ savedState.set(null);
4376
+ }
4377
+ if (sourceQuality) {
4378
+ newSource = sourceQuality;
4379
+ triggerEvent = new DOMEvent("quality-change", {
4380
+ detail: { quality: currentQuality }
4381
+ });
4382
+ }
4383
+ }
4384
+ if (!isSameSrc(currentSource, newSource)) {
4385
+ this.#notifySourceChange(newSource, newLoader, triggerEvent);
4386
+ }
4387
+ if (newLoader !== peek(this.#loader)) {
4388
+ this.#notifyLoaderChange(newLoader, triggerEvent);
4389
+ }
4390
+ return newSource;
4391
+ }
4392
+ #notifySourceChange(src, loader, trigger) {
4393
+ this.#media.notify("source-change", src, trigger);
4394
+ this.#media.notify("media-type-change", loader?.mediaType(src) || "unknown", trigger);
4395
+ }
4396
+ #notifyLoaderChange(loader, trigger) {
4397
+ this.#media.$providerSetup.set(false);
4398
+ this.#media.notify("provider-change", null, trigger);
4399
+ loader && peek(() => loader.preconnect?.(this.#media));
4400
+ this.#loader.set(loader);
4401
+ this.#media.notify("provider-loader-change", loader, trigger);
4402
+ }
4403
+ #onSetup() {
4404
+ const provider = this.#media.$provider();
4405
+ if (!provider || peek(this.#media.$providerSetup)) return;
4406
+ if (this.#media.$state.canLoad()) {
4407
+ scoped(() => provider.setup(), provider.scope);
4408
+ this.#media.$providerSetup.set(true);
4409
+ return;
4410
+ }
4411
+ peek(() => provider.preconnect?.());
4412
+ }
4413
+ #onLoadSource() {
4414
+ if (!this.#media.$providerSetup()) return;
4415
+ const provider = this.#media.$provider(), source = this.#media.$state.source(), crossOrigin = peek(this.#media.$state.crossOrigin), preferNativeHLS = peek(this.#media.$props.preferNativeHLS);
4416
+ if (isSameSrc(provider?.currentSrc, source)) {
4417
+ return;
4418
+ }
4419
+ if (this.#media.$state.canLoad()) {
4420
+ const abort = new AbortController();
4421
+ if (isHLSSrc(source)) {
4422
+ if (preferNativeHLS || !isHLSSupported()) {
4423
+ resolveStreamTypeFromHLSManifest(source.src, {
4424
+ credentials: getRequestCredentials(crossOrigin),
4425
+ signal: abort.signal
4426
+ }).then((streamType) => {
4427
+ this.#media.notify("stream-type-change", streamType);
4428
+ }).catch(noop);
4429
+ }
4430
+ } else if (isDASHSrc(source)) {
4431
+ resolveStreamTypeFromDASHManifest(source.src, {
4432
+ credentials: getRequestCredentials(crossOrigin),
4433
+ signal: abort.signal
4434
+ }).then((streamType) => {
4435
+ this.#media.notify("stream-type-change", streamType);
4436
+ }).catch(noop);
4437
+ } else {
4438
+ this.#media.notify("stream-type-change", "on-demand");
4439
+ }
4440
+ peek(() => {
4441
+ const preload = peek(this.#media.$state.preload);
4442
+ return provider?.loadSource(source, preload).catch((error) => {
4443
+ });
4444
+ });
4445
+ return () => abort.abort();
4446
+ }
4447
+ try {
4448
+ isString(source.src) && preconnect(new URL(source.src).origin);
4449
+ } catch (error) {
4450
+ }
4451
+ }
4452
+ #onLoadPoster() {
4453
+ const loader = this.#loader(), { providedPoster, source, canLoadPoster } = this.#media.$state;
4454
+ if (!loader || !loader.loadPoster || !source() || !canLoadPoster() || providedPoster()) return;
4455
+ const abort = new AbortController(), trigger = new DOMEvent("source-change", { detail: source });
4456
+ loader.loadPoster(source(), this.#media, abort).then((url) => {
4457
+ this.#media.notify("poster-change", url || "", trigger);
4458
+ }).catch(() => {
4459
+ this.#media.notify("poster-change", "", trigger);
4460
+ });
4461
+ return () => {
4462
+ abort.abort();
4463
+ };
4464
+ }
4465
+ }
4466
+ function normalizeSrc(src) {
4467
+ return (isArray(src) ? src : [src]).map((src2) => {
4468
+ if (isString(src2)) {
4469
+ return { src: src2, type: inferType(src2) };
4470
+ } else {
4471
+ return { ...src2, type: inferType(src2.src, src2.type) };
4472
+ }
4473
+ });
4474
+ }
4475
+ function inferType(src, type) {
4476
+ if (isString(type) && type.length) {
4477
+ return type;
4478
+ } else if (isString(src) && sourceTypes.has(src)) {
4479
+ return sourceTypes.get(src);
4480
+ } else if (!type && isHLSSrc({ src, type: "" })) {
4481
+ return "application/x-mpegurl";
4482
+ } else if (!type && isDASHSrc({ src, type: "" })) {
4483
+ return "application/dash+xml";
4484
+ } else if (!isString(src) || src.startsWith("blob:")) {
4485
+ return "video/object";
4486
+ } else if (src.includes("youtube") || src.includes("youtu.be")) {
4487
+ return "video/youtube";
4488
+ } else if (src.includes("vimeo") && !src.includes("progressive_redirect") && !src.includes(".m3u8")) {
4489
+ return "video/vimeo";
4490
+ }
4491
+ return "?";
4492
+ }
4493
+ function isSameSrc(a, b) {
4494
+ return a?.src === b?.src && a?.type === b?.type;
4495
+ }
4496
+
4497
+ class Tracks {
4498
+ #domTracks;
4499
+ #media;
4500
+ #prevTracks = [];
4501
+ constructor(domTracks, media) {
4502
+ this.#domTracks = domTracks;
4503
+ this.#media = media;
4504
+ effect(this.#onTracksChange.bind(this));
4505
+ }
4506
+ #onTracksChange() {
4507
+ const newTracks = this.#domTracks();
4508
+ for (const oldTrack of this.#prevTracks) {
4509
+ if (!newTracks.some((t) => t.id === oldTrack.id)) {
4510
+ const track = oldTrack.id && this.#media.textTracks.getById(oldTrack.id);
4511
+ if (track) this.#media.textTracks.remove(track);
4512
+ }
4513
+ }
4514
+ for (const newTrack of newTracks) {
4515
+ const id = newTrack.id || TextTrack.createId(newTrack);
4516
+ if (!this.#media.textTracks.getById(id)) {
4517
+ newTrack.id = id;
4518
+ this.#media.textTracks.add(newTrack);
4519
+ }
4520
+ }
4521
+ this.#prevTracks = newTracks;
4522
+ }
4523
+ }
4524
+
4525
+ class MediaProvider extends Component {
4526
+ static props = {
4527
+ loaders: []
4528
+ };
4529
+ static state = new State({
4530
+ loader: null
4531
+ });
4532
+ #media;
4533
+ #sources;
4534
+ #domSources = signal([]);
4535
+ #domTracks = signal([]);
4536
+ #loader = null;
4537
+ onSetup() {
4538
+ this.#media = useMediaContext();
4539
+ this.#sources = new SourceSelection(
4540
+ this.#domSources,
4541
+ this.#media,
4542
+ this.$state.loader,
4543
+ this.$props.loaders()
4544
+ );
4545
+ }
4546
+ onAttach(el) {
4547
+ el.setAttribute("data-media-provider", "");
4548
+ }
4549
+ onConnect(el) {
4550
+ this.#sources.connect();
4551
+ new Tracks(this.#domTracks, this.#media);
4552
+ const resize = new ResizeObserver(animationFrameThrottle(this.#onResize.bind(this)));
4553
+ resize.observe(el);
4554
+ const mutations = new MutationObserver(this.#onMutation.bind(this));
4555
+ mutations.observe(el, { attributes: true, childList: true });
4556
+ this.#onResize();
4557
+ this.#onMutation();
4558
+ onDispose(() => {
4559
+ resize.disconnect();
4560
+ mutations.disconnect();
4561
+ });
4562
+ }
4563
+ #loadRafId = -1;
4564
+ load(target) {
4565
+ target?.setAttribute("aria-hidden", "true");
4566
+ window.cancelAnimationFrame(this.#loadRafId);
4567
+ this.#loadRafId = requestAnimationFrame(() => this.#runLoader(target));
4568
+ onDispose(() => {
4569
+ window.cancelAnimationFrame(this.#loadRafId);
4570
+ });
4571
+ }
4572
+ #runLoader(target) {
4573
+ if (!this.scope) return;
4574
+ const loader = this.$state.loader(), { $provider } = this.#media;
4575
+ if (this.#loader === loader && loader?.target === target && peek($provider)) return;
4576
+ this.#destroyProvider();
4577
+ this.#loader = loader;
4578
+ if (loader) loader.target = target || null;
4579
+ if (!loader || !target) return;
4580
+ loader.load(this.#media).then((provider) => {
4581
+ if (!this.scope) return;
4582
+ if (peek(this.$state.loader) !== loader) return;
4583
+ this.#media.notify("provider-change", provider);
4584
+ });
4585
+ }
4586
+ onDestroy() {
4587
+ this.#loader = null;
4588
+ this.#destroyProvider();
4589
+ }
4590
+ #destroyProvider() {
4591
+ this.#media?.notify("provider-change", null);
4592
+ }
4593
+ #onResize() {
4594
+ if (!this.el) return;
4595
+ const { player, $state } = this.#media, width = this.el.offsetWidth, height = this.el.offsetHeight;
4596
+ if (!player) return;
4597
+ $state.mediaWidth.set(width);
4598
+ $state.mediaHeight.set(height);
4599
+ if (player.el) {
4600
+ setStyle(player.el, "--media-width", width + "px");
4601
+ setStyle(player.el, "--media-height", height + "px");
4602
+ }
4603
+ }
4604
+ #onMutation() {
4605
+ const sources = [], tracks = [], children = this.el.children;
4606
+ for (const el of children) {
4607
+ if (el.hasAttribute("data-vds")) continue;
4608
+ if (el instanceof HTMLSourceElement) {
4609
+ const src = {
4610
+ id: el.id,
4611
+ src: el.src,
4612
+ type: el.type
4613
+ };
4614
+ for (const prop of ["id", "src", "width", "height", "bitrate", "codec"]) {
4615
+ const value = el.getAttribute(`data-${prop}`);
4616
+ if (isString(value)) src[prop] = /id|src|codec/.test(prop) ? value : Number(value);
4617
+ }
4618
+ sources.push(src);
4619
+ } else if (el instanceof HTMLTrackElement) {
4620
+ const track = {
4621
+ src: el.src,
4622
+ kind: el.track.kind,
4623
+ language: el.srclang,
4624
+ label: el.label,
4625
+ default: el.default,
4626
+ type: el.getAttribute("data-type")
4627
+ };
4628
+ tracks.push({
4629
+ id: el.id || TextTrack.createId(track),
4630
+ ...track
4631
+ });
4632
+ }
4633
+ }
4634
+ this.#domSources.set(sources);
4635
+ this.#domTracks.set(tracks);
4636
+ tick();
4637
+ }
4638
+ }
4639
+ const mediaprovider__proto = MediaProvider.prototype;
4640
+ method(mediaprovider__proto, "load");
4641
+
4642
+ export { AudioProviderLoader, AudioTrackList, DASHProviderLoader, FullscreenController, HLSProviderLoader, IS_CHROME, List, LocalMediaStorage, MEDIA_KEY_SHORTCUTS, MediaControls, MediaPlayer, MediaProvider, MediaRemoteControl, ScreenOrientationController, TextRenderers, TextTrackList, TimeRange, VideoProviderLoader, VideoQualityList, VimeoProviderLoader, YouTubeProviderLoader, boundTime, canChangeVolume, canFullscreen, canOrientScreen, canPlayHLSNatively, canPlayVideoType, canRotateScreen, canUsePictureInPicture, canUseVideoPresentation, getTimeRangesEnd, getTimeRangesStart, isAudioProvider, isDASHProvider, isGoogleCastProvider, isHLSProvider, isHTMLAudioElement, isHTMLIFrameElement, isHTMLMediaElement, isHTMLVideoElement, isVideoProvider, isVideoQualitySrc, isVimeoProvider, isYouTubeProvider, mediaState, normalizeTimeIntervals, softResetMediaState, updateTimeIntervals };