@scarlett-player/embed 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -5
- package/dist/create-embed.d.ts +67 -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 +820 -52
- 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 +2240 -215
- 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 +2221 -202
- 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 +19 -0
- package/dist/index-video.d.ts.map +1 -0
- package/dist/index.d.ts +20 -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 +156 -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 +35 -13
- package/package.json +19 -17
- package/dist/hls2.js.map +0 -1
package/dist/embed.audio.js
CHANGED
|
@@ -149,6 +149,7 @@ class StateManager {
|
|
|
149
149
|
this.signals = /* @__PURE__ */ new Map();
|
|
150
150
|
this.changeSubscribers = /* @__PURE__ */ new Set();
|
|
151
151
|
this.definedDefaults = /* @__PURE__ */ new Map();
|
|
152
|
+
this.destroyed = false;
|
|
152
153
|
this.initializeSignals(initialState);
|
|
153
154
|
}
|
|
154
155
|
/**
|
|
@@ -209,6 +210,7 @@ class StateManager {
|
|
|
209
210
|
*
|
|
210
211
|
* @param key - State property key
|
|
211
212
|
* @returns Signal for the property
|
|
213
|
+
* @throws If the manager has been destroyed, or the key was never registered
|
|
212
214
|
*
|
|
213
215
|
* @example
|
|
214
216
|
* ```ts
|
|
@@ -218,6 +220,9 @@ class StateManager {
|
|
|
218
220
|
* ```
|
|
219
221
|
*/
|
|
220
222
|
get(key) {
|
|
223
|
+
if (this.destroyed) {
|
|
224
|
+
throw new Error(`[StateManager] Manager is destroyed (reading '${key}')`);
|
|
225
|
+
}
|
|
221
226
|
const stateSignal = this.signals.get(key);
|
|
222
227
|
if (!stateSignal) {
|
|
223
228
|
throw new Error(`[StateManager] Unknown state key: ${key}`);
|
|
@@ -229,6 +234,7 @@ class StateManager {
|
|
|
229
234
|
*
|
|
230
235
|
* @param key - State property key
|
|
231
236
|
* @returns Current value
|
|
237
|
+
* @throws If the manager has been destroyed, or the key was never registered
|
|
232
238
|
*
|
|
233
239
|
* @example
|
|
234
240
|
* ```ts
|
|
@@ -396,6 +402,11 @@ class StateManager {
|
|
|
396
402
|
/**
|
|
397
403
|
* Destroy the state manager and cleanup all signals.
|
|
398
404
|
*
|
|
405
|
+
* After this, every read or write ({@link get}, {@link getValue},
|
|
406
|
+
* {@link set}) throws a destroyed-specific error. Returning last-known
|
|
407
|
+
* values instead was considered and rejected: it silently masks the
|
|
408
|
+
* lifecycle bugs this throw exposes.
|
|
409
|
+
*
|
|
399
410
|
* @example
|
|
400
411
|
* ```ts
|
|
401
412
|
* state.destroy();
|
|
@@ -405,6 +416,7 @@ class StateManager {
|
|
|
405
416
|
this.signals.forEach((stateSignal) => stateSignal.destroy());
|
|
406
417
|
this.signals.clear();
|
|
407
418
|
this.changeSubscribers.clear();
|
|
419
|
+
this.destroyed = true;
|
|
408
420
|
}
|
|
409
421
|
}
|
|
410
422
|
const DEFAULT_OPTIONS = {
|
|
@@ -1552,6 +1564,49 @@ class PluginManager {
|
|
|
1552
1564
|
}
|
|
1553
1565
|
}
|
|
1554
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
|
+
}
|
|
1555
1610
|
class ScarlettPlayer {
|
|
1556
1611
|
/**
|
|
1557
1612
|
* Create a new ScarlettPlayer.
|
|
@@ -1564,6 +1619,11 @@ class ScarlettPlayer {
|
|
|
1564
1619
|
this.seekingWhilePlaying = false;
|
|
1565
1620
|
this.seekResumeTimeout = null;
|
|
1566
1621
|
this.loadGeneration = 0;
|
|
1622
|
+
this.listenersWired = false;
|
|
1623
|
+
this.readyEmitted = false;
|
|
1624
|
+
this.fullscreenAnnounced = false;
|
|
1625
|
+
this.unwireFullscreen = null;
|
|
1626
|
+
this.initializing = null;
|
|
1567
1627
|
if (typeof options.container === "string") {
|
|
1568
1628
|
const el = document.querySelector(options.container);
|
|
1569
1629
|
if (!el || !(el instanceof HTMLElement)) {
|
|
@@ -1604,31 +1664,79 @@ class ScarlettPlayer {
|
|
|
1604
1664
|
this.eventBus.on("media:error", ({ error }) => {
|
|
1605
1665
|
this.errorHandler.record(error, { channel: "media:error" });
|
|
1606
1666
|
});
|
|
1667
|
+
this.wireFullscreenListeners();
|
|
1607
1668
|
if (options.plugins) {
|
|
1608
1669
|
for (const plugin of options.plugins) {
|
|
1609
1670
|
this.pluginManager.register(plugin);
|
|
1610
1671
|
}
|
|
1611
1672
|
}
|
|
1612
|
-
this.logger.info("ScarlettPlayer
|
|
1673
|
+
this.logger.info("ScarlettPlayer constructed", {
|
|
1613
1674
|
autoplay: options.autoplay,
|
|
1614
1675
|
plugins: options.plugins?.length ?? 0
|
|
1615
1676
|
});
|
|
1616
|
-
this.eventBus.emit("player:ready", void 0);
|
|
1617
1677
|
}
|
|
1618
1678
|
/**
|
|
1619
|
-
*
|
|
1620
|
-
*
|
|
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
|
|
1621
1697
|
*/
|
|
1622
|
-
|
|
1623
|
-
this.
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
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);
|
|
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);
|
|
1628
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;
|
|
1629
1736
|
this.eventBus.on("media:load-request", async ({ src, autoplay }) => {
|
|
1630
1737
|
if (this.stateManager.getValue("chromecastActive")) return;
|
|
1631
1738
|
await this.load(src);
|
|
1739
|
+
if (this.destroyed) return;
|
|
1632
1740
|
if (autoplay !== false) {
|
|
1633
1741
|
await this.play();
|
|
1634
1742
|
}
|
|
@@ -1637,23 +1745,50 @@ class ScarlettPlayer {
|
|
|
1637
1745
|
const was_live = this.stateManager.getValue("live");
|
|
1638
1746
|
const resume_at = this.stateManager.getValue("currentTime");
|
|
1639
1747
|
await this.load(src);
|
|
1748
|
+
if (this.destroyed) return;
|
|
1640
1749
|
if (this.stateManager.getValue("error")) return;
|
|
1641
1750
|
if (was_live) {
|
|
1642
1751
|
this.seekToLive();
|
|
1643
1752
|
} else if (resume_at > 0) {
|
|
1644
1753
|
this.seek(resume_at);
|
|
1645
1754
|
}
|
|
1755
|
+
if (this.destroyed) return;
|
|
1646
1756
|
await this.play();
|
|
1647
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();
|
|
1648
1772
|
if (this.initialSrc) {
|
|
1649
1773
|
await this.load(this.initialSrc);
|
|
1650
1774
|
}
|
|
1651
|
-
return Promise.resolve();
|
|
1652
1775
|
}
|
|
1653
1776
|
/**
|
|
1654
1777
|
* Load a media source.
|
|
1655
1778
|
*
|
|
1656
|
-
*
|
|
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.
|
|
1657
1792
|
*
|
|
1658
1793
|
* @param source - Media source URL
|
|
1659
1794
|
* @returns Promise that resolves when source is loaded
|
|
@@ -1685,6 +1820,7 @@ class ScarlettPlayer {
|
|
|
1685
1820
|
await this.pluginManager.destroyPlugin(previousProviderId);
|
|
1686
1821
|
this._currentProvider = null;
|
|
1687
1822
|
}
|
|
1823
|
+
await this.ensureInitialized();
|
|
1688
1824
|
if (generation !== this.loadGeneration) {
|
|
1689
1825
|
this.logger.info("Load superseded by newer load call", { source });
|
|
1690
1826
|
return;
|
|
@@ -1882,6 +2018,31 @@ class ScarlettPlayer {
|
|
|
1882
2018
|
this.stateManager.set("autoplay", autoplay);
|
|
1883
2019
|
this.logger.debug("Autoplay set", { autoplay });
|
|
1884
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
|
+
}
|
|
1885
2046
|
/**
|
|
1886
2047
|
* Subscribe to an event.
|
|
1887
2048
|
*
|
|
@@ -1912,9 +2073,13 @@ class ScarlettPlayer {
|
|
|
1912
2073
|
*
|
|
1913
2074
|
* @example
|
|
1914
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 });
|
|
1915
2079
|
* player.once('player:ready', () => {
|
|
1916
2080
|
* console.log('Player ready!');
|
|
1917
2081
|
* });
|
|
2082
|
+
* await player.init();
|
|
1918
2083
|
* ```
|
|
1919
2084
|
*/
|
|
1920
2085
|
once(event, handler) {
|
|
@@ -2018,36 +2183,83 @@ class ScarlettPlayer {
|
|
|
2018
2183
|
return -1;
|
|
2019
2184
|
}
|
|
2020
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
|
+
}
|
|
2021
2231
|
/**
|
|
2022
2232
|
* Request fullscreen mode.
|
|
2233
|
+
*
|
|
2234
|
+
* @returns Promise resolving once the browser has accepted or refused
|
|
2023
2235
|
*/
|
|
2024
2236
|
async requestFullscreen() {
|
|
2025
2237
|
this.checkDestroyed();
|
|
2238
|
+
this.fullscreenAnnounced = false;
|
|
2026
2239
|
try {
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2240
|
+
await enterFullscreen(this.container);
|
|
2241
|
+
if (!this.fullscreenAnnounced) {
|
|
2242
|
+
this.stateManager.set("fullscreen", true);
|
|
2243
|
+
this.eventBus.emit("fullscreen:change", { fullscreen: true });
|
|
2031
2244
|
}
|
|
2032
|
-
this.stateManager.set("fullscreen", true);
|
|
2033
|
-
this.eventBus.emit("fullscreen:change", { fullscreen: true });
|
|
2034
2245
|
} catch (error) {
|
|
2035
2246
|
this.logger.error("Fullscreen request failed", { error });
|
|
2036
2247
|
}
|
|
2037
2248
|
}
|
|
2038
2249
|
/**
|
|
2039
2250
|
* Exit fullscreen mode.
|
|
2251
|
+
*
|
|
2252
|
+
* @returns Promise resolving once the browser has accepted or refused
|
|
2040
2253
|
*/
|
|
2041
2254
|
async exitFullscreen() {
|
|
2042
2255
|
this.checkDestroyed();
|
|
2256
|
+
this.fullscreenAnnounced = false;
|
|
2043
2257
|
try {
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2258
|
+
await exitFullscreen(this.container);
|
|
2259
|
+
if (!this.fullscreenAnnounced) {
|
|
2260
|
+
this.stateManager.set("fullscreen", false);
|
|
2261
|
+
this.eventBus.emit("fullscreen:change", { fullscreen: false });
|
|
2048
2262
|
}
|
|
2049
|
-
this.stateManager.set("fullscreen", false);
|
|
2050
|
-
this.eventBus.emit("fullscreen:change", { fullscreen: false });
|
|
2051
2263
|
} catch (error) {
|
|
2052
2264
|
this.logger.error("Exit fullscreen failed", { error });
|
|
2053
2265
|
}
|
|
@@ -2140,10 +2352,13 @@ class ScarlettPlayer {
|
|
|
2140
2352
|
return;
|
|
2141
2353
|
}
|
|
2142
2354
|
this.logger.info("Destroying player");
|
|
2355
|
+
this.loadGeneration++;
|
|
2143
2356
|
if (this.seekResumeTimeout !== null) {
|
|
2144
2357
|
clearTimeout(this.seekResumeTimeout);
|
|
2145
2358
|
this.seekResumeTimeout = null;
|
|
2146
2359
|
}
|
|
2360
|
+
this.unwireFullscreen?.();
|
|
2361
|
+
this.unwireFullscreen = null;
|
|
2147
2362
|
this.eventBus.emit("player:destroy", void 0);
|
|
2148
2363
|
this.pluginManager.destroyAll();
|
|
2149
2364
|
this.eventBus.destroy();
|
|
@@ -2224,6 +2439,16 @@ class ScarlettPlayer {
|
|
|
2224
2439
|
get autoplay() {
|
|
2225
2440
|
return this.stateManager.getValue("autoplay");
|
|
2226
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
|
+
}
|
|
2227
2452
|
/**
|
|
2228
2453
|
* Check if player is destroyed.
|
|
2229
2454
|
* @private
|
|
@@ -2287,6 +2512,15 @@ var __export = (target, all) => {
|
|
|
2287
2512
|
for (var name in all)
|
|
2288
2513
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
2289
2514
|
};
|
|
2515
|
+
function sanitizeUrl(url) {
|
|
2516
|
+
if (!url) return void 0;
|
|
2517
|
+
try {
|
|
2518
|
+
const parsed = new URL(url);
|
|
2519
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
2520
|
+
} catch {
|
|
2521
|
+
return void 0;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2290
2524
|
function formatLevel(level) {
|
|
2291
2525
|
if (level.name) {
|
|
2292
2526
|
return level.name;
|
|
@@ -2486,8 +2720,14 @@ function setupVideoEventHandlers(video, api) {
|
|
|
2486
2720
|
video.addEventListener(event, handler);
|
|
2487
2721
|
handlers.push({ event, handler });
|
|
2488
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
|
+
};
|
|
2489
2728
|
addHandler("play", () => {
|
|
2490
2729
|
api.setState("paused", false);
|
|
2730
|
+
syncEndedFromElement();
|
|
2491
2731
|
});
|
|
2492
2732
|
addHandler("playing", () => {
|
|
2493
2733
|
api.setState("playing", true);
|
|
@@ -2495,6 +2735,7 @@ function setupVideoEventHandlers(video, api) {
|
|
|
2495
2735
|
api.setState("waiting", false);
|
|
2496
2736
|
api.setState("buffering", false);
|
|
2497
2737
|
api.setState("playbackState", "playing");
|
|
2738
|
+
syncEndedFromElement();
|
|
2498
2739
|
});
|
|
2499
2740
|
addHandler("pause", () => {
|
|
2500
2741
|
api.setState("playing", false);
|
|
@@ -2549,6 +2790,7 @@ function setupVideoEventHandlers(video, api) {
|
|
|
2549
2790
|
});
|
|
2550
2791
|
addHandler("seeking", () => {
|
|
2551
2792
|
api.setState("seeking", true);
|
|
2793
|
+
syncEndedFromElement();
|
|
2552
2794
|
});
|
|
2553
2795
|
addHandler("seeked", () => {
|
|
2554
2796
|
api.setState("seeking", false);
|
|
@@ -2653,6 +2895,7 @@ function createValidatingPlaylistLoader(Hls) {
|
|
|
2653
2895
|
}
|
|
2654
2896
|
};
|
|
2655
2897
|
}
|
|
2898
|
+
var PKG_VERSION$5 = "1.7.1";
|
|
2656
2899
|
var DEFAULT_CONFIG$3 = {
|
|
2657
2900
|
debug: false,
|
|
2658
2901
|
autoStartLoad: true,
|
|
@@ -2708,6 +2951,12 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2708
2951
|
let reconnectWindowStart = 0;
|
|
2709
2952
|
let reconnectResumePosition = 0;
|
|
2710
2953
|
let onlineListener = null;
|
|
2954
|
+
let reconnectTriggerError = null;
|
|
2955
|
+
let reconnectExhausted = false;
|
|
2956
|
+
const applyPoster = () => {
|
|
2957
|
+
if (!video) return;
|
|
2958
|
+
video.poster = api?.getState("poster") || "";
|
|
2959
|
+
};
|
|
2711
2960
|
const getOrCreateVideo = () => {
|
|
2712
2961
|
if (video) return video;
|
|
2713
2962
|
const existing = api?.container.querySelector("video");
|
|
@@ -2720,10 +2969,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2720
2969
|
video.preload = "metadata";
|
|
2721
2970
|
video.controls = false;
|
|
2722
2971
|
video.playsInline = true;
|
|
2723
|
-
|
|
2724
|
-
if (poster) {
|
|
2725
|
-
video.poster = poster;
|
|
2726
|
-
}
|
|
2972
|
+
applyPoster();
|
|
2727
2973
|
api?.container.appendChild(video);
|
|
2728
2974
|
return video;
|
|
2729
2975
|
};
|
|
@@ -2816,6 +3062,22 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2816
3062
|
return ErrorCode.PLAYBACK_FAILED;
|
|
2817
3063
|
}
|
|
2818
3064
|
};
|
|
3065
|
+
const buildErrorDetail = (error, retriesExhausted) => {
|
|
3066
|
+
const attempts = error.type === "network" ? networkRetryCount : error.type === "media" ? mediaRetryCount : 0;
|
|
3067
|
+
const detail = {
|
|
3068
|
+
type: error.type,
|
|
3069
|
+
retriesExhausted,
|
|
3070
|
+
attempts
|
|
3071
|
+
};
|
|
3072
|
+
if (typeof error.response?.code === "number" && error.response.code > 0) {
|
|
3073
|
+
detail.httpStatus = error.response.code;
|
|
3074
|
+
}
|
|
3075
|
+
const url = sanitizeUrl(error.url);
|
|
3076
|
+
if (url) {
|
|
3077
|
+
detail.url = url;
|
|
3078
|
+
}
|
|
3079
|
+
return detail;
|
|
3080
|
+
};
|
|
2819
3081
|
const emitFatalError = (error, retriesExhausted) => {
|
|
2820
3082
|
const message = retriesExhausted ? `HLS error: ${error.details} (max retries exceeded)` : `HLS error: ${error.details}`;
|
|
2821
3083
|
api?.logger.error(message, { type: error.type, details: error.details });
|
|
@@ -2825,7 +3087,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2825
3087
|
code: mapFatalErrorCode(error),
|
|
2826
3088
|
message,
|
|
2827
3089
|
fatal: true,
|
|
2828
|
-
timestamp: Date.now()
|
|
3090
|
+
timestamp: Date.now(),
|
|
3091
|
+
detail: buildErrorDetail(error, retriesExhausted)
|
|
2829
3092
|
});
|
|
2830
3093
|
maybeScheduleReconnect(error);
|
|
2831
3094
|
};
|
|
@@ -2902,6 +3165,62 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2902
3165
|
}
|
|
2903
3166
|
return false;
|
|
2904
3167
|
};
|
|
3168
|
+
const handleNativeFatalError = (error, resumePosition) => {
|
|
3169
|
+
const is_network = error.type === "network";
|
|
3170
|
+
const max_retries = is_network ? mergedConfig.maxNetworkRetries ?? 3 : mergedConfig.maxMediaRetries ?? 2;
|
|
3171
|
+
const used = is_network ? networkRetryCount : mediaRetryCount;
|
|
3172
|
+
if (!currentSrc || used >= max_retries) {
|
|
3173
|
+
emitFatalError(error, used >= max_retries);
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
3176
|
+
const resume_position = resumePosition ?? video?.currentTime ?? 0;
|
|
3177
|
+
if (is_network) {
|
|
3178
|
+
networkRetryCount++;
|
|
3179
|
+
} else {
|
|
3180
|
+
mediaRetryCount++;
|
|
3181
|
+
}
|
|
3182
|
+
const attempt = used + 1;
|
|
3183
|
+
const delay = getRetryDelay(attempt - 1);
|
|
3184
|
+
api?.logger.info(
|
|
3185
|
+
`Attempting native ${error.type} error recovery (attempt ${attempt}/${max_retries}) in ${delay}ms`
|
|
3186
|
+
);
|
|
3187
|
+
api?.emit(is_network ? "error:network" : "error:media", {
|
|
3188
|
+
error: new Error(error.details)
|
|
3189
|
+
});
|
|
3190
|
+
if (retryTimeout) {
|
|
3191
|
+
clearTimeout(retryTimeout);
|
|
3192
|
+
}
|
|
3193
|
+
const retry_session = loadSession;
|
|
3194
|
+
retryTimeout = setTimeout(() => {
|
|
3195
|
+
if (retry_session !== loadSession) return;
|
|
3196
|
+
void recoverNative(error, resume_position);
|
|
3197
|
+
}, delay);
|
|
3198
|
+
};
|
|
3199
|
+
const recoverNative = async (error, resumePosition) => {
|
|
3200
|
+
if (!currentSrc) return;
|
|
3201
|
+
const session = ++loadSession;
|
|
3202
|
+
const saved_src = currentSrc;
|
|
3203
|
+
const was_live = api?.getState("live") ?? false;
|
|
3204
|
+
try {
|
|
3205
|
+
teardownPipeline(new Error("HLS load cancelled: native error recovery"));
|
|
3206
|
+
api?.setState("playbackState", "loading");
|
|
3207
|
+
await loadNative(saved_src);
|
|
3208
|
+
if (session !== loadSession) return;
|
|
3209
|
+
if (!was_live && video && resumePosition > 0) {
|
|
3210
|
+
video.currentTime = resumePosition;
|
|
3211
|
+
}
|
|
3212
|
+
api?.setState("playbackState", "ready");
|
|
3213
|
+
api?.setState("buffering", false);
|
|
3214
|
+
try {
|
|
3215
|
+
await video?.play();
|
|
3216
|
+
} catch {
|
|
3217
|
+
}
|
|
3218
|
+
} catch {
|
|
3219
|
+
if (session !== loadSession) return;
|
|
3220
|
+
api?.logger.warn("Native error recovery attempt failed");
|
|
3221
|
+
handleNativeFatalError(error, resumePosition);
|
|
3222
|
+
}
|
|
3223
|
+
};
|
|
2905
3224
|
const loadNative = async (src) => {
|
|
2906
3225
|
const session = loadSession;
|
|
2907
3226
|
const videoEl = getOrCreateVideo();
|
|
@@ -2943,12 +3262,24 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2943
3262
|
const media_error = videoEl.error;
|
|
2944
3263
|
const hls_error = {
|
|
2945
3264
|
type: media_error?.code === MediaError.MEDIA_ERR_NETWORK ? "network" : "media",
|
|
2946
|
-
details: media_error?.message || "Native HLS playback error"
|
|
3265
|
+
details: media_error?.message || "Native HLS playback error",
|
|
3266
|
+
fatal: true
|
|
2947
3267
|
};
|
|
2948
|
-
|
|
3268
|
+
handleNativeFatalError(hls_error);
|
|
2949
3269
|
};
|
|
2950
3270
|
videoEl.addEventListener("error", onFatalVideoError);
|
|
2951
|
-
const
|
|
3271
|
+
const onPlayingResetBudget = () => {
|
|
3272
|
+
if (networkRetryCount > 0 || mediaRetryCount > 0) {
|
|
3273
|
+
api?.logger.debug("Native playback recovered, resetting retry budgets");
|
|
3274
|
+
networkRetryCount = 0;
|
|
3275
|
+
mediaRetryCount = 0;
|
|
3276
|
+
}
|
|
3277
|
+
};
|
|
3278
|
+
videoEl.addEventListener("playing", onPlayingResetBudget);
|
|
3279
|
+
const removeFatalListener = () => {
|
|
3280
|
+
videoEl.removeEventListener("error", onFatalVideoError);
|
|
3281
|
+
videoEl.removeEventListener("playing", onPlayingResetBudget);
|
|
3282
|
+
};
|
|
2952
3283
|
const previous_cleanup = cleanupVideoEvents;
|
|
2953
3284
|
cleanupVideoEvents = () => {
|
|
2954
3285
|
removeFatalListener();
|
|
@@ -3073,12 +3404,38 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3073
3404
|
reconnectAttempts = 0;
|
|
3074
3405
|
reconnectWindowStart = 0;
|
|
3075
3406
|
reconnectResumePosition = 0;
|
|
3407
|
+
reconnectTriggerError = null;
|
|
3408
|
+
reconnectExhausted = false;
|
|
3409
|
+
};
|
|
3410
|
+
const emitReconnectExhausted = (elapsedMs, windowMs) => {
|
|
3411
|
+
if (reconnectExhausted) return;
|
|
3412
|
+
reconnectExhausted = true;
|
|
3413
|
+
const attempts = reconnectAttempts;
|
|
3414
|
+
const trigger = reconnectTriggerError;
|
|
3415
|
+
api?.emit("error:reconnect-exhausted", { attempts, elapsedMs, windowMs });
|
|
3416
|
+
api?.setState("playbackState", "error");
|
|
3417
|
+
api?.setState("buffering", false);
|
|
3418
|
+
api?.emit("error", {
|
|
3419
|
+
code: trigger ? mapFatalErrorCode(trigger) : ErrorCode.PLAYBACK_FAILED,
|
|
3420
|
+
message: `HLS auto-reconnect gave up after ${attempts} attempts over ${Math.round(elapsedMs / 1e3)}s`,
|
|
3421
|
+
fatal: true,
|
|
3422
|
+
timestamp: Date.now(),
|
|
3423
|
+
detail: {
|
|
3424
|
+
type: trigger?.type ?? "other",
|
|
3425
|
+
retriesExhausted: true,
|
|
3426
|
+
attempts,
|
|
3427
|
+
reconnectExhausted: true
|
|
3428
|
+
}
|
|
3429
|
+
});
|
|
3076
3430
|
};
|
|
3077
3431
|
const scheduleReconnectAttempt = () => {
|
|
3432
|
+
if (reconnectExhausted) return;
|
|
3078
3433
|
if (reconnectTimer) return;
|
|
3079
3434
|
const window_ms = mergedConfig.reconnectWindowMs ?? 3e5;
|
|
3080
|
-
|
|
3435
|
+
const elapsed_ms = Date.now() - reconnectWindowStart;
|
|
3436
|
+
if (elapsed_ms > window_ms) {
|
|
3081
3437
|
api?.logger.warn(`Auto-reconnect window exhausted after ${reconnectAttempts} attempts`);
|
|
3438
|
+
emitReconnectExhausted(elapsed_ms, window_ms);
|
|
3082
3439
|
return;
|
|
3083
3440
|
}
|
|
3084
3441
|
const base_delay = mergedConfig.reconnectBaseDelayMs ?? 2e3;
|
|
@@ -3086,7 +3443,12 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3086
3443
|
const backoff = Math.min(base_delay * Math.pow(2, reconnectAttempts), max_delay);
|
|
3087
3444
|
const delay = Math.round(backoff * (0.7 + Math.random() * 0.3));
|
|
3088
3445
|
api?.logger.info(`Scheduling auto-reconnect attempt ${reconnectAttempts + 1} in ${delay}ms`);
|
|
3089
|
-
api?.emit("error:reconnecting", {
|
|
3446
|
+
api?.emit("error:reconnecting", {
|
|
3447
|
+
attempt: reconnectAttempts + 1,
|
|
3448
|
+
delayMs: delay,
|
|
3449
|
+
elapsedMs: elapsed_ms,
|
|
3450
|
+
windowMs: window_ms
|
|
3451
|
+
});
|
|
3090
3452
|
reconnectTimer = setTimeout(() => {
|
|
3091
3453
|
reconnectTimer = null;
|
|
3092
3454
|
void attemptReconnect();
|
|
@@ -3099,6 +3461,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3099
3461
|
if (reconnectWindowStart === 0) {
|
|
3100
3462
|
reconnectWindowStart = Date.now();
|
|
3101
3463
|
reconnectResumePosition = video?.currentTime ?? 0;
|
|
3464
|
+
reconnectTriggerError = error;
|
|
3102
3465
|
}
|
|
3103
3466
|
scheduleReconnectAttempt();
|
|
3104
3467
|
};
|
|
@@ -3130,7 +3493,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3130
3493
|
}
|
|
3131
3494
|
api.setState("playbackState", "ready");
|
|
3132
3495
|
api.setState("buffering", false);
|
|
3133
|
-
api.emit("error:recovered",
|
|
3496
|
+
api.emit("error:recovered", {
|
|
3497
|
+
attempt: reconnectAttempts,
|
|
3498
|
+
elapsedMs: Date.now() - reconnectWindowStart
|
|
3499
|
+
});
|
|
3134
3500
|
api.logger.info("Auto-reconnect succeeded");
|
|
3135
3501
|
cancelReconnect();
|
|
3136
3502
|
try {
|
|
@@ -3146,7 +3512,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3146
3512
|
const plugin = {
|
|
3147
3513
|
id: "hls-provider",
|
|
3148
3514
|
name: variant.name,
|
|
3149
|
-
version:
|
|
3515
|
+
version: PKG_VERSION$5,
|
|
3150
3516
|
type: "provider",
|
|
3151
3517
|
description: variant.description,
|
|
3152
3518
|
canPlay(src) {
|
|
@@ -3237,6 +3603,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3237
3603
|
};
|
|
3238
3604
|
window.addEventListener("online", onlineListener);
|
|
3239
3605
|
}
|
|
3606
|
+
const unsubPoster = api.subscribeToState((event) => {
|
|
3607
|
+
if (event.key === "poster") applyPoster();
|
|
3608
|
+
});
|
|
3240
3609
|
api.onDestroy(() => {
|
|
3241
3610
|
unsubPlay();
|
|
3242
3611
|
unsubPause();
|
|
@@ -3245,6 +3614,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3245
3614
|
unsubMute();
|
|
3246
3615
|
unsubRate();
|
|
3247
3616
|
unsubQuality();
|
|
3617
|
+
unsubPoster();
|
|
3248
3618
|
});
|
|
3249
3619
|
},
|
|
3250
3620
|
async destroy() {
|
|
@@ -3270,6 +3640,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3270
3640
|
hasPlayedContent = false;
|
|
3271
3641
|
cleanup(new Error("HLS load cancelled: superseded by a new load"));
|
|
3272
3642
|
currentSrc = src;
|
|
3643
|
+
applyPoster();
|
|
3273
3644
|
api.setState("playbackState", "loading");
|
|
3274
3645
|
api.setState("buffering", true);
|
|
3275
3646
|
if (api.getState("airplayActive") && loader.supportsNativeHLS()) {
|
|
@@ -3404,8 +3775,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3404
3775
|
};
|
|
3405
3776
|
return plugin;
|
|
3406
3777
|
}
|
|
3407
|
-
var
|
|
3408
|
-
__export(
|
|
3778
|
+
var hls_loader_light_exports = {};
|
|
3779
|
+
__export(hls_loader_light_exports, {
|
|
3409
3780
|
createHlsInstance: () => createHlsInstance,
|
|
3410
3781
|
getHlsConstructor: () => getHlsConstructor,
|
|
3411
3782
|
isHLSSupported: () => isHLSSupported,
|
|
@@ -3448,7 +3819,7 @@ async function loadHlsJs() {
|
|
|
3448
3819
|
}
|
|
3449
3820
|
loadingPromise = (async () => {
|
|
3450
3821
|
try {
|
|
3451
|
-
const hlsModule = await import("./hls.js");
|
|
3822
|
+
const hlsModule = await import("./hls.light.js");
|
|
3452
3823
|
hlsConstructor = hlsModule.default;
|
|
3453
3824
|
if (!hlsConstructor.isSupported()) {
|
|
3454
3825
|
throw new Error("hls.js is not supported in this browser");
|
|
@@ -3478,16 +3849,396 @@ function resetLoader() {
|
|
|
3478
3849
|
}
|
|
3479
3850
|
function createHLSPlugin(config) {
|
|
3480
3851
|
return createHLSPluginWith(
|
|
3481
|
-
|
|
3852
|
+
hls_loader_light_exports,
|
|
3482
3853
|
{
|
|
3483
|
-
name: "HLS Provider",
|
|
3484
|
-
description: "HLS playback provider using hls.js",
|
|
3485
|
-
logSuffix: "",
|
|
3486
|
-
engineLabel: "hls.js"
|
|
3854
|
+
name: "HLS Provider (Light)",
|
|
3855
|
+
description: "HLS playback provider using hls.js/light (smaller bundle)",
|
|
3856
|
+
logSuffix: " (light)",
|
|
3857
|
+
engineLabel: "hls.js/light"
|
|
3487
3858
|
},
|
|
3488
3859
|
config
|
|
3489
3860
|
);
|
|
3490
3861
|
}
|
|
3862
|
+
var PKG_VERSION$4 = "1.7.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$4,
|
|
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((resolve, 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
|
+
resolve();
|
|
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 PKG_VERSION$3 = "1.7.1";
|
|
3491
4242
|
var DEFAULT_THEME = {
|
|
3492
4243
|
primary: "#6366f1",
|
|
3493
4244
|
background: "#18181b",
|
|
@@ -4114,7 +4865,7 @@ function createAudioUIPlugin(config) {
|
|
|
4114
4865
|
const plugin = {
|
|
4115
4866
|
id: "audio-ui",
|
|
4116
4867
|
name: "Audio UI",
|
|
4117
|
-
version:
|
|
4868
|
+
version: PKG_VERSION$3,
|
|
4118
4869
|
type: "ui",
|
|
4119
4870
|
description: "Compact audio player interface",
|
|
4120
4871
|
async init(pluginApi) {
|
|
@@ -4486,6 +5237,7 @@ function injectStyles() {
|
|
|
4486
5237
|
document.head.appendChild(el);
|
|
4487
5238
|
return el;
|
|
4488
5239
|
}
|
|
5240
|
+
var PKG_VERSION$2 = "1.7.1";
|
|
4489
5241
|
var DEFAULT_CONFIG$1 = {
|
|
4490
5242
|
autoAdvance: true,
|
|
4491
5243
|
preloadNext: true,
|
|
@@ -4634,9 +5386,7 @@ function createPlaylistPlugin(config) {
|
|
|
4634
5386
|
currentIndex = index;
|
|
4635
5387
|
api?.logger.info("Track changed", { index, title: track.title, src: track.src });
|
|
4636
5388
|
api?.setState("title", track.title || "");
|
|
4637
|
-
|
|
4638
|
-
api?.setState("poster", track.artwork);
|
|
4639
|
-
}
|
|
5389
|
+
api?.setState("poster", track.artwork || "");
|
|
4640
5390
|
api?.setState("mediaType", track.type || "audio");
|
|
4641
5391
|
emitChange();
|
|
4642
5392
|
if (mergedConfig.autoLoad !== false && track.src) {
|
|
@@ -4646,7 +5396,7 @@ function createPlaylistPlugin(config) {
|
|
|
4646
5396
|
const plugin = {
|
|
4647
5397
|
id: "playlist",
|
|
4648
5398
|
name: "Playlist",
|
|
4649
|
-
version:
|
|
5399
|
+
version: PKG_VERSION$2,
|
|
4650
5400
|
type: "feature",
|
|
4651
5401
|
description: "Playlist management with shuffle, repeat, and gapless playback",
|
|
4652
5402
|
async init(pluginApi) {
|
|
@@ -4676,7 +5426,7 @@ function createPlaylistPlugin(config) {
|
|
|
4676
5426
|
}
|
|
4677
5427
|
});
|
|
4678
5428
|
injectStyles();
|
|
4679
|
-
void import("./
|
|
5429
|
+
void import("./embed.audio.index.js").then(({ registerControl }) => {
|
|
4680
5430
|
const self = plugin;
|
|
4681
5431
|
registerControl(
|
|
4682
5432
|
"playlist-previous",
|
|
@@ -4902,6 +5652,7 @@ function createPlaylistPlugin(config) {
|
|
|
4902
5652
|
};
|
|
4903
5653
|
return plugin;
|
|
4904
5654
|
}
|
|
5655
|
+
var PKG_VERSION$1 = "1.7.1";
|
|
4905
5656
|
var DEFAULT_CONFIG = {
|
|
4906
5657
|
enablePlayPause: true,
|
|
4907
5658
|
enableSeek: true,
|
|
@@ -5049,7 +5800,7 @@ function createMediaSessionPlugin(config) {
|
|
|
5049
5800
|
const plugin = {
|
|
5050
5801
|
id: "media-session",
|
|
5051
5802
|
name: "Media Session",
|
|
5052
|
-
version:
|
|
5803
|
+
version: PKG_VERSION$1,
|
|
5053
5804
|
type: "feature",
|
|
5054
5805
|
description: "Media Session API integration for system-level media controls",
|
|
5055
5806
|
async init(pluginApi) {
|
|
@@ -5189,6 +5940,14 @@ function parseDataAttributes(element) {
|
|
|
5189
5940
|
if (controls !== null) {
|
|
5190
5941
|
config.controls = controls !== "false";
|
|
5191
5942
|
}
|
|
5943
|
+
const bigPlayButton = getAttr(element, "data-big-play-button", "big-play-button");
|
|
5944
|
+
if (bigPlayButton !== null) {
|
|
5945
|
+
config.bigPlayButton = bigPlayButton !== "false";
|
|
5946
|
+
}
|
|
5947
|
+
const gestures = getAttr(element, "data-gestures", "gestures");
|
|
5948
|
+
if (gestures !== null) {
|
|
5949
|
+
config.gestures = gestures !== "false";
|
|
5950
|
+
}
|
|
5192
5951
|
const keyboard = getAttr(element, "data-keyboard", "keyboard");
|
|
5193
5952
|
if (keyboard !== null) {
|
|
5194
5953
|
config.keyboard = keyboard !== "false";
|
|
@@ -5338,6 +6097,9 @@ async function createEmbedPlayer(container, config, pluginCreators2, availableTy
|
|
|
5338
6097
|
if (config.primaryColor) theme.primaryColor = config.primaryColor;
|
|
5339
6098
|
if (config.backgroundColor) theme.backgroundColor = config.backgroundColor;
|
|
5340
6099
|
const plugins = [pluginCreators2.hls()];
|
|
6100
|
+
if (pluginCreators2.native) {
|
|
6101
|
+
plugins.push(pluginCreators2.native());
|
|
6102
|
+
}
|
|
5341
6103
|
if (pluginCreators2.playlist && config.playlist?.length) {
|
|
5342
6104
|
plugins.push(pluginCreators2.playlist({
|
|
5343
6105
|
items: config.playlist.map((item, index) => ({
|
|
@@ -5364,6 +6126,9 @@ async function createEmbedPlayer(container, config, pluginCreators2, availableTy
|
|
|
5364
6126
|
if (pluginCreators2.captions) {
|
|
5365
6127
|
plugins.push(pluginCreators2.captions(config.captions || {}));
|
|
5366
6128
|
}
|
|
6129
|
+
if (type === "video" && pluginCreators2.gestures && config.gestures !== false) {
|
|
6130
|
+
plugins.push(pluginCreators2.gestures({}));
|
|
6131
|
+
}
|
|
5367
6132
|
if (pluginCreators2.analytics && config.analytics?.beaconUrl) {
|
|
5368
6133
|
plugins.push(pluginCreators2.analytics({
|
|
5369
6134
|
beaconUrl: config.analytics.beaconUrl,
|
|
@@ -5376,6 +6141,7 @@ async function createEmbedPlayer(container, config, pluginCreators2, availableTy
|
|
|
5376
6141
|
const uiConfig = {};
|
|
5377
6142
|
if (Object.keys(theme).length > 0) uiConfig.theme = theme;
|
|
5378
6143
|
if (config.hideDelay !== void 0) uiConfig.hideDelay = config.hideDelay;
|
|
6144
|
+
if (config.bigPlayButton !== void 0) uiConfig.bigPlayButton = config.bigPlayButton;
|
|
5379
6145
|
plugins.push(pluginCreators2.videoUI(uiConfig));
|
|
5380
6146
|
} else if ((type === "audio" || type === "audio-mini") && pluginCreators2.audioUI) {
|
|
5381
6147
|
plugins.push(pluginCreators2.audioUI({
|
|
@@ -5481,10 +6247,12 @@ function setupAutoInit(pluginCreators2, availableTypes) {
|
|
|
5481
6247
|
}
|
|
5482
6248
|
}
|
|
5483
6249
|
}
|
|
5484
|
-
const
|
|
6250
|
+
const PKG_VERSION = "1.7.1";
|
|
6251
|
+
const VERSION = `${PKG_VERSION}-audio`;
|
|
5485
6252
|
const AVAILABLE_TYPES = ["audio", "audio-mini"];
|
|
5486
6253
|
const pluginCreators = {
|
|
5487
6254
|
hls: createHLSPlugin,
|
|
6255
|
+
native: createNativePlugin,
|
|
5488
6256
|
audioUI: createAudioUIPlugin,
|
|
5489
6257
|
playlist: createPlaylistPlugin,
|
|
5490
6258
|
mediaSession: createMediaSessionPlugin
|