@mattebox/player 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +24 -0
  3. package/README.md +22 -0
  4. package/dist/cdn/mattebox-player.min.js +340 -0
  5. package/dist/controls/bar.d.ts +56 -0
  6. package/dist/controls/bar.d.ts.map +1 -0
  7. package/dist/controls/bar.js +245 -0
  8. package/dist/controls/bar.js.map +1 -0
  9. package/dist/controls/buttons.d.ts +13 -0
  10. package/dist/controls/buttons.d.ts.map +1 -0
  11. package/dist/controls/buttons.js +157 -0
  12. package/dist/controls/buttons.js.map +1 -0
  13. package/dist/controls/control.d.ts +12 -0
  14. package/dist/controls/control.d.ts.map +1 -0
  15. package/dist/controls/control.js +2 -0
  16. package/dist/controls/control.js.map +1 -0
  17. package/dist/controls/cues.d.ts +33 -0
  18. package/dist/controls/cues.d.ts.map +1 -0
  19. package/dist/controls/cues.js +170 -0
  20. package/dist/controls/cues.js.map +1 -0
  21. package/dist/controls/fullscreen.d.ts +21 -0
  22. package/dist/controls/fullscreen.d.ts.map +1 -0
  23. package/dist/controls/fullscreen.js +78 -0
  24. package/dist/controls/fullscreen.js.map +1 -0
  25. package/dist/controls/icons.d.ts +35 -0
  26. package/dist/controls/icons.d.ts.map +1 -0
  27. package/dist/controls/icons.js +76 -0
  28. package/dist/controls/icons.js.map +1 -0
  29. package/dist/controls/keys.d.ts +26 -0
  30. package/dist/controls/keys.d.ts.map +1 -0
  31. package/dist/controls/keys.js +66 -0
  32. package/dist/controls/keys.js.map +1 -0
  33. package/dist/controls/live.d.ts +10 -0
  34. package/dist/controls/live.d.ts.map +1 -0
  35. package/dist/controls/live.js +59 -0
  36. package/dist/controls/live.js.map +1 -0
  37. package/dist/controls/menu.d.ts +34 -0
  38. package/dist/controls/menu.d.ts.map +1 -0
  39. package/dist/controls/menu.js +225 -0
  40. package/dist/controls/menu.js.map +1 -0
  41. package/dist/controls/menus.d.ts +43 -0
  42. package/dist/controls/menus.d.ts.map +1 -0
  43. package/dist/controls/menus.js +262 -0
  44. package/dist/controls/menus.js.map +1 -0
  45. package/dist/controls/options.d.ts +44 -0
  46. package/dist/controls/options.d.ts.map +1 -0
  47. package/dist/controls/options.js +54 -0
  48. package/dist/controls/options.js.map +1 -0
  49. package/dist/controls/pip.d.ts +16 -0
  50. package/dist/controls/pip.d.ts.map +1 -0
  51. package/dist/controls/pip.js +53 -0
  52. package/dist/controls/pip.js.map +1 -0
  53. package/dist/controls/ranges.d.ts +24 -0
  54. package/dist/controls/ranges.d.ts.map +1 -0
  55. package/dist/controls/ranges.js +48 -0
  56. package/dist/controls/ranges.js.map +1 -0
  57. package/dist/controls/screens.d.ts +22 -0
  58. package/dist/controls/screens.d.ts.map +1 -0
  59. package/dist/controls/screens.js +69 -0
  60. package/dist/controls/screens.js.map +1 -0
  61. package/dist/controls/seek.d.ts +52 -0
  62. package/dist/controls/seek.d.ts.map +1 -0
  63. package/dist/controls/seek.js +215 -0
  64. package/dist/controls/seek.js.map +1 -0
  65. package/dist/controls/slider.d.ts +28 -0
  66. package/dist/controls/slider.d.ts.map +1 -0
  67. package/dist/controls/slider.js +145 -0
  68. package/dist/controls/slider.js.map +1 -0
  69. package/dist/controls/speed.d.ts +8 -0
  70. package/dist/controls/speed.d.ts.map +1 -0
  71. package/dist/controls/speed.js +34 -0
  72. package/dist/controls/speed.js.map +1 -0
  73. package/dist/controls/time.d.ts +8 -0
  74. package/dist/controls/time.d.ts.map +1 -0
  75. package/dist/controls/time.js +25 -0
  76. package/dist/controls/time.js.map +1 -0
  77. package/dist/controls/volume.d.ts +8 -0
  78. package/dist/controls/volume.d.ts.map +1 -0
  79. package/dist/controls/volume.js +29 -0
  80. package/dist/controls/volume.js.map +1 -0
  81. package/dist/dom.d.ts +18 -0
  82. package/dist/dom.d.ts.map +1 -0
  83. package/dist/dom.js +45 -0
  84. package/dist/dom.js.map +1 -0
  85. package/dist/element.d.ts +101 -0
  86. package/dist/element.d.ts.map +1 -0
  87. package/dist/element.js +327 -0
  88. package/dist/element.js.map +1 -0
  89. package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/asyncToGenerator.js +27 -0
  90. package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/defineProperty.js +12 -0
  91. package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/objectSpread2.js +25 -0
  92. package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/toPrimitive.js +14 -0
  93. package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/toPropertyKey.js +9 -0
  94. package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/typeof.js +11 -0
  95. package/dist/es2015/controls/bar.js +227 -0
  96. package/dist/es2015/controls/buttons.js +146 -0
  97. package/dist/es2015/controls/cues.js +153 -0
  98. package/dist/es2015/controls/fullscreen.js +54 -0
  99. package/dist/es2015/controls/icons.js +50 -0
  100. package/dist/es2015/controls/keys.js +60 -0
  101. package/dist/es2015/controls/live.js +60 -0
  102. package/dist/es2015/controls/menu.js +226 -0
  103. package/dist/es2015/controls/menus.js +258 -0
  104. package/dist/es2015/controls/options.js +46 -0
  105. package/dist/es2015/controls/pip.js +39 -0
  106. package/dist/es2015/controls/ranges.js +51 -0
  107. package/dist/es2015/controls/screens.js +74 -0
  108. package/dist/es2015/controls/seek.js +203 -0
  109. package/dist/es2015/controls/slider.js +136 -0
  110. package/dist/es2015/controls/speed.js +41 -0
  111. package/dist/es2015/controls/time.js +27 -0
  112. package/dist/es2015/controls/volume.js +30 -0
  113. package/dist/es2015/dom.js +43 -0
  114. package/dist/es2015/element.js +311 -0
  115. package/dist/es2015/index.js +10 -0
  116. package/dist/es2015/namespaces.js +7 -0
  117. package/dist/es2015/panels/drm.js +29 -0
  118. package/dist/es2015/panels/error.js +25 -0
  119. package/dist/es2015/panels/live.js +48 -0
  120. package/dist/es2015/panels/quality.js +52 -0
  121. package/dist/es2015/panels/tracks.js +47 -0
  122. package/dist/es2015/presets.js +46 -0
  123. package/dist/es2015/style.js +347 -0
  124. package/dist/index.d.ts +4 -0
  125. package/dist/index.d.ts.map +1 -0
  126. package/dist/index.js +9 -0
  127. package/dist/index.js.map +1 -0
  128. package/dist/namespaces.d.ts +45 -0
  129. package/dist/namespaces.d.ts.map +1 -0
  130. package/dist/namespaces.js +5 -0
  131. package/dist/namespaces.js.map +1 -0
  132. package/dist/panels/drm.d.ts +8 -0
  133. package/dist/panels/drm.d.ts.map +1 -0
  134. package/dist/panels/drm.js +36 -0
  135. package/dist/panels/drm.js.map +1 -0
  136. package/dist/panels/error.d.ts +13 -0
  137. package/dist/panels/error.d.ts.map +1 -0
  138. package/dist/panels/error.js +23 -0
  139. package/dist/panels/error.js.map +1 -0
  140. package/dist/panels/live.d.ts +3 -0
  141. package/dist/panels/live.d.ts.map +1 -0
  142. package/dist/panels/live.js +56 -0
  143. package/dist/panels/live.js.map +1 -0
  144. package/dist/panels/panel.d.ts +13 -0
  145. package/dist/panels/panel.d.ts.map +1 -0
  146. package/dist/panels/panel.js +2 -0
  147. package/dist/panels/panel.js.map +1 -0
  148. package/dist/panels/quality.d.ts +3 -0
  149. package/dist/panels/quality.d.ts.map +1 -0
  150. package/dist/panels/quality.js +59 -0
  151. package/dist/panels/quality.js.map +1 -0
  152. package/dist/panels/tracks.d.ts +3 -0
  153. package/dist/panels/tracks.d.ts.map +1 -0
  154. package/dist/panels/tracks.js +49 -0
  155. package/dist/panels/tracks.js.map +1 -0
  156. package/dist/presets.d.ts +23 -0
  157. package/dist/presets.d.ts.map +1 -0
  158. package/dist/presets.js +36 -0
  159. package/dist/presets.js.map +1 -0
  160. package/dist/style.d.ts +13 -0
  161. package/dist/style.d.ts.map +1 -0
  162. package/dist/style.js +345 -0
  163. package/dist/style.js.map +1 -0
  164. package/package.json +58 -0
