@scarlett-player/embed 1.7.0 → 1.8.1
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 +152 -26
- package/dist/create-embed.d.ts +76 -0
- package/dist/create-embed.d.ts.map +1 -0
- package/dist/{hls2.js → embed.audio.index.js} +1 -1
- package/dist/embed.audio.index.js.map +1 -0
- package/dist/embed.audio.js +702 -45
- package/dist/embed.audio.js.map +1 -1
- package/dist/embed.audio.umd.cjs +1 -1
- package/dist/embed.audio.umd.cjs.map +1 -1
- package/dist/embed.js +2855 -116
- package/dist/embed.js.map +1 -1
- package/dist/embed.umd.cjs +1 -1
- package/dist/embed.umd.cjs.map +1 -1
- package/dist/embed.video.js +2885 -147
- package/dist/embed.video.js.map +1 -1
- package/dist/embed.video.umd.cjs +1 -1
- package/dist/embed.video.umd.cjs.map +1 -1
- package/dist/hls.light.js +21849 -0
- package/dist/hls.light.js.map +1 -0
- package/dist/index-audio.d.ts +21 -0
- package/dist/index-audio.d.ts.map +1 -0
- package/dist/index-video.d.ts +20 -0
- package/dist/index-video.d.ts.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/parser.d.ts +15 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/types.d.ts +180 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.d.ts.map +1 -0
- package/iframe.html +54 -13
- package/package.json +20 -17
- package/dist/hls2.js.map +0 -1
package/dist/embed.video.js
CHANGED
|
@@ -932,9 +932,9 @@ class Logger {
|
|
|
932
932
|
* ```
|
|
933
933
|
*/
|
|
934
934
|
removeHandler(handler) {
|
|
935
|
-
const
|
|
936
|
-
if (
|
|
937
|
-
this.handlers.splice(
|
|
935
|
+
const index2 = this.handlers.indexOf(handler);
|
|
936
|
+
if (index2 !== -1) {
|
|
937
|
+
this.handlers.splice(index2, 1);
|
|
938
938
|
}
|
|
939
939
|
}
|
|
940
940
|
/**
|
|
@@ -1564,6 +1564,49 @@ class PluginManager {
|
|
|
1564
1564
|
}
|
|
1565
1565
|
}
|
|
1566
1566
|
}
|
|
1567
|
+
function videoIn(container) {
|
|
1568
|
+
return container.querySelector("video");
|
|
1569
|
+
}
|
|
1570
|
+
function isFullscreen(container) {
|
|
1571
|
+
const doc = document;
|
|
1572
|
+
const active = doc.fullscreenElement ?? doc.webkitFullscreenElement ?? null;
|
|
1573
|
+
if (active && container.contains(active)) {
|
|
1574
|
+
return true;
|
|
1575
|
+
}
|
|
1576
|
+
return !!videoIn(container)?.webkitDisplayingFullscreen;
|
|
1577
|
+
}
|
|
1578
|
+
async function enterFullscreen(container) {
|
|
1579
|
+
const el = container;
|
|
1580
|
+
if (el.requestFullscreen) {
|
|
1581
|
+
await el.requestFullscreen();
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
if (el.webkitRequestFullscreen) {
|
|
1585
|
+
await el.webkitRequestFullscreen();
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
const video = videoIn(container);
|
|
1589
|
+
if (video?.webkitEnterFullscreen) {
|
|
1590
|
+
video.webkitEnterFullscreen();
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
throw new Error("Fullscreen is not supported");
|
|
1594
|
+
}
|
|
1595
|
+
async function exitFullscreen(container) {
|
|
1596
|
+
const video = videoIn(container);
|
|
1597
|
+
if (video?.webkitDisplayingFullscreen) {
|
|
1598
|
+
video.webkitExitFullscreen?.();
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
const doc = document;
|
|
1602
|
+
if (doc.exitFullscreen) {
|
|
1603
|
+
await doc.exitFullscreen();
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
if (doc.webkitExitFullscreen) {
|
|
1607
|
+
await doc.webkitExitFullscreen();
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1567
1610
|
class ScarlettPlayer {
|
|
1568
1611
|
/**
|
|
1569
1612
|
* Create a new ScarlettPlayer.
|
|
@@ -1576,6 +1619,11 @@ class ScarlettPlayer {
|
|
|
1576
1619
|
this.seekingWhilePlaying = false;
|
|
1577
1620
|
this.seekResumeTimeout = null;
|
|
1578
1621
|
this.loadGeneration = 0;
|
|
1622
|
+
this.listenersWired = false;
|
|
1623
|
+
this.readyEmitted = false;
|
|
1624
|
+
this.fullscreenAnnounced = false;
|
|
1625
|
+
this.unwireFullscreen = null;
|
|
1626
|
+
this.initializing = null;
|
|
1579
1627
|
if (typeof options.container === "string") {
|
|
1580
1628
|
const el = document.querySelector(options.container);
|
|
1581
1629
|
if (!el || !(el instanceof HTMLElement)) {
|
|
@@ -1616,28 +1664,75 @@ class ScarlettPlayer {
|
|
|
1616
1664
|
this.eventBus.on("media:error", ({ error }) => {
|
|
1617
1665
|
this.errorHandler.record(error, { channel: "media:error" });
|
|
1618
1666
|
});
|
|
1667
|
+
this.wireFullscreenListeners();
|
|
1619
1668
|
if (options.plugins) {
|
|
1620
1669
|
for (const plugin of options.plugins) {
|
|
1621
1670
|
this.pluginManager.register(plugin);
|
|
1622
1671
|
}
|
|
1623
1672
|
}
|
|
1624
|
-
this.logger.info("ScarlettPlayer
|
|
1673
|
+
this.logger.info("ScarlettPlayer constructed", {
|
|
1625
1674
|
autoplay: options.autoplay,
|
|
1626
1675
|
plugins: options.plugins?.length ?? 0
|
|
1627
1676
|
});
|
|
1628
|
-
this.eventBus.emit("player:ready", void 0);
|
|
1629
1677
|
}
|
|
1630
1678
|
/**
|
|
1631
|
-
*
|
|
1632
|
-
*
|
|
1679
|
+
* Initialise every registered non-provider plugin and wire the player's
|
|
1680
|
+
* own lifecycle listeners. Idempotent, and safe to call re-entrantly.
|
|
1681
|
+
*
|
|
1682
|
+
* This exists because `new ScarlettPlayer(...)` followed by `load()` used
|
|
1683
|
+
* to leave the player with a provider and nothing else: the READMEs and 12
|
|
1684
|
+
* plugin `@example` blocks show exactly that shape, and every one of them
|
|
1685
|
+
* produced a dead UI (no controls, no overlay, no playlist). `load()` now
|
|
1686
|
+
* calls this first, so the trap cannot be reached.
|
|
1687
|
+
*
|
|
1688
|
+
* The `media:load-request` and `error:retry` listeners live here rather
|
|
1689
|
+
* than in the constructor because they are part of initialisation, not
|
|
1690
|
+
* construction: without them the playlist plugin cannot load a track and
|
|
1691
|
+
* the error overlay's "Try Again" button does nothing.
|
|
1692
|
+
*
|
|
1693
|
+
* Provider plugins are excluded: they are initialised lazily, per source,
|
|
1694
|
+
* by `load()` once `selectProvider()` has picked one.
|
|
1695
|
+
*
|
|
1696
|
+
* @returns Promise resolving when the pass (or the in-flight one) completes
|
|
1633
1697
|
*/
|
|
1634
|
-
|
|
1635
|
-
this.
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1698
|
+
ensureInitialized() {
|
|
1699
|
+
if (this.initializing) return this.initializing;
|
|
1700
|
+
this.initializing = this.runInitialization().finally(() => {
|
|
1701
|
+
this.initializing = null;
|
|
1702
|
+
});
|
|
1703
|
+
return this.initializing;
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* One initialisation pass. Never call directly; go through
|
|
1707
|
+
* `ensureInitialized()`, which owns the re-entrancy guard.
|
|
1708
|
+
*
|
|
1709
|
+
* @returns Promise resolving when the pass completes
|
|
1710
|
+
*/
|
|
1711
|
+
async runInitialization() {
|
|
1712
|
+
for (const id of this.pluginManager.getPluginIds()) {
|
|
1713
|
+
if (this.destroyed) return;
|
|
1714
|
+
const plugin = this.pluginManager.getPlugin(id);
|
|
1715
|
+
if (!plugin || plugin.type === "provider") continue;
|
|
1716
|
+
if (this.pluginManager.getPluginState(id) !== "registered") continue;
|
|
1717
|
+
await this.pluginManager.initPlugin(id);
|
|
1640
1718
|
}
|
|
1719
|
+
if (this.destroyed) return;
|
|
1720
|
+
this.wireLifecycleListeners();
|
|
1721
|
+
if (!this.readyEmitted) {
|
|
1722
|
+
this.readyEmitted = true;
|
|
1723
|
+
this.eventBus.emit("player:ready", void 0);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Wire the two listeners the player owns, exactly once.
|
|
1728
|
+
*
|
|
1729
|
+
* Guarded by a flag rather than by "init() runs once" because
|
|
1730
|
+
* `ensureInitialized()` runs on every `load()`: wiring them twice would
|
|
1731
|
+
* load and play each requested source twice.
|
|
1732
|
+
*/
|
|
1733
|
+
wireLifecycleListeners() {
|
|
1734
|
+
if (this.listenersWired) return;
|
|
1735
|
+
this.listenersWired = true;
|
|
1641
1736
|
this.eventBus.on("media:load-request", async ({ src, autoplay }) => {
|
|
1642
1737
|
if (this.stateManager.getValue("chromecastActive")) return;
|
|
1643
1738
|
await this.load(src);
|
|
@@ -1660,15 +1755,40 @@ class ScarlettPlayer {
|
|
|
1660
1755
|
if (this.destroyed) return;
|
|
1661
1756
|
await this.play();
|
|
1662
1757
|
});
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Initialize the player asynchronously.
|
|
1761
|
+
*
|
|
1762
|
+
* Initialises non-provider plugins, wires the lifecycle listeners and loads
|
|
1763
|
+
* `initialSrc` when one was given. Idempotent: calling it twice, or calling
|
|
1764
|
+
* it after a `load()` has already initialised the player, wires nothing a
|
|
1765
|
+
* second time and re-emits nothing.
|
|
1766
|
+
*
|
|
1767
|
+
* @returns Promise resolving when initialisation (and any initial load) is done
|
|
1768
|
+
*/
|
|
1769
|
+
async init() {
|
|
1770
|
+
this.checkDestroyed();
|
|
1771
|
+
await this.ensureInitialized();
|
|
1663
1772
|
if (this.initialSrc) {
|
|
1664
1773
|
await this.load(this.initialSrc);
|
|
1665
1774
|
}
|
|
1666
|
-
return Promise.resolve();
|
|
1667
1775
|
}
|
|
1668
1776
|
/**
|
|
1669
1777
|
* Load a media source.
|
|
1670
1778
|
*
|
|
1671
|
-
*
|
|
1779
|
+
* Initialises the player if that has not happened yet (see
|
|
1780
|
+
* `ensureInitialized()`), then selects the provider plugin for the source
|
|
1781
|
+
* and loads it. The auto-initialisation is what makes the widely copied
|
|
1782
|
+
* `new ScarlettPlayer(...)` plus `load()` shape work: before it, that shape
|
|
1783
|
+
* produced a player with a provider and no UI, no error overlay and no
|
|
1784
|
+
* working playlist.
|
|
1785
|
+
*
|
|
1786
|
+
* Resets playback state, and deliberately does NOT touch `poster`. The
|
|
1787
|
+
* poster is metadata owned by whoever set it (the consumer through
|
|
1788
|
+
* `PlayerOptions.poster` or `setPoster()`, or the playlist plugin on a track
|
|
1789
|
+
* change), not playback state, and it is written BEFORE the load that goes
|
|
1790
|
+
* with it: clearing it here would blank the image over exactly the gap it
|
|
1791
|
+
* exists to cover, while the next source loads.
|
|
1672
1792
|
*
|
|
1673
1793
|
* @param source - Media source URL
|
|
1674
1794
|
* @returns Promise that resolves when source is loaded
|
|
@@ -1700,6 +1820,7 @@ class ScarlettPlayer {
|
|
|
1700
1820
|
await this.pluginManager.destroyPlugin(previousProviderId);
|
|
1701
1821
|
this._currentProvider = null;
|
|
1702
1822
|
}
|
|
1823
|
+
await this.ensureInitialized();
|
|
1703
1824
|
if (generation !== this.loadGeneration) {
|
|
1704
1825
|
this.logger.info("Load superseded by newer load call", { source });
|
|
1705
1826
|
return;
|
|
@@ -1897,6 +2018,31 @@ class ScarlettPlayer {
|
|
|
1897
2018
|
this.stateManager.set("autoplay", autoplay);
|
|
1898
2019
|
this.logger.debug("Autoplay set", { autoplay });
|
|
1899
2020
|
}
|
|
2021
|
+
/**
|
|
2022
|
+
* Set the poster image shown until the first frame renders.
|
|
2023
|
+
*
|
|
2024
|
+
* Writes the `poster` state key; the provider plugins subscribe to it and
|
|
2025
|
+
* mirror it onto the media element, so this takes effect on a player that
|
|
2026
|
+
* is already running. Before this method existed the poster could only be
|
|
2027
|
+
* chosen at construction, which left a playlist showing the previous
|
|
2028
|
+
* track's art (and a Vue `poster` prop change doing nothing at all).
|
|
2029
|
+
*
|
|
2030
|
+
* An empty string clears the poster, which is how a consumer takes the
|
|
2031
|
+
* image away rather than replacing it.
|
|
2032
|
+
*
|
|
2033
|
+
* @param url - Poster image URL, or '' to clear it
|
|
2034
|
+
*
|
|
2035
|
+
* @example
|
|
2036
|
+
* ```ts
|
|
2037
|
+
* player.setPoster('https://example.com/art.jpg');
|
|
2038
|
+
* player.setPoster(''); // back to the bare video surface
|
|
2039
|
+
* ```
|
|
2040
|
+
*/
|
|
2041
|
+
setPoster(url) {
|
|
2042
|
+
this.checkDestroyed();
|
|
2043
|
+
this.stateManager.set("poster", url);
|
|
2044
|
+
this.logger.debug("Poster set", { poster: url });
|
|
2045
|
+
}
|
|
1900
2046
|
/**
|
|
1901
2047
|
* Subscribe to an event.
|
|
1902
2048
|
*
|
|
@@ -1927,9 +2073,13 @@ class ScarlettPlayer {
|
|
|
1927
2073
|
*
|
|
1928
2074
|
* @example
|
|
1929
2075
|
* ```ts
|
|
2076
|
+
* // player:ready fires once, at the end of the first initialisation, so a
|
|
2077
|
+
* // one-shot listener has to be attached before init() or load() runs.
|
|
2078
|
+
* const player = new ScarlettPlayer({ container });
|
|
1930
2079
|
* player.once('player:ready', () => {
|
|
1931
2080
|
* console.log('Player ready!');
|
|
1932
2081
|
* });
|
|
2082
|
+
* await player.init();
|
|
1933
2083
|
* ```
|
|
1934
2084
|
*/
|
|
1935
2085
|
once(event, handler) {
|
|
@@ -1998,7 +2148,7 @@ class ScarlettPlayer {
|
|
|
1998
2148
|
* Set quality level (-1 for auto).
|
|
1999
2149
|
* @param index - Quality level index
|
|
2000
2150
|
*/
|
|
2001
|
-
setQuality(
|
|
2151
|
+
setQuality(index2) {
|
|
2002
2152
|
this.checkDestroyed();
|
|
2003
2153
|
if (!this._currentProvider) {
|
|
2004
2154
|
this.logger.warn("No provider available for quality change");
|
|
@@ -2006,17 +2156,17 @@ class ScarlettPlayer {
|
|
|
2006
2156
|
}
|
|
2007
2157
|
const provider = this._currentProvider;
|
|
2008
2158
|
if (typeof provider.setLevel === "function") {
|
|
2009
|
-
if (
|
|
2159
|
+
if (index2 !== -1) {
|
|
2010
2160
|
const levels = this.getQualities();
|
|
2011
|
-
if (levels.length > 0 && (
|
|
2012
|
-
this.logger.warn(`Invalid quality index: ${
|
|
2161
|
+
if (levels.length > 0 && (index2 < 0 || index2 >= levels.length)) {
|
|
2162
|
+
this.logger.warn(`Invalid quality index: ${index2} (available: ${levels.length})`);
|
|
2013
2163
|
return;
|
|
2014
2164
|
}
|
|
2015
2165
|
}
|
|
2016
|
-
provider.setLevel(
|
|
2166
|
+
provider.setLevel(index2);
|
|
2017
2167
|
this.eventBus.emit("quality:change", {
|
|
2018
|
-
quality:
|
|
2019
|
-
auto:
|
|
2168
|
+
quality: index2 === -1 ? "auto" : `level-${index2}`,
|
|
2169
|
+
auto: index2 === -1
|
|
2020
2170
|
});
|
|
2021
2171
|
}
|
|
2022
2172
|
}
|
|
@@ -2033,36 +2183,83 @@ class ScarlettPlayer {
|
|
|
2033
2183
|
return -1;
|
|
2034
2184
|
}
|
|
2035
2185
|
// ===== Fullscreen Methods =====
|
|
2186
|
+
/**
|
|
2187
|
+
* Listen for fullscreen changes the player did not initiate.
|
|
2188
|
+
*
|
|
2189
|
+
* Nothing used to: the `fullscreen` state key was written only by this
|
|
2190
|
+
* class's own `requestFullscreen()` and `exitFullscreen()`. Everything else
|
|
2191
|
+
* left it lying. Entering fullscreen through the UI button or the `f`
|
|
2192
|
+
* shortcut never flipped the icon to "Exit fullscreen", `player.fullscreen`
|
|
2193
|
+
* stayed false and `fullscreen:change` never fired; and after a programmatic
|
|
2194
|
+
* `requestFullscreen()` an Escape exit left the state stuck at true.
|
|
2195
|
+
*
|
|
2196
|
+
* `webkitbeginfullscreen` and `webkitendfullscreen` are the iPhone's native
|
|
2197
|
+
* player announcing itself. They are dispatched on the video element, they do
|
|
2198
|
+
* not bubble, and the element does not exist yet when this runs (a provider
|
|
2199
|
+
* plugin creates it, per source), so they are bound to the container in the
|
|
2200
|
+
* CAPTURE phase, which is the one phase that sees a non-bubbling event on a
|
|
2201
|
+
* descendant.
|
|
2202
|
+
*/
|
|
2203
|
+
wireFullscreenListeners() {
|
|
2204
|
+
const onChange = () => {
|
|
2205
|
+
this.fullscreenAnnounced = true;
|
|
2206
|
+
this.setFullscreenState(isFullscreen(this.container));
|
|
2207
|
+
};
|
|
2208
|
+
document.addEventListener("fullscreenchange", onChange);
|
|
2209
|
+
document.addEventListener("webkitfullscreenchange", onChange);
|
|
2210
|
+
this.container.addEventListener("webkitbeginfullscreen", onChange, true);
|
|
2211
|
+
this.container.addEventListener("webkitendfullscreen", onChange, true);
|
|
2212
|
+
this.unwireFullscreen = () => {
|
|
2213
|
+
document.removeEventListener("fullscreenchange", onChange);
|
|
2214
|
+
document.removeEventListener("webkitfullscreenchange", onChange);
|
|
2215
|
+
this.container.removeEventListener("webkitbeginfullscreen", onChange, true);
|
|
2216
|
+
this.container.removeEventListener("webkitendfullscreen", onChange, true);
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* Record a fullscreen transition, once.
|
|
2221
|
+
*
|
|
2222
|
+
* @param next - The state the browser is now in
|
|
2223
|
+
*/
|
|
2224
|
+
setFullscreenState(next) {
|
|
2225
|
+
if (this.stateManager.getValue("fullscreen") === next) {
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
this.stateManager.set("fullscreen", next);
|
|
2229
|
+
this.eventBus.emit("fullscreen:change", { fullscreen: next });
|
|
2230
|
+
}
|
|
2036
2231
|
/**
|
|
2037
2232
|
* Request fullscreen mode.
|
|
2233
|
+
*
|
|
2234
|
+
* @returns Promise resolving once the browser has accepted or refused
|
|
2038
2235
|
*/
|
|
2039
2236
|
async requestFullscreen() {
|
|
2040
2237
|
this.checkDestroyed();
|
|
2238
|
+
this.fullscreenAnnounced = false;
|
|
2041
2239
|
try {
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2240
|
+
await enterFullscreen(this.container);
|
|
2241
|
+
if (!this.fullscreenAnnounced) {
|
|
2242
|
+
this.stateManager.set("fullscreen", true);
|
|
2243
|
+
this.eventBus.emit("fullscreen:change", { fullscreen: true });
|
|
2046
2244
|
}
|
|
2047
|
-
this.stateManager.set("fullscreen", true);
|
|
2048
|
-
this.eventBus.emit("fullscreen:change", { fullscreen: true });
|
|
2049
2245
|
} catch (error) {
|
|
2050
2246
|
this.logger.error("Fullscreen request failed", { error });
|
|
2051
2247
|
}
|
|
2052
2248
|
}
|
|
2053
2249
|
/**
|
|
2054
2250
|
* Exit fullscreen mode.
|
|
2251
|
+
*
|
|
2252
|
+
* @returns Promise resolving once the browser has accepted or refused
|
|
2055
2253
|
*/
|
|
2056
2254
|
async exitFullscreen() {
|
|
2057
2255
|
this.checkDestroyed();
|
|
2256
|
+
this.fullscreenAnnounced = false;
|
|
2058
2257
|
try {
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2258
|
+
await exitFullscreen(this.container);
|
|
2259
|
+
if (!this.fullscreenAnnounced) {
|
|
2260
|
+
this.stateManager.set("fullscreen", false);
|
|
2261
|
+
this.eventBus.emit("fullscreen:change", { fullscreen: false });
|
|
2063
2262
|
}
|
|
2064
|
-
this.stateManager.set("fullscreen", false);
|
|
2065
|
-
this.eventBus.emit("fullscreen:change", { fullscreen: false });
|
|
2066
2263
|
} catch (error) {
|
|
2067
2264
|
this.logger.error("Exit fullscreen failed", { error });
|
|
2068
2265
|
}
|
|
@@ -2160,6 +2357,8 @@ class ScarlettPlayer {
|
|
|
2160
2357
|
clearTimeout(this.seekResumeTimeout);
|
|
2161
2358
|
this.seekResumeTimeout = null;
|
|
2162
2359
|
}
|
|
2360
|
+
this.unwireFullscreen?.();
|
|
2361
|
+
this.unwireFullscreen = null;
|
|
2163
2362
|
this.eventBus.emit("player:destroy", void 0);
|
|
2164
2363
|
this.pluginManager.destroyAll();
|
|
2165
2364
|
this.eventBus.destroy();
|
|
@@ -2240,6 +2439,16 @@ class ScarlettPlayer {
|
|
|
2240
2439
|
get autoplay() {
|
|
2241
2440
|
return this.stateManager.getValue("autoplay");
|
|
2242
2441
|
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Get the current poster URL ('' when there is none).
|
|
2444
|
+
*
|
|
2445
|
+
* Reads state rather than the media element: the element only exists once a
|
|
2446
|
+
* provider has been initialised, and for an audio source it never carries
|
|
2447
|
+
* the attribute at all.
|
|
2448
|
+
*/
|
|
2449
|
+
get poster() {
|
|
2450
|
+
return this.stateManager.getValue("poster");
|
|
2451
|
+
}
|
|
2243
2452
|
/**
|
|
2244
2453
|
* Check if player is destroyed.
|
|
2245
2454
|
* @private
|
|
@@ -2348,8 +2557,8 @@ function formatBitrate(bitrate) {
|
|
|
2348
2557
|
return `${bitrate} bps`;
|
|
2349
2558
|
}
|
|
2350
2559
|
function mapLevels(levels, _currentLevel) {
|
|
2351
|
-
return levels.map((level,
|
|
2352
|
-
index,
|
|
2560
|
+
return levels.map((level, index2) => ({
|
|
2561
|
+
index: index2,
|
|
2353
2562
|
width: level.width || 0,
|
|
2354
2563
|
height: level.height || 0,
|
|
2355
2564
|
bitrate: level.bitrate || 0,
|
|
@@ -2404,13 +2613,13 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
2404
2613
|
};
|
|
2405
2614
|
addHandler("hlsManifestParsed", (_event, data) => {
|
|
2406
2615
|
api.logger.debug("HLS manifest parsed", { levels: data.levels.length });
|
|
2407
|
-
const levels = data.levels.map((level,
|
|
2408
|
-
id: `level-${
|
|
2616
|
+
const levels = data.levels.map((level, index2) => ({
|
|
2617
|
+
id: `level-${index2}`,
|
|
2409
2618
|
label: formatLevel(level),
|
|
2410
2619
|
width: level.width,
|
|
2411
2620
|
height: level.height,
|
|
2412
2621
|
bitrate: level.bitrate,
|
|
2413
|
-
active:
|
|
2622
|
+
active: index2 === hls.currentLevel
|
|
2414
2623
|
}));
|
|
2415
2624
|
api.setState("qualities", levels);
|
|
2416
2625
|
api.emit("quality:levels", {
|
|
@@ -2511,8 +2720,14 @@ function setupVideoEventHandlers(video, api) {
|
|
|
2511
2720
|
video.addEventListener(event, handler);
|
|
2512
2721
|
handlers.push({ event, handler });
|
|
2513
2722
|
};
|
|
2723
|
+
const syncEndedFromElement = () => {
|
|
2724
|
+
if (video.ended || !api.getState("ended")) return;
|
|
2725
|
+
api.setState("ended", false);
|
|
2726
|
+
api.setState("playbackState", video.paused ? "paused" : "playing");
|
|
2727
|
+
};
|
|
2514
2728
|
addHandler("play", () => {
|
|
2515
2729
|
api.setState("paused", false);
|
|
2730
|
+
syncEndedFromElement();
|
|
2516
2731
|
});
|
|
2517
2732
|
addHandler("playing", () => {
|
|
2518
2733
|
api.setState("playing", true);
|
|
@@ -2520,6 +2735,7 @@ function setupVideoEventHandlers(video, api) {
|
|
|
2520
2735
|
api.setState("waiting", false);
|
|
2521
2736
|
api.setState("buffering", false);
|
|
2522
2737
|
api.setState("playbackState", "playing");
|
|
2738
|
+
syncEndedFromElement();
|
|
2523
2739
|
});
|
|
2524
2740
|
addHandler("pause", () => {
|
|
2525
2741
|
api.setState("playing", false);
|
|
@@ -2574,6 +2790,7 @@ function setupVideoEventHandlers(video, api) {
|
|
|
2574
2790
|
});
|
|
2575
2791
|
addHandler("seeking", () => {
|
|
2576
2792
|
api.setState("seeking", true);
|
|
2793
|
+
syncEndedFromElement();
|
|
2577
2794
|
});
|
|
2578
2795
|
addHandler("seeked", () => {
|
|
2579
2796
|
api.setState("seeking", false);
|
|
@@ -2678,6 +2895,7 @@ function createValidatingPlaylistLoader(Hls) {
|
|
|
2678
2895
|
}
|
|
2679
2896
|
};
|
|
2680
2897
|
}
|
|
2898
|
+
var PKG_VERSION$7 = "1.8.1";
|
|
2681
2899
|
var DEFAULT_CONFIG = {
|
|
2682
2900
|
debug: false,
|
|
2683
2901
|
autoStartLoad: true,
|
|
@@ -2735,6 +2953,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2735
2953
|
let onlineListener = null;
|
|
2736
2954
|
let reconnectTriggerError = null;
|
|
2737
2955
|
let reconnectExhausted = false;
|
|
2956
|
+
const applyPoster = () => {
|
|
2957
|
+
if (!video) return;
|
|
2958
|
+
video.poster = api?.getState("poster") || "";
|
|
2959
|
+
};
|
|
2738
2960
|
const getOrCreateVideo = () => {
|
|
2739
2961
|
if (video) return video;
|
|
2740
2962
|
const existing = api?.container.querySelector("video");
|
|
@@ -2747,10 +2969,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2747
2969
|
video.preload = "metadata";
|
|
2748
2970
|
video.controls = false;
|
|
2749
2971
|
video.playsInline = true;
|
|
2750
|
-
|
|
2751
|
-
if (poster) {
|
|
2752
|
-
video.poster = poster;
|
|
2753
|
-
}
|
|
2972
|
+
applyPoster();
|
|
2754
2973
|
api?.container.appendChild(video);
|
|
2755
2974
|
return video;
|
|
2756
2975
|
};
|
|
@@ -3009,7 +3228,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3009
3228
|
if (api) {
|
|
3010
3229
|
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api);
|
|
3011
3230
|
}
|
|
3012
|
-
return new Promise((
|
|
3231
|
+
return new Promise((resolve2, reject) => {
|
|
3013
3232
|
let watchdog = null;
|
|
3014
3233
|
let settled = false;
|
|
3015
3234
|
const settle = () => {
|
|
@@ -3068,7 +3287,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3068
3287
|
};
|
|
3069
3288
|
api?.setState("source", { src, type: "application/x-mpegURL" });
|
|
3070
3289
|
api?.emit("media:loaded", { src, type: "application/x-mpegURL" });
|
|
3071
|
-
|
|
3290
|
+
resolve2();
|
|
3072
3291
|
};
|
|
3073
3292
|
const onError = () => {
|
|
3074
3293
|
if (settled) return;
|
|
@@ -3102,7 +3321,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3102
3321
|
if (api) {
|
|
3103
3322
|
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api);
|
|
3104
3323
|
}
|
|
3105
|
-
return new Promise((
|
|
3324
|
+
return new Promise((resolve2, reject) => {
|
|
3106
3325
|
if (!hls || !api) {
|
|
3107
3326
|
reject(new Error("HLS not initialized"));
|
|
3108
3327
|
return;
|
|
@@ -3137,7 +3356,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3137
3356
|
hasPlayedContent = true;
|
|
3138
3357
|
api?.setState("source", { src, type: "application/x-mpegURL" });
|
|
3139
3358
|
api?.emit("media:loaded", { src, type: "application/x-mpegURL" });
|
|
3140
|
-
|
|
3359
|
+
resolve2();
|
|
3141
3360
|
}
|
|
3142
3361
|
},
|
|
3143
3362
|
onLevelSwitched: () => {
|
|
@@ -3293,7 +3512,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3293
3512
|
const plugin = {
|
|
3294
3513
|
id: "hls-provider",
|
|
3295
3514
|
name: variant.name,
|
|
3296
|
-
version:
|
|
3515
|
+
version: PKG_VERSION$7,
|
|
3297
3516
|
type: "provider",
|
|
3298
3517
|
description: variant.description,
|
|
3299
3518
|
canPlay(src) {
|
|
@@ -3384,6 +3603,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3384
3603
|
};
|
|
3385
3604
|
window.addEventListener("online", onlineListener);
|
|
3386
3605
|
}
|
|
3606
|
+
const unsubPoster = api.subscribeToState((event) => {
|
|
3607
|
+
if (event.key === "poster") applyPoster();
|
|
3608
|
+
});
|
|
3387
3609
|
api.onDestroy(() => {
|
|
3388
3610
|
unsubPlay();
|
|
3389
3611
|
unsubPause();
|
|
@@ -3392,6 +3614,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3392
3614
|
unsubMute();
|
|
3393
3615
|
unsubRate();
|
|
3394
3616
|
unsubQuality();
|
|
3617
|
+
unsubPoster();
|
|
3395
3618
|
});
|
|
3396
3619
|
},
|
|
3397
3620
|
async destroy() {
|
|
@@ -3417,6 +3640,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3417
3640
|
hasPlayedContent = false;
|
|
3418
3641
|
cleanup(new Error("HLS load cancelled: superseded by a new load"));
|
|
3419
3642
|
currentSrc = src;
|
|
3643
|
+
applyPoster();
|
|
3420
3644
|
api.setState("playbackState", "loading");
|
|
3421
3645
|
api.setState("buffering", true);
|
|
3422
3646
|
if (api.getState("airplayActive") && loader.supportsNativeHLS()) {
|
|
@@ -3445,12 +3669,12 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3445
3669
|
if (isNative || !hls) return -1;
|
|
3446
3670
|
return hls.currentLevel;
|
|
3447
3671
|
},
|
|
3448
|
-
setLevel(
|
|
3672
|
+
setLevel(index2) {
|
|
3449
3673
|
if (isNative || !hls) {
|
|
3450
3674
|
api?.logger.warn("Quality selection not available in native HLS mode");
|
|
3451
3675
|
return;
|
|
3452
3676
|
}
|
|
3453
|
-
hls.currentLevel =
|
|
3677
|
+
hls.currentLevel = index2;
|
|
3454
3678
|
},
|
|
3455
3679
|
getLevels() {
|
|
3456
3680
|
if (isNative || !hls) return [];
|
|
@@ -3635,7 +3859,462 @@ function createHLSPlugin(config) {
|
|
|
3635
3859
|
config
|
|
3636
3860
|
);
|
|
3637
3861
|
}
|
|
3638
|
-
var
|
|
3862
|
+
var PKG_VERSION$6 = "1.8.1";
|
|
3863
|
+
var VIDEO_EXTENSIONS = ["mp4", "webm", "mov", "mkv", "ogv", "m4v"];
|
|
3864
|
+
var AUDIO_EXTENSIONS = ["mp3", "wav", "ogg", "flac", "aac", "m4a", "opus", "weba"];
|
|
3865
|
+
var SUPPORTED_EXTENSIONS = [...VIDEO_EXTENSIONS, ...AUDIO_EXTENSIONS];
|
|
3866
|
+
var MIME_TYPES = {
|
|
3867
|
+
// Video
|
|
3868
|
+
mp4: "video/mp4",
|
|
3869
|
+
m4v: "video/mp4",
|
|
3870
|
+
webm: "video/webm",
|
|
3871
|
+
mov: "video/quicktime",
|
|
3872
|
+
mkv: "video/x-matroska",
|
|
3873
|
+
ogv: "video/ogg",
|
|
3874
|
+
// Audio
|
|
3875
|
+
mp3: "audio/mpeg",
|
|
3876
|
+
wav: "audio/wav",
|
|
3877
|
+
ogg: "audio/ogg",
|
|
3878
|
+
flac: "audio/flac",
|
|
3879
|
+
aac: "audio/aac",
|
|
3880
|
+
m4a: "audio/mp4",
|
|
3881
|
+
opus: "audio/opus",
|
|
3882
|
+
weba: "audio/webm"
|
|
3883
|
+
};
|
|
3884
|
+
function createNativePlugin(config) {
|
|
3885
|
+
const preload = config?.preload ?? "metadata";
|
|
3886
|
+
const load_timeout_ms = config?.loadTimeoutMs ?? 3e4;
|
|
3887
|
+
let api = null;
|
|
3888
|
+
let video = null;
|
|
3889
|
+
let cleanupEvents = null;
|
|
3890
|
+
let derived_title = null;
|
|
3891
|
+
let is_audio_source = false;
|
|
3892
|
+
const getExtension = (src) => {
|
|
3893
|
+
try {
|
|
3894
|
+
const url = new URL(src, window.location.href);
|
|
3895
|
+
const pathname = url.pathname;
|
|
3896
|
+
const ext = pathname.split(".").pop()?.toLowerCase() ?? "";
|
|
3897
|
+
return ext;
|
|
3898
|
+
} catch {
|
|
3899
|
+
const rawExt = src.split(".").pop()?.toLowerCase() ?? "";
|
|
3900
|
+
return rawExt.split("?")[0] ?? "";
|
|
3901
|
+
}
|
|
3902
|
+
};
|
|
3903
|
+
const getMimeType = (ext) => {
|
|
3904
|
+
return MIME_TYPES[ext] || "video/mp4";
|
|
3905
|
+
};
|
|
3906
|
+
const isAudioExtension = (ext) => {
|
|
3907
|
+
return AUDIO_EXTENSIONS.includes(ext);
|
|
3908
|
+
};
|
|
3909
|
+
const canBrowserPlay = (mimeType) => {
|
|
3910
|
+
const isAudio = mimeType.startsWith("audio/");
|
|
3911
|
+
const testElement = isAudio ? document.createElement("audio") : document.createElement("video");
|
|
3912
|
+
const canPlay = testElement.canPlayType(mimeType);
|
|
3913
|
+
return canPlay === "probably" || canPlay === "maybe";
|
|
3914
|
+
};
|
|
3915
|
+
const applyPoster = () => {
|
|
3916
|
+
if (!video) return;
|
|
3917
|
+
if (is_audio_source) {
|
|
3918
|
+
video.poster = "";
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
video.poster = api?.getState("poster") || "";
|
|
3922
|
+
};
|
|
3923
|
+
const getOrCreateVideo = () => {
|
|
3924
|
+
if (video) return video;
|
|
3925
|
+
const existing = api?.container.querySelector("video");
|
|
3926
|
+
if (existing) {
|
|
3927
|
+
video = existing;
|
|
3928
|
+
return video;
|
|
3929
|
+
}
|
|
3930
|
+
video = document.createElement("video");
|
|
3931
|
+
video.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;display:block;object-fit:contain;background:#000";
|
|
3932
|
+
video.preload = preload;
|
|
3933
|
+
video.controls = false;
|
|
3934
|
+
video.playsInline = true;
|
|
3935
|
+
applyPoster();
|
|
3936
|
+
api?.container.appendChild(video);
|
|
3937
|
+
return video;
|
|
3938
|
+
};
|
|
3939
|
+
const setupEventListeners = (videoEl) => {
|
|
3940
|
+
const handlers = [];
|
|
3941
|
+
const on = (event, handler) => {
|
|
3942
|
+
videoEl.addEventListener(event, handler);
|
|
3943
|
+
handlers.push([event, handler]);
|
|
3944
|
+
};
|
|
3945
|
+
const syncEndedFromElement = () => {
|
|
3946
|
+
if (videoEl.ended || !api?.getState("ended")) return;
|
|
3947
|
+
api?.setState("ended", false);
|
|
3948
|
+
api?.setState("playbackState", videoEl.paused ? "paused" : "playing");
|
|
3949
|
+
};
|
|
3950
|
+
on("play", () => {
|
|
3951
|
+
api?.setState("paused", false);
|
|
3952
|
+
syncEndedFromElement();
|
|
3953
|
+
});
|
|
3954
|
+
on("playing", () => {
|
|
3955
|
+
api?.setState("playing", true);
|
|
3956
|
+
api?.setState("paused", false);
|
|
3957
|
+
api?.setState("playbackState", "playing");
|
|
3958
|
+
api?.emit("playback:play", void 0);
|
|
3959
|
+
syncEndedFromElement();
|
|
3960
|
+
});
|
|
3961
|
+
on("pause", () => {
|
|
3962
|
+
api?.setState("playing", false);
|
|
3963
|
+
api?.setState("paused", true);
|
|
3964
|
+
api?.setState("playbackState", "paused");
|
|
3965
|
+
api?.emit("playback:pause", void 0);
|
|
3966
|
+
});
|
|
3967
|
+
on("ended", () => {
|
|
3968
|
+
api?.setState("playing", false);
|
|
3969
|
+
api?.setState("ended", true);
|
|
3970
|
+
api?.setState("playbackState", "ended");
|
|
3971
|
+
api?.emit("playback:ended", void 0);
|
|
3972
|
+
});
|
|
3973
|
+
on("timeupdate", () => {
|
|
3974
|
+
api?.setState("currentTime", videoEl.currentTime);
|
|
3975
|
+
api?.emit("playback:timeupdate", { currentTime: videoEl.currentTime });
|
|
3976
|
+
});
|
|
3977
|
+
on("durationchange", () => {
|
|
3978
|
+
api?.setState("duration", videoEl.duration || 0);
|
|
3979
|
+
});
|
|
3980
|
+
on("loadedmetadata", () => {
|
|
3981
|
+
api?.setState("duration", videoEl.duration || 0);
|
|
3982
|
+
api?.emit("media:loadedmetadata", { duration: videoEl.duration || 0 });
|
|
3983
|
+
});
|
|
3984
|
+
on("canplay", () => {
|
|
3985
|
+
api?.setState("buffering", false);
|
|
3986
|
+
api?.emit("media:canplay", void 0);
|
|
3987
|
+
});
|
|
3988
|
+
on("canplaythrough", () => {
|
|
3989
|
+
api?.emit("media:canplaythrough", void 0);
|
|
3990
|
+
});
|
|
3991
|
+
on("waiting", () => {
|
|
3992
|
+
api?.setState("buffering", true);
|
|
3993
|
+
api?.emit("media:waiting", void 0);
|
|
3994
|
+
});
|
|
3995
|
+
on("progress", () => {
|
|
3996
|
+
if (videoEl.buffered.length > 0) {
|
|
3997
|
+
const bufferedEnd = videoEl.buffered.end(videoEl.buffered.length - 1);
|
|
3998
|
+
const duration = videoEl.duration || 0;
|
|
3999
|
+
const buffered = duration > 0 ? bufferedEnd / duration : 0;
|
|
4000
|
+
api?.setState("bufferedAmount", buffered);
|
|
4001
|
+
api?.emit("media:progress", { buffered });
|
|
4002
|
+
}
|
|
4003
|
+
});
|
|
4004
|
+
on("seeking", () => {
|
|
4005
|
+
api?.setState("seeking", true);
|
|
4006
|
+
syncEndedFromElement();
|
|
4007
|
+
});
|
|
4008
|
+
on("seeked", () => {
|
|
4009
|
+
api?.setState("seeking", false);
|
|
4010
|
+
api?.emit("playback:seeked", { time: videoEl.currentTime });
|
|
4011
|
+
});
|
|
4012
|
+
on("volumechange", () => {
|
|
4013
|
+
api?.setState("volume", videoEl.volume);
|
|
4014
|
+
api?.setState("muted", videoEl.muted);
|
|
4015
|
+
api?.emit("volume:change", { volume: videoEl.volume, muted: videoEl.muted });
|
|
4016
|
+
});
|
|
4017
|
+
on("ratechange", () => {
|
|
4018
|
+
api?.setState("playbackRate", videoEl.playbackRate);
|
|
4019
|
+
api?.emit("playback:ratechange", { rate: videoEl.playbackRate });
|
|
4020
|
+
});
|
|
4021
|
+
on("stalled", () => {
|
|
4022
|
+
api?.setState("buffering", true);
|
|
4023
|
+
api?.emit("media:stalled", void 0);
|
|
4024
|
+
api?.logger.warn("Media stalled - network may be slow");
|
|
4025
|
+
});
|
|
4026
|
+
on("suspend", () => {
|
|
4027
|
+
api?.emit("media:suspend", void 0);
|
|
4028
|
+
});
|
|
4029
|
+
on("abort", () => {
|
|
4030
|
+
api?.emit("media:abort", void 0);
|
|
4031
|
+
});
|
|
4032
|
+
on("error", () => {
|
|
4033
|
+
const error = videoEl.error;
|
|
4034
|
+
let message = "Unknown video error";
|
|
4035
|
+
if (error) {
|
|
4036
|
+
switch (error.code) {
|
|
4037
|
+
case MediaError.MEDIA_ERR_ABORTED:
|
|
4038
|
+
message = "Playback aborted";
|
|
4039
|
+
break;
|
|
4040
|
+
case MediaError.MEDIA_ERR_NETWORK:
|
|
4041
|
+
message = "Network error";
|
|
4042
|
+
break;
|
|
4043
|
+
case MediaError.MEDIA_ERR_DECODE:
|
|
4044
|
+
message = "Decode error - format may not be supported";
|
|
4045
|
+
break;
|
|
4046
|
+
case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
|
|
4047
|
+
message = "Format not supported";
|
|
4048
|
+
break;
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
api?.logger.error("Video error", { code: error?.code, message });
|
|
4052
|
+
api?.emit("error", {
|
|
4053
|
+
code: ErrorCode.PLAYBACK_FAILED,
|
|
4054
|
+
message,
|
|
4055
|
+
fatal: true,
|
|
4056
|
+
timestamp: Date.now()
|
|
4057
|
+
});
|
|
4058
|
+
});
|
|
4059
|
+
on("enterpictureinpicture", () => {
|
|
4060
|
+
api?.setState("pip", true);
|
|
4061
|
+
api?.logger.debug("PiP: entered (standard)");
|
|
4062
|
+
});
|
|
4063
|
+
on("leavepictureinpicture", () => {
|
|
4064
|
+
api?.setState("pip", false);
|
|
4065
|
+
api?.logger.debug("PiP: exited (standard)");
|
|
4066
|
+
if (!videoEl.paused || api?.getState("playing")) {
|
|
4067
|
+
videoEl.play().catch(() => {
|
|
4068
|
+
});
|
|
4069
|
+
}
|
|
4070
|
+
});
|
|
4071
|
+
const webkitVideo = videoEl;
|
|
4072
|
+
if ("webkitPresentationMode" in videoEl) {
|
|
4073
|
+
on("webkitpresentationmodechanged", () => {
|
|
4074
|
+
const mode = webkitVideo.webkitPresentationMode;
|
|
4075
|
+
api?.setState("pip", mode === "picture-in-picture");
|
|
4076
|
+
api?.logger.debug(`PiP: mode changed to ${mode} (webkit)`);
|
|
4077
|
+
if (mode === "inline" && videoEl.paused) {
|
|
4078
|
+
videoEl.play().catch(() => {
|
|
4079
|
+
});
|
|
4080
|
+
}
|
|
4081
|
+
});
|
|
4082
|
+
}
|
|
4083
|
+
return () => {
|
|
4084
|
+
handlers.forEach(([event, handler]) => {
|
|
4085
|
+
videoEl.removeEventListener(event, handler);
|
|
4086
|
+
});
|
|
4087
|
+
};
|
|
4088
|
+
};
|
|
4089
|
+
const cleanup = () => {
|
|
4090
|
+
cleanupEvents?.();
|
|
4091
|
+
cleanupEvents = null;
|
|
4092
|
+
if (video) {
|
|
4093
|
+
video.pause();
|
|
4094
|
+
video.removeAttribute("src");
|
|
4095
|
+
video.load();
|
|
4096
|
+
}
|
|
4097
|
+
};
|
|
4098
|
+
const plugin = {
|
|
4099
|
+
id: "native-provider",
|
|
4100
|
+
name: "Native Media Provider",
|
|
4101
|
+
version: PKG_VERSION$6,
|
|
4102
|
+
type: "provider",
|
|
4103
|
+
description: "Native HTML5 playback for video (MP4, WebM, MOV) and audio (MP3, WAV, FLAC, AAC)",
|
|
4104
|
+
canPlay(src) {
|
|
4105
|
+
const ext = getExtension(src);
|
|
4106
|
+
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
4107
|
+
return false;
|
|
4108
|
+
}
|
|
4109
|
+
const mimeType = getMimeType(ext);
|
|
4110
|
+
return canBrowserPlay(mimeType);
|
|
4111
|
+
},
|
|
4112
|
+
async init(pluginApi) {
|
|
4113
|
+
api = pluginApi;
|
|
4114
|
+
api.logger.info("Native video plugin initialized");
|
|
4115
|
+
const unsubPlay = api.on("playback:play", async () => {
|
|
4116
|
+
if (!video) return;
|
|
4117
|
+
try {
|
|
4118
|
+
await video.play();
|
|
4119
|
+
} catch (e) {
|
|
4120
|
+
api?.logger.error("Play failed", e);
|
|
4121
|
+
}
|
|
4122
|
+
});
|
|
4123
|
+
const unsubPause = api.on("playback:pause", () => {
|
|
4124
|
+
video?.pause();
|
|
4125
|
+
});
|
|
4126
|
+
const unsubSeek = api.on("playback:seeking", ({ time }) => {
|
|
4127
|
+
if (!video) return;
|
|
4128
|
+
const clampedTime = Math.max(0, Math.min(time, video.duration || 0));
|
|
4129
|
+
video.currentTime = clampedTime;
|
|
4130
|
+
});
|
|
4131
|
+
const unsubVolume = api.on("volume:change", ({ volume, muted }) => {
|
|
4132
|
+
if (video) {
|
|
4133
|
+
video.volume = volume;
|
|
4134
|
+
video.muted = muted;
|
|
4135
|
+
}
|
|
4136
|
+
});
|
|
4137
|
+
const unsubMute = api.on("volume:mute", ({ muted }) => {
|
|
4138
|
+
if (video) video.muted = muted;
|
|
4139
|
+
});
|
|
4140
|
+
const unsubRate = api.on("playback:ratechange", ({ rate }) => {
|
|
4141
|
+
if (video) video.playbackRate = rate;
|
|
4142
|
+
});
|
|
4143
|
+
const unsubPoster = api.subscribeToState((event) => {
|
|
4144
|
+
if (event.key === "poster") applyPoster();
|
|
4145
|
+
});
|
|
4146
|
+
api.onDestroy(() => {
|
|
4147
|
+
unsubPlay();
|
|
4148
|
+
unsubPause();
|
|
4149
|
+
unsubSeek();
|
|
4150
|
+
unsubVolume();
|
|
4151
|
+
unsubMute();
|
|
4152
|
+
unsubRate();
|
|
4153
|
+
unsubPoster();
|
|
4154
|
+
});
|
|
4155
|
+
},
|
|
4156
|
+
async destroy() {
|
|
4157
|
+
api?.logger.info("Native video plugin destroying");
|
|
4158
|
+
cleanup();
|
|
4159
|
+
if (video?.parentNode) {
|
|
4160
|
+
video.parentNode.removeChild(video);
|
|
4161
|
+
}
|
|
4162
|
+
video = null;
|
|
4163
|
+
api = null;
|
|
4164
|
+
derived_title = null;
|
|
4165
|
+
is_audio_source = false;
|
|
4166
|
+
},
|
|
4167
|
+
async loadSource(src) {
|
|
4168
|
+
if (!api) throw new Error("Plugin not initialized");
|
|
4169
|
+
const ext = getExtension(src);
|
|
4170
|
+
const mimeType = getMimeType(ext);
|
|
4171
|
+
const isAudio = isAudioExtension(ext);
|
|
4172
|
+
is_audio_source = isAudio;
|
|
4173
|
+
api.logger.info("Loading native media source", { src, mimeType, isAudio });
|
|
4174
|
+
cleanup();
|
|
4175
|
+
api.setState("playbackState", "loading");
|
|
4176
|
+
api.setState("buffering", true);
|
|
4177
|
+
api.setState("mediaType", isAudio ? "audio" : "video");
|
|
4178
|
+
if (isAudio) {
|
|
4179
|
+
const current_title = api.getState("title");
|
|
4180
|
+
if (!current_title || current_title === derived_title) {
|
|
4181
|
+
try {
|
|
4182
|
+
const url = new URL(src, window.location.href);
|
|
4183
|
+
const filename = url.pathname.split("/").pop() || "Audio";
|
|
4184
|
+
const title = decodeURIComponent(filename.replace(/\.[^.]+$/, "").replace(/[-_]/g, " "));
|
|
4185
|
+
derived_title = title;
|
|
4186
|
+
api.setState("title", title);
|
|
4187
|
+
} catch {
|
|
4188
|
+
derived_title = "Audio";
|
|
4189
|
+
api.setState("title", "Audio");
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
api.setState("qualities", []);
|
|
4194
|
+
api.setState("currentQuality", null);
|
|
4195
|
+
const videoEl = getOrCreateVideo();
|
|
4196
|
+
videoEl.style.display = isAudio ? "none" : "block";
|
|
4197
|
+
applyPoster();
|
|
4198
|
+
cleanupEvents = setupEventListeners(videoEl);
|
|
4199
|
+
return new Promise((resolve2, reject) => {
|
|
4200
|
+
let watchdog = null;
|
|
4201
|
+
const settle = () => {
|
|
4202
|
+
videoEl.removeEventListener("loadedmetadata", onLoaded);
|
|
4203
|
+
videoEl.removeEventListener("error", onError);
|
|
4204
|
+
if (watchdog !== null) {
|
|
4205
|
+
clearTimeout(watchdog);
|
|
4206
|
+
watchdog = null;
|
|
4207
|
+
}
|
|
4208
|
+
};
|
|
4209
|
+
const onLoaded = () => {
|
|
4210
|
+
settle();
|
|
4211
|
+
const muted = api?.getState("muted");
|
|
4212
|
+
const volume = api?.getState("volume");
|
|
4213
|
+
if (muted !== void 0) videoEl.muted = muted;
|
|
4214
|
+
if (volume !== void 0) videoEl.volume = volume;
|
|
4215
|
+
api?.setState("source", { src, type: mimeType });
|
|
4216
|
+
api?.setState("playbackState", "ready");
|
|
4217
|
+
api?.setState("buffering", false);
|
|
4218
|
+
api?.emit("media:loaded", { src, type: mimeType });
|
|
4219
|
+
resolve2();
|
|
4220
|
+
};
|
|
4221
|
+
const onError = () => {
|
|
4222
|
+
settle();
|
|
4223
|
+
const error = videoEl.error;
|
|
4224
|
+
reject(new Error(error?.message || "Failed to load video source"));
|
|
4225
|
+
};
|
|
4226
|
+
if (load_timeout_ms > 0) {
|
|
4227
|
+
watchdog = setTimeout(() => {
|
|
4228
|
+
settle();
|
|
4229
|
+
reject(new Error("Video took too long to load (network timeout)"));
|
|
4230
|
+
}, load_timeout_ms);
|
|
4231
|
+
}
|
|
4232
|
+
videoEl.addEventListener("loadedmetadata", onLoaded);
|
|
4233
|
+
videoEl.addEventListener("error", onError);
|
|
4234
|
+
videoEl.src = src;
|
|
4235
|
+
videoEl.load();
|
|
4236
|
+
});
|
|
4237
|
+
}
|
|
4238
|
+
};
|
|
4239
|
+
return plugin;
|
|
4240
|
+
}
|
|
4241
|
+
var UNKNOWN_RANK = 3;
|
|
4242
|
+
var DEFAULT_PRIORITY = {
|
|
4243
|
+
"bandwidth-indicator": { rank: 0, exit: "hide" },
|
|
4244
|
+
"skip-backward": { rank: 1, exit: "overflow" },
|
|
4245
|
+
"skip-forward": { rank: 1, exit: "overflow" },
|
|
4246
|
+
pip: { rank: 2, exit: "overflow" },
|
|
4247
|
+
chromecast: { rank: 4, exit: "overflow" },
|
|
4248
|
+
airplay: { rank: 4, exit: "overflow" },
|
|
4249
|
+
volume: { rank: 5, exit: "overflow" },
|
|
4250
|
+
captions: { rank: 6, exit: "overflow" },
|
|
4251
|
+
quality: { rank: 6, exit: "hide" },
|
|
4252
|
+
time: { rank: 7, exit: "hide" },
|
|
4253
|
+
play: { rank: "never", exit: "overflow" },
|
|
4254
|
+
"live-indicator": { rank: "never", exit: "overflow" },
|
|
4255
|
+
settings: { rank: "never", exit: "overflow" },
|
|
4256
|
+
fullscreen: { rank: "never", exit: "overflow" },
|
|
4257
|
+
spacer: { rank: "never", exit: "overflow" }
|
|
4258
|
+
};
|
|
4259
|
+
function resolveFitItems(layout, priority) {
|
|
4260
|
+
return layout.map((id) => {
|
|
4261
|
+
const rule = DEFAULT_PRIORITY[id];
|
|
4262
|
+
const rank = priority?.[id] ?? rule?.rank ?? UNKNOWN_RANK;
|
|
4263
|
+
return { id, rank, exit: rule?.exit ?? "overflow" };
|
|
4264
|
+
});
|
|
4265
|
+
}
|
|
4266
|
+
function assertFitLayout(layout, priority) {
|
|
4267
|
+
if (!layout.includes("quality") || layout.includes("settings")) {
|
|
4268
|
+
return;
|
|
4269
|
+
}
|
|
4270
|
+
const [quality] = resolveFitItems(["quality"], priority);
|
|
4271
|
+
if (quality.rank === "never") {
|
|
4272
|
+
return;
|
|
4273
|
+
}
|
|
4274
|
+
throw new Error(
|
|
4275
|
+
`uiPlugin: a layout with "quality" needs "settings" as well. The quality control hides when the bar does not fit, and the settings menu is where its Quality row lives. Add "settings" to controls, pin quality with priority: { quality: 'never' }, or set responsive: false.`
|
|
4276
|
+
);
|
|
4277
|
+
}
|
|
4278
|
+
function needed(widths, gap, overflowButtonWidth, trayUsed) {
|
|
4279
|
+
const content = widths.reduce((sum, width) => sum + width, 0);
|
|
4280
|
+
const gaps = gap * Math.max(0, widths.length - 1);
|
|
4281
|
+
const tray = trayUsed ? overflowButtonWidth + gap : 0;
|
|
4282
|
+
return content + gaps + tray;
|
|
4283
|
+
}
|
|
4284
|
+
function planFit(items, available, gap, overflowButtonWidth) {
|
|
4285
|
+
const ids = items.map((item) => item.id);
|
|
4286
|
+
if (available <= 0) {
|
|
4287
|
+
return { inBar: ids, overflow: [], hidden: [] };
|
|
4288
|
+
}
|
|
4289
|
+
const counted = items.filter((item) => item.visible && item.width > 0);
|
|
4290
|
+
const remaining = [...counted];
|
|
4291
|
+
const overflow = /* @__PURE__ */ new Set();
|
|
4292
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
4293
|
+
while (needed(
|
|
4294
|
+
remaining.map((item) => item.width),
|
|
4295
|
+
gap,
|
|
4296
|
+
overflowButtonWidth,
|
|
4297
|
+
overflow.size > 0
|
|
4298
|
+
) > available) {
|
|
4299
|
+
let victim = -1;
|
|
4300
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
4301
|
+
const candidate = remaining[i];
|
|
4302
|
+
if (candidate.rank === "never") continue;
|
|
4303
|
+
if (victim === -1 || candidate.rank <= remaining[victim].rank) {
|
|
4304
|
+
victim = i;
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
if (victim === -1) break;
|
|
4308
|
+
const [removed] = remaining.splice(victim, 1);
|
|
4309
|
+
(removed.exit === "hide" ? hidden : overflow).add(removed.id);
|
|
4310
|
+
}
|
|
4311
|
+
return {
|
|
4312
|
+
inBar: ids.filter((id) => !overflow.has(id) && !hidden.has(id)),
|
|
4313
|
+
overflow: ids.filter((id) => overflow.has(id)),
|
|
4314
|
+
hidden: ids.filter((id) => hidden.has(id))
|
|
4315
|
+
};
|
|
4316
|
+
}
|
|
4317
|
+
var styles$2 = `
|
|
3639
4318
|
/* ============================================
|
|
3640
4319
|
Container & Base
|
|
3641
4320
|
============================================ */
|
|
@@ -3695,7 +4374,17 @@ var styles = `
|
|
|
3695
4374
|
display: flex;
|
|
3696
4375
|
align-items: center;
|
|
3697
4376
|
padding: 0 12px 12px;
|
|
4377
|
+
/* Composed through a variable so the fullscreen rule below can add the
|
|
4378
|
+
device's own inset without restating the 12px. */
|
|
4379
|
+
padding-bottom: calc(12px + var(--sp-inset-bottom, 0px));
|
|
3698
4380
|
gap: 4px;
|
|
4381
|
+
/* Declared on the bar because the fit needs the same number the volume rules
|
|
4382
|
+
below use: the slider expands mid-interaction and the plugin reserves the
|
|
4383
|
+
room in advance (see interactionReserve() in index.ts, which reads this
|
|
4384
|
+
property off this element at init). One declaration, so the stylesheet and
|
|
4385
|
+
the arithmetic cannot drift. Both the bar and the overflow tray are inside
|
|
4386
|
+
.sp-controls, so a volume control inherits it wherever the fit put it. */
|
|
4387
|
+
--sp-volume-slider-width: 64px;
|
|
3699
4388
|
opacity: 0;
|
|
3700
4389
|
transform: translateY(4px);
|
|
3701
4390
|
transition: opacity 0.25s ease, transform 0.25s ease;
|
|
@@ -3713,12 +4402,37 @@ var styles = `
|
|
|
3713
4402
|
pointer-events: none;
|
|
3714
4403
|
}
|
|
3715
4404
|
|
|
4405
|
+
/* ============================================
|
|
4406
|
+
Safe Area (fullscreen only)
|
|
4407
|
+
|
|
4408
|
+
Scoped to :fullscreen on purpose. Applied unconditionally, the inset would
|
|
4409
|
+
push an inline player's controls up on any page whose viewport meta says
|
|
4410
|
+
viewport-fit=cover, where there is no notch or home indicator over the
|
|
4411
|
+
player at all. Both the bar and the progress wrapper are direct children of
|
|
4412
|
+
the container, which is the element that goes fullscreen.
|
|
4413
|
+
|
|
4414
|
+
The :-webkit-full-screen twin is a separate rule because an unknown
|
|
4415
|
+
pseudo-class anywhere in a selector list invalidates the whole rule.
|
|
4416
|
+
|
|
4417
|
+
Nothing is needed in packages/embed/iframe.html: viewport-fit has no effect
|
|
4418
|
+
inside an iframe.
|
|
4419
|
+
============================================ */
|
|
4420
|
+
:fullscreen > .sp-controls,
|
|
4421
|
+
:fullscreen > .sp-progress-wrapper {
|
|
4422
|
+
--sp-inset-bottom: env(safe-area-inset-bottom, 0px);
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
:-webkit-full-screen > .sp-controls,
|
|
4426
|
+
:-webkit-full-screen > .sp-progress-wrapper {
|
|
4427
|
+
--sp-inset-bottom: env(safe-area-inset-bottom, 0px);
|
|
4428
|
+
}
|
|
4429
|
+
|
|
3716
4430
|
/* ============================================
|
|
3717
4431
|
Progress Bar (Above Controls)
|
|
3718
4432
|
============================================ */
|
|
3719
4433
|
.sp-progress-wrapper {
|
|
3720
4434
|
position: absolute;
|
|
3721
|
-
bottom: 48px;
|
|
4435
|
+
bottom: calc(48px + var(--sp-inset-bottom, 0px));
|
|
3722
4436
|
left: 12px;
|
|
3723
4437
|
right: 12px;
|
|
3724
4438
|
height: 20px;
|
|
@@ -3734,6 +4448,31 @@ var styles = `
|
|
|
3734
4448
|
opacity: 1;
|
|
3735
4449
|
}
|
|
3736
4450
|
|
|
4451
|
+
/* Touch: a 20px wrapper is not a 20px target. The control bar is a later
|
|
4452
|
+
sibling at the same z-index and spans 0..56px from the bottom, so it wins
|
|
4453
|
+
hit-testing in the 48..56 overlap and the exclusive region for scrubbing is
|
|
4454
|
+
12px. The wrapper grows UPWARD to 44px (48..92) because growing downward
|
|
4455
|
+
would be swallowed by the bar; the 3px bar itself stays exactly where it was
|
|
4456
|
+
(centred 8.5px above the wrapper's bottom edge, which is what
|
|
4457
|
+
align-items: center gave it inside 20px). The handle and tooltip
|
|
4458
|
+
enlargements are gated behind (hover: hover) and never match a finger, but
|
|
4459
|
+
.sp-progress--dragging is not, so the handle still appears mid-drag.
|
|
4460
|
+
|
|
4461
|
+
any-pointer, not pointer: (pointer: coarse) describes the PRIMARY pointer
|
|
4462
|
+
only, so a hybrid laptop with a mouse and a touchscreen reports fine and kept
|
|
4463
|
+
the 12px exclusive region under a finger. (any-pointer: coarse) is true
|
|
4464
|
+
whenever a coarse pointer is available at all, which is the population that
|
|
4465
|
+
needs the target. The cost on such a machine is 24px of extra hit area for
|
|
4466
|
+
the mouse, over the player's own bottom edge. */
|
|
4467
|
+
@media (any-pointer: coarse) {
|
|
4468
|
+
.sp-progress-wrapper {
|
|
4469
|
+
height: 44px;
|
|
4470
|
+
align-items: flex-end;
|
|
4471
|
+
padding-bottom: 8.5px;
|
|
4472
|
+
box-sizing: border-box;
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
|
|
3737
4476
|
.sp-progress {
|
|
3738
4477
|
position: relative;
|
|
3739
4478
|
width: 100%;
|
|
@@ -3942,18 +4681,73 @@ var styles = `
|
|
|
3942
4681
|
}
|
|
3943
4682
|
|
|
3944
4683
|
/* ============================================
|
|
3945
|
-
|
|
3946
|
-
============================================ */
|
|
3947
|
-
.sp-time {
|
|
3948
|
-
font-size: 13px;
|
|
3949
|
-
font-variant-numeric: tabular-nums;
|
|
3950
|
-
color: rgba(255, 255, 255, 0.9);
|
|
3951
|
-
white-space: nowrap;
|
|
3952
|
-
padding: 0 4px;
|
|
3953
|
-
letter-spacing: 0.02em;
|
|
3954
|
-
}
|
|
4684
|
+
Overflow Tray
|
|
3955
4685
|
|
|
3956
|
-
|
|
4686
|
+
The wrapper is deliberately unpositioned: the strip is absolutely
|
|
4687
|
+
positioned against .sp-controls (the nearest positioned ancestor), so it
|
|
4688
|
+
spans the bar's width and sits directly above it instead of hanging off a
|
|
4689
|
+
44px button.
|
|
4690
|
+
|
|
4691
|
+
The strip wraps horizontally and keeps overflow visible. A vertical menu of
|
|
4692
|
+
44px rows would be taller than a portrait phone player (211px at 375px wide,
|
|
4693
|
+
measured 2026-09-05) and a scrolling one would clip the popovers registered
|
|
4694
|
+
controls own.
|
|
4695
|
+
============================================ */
|
|
4696
|
+
.sp-overflow {
|
|
4697
|
+
display: flex;
|
|
4698
|
+
align-items: center;
|
|
4699
|
+
flex-shrink: 0;
|
|
4700
|
+
}
|
|
4701
|
+
|
|
4702
|
+
.sp-overflow-tray {
|
|
4703
|
+
position: absolute;
|
|
4704
|
+
bottom: 100%;
|
|
4705
|
+
left: 0;
|
|
4706
|
+
right: 0;
|
|
4707
|
+
display: flex;
|
|
4708
|
+
flex-wrap: wrap;
|
|
4709
|
+
justify-content: flex-end;
|
|
4710
|
+
gap: 4px;
|
|
4711
|
+
padding: 8px 12px;
|
|
4712
|
+
background: rgba(20, 20, 20, 0.95);
|
|
4713
|
+
backdrop-filter: blur(8px);
|
|
4714
|
+
-webkit-backdrop-filter: blur(8px);
|
|
4715
|
+
border-radius: 8px;
|
|
4716
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
|
|
4717
|
+
overflow: visible;
|
|
4718
|
+
opacity: 0;
|
|
4719
|
+
visibility: hidden;
|
|
4720
|
+
transform: translateY(8px);
|
|
4721
|
+
transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
|
|
4722
|
+
z-index: 20;
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4725
|
+
.sp-overflow-tray--open {
|
|
4726
|
+
opacity: 1;
|
|
4727
|
+
visibility: visible;
|
|
4728
|
+
transform: translateY(0);
|
|
4729
|
+
}
|
|
4730
|
+
|
|
4731
|
+
/* Beats a control's own inline style.display = '' on its next update(), so a
|
|
4732
|
+
control the fit took off screen stays off screen until the fit says
|
|
4733
|
+
otherwise. */
|
|
4734
|
+
.sp-control--collapsed {
|
|
4735
|
+
display: none !important;
|
|
4736
|
+
}
|
|
4737
|
+
|
|
4738
|
+
/* ============================================
|
|
4739
|
+
Time Display
|
|
4740
|
+
============================================ */
|
|
4741
|
+
.sp-time {
|
|
4742
|
+
font-size: 13px;
|
|
4743
|
+
font-variant-numeric: tabular-nums;
|
|
4744
|
+
color: rgba(255, 255, 255, 0.9);
|
|
4745
|
+
white-space: nowrap;
|
|
4746
|
+
padding: 0 4px;
|
|
4747
|
+
letter-spacing: 0.02em;
|
|
4748
|
+
}
|
|
4749
|
+
|
|
4750
|
+
/* ============================================
|
|
3957
4751
|
Volume Control
|
|
3958
4752
|
============================================ */
|
|
3959
4753
|
.sp-volume {
|
|
@@ -3968,14 +4762,18 @@ var styles = `
|
|
|
3968
4762
|
transition: width 0.2s ease;
|
|
3969
4763
|
}
|
|
3970
4764
|
|
|
4765
|
+
/* Both widths come from --sp-volume-slider-width on .sp-controls, which is also
|
|
4766
|
+
what the fit reserves for this control. focus-within is deliberately not
|
|
4767
|
+
gated on hover: a tap on the mute button focuses it, which is how the slider
|
|
4768
|
+
opens on a phone. */
|
|
3971
4769
|
@media (hover: hover) {
|
|
3972
4770
|
.sp-volume:hover .sp-volume__slider-wrap {
|
|
3973
|
-
width:
|
|
4771
|
+
width: var(--sp-volume-slider-width);
|
|
3974
4772
|
}
|
|
3975
4773
|
}
|
|
3976
4774
|
|
|
3977
4775
|
.sp-volume:focus-within .sp-volume__slider-wrap {
|
|
3978
|
-
width:
|
|
4776
|
+
width: var(--sp-volume-slider-width);
|
|
3979
4777
|
}
|
|
3980
4778
|
|
|
3981
4779
|
.sp-volume__slider {
|
|
@@ -4076,6 +4874,14 @@ var styles = `
|
|
|
4076
4874
|
position: absolute;
|
|
4077
4875
|
bottom: calc(100% + 8px);
|
|
4078
4876
|
right: 0;
|
|
4877
|
+
/* Bounded to the player, see .sp-settings-panel. border-box because the
|
|
4878
|
+
bound is a content-box height by default and this menu adds 8px of padding
|
|
4879
|
+
top and bottom: at the 139px bound a 211px player gives, it rendered 155px
|
|
4880
|
+
and the host clipped the last 16px of it. */
|
|
4881
|
+
box-sizing: border-box;
|
|
4882
|
+
max-height: var(--sp-menu-max-height, none);
|
|
4883
|
+
overflow-y: auto;
|
|
4884
|
+
-webkit-overflow-scrolling: touch;
|
|
4079
4885
|
background: rgba(20, 20, 20, 0.95);
|
|
4080
4886
|
backdrop-filter: blur(8px);
|
|
4081
4887
|
-webkit-backdrop-filter: blur(8px);
|
|
@@ -4144,6 +4950,29 @@ var styles = `
|
|
|
4144
4950
|
position: absolute;
|
|
4145
4951
|
bottom: calc(100% + 8px);
|
|
4146
4952
|
right: 0;
|
|
4953
|
+
/* Bounded to the room above the control bar, written by the UI plugin's
|
|
4954
|
+
ResizeObserver as max(120px, container height - the bar's measured height
|
|
4955
|
+
- 16px). The bar is measured rather than assumed because its
|
|
4956
|
+
padding-bottom carries the safe-area inset in fullscreen, which moves the
|
|
4957
|
+
anchor these menus hang from. The Speed sub-panel is 253px (a
|
|
4958
|
+
37px header plus six 36px rows) against a 211px portrait phone player, so
|
|
4959
|
+
without this the host's overflow: hidden cuts off the Back header and the
|
|
4960
|
+
first three speeds and playback speed is unreachable (measured at 375x211
|
|
4961
|
+
on 2026-09-05). With the variable unset the panel behaves exactly as it
|
|
4962
|
+
did before.
|
|
4963
|
+
|
|
4964
|
+
Not applied to .sp-overflow-tray: that one has to keep overflow visible so
|
|
4965
|
+
the popovers its adopted controls own are not clipped.
|
|
4966
|
+
|
|
4967
|
+
border-box because max-height bounds the content box: .sp-settings-panel--main
|
|
4968
|
+
is this same element with 4px of padding top and bottom, so wherever the
|
|
4969
|
+
bound binds the main menu, it rendered 8px past it and the host clipped the
|
|
4970
|
+
difference. The --sub views set padding: 0 and were already exact, which is
|
|
4971
|
+
why the browser harness's speed-panel check could not see this. */
|
|
4972
|
+
box-sizing: border-box;
|
|
4973
|
+
max-height: var(--sp-menu-max-height, none);
|
|
4974
|
+
overflow-y: auto;
|
|
4975
|
+
-webkit-overflow-scrolling: touch;
|
|
4147
4976
|
background: rgba(20, 20, 20, 0.95);
|
|
4148
4977
|
backdrop-filter: blur(8px);
|
|
4149
4978
|
-webkit-backdrop-filter: blur(8px);
|
|
@@ -4155,7 +4984,6 @@ var styles = `
|
|
|
4155
4984
|
transform: translateY(8px);
|
|
4156
4985
|
transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
|
|
4157
4986
|
z-index: 20;
|
|
4158
|
-
overflow: hidden;
|
|
4159
4987
|
}
|
|
4160
4988
|
|
|
4161
4989
|
.sp-settings-panel--open {
|
|
@@ -4302,6 +5130,70 @@ var styles = `
|
|
|
4302
5130
|
opacity: 0.4;
|
|
4303
5131
|
}
|
|
4304
5132
|
|
|
5133
|
+
/* ============================================
|
|
5134
|
+
Big Play Button
|
|
5135
|
+
|
|
5136
|
+
z-index 12 puts it above the gradient (5) and above the gestures plugin's
|
|
5137
|
+
tap surface (6), so a tap lands on the button and starts playback instead
|
|
5138
|
+
of being read as a tap-to-toggle-controls gesture - exactly how the control
|
|
5139
|
+
bar's play button (10) already behaves. It stays below the spinner (15) and
|
|
5140
|
+
the error overlay (25), both of which own the middle of the picture when
|
|
5141
|
+
they are up.
|
|
5142
|
+
|
|
5143
|
+
Hidden with visibility, not opacity alone, so it takes no pointer events
|
|
5144
|
+
while it is away.
|
|
5145
|
+
============================================ */
|
|
5146
|
+
.sp-big-play {
|
|
5147
|
+
position: absolute;
|
|
5148
|
+
top: 50%;
|
|
5149
|
+
left: 50%;
|
|
5150
|
+
transform: translate(-50%, -50%);
|
|
5151
|
+
z-index: 12;
|
|
5152
|
+
display: flex;
|
|
5153
|
+
align-items: center;
|
|
5154
|
+
justify-content: center;
|
|
5155
|
+
/* Comfortably past the 44px minimum touch target the control bar uses. */
|
|
5156
|
+
width: 72px;
|
|
5157
|
+
height: 72px;
|
|
5158
|
+
padding: 0;
|
|
5159
|
+
border: none;
|
|
5160
|
+
border-radius: 50%;
|
|
5161
|
+
background: var(--sp-accent, #e50914);
|
|
5162
|
+
color: #fff;
|
|
5163
|
+
cursor: pointer;
|
|
5164
|
+
opacity: 0;
|
|
5165
|
+
visibility: hidden;
|
|
5166
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
|
|
5167
|
+
transition: opacity 0.2s ease, visibility 0.2s, transform 0.15s ease,
|
|
5168
|
+
background 0.15s ease;
|
|
5169
|
+
}
|
|
5170
|
+
|
|
5171
|
+
.sp-big-play--visible {
|
|
5172
|
+
opacity: 1;
|
|
5173
|
+
visibility: visible;
|
|
5174
|
+
}
|
|
5175
|
+
|
|
5176
|
+
.sp-big-play svg {
|
|
5177
|
+
width: 36px;
|
|
5178
|
+
height: 36px;
|
|
5179
|
+
fill: currentColor;
|
|
5180
|
+
/* Optical centring: the play triangle's mass sits left of the glyph box. */
|
|
5181
|
+
margin-left: 3px;
|
|
5182
|
+
}
|
|
5183
|
+
|
|
5184
|
+
.sp-big-play:hover {
|
|
5185
|
+
transform: translate(-50%, -50%) scale(1.06);
|
|
5186
|
+
}
|
|
5187
|
+
|
|
5188
|
+
.sp-big-play:active {
|
|
5189
|
+
transform: translate(-50%, -50%) scale(0.96);
|
|
5190
|
+
}
|
|
5191
|
+
|
|
5192
|
+
.sp-big-play:focus-visible {
|
|
5193
|
+
outline: 2px solid #fff;
|
|
5194
|
+
outline-offset: 3px;
|
|
5195
|
+
}
|
|
5196
|
+
|
|
4305
5197
|
/* ============================================
|
|
4306
5198
|
Error Overlay
|
|
4307
5199
|
============================================ */
|
|
@@ -4476,17 +5368,24 @@ var styles = `
|
|
|
4476
5368
|
.sp-control,
|
|
4477
5369
|
.sp-volume__slider-wrap,
|
|
4478
5370
|
.sp-quality-menu,
|
|
5371
|
+
.sp-overflow-tray,
|
|
4479
5372
|
.sp-settings-panel,
|
|
4480
5373
|
.sp-settings-panel__row,
|
|
4481
5374
|
.sp-settings-panel__item,
|
|
4482
5375
|
.sp-settings-panel__header,
|
|
4483
5376
|
.sp-buffering,
|
|
5377
|
+
.sp-big-play,
|
|
4484
5378
|
.sp-error-overlay,
|
|
4485
5379
|
.sp-error-overlay__retry,
|
|
4486
5380
|
.sp-error-overlay__dismiss {
|
|
4487
5381
|
transition: none;
|
|
4488
5382
|
}
|
|
4489
5383
|
|
|
5384
|
+
.sp-big-play:hover,
|
|
5385
|
+
.sp-big-play:active {
|
|
5386
|
+
transform: translate(-50%, -50%);
|
|
5387
|
+
}
|
|
5388
|
+
|
|
4490
5389
|
.sp-live__dot,
|
|
4491
5390
|
.sp-spin {
|
|
4492
5391
|
animation: none;
|
|
@@ -4504,7 +5403,7 @@ var styles = `
|
|
|
4504
5403
|
--sp-icon-size: 24px;
|
|
4505
5404
|
}
|
|
4506
5405
|
`;
|
|
4507
|
-
var icons = {
|
|
5406
|
+
var icons$1 = {
|
|
4508
5407
|
play: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`,
|
|
4509
5408
|
pause: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/></svg>`,
|
|
4510
5409
|
replay: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg>`,
|
|
@@ -4523,6 +5422,8 @@ var icons = {
|
|
|
4523
5422
|
captionsOff: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.5 5.5v13h-15v-13h15zM19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2z"/></svg>`,
|
|
4524
5423
|
checkmark: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>`,
|
|
4525
5424
|
chevronUp: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"/></svg>`,
|
|
5425
|
+
/** Vertical ellipsis for the overflow tray button. */
|
|
5426
|
+
more: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>`,
|
|
4526
5427
|
chevronDown: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/></svg>`,
|
|
4527
5428
|
spinner: `<svg viewBox="0 0 24 24" fill="currentColor" class="sp-spin"><path d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z"/></svg>`,
|
|
4528
5429
|
forward10: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18 13c0 3.31-2.69 6-6 6s-6-2.69-6-6 2.69-6 6-6v4l5-5-5-5v4c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8h-2z"/><path d="M10.9 16V11.73l-.72.36-.48-.86 1.48-.73h.85V16h-1.13zm2.77-2.14c0-.66.13-1.2.38-1.6.26-.41.66-.62 1.2-.62.55 0 .95.21 1.21.62.25.4.38.94.38 1.6 0 .67-.13 1.2-.38 1.61-.26.41-.66.61-1.21.61-.54 0-.94-.2-1.2-.61-.25-.41-.38-.94-.38-1.61zm1.12 0c0 .45.05.79.15 1.03.1.23.26.35.48.35s.38-.12.49-.35c.1-.24.15-.58.15-1.03s-.05-.78-.15-1.02c-.11-.23-.27-.35-.49-.35s-.38.12-.48.35c-.1.24-.15.57-.15 1.02z"/></svg>`,
|
|
@@ -4595,7 +5496,7 @@ var PlayButton = class {
|
|
|
4595
5496
|
this.toggle();
|
|
4596
5497
|
};
|
|
4597
5498
|
this.api = api;
|
|
4598
|
-
this.el = createButton("sp-play", "Play", icons.play);
|
|
5499
|
+
this.el = createButton("sp-play", "Play", icons$1.play);
|
|
4599
5500
|
this.el.addEventListener("click", this.clickHandler);
|
|
4600
5501
|
}
|
|
4601
5502
|
render() {
|
|
@@ -4607,13 +5508,13 @@ var PlayButton = class {
|
|
|
4607
5508
|
let icon;
|
|
4608
5509
|
let label;
|
|
4609
5510
|
if (ended) {
|
|
4610
|
-
icon = icons.replay;
|
|
5511
|
+
icon = icons$1.replay;
|
|
4611
5512
|
label = "Replay";
|
|
4612
5513
|
} else if (playing) {
|
|
4613
|
-
icon = icons.pause;
|
|
5514
|
+
icon = icons$1.pause;
|
|
4614
5515
|
label = "Pause";
|
|
4615
5516
|
} else {
|
|
4616
|
-
icon = icons.play;
|
|
5517
|
+
icon = icons$1.play;
|
|
4617
5518
|
label = "Play";
|
|
4618
5519
|
}
|
|
4619
5520
|
setHTML(this.el, icon);
|
|
@@ -4639,6 +5540,102 @@ var PlayButton = class {
|
|
|
4639
5540
|
this.el.remove();
|
|
4640
5541
|
}
|
|
4641
5542
|
};
|
|
5543
|
+
var BigPlayButton = class {
|
|
5544
|
+
/**
|
|
5545
|
+
* @param api - Plugin API for state and container access
|
|
5546
|
+
* @param isOverlayVisible - Whether the error overlay is showing; the button
|
|
5547
|
+
* must not sit on top of it, and `error` state alone does not say (a
|
|
5548
|
+
* dismissed overlay leaves the error behind)
|
|
5549
|
+
*/
|
|
5550
|
+
constructor(api, isOverlayVisible = () => false) {
|
|
5551
|
+
this.hasStarted = false;
|
|
5552
|
+
this.clickHandler = () => {
|
|
5553
|
+
this.start();
|
|
5554
|
+
};
|
|
5555
|
+
this.api = api;
|
|
5556
|
+
this.isOverlayVisible = isOverlayVisible;
|
|
5557
|
+
const btn = document.createElement("button");
|
|
5558
|
+
btn.className = "sp-big-play";
|
|
5559
|
+
btn.setAttribute("type", "button");
|
|
5560
|
+
btn.setAttribute("aria-label", "Play");
|
|
5561
|
+
setHTML(btn, icons$1.play);
|
|
5562
|
+
btn.addEventListener("click", this.clickHandler);
|
|
5563
|
+
this.el = btn;
|
|
5564
|
+
}
|
|
5565
|
+
render() {
|
|
5566
|
+
return this.el;
|
|
5567
|
+
}
|
|
5568
|
+
/**
|
|
5569
|
+
* Show or hide the button, and swap in the replay glyph after `ended`.
|
|
5570
|
+
*
|
|
5571
|
+
* Driven by the same `scheduleUpdate()` pass as every other control, so the
|
|
5572
|
+
* button cannot disagree with the control bar about what state playback is
|
|
5573
|
+
* in.
|
|
5574
|
+
*/
|
|
5575
|
+
update() {
|
|
5576
|
+
const playing = this.api.getState("playing");
|
|
5577
|
+
const ended = this.hasEnded();
|
|
5578
|
+
const currentTime = this.api.getState("currentTime");
|
|
5579
|
+
const playbackState = this.api.getState("playbackState");
|
|
5580
|
+
const error = this.api.getState("error");
|
|
5581
|
+
if (playing) {
|
|
5582
|
+
this.hasStarted = true;
|
|
5583
|
+
}
|
|
5584
|
+
let visible;
|
|
5585
|
+
if (error || this.isOverlayVisible()) {
|
|
5586
|
+
visible = false;
|
|
5587
|
+
} else if (playbackState === "loading") {
|
|
5588
|
+
visible = false;
|
|
5589
|
+
} else if (playing) {
|
|
5590
|
+
visible = false;
|
|
5591
|
+
} else if (ended) {
|
|
5592
|
+
visible = true;
|
|
5593
|
+
} else if (this.hasStarted || currentTime !== 0) {
|
|
5594
|
+
visible = false;
|
|
5595
|
+
} else {
|
|
5596
|
+
visible = playbackState === "idle" || playbackState === "ready";
|
|
5597
|
+
}
|
|
5598
|
+
setHTML(this.el, ended ? icons$1.replay : icons$1.play);
|
|
5599
|
+
setAttr(this.el, "aria-label", ended ? "Replay" : "Play");
|
|
5600
|
+
this.el.classList.toggle("sp-big-play--visible", visible);
|
|
5601
|
+
}
|
|
5602
|
+
/**
|
|
5603
|
+
* Whether playback has actually ended, asked of the media element.
|
|
5604
|
+
*
|
|
5605
|
+
* NOT the `ended` state key. Measured in Chrome on 2026-09-02: neither
|
|
5606
|
+
* provider clears that key on a replay (only `load()` does), so after a
|
|
5607
|
+
* viewer replays a video it stays true for the rest of the session, while
|
|
5608
|
+
* `video.ended` correctly goes false the moment the position leaves the
|
|
5609
|
+
* end. Trusting the key would leave this button sitting over playing video,
|
|
5610
|
+
* and would make a later pause bring it back as Replay. The key is the
|
|
5611
|
+
* fallback for the window before a provider has created an element.
|
|
5612
|
+
*/
|
|
5613
|
+
hasEnded() {
|
|
5614
|
+
const video = getVideo(this.api.container);
|
|
5615
|
+
return video ? video.ended : Boolean(this.api.getState("ended"));
|
|
5616
|
+
}
|
|
5617
|
+
/**
|
|
5618
|
+
* Start (or restart) playback.
|
|
5619
|
+
*
|
|
5620
|
+
* The same two branches as the control bar's play button: restart from zero
|
|
5621
|
+
* after the video ended, otherwise just play. There is no pause branch,
|
|
5622
|
+
* because this button is never on screen while playback is running. It reads
|
|
5623
|
+
* `video.ended` for the same reason `hasEnded()` does.
|
|
5624
|
+
*/
|
|
5625
|
+
start() {
|
|
5626
|
+
const video = getVideo(this.api.container);
|
|
5627
|
+
if (!video) return;
|
|
5628
|
+
if (video.ended) {
|
|
5629
|
+
video.currentTime = 0;
|
|
5630
|
+
}
|
|
5631
|
+
video.play().catch(() => {
|
|
5632
|
+
});
|
|
5633
|
+
}
|
|
5634
|
+
destroy() {
|
|
5635
|
+
this.el.removeEventListener("click", this.clickHandler);
|
|
5636
|
+
this.el.remove();
|
|
5637
|
+
}
|
|
5638
|
+
};
|
|
4642
5639
|
var ThumbnailPreview = class {
|
|
4643
5640
|
constructor() {
|
|
4644
5641
|
this.config = null;
|
|
@@ -4680,9 +5677,9 @@ var ThumbnailPreview = class {
|
|
|
4680
5677
|
return;
|
|
4681
5678
|
}
|
|
4682
5679
|
const { src, width, height, columns, interval } = this.config;
|
|
4683
|
-
const
|
|
4684
|
-
const col =
|
|
4685
|
-
const row = Math.floor(
|
|
5680
|
+
const index2 = Math.floor(time / interval);
|
|
5681
|
+
const col = index2 % columns;
|
|
5682
|
+
const row = Math.floor(index2 / columns);
|
|
4686
5683
|
this.img.style.backgroundImage = `url(${src})`;
|
|
4687
5684
|
this.img.style.backgroundPosition = `-${col * width}px -${row * height}px`;
|
|
4688
5685
|
this.img.style.backgroundSize = `${columns * width}px auto`;
|
|
@@ -5146,7 +6143,7 @@ var VolumeControl = class {
|
|
|
5146
6143
|
"aria-label": "Mute",
|
|
5147
6144
|
type: "button"
|
|
5148
6145
|
});
|
|
5149
|
-
this.btn.innerHTML = icons.volumeHigh;
|
|
6146
|
+
this.btn.innerHTML = icons$1.volumeHigh;
|
|
5150
6147
|
this.btn.onclick = () => this.toggleMute();
|
|
5151
6148
|
const sliderWrap = createElement("div", { className: "sp-volume__slider-wrap" });
|
|
5152
6149
|
this.slider = createElement("div", { className: "sp-volume__slider" });
|
|
@@ -5178,13 +6175,13 @@ var VolumeControl = class {
|
|
|
5178
6175
|
let icon;
|
|
5179
6176
|
let label;
|
|
5180
6177
|
if (muted || volume === 0) {
|
|
5181
|
-
icon = icons.volumeMute;
|
|
6178
|
+
icon = icons$1.volumeMute;
|
|
5182
6179
|
label = "Unmute";
|
|
5183
6180
|
} else if (volume < 0.5) {
|
|
5184
|
-
icon = icons.volumeLow;
|
|
6181
|
+
icon = icons$1.volumeLow;
|
|
5185
6182
|
label = "Mute";
|
|
5186
6183
|
} else {
|
|
5187
|
-
icon = icons.volumeHigh;
|
|
6184
|
+
icon = icons$1.volumeHigh;
|
|
5188
6185
|
label = "Mute";
|
|
5189
6186
|
}
|
|
5190
6187
|
setHTML(this.btn, icon);
|
|
@@ -5291,7 +6288,7 @@ var QualityMenu = class {
|
|
|
5291
6288
|
this.lastQualitiesJson = "";
|
|
5292
6289
|
this.api = api;
|
|
5293
6290
|
this.el = createElement("div", { className: "sp-quality" });
|
|
5294
|
-
this.btn = createButton("sp-quality__btn", "Quality", icons.settings);
|
|
6291
|
+
this.btn = createButton("sp-quality__btn", "Quality", icons$1.settings);
|
|
5295
6292
|
this.btnLabel = createElement("span", { className: "sp-quality__label" });
|
|
5296
6293
|
this.btnLabel.textContent = "Auto";
|
|
5297
6294
|
this.btn.appendChild(this.btnLabel);
|
|
@@ -5403,7 +6400,7 @@ var CastButton = class {
|
|
|
5403
6400
|
this.api = api;
|
|
5404
6401
|
this.type = type;
|
|
5405
6402
|
this.supported = type === "chromecast" ? isChromecastSupported() : isAirPlaySupported();
|
|
5406
|
-
const icon = type === "chromecast" ? icons.chromecast : icons.airplay;
|
|
6403
|
+
const icon = type === "chromecast" ? icons$1.chromecast : icons$1.airplay;
|
|
5407
6404
|
const label = type === "chromecast" ? "Cast" : "AirPlay";
|
|
5408
6405
|
this.el = createButton(`sp-cast sp-cast--${type}`, label, icon);
|
|
5409
6406
|
this.el.addEventListener("click", () => this.handleClick());
|
|
@@ -5427,10 +6424,10 @@ var CastButton = class {
|
|
|
5427
6424
|
this.el.classList.toggle("sp-cast--active", !!active);
|
|
5428
6425
|
this.el.classList.toggle("sp-cast--unavailable", !available && !active);
|
|
5429
6426
|
if (active) {
|
|
5430
|
-
setHTML(this.el, icons.chromecastConnected);
|
|
6427
|
+
setHTML(this.el, icons$1.chromecastConnected);
|
|
5431
6428
|
setAttr(this.el, "aria-label", "Stop casting");
|
|
5432
6429
|
} else {
|
|
5433
|
-
setHTML(this.el, icons.chromecast);
|
|
6430
|
+
setHTML(this.el, icons$1.chromecast);
|
|
5434
6431
|
setAttr(this.el, "aria-label", available ? "Cast" : "No Cast devices found");
|
|
5435
6432
|
}
|
|
5436
6433
|
} else {
|
|
@@ -5481,7 +6478,7 @@ var PipButton = class {
|
|
|
5481
6478
|
this.api = api;
|
|
5482
6479
|
const probe = document.createElement("video");
|
|
5483
6480
|
this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in probe;
|
|
5484
|
-
this.el = createButton("sp-pip", "Picture-in-Picture", icons.pip);
|
|
6481
|
+
this.el = createButton("sp-pip", "Picture-in-Picture", icons$1.pip);
|
|
5485
6482
|
this.el.addEventListener("click", this.clickHandler);
|
|
5486
6483
|
if (!this.supported) {
|
|
5487
6484
|
this.el.style.display = "none";
|
|
@@ -5504,7 +6501,7 @@ var PipButton = class {
|
|
|
5504
6501
|
const enabled = pip || this.isMediaReady();
|
|
5505
6502
|
this.el.disabled = !enabled;
|
|
5506
6503
|
setAttr(this.el, "aria-disabled", String(!enabled));
|
|
5507
|
-
setHTML(this.el, pip ? icons.exitPip : icons.pip);
|
|
6504
|
+
setHTML(this.el, pip ? icons$1.exitPip : icons$1.pip);
|
|
5508
6505
|
setAttr(this.el, "aria-label", pip ? "Exit Picture-in-Picture" : "Picture-in-Picture");
|
|
5509
6506
|
this.el.classList.toggle("sp-pip--active", pip);
|
|
5510
6507
|
}
|
|
@@ -5553,7 +6550,7 @@ var FullscreenButton = class {
|
|
|
5553
6550
|
this.toggle();
|
|
5554
6551
|
};
|
|
5555
6552
|
this.api = api;
|
|
5556
|
-
this.el = createButton("sp-fullscreen", "Fullscreen", icons.fullscreen);
|
|
6553
|
+
this.el = createButton("sp-fullscreen", "Fullscreen", icons$1.fullscreen);
|
|
5557
6554
|
this.el.addEventListener("click", this.clickHandler);
|
|
5558
6555
|
}
|
|
5559
6556
|
render() {
|
|
@@ -5562,23 +6559,29 @@ var FullscreenButton = class {
|
|
|
5562
6559
|
update() {
|
|
5563
6560
|
const fullscreen = this.api.getState("fullscreen");
|
|
5564
6561
|
if (fullscreen) {
|
|
5565
|
-
setHTML(this.el, icons.exitFullscreen);
|
|
6562
|
+
setHTML(this.el, icons$1.exitFullscreen);
|
|
5566
6563
|
setAttr(this.el, "aria-label", "Exit fullscreen");
|
|
5567
6564
|
} else {
|
|
5568
|
-
setHTML(this.el, icons.fullscreen);
|
|
6565
|
+
setHTML(this.el, icons$1.fullscreen);
|
|
5569
6566
|
setAttr(this.el, "aria-label", "Fullscreen");
|
|
5570
6567
|
}
|
|
5571
6568
|
}
|
|
6569
|
+
/**
|
|
6570
|
+
* Enter or leave fullscreen.
|
|
6571
|
+
*
|
|
6572
|
+
* The direction comes from the browser rather than from the `fullscreen`
|
|
6573
|
+
* state key: state is a report of what happened, and a stale one would invert
|
|
6574
|
+
* the button. Rejections are swallowed because the browser refuses these
|
|
6575
|
+
* routinely (no user gesture, denied by permission policy) and an unhandled
|
|
6576
|
+
* rejection helps nobody.
|
|
6577
|
+
*/
|
|
5572
6578
|
async toggle() {
|
|
5573
6579
|
const container = this.api.container;
|
|
5574
|
-
const video = getVideo(container);
|
|
5575
6580
|
try {
|
|
5576
|
-
if (
|
|
5577
|
-
await
|
|
5578
|
-
} else
|
|
5579
|
-
await container
|
|
5580
|
-
} else if (video?.webkitEnterFullscreen) {
|
|
5581
|
-
video.webkitEnterFullscreen();
|
|
6581
|
+
if (isFullscreen(container)) {
|
|
6582
|
+
await exitFullscreen(container);
|
|
6583
|
+
} else {
|
|
6584
|
+
await enterFullscreen(container);
|
|
5582
6585
|
}
|
|
5583
6586
|
} catch {
|
|
5584
6587
|
}
|
|
@@ -5669,7 +6672,7 @@ var ErrorOverlay = class {
|
|
|
5669
6672
|
content.className = "sp-error-overlay__content";
|
|
5670
6673
|
const iconEl = document.createElement("div");
|
|
5671
6674
|
iconEl.className = "sp-error-overlay__icon";
|
|
5672
|
-
iconEl.innerHTML = icons.error;
|
|
6675
|
+
iconEl.innerHTML = icons$1.error;
|
|
5673
6676
|
const messageEl = document.createElement("p");
|
|
5674
6677
|
messageEl.className = "sp-error-overlay__message";
|
|
5675
6678
|
messageEl.textContent = "Something went wrong.";
|
|
@@ -5775,7 +6778,7 @@ var SettingsMenu = class {
|
|
|
5775
6778
|
this.lastQualitiesJson = "";
|
|
5776
6779
|
this.api = api;
|
|
5777
6780
|
this.el = createElement("div", { className: "sp-settings" });
|
|
5778
|
-
this.btn = createButton("sp-settings__btn", "Settings", icons.settings);
|
|
6781
|
+
this.btn = createButton("sp-settings__btn", "Settings", icons$1.settings);
|
|
5779
6782
|
this.btn.setAttribute("aria-haspopup", "true");
|
|
5780
6783
|
this.btn.setAttribute("aria-expanded", "false");
|
|
5781
6784
|
this.btn.addEventListener("click", (e) => {
|
|
@@ -5920,7 +6923,7 @@ var SettingsMenu = class {
|
|
|
5920
6923
|
const rightSide = createElement("span", { className: "sp-settings-panel__value" });
|
|
5921
6924
|
rightSide.textContent = value;
|
|
5922
6925
|
const arrow = createElement("span", { className: "sp-settings-panel__arrow" });
|
|
5923
|
-
arrow.innerHTML = icons.chevronDown;
|
|
6926
|
+
arrow.innerHTML = icons$1.chevronDown;
|
|
5924
6927
|
rightSide.appendChild(arrow);
|
|
5925
6928
|
row.appendChild(labelEl);
|
|
5926
6929
|
row.appendChild(rightSide);
|
|
@@ -6020,7 +7023,7 @@ var SettingsMenu = class {
|
|
|
6020
7023
|
header.setAttribute("role", "menuitem");
|
|
6021
7024
|
header.setAttribute("tabindex", "0");
|
|
6022
7025
|
const backArrow = createElement("span", { className: "sp-settings-panel__back" });
|
|
6023
|
-
backArrow.innerHTML = icons.chevronUp;
|
|
7026
|
+
backArrow.innerHTML = icons$1.chevronUp;
|
|
6024
7027
|
const label = createElement("span", { className: "sp-settings-panel__header-label" });
|
|
6025
7028
|
label.textContent = title;
|
|
6026
7029
|
header.appendChild(backArrow);
|
|
@@ -6047,7 +7050,7 @@ var SettingsMenu = class {
|
|
|
6047
7050
|
const labelEl = createElement("span");
|
|
6048
7051
|
labelEl.textContent = label;
|
|
6049
7052
|
const check = createElement("span", { className: "sp-settings-panel__check" });
|
|
6050
|
-
check.innerHTML = icons.checkmark;
|
|
7053
|
+
check.innerHTML = icons$1.checkmark;
|
|
6051
7054
|
item.appendChild(labelEl);
|
|
6052
7055
|
item.appendChild(check);
|
|
6053
7056
|
item.addEventListener("keydown", (e) => {
|
|
@@ -6141,7 +7144,7 @@ var SkipButton = class {
|
|
|
6141
7144
|
this.api = api;
|
|
6142
7145
|
this.direction = direction;
|
|
6143
7146
|
this.seconds = seconds;
|
|
6144
|
-
const icon = direction === "backward" ? icons.replay10 : icons.forward10;
|
|
7147
|
+
const icon = direction === "backward" ? icons$1.replay10 : icons$1.forward10;
|
|
6145
7148
|
const label = direction === "backward" ? `Rewind ${seconds} seconds` : `Forward ${seconds} seconds`;
|
|
6146
7149
|
this.el = createButton(
|
|
6147
7150
|
`sp-skip sp-skip--${direction}`,
|
|
@@ -6203,7 +7206,7 @@ var CaptionsButton = class {
|
|
|
6203
7206
|
this.toggle();
|
|
6204
7207
|
};
|
|
6205
7208
|
this.api = api;
|
|
6206
|
-
this.el = createButton("sp-captions", "Captions", icons.captionsOff);
|
|
7209
|
+
this.el = createButton("sp-captions", "Captions", icons$1.captionsOff);
|
|
6207
7210
|
this.el.addEventListener("click", this.clickHandler);
|
|
6208
7211
|
}
|
|
6209
7212
|
render() {
|
|
@@ -6218,11 +7221,11 @@ var CaptionsButton = class {
|
|
|
6218
7221
|
}
|
|
6219
7222
|
this.el.style.display = "";
|
|
6220
7223
|
if (currentTrack) {
|
|
6221
|
-
setHTML(this.el, icons.captions);
|
|
7224
|
+
setHTML(this.el, icons$1.captions);
|
|
6222
7225
|
setAttr(this.el, "aria-label", `Captions: ${currentTrack.label}`);
|
|
6223
7226
|
this.el.classList.add("sp-captions--active");
|
|
6224
7227
|
} else {
|
|
6225
|
-
setHTML(this.el, icons.captionsOff);
|
|
7228
|
+
setHTML(this.el, icons$1.captionsOff);
|
|
6226
7229
|
setAttr(this.el, "aria-label", "Captions");
|
|
6227
7230
|
this.el.classList.remove("sp-captions--active");
|
|
6228
7231
|
}
|
|
@@ -6273,8 +7276,161 @@ var BandwidthIndicator = class {
|
|
|
6273
7276
|
this.el.remove();
|
|
6274
7277
|
}
|
|
6275
7278
|
};
|
|
7279
|
+
var OverflowTray = class {
|
|
7280
|
+
/**
|
|
7281
|
+
* @param api - Plugin API, kept for parity with the other controls and for
|
|
7282
|
+
* logging; the tray itself reads no state
|
|
7283
|
+
*/
|
|
7284
|
+
constructor(api) {
|
|
7285
|
+
this.api = api;
|
|
7286
|
+
this.isOpen = false;
|
|
7287
|
+
this.toggleHandler = () => {
|
|
7288
|
+
this.isOpen ? this.close() : this.open();
|
|
7289
|
+
};
|
|
7290
|
+
this.el = createElement("div", { className: "sp-overflow" });
|
|
7291
|
+
this.btn = createButton("sp-overflow__btn", "More controls", icons$1.more);
|
|
7292
|
+
this.btn.setAttribute("aria-haspopup", "true");
|
|
7293
|
+
this.btn.setAttribute("aria-expanded", "false");
|
|
7294
|
+
this.btn.addEventListener("click", this.toggleHandler);
|
|
7295
|
+
this.panel = createElement("div", {
|
|
7296
|
+
className: "sp-overflow-tray",
|
|
7297
|
+
role: "group",
|
|
7298
|
+
"aria-label": "More controls"
|
|
7299
|
+
});
|
|
7300
|
+
this.el.appendChild(this.btn);
|
|
7301
|
+
this.el.appendChild(this.panel);
|
|
7302
|
+
this.el.style.display = "none";
|
|
7303
|
+
this.closeHandler = (e) => {
|
|
7304
|
+
if (!this.el.contains(e.target)) {
|
|
7305
|
+
this.close();
|
|
7306
|
+
}
|
|
7307
|
+
};
|
|
7308
|
+
document.addEventListener("click", this.closeHandler);
|
|
7309
|
+
this.keyHandler = (e) => {
|
|
7310
|
+
if (!this.isOpen || e.key !== "Escape") return;
|
|
7311
|
+
e.preventDefault();
|
|
7312
|
+
e.stopPropagation();
|
|
7313
|
+
this.close();
|
|
7314
|
+
this.btn.focus();
|
|
7315
|
+
};
|
|
7316
|
+
document.addEventListener("keydown", this.keyHandler);
|
|
7317
|
+
}
|
|
7318
|
+
/**
|
|
7319
|
+
* The bar item to place in the control bar.
|
|
7320
|
+
*
|
|
7321
|
+
* @returns The wrapper holding the button and the strip
|
|
7322
|
+
*/
|
|
7323
|
+
render() {
|
|
7324
|
+
return this.el;
|
|
7325
|
+
}
|
|
7326
|
+
/**
|
|
7327
|
+
* No state of its own: the fit loop owns what is inside it.
|
|
7328
|
+
*/
|
|
7329
|
+
update() {
|
|
7330
|
+
}
|
|
7331
|
+
/**
|
|
7332
|
+
* Move a control's element into the tray.
|
|
7333
|
+
*
|
|
7334
|
+
* The element is moved as-is, so its class, icon, aria-label, event handlers
|
|
7335
|
+
* and `update()` all keep working.
|
|
7336
|
+
*
|
|
7337
|
+
* @param el - The control element leaving the bar
|
|
7338
|
+
*/
|
|
7339
|
+
adopt(el) {
|
|
7340
|
+
this.panel.appendChild(el);
|
|
7341
|
+
this.syncVisibility();
|
|
7342
|
+
}
|
|
7343
|
+
/**
|
|
7344
|
+
* Take a control's element back out of the tray.
|
|
7345
|
+
*
|
|
7346
|
+
* The caller decides where in the bar it goes; this only detaches it and
|
|
7347
|
+
* updates the button's visibility.
|
|
7348
|
+
*
|
|
7349
|
+
* @param el - The control element returning to the bar
|
|
7350
|
+
* @returns The same element, detached
|
|
7351
|
+
*/
|
|
7352
|
+
release(el) {
|
|
7353
|
+
if (el.parentNode === this.panel) {
|
|
7354
|
+
this.panel.removeChild(el);
|
|
7355
|
+
}
|
|
7356
|
+
this.syncVisibility();
|
|
7357
|
+
return el;
|
|
7358
|
+
}
|
|
7359
|
+
/**
|
|
7360
|
+
* Whether an element is currently held by the tray.
|
|
7361
|
+
*
|
|
7362
|
+
* @param el - Element to test
|
|
7363
|
+
* @returns True when the tray is its parent
|
|
7364
|
+
*/
|
|
7365
|
+
holds(el) {
|
|
7366
|
+
return el.parentNode === this.panel;
|
|
7367
|
+
}
|
|
7368
|
+
/**
|
|
7369
|
+
* Open the strip.
|
|
7370
|
+
*/
|
|
7371
|
+
open() {
|
|
7372
|
+
if (this.isOpen) return;
|
|
7373
|
+
this.isOpen = true;
|
|
7374
|
+
this.panel.classList.add("sp-overflow-tray--open");
|
|
7375
|
+
this.btn.setAttribute("aria-expanded", "true");
|
|
7376
|
+
}
|
|
7377
|
+
/**
|
|
7378
|
+
* Close the strip.
|
|
7379
|
+
*
|
|
7380
|
+
* Deliberately not called after a control inside it is used: skip, PiP and
|
|
7381
|
+
* cast are things a viewer taps more than once in a row.
|
|
7382
|
+
*/
|
|
7383
|
+
close() {
|
|
7384
|
+
if (!this.isOpen) return;
|
|
7385
|
+
this.isOpen = false;
|
|
7386
|
+
this.panel.classList.remove("sp-overflow-tray--open");
|
|
7387
|
+
this.btn.setAttribute("aria-expanded", "false");
|
|
7388
|
+
}
|
|
7389
|
+
/**
|
|
7390
|
+
* Show the button only while the tray holds something the viewer can see.
|
|
7391
|
+
*
|
|
7392
|
+
* A control that hid itself (no cast device on the network, no text tracks)
|
|
7393
|
+
* can be sitting in the tray with `display: none`, and a button that opens an
|
|
7394
|
+
* empty strip is worse than no button at all.
|
|
7395
|
+
*/
|
|
7396
|
+
syncVisibility() {
|
|
7397
|
+
const usable = Array.from(this.panel.children).some(
|
|
7398
|
+
(child) => child.style.display !== "none"
|
|
7399
|
+
);
|
|
7400
|
+
this.el.style.display = usable ? "" : "none";
|
|
7401
|
+
if (!usable && this.isOpen) {
|
|
7402
|
+
this.close();
|
|
7403
|
+
}
|
|
7404
|
+
}
|
|
7405
|
+
/**
|
|
7406
|
+
* Re-check the button's visibility after the controls have updated
|
|
7407
|
+
* themselves.
|
|
7408
|
+
*/
|
|
7409
|
+
refresh() {
|
|
7410
|
+
this.syncVisibility();
|
|
7411
|
+
}
|
|
7412
|
+
/**
|
|
7413
|
+
* Remove the document listeners and the tray itself.
|
|
7414
|
+
*
|
|
7415
|
+
* Adopted elements are left where they are: they belong to their own
|
|
7416
|
+
* controls, which are destroyed by the plugin alongside this one.
|
|
7417
|
+
*/
|
|
7418
|
+
destroy() {
|
|
7419
|
+
document.removeEventListener("click", this.closeHandler);
|
|
7420
|
+
document.removeEventListener("keydown", this.keyHandler);
|
|
7421
|
+
this.btn.removeEventListener("click", this.toggleHandler);
|
|
7422
|
+
this.el.remove();
|
|
7423
|
+
this.api.logger.debug("Overflow tray destroyed");
|
|
7424
|
+
}
|
|
7425
|
+
};
|
|
6276
7426
|
var registry = /* @__PURE__ */ new Map();
|
|
6277
7427
|
var listeners = /* @__PURE__ */ new Set();
|
|
7428
|
+
function registerControl(id, factory) {
|
|
7429
|
+
registry.set(id, factory);
|
|
7430
|
+
for (const listener of listeners) {
|
|
7431
|
+
listener(id);
|
|
7432
|
+
}
|
|
7433
|
+
}
|
|
6278
7434
|
function getControlFactory(id) {
|
|
6279
7435
|
return registry.get(id) ?? null;
|
|
6280
7436
|
}
|
|
@@ -6284,6 +7440,7 @@ function onControlRegistered(listener) {
|
|
|
6284
7440
|
listeners.delete(listener);
|
|
6285
7441
|
};
|
|
6286
7442
|
}
|
|
7443
|
+
var PKG_VERSION$5 = "1.8.1";
|
|
6287
7444
|
var DEFAULT_LAYOUT = [
|
|
6288
7445
|
"play",
|
|
6289
7446
|
"skip-backward",
|
|
@@ -6301,6 +7458,14 @@ var DEFAULT_LAYOUT = [
|
|
|
6301
7458
|
"fullscreen"
|
|
6302
7459
|
];
|
|
6303
7460
|
var DEFAULT_HIDE_DELAY = 3e3;
|
|
7461
|
+
var UNMEASURED_CONTROL_WIDTH = 48;
|
|
7462
|
+
var FALLBACK_VOLUME_SLIDER_WIDTH = 64;
|
|
7463
|
+
var OVERFLOW_BUTTON_WIDTH = 44;
|
|
7464
|
+
var FALLBACK_BAR_PADDING_X = 24;
|
|
7465
|
+
var FALLBACK_BAR_GAP = 4;
|
|
7466
|
+
var MENU_HEIGHT_RESERVE = 16;
|
|
7467
|
+
var FALLBACK_BAR_HEIGHT = 56;
|
|
7468
|
+
var MIN_MENU_HEIGHT = 120;
|
|
6304
7469
|
function uiPlugin(config = {}) {
|
|
6305
7470
|
let api;
|
|
6306
7471
|
let controlBar = null;
|
|
@@ -6308,6 +7473,7 @@ function uiPlugin(config = {}) {
|
|
|
6308
7473
|
let progressBar = null;
|
|
6309
7474
|
let bufferingIndicator = null;
|
|
6310
7475
|
let errorOverlay = null;
|
|
7476
|
+
let bigPlayButton = null;
|
|
6311
7477
|
let styleEl = null;
|
|
6312
7478
|
let controls = [];
|
|
6313
7479
|
let hideTimeout = null;
|
|
@@ -6318,8 +7484,22 @@ function uiPlugin(config = {}) {
|
|
|
6318
7484
|
let recoveredUnsubscribe = null;
|
|
6319
7485
|
let controlsVisible = true;
|
|
6320
7486
|
let rafHandle = null;
|
|
7487
|
+
let tray = null;
|
|
7488
|
+
let entries = [];
|
|
7489
|
+
let timeEntry = null;
|
|
7490
|
+
let resizeObserver = null;
|
|
7491
|
+
let barPaddingX = FALLBACK_BAR_PADDING_X;
|
|
7492
|
+
let barGap = FALLBACK_BAR_GAP;
|
|
7493
|
+
let volumeSliderWidth = FALLBACK_VOLUME_SLIDER_WIDTH;
|
|
7494
|
+
let lastFitSignature = null;
|
|
7495
|
+
let fitPending = true;
|
|
6321
7496
|
const layout = config.controls || DEFAULT_LAYOUT;
|
|
6322
7497
|
const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;
|
|
7498
|
+
const showBigPlayButton = config.bigPlayButton !== false;
|
|
7499
|
+
const responsive = config.responsive !== false;
|
|
7500
|
+
if (responsive) {
|
|
7501
|
+
assertFitLayout(layout, config.priority);
|
|
7502
|
+
}
|
|
6323
7503
|
const createControl = (slot) => {
|
|
6324
7504
|
switch (slot) {
|
|
6325
7505
|
case "play":
|
|
@@ -6373,48 +7553,221 @@ function uiPlugin(config = {}) {
|
|
|
6373
7553
|
if (!controlBar) {
|
|
6374
7554
|
return;
|
|
6375
7555
|
}
|
|
7556
|
+
const rules = new Map(
|
|
7557
|
+
resolveFitItems(layout, config.priority).map((template) => [template.id, template])
|
|
7558
|
+
);
|
|
6376
7559
|
for (const slot of layout) {
|
|
6377
7560
|
const control = createControl(slot);
|
|
6378
|
-
if (control) {
|
|
6379
|
-
|
|
6380
|
-
|
|
7561
|
+
if (!control) {
|
|
7562
|
+
continue;
|
|
7563
|
+
}
|
|
7564
|
+
controls.push(control);
|
|
7565
|
+
const el = control.render();
|
|
7566
|
+
controlBar.appendChild(el);
|
|
7567
|
+
const rule = rules.get(slot);
|
|
7568
|
+
const entry = {
|
|
7569
|
+
slot,
|
|
7570
|
+
control,
|
|
7571
|
+
el,
|
|
7572
|
+
rank: rule?.rank ?? "never",
|
|
7573
|
+
exit: rule?.exit ?? "overflow",
|
|
7574
|
+
width: -1
|
|
7575
|
+
};
|
|
7576
|
+
entries.push(entry);
|
|
7577
|
+
if (slot === "time") {
|
|
7578
|
+
timeEntry = entry;
|
|
6381
7579
|
}
|
|
6382
7580
|
}
|
|
7581
|
+
if (responsive) {
|
|
7582
|
+
tray = new OverflowTray(api);
|
|
7583
|
+
controls.push(tray);
|
|
7584
|
+
controlBar.appendChild(tray.render());
|
|
7585
|
+
placeTrayButton();
|
|
7586
|
+
}
|
|
6383
7587
|
};
|
|
6384
|
-
const
|
|
6385
|
-
if (!controlBar) {
|
|
7588
|
+
const placeTrayButton = () => {
|
|
7589
|
+
if (!controlBar || !tray) {
|
|
6386
7590
|
return;
|
|
6387
7591
|
}
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
controlBar.
|
|
6391
|
-
|
|
6392
|
-
|
|
7592
|
+
const trayEl = tray.render();
|
|
7593
|
+
const fullscreen = entries.find((entry) => entry.slot === "fullscreen");
|
|
7594
|
+
const before = fullscreen && fullscreen.el.parentNode === controlBar ? fullscreen.el : null;
|
|
7595
|
+
if (before) {
|
|
7596
|
+
if (trayEl.nextSibling !== before) {
|
|
7597
|
+
controlBar.insertBefore(trayEl, before);
|
|
7598
|
+
}
|
|
7599
|
+
return;
|
|
7600
|
+
}
|
|
7601
|
+
if (controlBar.lastChild !== trayEl) {
|
|
7602
|
+
controlBar.appendChild(trayEl);
|
|
7603
|
+
}
|
|
6393
7604
|
};
|
|
6394
|
-
const
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
const isLoading = playbackState === "loading";
|
|
6401
|
-
const showSpinner = waiting || seeking && !api?.getState("paused") || isLoading;
|
|
6402
|
-
bufferingIndicator?.classList.toggle("sp-buffering--visible", !!showSpinner);
|
|
6403
|
-
errorOverlay?.update();
|
|
7605
|
+
const visibilitySignature = () => {
|
|
7606
|
+
let flags = "";
|
|
7607
|
+
for (const entry of entries) {
|
|
7608
|
+
flags += entry.el.style.display === "none" ? "0" : "1";
|
|
7609
|
+
}
|
|
7610
|
+
return `${flags}:${timeEntry?.el.textContent?.length ?? 0}`;
|
|
6404
7611
|
};
|
|
6405
|
-
const
|
|
6406
|
-
if (
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
7612
|
+
const applyFit = (plan) => {
|
|
7613
|
+
if (!controlBar || !tray) {
|
|
7614
|
+
return;
|
|
7615
|
+
}
|
|
7616
|
+
const overflow = new Set(plan.overflow);
|
|
7617
|
+
const hidden = new Set(plan.hidden);
|
|
7618
|
+
for (const entry of entries) {
|
|
7619
|
+
if (entry.slot === "spacer") {
|
|
7620
|
+
continue;
|
|
7621
|
+
}
|
|
7622
|
+
if (hidden.has(entry.slot)) {
|
|
7623
|
+
if (tray.holds(entry.el)) {
|
|
7624
|
+
returnToBar(entry);
|
|
7625
|
+
}
|
|
7626
|
+
entry.el.classList.add("sp-control--collapsed");
|
|
7627
|
+
continue;
|
|
7628
|
+
}
|
|
7629
|
+
entry.el.classList.remove("sp-control--collapsed");
|
|
7630
|
+
if (overflow.has(entry.slot)) {
|
|
7631
|
+
if (!tray.holds(entry.el)) {
|
|
7632
|
+
tray.adopt(entry.el);
|
|
7633
|
+
}
|
|
7634
|
+
} else if (tray.holds(entry.el)) {
|
|
7635
|
+
returnToBar(entry);
|
|
7636
|
+
}
|
|
7637
|
+
}
|
|
7638
|
+
tray.refresh();
|
|
7639
|
+
placeTrayButton();
|
|
6411
7640
|
};
|
|
6412
|
-
const
|
|
6413
|
-
if (
|
|
6414
|
-
resetHideTimer();
|
|
7641
|
+
const returnToBar = (entry) => {
|
|
7642
|
+
if (!controlBar || !tray) {
|
|
6415
7643
|
return;
|
|
6416
7644
|
}
|
|
6417
|
-
|
|
7645
|
+
const el = tray.release(entry.el);
|
|
7646
|
+
const trayEl = tray.render();
|
|
7647
|
+
let before = trayEl.parentNode === controlBar ? trayEl : null;
|
|
7648
|
+
for (let i = entries.indexOf(entry) + 1; i < entries.length; i++) {
|
|
7649
|
+
if (entries[i].el.parentNode === controlBar) {
|
|
7650
|
+
before = entries[i].el;
|
|
7651
|
+
break;
|
|
7652
|
+
}
|
|
7653
|
+
}
|
|
7654
|
+
controlBar.insertBefore(el, before);
|
|
7655
|
+
};
|
|
7656
|
+
const expandedWidth = (entry) => {
|
|
7657
|
+
if (entry.slot !== "volume") {
|
|
7658
|
+
return 0;
|
|
7659
|
+
}
|
|
7660
|
+
const wrap = entry.el.querySelector(".sp-volume__slider-wrap");
|
|
7661
|
+
return wrap ? wrap.getBoundingClientRect().width : 0;
|
|
7662
|
+
};
|
|
7663
|
+
const interactionReserve = (entry) => entry.slot === "volume" ? volumeSliderWidth : 0;
|
|
7664
|
+
const readBarMetrics = (bar) => {
|
|
7665
|
+
const barStyle = getComputedStyle(bar);
|
|
7666
|
+
const paddingLeft = parseFloat(barStyle.paddingLeft);
|
|
7667
|
+
const paddingRight = parseFloat(barStyle.paddingRight);
|
|
7668
|
+
const gap = parseFloat(barStyle.columnGap || barStyle.gap);
|
|
7669
|
+
const sliderWidth = parseFloat(
|
|
7670
|
+
barStyle.getPropertyValue("--sp-volume-slider-width")
|
|
7671
|
+
);
|
|
7672
|
+
barPaddingX = Number.isFinite(paddingLeft) && Number.isFinite(paddingRight) ? paddingLeft + paddingRight : FALLBACK_BAR_PADDING_X;
|
|
7673
|
+
barGap = Number.isFinite(gap) ? gap : FALLBACK_BAR_GAP;
|
|
7674
|
+
volumeSliderWidth = Number.isFinite(sliderWidth) ? sliderWidth : FALLBACK_VOLUME_SLIDER_WIDTH;
|
|
7675
|
+
};
|
|
7676
|
+
const fitControls = () => {
|
|
7677
|
+
if (!responsive || !controlBar || !tray) {
|
|
7678
|
+
return;
|
|
7679
|
+
}
|
|
7680
|
+
if (controlBar.clientWidth === 0) {
|
|
7681
|
+
return;
|
|
7682
|
+
}
|
|
7683
|
+
readBarMetrics(controlBar);
|
|
7684
|
+
const spacerGaps = entries.filter(
|
|
7685
|
+
(entry) => entry.slot === "spacer" && entry.el.style.display !== "none"
|
|
7686
|
+
).length;
|
|
7687
|
+
const available = controlBar.clientWidth - barPaddingX - spacerGaps * barGap;
|
|
7688
|
+
const items = [];
|
|
7689
|
+
for (const entry of entries) {
|
|
7690
|
+
if (entry.slot === "spacer") {
|
|
7691
|
+
continue;
|
|
7692
|
+
}
|
|
7693
|
+
const visible = entry.el.style.display !== "none";
|
|
7694
|
+
const measurable = visible && entry.el.parentNode === controlBar && !entry.el.classList.contains("sp-control--collapsed");
|
|
7695
|
+
if (measurable) {
|
|
7696
|
+
entry.width = entry.el.getBoundingClientRect().width - expandedWidth(entry);
|
|
7697
|
+
} else if (entry.width < 0) {
|
|
7698
|
+
entry.width = UNMEASURED_CONTROL_WIDTH;
|
|
7699
|
+
}
|
|
7700
|
+
items.push({
|
|
7701
|
+
id: entry.slot,
|
|
7702
|
+
rank: entry.rank,
|
|
7703
|
+
exit: entry.exit,
|
|
7704
|
+
width: entry.width + interactionReserve(entry),
|
|
7705
|
+
visible
|
|
7706
|
+
});
|
|
7707
|
+
}
|
|
7708
|
+
const trayEl = tray.render();
|
|
7709
|
+
const trayWidth = trayEl.style.display === "none" ? 0 : trayEl.getBoundingClientRect().width;
|
|
7710
|
+
applyFit(planFit(items, available, barGap, trayWidth || OVERFLOW_BUTTON_WIDTH));
|
|
7711
|
+
lastFitSignature = visibilitySignature();
|
|
7712
|
+
fitPending = false;
|
|
7713
|
+
};
|
|
7714
|
+
const maybeFit = () => {
|
|
7715
|
+
if (!responsive) {
|
|
7716
|
+
return;
|
|
7717
|
+
}
|
|
7718
|
+
if (!fitPending && visibilitySignature() === lastFitSignature) {
|
|
7719
|
+
return;
|
|
7720
|
+
}
|
|
7721
|
+
fitControls();
|
|
7722
|
+
};
|
|
7723
|
+
const applyMenuBounds = (height) => {
|
|
7724
|
+
const barHeight = controlBar?.offsetHeight || FALLBACK_BAR_HEIGHT;
|
|
7725
|
+
api?.container?.style.setProperty(
|
|
7726
|
+
"--sp-menu-max-height",
|
|
7727
|
+
`${Math.max(MIN_MENU_HEIGHT, Math.round(height) - barHeight - MENU_HEIGHT_RESERVE)}px`
|
|
7728
|
+
);
|
|
7729
|
+
};
|
|
7730
|
+
const rebuildControlBar = () => {
|
|
7731
|
+
if (!controlBar) {
|
|
7732
|
+
return;
|
|
7733
|
+
}
|
|
7734
|
+
controls.forEach((c) => c.destroy());
|
|
7735
|
+
controls = [];
|
|
7736
|
+
entries = [];
|
|
7737
|
+
timeEntry = null;
|
|
7738
|
+
tray = null;
|
|
7739
|
+
controlBar.replaceChildren();
|
|
7740
|
+
lastFitSignature = null;
|
|
7741
|
+
fitPending = true;
|
|
7742
|
+
populateControlBar();
|
|
7743
|
+
updateControls();
|
|
7744
|
+
};
|
|
7745
|
+
const updateControls = () => {
|
|
7746
|
+
controls.forEach((c) => c.update());
|
|
7747
|
+
progressBar?.update();
|
|
7748
|
+
const waiting = api?.getState("waiting");
|
|
7749
|
+
const seeking = api?.getState("seeking");
|
|
7750
|
+
const playbackState = api?.getState("playbackState");
|
|
7751
|
+
const isLoading = playbackState === "loading";
|
|
7752
|
+
const showSpinner = waiting || seeking && !api?.getState("paused") || isLoading;
|
|
7753
|
+
bufferingIndicator?.classList.toggle("sp-buffering--visible", !!showSpinner);
|
|
7754
|
+
errorOverlay?.update();
|
|
7755
|
+
bigPlayButton?.update();
|
|
7756
|
+
maybeFit();
|
|
7757
|
+
};
|
|
7758
|
+
const scheduleUpdate = () => {
|
|
7759
|
+
if (rafHandle !== null) return;
|
|
7760
|
+
rafHandle = requestAnimationFrame(() => {
|
|
7761
|
+
rafHandle = null;
|
|
7762
|
+
updateControls();
|
|
7763
|
+
});
|
|
7764
|
+
};
|
|
7765
|
+
const showControls = () => {
|
|
7766
|
+
if (controlsVisible) {
|
|
7767
|
+
resetHideTimer();
|
|
7768
|
+
return;
|
|
7769
|
+
}
|
|
7770
|
+
controlsVisible = true;
|
|
6418
7771
|
controlBar?.classList.add("sp-controls--visible");
|
|
6419
7772
|
controlBar?.classList.remove("sp-controls--hidden");
|
|
6420
7773
|
gradient?.classList.add("sp-gradient--visible");
|
|
@@ -6479,11 +7832,11 @@ function uiPlugin(config = {}) {
|
|
|
6479
7832
|
break;
|
|
6480
7833
|
case "f":
|
|
6481
7834
|
e.preventDefault();
|
|
6482
|
-
if (
|
|
6483
|
-
|
|
7835
|
+
if (isFullscreen(api.container)) {
|
|
7836
|
+
exitFullscreen(api.container).catch(() => {
|
|
6484
7837
|
});
|
|
6485
7838
|
} else {
|
|
6486
|
-
api.container
|
|
7839
|
+
enterFullscreen(api.container).catch(() => {
|
|
6487
7840
|
});
|
|
6488
7841
|
}
|
|
6489
7842
|
break;
|
|
@@ -6521,11 +7874,11 @@ function uiPlugin(config = {}) {
|
|
|
6521
7874
|
id: "ui-controls",
|
|
6522
7875
|
name: "UI Controls",
|
|
6523
7876
|
type: "ui",
|
|
6524
|
-
version:
|
|
7877
|
+
version: PKG_VERSION$5,
|
|
6525
7878
|
async init(pluginApi) {
|
|
6526
7879
|
api = pluginApi;
|
|
6527
7880
|
styleEl = document.createElement("style");
|
|
6528
|
-
styleEl.textContent = styles;
|
|
7881
|
+
styleEl.textContent = styles$2;
|
|
6529
7882
|
document.head.appendChild(styleEl);
|
|
6530
7883
|
if (config.theme) {
|
|
6531
7884
|
this.setTheme(config.theme);
|
|
@@ -6545,7 +7898,7 @@ function uiPlugin(config = {}) {
|
|
|
6545
7898
|
container.appendChild(gradient);
|
|
6546
7899
|
bufferingIndicator = document.createElement("div");
|
|
6547
7900
|
bufferingIndicator.className = "sp-buffering";
|
|
6548
|
-
bufferingIndicator.innerHTML = icons.spinner;
|
|
7901
|
+
bufferingIndicator.innerHTML = icons$1.spinner;
|
|
6549
7902
|
bufferingIndicator.setAttribute("aria-hidden", "true");
|
|
6550
7903
|
container.appendChild(bufferingIndicator);
|
|
6551
7904
|
errorOverlay = new ErrorOverlay(api);
|
|
@@ -6562,6 +7915,10 @@ function uiPlugin(config = {}) {
|
|
|
6562
7915
|
recoveredUnsubscribe = api.on("error:recovered", () => {
|
|
6563
7916
|
errorOverlay?.hide();
|
|
6564
7917
|
});
|
|
7918
|
+
if (showBigPlayButton) {
|
|
7919
|
+
bigPlayButton = new BigPlayButton(api, () => errorOverlay?.isVisible() ?? false);
|
|
7920
|
+
container.appendChild(bigPlayButton.render());
|
|
7921
|
+
}
|
|
6565
7922
|
progressBar = new ProgressBar(api);
|
|
6566
7923
|
container.appendChild(progressBar.render());
|
|
6567
7924
|
if (!isPlaying) {
|
|
@@ -6573,6 +7930,14 @@ function uiPlugin(config = {}) {
|
|
|
6573
7930
|
controlBar.setAttribute("aria-label", "Video controls");
|
|
6574
7931
|
populateControlBar();
|
|
6575
7932
|
container.appendChild(controlBar);
|
|
7933
|
+
if (responsive && typeof ResizeObserver === "function") {
|
|
7934
|
+
resizeObserver = new ResizeObserver((observed) => {
|
|
7935
|
+
applyMenuBounds(observed[0]?.contentRect.height ?? container.clientHeight);
|
|
7936
|
+
fitPending = true;
|
|
7937
|
+
scheduleUpdate();
|
|
7938
|
+
});
|
|
7939
|
+
resizeObserver.observe(container);
|
|
7940
|
+
}
|
|
6576
7941
|
controlRegistryUnsubscribe = onControlRegistered((id) => {
|
|
6577
7942
|
if (!layout.includes(id)) {
|
|
6578
7943
|
return;
|
|
@@ -6589,7 +7954,6 @@ function uiPlugin(config = {}) {
|
|
|
6589
7954
|
container.addEventListener("click", handleInteraction);
|
|
6590
7955
|
document.addEventListener("keydown", handleKeyDown);
|
|
6591
7956
|
stateUnsubscribe = api.subscribeToState(scheduleUpdate);
|
|
6592
|
-
document.addEventListener("fullscreenchange", scheduleUpdate);
|
|
6593
7957
|
updateControls();
|
|
6594
7958
|
if (!container.hasAttribute("tabindex")) {
|
|
6595
7959
|
container.setAttribute("tabindex", "0");
|
|
@@ -6610,6 +7974,9 @@ function uiPlugin(config = {}) {
|
|
|
6610
7974
|
cancelAnimationFrame(rafHandle);
|
|
6611
7975
|
rafHandle = null;
|
|
6612
7976
|
}
|
|
7977
|
+
resizeObserver?.disconnect();
|
|
7978
|
+
resizeObserver = null;
|
|
7979
|
+
api?.container?.style.removeProperty("--sp-menu-max-height");
|
|
6613
7980
|
stateUnsubscribe?.();
|
|
6614
7981
|
stateUnsubscribe = null;
|
|
6615
7982
|
errorUnsubscribe?.();
|
|
@@ -6628,15 +7995,19 @@ function uiPlugin(config = {}) {
|
|
|
6628
7995
|
api.container.removeEventListener("click", handleInteraction);
|
|
6629
7996
|
}
|
|
6630
7997
|
document.removeEventListener("keydown", handleKeyDown);
|
|
6631
|
-
document.removeEventListener("fullscreenchange", scheduleUpdate);
|
|
6632
7998
|
controlRegistryUnsubscribe?.();
|
|
6633
7999
|
controlRegistryUnsubscribe = null;
|
|
6634
8000
|
controls.forEach((c) => c.destroy());
|
|
6635
8001
|
controls = [];
|
|
8002
|
+
entries = [];
|
|
8003
|
+
timeEntry = null;
|
|
8004
|
+
tray = null;
|
|
6636
8005
|
progressBar?.destroy();
|
|
6637
8006
|
progressBar = null;
|
|
6638
8007
|
errorOverlay?.destroy();
|
|
6639
8008
|
errorOverlay = null;
|
|
8009
|
+
bigPlayButton?.destroy();
|
|
8010
|
+
bigPlayButton = null;
|
|
6640
8011
|
controlBar?.remove();
|
|
6641
8012
|
controlBar = null;
|
|
6642
8013
|
gradient?.remove();
|
|
@@ -6682,6 +8053,21 @@ function uiPlugin(config = {}) {
|
|
|
6682
8053
|
}
|
|
6683
8054
|
};
|
|
6684
8055
|
}
|
|
8056
|
+
const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
8057
|
+
__proto__: null,
|
|
8058
|
+
DEFAULT_PRIORITY,
|
|
8059
|
+
assertFitLayout,
|
|
8060
|
+
formatLiveTime,
|
|
8061
|
+
formatTime,
|
|
8062
|
+
getControlFactory,
|
|
8063
|
+
icons: icons$1,
|
|
8064
|
+
planFit,
|
|
8065
|
+
registerControl,
|
|
8066
|
+
resolveFitItems,
|
|
8067
|
+
styles: styles$2,
|
|
8068
|
+
uiPlugin
|
|
8069
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
8070
|
+
var PKG_VERSION$4 = "1.8.1";
|
|
6685
8071
|
var POSITIONS = ["top-left", "top-right", "bottom-left", "bottom-right", "center"];
|
|
6686
8072
|
function getPositionStyles(padding, bottomPadding) {
|
|
6687
8073
|
return {
|
|
@@ -6791,7 +8177,7 @@ function createWatermarkPlugin(config = {}) {
|
|
|
6791
8177
|
return {
|
|
6792
8178
|
id: "watermark",
|
|
6793
8179
|
name: "Watermark",
|
|
6794
|
-
version:
|
|
8180
|
+
version: PKG_VERSION$4,
|
|
6795
8181
|
type: "feature",
|
|
6796
8182
|
description: "Anti-piracy watermark overlay with text/image support and dynamic repositioning",
|
|
6797
8183
|
init(pluginApi) {
|
|
@@ -6876,6 +8262,7 @@ function createWatermarkPlugin(config = {}) {
|
|
|
6876
8262
|
}
|
|
6877
8263
|
};
|
|
6878
8264
|
}
|
|
8265
|
+
var PKG_VERSION$3 = "1.8.1";
|
|
6879
8266
|
var HLS_SUBTITLE_TRACKS_UPDATED = "hlsSubtitleTracksUpdated";
|
|
6880
8267
|
var HLS_INSTANCE_RETRY_MS = 500;
|
|
6881
8268
|
function createCaptionsPlugin(config = {}) {
|
|
@@ -7046,7 +8433,7 @@ function createCaptionsPlugin(config = {}) {
|
|
|
7046
8433
|
return {
|
|
7047
8434
|
id: "captions",
|
|
7048
8435
|
name: "Captions",
|
|
7049
|
-
version:
|
|
8436
|
+
version: PKG_VERSION$3,
|
|
7050
8437
|
type: "feature",
|
|
7051
8438
|
description: "WebVTT subtitles and closed captions with HLS extraction",
|
|
7052
8439
|
init(pluginApi) {
|
|
@@ -7095,6 +8482,1305 @@ function createCaptionsPlugin(config = {}) {
|
|
|
7095
8482
|
}
|
|
7096
8483
|
};
|
|
7097
8484
|
}
|
|
8485
|
+
var ZONE_HYSTERESIS = 0.05;
|
|
8486
|
+
var DEFAULT_RECOGNIZER_OPTIONS = {
|
|
8487
|
+
doubleTapWindowMs: 275,
|
|
8488
|
+
accumulationWindowMs: 650,
|
|
8489
|
+
leftZone: 0.33,
|
|
8490
|
+
rightZone: 0.33,
|
|
8491
|
+
slopPx: 10
|
|
8492
|
+
};
|
|
8493
|
+
function zoneFor(fraction, options) {
|
|
8494
|
+
if (fraction <= options.leftZone) return "left";
|
|
8495
|
+
if (fraction >= 1 - options.rightZone) return "right";
|
|
8496
|
+
return "middle";
|
|
8497
|
+
}
|
|
8498
|
+
function createRecognizer(options = {}) {
|
|
8499
|
+
const config = { ...DEFAULT_RECOGNIZER_OPTIONS, ...options };
|
|
8500
|
+
let pending = null;
|
|
8501
|
+
let lastTapZone = null;
|
|
8502
|
+
let lastTapAt = 0;
|
|
8503
|
+
let accumulatingZone = null;
|
|
8504
|
+
let accumulatedCount = 0;
|
|
8505
|
+
let lastAccumulateAt = 0;
|
|
8506
|
+
let activePointers = 0;
|
|
8507
|
+
const resetSequence = () => {
|
|
8508
|
+
lastTapZone = null;
|
|
8509
|
+
lastTapAt = 0;
|
|
8510
|
+
accumulatingZone = null;
|
|
8511
|
+
accumulatedCount = 0;
|
|
8512
|
+
lastAccumulateAt = 0;
|
|
8513
|
+
};
|
|
8514
|
+
const resolveZone = (fraction) => {
|
|
8515
|
+
const raw = zoneFor(fraction, config);
|
|
8516
|
+
const active = accumulatingZone ?? lastTapZone;
|
|
8517
|
+
if (!active || raw === active) return raw;
|
|
8518
|
+
if (active === "left" && fraction <= config.leftZone + ZONE_HYSTERESIS) return "left";
|
|
8519
|
+
if (active === "right" && fraction >= 1 - config.rightZone - ZONE_HYSTERESIS) return "right";
|
|
8520
|
+
return raw;
|
|
8521
|
+
};
|
|
8522
|
+
const expire = (now) => {
|
|
8523
|
+
if (accumulatingZone && now - lastAccumulateAt > config.accumulationWindowMs) {
|
|
8524
|
+
resetSequence();
|
|
8525
|
+
} else if (!accumulatingZone && lastTapZone && now - lastTapAt > config.doubleTapWindowMs) {
|
|
8526
|
+
lastTapZone = null;
|
|
8527
|
+
lastTapAt = 0;
|
|
8528
|
+
}
|
|
8529
|
+
return [];
|
|
8530
|
+
};
|
|
8531
|
+
return {
|
|
8532
|
+
handle(record) {
|
|
8533
|
+
const events = [];
|
|
8534
|
+
switch (record.type) {
|
|
8535
|
+
case "down": {
|
|
8536
|
+
activePointers += 1;
|
|
8537
|
+
if (activePointers > 1) {
|
|
8538
|
+
if (pending) pending.invalid = true;
|
|
8539
|
+
if (accumulatingZone || lastTapZone) {
|
|
8540
|
+
resetSequence();
|
|
8541
|
+
events.push({ type: "cancel" });
|
|
8542
|
+
}
|
|
8543
|
+
return events;
|
|
8544
|
+
}
|
|
8545
|
+
expire(record.timeStamp);
|
|
8546
|
+
pending = {
|
|
8547
|
+
pointerId: record.pointerId,
|
|
8548
|
+
x: record.x,
|
|
8549
|
+
y: record.y,
|
|
8550
|
+
fraction: record.fraction,
|
|
8551
|
+
timeStamp: record.timeStamp,
|
|
8552
|
+
invalid: false
|
|
8553
|
+
};
|
|
8554
|
+
return events;
|
|
8555
|
+
}
|
|
8556
|
+
case "move": {
|
|
8557
|
+
if (!pending || pending.pointerId !== record.pointerId || pending.invalid) {
|
|
8558
|
+
return events;
|
|
8559
|
+
}
|
|
8560
|
+
const dx = record.x - pending.x;
|
|
8561
|
+
const dy = record.y - pending.y;
|
|
8562
|
+
if (Math.hypot(dx, dy) > config.slopPx) {
|
|
8563
|
+
pending.invalid = true;
|
|
8564
|
+
if (accumulatingZone || lastTapZone) {
|
|
8565
|
+
resetSequence();
|
|
8566
|
+
events.push({ type: "cancel" });
|
|
8567
|
+
}
|
|
8568
|
+
}
|
|
8569
|
+
return events;
|
|
8570
|
+
}
|
|
8571
|
+
case "up": {
|
|
8572
|
+
activePointers = Math.max(0, activePointers - 1);
|
|
8573
|
+
const candidate = pending;
|
|
8574
|
+
pending = null;
|
|
8575
|
+
if (!candidate || candidate.pointerId !== record.pointerId || candidate.invalid) {
|
|
8576
|
+
return events;
|
|
8577
|
+
}
|
|
8578
|
+
expire(record.timeStamp);
|
|
8579
|
+
const zone = resolveZone(record.fraction);
|
|
8580
|
+
if (accumulatingZone) {
|
|
8581
|
+
if (zone === accumulatingZone) {
|
|
8582
|
+
accumulatedCount += 1;
|
|
8583
|
+
lastAccumulateAt = record.timeStamp;
|
|
8584
|
+
events.push({ type: "accumulate", zone, count: accumulatedCount });
|
|
8585
|
+
} else {
|
|
8586
|
+
resetSequence();
|
|
8587
|
+
events.push({ type: "cancel" });
|
|
8588
|
+
}
|
|
8589
|
+
return events;
|
|
8590
|
+
}
|
|
8591
|
+
if (lastTapZone && zone === lastTapZone && record.timeStamp - lastTapAt <= config.doubleTapWindowMs) {
|
|
8592
|
+
accumulatingZone = zone;
|
|
8593
|
+
accumulatedCount = 1;
|
|
8594
|
+
lastAccumulateAt = record.timeStamp;
|
|
8595
|
+
lastTapZone = null;
|
|
8596
|
+
lastTapAt = 0;
|
|
8597
|
+
events.push({ type: "double-tap", zone, count: 1 });
|
|
8598
|
+
return events;
|
|
8599
|
+
}
|
|
8600
|
+
lastTapZone = zone;
|
|
8601
|
+
lastTapAt = record.timeStamp;
|
|
8602
|
+
events.push({ type: "tap", zone });
|
|
8603
|
+
return events;
|
|
8604
|
+
}
|
|
8605
|
+
case "cancel": {
|
|
8606
|
+
activePointers = Math.max(0, activePointers - 1);
|
|
8607
|
+
pending = null;
|
|
8608
|
+
if (accumulatingZone || lastTapZone) {
|
|
8609
|
+
resetSequence();
|
|
8610
|
+
events.push({ type: "cancel" });
|
|
8611
|
+
}
|
|
8612
|
+
return events;
|
|
8613
|
+
}
|
|
8614
|
+
default:
|
|
8615
|
+
return events;
|
|
8616
|
+
}
|
|
8617
|
+
},
|
|
8618
|
+
tick(now) {
|
|
8619
|
+
return expire(now);
|
|
8620
|
+
},
|
|
8621
|
+
reset() {
|
|
8622
|
+
pending = null;
|
|
8623
|
+
activePointers = 0;
|
|
8624
|
+
resetSequence();
|
|
8625
|
+
},
|
|
8626
|
+
isAccumulating() {
|
|
8627
|
+
return accumulatingZone !== null;
|
|
8628
|
+
}
|
|
8629
|
+
};
|
|
8630
|
+
}
|
|
8631
|
+
var STYLE_ID$1 = "sp-gestures-styles";
|
|
8632
|
+
var styles$1 = `
|
|
8633
|
+
.sp-gestures {
|
|
8634
|
+
position: absolute;
|
|
8635
|
+
top: 0;
|
|
8636
|
+
left: 0;
|
|
8637
|
+
right: 0;
|
|
8638
|
+
/* The bottom strip belongs to the progress bar and control bar. */
|
|
8639
|
+
bottom: 64px;
|
|
8640
|
+
z-index: 6;
|
|
8641
|
+
touch-action: manipulation;
|
|
8642
|
+
user-select: none;
|
|
8643
|
+
-webkit-user-select: none;
|
|
8644
|
+
-webkit-touch-callout: none;
|
|
8645
|
+
}
|
|
8646
|
+
|
|
8647
|
+
.sp-gestures__zone {
|
|
8648
|
+
position: absolute;
|
|
8649
|
+
top: 0;
|
|
8650
|
+
bottom: 0;
|
|
8651
|
+
display: flex;
|
|
8652
|
+
align-items: center;
|
|
8653
|
+
justify-content: center;
|
|
8654
|
+
pointer-events: none;
|
|
8655
|
+
opacity: 0;
|
|
8656
|
+
color: #fff;
|
|
8657
|
+
transition: opacity 0.25s ease;
|
|
8658
|
+
}
|
|
8659
|
+
|
|
8660
|
+
.sp-gestures__zone--left {
|
|
8661
|
+
left: 0;
|
|
8662
|
+
border-radius: 0 50% 50% 0;
|
|
8663
|
+
}
|
|
8664
|
+
|
|
8665
|
+
.sp-gestures__zone--right {
|
|
8666
|
+
right: 0;
|
|
8667
|
+
border-radius: 50% 0 0 50%;
|
|
8668
|
+
}
|
|
8669
|
+
|
|
8670
|
+
.sp-gestures__zone--active {
|
|
8671
|
+
opacity: 1;
|
|
8672
|
+
background: rgba(255, 255, 255, 0.12);
|
|
8673
|
+
}
|
|
8674
|
+
|
|
8675
|
+
.sp-gestures__label {
|
|
8676
|
+
font-size: 13px;
|
|
8677
|
+
font-weight: 600;
|
|
8678
|
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
|
|
8679
|
+
}
|
|
8680
|
+
|
|
8681
|
+
.sp-gestures__live {
|
|
8682
|
+
position: absolute;
|
|
8683
|
+
width: 1px;
|
|
8684
|
+
height: 1px;
|
|
8685
|
+
margin: -1px;
|
|
8686
|
+
padding: 0;
|
|
8687
|
+
overflow: hidden;
|
|
8688
|
+
clip: rect(0 0 0 0);
|
|
8689
|
+
white-space: nowrap;
|
|
8690
|
+
border: 0;
|
|
8691
|
+
}
|
|
8692
|
+
|
|
8693
|
+
@media (prefers-reduced-motion: reduce) {
|
|
8694
|
+
.sp-gestures__zone {
|
|
8695
|
+
transition: none;
|
|
8696
|
+
}
|
|
8697
|
+
}
|
|
8698
|
+
`;
|
|
8699
|
+
var GestureOverlay = class {
|
|
8700
|
+
constructor(container, options) {
|
|
8701
|
+
this.container = container;
|
|
8702
|
+
this.options = options;
|
|
8703
|
+
this.styleEl = null;
|
|
8704
|
+
this.hideTimer = null;
|
|
8705
|
+
this.pointerHandler = (event) => {
|
|
8706
|
+
if (event.pointerType !== "touch") return;
|
|
8707
|
+
const rect = this.el.getBoundingClientRect();
|
|
8708
|
+
const width = rect.width || 1;
|
|
8709
|
+
this.options.onPointer({
|
|
8710
|
+
type: event.type === "pointerdown" ? "down" : event.type === "pointermove" ? "move" : event.type === "pointerup" ? "up" : "cancel",
|
|
8711
|
+
x: event.clientX,
|
|
8712
|
+
y: event.clientY,
|
|
8713
|
+
fraction: Math.max(0, Math.min(1, (event.clientX - rect.left) / width)),
|
|
8714
|
+
pointerId: event.pointerId,
|
|
8715
|
+
timeStamp: event.timeStamp
|
|
8716
|
+
});
|
|
8717
|
+
};
|
|
8718
|
+
this.injectStyles();
|
|
8719
|
+
this.el = document.createElement("div");
|
|
8720
|
+
this.el.className = "sp-gestures";
|
|
8721
|
+
const left = this.createZone("left");
|
|
8722
|
+
const right = this.createZone("right");
|
|
8723
|
+
this.zones = { left: left.zone, right: right.zone };
|
|
8724
|
+
this.labels = { left: left.label, right: right.label };
|
|
8725
|
+
this.live = document.createElement("div");
|
|
8726
|
+
this.live.className = "sp-gestures__live";
|
|
8727
|
+
this.live.setAttribute("aria-live", "polite");
|
|
8728
|
+
this.live.setAttribute("role", "status");
|
|
8729
|
+
this.el.appendChild(left.zone);
|
|
8730
|
+
this.el.appendChild(right.zone);
|
|
8731
|
+
this.el.appendChild(this.live);
|
|
8732
|
+
this.el.addEventListener("pointerdown", this.pointerHandler);
|
|
8733
|
+
this.el.addEventListener("pointermove", this.pointerHandler);
|
|
8734
|
+
this.el.addEventListener("pointerup", this.pointerHandler);
|
|
8735
|
+
this.el.addEventListener("pointercancel", this.pointerHandler);
|
|
8736
|
+
container.appendChild(this.el);
|
|
8737
|
+
}
|
|
8738
|
+
/** Size the zones to match the recognizer's split. */
|
|
8739
|
+
setZoneWidths(left, right) {
|
|
8740
|
+
this.zones.left.style.width = `${left * 100}%`;
|
|
8741
|
+
this.zones.right.style.width = `${right * 100}%`;
|
|
8742
|
+
}
|
|
8743
|
+
/**
|
|
8744
|
+
* Show the cumulative seek for a zone.
|
|
8745
|
+
*
|
|
8746
|
+
* @param zone - Which side was tapped
|
|
8747
|
+
* @param seconds - Total seconds this sequence has moved
|
|
8748
|
+
*/
|
|
8749
|
+
showSeek(zone, seconds) {
|
|
8750
|
+
if (zone === "middle") return;
|
|
8751
|
+
const direction = zone === "right" ? "forward" : "back";
|
|
8752
|
+
this.live.textContent = `${seconds} seconds ${direction}`;
|
|
8753
|
+
if (!this.options.feedback) return;
|
|
8754
|
+
const target = this.zones[zone];
|
|
8755
|
+
this.labels[zone].textContent = `${seconds} seconds`;
|
|
8756
|
+
target.classList.add("sp-gestures__zone--active");
|
|
8757
|
+
if (this.hideTimer) clearTimeout(this.hideTimer);
|
|
8758
|
+
this.hideTimer = setTimeout(() => {
|
|
8759
|
+
this.zones.left.classList.remove("sp-gestures__zone--active");
|
|
8760
|
+
this.zones.right.classList.remove("sp-gestures__zone--active");
|
|
8761
|
+
this.hideTimer = null;
|
|
8762
|
+
}, 600);
|
|
8763
|
+
}
|
|
8764
|
+
/** Announce that a forward seek was refused because the viewer is at the live edge. */
|
|
8765
|
+
announceLiveEdge() {
|
|
8766
|
+
this.live.textContent = "Already at the live edge";
|
|
8767
|
+
}
|
|
8768
|
+
destroy() {
|
|
8769
|
+
if (this.hideTimer) {
|
|
8770
|
+
clearTimeout(this.hideTimer);
|
|
8771
|
+
this.hideTimer = null;
|
|
8772
|
+
}
|
|
8773
|
+
this.el.removeEventListener("pointerdown", this.pointerHandler);
|
|
8774
|
+
this.el.removeEventListener("pointermove", this.pointerHandler);
|
|
8775
|
+
this.el.removeEventListener("pointerup", this.pointerHandler);
|
|
8776
|
+
this.el.removeEventListener("pointercancel", this.pointerHandler);
|
|
8777
|
+
this.el.remove();
|
|
8778
|
+
this.styleEl?.remove();
|
|
8779
|
+
this.styleEl = null;
|
|
8780
|
+
}
|
|
8781
|
+
/** Exposed for tests and for hosts that want to inspect the surface. */
|
|
8782
|
+
getElement() {
|
|
8783
|
+
return this.el;
|
|
8784
|
+
}
|
|
8785
|
+
createZone(side) {
|
|
8786
|
+
const zone = document.createElement("div");
|
|
8787
|
+
zone.className = `sp-gestures__zone sp-gestures__zone--${side}`;
|
|
8788
|
+
zone.setAttribute("aria-hidden", "true");
|
|
8789
|
+
const label = document.createElement("span");
|
|
8790
|
+
label.className = "sp-gestures__label";
|
|
8791
|
+
zone.appendChild(label);
|
|
8792
|
+
return { zone, label };
|
|
8793
|
+
}
|
|
8794
|
+
injectStyles() {
|
|
8795
|
+
if (document.getElementById(STYLE_ID$1)) return;
|
|
8796
|
+
this.styleEl = document.createElement("style");
|
|
8797
|
+
this.styleEl.id = STYLE_ID$1;
|
|
8798
|
+
this.styleEl.textContent = styles$1;
|
|
8799
|
+
document.head.appendChild(this.styleEl);
|
|
8800
|
+
}
|
|
8801
|
+
};
|
|
8802
|
+
var PKG_VERSION$2 = "1.8.1";
|
|
8803
|
+
function hasCoarsePointer() {
|
|
8804
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
8805
|
+
return false;
|
|
8806
|
+
}
|
|
8807
|
+
try {
|
|
8808
|
+
return window.matchMedia("(any-pointer: coarse)").matches;
|
|
8809
|
+
} catch {
|
|
8810
|
+
return false;
|
|
8811
|
+
}
|
|
8812
|
+
}
|
|
8813
|
+
function createGesturesPlugin(config = {}) {
|
|
8814
|
+
let api = null;
|
|
8815
|
+
let overlay = null;
|
|
8816
|
+
let recognizer = null;
|
|
8817
|
+
let active = false;
|
|
8818
|
+
let runSeconds = 0;
|
|
8819
|
+
let pendingHide = null;
|
|
8820
|
+
const seekSeconds = config.seekSeconds ?? 10;
|
|
8821
|
+
const doubleTapWindowMs = config.doubleTapWindowMs ?? DEFAULT_RECOGNIZER_OPTIONS.doubleTapWindowMs;
|
|
8822
|
+
const leftZone = config.zones?.left ?? DEFAULT_RECOGNIZER_OPTIONS.leftZone;
|
|
8823
|
+
const rightZone = config.zones?.right ?? DEFAULT_RECOGNIZER_OPTIONS.rightZone;
|
|
8824
|
+
const feedback = config.feedback !== false;
|
|
8825
|
+
const haptics = config.haptics !== false;
|
|
8826
|
+
const tapToToggleControls = config.tapToToggleControls !== false;
|
|
8827
|
+
const canSeek = () => {
|
|
8828
|
+
if (!api) return false;
|
|
8829
|
+
if (api.getState("chromecastActive") || api.getState("airplayActive")) return false;
|
|
8830
|
+
const live = api.getState("live");
|
|
8831
|
+
const seekableRange = api.getState("seekableRange");
|
|
8832
|
+
if (live && !seekableRange) return false;
|
|
8833
|
+
const duration = api.getState("duration");
|
|
8834
|
+
if (!live && (!duration || !Number.isFinite(duration))) return false;
|
|
8835
|
+
return true;
|
|
8836
|
+
};
|
|
8837
|
+
const applySeek = (zone) => {
|
|
8838
|
+
if (!api || zone === "middle") return false;
|
|
8839
|
+
const video = api.container.querySelector("video");
|
|
8840
|
+
if (!video) return false;
|
|
8841
|
+
const delta = zone === "right" ? seekSeconds : -seekSeconds;
|
|
8842
|
+
const live = api.getState("live");
|
|
8843
|
+
const seekableRange = api.getState("seekableRange");
|
|
8844
|
+
const current = video.currentTime;
|
|
8845
|
+
let target;
|
|
8846
|
+
if (live && seekableRange) {
|
|
8847
|
+
target = Math.max(seekableRange.start, Math.min(seekableRange.end, current + delta));
|
|
8848
|
+
if (zone === "right" && target <= current) {
|
|
8849
|
+
overlay?.announceLiveEdge();
|
|
8850
|
+
return false;
|
|
8851
|
+
}
|
|
8852
|
+
} else {
|
|
8853
|
+
const duration = video.duration;
|
|
8854
|
+
const max = Number.isFinite(duration) && duration > 0 ? duration - 0.25 : current;
|
|
8855
|
+
target = Math.max(0, Math.min(max, current + delta));
|
|
8856
|
+
}
|
|
8857
|
+
video.currentTime = target;
|
|
8858
|
+
api.emit("playback:seeking", { time: target });
|
|
8859
|
+
if (haptics && typeof navigator !== "undefined" && typeof navigator.vibrate === "function") {
|
|
8860
|
+
navigator.vibrate(10);
|
|
8861
|
+
}
|
|
8862
|
+
return true;
|
|
8863
|
+
};
|
|
8864
|
+
const ui = () => api?.getPlugin("ui-controls") ?? null;
|
|
8865
|
+
const clearPendingHide = () => {
|
|
8866
|
+
if (pendingHide) {
|
|
8867
|
+
clearTimeout(pendingHide);
|
|
8868
|
+
pendingHide = null;
|
|
8869
|
+
}
|
|
8870
|
+
};
|
|
8871
|
+
const handleTap = () => {
|
|
8872
|
+
if (!tapToToggleControls || !api) return;
|
|
8873
|
+
const controls = ui();
|
|
8874
|
+
if (!controls) return;
|
|
8875
|
+
const visible = Boolean(api.getState("controlsVisible"));
|
|
8876
|
+
const paused = Boolean(api.getState("paused"));
|
|
8877
|
+
if (!visible) {
|
|
8878
|
+
controls.show();
|
|
8879
|
+
return;
|
|
8880
|
+
}
|
|
8881
|
+
if (paused) return;
|
|
8882
|
+
clearPendingHide();
|
|
8883
|
+
pendingHide = setTimeout(() => {
|
|
8884
|
+
pendingHide = null;
|
|
8885
|
+
controls.hide();
|
|
8886
|
+
}, doubleTapWindowMs);
|
|
8887
|
+
};
|
|
8888
|
+
const handleSeekStep = (zone) => {
|
|
8889
|
+
clearPendingHide();
|
|
8890
|
+
if (!canSeek()) return;
|
|
8891
|
+
const moved = applySeek(zone);
|
|
8892
|
+
if (!moved) return;
|
|
8893
|
+
runSeconds += seekSeconds;
|
|
8894
|
+
overlay?.showSeek(zone, runSeconds);
|
|
8895
|
+
api?.emit("gesture:seek", {
|
|
8896
|
+
direction: zone === "right" ? "forward" : "backward",
|
|
8897
|
+
seconds: seekSeconds,
|
|
8898
|
+
cumulative: runSeconds
|
|
8899
|
+
});
|
|
8900
|
+
};
|
|
8901
|
+
const onPointer = (record) => {
|
|
8902
|
+
if (!recognizer) return;
|
|
8903
|
+
for (const event of recognizer.handle(record)) {
|
|
8904
|
+
switch (event.type) {
|
|
8905
|
+
case "tap":
|
|
8906
|
+
api?.emit("gesture:tap", { zone: event.zone });
|
|
8907
|
+
handleTap();
|
|
8908
|
+
break;
|
|
8909
|
+
case "double-tap":
|
|
8910
|
+
runSeconds = 0;
|
|
8911
|
+
handleSeekStep(event.zone);
|
|
8912
|
+
break;
|
|
8913
|
+
case "accumulate":
|
|
8914
|
+
handleSeekStep(event.zone);
|
|
8915
|
+
break;
|
|
8916
|
+
case "cancel":
|
|
8917
|
+
runSeconds = 0;
|
|
8918
|
+
clearPendingHide();
|
|
8919
|
+
break;
|
|
8920
|
+
}
|
|
8921
|
+
}
|
|
8922
|
+
};
|
|
8923
|
+
return {
|
|
8924
|
+
id: "gestures",
|
|
8925
|
+
name: "Gestures",
|
|
8926
|
+
version: PKG_VERSION$2,
|
|
8927
|
+
type: "feature",
|
|
8928
|
+
init(pluginApi) {
|
|
8929
|
+
api = pluginApi;
|
|
8930
|
+
const enabled = config.enabled ?? "auto";
|
|
8931
|
+
active = enabled === "auto" ? hasCoarsePointer() : Boolean(enabled);
|
|
8932
|
+
if (!active) {
|
|
8933
|
+
api.logger.debug("[gestures] no coarse pointer, gesture surface not installed");
|
|
8934
|
+
return;
|
|
8935
|
+
}
|
|
8936
|
+
if (api.getState("mediaType") === "audio") {
|
|
8937
|
+
active = false;
|
|
8938
|
+
return;
|
|
8939
|
+
}
|
|
8940
|
+
recognizer = createRecognizer({
|
|
8941
|
+
doubleTapWindowMs,
|
|
8942
|
+
accumulationWindowMs: config.accumulationWindowMs,
|
|
8943
|
+
leftZone,
|
|
8944
|
+
rightZone,
|
|
8945
|
+
slopPx: config.slopPx
|
|
8946
|
+
});
|
|
8947
|
+
overlay = new GestureOverlay(api.container, { onPointer, feedback });
|
|
8948
|
+
overlay.setZoneWidths(leftZone, rightZone);
|
|
8949
|
+
api.onDestroy(() => {
|
|
8950
|
+
clearPendingHide();
|
|
8951
|
+
overlay?.destroy();
|
|
8952
|
+
overlay = null;
|
|
8953
|
+
recognizer?.reset();
|
|
8954
|
+
recognizer = null;
|
|
8955
|
+
});
|
|
8956
|
+
},
|
|
8957
|
+
destroy() {
|
|
8958
|
+
clearPendingHide();
|
|
8959
|
+
overlay?.destroy();
|
|
8960
|
+
overlay = null;
|
|
8961
|
+
recognizer?.reset();
|
|
8962
|
+
recognizer = null;
|
|
8963
|
+
active = false;
|
|
8964
|
+
runSeconds = 0;
|
|
8965
|
+
api = null;
|
|
8966
|
+
},
|
|
8967
|
+
ownsTapInteraction() {
|
|
8968
|
+
return active && tapToToggleControls;
|
|
8969
|
+
}
|
|
8970
|
+
};
|
|
8971
|
+
}
|
|
8972
|
+
function resolve(value, fallback) {
|
|
8973
|
+
if (typeof value === "function") {
|
|
8974
|
+
return value();
|
|
8975
|
+
}
|
|
8976
|
+
if (typeof value === "string" && value.length > 0) {
|
|
8977
|
+
return value;
|
|
8978
|
+
}
|
|
8979
|
+
return fallback();
|
|
8980
|
+
}
|
|
8981
|
+
function resolveBaseUrl(config) {
|
|
8982
|
+
return resolve(
|
|
8983
|
+
config.url,
|
|
8984
|
+
() => typeof window === "undefined" ? "" : window.location.href
|
|
8985
|
+
);
|
|
8986
|
+
}
|
|
8987
|
+
function resolveTitle(config) {
|
|
8988
|
+
return resolve(config.title, () => typeof document === "undefined" ? "" : document.title);
|
|
8989
|
+
}
|
|
8990
|
+
function applyTimestamp(baseUrl, currentTime, isLive, config) {
|
|
8991
|
+
const enabled = config.withTimestamp !== false;
|
|
8992
|
+
if (!enabled || isLive || !Number.isFinite(currentTime) || currentTime <= 0) {
|
|
8993
|
+
return baseUrl;
|
|
8994
|
+
}
|
|
8995
|
+
const param = config.timestampParam ?? "t";
|
|
8996
|
+
const rounded = config.roundTimestamp === false ? currentTime : Math.floor(currentTime);
|
|
8997
|
+
if (rounded <= 0) {
|
|
8998
|
+
return baseUrl;
|
|
8999
|
+
}
|
|
9000
|
+
try {
|
|
9001
|
+
const base = typeof window === "undefined" ? void 0 : window.location.href;
|
|
9002
|
+
const url = new URL(baseUrl, base);
|
|
9003
|
+
url.searchParams.set(param, String(rounded));
|
|
9004
|
+
return url.toString();
|
|
9005
|
+
} catch {
|
|
9006
|
+
return baseUrl;
|
|
9007
|
+
}
|
|
9008
|
+
}
|
|
9009
|
+
var icons = {
|
|
9010
|
+
/**
|
|
9011
|
+
* Three connected nodes. The one share glyph that reads the same everywhere:
|
|
9012
|
+
* it is the Material/Android share icon, and it carries no platform baggage.
|
|
9013
|
+
* The tray-and-arrow below is native on iOS but reads as "upload" or "export"
|
|
9014
|
+
* to everyone else, which is why it is no longer the default.
|
|
9015
|
+
*/
|
|
9016
|
+
share: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.6" y1="10.6" x2="15.4" y2="6.4"/><line x1="8.6" y1="13.4" x2="15.4" y2="17.6"/></svg>',
|
|
9017
|
+
/** iOS-style tray and arrow. Kept for hosts targeting an iOS-heavy audience. */
|
|
9018
|
+
upload: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>',
|
|
9019
|
+
native: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>',
|
|
9020
|
+
copy: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>',
|
|
9021
|
+
embed: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
|
|
9022
|
+
x: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.9 2H22l-7.1 8.1L23 22h-6.6l-5.2-6.8L5.3 22H2.2l7.6-8.7L1.7 2h6.8l4.7 6.2L18.9 2Zm-1.1 18h1.7L7.3 3.8H5.5L17.8 20Z"/></svg>',
|
|
9023
|
+
facebook: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M22 12a10 10 0 1 0-11.6 9.9v-7H7.9V12h2.5V9.8c0-2.5 1.5-3.9 3.8-3.9 1.1 0 2.2.2 2.2.2v2.5h-1.3c-1.2 0-1.6.8-1.6 1.6V12h2.8l-.4 2.9h-2.3v7A10 10 0 0 0 22 12Z"/></svg>',
|
|
9024
|
+
linkedin: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4.98 3.5A2.5 2.5 0 1 1 0 3.5a2.5 2.5 0 0 1 4.98 0ZM.2 8.3h4.6V24H.2V8.3Zm7.6 0h4.4v2.1h.1a4.8 4.8 0 0 1 4.3-2.4c4.6 0 5.4 3 5.4 6.9V24h-4.6v-7.6c0-1.8 0-4.1-2.5-4.1s-2.9 2-2.9 4V24H7.8V8.3Z"/></svg>',
|
|
9025
|
+
whatsapp: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2a10 10 0 0 0-8.5 15.2L2 22l4.9-1.4A10 10 0 1 0 12 2Zm5.1 14.1c-.2.6-1.2 1.2-1.7 1.2-.5.1-1 .1-1.6-.1-.4-.1-.9-.3-1.5-.6a11 11 0 0 1-4.2-3.8c-.3-.5-.7-1.2-.7-2 0-.8.4-1.2.6-1.4.2-.2.4-.3.6-.3h.4c.1 0 .3 0 .5.4l.7 1.6c0 .1.1.3 0 .4l-.3.4-.2.3c-.1.1-.2.2 0 .5.2.3.7 1.1 1.4 1.8.9.8 1.6 1 1.9 1.2.2.1.4.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.6.8c.2.1.4.2.4.3.1.1.1.5-.1 1Z"/></svg>',
|
|
9026
|
+
telegram: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21.9 4.3 18.6 20c-.2 1.1-.9 1.4-1.8.9l-4.9-3.6-2.4 2.3c-.3.3-.5.5-1 .5l.4-5 9.1-8.2c.4-.4-.1-.6-.6-.2L6.2 13.1l-4.8-1.5c-1-.3-1-1 .2-1.5l18.9-7.3c.9-.3 1.6.2 1.4 1.5Z"/></svg>',
|
|
9027
|
+
reddit: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M22 12a2.1 2.1 0 0 0-3.6-1.5 10.4 10.4 0 0 0-5.4-1.7l.9-4.2 3 .6a1.8 1.8 0 1 0 .2-1.2l-3.6-.8a.6.6 0 0 0-.7.5L11.7 8.8a10.4 10.4 0 0 0-5.5 1.7A2.1 2.1 0 1 0 3.6 14a4 4 0 0 0 0 .6c0 3.1 3.7 5.6 8.4 5.6s8.4-2.5 8.4-5.6a4 4 0 0 0 0-.6A2.1 2.1 0 0 0 22 12ZM7.9 13.5a1.5 1.5 0 1 1 1.5 1.5 1.5 1.5 0 0 1-1.5-1.5Zm8.3 4.1a5.6 5.6 0 0 1-4.2 1.3 5.6 5.6 0 0 1-4.2-1.3.5.5 0 0 1 .7-.7 4.7 4.7 0 0 0 3.5 1 4.7 4.7 0 0 0 3.5-1 .5.5 0 1 1 .7.7Zm-.6-2.6a1.5 1.5 0 1 1 1.5-1.5 1.5 1.5 0 0 1-1.5 1.5Z"/></svg>',
|
|
9028
|
+
email: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 6L2 7"/></svg>'
|
|
9029
|
+
};
|
|
9030
|
+
var encode = encodeURIComponent;
|
|
9031
|
+
var BUILTIN_TARGETS = {
|
|
9032
|
+
native: { id: "native", label: "Share", icon: icons.native },
|
|
9033
|
+
copy: { id: "copy", label: "Copy link", icon: icons.copy },
|
|
9034
|
+
embed: { id: "embed", label: "Embed", icon: icons.embed },
|
|
9035
|
+
x: {
|
|
9036
|
+
id: "x",
|
|
9037
|
+
label: "X",
|
|
9038
|
+
icon: icons.x,
|
|
9039
|
+
href: (c) => `https://x.com/intent/tweet?url=${encode(c.url)}&text=${encode(c.title)}`
|
|
9040
|
+
},
|
|
9041
|
+
facebook: {
|
|
9042
|
+
id: "facebook",
|
|
9043
|
+
label: "Facebook",
|
|
9044
|
+
icon: icons.facebook,
|
|
9045
|
+
href: (c) => `https://www.facebook.com/sharer/sharer.php?u=${encode(c.url)}`
|
|
9046
|
+
},
|
|
9047
|
+
linkedin: {
|
|
9048
|
+
id: "linkedin",
|
|
9049
|
+
label: "LinkedIn",
|
|
9050
|
+
icon: icons.linkedin,
|
|
9051
|
+
href: (c) => `https://www.linkedin.com/sharing/share-offsite/?url=${encode(c.url)}`
|
|
9052
|
+
},
|
|
9053
|
+
whatsapp: {
|
|
9054
|
+
id: "whatsapp",
|
|
9055
|
+
label: "WhatsApp",
|
|
9056
|
+
icon: icons.whatsapp,
|
|
9057
|
+
href: (c) => `https://api.whatsapp.com/send?text=${encode(`${c.title} ${c.url}`)}`
|
|
9058
|
+
},
|
|
9059
|
+
telegram: {
|
|
9060
|
+
id: "telegram",
|
|
9061
|
+
label: "Telegram",
|
|
9062
|
+
icon: icons.telegram,
|
|
9063
|
+
href: (c) => `https://t.me/share/url?url=${encode(c.url)}&text=${encode(c.title)}`
|
|
9064
|
+
},
|
|
9065
|
+
reddit: {
|
|
9066
|
+
id: "reddit",
|
|
9067
|
+
label: "Reddit",
|
|
9068
|
+
icon: icons.reddit,
|
|
9069
|
+
href: (c) => `https://www.reddit.com/submit?url=${encode(c.url)}&title=${encode(c.title)}`
|
|
9070
|
+
},
|
|
9071
|
+
email: {
|
|
9072
|
+
id: "email",
|
|
9073
|
+
label: "Email",
|
|
9074
|
+
icon: icons.email,
|
|
9075
|
+
href: (c) => `mailto:?subject=${encode(c.title)}&body=${encode(c.url)}`
|
|
9076
|
+
}
|
|
9077
|
+
};
|
|
9078
|
+
var DEFAULT_TARGETS = ["native", "copy", "embed"];
|
|
9079
|
+
function resolveTargets(configured, onUnknown) {
|
|
9080
|
+
const resolved = [];
|
|
9081
|
+
for (const entry of configured) {
|
|
9082
|
+
if (typeof entry !== "string") {
|
|
9083
|
+
resolved.push({ ...entry, icon: entry.icon ?? icons[entry.id] });
|
|
9084
|
+
continue;
|
|
9085
|
+
}
|
|
9086
|
+
const builtin = BUILTIN_TARGETS[entry];
|
|
9087
|
+
if (builtin) {
|
|
9088
|
+
resolved.push(builtin);
|
|
9089
|
+
} else {
|
|
9090
|
+
onUnknown?.(entry);
|
|
9091
|
+
}
|
|
9092
|
+
}
|
|
9093
|
+
return resolved;
|
|
9094
|
+
}
|
|
9095
|
+
function canUseNativeShare() {
|
|
9096
|
+
return typeof navigator !== "undefined" && typeof navigator.share === "function";
|
|
9097
|
+
}
|
|
9098
|
+
async function nativeShare(context) {
|
|
9099
|
+
try {
|
|
9100
|
+
await navigator.share({ title: context.title, url: context.url });
|
|
9101
|
+
return true;
|
|
9102
|
+
} catch (error) {
|
|
9103
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
9104
|
+
return false;
|
|
9105
|
+
}
|
|
9106
|
+
throw error;
|
|
9107
|
+
}
|
|
9108
|
+
}
|
|
9109
|
+
async function copyText(text) {
|
|
9110
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
9111
|
+
try {
|
|
9112
|
+
await navigator.clipboard.writeText(text);
|
|
9113
|
+
return true;
|
|
9114
|
+
} catch {
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
9117
|
+
if (typeof document === "undefined") {
|
|
9118
|
+
return false;
|
|
9119
|
+
}
|
|
9120
|
+
const textarea = document.createElement("textarea");
|
|
9121
|
+
textarea.value = text;
|
|
9122
|
+
textarea.setAttribute("readonly", "");
|
|
9123
|
+
textarea.style.position = "fixed";
|
|
9124
|
+
textarea.style.top = "-9999px";
|
|
9125
|
+
textarea.style.opacity = "0";
|
|
9126
|
+
document.body.appendChild(textarea);
|
|
9127
|
+
try {
|
|
9128
|
+
textarea.select();
|
|
9129
|
+
textarea.setSelectionRange(0, textarea.value.length);
|
|
9130
|
+
return document.execCommand("copy");
|
|
9131
|
+
} catch {
|
|
9132
|
+
return false;
|
|
9133
|
+
} finally {
|
|
9134
|
+
textarea.remove();
|
|
9135
|
+
}
|
|
9136
|
+
}
|
|
9137
|
+
function buildEmbedSnippet(context, config) {
|
|
9138
|
+
if (config.embedSnippet) {
|
|
9139
|
+
return config.embedSnippet(context);
|
|
9140
|
+
}
|
|
9141
|
+
if (!config.embedBaseUrl) {
|
|
9142
|
+
return null;
|
|
9143
|
+
}
|
|
9144
|
+
try {
|
|
9145
|
+
const base = typeof window === "undefined" ? void 0 : window.location.href;
|
|
9146
|
+
const embedUrl = new URL(config.embedBaseUrl, base);
|
|
9147
|
+
if (context.currentTime > 0 && !context.isLive) {
|
|
9148
|
+
embedUrl.searchParams.set("startTime", String(Math.floor(context.currentTime)));
|
|
9149
|
+
}
|
|
9150
|
+
embedUrl.searchParams.set("shareUrl", context.url);
|
|
9151
|
+
return `<iframe src="${embedUrl.toString()}" width="640" height="360" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen loading="lazy"></iframe>`;
|
|
9152
|
+
} catch {
|
|
9153
|
+
return null;
|
|
9154
|
+
}
|
|
9155
|
+
}
|
|
9156
|
+
var TOAST_MS = 1800;
|
|
9157
|
+
var ShareSheet = class {
|
|
9158
|
+
constructor(api, callbacks) {
|
|
9159
|
+
this.api = api;
|
|
9160
|
+
this.callbacks = callbacks;
|
|
9161
|
+
this.backdrop = null;
|
|
9162
|
+
this.sheet = null;
|
|
9163
|
+
this.toast = null;
|
|
9164
|
+
this.toastTimer = null;
|
|
9165
|
+
this.previouslyFocused = null;
|
|
9166
|
+
this.open = false;
|
|
9167
|
+
this.keydownHandler = (event) => {
|
|
9168
|
+
if (event.key === "Escape") {
|
|
9169
|
+
event.stopPropagation();
|
|
9170
|
+
this.close();
|
|
9171
|
+
return;
|
|
9172
|
+
}
|
|
9173
|
+
if (event.key === "Tab") {
|
|
9174
|
+
this.trapFocus(event);
|
|
9175
|
+
}
|
|
9176
|
+
};
|
|
9177
|
+
}
|
|
9178
|
+
/** Whether the sheet is currently showing. */
|
|
9179
|
+
isOpen() {
|
|
9180
|
+
return this.open;
|
|
9181
|
+
}
|
|
9182
|
+
/**
|
|
9183
|
+
* Show the sheet for a set of targets.
|
|
9184
|
+
*/
|
|
9185
|
+
show(targets) {
|
|
9186
|
+
if (this.open) {
|
|
9187
|
+
return;
|
|
9188
|
+
}
|
|
9189
|
+
this.previouslyFocused = document.activeElement;
|
|
9190
|
+
this.open = true;
|
|
9191
|
+
this.backdrop = document.createElement("div");
|
|
9192
|
+
this.backdrop.className = "sp-share-backdrop";
|
|
9193
|
+
this.backdrop.addEventListener("click", () => this.close());
|
|
9194
|
+
this.sheet = document.createElement("div");
|
|
9195
|
+
this.sheet.className = "sp-share-sheet";
|
|
9196
|
+
this.sheet.setAttribute("role", "dialog");
|
|
9197
|
+
this.sheet.setAttribute("aria-modal", "true");
|
|
9198
|
+
this.sheet.setAttribute("aria-label", "Share");
|
|
9199
|
+
const grip = document.createElement("div");
|
|
9200
|
+
grip.className = "sp-share-grip";
|
|
9201
|
+
this.sheet.appendChild(grip);
|
|
9202
|
+
const heading = document.createElement("p");
|
|
9203
|
+
heading.className = "sp-share-title";
|
|
9204
|
+
heading.textContent = "Share";
|
|
9205
|
+
this.sheet.appendChild(heading);
|
|
9206
|
+
const list = document.createElement("ul");
|
|
9207
|
+
list.className = "sp-share-targets";
|
|
9208
|
+
for (const target of targets) {
|
|
9209
|
+
list.appendChild(this.renderTarget(target));
|
|
9210
|
+
}
|
|
9211
|
+
this.sheet.appendChild(list);
|
|
9212
|
+
this.api.container.appendChild(this.backdrop);
|
|
9213
|
+
this.api.container.appendChild(this.sheet);
|
|
9214
|
+
requestAnimationFrame(() => {
|
|
9215
|
+
this.backdrop?.classList.add("sp-share-backdrop--open");
|
|
9216
|
+
this.sheet?.classList.add("sp-share-sheet--open");
|
|
9217
|
+
});
|
|
9218
|
+
document.addEventListener("keydown", this.keydownHandler, true);
|
|
9219
|
+
const first = this.sheet.querySelector(".sp-share-target");
|
|
9220
|
+
first?.focus();
|
|
9221
|
+
}
|
|
9222
|
+
/** Hide the sheet and restore focus. */
|
|
9223
|
+
close() {
|
|
9224
|
+
if (!this.open) {
|
|
9225
|
+
return;
|
|
9226
|
+
}
|
|
9227
|
+
this.open = false;
|
|
9228
|
+
document.removeEventListener("keydown", this.keydownHandler, true);
|
|
9229
|
+
this.backdrop?.classList.remove("sp-share-backdrop--open");
|
|
9230
|
+
this.sheet?.classList.remove("sp-share-sheet--open");
|
|
9231
|
+
const backdrop = this.backdrop;
|
|
9232
|
+
const sheet = this.sheet;
|
|
9233
|
+
this.backdrop = null;
|
|
9234
|
+
this.sheet = null;
|
|
9235
|
+
setTimeout(() => {
|
|
9236
|
+
backdrop?.remove();
|
|
9237
|
+
sheet?.remove();
|
|
9238
|
+
}, 220);
|
|
9239
|
+
this.previouslyFocused?.focus();
|
|
9240
|
+
this.previouslyFocused = null;
|
|
9241
|
+
this.callbacks.onClose();
|
|
9242
|
+
}
|
|
9243
|
+
/**
|
|
9244
|
+
* Show a transient confirmation, e.g. after copying.
|
|
9245
|
+
*/
|
|
9246
|
+
showToast(message) {
|
|
9247
|
+
if (this.toastTimer) {
|
|
9248
|
+
clearTimeout(this.toastTimer);
|
|
9249
|
+
this.toastTimer = null;
|
|
9250
|
+
}
|
|
9251
|
+
if (!this.toast) {
|
|
9252
|
+
this.toast = document.createElement("div");
|
|
9253
|
+
this.toast.className = "sp-share-toast";
|
|
9254
|
+
this.toast.setAttribute("role", "status");
|
|
9255
|
+
this.toast.setAttribute("aria-live", "polite");
|
|
9256
|
+
this.api.container.appendChild(this.toast);
|
|
9257
|
+
}
|
|
9258
|
+
this.toast.textContent = message;
|
|
9259
|
+
requestAnimationFrame(() => this.toast?.classList.add("sp-share-toast--visible"));
|
|
9260
|
+
this.toastTimer = setTimeout(() => {
|
|
9261
|
+
this.toast?.classList.remove("sp-share-toast--visible");
|
|
9262
|
+
this.toastTimer = null;
|
|
9263
|
+
}, TOAST_MS);
|
|
9264
|
+
}
|
|
9265
|
+
/**
|
|
9266
|
+
* Last-resort clipboard fallback: show the text for manual copying.
|
|
9267
|
+
*/
|
|
9268
|
+
showManualCopy(text) {
|
|
9269
|
+
if (!this.sheet) {
|
|
9270
|
+
return;
|
|
9271
|
+
}
|
|
9272
|
+
const row = document.createElement("div");
|
|
9273
|
+
row.className = "sp-share-fallback";
|
|
9274
|
+
const input = document.createElement("input");
|
|
9275
|
+
input.type = "text";
|
|
9276
|
+
input.readOnly = true;
|
|
9277
|
+
input.value = text;
|
|
9278
|
+
input.setAttribute("aria-label", "Link to copy");
|
|
9279
|
+
row.appendChild(input);
|
|
9280
|
+
this.sheet.appendChild(row);
|
|
9281
|
+
input.focus();
|
|
9282
|
+
input.select();
|
|
9283
|
+
}
|
|
9284
|
+
/** Remove everything and detach listeners. */
|
|
9285
|
+
destroy() {
|
|
9286
|
+
document.removeEventListener("keydown", this.keydownHandler, true);
|
|
9287
|
+
if (this.toastTimer) {
|
|
9288
|
+
clearTimeout(this.toastTimer);
|
|
9289
|
+
this.toastTimer = null;
|
|
9290
|
+
}
|
|
9291
|
+
this.backdrop?.remove();
|
|
9292
|
+
this.sheet?.remove();
|
|
9293
|
+
this.toast?.remove();
|
|
9294
|
+
this.backdrop = null;
|
|
9295
|
+
this.sheet = null;
|
|
9296
|
+
this.toast = null;
|
|
9297
|
+
this.open = false;
|
|
9298
|
+
this.previouslyFocused = null;
|
|
9299
|
+
}
|
|
9300
|
+
/** Build one target button. */
|
|
9301
|
+
renderTarget(target) {
|
|
9302
|
+
const item = document.createElement("li");
|
|
9303
|
+
const button = document.createElement("button");
|
|
9304
|
+
button.type = "button";
|
|
9305
|
+
button.className = `sp-share-target sp-share-target--${target.id}`;
|
|
9306
|
+
button.setAttribute("aria-label", target.label);
|
|
9307
|
+
const icon = document.createElement("span");
|
|
9308
|
+
icon.className = "sp-share-icon";
|
|
9309
|
+
icon.setAttribute("aria-hidden", "true");
|
|
9310
|
+
if (target.icon) {
|
|
9311
|
+
icon.innerHTML = target.icon;
|
|
9312
|
+
}
|
|
9313
|
+
const label = document.createElement("span");
|
|
9314
|
+
label.className = "sp-share-label";
|
|
9315
|
+
label.textContent = target.label;
|
|
9316
|
+
button.appendChild(icon);
|
|
9317
|
+
button.appendChild(label);
|
|
9318
|
+
button.addEventListener("click", () => this.callbacks.onSelect(target));
|
|
9319
|
+
item.appendChild(button);
|
|
9320
|
+
return item;
|
|
9321
|
+
}
|
|
9322
|
+
/** Keep Tab inside the dialog while it is open. */
|
|
9323
|
+
trapFocus(event) {
|
|
9324
|
+
if (!this.sheet) {
|
|
9325
|
+
return;
|
|
9326
|
+
}
|
|
9327
|
+
const focusable = this.sheet.querySelectorAll('button, input, [tabindex]:not([tabindex="-1"])');
|
|
9328
|
+
if (focusable.length === 0) {
|
|
9329
|
+
return;
|
|
9330
|
+
}
|
|
9331
|
+
const first = focusable[0];
|
|
9332
|
+
const last = focusable[focusable.length - 1];
|
|
9333
|
+
const active = document.activeElement;
|
|
9334
|
+
if (event.shiftKey && active === first) {
|
|
9335
|
+
event.preventDefault();
|
|
9336
|
+
last?.focus();
|
|
9337
|
+
} else if (!event.shiftKey && active === last) {
|
|
9338
|
+
event.preventDefault();
|
|
9339
|
+
first?.focus();
|
|
9340
|
+
}
|
|
9341
|
+
}
|
|
9342
|
+
};
|
|
9343
|
+
var ALLOWED_ELEMENTS = /* @__PURE__ */ new Set([
|
|
9344
|
+
"svg",
|
|
9345
|
+
"g",
|
|
9346
|
+
"path",
|
|
9347
|
+
"circle",
|
|
9348
|
+
"ellipse",
|
|
9349
|
+
"line",
|
|
9350
|
+
"polyline",
|
|
9351
|
+
"polygon",
|
|
9352
|
+
"rect",
|
|
9353
|
+
"defs",
|
|
9354
|
+
"lineargradient",
|
|
9355
|
+
"radialgradient",
|
|
9356
|
+
"stop",
|
|
9357
|
+
"clippath",
|
|
9358
|
+
"mask",
|
|
9359
|
+
"use",
|
|
9360
|
+
"symbol",
|
|
9361
|
+
"title",
|
|
9362
|
+
"desc"
|
|
9363
|
+
]);
|
|
9364
|
+
function sanitizeIcon(markup) {
|
|
9365
|
+
if (typeof markup !== "string" || markup.trim() === "") {
|
|
9366
|
+
return null;
|
|
9367
|
+
}
|
|
9368
|
+
if (typeof DOMParser === "undefined") {
|
|
9369
|
+
return null;
|
|
9370
|
+
}
|
|
9371
|
+
const doc = new DOMParser().parseFromString(markup, "text/html");
|
|
9372
|
+
const svg = doc.body.querySelector("svg");
|
|
9373
|
+
if (!svg) {
|
|
9374
|
+
return null;
|
|
9375
|
+
}
|
|
9376
|
+
for (const element of Array.from(svg.querySelectorAll("*"))) {
|
|
9377
|
+
if (!ALLOWED_ELEMENTS.has(element.nodeName.toLowerCase())) {
|
|
9378
|
+
element.remove();
|
|
9379
|
+
continue;
|
|
9380
|
+
}
|
|
9381
|
+
stripDangerousAttributes(element);
|
|
9382
|
+
}
|
|
9383
|
+
stripDangerousAttributes(svg);
|
|
9384
|
+
return svg.outerHTML;
|
|
9385
|
+
}
|
|
9386
|
+
function stripDangerousAttributes(element) {
|
|
9387
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
9388
|
+
const name = attribute.name.toLowerCase();
|
|
9389
|
+
if (name.startsWith("on")) {
|
|
9390
|
+
element.removeAttribute(attribute.name);
|
|
9391
|
+
continue;
|
|
9392
|
+
}
|
|
9393
|
+
if (name === "href" || name === "xlink:href" || name === "src") {
|
|
9394
|
+
const value = attribute.value.replace(/\s+/g, "").toLowerCase();
|
|
9395
|
+
if (value.startsWith("javascript:") || value.startsWith("data:text/html")) {
|
|
9396
|
+
element.removeAttribute(attribute.name);
|
|
9397
|
+
}
|
|
9398
|
+
}
|
|
9399
|
+
}
|
|
9400
|
+
}
|
|
9401
|
+
var ShareButton = class {
|
|
9402
|
+
constructor(_api, onActivate, options = {}) {
|
|
9403
|
+
this.onActivate = onActivate;
|
|
9404
|
+
this.clickHandler = () => {
|
|
9405
|
+
this.onActivate();
|
|
9406
|
+
};
|
|
9407
|
+
const label = options.label ?? "Share";
|
|
9408
|
+
this.el = document.createElement("button");
|
|
9409
|
+
this.el.type = "button";
|
|
9410
|
+
this.el.className = "sp-share sp-control";
|
|
9411
|
+
this.el.setAttribute("aria-label", label);
|
|
9412
|
+
this.el.setAttribute("title", label);
|
|
9413
|
+
this.el.setAttribute("aria-haspopup", "dialog");
|
|
9414
|
+
const icon = options.icon ? sanitizeIcon(options.icon) : null;
|
|
9415
|
+
this.el.innerHTML = icon ?? icons.share ?? "";
|
|
9416
|
+
this.el.addEventListener("click", this.clickHandler);
|
|
9417
|
+
}
|
|
9418
|
+
render() {
|
|
9419
|
+
return this.el;
|
|
9420
|
+
}
|
|
9421
|
+
update() {
|
|
9422
|
+
}
|
|
9423
|
+
destroy() {
|
|
9424
|
+
this.el.removeEventListener("click", this.clickHandler);
|
|
9425
|
+
this.el.remove();
|
|
9426
|
+
}
|
|
9427
|
+
};
|
|
9428
|
+
var styles = `
|
|
9429
|
+
.sp-share-backdrop {
|
|
9430
|
+
position: absolute;
|
|
9431
|
+
inset: 0;
|
|
9432
|
+
background: rgba(0, 0, 0, 0.55);
|
|
9433
|
+
opacity: 0;
|
|
9434
|
+
transition: opacity 180ms ease;
|
|
9435
|
+
z-index: 30;
|
|
9436
|
+
}
|
|
9437
|
+
.sp-share-backdrop--open { opacity: 1; }
|
|
9438
|
+
|
|
9439
|
+
.sp-share-sheet {
|
|
9440
|
+
position: absolute;
|
|
9441
|
+
left: 0;
|
|
9442
|
+
right: 0;
|
|
9443
|
+
bottom: 0;
|
|
9444
|
+
z-index: 31;
|
|
9445
|
+
background: #1c1c1e;
|
|
9446
|
+
color: #fff;
|
|
9447
|
+
border-radius: 14px 14px 0 0;
|
|
9448
|
+
padding: 8px 12px calc(12px + env(safe-area-inset-bottom, 0px));
|
|
9449
|
+
box-shadow: 0 -6px 28px rgba(0, 0, 0, 0.45);
|
|
9450
|
+
transform: translateY(100%);
|
|
9451
|
+
transition: transform 220ms cubic-bezier(0.32, 0.72, 0, 1);
|
|
9452
|
+
max-height: 80%;
|
|
9453
|
+
overflow-y: auto;
|
|
9454
|
+
-webkit-overflow-scrolling: touch;
|
|
9455
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
9456
|
+
}
|
|
9457
|
+
.sp-share-sheet--open { transform: translateY(0); }
|
|
9458
|
+
|
|
9459
|
+
/* Grab handle - the affordance that says "this drags/dismisses" on a phone. */
|
|
9460
|
+
.sp-share-grip {
|
|
9461
|
+
width: 36px;
|
|
9462
|
+
height: 4px;
|
|
9463
|
+
border-radius: 2px;
|
|
9464
|
+
background: rgba(255, 255, 255, 0.3);
|
|
9465
|
+
margin: 4px auto 10px;
|
|
9466
|
+
}
|
|
9467
|
+
|
|
9468
|
+
.sp-share-title {
|
|
9469
|
+
font-size: 13px;
|
|
9470
|
+
font-weight: 600;
|
|
9471
|
+
letter-spacing: 0.02em;
|
|
9472
|
+
text-transform: uppercase;
|
|
9473
|
+
color: rgba(255, 255, 255, 0.55);
|
|
9474
|
+
padding: 0 8px 8px;
|
|
9475
|
+
margin: 0;
|
|
9476
|
+
}
|
|
9477
|
+
|
|
9478
|
+
.sp-share-targets {
|
|
9479
|
+
display: grid;
|
|
9480
|
+
grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
|
|
9481
|
+
gap: 4px;
|
|
9482
|
+
list-style: none;
|
|
9483
|
+
margin: 0;
|
|
9484
|
+
padding: 0;
|
|
9485
|
+
}
|
|
9486
|
+
|
|
9487
|
+
.sp-share-target {
|
|
9488
|
+
display: flex;
|
|
9489
|
+
flex-direction: column;
|
|
9490
|
+
align-items: center;
|
|
9491
|
+
justify-content: center;
|
|
9492
|
+
gap: 8px;
|
|
9493
|
+
/* 44px is the WCAG 2.5.5 floor; 72px is a comfortable thumb target. */
|
|
9494
|
+
min-height: 72px;
|
|
9495
|
+
min-width: 44px;
|
|
9496
|
+
padding: 10px 6px;
|
|
9497
|
+
border: 0;
|
|
9498
|
+
border-radius: 10px;
|
|
9499
|
+
background: transparent;
|
|
9500
|
+
color: inherit;
|
|
9501
|
+
font: inherit;
|
|
9502
|
+
font-size: 12px;
|
|
9503
|
+
cursor: pointer;
|
|
9504
|
+
-webkit-tap-highlight-color: transparent;
|
|
9505
|
+
transition: background-color 120ms ease, transform 120ms ease;
|
|
9506
|
+
}
|
|
9507
|
+
.sp-share-target:active { transform: scale(0.94); background: rgba(255, 255, 255, 0.14); }
|
|
9508
|
+
.sp-share-target:focus-visible { outline: 2px solid #fff; outline-offset: -2px; }
|
|
9509
|
+
@media (hover: hover) {
|
|
9510
|
+
.sp-share-target:hover { background: rgba(255, 255, 255, 0.1); }
|
|
9511
|
+
}
|
|
9512
|
+
|
|
9513
|
+
.sp-share-target svg {
|
|
9514
|
+
width: 24px;
|
|
9515
|
+
height: 24px;
|
|
9516
|
+
display: block;
|
|
9517
|
+
}
|
|
9518
|
+
|
|
9519
|
+
.sp-share-icon {
|
|
9520
|
+
display: flex;
|
|
9521
|
+
align-items: center;
|
|
9522
|
+
justify-content: center;
|
|
9523
|
+
width: 44px;
|
|
9524
|
+
height: 44px;
|
|
9525
|
+
border-radius: 50%;
|
|
9526
|
+
background: rgba(255, 255, 255, 0.12);
|
|
9527
|
+
}
|
|
9528
|
+
|
|
9529
|
+
.sp-share-label {
|
|
9530
|
+
line-height: 1.2;
|
|
9531
|
+
text-align: center;
|
|
9532
|
+
max-width: 100%;
|
|
9533
|
+
overflow: hidden;
|
|
9534
|
+
text-overflow: ellipsis;
|
|
9535
|
+
white-space: nowrap;
|
|
9536
|
+
}
|
|
9537
|
+
|
|
9538
|
+
/* Copy confirmation, announced politely to assistive tech. */
|
|
9539
|
+
.sp-share-toast {
|
|
9540
|
+
position: absolute;
|
|
9541
|
+
left: 50%;
|
|
9542
|
+
bottom: 16px;
|
|
9543
|
+
transform: translateX(-50%) translateY(8px);
|
|
9544
|
+
z-index: 32;
|
|
9545
|
+
background: rgba(28, 28, 30, 0.95);
|
|
9546
|
+
color: #fff;
|
|
9547
|
+
font-size: 13px;
|
|
9548
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
9549
|
+
padding: 10px 16px;
|
|
9550
|
+
border-radius: 20px;
|
|
9551
|
+
opacity: 0;
|
|
9552
|
+
pointer-events: none;
|
|
9553
|
+
transition: opacity 160ms ease, transform 160ms ease;
|
|
9554
|
+
}
|
|
9555
|
+
.sp-share-toast--visible { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
9556
|
+
|
|
9557
|
+
/* Fallback when the clipboard is unavailable: show the URL to copy by hand. */
|
|
9558
|
+
.sp-share-fallback {
|
|
9559
|
+
display: flex;
|
|
9560
|
+
gap: 8px;
|
|
9561
|
+
padding: 8px;
|
|
9562
|
+
}
|
|
9563
|
+
.sp-share-fallback input {
|
|
9564
|
+
flex: 1;
|
|
9565
|
+
min-width: 0;
|
|
9566
|
+
/* 16px keeps iOS Safari from zooming the viewport on focus. */
|
|
9567
|
+
font-size: 16px;
|
|
9568
|
+
padding: 10px 12px;
|
|
9569
|
+
border-radius: 8px;
|
|
9570
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
9571
|
+
background: rgba(255, 255, 255, 0.08);
|
|
9572
|
+
color: #fff;
|
|
9573
|
+
}
|
|
9574
|
+
|
|
9575
|
+
@media (min-width: 640px) {
|
|
9576
|
+
.sp-share-backdrop { background: rgba(0, 0, 0, 0.25); }
|
|
9577
|
+
|
|
9578
|
+
.sp-share-sheet {
|
|
9579
|
+
left: auto;
|
|
9580
|
+
right: 12px;
|
|
9581
|
+
bottom: 64px;
|
|
9582
|
+
width: 320px;
|
|
9583
|
+
border-radius: 12px;
|
|
9584
|
+
padding: 10px;
|
|
9585
|
+
transform: translateY(8px) scale(0.98);
|
|
9586
|
+
opacity: 0;
|
|
9587
|
+
transition: opacity 140ms ease, transform 140ms ease;
|
|
9588
|
+
max-height: 60%;
|
|
9589
|
+
}
|
|
9590
|
+
.sp-share-sheet--open { transform: translateY(0) scale(1); opacity: 1; }
|
|
9591
|
+
|
|
9592
|
+
.sp-share-grip { display: none; }
|
|
9593
|
+
.sp-share-targets { grid-template-columns: repeat(auto-fill, minmax(76px, 1fr)); }
|
|
9594
|
+
}
|
|
9595
|
+
|
|
9596
|
+
@media (prefers-reduced-motion: reduce) {
|
|
9597
|
+
.sp-share-backdrop,
|
|
9598
|
+
.sp-share-sheet,
|
|
9599
|
+
.sp-share-target,
|
|
9600
|
+
.sp-share-toast {
|
|
9601
|
+
transition: none;
|
|
9602
|
+
}
|
|
9603
|
+
}
|
|
9604
|
+
`;
|
|
9605
|
+
var PKG_VERSION$1 = "1.8.1";
|
|
9606
|
+
var STYLE_ID = "sp-share-styles";
|
|
9607
|
+
function createSharePlugin(config = {}) {
|
|
9608
|
+
let api = null;
|
|
9609
|
+
let sheet = null;
|
|
9610
|
+
let styleEl = null;
|
|
9611
|
+
const configuredTargets = config.targets ?? DEFAULT_TARGETS;
|
|
9612
|
+
const buildContext = () => {
|
|
9613
|
+
const currentTime = api?.getState("currentTime") ?? 0;
|
|
9614
|
+
const isLive = Boolean(api?.getState("live"));
|
|
9615
|
+
const baseUrl = resolveBaseUrl(config);
|
|
9616
|
+
return {
|
|
9617
|
+
url: applyTimestamp(baseUrl, currentTime, isLive, config),
|
|
9618
|
+
title: resolveTitle(config),
|
|
9619
|
+
currentTime,
|
|
9620
|
+
isLive
|
|
9621
|
+
};
|
|
9622
|
+
};
|
|
9623
|
+
const reportError = (error) => {
|
|
9624
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
9625
|
+
api?.logger.error("Share failed", { error: err });
|
|
9626
|
+
config.onError?.(err);
|
|
9627
|
+
};
|
|
9628
|
+
const reportShared = (targetId, url) => {
|
|
9629
|
+
config.onShare?.(targetId, url);
|
|
9630
|
+
api?.emit("share:completed", { targetId, url });
|
|
9631
|
+
};
|
|
9632
|
+
const availableTargets = (context) => resolveTargets(configuredTargets, (id) => api?.logger.warn(`Unknown share target: ${id}`)).filter(
|
|
9633
|
+
(target) => {
|
|
9634
|
+
if (target.id === "native") {
|
|
9635
|
+
return canUseNativeShare();
|
|
9636
|
+
}
|
|
9637
|
+
if (target.id === "embed") {
|
|
9638
|
+
return buildEmbedSnippet(context, config) !== null;
|
|
9639
|
+
}
|
|
9640
|
+
return true;
|
|
9641
|
+
}
|
|
9642
|
+
);
|
|
9643
|
+
const runTarget = async (target, context) => {
|
|
9644
|
+
if (target.id === "native") {
|
|
9645
|
+
const shared = await nativeShare(context);
|
|
9646
|
+
if (shared) {
|
|
9647
|
+
reportShared(target.id, context.url);
|
|
9648
|
+
}
|
|
9649
|
+
return;
|
|
9650
|
+
}
|
|
9651
|
+
if (target.id === "copy") {
|
|
9652
|
+
const copied = await copyText(context.url);
|
|
9653
|
+
if (copied) {
|
|
9654
|
+
sheet?.showToast("Link copied");
|
|
9655
|
+
reportShared(target.id, context.url);
|
|
9656
|
+
} else {
|
|
9657
|
+
sheet?.showManualCopy(context.url);
|
|
9658
|
+
}
|
|
9659
|
+
return;
|
|
9660
|
+
}
|
|
9661
|
+
if (target.id === "embed") {
|
|
9662
|
+
const snippet = buildEmbedSnippet(context, config);
|
|
9663
|
+
if (!snippet) {
|
|
9664
|
+
return;
|
|
9665
|
+
}
|
|
9666
|
+
const copied = await copyText(snippet);
|
|
9667
|
+
if (copied) {
|
|
9668
|
+
sheet?.showToast("Embed code copied");
|
|
9669
|
+
reportShared(target.id, context.url);
|
|
9670
|
+
} else {
|
|
9671
|
+
sheet?.showManualCopy(snippet);
|
|
9672
|
+
}
|
|
9673
|
+
return;
|
|
9674
|
+
}
|
|
9675
|
+
if (target.href) {
|
|
9676
|
+
window.open(target.href(context), "_blank", "noopener,noreferrer");
|
|
9677
|
+
reportShared(target.id, context.url);
|
|
9678
|
+
}
|
|
9679
|
+
};
|
|
9680
|
+
const openSheet = () => {
|
|
9681
|
+
if (!sheet) {
|
|
9682
|
+
return;
|
|
9683
|
+
}
|
|
9684
|
+
const context = buildContext();
|
|
9685
|
+
const targets = availableTargets(context);
|
|
9686
|
+
if (targets.length === 0) {
|
|
9687
|
+
api?.logger.warn("No share targets available");
|
|
9688
|
+
return;
|
|
9689
|
+
}
|
|
9690
|
+
sheet.show(targets);
|
|
9691
|
+
api?.emit("share:opened", void 0);
|
|
9692
|
+
};
|
|
9693
|
+
const activate = async () => {
|
|
9694
|
+
const hostChoseTargets = config.targets !== void 0;
|
|
9695
|
+
if (!hostChoseTargets && canUseNativeShare()) {
|
|
9696
|
+
const context = buildContext();
|
|
9697
|
+
try {
|
|
9698
|
+
const shared = await nativeShare(context);
|
|
9699
|
+
if (shared) {
|
|
9700
|
+
reportShared("native", context.url);
|
|
9701
|
+
}
|
|
9702
|
+
} catch (error) {
|
|
9703
|
+
reportError(error);
|
|
9704
|
+
}
|
|
9705
|
+
return;
|
|
9706
|
+
}
|
|
9707
|
+
openSheet();
|
|
9708
|
+
};
|
|
9709
|
+
return {
|
|
9710
|
+
id: "share",
|
|
9711
|
+
name: "Share",
|
|
9712
|
+
version: PKG_VERSION$1,
|
|
9713
|
+
type: "feature",
|
|
9714
|
+
description: "Native share sheet, copy link, timestamps and embed codes",
|
|
9715
|
+
init(pluginApi) {
|
|
9716
|
+
api = pluginApi;
|
|
9717
|
+
api.logger.debug("Share plugin initialized");
|
|
9718
|
+
if (!document.getElementById(STYLE_ID)) {
|
|
9719
|
+
styleEl = document.createElement("style");
|
|
9720
|
+
styleEl.id = STYLE_ID;
|
|
9721
|
+
styleEl.textContent = styles;
|
|
9722
|
+
document.head.appendChild(styleEl);
|
|
9723
|
+
}
|
|
9724
|
+
sheet = new ShareSheet(api, {
|
|
9725
|
+
onSelect: (target) => {
|
|
9726
|
+
const context = buildContext();
|
|
9727
|
+
if (target.id !== "copy" && target.id !== "embed") {
|
|
9728
|
+
sheet?.close();
|
|
9729
|
+
}
|
|
9730
|
+
void runTarget(target, context).catch(reportError);
|
|
9731
|
+
},
|
|
9732
|
+
onClose: () => {
|
|
9733
|
+
api?.emit("share:closed", void 0);
|
|
9734
|
+
}
|
|
9735
|
+
});
|
|
9736
|
+
void Promise.resolve().then(() => index).then(({ registerControl: registerControl2 }) => {
|
|
9737
|
+
registerControl2(
|
|
9738
|
+
"share",
|
|
9739
|
+
(controlApi) => new ShareButton(controlApi, () => void activate(), {
|
|
9740
|
+
icon: config.buttonIcon,
|
|
9741
|
+
label: config.buttonLabel
|
|
9742
|
+
})
|
|
9743
|
+
);
|
|
9744
|
+
}).catch(() => {
|
|
9745
|
+
api?.logger.debug("@scarlett-player/ui not present, share control not registered");
|
|
9746
|
+
});
|
|
9747
|
+
api.onDestroy(() => {
|
|
9748
|
+
sheet?.destroy();
|
|
9749
|
+
sheet = null;
|
|
9750
|
+
});
|
|
9751
|
+
},
|
|
9752
|
+
destroy() {
|
|
9753
|
+
sheet?.destroy();
|
|
9754
|
+
sheet = null;
|
|
9755
|
+
styleEl?.remove();
|
|
9756
|
+
styleEl = null;
|
|
9757
|
+
api = null;
|
|
9758
|
+
},
|
|
9759
|
+
async share(targetId) {
|
|
9760
|
+
if (!targetId) {
|
|
9761
|
+
await activate();
|
|
9762
|
+
return;
|
|
9763
|
+
}
|
|
9764
|
+
const context = buildContext();
|
|
9765
|
+
const target = availableTargets(context).find((t) => t.id === targetId);
|
|
9766
|
+
if (!target) {
|
|
9767
|
+
reportError(new Error(`Unknown or unavailable share target: ${targetId}`));
|
|
9768
|
+
return;
|
|
9769
|
+
}
|
|
9770
|
+
try {
|
|
9771
|
+
await runTarget(target, context);
|
|
9772
|
+
} catch (error) {
|
|
9773
|
+
reportError(error);
|
|
9774
|
+
}
|
|
9775
|
+
},
|
|
9776
|
+
getShareUrl() {
|
|
9777
|
+
return buildContext().url;
|
|
9778
|
+
},
|
|
9779
|
+
close() {
|
|
9780
|
+
sheet?.close();
|
|
9781
|
+
}
|
|
9782
|
+
};
|
|
9783
|
+
}
|
|
7098
9784
|
function getAttr(element, ...names) {
|
|
7099
9785
|
for (const name of names) {
|
|
7100
9786
|
const value = element.getAttribute(name);
|
|
@@ -7124,6 +9810,14 @@ function parseDataAttributes(element) {
|
|
|
7124
9810
|
if (controls !== null) {
|
|
7125
9811
|
config.controls = controls !== "false";
|
|
7126
9812
|
}
|
|
9813
|
+
const bigPlayButton = getAttr(element, "data-big-play-button", "big-play-button");
|
|
9814
|
+
if (bigPlayButton !== null) {
|
|
9815
|
+
config.bigPlayButton = bigPlayButton !== "false";
|
|
9816
|
+
}
|
|
9817
|
+
const gestures = getAttr(element, "data-gestures", "gestures");
|
|
9818
|
+
if (gestures !== null) {
|
|
9819
|
+
config.gestures = gestures !== "false";
|
|
9820
|
+
}
|
|
7127
9821
|
const keyboard = getAttr(element, "data-keyboard", "keyboard");
|
|
7128
9822
|
if (keyboard !== null) {
|
|
7129
9823
|
config.keyboard = keyboard !== "false";
|
|
@@ -7152,6 +9846,14 @@ function parseDataAttributes(element) {
|
|
|
7152
9846
|
if (album) {
|
|
7153
9847
|
config.album = album;
|
|
7154
9848
|
}
|
|
9849
|
+
const shareUrl = getAttr(element, "data-share-url", "share-url");
|
|
9850
|
+
if (shareUrl) {
|
|
9851
|
+
config.shareUrl = shareUrl;
|
|
9852
|
+
}
|
|
9853
|
+
const embedBaseUrl = getAttr(element, "data-embed-base-url", "embed-base-url");
|
|
9854
|
+
if (embedBaseUrl) {
|
|
9855
|
+
config.embedBaseUrl = embedBaseUrl;
|
|
9856
|
+
}
|
|
7155
9857
|
const brandColor = getAttr(element, "data-brand-color", "data-color", "color");
|
|
7156
9858
|
if (brandColor) {
|
|
7157
9859
|
config.brandColor = brandColor;
|
|
@@ -7254,6 +9956,23 @@ function applyContainerStyles(container, config) {
|
|
|
7254
9956
|
container.style.width = container.style.width || "100%";
|
|
7255
9957
|
}
|
|
7256
9958
|
}
|
|
9959
|
+
const SHARE_CONTROL_LAYOUT = [
|
|
9960
|
+
"play",
|
|
9961
|
+
"skip-backward",
|
|
9962
|
+
"skip-forward",
|
|
9963
|
+
"volume",
|
|
9964
|
+
"time",
|
|
9965
|
+
"live-indicator",
|
|
9966
|
+
"bandwidth-indicator",
|
|
9967
|
+
"spacer",
|
|
9968
|
+
"share",
|
|
9969
|
+
"settings",
|
|
9970
|
+
"captions",
|
|
9971
|
+
"chromecast",
|
|
9972
|
+
"airplay",
|
|
9973
|
+
"pip",
|
|
9974
|
+
"fullscreen"
|
|
9975
|
+
];
|
|
7257
9976
|
async function createEmbedPlayer(container, config, pluginCreators2, availableTypes) {
|
|
7258
9977
|
const type = config.type || "video";
|
|
7259
9978
|
if (!availableTypes.includes(type)) {
|
|
@@ -7273,10 +9992,13 @@ async function createEmbedPlayer(container, config, pluginCreators2, availableTy
|
|
|
7273
9992
|
if (config.primaryColor) theme.primaryColor = config.primaryColor;
|
|
7274
9993
|
if (config.backgroundColor) theme.backgroundColor = config.backgroundColor;
|
|
7275
9994
|
const plugins = [pluginCreators2.hls()];
|
|
9995
|
+
if (pluginCreators2.native) {
|
|
9996
|
+
plugins.push(pluginCreators2.native());
|
|
9997
|
+
}
|
|
7276
9998
|
if (pluginCreators2.playlist && config.playlist?.length) {
|
|
7277
9999
|
plugins.push(pluginCreators2.playlist({
|
|
7278
|
-
items: config.playlist.map((item,
|
|
7279
|
-
id: `item-${
|
|
10000
|
+
items: config.playlist.map((item, index2) => ({
|
|
10001
|
+
id: `item-${index2}`,
|
|
7280
10002
|
src: item.src,
|
|
7281
10003
|
title: item.title,
|
|
7282
10004
|
artist: item.artist,
|
|
@@ -7299,6 +10021,16 @@ async function createEmbedPlayer(container, config, pluginCreators2, availableTy
|
|
|
7299
10021
|
if (pluginCreators2.captions) {
|
|
7300
10022
|
plugins.push(pluginCreators2.captions(config.captions || {}));
|
|
7301
10023
|
}
|
|
10024
|
+
if (type === "video" && pluginCreators2.gestures && config.gestures !== false) {
|
|
10025
|
+
plugins.push(pluginCreators2.gestures({}));
|
|
10026
|
+
}
|
|
10027
|
+
const shareEnabled = type === "video" && Boolean(config.shareUrl) && config.controls !== false;
|
|
10028
|
+
if (shareEnabled && pluginCreators2.share) {
|
|
10029
|
+
const shareConfig = { url: config.shareUrl };
|
|
10030
|
+
if (config.title) shareConfig.title = config.title;
|
|
10031
|
+
if (config.embedBaseUrl) shareConfig.embedBaseUrl = config.embedBaseUrl;
|
|
10032
|
+
plugins.push(pluginCreators2.share(shareConfig));
|
|
10033
|
+
}
|
|
7302
10034
|
if (pluginCreators2.analytics && config.analytics?.beaconUrl) {
|
|
7303
10035
|
plugins.push(pluginCreators2.analytics({
|
|
7304
10036
|
beaconUrl: config.analytics.beaconUrl,
|
|
@@ -7311,6 +10043,8 @@ async function createEmbedPlayer(container, config, pluginCreators2, availableTy
|
|
|
7311
10043
|
const uiConfig = {};
|
|
7312
10044
|
if (Object.keys(theme).length > 0) uiConfig.theme = theme;
|
|
7313
10045
|
if (config.hideDelay !== void 0) uiConfig.hideDelay = config.hideDelay;
|
|
10046
|
+
if (config.bigPlayButton !== void 0) uiConfig.bigPlayButton = config.bigPlayButton;
|
|
10047
|
+
if (shareEnabled && pluginCreators2.share) uiConfig.controls = SHARE_CONTROL_LAYOUT;
|
|
7314
10048
|
plugins.push(pluginCreators2.videoUI(uiConfig));
|
|
7315
10049
|
} else if ((type === "audio" || type === "audio-mini") && pluginCreators2.audioUI) {
|
|
7316
10050
|
plugins.push(pluginCreators2.audioUI({
|
|
@@ -7416,13 +10150,17 @@ function setupAutoInit(pluginCreators2, availableTypes) {
|
|
|
7416
10150
|
}
|
|
7417
10151
|
}
|
|
7418
10152
|
}
|
|
7419
|
-
const
|
|
10153
|
+
const PKG_VERSION = "1.8.1";
|
|
10154
|
+
const VERSION = `${PKG_VERSION}-video`;
|
|
7420
10155
|
const AVAILABLE_TYPES = ["video"];
|
|
7421
10156
|
const pluginCreators = {
|
|
7422
10157
|
hls: createHLSPlugin,
|
|
10158
|
+
native: createNativePlugin,
|
|
7423
10159
|
videoUI: uiPlugin,
|
|
7424
10160
|
watermark: createWatermarkPlugin,
|
|
7425
|
-
captions: createCaptionsPlugin
|
|
10161
|
+
captions: createCaptionsPlugin,
|
|
10162
|
+
gestures: createGesturesPlugin,
|
|
10163
|
+
share: createSharePlugin
|
|
7426
10164
|
// Audio UI not available in this build
|
|
7427
10165
|
// Analytics not available in this build
|
|
7428
10166
|
// Playlist not available in this build
|