@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,203 @@
1
+ import { el } from "../dom.js";
2
+ import { EMPTY, at, clip, fraction, spans, window } from "./ranges.js";
3
+ import { slider } from "./slider.js";
4
+ import { clock, describe, format } from "./time.js";
5
+ //#region src/controls/seek.ts
6
+ /** What the tile scales down to when the page sets no `--mbx-preview-width`. */
7
+ const PREVIEW_WIDTH = 160;
8
+ /** The buffer goal assumed for a session that reports none. */
9
+ const BUFFER_GOAL = 30;
10
+ /** The events on which the row re-reads the video. */
11
+ const EVENTS = [
12
+ "timeupdate",
13
+ "progress",
14
+ "durationchange",
15
+ "loadedmetadata",
16
+ "seeking",
17
+ "seeked",
18
+ "emptied"
19
+ ];
20
+ function percent(part) {
21
+ return `${part * 100}%`;
22
+ }
23
+ function seekBar(video, flag, options) {
24
+ let source = {};
25
+ let span = EMPTY;
26
+ let pending = null;
27
+ let frame = 0;
28
+ function isLive() {
29
+ const live = source.live;
30
+ return live !== void 0 && live.edge !== null;
31
+ }
32
+ /** The wall clock at a presentation time, when the session can say it. */
33
+ function wall(time) {
34
+ var _source$pdt$toWallClo, _source$pdt;
35
+ const epoch = (_source$pdt$toWallClo = (_source$pdt = source.pdt) === null || _source$pdt === void 0 ? void 0 : _source$pdt.toWallClock(time)) !== null && _source$pdt$toWallClo !== void 0 ? _source$pdt$toWallClo : null;
36
+ return epoch === null ? null : clock(epoch);
37
+ }
38
+ /** What a screen reader hears at a time. */
39
+ function say(time) {
40
+ if (!isLive()) return describe(time, span.end);
41
+ const behind = `${format(span.end - time)} behind live`;
42
+ const at = wall(time);
43
+ return at === null ? behind : `${at}, ${behind}`;
44
+ }
45
+ /** What the preview and the time show at a time. */
46
+ function label(time) {
47
+ var _wall;
48
+ if (!isLive()) return format(time);
49
+ return (_wall = wall(time)) !== null && _wall !== void 0 ? _wall : `-${format(span.end - time)}`;
50
+ }
51
+ function flush() {
52
+ frame = 0;
53
+ if (pending === null) return;
54
+ video.currentTime = pending;
55
+ pending = null;
56
+ }
57
+ const current = el("span", "current-time", "0:00");
58
+ const duration = el("span", "duration", "0:00");
59
+ const bar = slider({
60
+ name: "seek",
61
+ label: "Seek",
62
+ step: options.seekStep,
63
+ page: options.seekPage,
64
+ onInput(value) {
65
+ bar.set(value, say(value));
66
+ current.textContent = label(value);
67
+ if (!bar.dragging()) {
68
+ pending = null;
69
+ video.currentTime = value;
70
+ return;
71
+ }
72
+ pending = value;
73
+ if (frame === 0) frame = requestAnimationFrame(flush);
74
+ }
75
+ });
76
+ const root = el("div", "row seek-row");
77
+ root.append(current, bar.root, duration);
78
+ const buffered = el("div", "buffered seek-buffered");
79
+ const hover = el("div", "hover seek-hover");
80
+ const edge = el("div", "edge seek-edge");
81
+ hover.hidden = true;
82
+ edge.hidden = true;
83
+ bar.track.prepend(buffered);
84
+ bar.track.append(hover, edge);
85
+ const preview = el("div", "preview seek-preview");
86
+ const image = el("div", "preview-image seek-preview-image");
87
+ const tile = el("div", "preview-tile seek-preview-tile");
88
+ const time = el("span", "preview-time seek-preview-time");
89
+ image.append(tile);
90
+ image.hidden = true;
91
+ preview.append(image, time);
92
+ preview.hidden = true;
93
+ bar.root.append(preview);
94
+ /** The width the page asked for through the token, or the default. */
95
+ function previewWidth() {
96
+ const set = Number.parseFloat(getComputedStyle(preview).getPropertyValue("--mbx-preview-width"));
97
+ return Number.isFinite(set) && set > 0 ? set : PREVIEW_WIDTH;
98
+ }
99
+ /** The tile for a time, if the track has one, drawn at its size and scaled to fit. */
100
+ function paintTile(when) {
101
+ var _source$thumbnails$at, _source$thumbnails;
102
+ const found = (_source$thumbnails$at = (_source$thumbnails = source.thumbnails) === null || _source$thumbnails === void 0 ? void 0 : _source$thumbnails.at(when)) !== null && _source$thumbnails$at !== void 0 ? _source$thumbnails$at : null;
103
+ image.hidden = found === null;
104
+ if (found === null) return;
105
+ const scale = previewWidth() / found.width;
106
+ image.style.width = `${found.width * scale}px`;
107
+ image.style.height = `${found.height * scale}px`;
108
+ tile.style.width = `${found.width}px`;
109
+ tile.style.height = `${found.height}px`;
110
+ tile.style.backgroundImage = `url("${found.url}")`;
111
+ tile.style.backgroundPosition = `-${found.x}px -${found.y}px`;
112
+ tile.style.transform = `scale(${scale})`;
113
+ }
114
+ function paintBuffered() {
115
+ var _buffered$lastElement;
116
+ const parts = clip(spans(video.buffered), span);
117
+ while (buffered.childElementCount > parts.length) (_buffered$lastElement = buffered.lastElementChild) === null || _buffered$lastElement === void 0 || _buffered$lastElement.remove();
118
+ while (buffered.childElementCount < parts.length) buffered.append(el("div", "buffered-range seek-buffered-range"));
119
+ let i = 0;
120
+ for (const child of buffered.children) {
121
+ const part = parts[i];
122
+ i += 1;
123
+ if (part === void 0) break;
124
+ const node = child;
125
+ node.style.left = percent(part[0]);
126
+ node.style.width = percent(part[1] - part[0]);
127
+ }
128
+ }
129
+ /** Whether a live window is wide enough to be worth a bar. */
130
+ function worthSeeking() {
131
+ var _source$bufferGoal, _source$bufferGoal2;
132
+ if (options.liveWindow === 0) return true;
133
+ const goal = (_source$bufferGoal = (_source$bufferGoal2 = source.bufferGoal) === null || _source$bufferGoal2 === void 0 ? void 0 : _source$bufferGoal2.call(source)) !== null && _source$bufferGoal !== void 0 ? _source$bufferGoal : BUFFER_GOAL;
134
+ return span.end - span.start >= options.liveWindow * goal;
135
+ }
136
+ function tick() {
137
+ var _source$window, _source$window2;
138
+ const on = isLive();
139
+ const own = on ? (_source$window = (_source$window2 = source.window) === null || _source$window2 === void 0 ? void 0 : _source$window2.call(source)) !== null && _source$window !== void 0 ? _source$window : null : null;
140
+ span = own !== null && own.end > own.start ? own : window(video.duration, spans(video.seekable), on);
141
+ bar.range(span.start, span.end);
142
+ paintBuffered();
143
+ const live = source.live;
144
+ edge.hidden = !on;
145
+ if (on && live !== void 0 && live.edge !== null) edge.style.left = percent(fraction(live.edge, span));
146
+ const seekable = !on || worthSeeking();
147
+ bar.root.hidden = !seekable;
148
+ current.hidden = !seekable;
149
+ duration.hidden = on;
150
+ flag("live", on);
151
+ flag("seekable", seekable);
152
+ if (!on) duration.textContent = format(span.end);
153
+ if (!bar.dragging()) {
154
+ bar.set(video.currentTime, say(video.currentTime));
155
+ current.textContent = label(video.currentTime);
156
+ }
157
+ }
158
+ /** The hover marker and the preview follow the pointer, clamped to the bar. */
159
+ function point(event) {
160
+ const rect = bar.rail.getBoundingClientRect();
161
+ if (rect.width === 0 || span.end <= span.start) return;
162
+ const part = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width));
163
+ hover.hidden = false;
164
+ hover.style.left = percent(part);
165
+ preview.hidden = false;
166
+ const when = at(part, span);
167
+ time.textContent = label(when);
168
+ paintTile(when);
169
+ const whole = bar.root.getBoundingClientRect();
170
+ const half = preview.offsetWidth / 2;
171
+ const x = rect.left - whole.left + part * rect.width;
172
+ preview.style.left = `${Math.min(whole.width - half, Math.max(half, x))}px`;
173
+ }
174
+ function leave() {
175
+ hover.hidden = true;
176
+ preview.hidden = true;
177
+ }
178
+ bar.root.addEventListener("pointermove", point);
179
+ bar.root.addEventListener("pointerleave", leave);
180
+ for (const name of EVENTS) video.addEventListener(name, tick);
181
+ tick();
182
+ return {
183
+ root,
184
+ attach(found) {
185
+ source = found;
186
+ tick();
187
+ },
188
+ detach() {
189
+ source = {};
190
+ leave();
191
+ tick();
192
+ },
193
+ dispose() {
194
+ cancelAnimationFrame(frame);
195
+ bar.root.removeEventListener("pointermove", point);
196
+ bar.root.removeEventListener("pointerleave", leave);
197
+ for (const name of EVENTS) video.removeEventListener(name, tick);
198
+ bar.dispose();
199
+ }
200
+ };
201
+ }
202
+ //#endregion
203
+ export { seekBar };
@@ -0,0 +1,136 @@
1
+ import { el, state } from "../dom.js";
2
+ //#region src/controls/slider.ts
3
+ /**
4
+ * The slider primitive the seek bar and the volume share: a `div` with
5
+ * `role="slider"`, pointer capture for the drag, and the arrow keys. It
6
+ * owns geometry only. What the value means and what happens on change is
7
+ * the caller's, through `onInput`.
8
+ *
9
+ * Not `<input type="range">`: a range input cannot carry layers such as
10
+ * buffered ranges, its geometry is not readable for a preview anchor, and
11
+ * it renders differently per browser. The track is exposed so a caller can
12
+ * lay its own layers on it.
13
+ *
14
+ * The track and the thumb sit on a rail inset from the root's edges by half
15
+ * a thumb, so the thumb at either end is whole and not clipped, and every
16
+ * fraction is measured against the rail.
17
+ */
18
+ function clamp(value, min, max) {
19
+ return Math.min(max, Math.max(min, value));
20
+ }
21
+ function slider(options) {
22
+ const { name, onInput } = options;
23
+ const root = el("div", `slider ${name}`);
24
+ root.setAttribute("role", "slider");
25
+ root.setAttribute("aria-label", options.label);
26
+ root.tabIndex = 0;
27
+ const rail = el("div", `rail ${name}-rail`);
28
+ const track = el("div", `track ${name}-track`);
29
+ const fill = el("div", `fill ${name}-fill`);
30
+ const thumb = el("div", `thumb ${name}-thumb`);
31
+ track.append(fill);
32
+ rail.append(track, thumb);
33
+ root.append(rail);
34
+ let min = 0;
35
+ let max = 1;
36
+ let value = 0;
37
+ let held = false;
38
+ function paint() {
39
+ const percent = `${(max > min ? (value - min) / (max - min) : 0) * 100}%`;
40
+ fill.style.width = percent;
41
+ thumb.style.left = percent;
42
+ }
43
+ /** The value under a horizontal pointer position. */
44
+ function at(clientX) {
45
+ const rect = rail.getBoundingClientRect();
46
+ if (rect.width === 0) return min;
47
+ return min + clamp((clientX - rect.left) / rect.width, 0, 1) * (max - min);
48
+ }
49
+ function hold(on) {
50
+ held = on;
51
+ state(root, `slider ${name}`, { dragging: on });
52
+ }
53
+ function down(event) {
54
+ if (event.button !== 0) return;
55
+ event.preventDefault();
56
+ hold(true);
57
+ try {
58
+ root.setPointerCapture(event.pointerId);
59
+ } catch (_unused) {}
60
+ onInput(at(event.clientX));
61
+ }
62
+ function move(event) {
63
+ if (held) onInput(at(event.clientX));
64
+ }
65
+ function up(event) {
66
+ if (!held) return;
67
+ hold(false);
68
+ onInput(at(event.clientX));
69
+ }
70
+ function cancel() {
71
+ if (held) hold(false);
72
+ }
73
+ function key(event) {
74
+ let next;
75
+ switch (event.key) {
76
+ case "ArrowRight":
77
+ case "ArrowUp":
78
+ next = value + options.step;
79
+ break;
80
+ case "ArrowLeft":
81
+ case "ArrowDown":
82
+ next = value - options.step;
83
+ break;
84
+ case "PageUp":
85
+ next = value + options.page;
86
+ break;
87
+ case "PageDown":
88
+ next = value - options.page;
89
+ break;
90
+ case "Home":
91
+ next = min;
92
+ break;
93
+ case "End":
94
+ next = max;
95
+ break;
96
+ default: return;
97
+ }
98
+ event.preventDefault();
99
+ onInput(clamp(next, min, max));
100
+ }
101
+ root.addEventListener("pointerdown", down);
102
+ root.addEventListener("pointermove", move);
103
+ root.addEventListener("pointerup", up);
104
+ root.addEventListener("pointercancel", cancel);
105
+ root.addEventListener("keydown", key);
106
+ return {
107
+ root,
108
+ track,
109
+ rail,
110
+ dragging() {
111
+ return held;
112
+ },
113
+ range(low, high) {
114
+ min = low;
115
+ max = high;
116
+ root.setAttribute("aria-valuemin", String(low));
117
+ root.setAttribute("aria-valuemax", String(high));
118
+ paint();
119
+ },
120
+ set(next, text) {
121
+ value = clamp(next, min, max);
122
+ root.setAttribute("aria-valuenow", String(value));
123
+ root.setAttribute("aria-valuetext", text);
124
+ paint();
125
+ },
126
+ dispose() {
127
+ root.removeEventListener("pointerdown", down);
128
+ root.removeEventListener("pointermove", move);
129
+ root.removeEventListener("pointerup", up);
130
+ root.removeEventListener("pointercancel", cancel);
131
+ root.removeEventListener("keydown", key);
132
+ }
133
+ };
134
+ }
135
+ //#endregion
136
+ export { slider };
@@ -0,0 +1,41 @@
1
+ import { menu } from "./menu.js";
2
+ //#region src/controls/speed.ts
3
+ const RATES = [
4
+ .5,
5
+ .75,
6
+ 1,
7
+ 1.25,
8
+ 1.5,
9
+ 2
10
+ ];
11
+ function speedMenu(video) {
12
+ const control = menu({
13
+ name: "speed",
14
+ label: "Playback speed",
15
+ icon: "playback-speed"
16
+ });
17
+ function render() {
18
+ const items = RATES.map((rate) => [String(rate), rate === 1 ? "Normal" : `${rate}×`]);
19
+ const current = String(video.playbackRate);
20
+ if (!RATES.some((rate) => String(rate) === current)) items.push([current, `${current}×`]);
21
+ control.fill([{
22
+ name: "rate",
23
+ items,
24
+ value: current,
25
+ onSelect(value) {
26
+ video.playbackRate = Number(value);
27
+ }
28
+ }]);
29
+ }
30
+ video.addEventListener("ratechange", render);
31
+ render();
32
+ return {
33
+ root: control.root,
34
+ dispose() {
35
+ video.removeEventListener("ratechange", render);
36
+ control.dispose();
37
+ }
38
+ };
39
+ }
40
+ //#endregion
41
+ export { speedMenu };
@@ -0,0 +1,27 @@
1
+ //#region src/controls/time.ts
2
+ /** Time as the bar shows it and as a screen reader hears it. Pure, node-tested. */
3
+ function pad(n) {
4
+ return n < 10 ? `0${n}` : `${n}`;
5
+ }
6
+ /** `m:ss`, or `h:mm:ss` from an hour up. Anything not a finite positive number reads as zero. */
7
+ function format(seconds) {
8
+ const total = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0;
9
+ const hours = Math.floor(total / 3600);
10
+ const minutes = Math.floor(total % 3600 / 60);
11
+ const rest = total % 60;
12
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(rest)}` : `${minutes}:${pad(rest)}`;
13
+ }
14
+ /** A wall-clock time in the viewer's locale, seconds included: "14:23:05". */
15
+ function clock(epochSeconds) {
16
+ return (/* @__PURE__ */ new Date(epochSeconds * 1e3)).toLocaleTimeString([], {
17
+ hour: "2-digit",
18
+ minute: "2-digit",
19
+ second: "2-digit"
20
+ });
21
+ }
22
+ /** What a screen reader hears for a position: "1:23 of 4:56", or the position alone when the duration is unknown or infinite. */
23
+ function describe(current, duration) {
24
+ return Number.isFinite(duration) ? `${format(current)} of ${format(duration)}` : format(current);
25
+ }
26
+ //#endregion
27
+ export { clock, describe, format };
@@ -0,0 +1,30 @@
1
+ import { slider } from "./slider.js";
2
+ //#region src/controls/volume.ts
3
+ function volumeSlider(video) {
4
+ const bar = slider({
5
+ name: "volume",
6
+ label: "Volume",
7
+ step: .05,
8
+ page: .2,
9
+ onInput(value) {
10
+ video.volume = value;
11
+ if (value > 0) video.muted = false;
12
+ }
13
+ });
14
+ bar.range(0, 1);
15
+ function render() {
16
+ const level = video.muted ? 0 : video.volume;
17
+ bar.set(level, `${Math.round(level * 100)}%`);
18
+ }
19
+ video.addEventListener("volumechange", render);
20
+ render();
21
+ return {
22
+ root: bar.root,
23
+ dispose() {
24
+ video.removeEventListener("volumechange", render);
25
+ bar.dispose();
26
+ }
27
+ };
28
+ }
29
+ //#endregion
30
+ export { volumeSlider };
@@ -0,0 +1,43 @@
1
+ //#region src/dom.ts
2
+ /**
3
+ * The DOM helpers the panels share.
4
+ *
5
+ * Every element a panel creates carries a `part`, because `::part()` cannot
6
+ * descend: `::part(quality) select` matches nothing, so an element without a
7
+ * name of its own is unreachable from the page's stylesheet. Names are public
8
+ * API from the first release. Each element also carries a generic name before
9
+ * its specific one, so `::part(select)` reaches every menu at once.
10
+ */
11
+ /** An element with its part names, and its text when it has any. */
12
+ function el(tag, part, text) {
13
+ const node = document.createElement(tag);
14
+ node.setAttribute("part", part);
15
+ if (text !== void 0) node.textContent = text;
16
+ return node;
17
+ }
18
+ /** A `<select>` inside its `<label>`, so neither needs an id to be associated. */
19
+ function menu(name, text) {
20
+ const label = el("label", `label ${name}-label`);
21
+ label.append(el("span", `text ${name}-text`, text));
22
+ const select = el("select", `select ${name}-select`);
23
+ label.append(select);
24
+ return [label, select];
25
+ }
26
+ /** Replaces a menu's options and selects `value`. */
27
+ function fill(select, items, value) {
28
+ select.replaceChildren();
29
+ for (const [id, text] of items) {
30
+ const option = el("option", "option", text);
31
+ option.value = id;
32
+ select.append(option);
33
+ }
34
+ select.value = value;
35
+ }
36
+ /** Sets `part` to a base plus the state names that are on. State rides the part name because `::part()` takes no attribute selector. */
37
+ function state(node, base, on) {
38
+ const names = [base];
39
+ for (const key of Object.keys(on)) if (on[key] === true) names.push(key);
40
+ node.setAttribute("part", names.join(" "));
41
+ }
42
+ //#endregion
43
+ export { el, fill, menu, state };