@scarlett-player/ui 1.12.0 → 1.13.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.
- package/README.md +34 -0
- package/dist/index.cjs +272 -50
- package/dist/index.d.cts +160 -2
- package/dist/index.d.ts +160 -2
- package/dist/index.js +268 -49
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -126,6 +126,40 @@ Any id a plugin registers through the control registry (`share`, `chapters`,
|
|
|
126
126
|
`playlist-previous`, `playlist-next`, ...) can be placed in the same list. A
|
|
127
127
|
control whose plugin is not loaded is skipped.
|
|
128
128
|
|
|
129
|
+
## Timeline extensions
|
|
130
|
+
|
|
131
|
+
The control registry lets a plugin contribute a *button*. `registerTimelineExtension` lets one contribute an editing layer over the **playback timeline** - the clip plugin's in/out handles are the first, and the reason it exists: an editor with its own miniature track cannot express a 30-second selection on a two-hour source, and makes the viewer pick points on a rail that is not the one they were just scrubbing.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { registerTimelineExtension } from '@scarlett-player/ui';
|
|
135
|
+
|
|
136
|
+
const release = registerTimelineExtension(api.container, (surface) => {
|
|
137
|
+
surface.element.appendChild(myHandles);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
update: () => reposition(surface.getRailRect()),
|
|
141
|
+
onSeekStart: () => suspendMyPreview(),
|
|
142
|
+
onSeekEnd: () => {},
|
|
143
|
+
destroy: () => myHandles.remove(),
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`surface` gives an extension four things and nothing else:
|
|
149
|
+
|
|
150
|
+
| Member | What it is |
|
|
151
|
+
|---|---|
|
|
152
|
+
| `element` | The layer to paint into: a sibling of the `role="slider"` seek element (never a child of it), positioned with exactly the rail's horizontal geometry and centred on it. `pointer-events: none`, so plain presses on the rail keep seeking; give your own hit targets `pointer-events: auto` |
|
|
153
|
+
| `getRailRect()` | The rail's box, in client coordinates - the same box the slider maps a press against, so a `clientX`-to-time mapping agrees with the player's own seeking to the pixel |
|
|
154
|
+
| `setEditing(active)` | Holds the control bar and progress bar visible and reserves the vertical room an editor needs around the rail. Independent of any control's `isMenuOpen()`, so an extension opened from a host's own button still holds the bar |
|
|
155
|
+
| `setDragging(active)` | Suppresses ordinary seeking while the extension owns the pointer, and gets the hover tooltip out of the way |
|
|
156
|
+
|
|
157
|
+
The extension gives back `update()` (called with the progress bar's own update, so it never has to observe the rail itself), `onSeekStart()` / `onSeekEnd()` for ordinary seeks it does **not** own - mouse, touch, keyboard and cancellation alike - and `destroy()`.
|
|
158
|
+
|
|
159
|
+
There is one active extension per player, keyed by container. Registering again replaces and destroys the previous one; the returned disposer only ever removes the registration it made, so a late cleanup cannot unmount its successor. Registration works before or after the UI plugin initialises. `unregisterTimelineExtension(container)` drops a player's registration wholesale, and `hasTimelineExtension(container)` reports whether one is present.
|
|
160
|
+
|
|
161
|
+
The UI package reads no plugin-specific state and gains no `IPluginAPI` surface for this: it offers a positioned layer and four lifecycle calls, and an extension paints into it.
|
|
162
|
+
|
|
129
163
|
## Keyboard Shortcuts
|
|
130
164
|
|
|
131
165
|
| Key | Action |
|
package/dist/index.cjs
CHANGED
|
@@ -26,15 +26,18 @@ __export(index_exports, {
|
|
|
26
26
|
formatLiveTime: () => import_core2.formatLiveTime,
|
|
27
27
|
formatTime: () => import_core2.formatTime,
|
|
28
28
|
getControlFactory: () => getControlFactory,
|
|
29
|
+
hasTimelineExtension: () => hasTimelineExtension,
|
|
29
30
|
icons: () => icons,
|
|
30
31
|
planFit: () => planFit,
|
|
31
32
|
registerControl: () => registerControl,
|
|
33
|
+
registerTimelineExtension: () => registerTimelineExtension,
|
|
32
34
|
resetControlRegistry: () => resetControlRegistry,
|
|
33
35
|
resolveFitItems: () => resolveFitItems,
|
|
34
36
|
styles: () => styles,
|
|
35
37
|
uiPlugin: () => uiPlugin,
|
|
36
38
|
unregisterControl: () => unregisterControl,
|
|
37
|
-
unregisterControlsFor: () => unregisterControlsFor
|
|
39
|
+
unregisterControlsFor: () => unregisterControlsFor,
|
|
40
|
+
unregisterTimelineExtension: () => unregisterTimelineExtension
|
|
38
41
|
});
|
|
39
42
|
module.exports = __toCommonJS(index_exports);
|
|
40
43
|
var import_core4 = require("@scarlett-player/core");
|
|
@@ -302,6 +305,37 @@ var styles = `
|
|
|
302
305
|
height: 5px;
|
|
303
306
|
}
|
|
304
307
|
|
|
308
|
+
/* ============================================
|
|
309
|
+
Timeline extension layer (timeline-registry.ts)
|
|
310
|
+
|
|
311
|
+
A zero-height line lying exactly on the rail's centre, spanning exactly the
|
|
312
|
+
rail's width, so an extension can position by percentage and land on the
|
|
313
|
+
same pixels the seek slider maps a press to. The 10px offset is the rail
|
|
314
|
+
centre in BOTH wrapper modes: the fine-pointer wrapper is 20px tall with the
|
|
315
|
+
3px rail centred (8.5..11.5 from the bottom), and the coarse-pointer wrapper
|
|
316
|
+
is 44px tall with 8.5px of bottom padding and align-items: flex-end, which
|
|
317
|
+
puts the rail in the same place.
|
|
318
|
+
|
|
319
|
+
Inert by default: only the explicit hit targets an extension puts inside it
|
|
320
|
+
take pointer input, so an ordinary press on the rail still seeks.
|
|
321
|
+
============================================ */
|
|
322
|
+
.sp-progress__extension {
|
|
323
|
+
position: absolute;
|
|
324
|
+
left: 0;
|
|
325
|
+
right: 0;
|
|
326
|
+
bottom: 10px;
|
|
327
|
+
height: 0;
|
|
328
|
+
z-index: 3;
|
|
329
|
+
pointer-events: none;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* Editing reserves a lane below the rail for an extension's second handle, by
|
|
333
|
+
lifting the whole wrapper clear of the control bar. The lane above needs no
|
|
334
|
+
reservation - it is over the picture. */
|
|
335
|
+
.sp-progress-wrapper--editing {
|
|
336
|
+
bottom: calc(92px + var(--sp-inset-bottom, 0px));
|
|
337
|
+
}
|
|
338
|
+
|
|
305
339
|
.sp-progress__track {
|
|
306
340
|
position: absolute;
|
|
307
341
|
top: 0;
|
|
@@ -372,6 +406,19 @@ var styles = `
|
|
|
372
406
|
transform: translate(-50%, -50%) scale(1);
|
|
373
407
|
}
|
|
374
408
|
|
|
409
|
+
/* While an extension owns the pointer the bar must not also look like it is
|
|
410
|
+
scrubbing: the tooltip is suppressed inline by the control, and the handle
|
|
411
|
+
stops responding to hover growth.
|
|
412
|
+
|
|
413
|
+
This has to come after the :hover and --dragging rules AND match their
|
|
414
|
+
specificity, which is why the hover case is spelled out rather than left to
|
|
415
|
+
the bare class: an extension drag is a pointer drag, so the pointer is over
|
|
416
|
+
the wrapper the whole time and the hover rule is always the competing one. */
|
|
417
|
+
.sp-progress-wrapper--ext-dragging .sp-progress__handle,
|
|
418
|
+
.sp-progress-wrapper--ext-dragging:hover .sp-progress__handle {
|
|
419
|
+
transform: translate(-50%, -50%);
|
|
420
|
+
}
|
|
421
|
+
|
|
375
422
|
/* Thumbnail Preview */
|
|
376
423
|
.sp-thumbnail-preview {
|
|
377
424
|
position: absolute;
|
|
@@ -1525,9 +1572,46 @@ var ThumbnailPreview = class {
|
|
|
1525
1572
|
}
|
|
1526
1573
|
};
|
|
1527
1574
|
|
|
1575
|
+
// src/timeline-registry.ts
|
|
1576
|
+
var registrations = /* @__PURE__ */ new WeakMap();
|
|
1577
|
+
var hosts = /* @__PURE__ */ new WeakMap();
|
|
1578
|
+
function registerTimelineExtension(owner, factory) {
|
|
1579
|
+
const token = {};
|
|
1580
|
+
registrations.set(owner, { factory, token });
|
|
1581
|
+
hosts.get(owner)?.setFactory(factory);
|
|
1582
|
+
return () => {
|
|
1583
|
+
const current = registrations.get(owner);
|
|
1584
|
+
if (!current || current.token !== token) return;
|
|
1585
|
+
registrations.delete(owner);
|
|
1586
|
+
hosts.get(owner)?.setFactory(null);
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
function unregisterTimelineExtension(owner) {
|
|
1590
|
+
if (!registrations.has(owner)) return false;
|
|
1591
|
+
registrations.delete(owner);
|
|
1592
|
+
hosts.get(owner)?.setFactory(null);
|
|
1593
|
+
return true;
|
|
1594
|
+
}
|
|
1595
|
+
function hasTimelineExtension(owner) {
|
|
1596
|
+
return registrations.has(owner);
|
|
1597
|
+
}
|
|
1598
|
+
function attachTimelineHost(owner, host) {
|
|
1599
|
+
hosts.set(owner, host);
|
|
1600
|
+
const registration = registrations.get(owner);
|
|
1601
|
+
if (registration) host.setFactory(registration.factory);
|
|
1602
|
+
return () => {
|
|
1603
|
+
if (hosts.get(owner) === host) hosts.delete(owner);
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1528
1607
|
// src/controls/ProgressBar.ts
|
|
1608
|
+
var SEEK_KEYS = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowRight", "Home", "End"]);
|
|
1529
1609
|
var ProgressBar = class {
|
|
1530
|
-
|
|
1610
|
+
/**
|
|
1611
|
+
* @param api - The per-player plugin API
|
|
1612
|
+
* @param options - Optional hooks; see {@link ProgressBarOptions}
|
|
1613
|
+
*/
|
|
1614
|
+
constructor(api, options = {}) {
|
|
1531
1615
|
this.isDragging = false;
|
|
1532
1616
|
this.lastSeekTime = 0;
|
|
1533
1617
|
this.seekThrottleMs = 100;
|
|
@@ -1537,8 +1621,18 @@ var ProgressBar = class {
|
|
|
1537
1621
|
this.renderedChapters = null;
|
|
1538
1622
|
/** Duration the marker layer was last built against, since positions are a percentage of it. */
|
|
1539
1623
|
this.renderedDuration = 0;
|
|
1624
|
+
/** The mounted extension, or null when none is registered. */
|
|
1625
|
+
this.extension = null;
|
|
1626
|
+
/** Detaches this bar from the registry on destroy. */
|
|
1627
|
+
this.detachTimelineHost = null;
|
|
1628
|
+
/** True while the extension holds the pointer: ordinary seeking is suppressed. */
|
|
1629
|
+
this.extensionDragging = false;
|
|
1630
|
+
/** True while the extension is editing: the bar is held open and geometry reserved. */
|
|
1631
|
+
this.extensionEditing = false;
|
|
1540
1632
|
this.onMouseDown = (e) => {
|
|
1633
|
+
if (this.isExtensionInput(e)) return;
|
|
1541
1634
|
e.preventDefault();
|
|
1635
|
+
this.extension?.onSeekStart();
|
|
1542
1636
|
const video = getVideo(this.api.container);
|
|
1543
1637
|
this.wasPlayingBeforeDrag = video ? !video.paused : false;
|
|
1544
1638
|
this.isDragging = true;
|
|
@@ -1557,6 +1651,7 @@ var ProgressBar = class {
|
|
|
1557
1651
|
this.seek(e.clientX, true);
|
|
1558
1652
|
this.isDragging = false;
|
|
1559
1653
|
this.el.classList.remove("sp-progress--dragging");
|
|
1654
|
+
this.extension?.onSeekEnd();
|
|
1560
1655
|
if (this.wasPlayingBeforeDrag) {
|
|
1561
1656
|
const video = getVideo(this.api.container);
|
|
1562
1657
|
if (video && video.paused) {
|
|
@@ -1571,7 +1666,9 @@ var ProgressBar = class {
|
|
|
1571
1666
|
}
|
|
1572
1667
|
};
|
|
1573
1668
|
this.onTouchStart = (e) => {
|
|
1669
|
+
if (this.isExtensionInput(e)) return;
|
|
1574
1670
|
e.preventDefault();
|
|
1671
|
+
this.extension?.onSeekStart();
|
|
1575
1672
|
const video = getVideo(this.api.container);
|
|
1576
1673
|
this.wasPlayingBeforeDrag = video ? !video.paused : false;
|
|
1577
1674
|
this.isDragging = true;
|
|
@@ -1594,6 +1691,7 @@ var ProgressBar = class {
|
|
|
1594
1691
|
}
|
|
1595
1692
|
this.isDragging = false;
|
|
1596
1693
|
this.el.classList.remove("sp-progress--dragging");
|
|
1694
|
+
this.extension?.onSeekEnd();
|
|
1597
1695
|
if (this.wasPlayingBeforeDrag) {
|
|
1598
1696
|
const video = getVideo(this.api.container);
|
|
1599
1697
|
if (video && video.paused) {
|
|
@@ -1610,6 +1708,7 @@ var ProgressBar = class {
|
|
|
1610
1708
|
}
|
|
1611
1709
|
};
|
|
1612
1710
|
this.onMouseMove = (e) => {
|
|
1711
|
+
if (this.isExtensionInput(e)) return;
|
|
1613
1712
|
this.updateTooltip(e.clientX);
|
|
1614
1713
|
};
|
|
1615
1714
|
this.onMouseLeave = () => {
|
|
@@ -1621,51 +1720,16 @@ var ProgressBar = class {
|
|
|
1621
1720
|
this.onKeyDown = (e) => {
|
|
1622
1721
|
const video = getVideo(this.api.container);
|
|
1623
1722
|
if (!video) return;
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
e.preventDefault();
|
|
1631
|
-
video.currentTime = Math.max(seekableRange.start, video.currentTime - step);
|
|
1632
|
-
break;
|
|
1633
|
-
case "ArrowRight":
|
|
1634
|
-
e.preventDefault();
|
|
1635
|
-
video.currentTime = Math.min(seekableRange.end, video.currentTime + step);
|
|
1636
|
-
break;
|
|
1637
|
-
case "Home":
|
|
1638
|
-
e.preventDefault();
|
|
1639
|
-
video.currentTime = seekableRange.start;
|
|
1640
|
-
break;
|
|
1641
|
-
case "End":
|
|
1642
|
-
e.preventDefault();
|
|
1643
|
-
video.currentTime = seekableRange.end;
|
|
1644
|
-
break;
|
|
1645
|
-
}
|
|
1646
|
-
} else {
|
|
1647
|
-
const duration = this.api.getState("duration") || 0;
|
|
1648
|
-
switch (e.key) {
|
|
1649
|
-
case "ArrowLeft":
|
|
1650
|
-
e.preventDefault();
|
|
1651
|
-
video.currentTime = Math.max(0, video.currentTime - step);
|
|
1652
|
-
break;
|
|
1653
|
-
case "ArrowRight":
|
|
1654
|
-
e.preventDefault();
|
|
1655
|
-
video.currentTime = Math.min(duration, video.currentTime + step);
|
|
1656
|
-
break;
|
|
1657
|
-
case "Home":
|
|
1658
|
-
e.preventDefault();
|
|
1659
|
-
video.currentTime = 0;
|
|
1660
|
-
break;
|
|
1661
|
-
case "End":
|
|
1662
|
-
e.preventDefault();
|
|
1663
|
-
video.currentTime = duration;
|
|
1664
|
-
break;
|
|
1665
|
-
}
|
|
1723
|
+
if (!SEEK_KEYS.has(e.key)) return;
|
|
1724
|
+
this.extension?.onSeekStart();
|
|
1725
|
+
try {
|
|
1726
|
+
this.applyKeyboardSeek(e, video);
|
|
1727
|
+
} finally {
|
|
1728
|
+
this.extension?.onSeekEnd();
|
|
1666
1729
|
}
|
|
1667
1730
|
};
|
|
1668
1731
|
this.api = api;
|
|
1732
|
+
this.options = options;
|
|
1669
1733
|
this.wrapper = createElement("div", { className: "sp-progress-wrapper" });
|
|
1670
1734
|
this.el = createElement("div", { className: "sp-progress" });
|
|
1671
1735
|
const track = createElement("div", { className: "sp-progress__track" });
|
|
@@ -1684,6 +1748,8 @@ var ProgressBar = class {
|
|
|
1684
1748
|
this.el.appendChild(this.thumbnailPreview.getElement());
|
|
1685
1749
|
this.el.appendChild(this.tooltip);
|
|
1686
1750
|
this.wrapper.appendChild(this.el);
|
|
1751
|
+
this.extensionLayer = createElement("div", { className: "sp-progress__extension" });
|
|
1752
|
+
this.wrapper.appendChild(this.extensionLayer);
|
|
1687
1753
|
this.el.setAttribute("role", "slider");
|
|
1688
1754
|
this.el.setAttribute("aria-label", "Seek");
|
|
1689
1755
|
this.el.setAttribute("aria-valuemin", "0");
|
|
@@ -1701,6 +1767,90 @@ var ProgressBar = class {
|
|
|
1701
1767
|
document.addEventListener("touchmove", this.onDocTouchMove, { passive: false });
|
|
1702
1768
|
document.addEventListener("touchend", this.onTouchEnd);
|
|
1703
1769
|
document.addEventListener("touchcancel", this.onTouchEnd);
|
|
1770
|
+
this.detachTimelineHost = attachTimelineHost(this.api.container, {
|
|
1771
|
+
setFactory: (factory) => this.mountExtension(factory)
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
// --------------------------------------------------------------------------
|
|
1775
|
+
// Timeline extension seam
|
|
1776
|
+
// --------------------------------------------------------------------------
|
|
1777
|
+
/**
|
|
1778
|
+
* Mount a registered extension factory, replacing any predecessor.
|
|
1779
|
+
*
|
|
1780
|
+
* The previous extension is destroyed and every lease it held is released
|
|
1781
|
+
* first, so a replacement never inherits a stale editing or dragging state.
|
|
1782
|
+
*
|
|
1783
|
+
* @param factory - The factory to mount, or null to unmount
|
|
1784
|
+
*/
|
|
1785
|
+
mountExtension(factory) {
|
|
1786
|
+
if (this.extension) {
|
|
1787
|
+
this.extension.destroy();
|
|
1788
|
+
this.extension = null;
|
|
1789
|
+
this.setExtensionDragging(false);
|
|
1790
|
+
this.setExtensionEditing(false);
|
|
1791
|
+
this.extensionLayer.replaceChildren();
|
|
1792
|
+
}
|
|
1793
|
+
if (!factory) return;
|
|
1794
|
+
const surface = {
|
|
1795
|
+
element: this.extensionLayer,
|
|
1796
|
+
getRailRect: () => this.el.getBoundingClientRect(),
|
|
1797
|
+
setEditing: (active) => this.setExtensionEditing(active),
|
|
1798
|
+
setDragging: (active) => this.setExtensionDragging(active)
|
|
1799
|
+
};
|
|
1800
|
+
this.extension = factory(surface);
|
|
1801
|
+
this.extension.update();
|
|
1802
|
+
}
|
|
1803
|
+
/** Whether a registered extension is currently editing. */
|
|
1804
|
+
isTimelineEditing() {
|
|
1805
|
+
return this.extensionEditing;
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Apply the editing lease: reserve the handle lanes and hold the bar open.
|
|
1809
|
+
*
|
|
1810
|
+
* @param active - Whether the extension is editing
|
|
1811
|
+
*/
|
|
1812
|
+
setExtensionEditing(active) {
|
|
1813
|
+
if (this.extensionEditing === active) return;
|
|
1814
|
+
this.extensionEditing = active;
|
|
1815
|
+
this.wrapper.classList.toggle("sp-progress-wrapper--editing", active);
|
|
1816
|
+
if (active) this.show();
|
|
1817
|
+
this.options.onEditingChange?.(active);
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Apply the pointer lease: suppress ordinary seeking while the extension
|
|
1821
|
+
* owns the gesture, and get the hover tooltip out of the way.
|
|
1822
|
+
*
|
|
1823
|
+
* @param active - Whether the extension owns the pointer
|
|
1824
|
+
*/
|
|
1825
|
+
setExtensionDragging(active) {
|
|
1826
|
+
if (this.extensionDragging === active) return;
|
|
1827
|
+
this.extensionDragging = active;
|
|
1828
|
+
this.wrapper.classList.toggle("sp-progress-wrapper--ext-dragging", active);
|
|
1829
|
+
if (active) {
|
|
1830
|
+
this.isDragging = false;
|
|
1831
|
+
this.el.classList.remove("sp-progress--dragging");
|
|
1832
|
+
this.tooltip.style.opacity = "0";
|
|
1833
|
+
this.thumbnailPreview.hide();
|
|
1834
|
+
} else {
|
|
1835
|
+
this.tooltip.style.opacity = "";
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
/**
|
|
1839
|
+
* Whether an input event belongs to the extension rather than to seeking.
|
|
1840
|
+
*
|
|
1841
|
+
* Two independent reasons, and both are needed: the extension holds the
|
|
1842
|
+
* pointer lease (its drag has moved off its handle and onto the rail), or
|
|
1843
|
+
* the event started inside the extension layer at all. Touch and mouse
|
|
1844
|
+
* compatibility events replay a handle press as a wrapper press, and only
|
|
1845
|
+
* the target check catches those.
|
|
1846
|
+
*
|
|
1847
|
+
* @param event - The incoming pointer, mouse or touch event
|
|
1848
|
+
* @returns True when the timeline must not act on it
|
|
1849
|
+
*/
|
|
1850
|
+
isExtensionInput(event) {
|
|
1851
|
+
if (this.extensionDragging) return true;
|
|
1852
|
+
const target = event.target;
|
|
1853
|
+
return target instanceof Node && this.extensionLayer.contains(target);
|
|
1704
1854
|
}
|
|
1705
1855
|
render() {
|
|
1706
1856
|
return this.wrapper;
|
|
@@ -1760,6 +1910,7 @@ var ProgressBar = class {
|
|
|
1760
1910
|
this.el.setAttribute("aria-valuenow", String(Math.floor(currentTime)));
|
|
1761
1911
|
this.el.setAttribute("aria-valuetext", (0, import_core2.formatTime)(currentTime));
|
|
1762
1912
|
}
|
|
1913
|
+
this.extension?.update();
|
|
1763
1914
|
}
|
|
1764
1915
|
/**
|
|
1765
1916
|
* Label of the chapter containing a point on the timeline.
|
|
@@ -1868,6 +2019,57 @@ var ProgressBar = class {
|
|
|
1868
2019
|
this.filled.style.width = `${percent * 100}%`;
|
|
1869
2020
|
this.handle.style.left = `${percent * 100}%`;
|
|
1870
2021
|
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Apply one keyboard seek to the element.
|
|
2024
|
+
*
|
|
2025
|
+
* @param e - The key event (already known to be a seek key)
|
|
2026
|
+
* @param video - The player's media element
|
|
2027
|
+
*/
|
|
2028
|
+
applyKeyboardSeek(e, video) {
|
|
2029
|
+
const step = 5;
|
|
2030
|
+
const live = this.api.getState("live");
|
|
2031
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
2032
|
+
if (live && seekableRange) {
|
|
2033
|
+
switch (e.key) {
|
|
2034
|
+
case "ArrowLeft":
|
|
2035
|
+
e.preventDefault();
|
|
2036
|
+
video.currentTime = Math.max(seekableRange.start, video.currentTime - step);
|
|
2037
|
+
break;
|
|
2038
|
+
case "ArrowRight":
|
|
2039
|
+
e.preventDefault();
|
|
2040
|
+
video.currentTime = Math.min(seekableRange.end, video.currentTime + step);
|
|
2041
|
+
break;
|
|
2042
|
+
case "Home":
|
|
2043
|
+
e.preventDefault();
|
|
2044
|
+
video.currentTime = seekableRange.start;
|
|
2045
|
+
break;
|
|
2046
|
+
case "End":
|
|
2047
|
+
e.preventDefault();
|
|
2048
|
+
video.currentTime = seekableRange.end;
|
|
2049
|
+
break;
|
|
2050
|
+
}
|
|
2051
|
+
} else {
|
|
2052
|
+
const duration = this.api.getState("duration") || 0;
|
|
2053
|
+
switch (e.key) {
|
|
2054
|
+
case "ArrowLeft":
|
|
2055
|
+
e.preventDefault();
|
|
2056
|
+
video.currentTime = Math.max(0, video.currentTime - step);
|
|
2057
|
+
break;
|
|
2058
|
+
case "ArrowRight":
|
|
2059
|
+
e.preventDefault();
|
|
2060
|
+
video.currentTime = Math.min(duration, video.currentTime + step);
|
|
2061
|
+
break;
|
|
2062
|
+
case "Home":
|
|
2063
|
+
e.preventDefault();
|
|
2064
|
+
video.currentTime = 0;
|
|
2065
|
+
break;
|
|
2066
|
+
case "End":
|
|
2067
|
+
e.preventDefault();
|
|
2068
|
+
video.currentTime = duration;
|
|
2069
|
+
break;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
1871
2073
|
seek(clientX, force = false) {
|
|
1872
2074
|
const video = getVideo(this.api.container);
|
|
1873
2075
|
if (!video) return;
|
|
@@ -1882,6 +2084,10 @@ var ProgressBar = class {
|
|
|
1882
2084
|
}
|
|
1883
2085
|
}
|
|
1884
2086
|
destroy() {
|
|
2087
|
+
this.detachTimelineHost?.();
|
|
2088
|
+
this.detachTimelineHost = null;
|
|
2089
|
+
this.extension?.destroy();
|
|
2090
|
+
this.extension = null;
|
|
1885
2091
|
this.wrapper.removeEventListener("mousedown", this.onMouseDown);
|
|
1886
2092
|
this.wrapper.removeEventListener("mousemove", this.onMouseMove);
|
|
1887
2093
|
this.wrapper.removeEventListener("mouseleave", this.onMouseLeave);
|
|
@@ -3777,10 +3983,13 @@ function uiPlugin(config = {}) {
|
|
|
3777
3983
|
updateControls();
|
|
3778
3984
|
});
|
|
3779
3985
|
};
|
|
3780
|
-
const hasOpenMenu = () =>
|
|
3781
|
-
|
|
3782
|
-
return
|
|
3783
|
-
|
|
3986
|
+
const hasOpenMenu = () => {
|
|
3987
|
+
if (progressBar?.isTimelineEditing()) return true;
|
|
3988
|
+
return controls.some((control) => {
|
|
3989
|
+
const menu = control;
|
|
3990
|
+
return typeof menu.isMenuOpen === "function" && menu.isMenuOpen();
|
|
3991
|
+
});
|
|
3992
|
+
};
|
|
3784
3993
|
const showControls = () => {
|
|
3785
3994
|
if (controlsVisible) {
|
|
3786
3995
|
resetHideTimer();
|
|
@@ -3962,7 +4171,17 @@ function uiPlugin(config = {}) {
|
|
|
3962
4171
|
bigPlayButton = new BigPlayButton(api, () => errorOverlay?.isVisible() ?? false);
|
|
3963
4172
|
container.appendChild(bigPlayButton.render());
|
|
3964
4173
|
}
|
|
3965
|
-
progressBar = new ProgressBar(api
|
|
4174
|
+
progressBar = new ProgressBar(api, {
|
|
4175
|
+
// A timeline editor is on screen for as long as the viewer needs it,
|
|
4176
|
+
// which is longer than any hide delay. Holding visibility here rather
|
|
4177
|
+
// than through a control's isMenuOpen() is deliberate: an extension
|
|
4178
|
+
// can be opened from a host's own button, in a layout that lists no
|
|
4179
|
+
// matching control at all.
|
|
4180
|
+
onEditingChange: (active) => {
|
|
4181
|
+
if (active) showControls();
|
|
4182
|
+
else resetHideTimer();
|
|
4183
|
+
}
|
|
4184
|
+
});
|
|
3966
4185
|
container.appendChild(progressBar.render());
|
|
3967
4186
|
if (!isPlaying) {
|
|
3968
4187
|
progressBar.show();
|
|
@@ -4132,13 +4351,16 @@ var index_default = uiPlugin;
|
|
|
4132
4351
|
formatLiveTime,
|
|
4133
4352
|
formatTime,
|
|
4134
4353
|
getControlFactory,
|
|
4354
|
+
hasTimelineExtension,
|
|
4135
4355
|
icons,
|
|
4136
4356
|
planFit,
|
|
4137
4357
|
registerControl,
|
|
4358
|
+
registerTimelineExtension,
|
|
4138
4359
|
resetControlRegistry,
|
|
4139
4360
|
resolveFitItems,
|
|
4140
4361
|
styles,
|
|
4141
4362
|
uiPlugin,
|
|
4142
4363
|
unregisterControl,
|
|
4143
|
-
unregisterControlsFor
|
|
4364
|
+
unregisterControlsFor,
|
|
4365
|
+
unregisterTimelineExtension
|
|
4144
4366
|
});
|