@@ -0,0 +1,226 @@
1
+ import { el, state } from "../dom.js";
2
+ import { glyph, icon } from "./icons.js";
3
+ //#region src/controls/menu.ts
4
+ /**
5
+ * The menu primitive the quality, audio, subtitles and speed menus share: a
6
+ * button that opens a popup of radio items above it, in one or more groups
7
+ * with a heading each, and pages behind entries that open them. The button
8
+ * carries the name and `aria-expanded`; the popup is `role="menu"`, every
9
+ * choice a `menuitemradio` with `aria-checked`, the checked one also
10
+ * carrying `checked` on its part name; an entry that opens a page is a
11
+ * `menuitem` with `aria-haspopup`, and the page starts with a Back.
12
+ *
13
+ * Open on click. Inside, the arrows move focus across every item, Home and
14
+ * End jump, Enter and Space choose, Escape goes back a page or closes and
15
+ * hands focus to the button. A choice closes the popup. A pointer down
16
+ * anywhere else closes it too.
17
+ *
18
+ * The popup never leaves the picture: on opening it takes the room above
19
+ * the button inside the stage as its height and scrolls past that.
20
+ */
21
+ /** Air between the popup's top and the stage's. */
22
+ const AIR = 8;
23
+ function isPage(entry) {
24
+ return "entries" in entry;
25
+ }
26
+ function menu(options) {
27
+ const { name } = options;
28
+ const root = el("div", `menu ${name}-menu`);
29
+ const button = el("button", `control ${name}-button`);
30
+ button.type = "button";
31
+ button.setAttribute("aria-label", options.label);
32
+ button.setAttribute("aria-haspopup", "menu");
33
+ button.setAttribute("aria-expanded", "false");
34
+ const svg = icon(options.icon);
35
+ button.append(svg);
36
+ const popup = el("div", `popup ${name}-popup`);
37
+ popup.setAttribute("role", "menu");
38
+ popup.setAttribute("aria-label", options.label);
39
+ popup.hidden = true;
40
+ root.append(button, popup);
41
+ let open = false;
42
+ let first = [];
43
+ /** The pages opened, the first being the menu itself. */
44
+ let stack = [];
45
+ const handlers = /* @__PURE__ */ new WeakMap();
46
+ function items() {
47
+ return [...popup.querySelectorAll("button")];
48
+ }
49
+ function checked() {
50
+ return items().find((item) => item.getAttribute("aria-checked") === "true");
51
+ }
52
+ /** The focused element as this tree sees it: inside shadow DOM, `document.activeElement` is the host. */
53
+ function focused() {
54
+ return popup.getRootNode().activeElement;
55
+ }
56
+ function outside(event) {
57
+ if (!event.composedPath().includes(root)) close();
58
+ }
59
+ /** Whatever room there is above the button inside the stage, so the popup never leaves the picture. */
60
+ function fit() {
61
+ const stage = root.closest("[part~=\"stage\"]");
62
+ if (stage === null) return;
63
+ const room = button.getBoundingClientRect().top - stage.getBoundingClientRect().top - AIR;
64
+ popup.style.maxHeight = `${Math.max(0, Math.floor(room))}px`;
65
+ }
66
+ function clear() {
67
+ for (const item of items()) {
68
+ const handler = handlers.get(item);
69
+ if (handler !== void 0) item.removeEventListener("click", handler);
70
+ }
71
+ popup.replaceChildren();
72
+ }
73
+ function item(part, text, handler) {
74
+ const node = el("button", part, text);
75
+ node.type = "button";
76
+ node.tabIndex = -1;
77
+ handlers.set(node, handler);
78
+ node.addEventListener("click", handler);
79
+ return node;
80
+ }
81
+ /** Draws the page on top of the stack. */
82
+ function render() {
83
+ clear();
84
+ const page = stack[stack.length - 1];
85
+ if (page === void 0) return;
86
+ let tab = true;
87
+ if (stack.length > 1) {
88
+ const back = item(`item back-item ${name}-back-item`, page.label, () => {
89
+ var _items$;
90
+ stack.pop();
91
+ render();
92
+ (_items$ = items()[0]) === null || _items$ === void 0 || _items$.focus();
93
+ });
94
+ back.setAttribute("role", "menuitem");
95
+ back.setAttribute("aria-label", `Back from ${page.label}`);
96
+ popup.append(back);
97
+ }
98
+ for (const entry of page.entries) {
99
+ if (isPage(entry)) {
100
+ const link = item(`item page-item ${name}-${entry.name}-item`, entry.label, () => {
101
+ var _items$2;
102
+ stack.push(entry);
103
+ render();
104
+ (_items$2 = items()[0]) === null || _items$2 === void 0 || _items$2.focus();
105
+ });
106
+ link.setAttribute("role", "menuitem");
107
+ link.setAttribute("aria-haspopup", "menu");
108
+ popup.append(link);
109
+ continue;
110
+ }
111
+ const section = el("div", `section ${name}-${entry.name}-section`);
112
+ section.setAttribute("role", "group");
113
+ if (entry.label !== void 0) {
114
+ section.setAttribute("aria-label", entry.label);
115
+ section.append(el("div", `section-label ${name}-${entry.name}-label`, entry.label));
116
+ }
117
+ for (const [id, text] of entry.items) {
118
+ const choice = item(`item ${name}-item ${name}-${entry.name}-item`, text, () => {
119
+ close();
120
+ button.focus();
121
+ entry.onSelect(id);
122
+ });
123
+ choice.value = id;
124
+ choice.setAttribute("role", "menuitemradio");
125
+ const on = id === entry.value;
126
+ choice.setAttribute("aria-checked", String(on));
127
+ state(choice, `item ${name}-item ${name}-${entry.name}-item`, { checked: on });
128
+ if (on && tab) {
129
+ choice.tabIndex = 0;
130
+ tab = false;
131
+ }
132
+ section.append(choice);
133
+ }
134
+ popup.append(section);
135
+ }
136
+ fit();
137
+ }
138
+ function close() {
139
+ if (!open) return;
140
+ open = false;
141
+ popup.hidden = true;
142
+ button.setAttribute("aria-expanded", "false");
143
+ state(root, `menu ${name}-menu`, { open: false });
144
+ document.removeEventListener("pointerdown", outside, true);
145
+ if (stack.length > 1) {
146
+ stack = stack.slice(0, 1);
147
+ render();
148
+ }
149
+ }
150
+ function show() {
151
+ var _ref, _checked;
152
+ if (open) return;
153
+ open = true;
154
+ popup.hidden = false;
155
+ button.setAttribute("aria-expanded", "true");
156
+ state(root, `menu ${name}-menu`, { open: true });
157
+ document.addEventListener("pointerdown", outside, true);
158
+ fit();
159
+ (_ref = (_checked = checked()) !== null && _checked !== void 0 ? _checked : items()[0]) === null || _ref === void 0 || _ref.focus();
160
+ }
161
+ function toggle() {
162
+ if (open) close();
163
+ else show();
164
+ }
165
+ function key(event) {
166
+ var _list$next;
167
+ const list = items();
168
+ const index = list.indexOf(focused());
169
+ let next;
170
+ switch (event.key) {
171
+ case "ArrowDown":
172
+ next = (index + 1) % list.length;
173
+ break;
174
+ case "ArrowUp":
175
+ next = (index - 1 + list.length) % list.length;
176
+ break;
177
+ case "Home":
178
+ next = 0;
179
+ break;
180
+ case "End":
181
+ next = list.length - 1;
182
+ break;
183
+ case "Escape":
184
+ event.preventDefault();
185
+ if (stack.length > 1) {
186
+ var _items$3;
187
+ stack.pop();
188
+ render();
189
+ (_items$3 = items()[0]) === null || _items$3 === void 0 || _items$3.focus();
190
+ return;
191
+ }
192
+ close();
193
+ button.focus();
194
+ return;
195
+ default: return;
196
+ }
197
+ event.preventDefault();
198
+ (_list$next = list[next]) === null || _list$next === void 0 || _list$next.focus();
199
+ }
200
+ button.addEventListener("click", toggle);
201
+ popup.addEventListener("keydown", key);
202
+ return {
203
+ root,
204
+ fill(entries) {
205
+ first = entries;
206
+ stack = [{
207
+ name,
208
+ label: options.label,
209
+ entries: first
210
+ }];
211
+ render();
212
+ },
213
+ show(glyphName) {
214
+ glyph(svg, glyphName);
215
+ },
216
+ close,
217
+ dispose() {
218
+ close();
219
+ clear();
220
+ button.removeEventListener("click", toggle);
221
+ popup.removeEventListener("keydown", key);
222
+ }
223
+ };
224
+ }
225
+ //#endregion
226
+ export { menu };
@@ -0,0 +1,258 @@
1
+ import { el } from "../dom.js";
2
+ import { namespaces } from "../namespaces.js";
3
+ import { icon } from "./icons.js";
4
+ import { menu } from "./menu.js";
5
+ //#region src/controls/menus.ts
6
+ const AUTO = "auto";
7
+ const OFF = "off";
8
+ /** The subtitle looks, as attribute name, choices, and default. */
9
+ const SUBTITLE_SIZE = "subtitle-size";
10
+ const SUBTITLE_BACKGROUND = "subtitle-background";
11
+ const SIZES = [
12
+ ["small", "Small"],
13
+ ["medium", "Medium"],
14
+ ["large", "Large"],
15
+ ["xlarge", "Extra large"]
16
+ ];
17
+ const BACKGROUNDS = [
18
+ ["none", "None"],
19
+ ["dark", "Dark"],
20
+ ["solid", "Solid"]
21
+ ];
22
+ /** A rendition's height when the manifest declared one, its bitrate otherwise. */
23
+ function renditionLabel(rendition) {
24
+ if (rendition.height !== void 0) return `${rendition.height}p`;
25
+ return `${Math.round(rendition.bitrate / 1e3)} kbps`;
26
+ }
27
+ /** The language, then the role, then the id: whichever the manifest gave. */
28
+ function trackLabel(track) {
29
+ const parts = [track.lang, track.role].filter((part) => part !== void 0);
30
+ return parts.length === 0 ? track.id : parts.join(" · ");
31
+ }
32
+ /** The quality menu: the pin, where "auto" means none. Hidden without renditions. */
33
+ const qualityMenu = ({ engine }) => {
34
+ const quality = engine.quality;
35
+ const control = menu({
36
+ name: "quality",
37
+ label: "Quality",
38
+ icon: "settings"
39
+ });
40
+ function render() {
41
+ var _quality$pinned;
42
+ const items = [[AUTO, "Auto"]];
43
+ for (const rendition of quality.renditions) items.push([rendition.id, renditionLabel(rendition)]);
44
+ control.fill([{
45
+ name: "rendition",
46
+ items,
47
+ value: (_quality$pinned = quality.pinned) !== null && _quality$pinned !== void 0 ? _quality$pinned : AUTO,
48
+ onSelect(value) {
49
+ if (value === AUTO) quality.auto();
50
+ else quality.pin(value);
51
+ render();
52
+ }
53
+ }]);
54
+ control.root.hidden = quality.renditions.length === 0;
55
+ }
56
+ const offs = [
57
+ engine.on("tracks:changed", render),
58
+ engine.on("quality:constraints-unsatisfiable", render),
59
+ engine.on("quality:pin-unsatisfiable", render)
60
+ ];
61
+ render();
62
+ return {
63
+ root: control.root,
64
+ dispose() {
65
+ for (const off of offs) off();
66
+ control.dispose();
67
+ }
68
+ };
69
+ };
70
+ /** The tracks of one content type as menu items, with the active one. */
71
+ function trackItems(engine, contentType, off) {
72
+ var _tracks$active$id, _tracks$active;
73
+ const tracks = engine.tracks;
74
+ const available = tracks.available.filter((track) => track.contentType === contentType);
75
+ const items = off ? [[OFF, "Off"]] : [];
76
+ for (const track of available) items.push([track.id, trackLabel(track)]);
77
+ return [
78
+ items,
79
+ (_tracks$active$id = (_tracks$active = tracks.active(contentType)) === null || _tracks$active === void 0 ? void 0 : _tracks$active.id) !== null && _tracks$active$id !== void 0 ? _tracks$active$id : OFF,
80
+ available.length
81
+ ];
82
+ }
83
+ /** The audio menu. Hidden unless there is a choice to make. */
84
+ const audioMenu = ({ engine }) => {
85
+ const control = menu({
86
+ name: "audio",
87
+ label: "Audio",
88
+ icon: "music"
89
+ });
90
+ function render() {
91
+ const [items, active, count] = trackItems(engine, "audio", false);
92
+ control.fill([{
93
+ name: "track",
94
+ items,
95
+ value: active,
96
+ onSelect(value) {
97
+ engine.tracks.select(value);
98
+ render();
99
+ }
100
+ }]);
101
+ control.root.hidden = count < 2;
102
+ }
103
+ const offs = [engine.on("tracks:changed", render), engine.on("tracks:selected", render)];
104
+ render();
105
+ return {
106
+ root: control.root,
107
+ dispose() {
108
+ for (const off of offs) off();
109
+ control.dispose();
110
+ }
111
+ };
112
+ };
113
+ /** An attribute's value where it is one of the choices, else the default. */
114
+ function choice(host, attribute, choices, fallback) {
115
+ const value = host.getAttribute(attribute);
116
+ return value !== null && choices.some(([id]) => id === value) ? value : fallback;
117
+ }
118
+ /**
119
+ * The subtitles menu, with "off" because a text selection is releasable
120
+ * and an audio one is not, and a Settings page behind it with the size and
121
+ * the background. The glyph says whether a track is on.
122
+ */
123
+ const textMenu = ({ engine, host }) => {
124
+ const control = menu({
125
+ name: "text",
126
+ label: "Subtitles",
127
+ icon: "closed-captions"
128
+ });
129
+ function looks(attribute, name, label, choices, fallback) {
130
+ return {
131
+ name,
132
+ label,
133
+ items: choices,
134
+ value: choice(host, attribute, choices, fallback),
135
+ onSelect(value) {
136
+ host.setAttribute(attribute, value);
137
+ render();
138
+ }
139
+ };
140
+ }
141
+ function render() {
142
+ const [items, active, count] = trackItems(engine, "text", true);
143
+ control.fill([{
144
+ name: "track",
145
+ label: "Track",
146
+ items,
147
+ value: active,
148
+ onSelect(value) {
149
+ if (value === OFF) engine.tracks.deselect("text");
150
+ else engine.tracks.select(value);
151
+ render();
152
+ }
153
+ }, {
154
+ name: "settings",
155
+ label: "Settings",
156
+ entries: [looks(SUBTITLE_SIZE, "size", "Size", SIZES, "medium"), looks(SUBTITLE_BACKGROUND, "background", "Background", BACKGROUNDS, "dark")]
157
+ }]);
158
+ control.show(active === OFF ? "closed-captions" : "closed-captions-on");
159
+ control.root.hidden = count === 0;
160
+ }
161
+ const offs = [engine.on("tracks:changed", render), engine.on("tracks:selected", render)];
162
+ render();
163
+ return {
164
+ root: control.root,
165
+ dispose() {
166
+ for (const off of offs) off();
167
+ control.dispose();
168
+ }
169
+ };
170
+ };
171
+ /** The name a viewer knows a key system by. */
172
+ function keySystemName(id) {
173
+ if (id.startsWith("com.widevine")) return "Widevine";
174
+ if (id.startsWith("com.microsoft.playready")) return "PlayReady";
175
+ if (id.startsWith("com.apple.fps")) return "FairPlay";
176
+ return id;
177
+ }
178
+ /** "3 keys: usable ×2, expired ×1", or "no key yet". */
179
+ function sessionsText(sessions) {
180
+ var _counts$get;
181
+ if (sessions.length === 0) return "no key yet";
182
+ const counts = /* @__PURE__ */ new Map();
183
+ for (const session of sessions) counts.set(session.status, ((_counts$get = counts.get(session.status)) !== null && _counts$get !== void 0 ? _counts$get : 0) + 1);
184
+ const parts = [];
185
+ for (const [status, count] of counts) parts.push(count > 1 ? `${status} ×${count}` : status);
186
+ return `${sessions.length} ${sessions.length === 1 ? "key" : "keys"}: ${parts.join(", ")}`;
187
+ }
188
+ /**
189
+ * A lock over `engine.drm`, hidden until a key session opens and absent
190
+ * without the EME stages. Hover or focus shows what it knows: the key
191
+ * system, and the key sessions with their statuses.
192
+ */
193
+ const drmBadge = ({ engine }) => {
194
+ const drm = namespaces(engine).drm;
195
+ const root = el("span", "badge drm-badge");
196
+ root.setAttribute("role", "img");
197
+ root.append(icon("lock-closed"));
198
+ root.hidden = true;
199
+ if (drm === void 0) return {
200
+ root,
201
+ dispose() {}
202
+ };
203
+ const tooltip = el("div", "tooltip drm-tooltip");
204
+ tooltip.setAttribute("role", "tooltip");
205
+ const system = el("div", "tooltip-title drm-tooltip-title");
206
+ const id = el("div", "tooltip-text drm-tooltip-id");
207
+ const keys = el("div", "tooltip-text drm-tooltip-keys");
208
+ tooltip.append(system, id, keys);
209
+ tooltip.hidden = true;
210
+ root.append(tooltip);
211
+ root.tabIndex = 0;
212
+ function render() {
213
+ var _drm$keySystem, _drm$sessions;
214
+ const name = (_drm$keySystem = drm === null || drm === void 0 ? void 0 : drm.keySystem) !== null && _drm$keySystem !== void 0 ? _drm$keySystem : null;
215
+ root.hidden = name === null;
216
+ if (name === null) return;
217
+ const sessions = (_drm$sessions = drm === null || drm === void 0 ? void 0 : drm.sessions) !== null && _drm$sessions !== void 0 ? _drm$sessions : [];
218
+ system.textContent = keySystemName(name);
219
+ id.textContent = name;
220
+ keys.textContent = sessionsText(sessions);
221
+ root.setAttribute("aria-label", `Protected by ${keySystemName(name)}, ${sessionsText(sessions)}`);
222
+ }
223
+ function show() {
224
+ tooltip.hidden = false;
225
+ }
226
+ function hide() {
227
+ tooltip.hidden = true;
228
+ }
229
+ root.addEventListener("pointerenter", show);
230
+ root.addEventListener("pointerleave", hide);
231
+ root.addEventListener("focus", show);
232
+ root.addEventListener("blur", hide);
233
+ const offs = [
234
+ engine.on("drm:keysystem", render),
235
+ engine.on("drm:keystatus", render),
236
+ engine.on("drm:encrypted", render)
237
+ ];
238
+ render();
239
+ return {
240
+ root,
241
+ dispose() {
242
+ for (const off of offs) off();
243
+ root.removeEventListener("pointerenter", show);
244
+ root.removeEventListener("pointerleave", hide);
245
+ root.removeEventListener("focus", show);
246
+ root.removeEventListener("blur", hide);
247
+ }
248
+ };
249
+ };
250
+ /** By the name the layout knows them by. */
251
+ const MENUS = {
252
+ subtitles: textMenu,
253
+ audio: audioMenu,
254
+ quality: qualityMenu,
255
+ drm: drmBadge
256
+ };
257
+ //#endregion
258
+ export { MENUS, SUBTITLE_BACKGROUND, SUBTITLE_SIZE, audioMenu, drmBadge, qualityMenu, textMenu };
@@ -0,0 +1,46 @@
1
+ //#region src/controls/options.ts
2
+ /** The buttons row by default. `drm`, the lock, is a control the row can carry and does not by default. */
3
+ const LAYOUT = "skip-back play skip-forward volume | speed subtitles audio quality pip fullscreen";
4
+ const DEFAULTS = {
5
+ idleMs: 3e3,
6
+ seekStep: 5,
7
+ seekPage: 30,
8
+ skipBack: 10,
9
+ skipForward: 10,
10
+ start: 1,
11
+ liveWindow: 3,
12
+ layout: LAYOUT
13
+ };
14
+ /** Each option's attribute. */
15
+ const ATTRIBUTES = {
16
+ idleMs: "idle-ms",
17
+ seekStep: "seek-step",
18
+ seekPage: "seek-page",
19
+ skipBack: "skip-back",
20
+ skipForward: "skip-forward",
21
+ start: "start",
22
+ liveWindow: "live-window",
23
+ layout: "layout"
24
+ };
25
+ const NUMERIC = Object.keys(DEFAULTS).filter((key) => key !== "layout");
26
+ /** The attribute where it is a non-negative number, else the option, else the default; the layout as given. */
27
+ function resolve(options, attribute) {
28
+ var _ref, _attribute;
29
+ const out = {};
30
+ for (const key of NUMERIC) {
31
+ const raw = attribute(ATTRIBUTES[key]);
32
+ const set = raw === null ? NaN : Number(raw);
33
+ const given = options === null || options === void 0 ? void 0 : options[key];
34
+ out[key] = Number.isFinite(set) && set >= 0 ? set : given !== void 0 && Number.isFinite(given) && given >= 0 ? given : DEFAULTS[key];
35
+ }
36
+ out.layout = (_ref = (_attribute = attribute(ATTRIBUTES.layout)) !== null && _attribute !== void 0 ? _attribute : options === null || options === void 0 ? void 0 : options.layout) !== null && _ref !== void 0 ? _ref : DEFAULTS.layout;
37
+ return out;
38
+ }
39
+ /** A layout as two lists of names, left and right of the `|`. Unknown names are the caller's to drop. */
40
+ function clusters(layout) {
41
+ const [left = "", right = ""] = layout.split("|");
42
+ const names = (part) => part.split(/\s+/).filter((name) => name !== "");
43
+ return [names(left), names(right)];
44
+ }
45
+ //#endregion
46
+ export { ATTRIBUTES, DEFAULTS, LAYOUT, clusters, resolve };
@@ -0,0 +1,39 @@
1
+ //#region src/controls/pip.ts
2
+ function watching(target, names) {
3
+ return (fn) => {
4
+ for (const name of names) target.addEventListener(name, fn);
5
+ return () => {
6
+ for (const name of names) target.removeEventListener(name, fn);
7
+ };
8
+ };
9
+ }
10
+ function pictureInPicture(video) {
11
+ var _safari$webkitSupport;
12
+ const safari = video;
13
+ if (typeof video.requestPictureInPicture === "function" && document.pictureInPictureEnabled) return {
14
+ supported: true,
15
+ active: () => document.pictureInPictureElement === video,
16
+ toggle() {
17
+ (document.pictureInPictureElement === video ? document.exitPictureInPicture() : video.requestPictureInPicture()).catch(() => void 0);
18
+ },
19
+ watch: watching(video, ["enterpictureinpicture", "leavepictureinpicture"])
20
+ };
21
+ if (typeof safari.webkitSetPresentationMode === "function" && ((_safari$webkitSupport = safari.webkitSupportsPresentationMode) === null || _safari$webkitSupport === void 0 ? void 0 : _safari$webkitSupport.call(safari, "picture-in-picture")) === true) return {
22
+ supported: true,
23
+ active: () => safari.webkitPresentationMode === "picture-in-picture",
24
+ toggle() {
25
+ var _safari$webkitSetPres;
26
+ const next = safari.webkitPresentationMode === "picture-in-picture" ? "inline" : "picture-in-picture";
27
+ (_safari$webkitSetPres = safari.webkitSetPresentationMode) === null || _safari$webkitSetPres === void 0 || _safari$webkitSetPres.call(safari, next);
28
+ },
29
+ watch: watching(video, ["webkitpresentationmodechanged"])
30
+ };
31
+ return {
32
+ supported: false,
33
+ active: () => false,
34
+ toggle() {},
35
+ watch: () => () => {}
36
+ };
37
+ }
38
+ //#endregion
39
+ export { pictureInPicture };
@@ -0,0 +1,51 @@
1
+ //#region src/controls/ranges.ts
2
+ const EMPTY = {
3
+ start: 0,
4
+ end: 0
5
+ };
6
+ /** A `TimeRanges` as spans. */
7
+ function spans(ranges) {
8
+ const out = [];
9
+ for (let i = 0; i < ranges.length; i += 1) out.push({
10
+ start: ranges.start(i),
11
+ end: ranges.end(i)
12
+ });
13
+ return out;
14
+ }
15
+ /**
16
+ * The span the bar maps: the last seekable range for live, which is the
17
+ * availability window the engine sets on the MediaSource; zero to the
18
+ * duration for VOD; empty while neither is known.
19
+ */
20
+ function window(duration, seekable, live) {
21
+ if (live) {
22
+ const last = seekable[seekable.length - 1];
23
+ if (last !== void 0 && last.end > last.start) return last;
24
+ }
25
+ return Number.isFinite(duration) && duration > 0 ? {
26
+ start: 0,
27
+ end: duration
28
+ } : EMPTY;
29
+ }
30
+ /** Where a time sits on a span, clamped to it. Zero on an empty span. */
31
+ function fraction(time, span) {
32
+ const length = span.end - span.start;
33
+ if (length <= 0) return 0;
34
+ return Math.min(1, Math.max(0, (time - span.start) / length));
35
+ }
36
+ /** The time at a fraction of a span. */
37
+ function at(part, span) {
38
+ return span.start + Math.min(1, Math.max(0, part)) * (span.end - span.start);
39
+ }
40
+ /** Ranges clipped to a span, as fraction pairs, dropping what falls outside or collapses. */
41
+ function clip(ranges, span) {
42
+ const out = [];
43
+ for (const range of ranges) {
44
+ const start = fraction(range.start, span);
45
+ const end = fraction(range.end, span);
46
+ if (end > start) out.push([start, end]);
47
+ }
48
+ return out;
49
+ }
50
+ //#endregion
51
+ export { EMPTY, at, clip, fraction, spans, window };
@@ -0,0 +1,74 @@
1
+ import { el } from "../dom.js";
2
+ import { glyph, icon } from "./icons.js";
3
+ //#region src/controls/screens.ts
4
+ /** `shown` false keeps it out entirely, which is what the `start` knob at zero asks. */
5
+ function startButton(video, flag, shown = true) {
6
+ const root = el("button", "control start-button");
7
+ root.type = "button";
8
+ const svg = icon("play");
9
+ root.append(svg);
10
+ function render() {
11
+ const paused = video.paused;
12
+ const name = video.ended ? "replay" : "play";
13
+ glyph(svg, name);
14
+ root.setAttribute("aria-label", video.ended ? "Replay" : "Play");
15
+ root.hidden = !shown || !paused;
16
+ flag("paused", paused);
17
+ }
18
+ function click() {
19
+ video.play().catch(() => void 0);
20
+ }
21
+ const names = [
22
+ "play",
23
+ "pause",
24
+ "ended",
25
+ "emptied"
26
+ ];
27
+ root.addEventListener("click", click);
28
+ for (const name of names) video.addEventListener(name, render);
29
+ render();
30
+ return {
31
+ root,
32
+ dispose() {
33
+ root.removeEventListener("click", click);
34
+ for (const name of names) video.removeEventListener(name, render);
35
+ root.remove();
36
+ }
37
+ };
38
+ }
39
+ function errorScreen(retry) {
40
+ const root = el("div", "error-screen");
41
+ root.setAttribute("role", "alert");
42
+ const box = el("div", "error-box");
43
+ const title = el("div", "error-title", "Playback failed");
44
+ const category = el("span", "value error-category");
45
+ const code = el("span", "value error-code");
46
+ const detail = el("div", "error-detail");
47
+ detail.append(category, el("span", "error-separator", " · "), code);
48
+ const button = el("button", "control error-retry", "Retry");
49
+ button.type = "button";
50
+ button.addEventListener("click", retry);
51
+ box.append(title, detail, button);
52
+ root.append(box);
53
+ root.hidden = true;
54
+ return {
55
+ root,
56
+ show(error) {
57
+ category.textContent = error.category;
58
+ code.textContent = error.code;
59
+ root.hidden = false;
60
+ },
61
+ clear() {
62
+ root.hidden = true;
63
+ },
64
+ shown() {
65
+ return !root.hidden;
66
+ },
67
+ dispose() {
68
+ button.removeEventListener("click", retry);
69
+ root.remove();
70
+ }
71
+ };
72
+ }
73
+ //#endregion
74
+ export { errorScreen, startButton };