@scarlett-player/embed 1.7.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 +666 -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 +1956 -78
- 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 +1938 -66
- 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
|
@@ -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);
|
|
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);
|
|
1640
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) {
|
|
@@ -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
|
|
@@ -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$5 = "1.7.1";
|
|
2681
2899
|
var DEFAULT_CONFIG$3 = {
|
|
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
|
};
|
|
@@ -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$5,
|
|
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()) {
|
|
@@ -3551,8 +3775,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3551
3775
|
};
|
|
3552
3776
|
return plugin;
|
|
3553
3777
|
}
|
|
3554
|
-
var
|
|
3555
|
-
__export(
|
|
3778
|
+
var hls_loader_light_exports = {};
|
|
3779
|
+
__export(hls_loader_light_exports, {
|
|
3556
3780
|
createHlsInstance: () => createHlsInstance,
|
|
3557
3781
|
getHlsConstructor: () => getHlsConstructor,
|
|
3558
3782
|
isHLSSupported: () => isHLSSupported,
|
|
@@ -3595,7 +3819,7 @@ async function loadHlsJs() {
|
|
|
3595
3819
|
}
|
|
3596
3820
|
loadingPromise = (async () => {
|
|
3597
3821
|
try {
|
|
3598
|
-
const hlsModule = await import("./hls.js");
|
|
3822
|
+
const hlsModule = await import("./hls.light.js");
|
|
3599
3823
|
hlsConstructor = hlsModule.default;
|
|
3600
3824
|
if (!hlsConstructor.isSupported()) {
|
|
3601
3825
|
throw new Error("hls.js is not supported in this browser");
|
|
@@ -3625,16 +3849,396 @@ function resetLoader() {
|
|
|
3625
3849
|
}
|
|
3626
3850
|
function createHLSPlugin(config) {
|
|
3627
3851
|
return createHLSPluginWith(
|
|
3628
|
-
|
|
3852
|
+
hls_loader_light_exports,
|
|
3629
3853
|
{
|
|
3630
|
-
name: "HLS Provider",
|
|
3631
|
-
description: "HLS playback provider using hls.js",
|
|
3632
|
-
logSuffix: "",
|
|
3633
|
-
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"
|
|
3634
3858
|
},
|
|
3635
3859
|
config
|
|
3636
3860
|
);
|
|
3637
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";
|
|
3638
4242
|
var DEFAULT_THEME = {
|
|
3639
4243
|
primary: "#6366f1",
|
|
3640
4244
|
background: "#18181b",
|
|
@@ -4261,7 +4865,7 @@ function createAudioUIPlugin(config) {
|
|
|
4261
4865
|
const plugin = {
|
|
4262
4866
|
id: "audio-ui",
|
|
4263
4867
|
name: "Audio UI",
|
|
4264
|
-
version:
|
|
4868
|
+
version: PKG_VERSION$3,
|
|
4265
4869
|
type: "ui",
|
|
4266
4870
|
description: "Compact audio player interface",
|
|
4267
4871
|
async init(pluginApi) {
|
|
@@ -4633,6 +5237,7 @@ function injectStyles() {
|
|
|
4633
5237
|
document.head.appendChild(el);
|
|
4634
5238
|
return el;
|
|
4635
5239
|
}
|
|
5240
|
+
var PKG_VERSION$2 = "1.7.1";
|
|
4636
5241
|
var DEFAULT_CONFIG$1 = {
|
|
4637
5242
|
autoAdvance: true,
|
|
4638
5243
|
preloadNext: true,
|
|
@@ -4781,9 +5386,7 @@ function createPlaylistPlugin(config) {
|
|
|
4781
5386
|
currentIndex = index;
|
|
4782
5387
|
api?.logger.info("Track changed", { index, title: track.title, src: track.src });
|
|
4783
5388
|
api?.setState("title", track.title || "");
|
|
4784
|
-
|
|
4785
|
-
api?.setState("poster", track.artwork);
|
|
4786
|
-
}
|
|
5389
|
+
api?.setState("poster", track.artwork || "");
|
|
4787
5390
|
api?.setState("mediaType", track.type || "audio");
|
|
4788
5391
|
emitChange();
|
|
4789
5392
|
if (mergedConfig.autoLoad !== false && track.src) {
|
|
@@ -4793,7 +5396,7 @@ function createPlaylistPlugin(config) {
|
|
|
4793
5396
|
const plugin = {
|
|
4794
5397
|
id: "playlist",
|
|
4795
5398
|
name: "Playlist",
|
|
4796
|
-
version:
|
|
5399
|
+
version: PKG_VERSION$2,
|
|
4797
5400
|
type: "feature",
|
|
4798
5401
|
description: "Playlist management with shuffle, repeat, and gapless playback",
|
|
4799
5402
|
async init(pluginApi) {
|
|
@@ -4823,7 +5426,7 @@ function createPlaylistPlugin(config) {
|
|
|
4823
5426
|
}
|
|
4824
5427
|
});
|
|
4825
5428
|
injectStyles();
|
|
4826
|
-
void import("./
|
|
5429
|
+
void import("./embed.audio.index.js").then(({ registerControl }) => {
|
|
4827
5430
|
const self = plugin;
|
|
4828
5431
|
registerControl(
|
|
4829
5432
|
"playlist-previous",
|
|
@@ -5049,6 +5652,7 @@ function createPlaylistPlugin(config) {
|
|
|
5049
5652
|
};
|
|
5050
5653
|
return plugin;
|
|
5051
5654
|
}
|
|
5655
|
+
var PKG_VERSION$1 = "1.7.1";
|
|
5052
5656
|
var DEFAULT_CONFIG = {
|
|
5053
5657
|
enablePlayPause: true,
|
|
5054
5658
|
enableSeek: true,
|
|
@@ -5196,7 +5800,7 @@ function createMediaSessionPlugin(config) {
|
|
|
5196
5800
|
const plugin = {
|
|
5197
5801
|
id: "media-session",
|
|
5198
5802
|
name: "Media Session",
|
|
5199
|
-
version:
|
|
5803
|
+
version: PKG_VERSION$1,
|
|
5200
5804
|
type: "feature",
|
|
5201
5805
|
description: "Media Session API integration for system-level media controls",
|
|
5202
5806
|
async init(pluginApi) {
|
|
@@ -5336,6 +5940,14 @@ function parseDataAttributes(element) {
|
|
|
5336
5940
|
if (controls !== null) {
|
|
5337
5941
|
config.controls = controls !== "false";
|
|
5338
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
|
+
}
|
|
5339
5951
|
const keyboard = getAttr(element, "data-keyboard", "keyboard");
|
|
5340
5952
|
if (keyboard !== null) {
|
|
5341
5953
|
config.keyboard = keyboard !== "false";
|
|
@@ -5485,6 +6097,9 @@ async function createEmbedPlayer(container, config, pluginCreators2, availableTy
|
|
|
5485
6097
|
if (config.primaryColor) theme.primaryColor = config.primaryColor;
|
|
5486
6098
|
if (config.backgroundColor) theme.backgroundColor = config.backgroundColor;
|
|
5487
6099
|
const plugins = [pluginCreators2.hls()];
|
|
6100
|
+
if (pluginCreators2.native) {
|
|
6101
|
+
plugins.push(pluginCreators2.native());
|
|
6102
|
+
}
|
|
5488
6103
|
if (pluginCreators2.playlist && config.playlist?.length) {
|
|
5489
6104
|
plugins.push(pluginCreators2.playlist({
|
|
5490
6105
|
items: config.playlist.map((item, index) => ({
|
|
@@ -5511,6 +6126,9 @@ async function createEmbedPlayer(container, config, pluginCreators2, availableTy
|
|
|
5511
6126
|
if (pluginCreators2.captions) {
|
|
5512
6127
|
plugins.push(pluginCreators2.captions(config.captions || {}));
|
|
5513
6128
|
}
|
|
6129
|
+
if (type === "video" && pluginCreators2.gestures && config.gestures !== false) {
|
|
6130
|
+
plugins.push(pluginCreators2.gestures({}));
|
|
6131
|
+
}
|
|
5514
6132
|
if (pluginCreators2.analytics && config.analytics?.beaconUrl) {
|
|
5515
6133
|
plugins.push(pluginCreators2.analytics({
|
|
5516
6134
|
beaconUrl: config.analytics.beaconUrl,
|
|
@@ -5523,6 +6141,7 @@ async function createEmbedPlayer(container, config, pluginCreators2, availableTy
|
|
|
5523
6141
|
const uiConfig = {};
|
|
5524
6142
|
if (Object.keys(theme).length > 0) uiConfig.theme = theme;
|
|
5525
6143
|
if (config.hideDelay !== void 0) uiConfig.hideDelay = config.hideDelay;
|
|
6144
|
+
if (config.bigPlayButton !== void 0) uiConfig.bigPlayButton = config.bigPlayButton;
|
|
5526
6145
|
plugins.push(pluginCreators2.videoUI(uiConfig));
|
|
5527
6146
|
} else if ((type === "audio" || type === "audio-mini") && pluginCreators2.audioUI) {
|
|
5528
6147
|
plugins.push(pluginCreators2.audioUI({
|
|
@@ -5628,10 +6247,12 @@ function setupAutoInit(pluginCreators2, availableTypes) {
|
|
|
5628
6247
|
}
|
|
5629
6248
|
}
|
|
5630
6249
|
}
|
|
5631
|
-
const
|
|
6250
|
+
const PKG_VERSION = "1.7.1";
|
|
6251
|
+
const VERSION = `${PKG_VERSION}-audio`;
|
|
5632
6252
|
const AVAILABLE_TYPES = ["audio", "audio-mini"];
|
|
5633
6253
|
const pluginCreators = {
|
|
5634
6254
|
hls: createHLSPlugin,
|
|
6255
|
+
native: createNativePlugin,
|
|
5635
6256
|
audioUI: createAudioUIPlugin,
|
|
5636
6257
|
playlist: createPlaylistPlugin,
|
|
5637
6258
|
mediaSession: createMediaSessionPlugin
|