@scarlett-player/embed 1.5.1 → 1.7.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/dist/embed.audio.js +454 -7
- 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 +638 -59
- 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 +270 -7
- 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/hls2.js +12 -0
- package/dist/hls2.js.map +1 -0
- package/package.json +10 -10
package/dist/embed.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 = {
|
|
@@ -920,9 +932,9 @@ class Logger {
|
|
|
920
932
|
* ```
|
|
921
933
|
*/
|
|
922
934
|
removeHandler(handler) {
|
|
923
|
-
const
|
|
924
|
-
if (
|
|
925
|
-
this.handlers.splice(
|
|
935
|
+
const index2 = this.handlers.indexOf(handler);
|
|
936
|
+
if (index2 !== -1) {
|
|
937
|
+
this.handlers.splice(index2, 1);
|
|
926
938
|
}
|
|
927
939
|
}
|
|
928
940
|
/**
|
|
@@ -1629,6 +1641,7 @@ class ScarlettPlayer {
|
|
|
1629
1641
|
this.eventBus.on("media:load-request", async ({ src, autoplay }) => {
|
|
1630
1642
|
if (this.stateManager.getValue("chromecastActive")) return;
|
|
1631
1643
|
await this.load(src);
|
|
1644
|
+
if (this.destroyed) return;
|
|
1632
1645
|
if (autoplay !== false) {
|
|
1633
1646
|
await this.play();
|
|
1634
1647
|
}
|
|
@@ -1637,12 +1650,14 @@ class ScarlettPlayer {
|
|
|
1637
1650
|
const was_live = this.stateManager.getValue("live");
|
|
1638
1651
|
const resume_at = this.stateManager.getValue("currentTime");
|
|
1639
1652
|
await this.load(src);
|
|
1653
|
+
if (this.destroyed) return;
|
|
1640
1654
|
if (this.stateManager.getValue("error")) return;
|
|
1641
1655
|
if (was_live) {
|
|
1642
1656
|
this.seekToLive();
|
|
1643
1657
|
} else if (resume_at > 0) {
|
|
1644
1658
|
this.seek(resume_at);
|
|
1645
1659
|
}
|
|
1660
|
+
if (this.destroyed) return;
|
|
1646
1661
|
await this.play();
|
|
1647
1662
|
});
|
|
1648
1663
|
if (this.initialSrc) {
|
|
@@ -1983,7 +1998,7 @@ class ScarlettPlayer {
|
|
|
1983
1998
|
* Set quality level (-1 for auto).
|
|
1984
1999
|
* @param index - Quality level index
|
|
1985
2000
|
*/
|
|
1986
|
-
setQuality(
|
|
2001
|
+
setQuality(index2) {
|
|
1987
2002
|
this.checkDestroyed();
|
|
1988
2003
|
if (!this._currentProvider) {
|
|
1989
2004
|
this.logger.warn("No provider available for quality change");
|
|
@@ -1991,17 +2006,17 @@ class ScarlettPlayer {
|
|
|
1991
2006
|
}
|
|
1992
2007
|
const provider = this._currentProvider;
|
|
1993
2008
|
if (typeof provider.setLevel === "function") {
|
|
1994
|
-
if (
|
|
2009
|
+
if (index2 !== -1) {
|
|
1995
2010
|
const levels = this.getQualities();
|
|
1996
|
-
if (levels.length > 0 && (
|
|
1997
|
-
this.logger.warn(`Invalid quality index: ${
|
|
2011
|
+
if (levels.length > 0 && (index2 < 0 || index2 >= levels.length)) {
|
|
2012
|
+
this.logger.warn(`Invalid quality index: ${index2} (available: ${levels.length})`);
|
|
1998
2013
|
return;
|
|
1999
2014
|
}
|
|
2000
2015
|
}
|
|
2001
|
-
provider.setLevel(
|
|
2016
|
+
provider.setLevel(index2);
|
|
2002
2017
|
this.eventBus.emit("quality:change", {
|
|
2003
|
-
quality:
|
|
2004
|
-
auto:
|
|
2018
|
+
quality: index2 === -1 ? "auto" : `level-${index2}`,
|
|
2019
|
+
auto: index2 === -1
|
|
2005
2020
|
});
|
|
2006
2021
|
}
|
|
2007
2022
|
}
|
|
@@ -2140,6 +2155,7 @@ class ScarlettPlayer {
|
|
|
2140
2155
|
return;
|
|
2141
2156
|
}
|
|
2142
2157
|
this.logger.info("Destroying player");
|
|
2158
|
+
this.loadGeneration++;
|
|
2143
2159
|
if (this.seekResumeTimeout !== null) {
|
|
2144
2160
|
clearTimeout(this.seekResumeTimeout);
|
|
2145
2161
|
this.seekResumeTimeout = null;
|
|
@@ -2287,6 +2303,15 @@ var __export = (target, all) => {
|
|
|
2287
2303
|
for (var name in all)
|
|
2288
2304
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
2289
2305
|
};
|
|
2306
|
+
function sanitizeUrl(url) {
|
|
2307
|
+
if (!url) return void 0;
|
|
2308
|
+
try {
|
|
2309
|
+
const parsed = new URL(url);
|
|
2310
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
2311
|
+
} catch {
|
|
2312
|
+
return void 0;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2290
2315
|
function formatLevel(level) {
|
|
2291
2316
|
if (level.name) {
|
|
2292
2317
|
return level.name;
|
|
@@ -2323,8 +2348,8 @@ function formatBitrate(bitrate) {
|
|
|
2323
2348
|
return `${bitrate} bps`;
|
|
2324
2349
|
}
|
|
2325
2350
|
function mapLevels(levels, _currentLevel) {
|
|
2326
|
-
return levels.map((level,
|
|
2327
|
-
index,
|
|
2351
|
+
return levels.map((level, index2) => ({
|
|
2352
|
+
index: index2,
|
|
2328
2353
|
width: level.width || 0,
|
|
2329
2354
|
height: level.height || 0,
|
|
2330
2355
|
bitrate: level.bitrate || 0,
|
|
@@ -2379,13 +2404,13 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
2379
2404
|
};
|
|
2380
2405
|
addHandler("hlsManifestParsed", (_event, data) => {
|
|
2381
2406
|
api.logger.debug("HLS manifest parsed", { levels: data.levels.length });
|
|
2382
|
-
const levels = data.levels.map((level,
|
|
2383
|
-
id: `level-${
|
|
2407
|
+
const levels = data.levels.map((level, index2) => ({
|
|
2408
|
+
id: `level-${index2}`,
|
|
2384
2409
|
label: formatLevel(level),
|
|
2385
2410
|
width: level.width,
|
|
2386
2411
|
height: level.height,
|
|
2387
2412
|
bitrate: level.bitrate,
|
|
2388
|
-
active:
|
|
2413
|
+
active: index2 === hls.currentLevel
|
|
2389
2414
|
}));
|
|
2390
2415
|
api.setState("qualities", levels);
|
|
2391
2416
|
api.emit("quality:levels", {
|
|
@@ -2708,6 +2733,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2708
2733
|
let reconnectWindowStart = 0;
|
|
2709
2734
|
let reconnectResumePosition = 0;
|
|
2710
2735
|
let onlineListener = null;
|
|
2736
|
+
let reconnectTriggerError = null;
|
|
2737
|
+
let reconnectExhausted = false;
|
|
2711
2738
|
const getOrCreateVideo = () => {
|
|
2712
2739
|
if (video) return video;
|
|
2713
2740
|
const existing = api?.container.querySelector("video");
|
|
@@ -2816,6 +2843,22 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2816
2843
|
return ErrorCode.PLAYBACK_FAILED;
|
|
2817
2844
|
}
|
|
2818
2845
|
};
|
|
2846
|
+
const buildErrorDetail = (error, retriesExhausted) => {
|
|
2847
|
+
const attempts = error.type === "network" ? networkRetryCount : error.type === "media" ? mediaRetryCount : 0;
|
|
2848
|
+
const detail = {
|
|
2849
|
+
type: error.type,
|
|
2850
|
+
retriesExhausted,
|
|
2851
|
+
attempts
|
|
2852
|
+
};
|
|
2853
|
+
if (typeof error.response?.code === "number" && error.response.code > 0) {
|
|
2854
|
+
detail.httpStatus = error.response.code;
|
|
2855
|
+
}
|
|
2856
|
+
const url = sanitizeUrl(error.url);
|
|
2857
|
+
if (url) {
|
|
2858
|
+
detail.url = url;
|
|
2859
|
+
}
|
|
2860
|
+
return detail;
|
|
2861
|
+
};
|
|
2819
2862
|
const emitFatalError = (error, retriesExhausted) => {
|
|
2820
2863
|
const message = retriesExhausted ? `HLS error: ${error.details} (max retries exceeded)` : `HLS error: ${error.details}`;
|
|
2821
2864
|
api?.logger.error(message, { type: error.type, details: error.details });
|
|
@@ -2825,7 +2868,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2825
2868
|
code: mapFatalErrorCode(error),
|
|
2826
2869
|
message,
|
|
2827
2870
|
fatal: true,
|
|
2828
|
-
timestamp: Date.now()
|
|
2871
|
+
timestamp: Date.now(),
|
|
2872
|
+
detail: buildErrorDetail(error, retriesExhausted)
|
|
2829
2873
|
});
|
|
2830
2874
|
maybeScheduleReconnect(error);
|
|
2831
2875
|
};
|
|
@@ -2902,6 +2946,62 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2902
2946
|
}
|
|
2903
2947
|
return false;
|
|
2904
2948
|
};
|
|
2949
|
+
const handleNativeFatalError = (error, resumePosition) => {
|
|
2950
|
+
const is_network = error.type === "network";
|
|
2951
|
+
const max_retries = is_network ? mergedConfig.maxNetworkRetries ?? 3 : mergedConfig.maxMediaRetries ?? 2;
|
|
2952
|
+
const used = is_network ? networkRetryCount : mediaRetryCount;
|
|
2953
|
+
if (!currentSrc || used >= max_retries) {
|
|
2954
|
+
emitFatalError(error, used >= max_retries);
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
const resume_position = resumePosition ?? video?.currentTime ?? 0;
|
|
2958
|
+
if (is_network) {
|
|
2959
|
+
networkRetryCount++;
|
|
2960
|
+
} else {
|
|
2961
|
+
mediaRetryCount++;
|
|
2962
|
+
}
|
|
2963
|
+
const attempt = used + 1;
|
|
2964
|
+
const delay = getRetryDelay(attempt - 1);
|
|
2965
|
+
api?.logger.info(
|
|
2966
|
+
`Attempting native ${error.type} error recovery (attempt ${attempt}/${max_retries}) in ${delay}ms`
|
|
2967
|
+
);
|
|
2968
|
+
api?.emit(is_network ? "error:network" : "error:media", {
|
|
2969
|
+
error: new Error(error.details)
|
|
2970
|
+
});
|
|
2971
|
+
if (retryTimeout) {
|
|
2972
|
+
clearTimeout(retryTimeout);
|
|
2973
|
+
}
|
|
2974
|
+
const retry_session = loadSession;
|
|
2975
|
+
retryTimeout = setTimeout(() => {
|
|
2976
|
+
if (retry_session !== loadSession) return;
|
|
2977
|
+
void recoverNative(error, resume_position);
|
|
2978
|
+
}, delay);
|
|
2979
|
+
};
|
|
2980
|
+
const recoverNative = async (error, resumePosition) => {
|
|
2981
|
+
if (!currentSrc) return;
|
|
2982
|
+
const session = ++loadSession;
|
|
2983
|
+
const saved_src = currentSrc;
|
|
2984
|
+
const was_live = api?.getState("live") ?? false;
|
|
2985
|
+
try {
|
|
2986
|
+
teardownPipeline(new Error("HLS load cancelled: native error recovery"));
|
|
2987
|
+
api?.setState("playbackState", "loading");
|
|
2988
|
+
await loadNative(saved_src);
|
|
2989
|
+
if (session !== loadSession) return;
|
|
2990
|
+
if (!was_live && video && resumePosition > 0) {
|
|
2991
|
+
video.currentTime = resumePosition;
|
|
2992
|
+
}
|
|
2993
|
+
api?.setState("playbackState", "ready");
|
|
2994
|
+
api?.setState("buffering", false);
|
|
2995
|
+
try {
|
|
2996
|
+
await video?.play();
|
|
2997
|
+
} catch {
|
|
2998
|
+
}
|
|
2999
|
+
} catch {
|
|
3000
|
+
if (session !== loadSession) return;
|
|
3001
|
+
api?.logger.warn("Native error recovery attempt failed");
|
|
3002
|
+
handleNativeFatalError(error, resumePosition);
|
|
3003
|
+
}
|
|
3004
|
+
};
|
|
2905
3005
|
const loadNative = async (src) => {
|
|
2906
3006
|
const session = loadSession;
|
|
2907
3007
|
const videoEl = getOrCreateVideo();
|
|
@@ -2943,12 +3043,24 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2943
3043
|
const media_error = videoEl.error;
|
|
2944
3044
|
const hls_error = {
|
|
2945
3045
|
type: media_error?.code === MediaError.MEDIA_ERR_NETWORK ? "network" : "media",
|
|
2946
|
-
details: media_error?.message || "Native HLS playback error"
|
|
3046
|
+
details: media_error?.message || "Native HLS playback error",
|
|
3047
|
+
fatal: true
|
|
2947
3048
|
};
|
|
2948
|
-
|
|
3049
|
+
handleNativeFatalError(hls_error);
|
|
2949
3050
|
};
|
|
2950
3051
|
videoEl.addEventListener("error", onFatalVideoError);
|
|
2951
|
-
const
|
|
3052
|
+
const onPlayingResetBudget = () => {
|
|
3053
|
+
if (networkRetryCount > 0 || mediaRetryCount > 0) {
|
|
3054
|
+
api?.logger.debug("Native playback recovered, resetting retry budgets");
|
|
3055
|
+
networkRetryCount = 0;
|
|
3056
|
+
mediaRetryCount = 0;
|
|
3057
|
+
}
|
|
3058
|
+
};
|
|
3059
|
+
videoEl.addEventListener("playing", onPlayingResetBudget);
|
|
3060
|
+
const removeFatalListener = () => {
|
|
3061
|
+
videoEl.removeEventListener("error", onFatalVideoError);
|
|
3062
|
+
videoEl.removeEventListener("playing", onPlayingResetBudget);
|
|
3063
|
+
};
|
|
2952
3064
|
const previous_cleanup = cleanupVideoEvents;
|
|
2953
3065
|
cleanupVideoEvents = () => {
|
|
2954
3066
|
removeFatalListener();
|
|
@@ -3073,12 +3185,38 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3073
3185
|
reconnectAttempts = 0;
|
|
3074
3186
|
reconnectWindowStart = 0;
|
|
3075
3187
|
reconnectResumePosition = 0;
|
|
3188
|
+
reconnectTriggerError = null;
|
|
3189
|
+
reconnectExhausted = false;
|
|
3190
|
+
};
|
|
3191
|
+
const emitReconnectExhausted = (elapsedMs, windowMs) => {
|
|
3192
|
+
if (reconnectExhausted) return;
|
|
3193
|
+
reconnectExhausted = true;
|
|
3194
|
+
const attempts = reconnectAttempts;
|
|
3195
|
+
const trigger = reconnectTriggerError;
|
|
3196
|
+
api?.emit("error:reconnect-exhausted", { attempts, elapsedMs, windowMs });
|
|
3197
|
+
api?.setState("playbackState", "error");
|
|
3198
|
+
api?.setState("buffering", false);
|
|
3199
|
+
api?.emit("error", {
|
|
3200
|
+
code: trigger ? mapFatalErrorCode(trigger) : ErrorCode.PLAYBACK_FAILED,
|
|
3201
|
+
message: `HLS auto-reconnect gave up after ${attempts} attempts over ${Math.round(elapsedMs / 1e3)}s`,
|
|
3202
|
+
fatal: true,
|
|
3203
|
+
timestamp: Date.now(),
|
|
3204
|
+
detail: {
|
|
3205
|
+
type: trigger?.type ?? "other",
|
|
3206
|
+
retriesExhausted: true,
|
|
3207
|
+
attempts,
|
|
3208
|
+
reconnectExhausted: true
|
|
3209
|
+
}
|
|
3210
|
+
});
|
|
3076
3211
|
};
|
|
3077
3212
|
const scheduleReconnectAttempt = () => {
|
|
3213
|
+
if (reconnectExhausted) return;
|
|
3078
3214
|
if (reconnectTimer) return;
|
|
3079
3215
|
const window_ms = mergedConfig.reconnectWindowMs ?? 3e5;
|
|
3080
|
-
|
|
3216
|
+
const elapsed_ms = Date.now() - reconnectWindowStart;
|
|
3217
|
+
if (elapsed_ms > window_ms) {
|
|
3081
3218
|
api?.logger.warn(`Auto-reconnect window exhausted after ${reconnectAttempts} attempts`);
|
|
3219
|
+
emitReconnectExhausted(elapsed_ms, window_ms);
|
|
3082
3220
|
return;
|
|
3083
3221
|
}
|
|
3084
3222
|
const base_delay = mergedConfig.reconnectBaseDelayMs ?? 2e3;
|
|
@@ -3086,7 +3224,12 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3086
3224
|
const backoff = Math.min(base_delay * Math.pow(2, reconnectAttempts), max_delay);
|
|
3087
3225
|
const delay = Math.round(backoff * (0.7 + Math.random() * 0.3));
|
|
3088
3226
|
api?.logger.info(`Scheduling auto-reconnect attempt ${reconnectAttempts + 1} in ${delay}ms`);
|
|
3089
|
-
api?.emit("error:reconnecting", {
|
|
3227
|
+
api?.emit("error:reconnecting", {
|
|
3228
|
+
attempt: reconnectAttempts + 1,
|
|
3229
|
+
delayMs: delay,
|
|
3230
|
+
elapsedMs: elapsed_ms,
|
|
3231
|
+
windowMs: window_ms
|
|
3232
|
+
});
|
|
3090
3233
|
reconnectTimer = setTimeout(() => {
|
|
3091
3234
|
reconnectTimer = null;
|
|
3092
3235
|
void attemptReconnect();
|
|
@@ -3099,6 +3242,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3099
3242
|
if (reconnectWindowStart === 0) {
|
|
3100
3243
|
reconnectWindowStart = Date.now();
|
|
3101
3244
|
reconnectResumePosition = video?.currentTime ?? 0;
|
|
3245
|
+
reconnectTriggerError = error;
|
|
3102
3246
|
}
|
|
3103
3247
|
scheduleReconnectAttempt();
|
|
3104
3248
|
};
|
|
@@ -3130,7 +3274,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3130
3274
|
}
|
|
3131
3275
|
api.setState("playbackState", "ready");
|
|
3132
3276
|
api.setState("buffering", false);
|
|
3133
|
-
api.emit("error:recovered",
|
|
3277
|
+
api.emit("error:recovered", {
|
|
3278
|
+
attempt: reconnectAttempts,
|
|
3279
|
+
elapsedMs: Date.now() - reconnectWindowStart
|
|
3280
|
+
});
|
|
3134
3281
|
api.logger.info("Auto-reconnect succeeded");
|
|
3135
3282
|
cancelReconnect();
|
|
3136
3283
|
try {
|
|
@@ -3298,12 +3445,12 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3298
3445
|
if (isNative || !hls) return -1;
|
|
3299
3446
|
return hls.currentLevel;
|
|
3300
3447
|
},
|
|
3301
|
-
setLevel(
|
|
3448
|
+
setLevel(index2) {
|
|
3302
3449
|
if (isNative || !hls) {
|
|
3303
3450
|
api?.logger.warn("Quality selection not available in native HLS mode");
|
|
3304
3451
|
return;
|
|
3305
3452
|
}
|
|
3306
|
-
hls.currentLevel =
|
|
3453
|
+
hls.currentLevel = index2;
|
|
3307
3454
|
},
|
|
3308
3455
|
getLevels() {
|
|
3309
3456
|
if (isNative || !hls) return [];
|
|
@@ -3488,7 +3635,7 @@ function createHLSPlugin(config) {
|
|
|
3488
3635
|
config
|
|
3489
3636
|
);
|
|
3490
3637
|
}
|
|
3491
|
-
var styles = `
|
|
3638
|
+
var styles$1 = `
|
|
3492
3639
|
/* ============================================
|
|
3493
3640
|
Container & Base
|
|
3494
3641
|
============================================ */
|
|
@@ -3635,6 +3782,25 @@ var styles = `
|
|
|
3635
3782
|
border-radius: inherit;
|
|
3636
3783
|
}
|
|
3637
3784
|
|
|
3785
|
+
/* Chapter markers */
|
|
3786
|
+
.sp-progress__markers {
|
|
3787
|
+
position: absolute;
|
|
3788
|
+
top: 0;
|
|
3789
|
+
left: 0;
|
|
3790
|
+
width: 100%;
|
|
3791
|
+
height: 100%;
|
|
3792
|
+
pointer-events: none;
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
.sp-progress__marker {
|
|
3796
|
+
position: absolute;
|
|
3797
|
+
top: 0;
|
|
3798
|
+
width: 2px;
|
|
3799
|
+
height: 100%;
|
|
3800
|
+
margin-left: -1px;
|
|
3801
|
+
background: rgba(0, 0, 0, 0.65);
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3638
3804
|
.sp-progress__handle {
|
|
3639
3805
|
position: absolute;
|
|
3640
3806
|
top: 50%;
|
|
@@ -3694,6 +3860,16 @@ var styles = `
|
|
|
3694
3860
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
3695
3861
|
}
|
|
3696
3862
|
|
|
3863
|
+
.sp-progress__tooltip-chapter {
|
|
3864
|
+
display: block;
|
|
3865
|
+
max-width: 220px;
|
|
3866
|
+
overflow: hidden;
|
|
3867
|
+
color: rgba(255, 255, 255, 0.75);
|
|
3868
|
+
font-weight: 400;
|
|
3869
|
+
font-variant-numeric: normal;
|
|
3870
|
+
text-overflow: ellipsis;
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3697
3873
|
@media (hover: hover) {
|
|
3698
3874
|
.sp-progress-wrapper:hover .sp-progress__tooltip {
|
|
3699
3875
|
opacity: 1;
|
|
@@ -4504,9 +4680,9 @@ var ThumbnailPreview = class {
|
|
|
4504
4680
|
return;
|
|
4505
4681
|
}
|
|
4506
4682
|
const { src, width, height, columns, interval } = this.config;
|
|
4507
|
-
const
|
|
4508
|
-
const col =
|
|
4509
|
-
const row = Math.floor(
|
|
4683
|
+
const index2 = Math.floor(time / interval);
|
|
4684
|
+
const col = index2 % columns;
|
|
4685
|
+
const row = Math.floor(index2 / columns);
|
|
4510
4686
|
this.img.style.backgroundImage = `url(${src})`;
|
|
4511
4687
|
this.img.style.backgroundPosition = `-${col * width}px -${row * height}px`;
|
|
4512
4688
|
this.img.style.backgroundSize = `${columns * width}px auto`;
|
|
@@ -4531,6 +4707,8 @@ var ProgressBar = class {
|
|
|
4531
4707
|
this.lastSeekTime = 0;
|
|
4532
4708
|
this.seekThrottleMs = 100;
|
|
4533
4709
|
this.wasPlayingBeforeDrag = false;
|
|
4710
|
+
this.renderedChapters = null;
|
|
4711
|
+
this.renderedDuration = 0;
|
|
4534
4712
|
this.onMouseDown = (e) => {
|
|
4535
4713
|
e.preventDefault();
|
|
4536
4714
|
const video = getVideo(this.api.container);
|
|
@@ -4665,12 +4843,14 @@ var ProgressBar = class {
|
|
|
4665
4843
|
const track = createElement("div", { className: "sp-progress__track" });
|
|
4666
4844
|
this.buffered = createElement("div", { className: "sp-progress__buffered" });
|
|
4667
4845
|
this.filled = createElement("div", { className: "sp-progress__filled" });
|
|
4846
|
+
this.markers = createElement("div", { className: "sp-progress__markers" });
|
|
4668
4847
|
this.handle = createElement("div", { className: "sp-progress__handle" });
|
|
4669
4848
|
this.tooltip = createElement("div", { className: "sp-progress__tooltip" });
|
|
4670
4849
|
this.tooltip.textContent = "0:00";
|
|
4671
4850
|
this.thumbnailPreview = new ThumbnailPreview();
|
|
4672
4851
|
track.appendChild(this.buffered);
|
|
4673
4852
|
track.appendChild(this.filled);
|
|
4853
|
+
track.appendChild(this.markers);
|
|
4674
4854
|
track.appendChild(this.handle);
|
|
4675
4855
|
this.el.appendChild(track);
|
|
4676
4856
|
this.el.appendChild(this.thumbnailPreview.getElement());
|
|
@@ -4720,6 +4900,7 @@ var ProgressBar = class {
|
|
|
4720
4900
|
this.thumbnailPreview.setConfig(thumbnails);
|
|
4721
4901
|
}
|
|
4722
4902
|
this.el.classList.toggle("sp-progress--live", !!live);
|
|
4903
|
+
this.updateMarkers(duration, live, seekableRange);
|
|
4723
4904
|
if (live && seekableRange) {
|
|
4724
4905
|
const rangeLength = seekableRange.end - seekableRange.start;
|
|
4725
4906
|
if (rangeLength > 0) {
|
|
@@ -4752,6 +4933,70 @@ var ProgressBar = class {
|
|
|
4752
4933
|
this.el.setAttribute("aria-valuetext", formatTime$1(currentTime));
|
|
4753
4934
|
}
|
|
4754
4935
|
}
|
|
4936
|
+
/**
|
|
4937
|
+
* Label of the chapter containing a point on the timeline.
|
|
4938
|
+
*
|
|
4939
|
+
* Mirrors the chapters plugin's own lookup: a start time belongs to its
|
|
4940
|
+
* chapter, an end time belongs to the next one, and a point in a gap between
|
|
4941
|
+
* sparse chapters belongs to neither.
|
|
4942
|
+
*
|
|
4943
|
+
* @param time - Position in seconds
|
|
4944
|
+
* @returns The chapter label, or null when the point is outside every chapter
|
|
4945
|
+
*/
|
|
4946
|
+
chapterLabelAt(time) {
|
|
4947
|
+
const chapters = this.api.getState("chapters") ?? [];
|
|
4948
|
+
for (let i = chapters.length - 1; i >= 0; i--) {
|
|
4949
|
+
const chapter = chapters[i];
|
|
4950
|
+
if (time < chapter.time) continue;
|
|
4951
|
+
const next = chapters[i + 1];
|
|
4952
|
+
const end = chapter.endTime ?? (next ? next.time : Infinity);
|
|
4953
|
+
return time < end ? chapter.label : null;
|
|
4954
|
+
}
|
|
4955
|
+
return null;
|
|
4956
|
+
}
|
|
4957
|
+
/**
|
|
4958
|
+
* Paint chapter dividers along the track.
|
|
4959
|
+
*
|
|
4960
|
+
* Reads the `chapters` state that core owns, so this works whether the list
|
|
4961
|
+
* came from the chapters plugin or the host set it directly, and renders
|
|
4962
|
+
* nothing at all when there are none.
|
|
4963
|
+
*
|
|
4964
|
+
* Rebuilds only when the list or the duration actually changed. `update()`
|
|
4965
|
+
* runs on every time update, and rebuilding a dozen nodes 4 times a second
|
|
4966
|
+
* would churn the DOM for no reason.
|
|
4967
|
+
*
|
|
4968
|
+
* @param duration - Media duration in seconds
|
|
4969
|
+
* @param live - Whether the media is live
|
|
4970
|
+
* @param seekableRange - DVR window, when the media is live
|
|
4971
|
+
*/
|
|
4972
|
+
updateMarkers(duration, live, seekableRange) {
|
|
4973
|
+
const chapters = this.api.getState("chapters") ?? [];
|
|
4974
|
+
const range = live ? seekableRange ? seekableRange.end - seekableRange.start : 0 : duration;
|
|
4975
|
+
if (chapters.length === 0 || range <= 0) {
|
|
4976
|
+
if (this.renderedChapters !== null) {
|
|
4977
|
+
this.markers.textContent = "";
|
|
4978
|
+
this.renderedChapters = null;
|
|
4979
|
+
this.renderedDuration = 0;
|
|
4980
|
+
}
|
|
4981
|
+
return;
|
|
4982
|
+
}
|
|
4983
|
+
if (chapters === this.renderedChapters && range === this.renderedDuration) {
|
|
4984
|
+
return;
|
|
4985
|
+
}
|
|
4986
|
+
const origin = live && seekableRange ? seekableRange.start : 0;
|
|
4987
|
+
this.markers.textContent = "";
|
|
4988
|
+
for (const chapter of chapters) {
|
|
4989
|
+
if (chapter.time <= origin) continue;
|
|
4990
|
+
const percent = (chapter.time - origin) / range * 100;
|
|
4991
|
+
if (percent <= 0 || percent >= 100) continue;
|
|
4992
|
+
const marker = createElement("div", { className: "sp-progress__marker" });
|
|
4993
|
+
marker.style.left = `${percent}%`;
|
|
4994
|
+
marker.title = chapter.label;
|
|
4995
|
+
this.markers.appendChild(marker);
|
|
4996
|
+
}
|
|
4997
|
+
this.renderedChapters = chapters;
|
|
4998
|
+
this.renderedDuration = range;
|
|
4999
|
+
}
|
|
4755
5000
|
getTimeFromPosition(clientX) {
|
|
4756
5001
|
const rect = this.el.getBoundingClientRect();
|
|
4757
5002
|
const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
@@ -4776,6 +5021,12 @@ var ProgressBar = class {
|
|
|
4776
5021
|
} else {
|
|
4777
5022
|
this.tooltip.textContent = formatTime$1(time);
|
|
4778
5023
|
}
|
|
5024
|
+
const chapterLabel = this.chapterLabelAt(time);
|
|
5025
|
+
if (chapterLabel) {
|
|
5026
|
+
const label = createElement("span", { className: "sp-progress__tooltip-chapter" });
|
|
5027
|
+
label.textContent = chapterLabel;
|
|
5028
|
+
this.tooltip.appendChild(label);
|
|
5029
|
+
}
|
|
4779
5030
|
this.tooltip.style.left = `${percent * 100}%`;
|
|
4780
5031
|
if (this.thumbnailPreview.isConfigured()) {
|
|
4781
5032
|
this.thumbnailPreview.show(time, percent);
|
|
@@ -6024,6 +6275,12 @@ var BandwidthIndicator = class {
|
|
|
6024
6275
|
};
|
|
6025
6276
|
var registry = /* @__PURE__ */ new Map();
|
|
6026
6277
|
var listeners = /* @__PURE__ */ new Set();
|
|
6278
|
+
function registerControl(id, factory) {
|
|
6279
|
+
registry.set(id, factory);
|
|
6280
|
+
for (const listener of listeners) {
|
|
6281
|
+
listener(id);
|
|
6282
|
+
}
|
|
6283
|
+
}
|
|
6027
6284
|
function getControlFactory(id) {
|
|
6028
6285
|
return registry.get(id) ?? null;
|
|
6029
6286
|
}
|
|
@@ -6187,7 +6444,15 @@ function uiPlugin(config = {}) {
|
|
|
6187
6444
|
}
|
|
6188
6445
|
hideTimeout = setTimeout(hideControls, hideDelay);
|
|
6189
6446
|
};
|
|
6447
|
+
let last_pointer_type = null;
|
|
6448
|
+
const handlePointerActivity = (event) => {
|
|
6449
|
+
last_pointer_type = event.pointerType;
|
|
6450
|
+
};
|
|
6190
6451
|
const handleInteraction = () => {
|
|
6452
|
+
if (last_pointer_type === "touch") {
|
|
6453
|
+
const gestures = api?.getPlugin("gestures");
|
|
6454
|
+
if (gestures?.ownsTapInteraction()) return;
|
|
6455
|
+
}
|
|
6191
6456
|
showControls();
|
|
6192
6457
|
};
|
|
6193
6458
|
const handleMouseLeave = () => {
|
|
@@ -6266,7 +6531,7 @@ function uiPlugin(config = {}) {
|
|
|
6266
6531
|
async init(pluginApi) {
|
|
6267
6532
|
api = pluginApi;
|
|
6268
6533
|
styleEl = document.createElement("style");
|
|
6269
|
-
styleEl.textContent = styles;
|
|
6534
|
+
styleEl.textContent = styles$1;
|
|
6270
6535
|
document.head.appendChild(styleEl);
|
|
6271
6536
|
if (config.theme) {
|
|
6272
6537
|
this.setTheme(config.theme);
|
|
@@ -6321,6 +6586,8 @@ function uiPlugin(config = {}) {
|
|
|
6321
6586
|
api.logger.debug(`Control "${id}" registered after init, rebuilding control bar`);
|
|
6322
6587
|
rebuildControlBar();
|
|
6323
6588
|
});
|
|
6589
|
+
container.addEventListener("pointerdown", handlePointerActivity, { passive: true });
|
|
6590
|
+
container.addEventListener("pointermove", handlePointerActivity, { passive: true });
|
|
6324
6591
|
container.addEventListener("mousemove", handleInteraction);
|
|
6325
6592
|
container.addEventListener("mouseenter", handleInteraction);
|
|
6326
6593
|
container.addEventListener("mouseleave", handleMouseLeave);
|
|
@@ -6358,6 +6625,8 @@ function uiPlugin(config = {}) {
|
|
|
6358
6625
|
recoveredUnsubscribe?.();
|
|
6359
6626
|
recoveredUnsubscribe = null;
|
|
6360
6627
|
if (api?.container) {
|
|
6628
|
+
api.container.removeEventListener("pointerdown", handlePointerActivity);
|
|
6629
|
+
api.container.removeEventListener("pointermove", handlePointerActivity);
|
|
6361
6630
|
api.container.removeEventListener("mousemove", handleInteraction);
|
|
6362
6631
|
api.container.removeEventListener("mouseenter", handleInteraction);
|
|
6363
6632
|
api.container.removeEventListener("mouseleave", handleMouseLeave);
|
|
@@ -6419,6 +6688,16 @@ function uiPlugin(config = {}) {
|
|
|
6419
6688
|
}
|
|
6420
6689
|
};
|
|
6421
6690
|
}
|
|
6691
|
+
const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
6692
|
+
__proto__: null,
|
|
6693
|
+
formatLiveTime,
|
|
6694
|
+
formatTime: formatTime$1,
|
|
6695
|
+
getControlFactory,
|
|
6696
|
+
icons,
|
|
6697
|
+
registerControl,
|
|
6698
|
+
styles: styles$1,
|
|
6699
|
+
uiPlugin
|
|
6700
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
6422
6701
|
var DEFAULT_THEME = {
|
|
6423
6702
|
primary: "#6366f1",
|
|
6424
6703
|
background: "#18181b",
|
|
@@ -7732,6 +8011,272 @@ function createAnalyticsPlugin(config) {
|
|
|
7732
8011
|
}
|
|
7733
8012
|
};
|
|
7734
8013
|
}
|
|
8014
|
+
var PLAYLIST_ICONS = {
|
|
8015
|
+
previous: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 6h2v12H6V6zm3.5 6L18 6v12l-8.5-6z"/></svg>',
|
|
8016
|
+
next: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M16 6h2v12h-2V6zM6 6l8.5 6L6 18V6z"/></svg>',
|
|
8017
|
+
list: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M3 6h11v2H3V6zm0 5h11v2H3v-2zm0 5h7v2H3v-2zm13-1.5V8l6 3.5-6 3.5z"/></svg>'
|
|
8018
|
+
};
|
|
8019
|
+
var PlaylistSkipButton = class {
|
|
8020
|
+
constructor(plugin, direction) {
|
|
8021
|
+
this.plugin = plugin;
|
|
8022
|
+
this.direction = direction;
|
|
8023
|
+
this.clickHandler = () => {
|
|
8024
|
+
if (this.direction === "next") {
|
|
8025
|
+
this.plugin.next();
|
|
8026
|
+
} else {
|
|
8027
|
+
this.plugin.previous();
|
|
8028
|
+
}
|
|
8029
|
+
};
|
|
8030
|
+
const label = direction === "next" ? "Next item" : "Previous item";
|
|
8031
|
+
this.el = document.createElement("button");
|
|
8032
|
+
this.el.type = "button";
|
|
8033
|
+
this.el.className = `sp-control sp-playlist-skip sp-playlist-skip--${direction}`;
|
|
8034
|
+
this.el.setAttribute("aria-label", label);
|
|
8035
|
+
this.el.setAttribute("title", label);
|
|
8036
|
+
this.el.innerHTML = PLAYLIST_ICONS[direction];
|
|
8037
|
+
this.el.addEventListener("click", this.clickHandler);
|
|
8038
|
+
}
|
|
8039
|
+
render() {
|
|
8040
|
+
return this.el;
|
|
8041
|
+
}
|
|
8042
|
+
update() {
|
|
8043
|
+
const state = this.plugin.getState();
|
|
8044
|
+
if (state.tracks.length <= 1) {
|
|
8045
|
+
this.el.style.display = "none";
|
|
8046
|
+
return;
|
|
8047
|
+
}
|
|
8048
|
+
this.el.style.display = "";
|
|
8049
|
+
const enabled = this.direction === "next" ? state.hasNext : state.hasPrevious;
|
|
8050
|
+
this.el.disabled = !enabled;
|
|
8051
|
+
}
|
|
8052
|
+
destroy() {
|
|
8053
|
+
this.el.removeEventListener("click", this.clickHandler);
|
|
8054
|
+
this.el.remove();
|
|
8055
|
+
}
|
|
8056
|
+
};
|
|
8057
|
+
var PlaylistPanel = class {
|
|
8058
|
+
constructor(plugin, options) {
|
|
8059
|
+
this.plugin = plugin;
|
|
8060
|
+
this.options = options;
|
|
8061
|
+
this.open = false;
|
|
8062
|
+
this.renderedIds = "";
|
|
8063
|
+
this.toggleHandler = (event) => {
|
|
8064
|
+
event.stopPropagation();
|
|
8065
|
+
this.setOpen(!this.open);
|
|
8066
|
+
};
|
|
8067
|
+
this.documentClickHandler = () => {
|
|
8068
|
+
if (this.open) this.setOpen(false);
|
|
8069
|
+
};
|
|
8070
|
+
this.keydownHandler = (event) => {
|
|
8071
|
+
if (event.key === "Escape" && this.open) {
|
|
8072
|
+
this.setOpen(false);
|
|
8073
|
+
this.button.focus();
|
|
8074
|
+
}
|
|
8075
|
+
};
|
|
8076
|
+
this.el = document.createElement("div");
|
|
8077
|
+
this.el.className = "sp-playlist";
|
|
8078
|
+
this.button = document.createElement("button");
|
|
8079
|
+
this.button.type = "button";
|
|
8080
|
+
this.button.className = "sp-control sp-playlist__button";
|
|
8081
|
+
this.button.setAttribute("aria-label", "Playlist");
|
|
8082
|
+
this.button.setAttribute("title", "Playlist");
|
|
8083
|
+
this.button.setAttribute("aria-haspopup", "true");
|
|
8084
|
+
this.button.setAttribute("aria-expanded", "false");
|
|
8085
|
+
this.button.innerHTML = PLAYLIST_ICONS.list;
|
|
8086
|
+
this.panel = document.createElement("div");
|
|
8087
|
+
this.panel.className = "sp-playlist__panel";
|
|
8088
|
+
this.panel.setAttribute("role", "menu");
|
|
8089
|
+
this.panel.hidden = true;
|
|
8090
|
+
this.el.appendChild(this.button);
|
|
8091
|
+
this.el.appendChild(this.panel);
|
|
8092
|
+
this.button.addEventListener("click", this.toggleHandler);
|
|
8093
|
+
document.addEventListener("click", this.documentClickHandler);
|
|
8094
|
+
document.addEventListener("keydown", this.keydownHandler);
|
|
8095
|
+
}
|
|
8096
|
+
render() {
|
|
8097
|
+
return this.el;
|
|
8098
|
+
}
|
|
8099
|
+
update() {
|
|
8100
|
+
const state = this.plugin.getState();
|
|
8101
|
+
if (state.tracks.length <= 1) {
|
|
8102
|
+
this.el.style.display = "none";
|
|
8103
|
+
return;
|
|
8104
|
+
}
|
|
8105
|
+
this.el.style.display = "";
|
|
8106
|
+
const signature = state.tracks.map((track) => track.id).join("|");
|
|
8107
|
+
if (signature !== this.renderedIds) {
|
|
8108
|
+
this.renderedIds = signature;
|
|
8109
|
+
this.renderPanel(state.tracks, state.currentIndex);
|
|
8110
|
+
return;
|
|
8111
|
+
}
|
|
8112
|
+
this.markActive(state.currentIndex);
|
|
8113
|
+
}
|
|
8114
|
+
destroy() {
|
|
8115
|
+
this.button.removeEventListener("click", this.toggleHandler);
|
|
8116
|
+
document.removeEventListener("click", this.documentClickHandler);
|
|
8117
|
+
document.removeEventListener("keydown", this.keydownHandler);
|
|
8118
|
+
this.el.remove();
|
|
8119
|
+
}
|
|
8120
|
+
setOpen(open) {
|
|
8121
|
+
this.open = open;
|
|
8122
|
+
this.panel.hidden = !open;
|
|
8123
|
+
this.button.setAttribute("aria-expanded", String(open));
|
|
8124
|
+
this.el.classList.toggle("sp-playlist--open", open);
|
|
8125
|
+
}
|
|
8126
|
+
markActive(currentIndex) {
|
|
8127
|
+
const items = this.panel.querySelectorAll(".sp-playlist__item");
|
|
8128
|
+
items.forEach((item, index2) => {
|
|
8129
|
+
item.classList.toggle("sp-playlist__item--active", index2 === currentIndex);
|
|
8130
|
+
item.setAttribute("aria-current", index2 === currentIndex ? "true" : "false");
|
|
8131
|
+
});
|
|
8132
|
+
}
|
|
8133
|
+
renderPanel(tracks, currentIndex) {
|
|
8134
|
+
this.panel.textContent = "";
|
|
8135
|
+
tracks.forEach((track, index2) => {
|
|
8136
|
+
const item = document.createElement("button");
|
|
8137
|
+
item.type = "button";
|
|
8138
|
+
item.className = "sp-playlist__item";
|
|
8139
|
+
item.setAttribute("role", "menuitem");
|
|
8140
|
+
item.setAttribute("aria-current", index2 === currentIndex ? "true" : "false");
|
|
8141
|
+
if (index2 === currentIndex) {
|
|
8142
|
+
item.classList.add("sp-playlist__item--active");
|
|
8143
|
+
}
|
|
8144
|
+
const position = document.createElement("span");
|
|
8145
|
+
position.className = "sp-playlist__position";
|
|
8146
|
+
position.textContent = String(index2 + 1);
|
|
8147
|
+
const text = document.createElement("span");
|
|
8148
|
+
text.className = "sp-playlist__text";
|
|
8149
|
+
const title = document.createElement("span");
|
|
8150
|
+
title.className = "sp-playlist__title";
|
|
8151
|
+
title.textContent = track.title ?? `Item ${index2 + 1}`;
|
|
8152
|
+
text.appendChild(title);
|
|
8153
|
+
if (track.artist) {
|
|
8154
|
+
const artist = document.createElement("span");
|
|
8155
|
+
artist.className = "sp-playlist__artist";
|
|
8156
|
+
artist.textContent = track.artist;
|
|
8157
|
+
text.appendChild(artist);
|
|
8158
|
+
}
|
|
8159
|
+
item.appendChild(position);
|
|
8160
|
+
item.appendChild(text);
|
|
8161
|
+
item.addEventListener("click", (event) => {
|
|
8162
|
+
event.stopPropagation();
|
|
8163
|
+
this.options.onSelect(index2);
|
|
8164
|
+
this.setOpen(false);
|
|
8165
|
+
});
|
|
8166
|
+
this.panel.appendChild(item);
|
|
8167
|
+
});
|
|
8168
|
+
}
|
|
8169
|
+
};
|
|
8170
|
+
var STYLE_ID = "sp-playlist-styles";
|
|
8171
|
+
var styles = `
|
|
8172
|
+
.sp-playlist-skip[disabled] {
|
|
8173
|
+
opacity: 0.4;
|
|
8174
|
+
cursor: default;
|
|
8175
|
+
}
|
|
8176
|
+
|
|
8177
|
+
.sp-playlist {
|
|
8178
|
+
position: relative;
|
|
8179
|
+
display: inline-flex;
|
|
8180
|
+
}
|
|
8181
|
+
|
|
8182
|
+
.sp-playlist__button svg,
|
|
8183
|
+
.sp-playlist-skip svg {
|
|
8184
|
+
width: 20px;
|
|
8185
|
+
height: 20px;
|
|
8186
|
+
}
|
|
8187
|
+
|
|
8188
|
+
.sp-playlist__panel {
|
|
8189
|
+
position: absolute;
|
|
8190
|
+
bottom: calc(100% + 8px);
|
|
8191
|
+
right: 0;
|
|
8192
|
+
z-index: 20;
|
|
8193
|
+
display: flex;
|
|
8194
|
+
flex-direction: column;
|
|
8195
|
+
min-width: 240px;
|
|
8196
|
+
max-width: 320px;
|
|
8197
|
+
max-height: 300px;
|
|
8198
|
+
overflow-y: auto;
|
|
8199
|
+
padding: 4px;
|
|
8200
|
+
border-radius: 8px;
|
|
8201
|
+
background: rgba(20, 20, 20, 0.96);
|
|
8202
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
|
8203
|
+
}
|
|
8204
|
+
|
|
8205
|
+
.sp-playlist__panel[hidden] {
|
|
8206
|
+
display: none;
|
|
8207
|
+
}
|
|
8208
|
+
|
|
8209
|
+
.sp-playlist__item {
|
|
8210
|
+
display: flex;
|
|
8211
|
+
gap: 10px;
|
|
8212
|
+
align-items: flex-start;
|
|
8213
|
+
width: 100%;
|
|
8214
|
+
padding: 8px 10px;
|
|
8215
|
+
border: 0;
|
|
8216
|
+
border-radius: 6px;
|
|
8217
|
+
background: transparent;
|
|
8218
|
+
color: #fff;
|
|
8219
|
+
font: inherit;
|
|
8220
|
+
text-align: left;
|
|
8221
|
+
cursor: pointer;
|
|
8222
|
+
}
|
|
8223
|
+
|
|
8224
|
+
.sp-playlist__item:hover,
|
|
8225
|
+
.sp-playlist__item:focus-visible {
|
|
8226
|
+
background: rgba(255, 255, 255, 0.12);
|
|
8227
|
+
}
|
|
8228
|
+
|
|
8229
|
+
.sp-playlist__item--active {
|
|
8230
|
+
background: rgba(255, 255, 255, 0.08);
|
|
8231
|
+
}
|
|
8232
|
+
|
|
8233
|
+
.sp-playlist__item--active .sp-playlist__title {
|
|
8234
|
+
font-weight: 600;
|
|
8235
|
+
}
|
|
8236
|
+
|
|
8237
|
+
.sp-playlist__position {
|
|
8238
|
+
flex: 0 0 auto;
|
|
8239
|
+
min-width: 18px;
|
|
8240
|
+
color: rgba(255, 255, 255, 0.7);
|
|
8241
|
+
font-variant-numeric: tabular-nums;
|
|
8242
|
+
font-size: 12px;
|
|
8243
|
+
line-height: 18px;
|
|
8244
|
+
}
|
|
8245
|
+
|
|
8246
|
+
.sp-playlist__text {
|
|
8247
|
+
display: flex;
|
|
8248
|
+
flex-direction: column;
|
|
8249
|
+
min-width: 0;
|
|
8250
|
+
}
|
|
8251
|
+
|
|
8252
|
+
.sp-playlist__title {
|
|
8253
|
+
font-size: 13px;
|
|
8254
|
+
line-height: 18px;
|
|
8255
|
+
}
|
|
8256
|
+
|
|
8257
|
+
.sp-playlist__artist {
|
|
8258
|
+
color: rgba(255, 255, 255, 0.6);
|
|
8259
|
+
font-size: 11px;
|
|
8260
|
+
line-height: 16px;
|
|
8261
|
+
}
|
|
8262
|
+
|
|
8263
|
+
@media (max-width: 480px) {
|
|
8264
|
+
.sp-playlist__panel {
|
|
8265
|
+
min-width: 200px;
|
|
8266
|
+
max-width: 76vw;
|
|
8267
|
+
}
|
|
8268
|
+
}
|
|
8269
|
+
`;
|
|
8270
|
+
function injectStyles() {
|
|
8271
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID)) {
|
|
8272
|
+
return null;
|
|
8273
|
+
}
|
|
8274
|
+
const el = document.createElement("style");
|
|
8275
|
+
el.id = STYLE_ID;
|
|
8276
|
+
el.textContent = styles;
|
|
8277
|
+
document.head.appendChild(el);
|
|
8278
|
+
return el;
|
|
8279
|
+
}
|
|
7735
8280
|
var DEFAULT_CONFIG$1 = {
|
|
7736
8281
|
autoAdvance: true,
|
|
7737
8282
|
preloadNext: true,
|
|
@@ -7871,14 +8416,14 @@ function createPlaylistPlugin(config) {
|
|
|
7871
8416
|
api?.emit("playlist:change", { track, index: currentIndex });
|
|
7872
8417
|
persistPlaylist();
|
|
7873
8418
|
};
|
|
7874
|
-
const setCurrentTrack = (
|
|
7875
|
-
if (
|
|
7876
|
-
api?.logger.warn("Invalid track index", { index });
|
|
8419
|
+
const setCurrentTrack = (index2) => {
|
|
8420
|
+
if (index2 < 0 || index2 >= tracks.length) {
|
|
8421
|
+
api?.logger.warn("Invalid track index", { index: index2 });
|
|
7877
8422
|
return;
|
|
7878
8423
|
}
|
|
7879
|
-
const track = tracks[
|
|
7880
|
-
currentIndex =
|
|
7881
|
-
api?.logger.info("Track changed", { index, title: track.title, src: track.src });
|
|
8424
|
+
const track = tracks[index2];
|
|
8425
|
+
currentIndex = index2;
|
|
8426
|
+
api?.logger.info("Track changed", { index: index2, title: track.title, src: track.src });
|
|
7882
8427
|
api?.setState("title", track.title || "");
|
|
7883
8428
|
if (track.artwork) {
|
|
7884
8429
|
api?.setState("poster", track.artwork);
|
|
@@ -7921,8 +8466,42 @@ function createPlaylistPlugin(config) {
|
|
|
7921
8466
|
api?.emit("playlist:ended", void 0);
|
|
7922
8467
|
}
|
|
7923
8468
|
});
|
|
8469
|
+
injectStyles();
|
|
8470
|
+
void Promise.resolve().then(() => index).then(({ registerControl: registerControl2 }) => {
|
|
8471
|
+
const self = plugin;
|
|
8472
|
+
registerControl2(
|
|
8473
|
+
"playlist-previous",
|
|
8474
|
+
() => new PlaylistSkipButton(self, "previous")
|
|
8475
|
+
);
|
|
8476
|
+
registerControl2("playlist-next", () => new PlaylistSkipButton(self, "next"));
|
|
8477
|
+
registerControl2(
|
|
8478
|
+
"playlist",
|
|
8479
|
+
() => new PlaylistPanel(self, {
|
|
8480
|
+
onSelect: (index2) => self.play(index2)
|
|
8481
|
+
})
|
|
8482
|
+
);
|
|
8483
|
+
}).catch(() => {
|
|
8484
|
+
api?.logger.debug("@scarlett-player/ui not present, playlist controls not registered");
|
|
8485
|
+
});
|
|
8486
|
+
const onKeyDown = (event) => {
|
|
8487
|
+
if (!api || !api.container.contains(document.activeElement)) return;
|
|
8488
|
+
const activeEl = document.activeElement;
|
|
8489
|
+
if (activeEl instanceof HTMLInputElement || activeEl instanceof HTMLTextAreaElement || activeEl instanceof HTMLSelectElement || activeEl?.isContentEditable) {
|
|
8490
|
+
return;
|
|
8491
|
+
}
|
|
8492
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
8493
|
+
if (event.key === "n" || event.key === "N") {
|
|
8494
|
+
event.preventDefault();
|
|
8495
|
+
plugin.next();
|
|
8496
|
+
} else if (event.key === "p" || event.key === "P") {
|
|
8497
|
+
event.preventDefault();
|
|
8498
|
+
plugin.previous();
|
|
8499
|
+
}
|
|
8500
|
+
};
|
|
8501
|
+
document.addEventListener("keydown", onKeyDown);
|
|
7924
8502
|
api.onDestroy(() => {
|
|
7925
8503
|
unsubEnded();
|
|
8504
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
7926
8505
|
if (advanceTimeout) {
|
|
7927
8506
|
clearTimeout(advanceTimeout);
|
|
7928
8507
|
advanceTimeout = null;
|
|
@@ -7939,10 +8518,10 @@ function createPlaylistPlugin(config) {
|
|
|
7939
8518
|
const newTracks = Array.isArray(trackOrTracks) ? trackOrTracks : [trackOrTracks];
|
|
7940
8519
|
newTracks.forEach((track) => {
|
|
7941
8520
|
const normalizedTrack = { ...track, id: track.id || generateId() };
|
|
7942
|
-
const
|
|
8521
|
+
const index2 = tracks.length;
|
|
7943
8522
|
tracks.push(normalizedTrack);
|
|
7944
|
-
api?.emit("playlist:add", { track: normalizedTrack, index });
|
|
7945
|
-
api?.logger.debug("Track added", { title: normalizedTrack.title, index });
|
|
8523
|
+
api?.emit("playlist:add", { track: normalizedTrack, index: index2 });
|
|
8524
|
+
api?.logger.debug("Track added", { title: normalizedTrack.title, index: index2 });
|
|
7946
8525
|
});
|
|
7947
8526
|
if (shuffle) {
|
|
7948
8527
|
const startIndex = tracks.length - newTracks.length;
|
|
@@ -7953,9 +8532,9 @@ function createPlaylistPlugin(config) {
|
|
|
7953
8532
|
}
|
|
7954
8533
|
persistPlaylist();
|
|
7955
8534
|
},
|
|
7956
|
-
insert(
|
|
8535
|
+
insert(index2, track) {
|
|
7957
8536
|
const normalizedTrack = { ...track, id: track.id || generateId() };
|
|
7958
|
-
const clampedIndex = Math.max(0, Math.min(
|
|
8537
|
+
const clampedIndex = Math.max(0, Math.min(index2, tracks.length));
|
|
7959
8538
|
tracks.splice(clampedIndex, 0, normalizedTrack);
|
|
7960
8539
|
if (currentIndex >= clampedIndex) {
|
|
7961
8540
|
currentIndex++;
|
|
@@ -7969,33 +8548,33 @@ function createPlaylistPlugin(config) {
|
|
|
7969
8548
|
persistPlaylist();
|
|
7970
8549
|
},
|
|
7971
8550
|
remove(idOrIndex) {
|
|
7972
|
-
let
|
|
8551
|
+
let index2;
|
|
7973
8552
|
if (typeof idOrIndex === "string") {
|
|
7974
|
-
|
|
7975
|
-
if (
|
|
8553
|
+
index2 = tracks.findIndex((t) => t.id === idOrIndex);
|
|
8554
|
+
if (index2 === -1) {
|
|
7976
8555
|
api?.logger.warn("Track not found", { id: idOrIndex });
|
|
7977
8556
|
return;
|
|
7978
8557
|
}
|
|
7979
8558
|
} else {
|
|
7980
|
-
|
|
8559
|
+
index2 = idOrIndex;
|
|
7981
8560
|
}
|
|
7982
|
-
if (
|
|
7983
|
-
api?.logger.warn("Invalid track index", { index });
|
|
8561
|
+
if (index2 < 0 || index2 >= tracks.length) {
|
|
8562
|
+
api?.logger.warn("Invalid track index", { index: index2 });
|
|
7984
8563
|
return;
|
|
7985
8564
|
}
|
|
7986
|
-
const [removedTrack] = tracks.splice(
|
|
7987
|
-
if (
|
|
8565
|
+
const [removedTrack] = tracks.splice(index2, 1);
|
|
8566
|
+
if (index2 < currentIndex) {
|
|
7988
8567
|
currentIndex--;
|
|
7989
|
-
} else if (
|
|
8568
|
+
} else if (index2 === currentIndex) {
|
|
7990
8569
|
if (currentIndex >= tracks.length) {
|
|
7991
8570
|
currentIndex = tracks.length - 1;
|
|
7992
8571
|
}
|
|
7993
8572
|
emitChange();
|
|
7994
8573
|
}
|
|
7995
8574
|
if (shuffle) {
|
|
7996
|
-
shuffleOrder = shuffleOrder.filter((i) => i !==
|
|
8575
|
+
shuffleOrder = shuffleOrder.filter((i) => i !== index2).map((i) => i > index2 ? i - 1 : i);
|
|
7997
8576
|
}
|
|
7998
|
-
api?.emit("playlist:remove", { track: removedTrack, index });
|
|
8577
|
+
api?.emit("playlist:remove", { track: removedTrack, index: index2 });
|
|
7999
8578
|
persistPlaylist();
|
|
8000
8579
|
},
|
|
8001
8580
|
clear() {
|
|
@@ -8006,23 +8585,23 @@ function createPlaylistPlugin(config) {
|
|
|
8006
8585
|
emitChange();
|
|
8007
8586
|
},
|
|
8008
8587
|
play(idOrIndex) {
|
|
8009
|
-
let
|
|
8588
|
+
let index2;
|
|
8010
8589
|
if (idOrIndex === void 0) {
|
|
8011
|
-
|
|
8590
|
+
index2 = currentIndex >= 0 ? currentIndex : shuffle ? getActualIndex(0) : 0;
|
|
8012
8591
|
} else if (typeof idOrIndex === "string") {
|
|
8013
|
-
|
|
8014
|
-
if (
|
|
8592
|
+
index2 = tracks.findIndex((t) => t.id === idOrIndex);
|
|
8593
|
+
if (index2 === -1) {
|
|
8015
8594
|
api?.logger.warn("Track not found", { id: idOrIndex });
|
|
8016
8595
|
return;
|
|
8017
8596
|
}
|
|
8018
8597
|
} else {
|
|
8019
|
-
|
|
8598
|
+
index2 = idOrIndex;
|
|
8020
8599
|
}
|
|
8021
8600
|
if (tracks.length === 0) {
|
|
8022
8601
|
api?.logger.warn("Playlist is empty");
|
|
8023
8602
|
return;
|
|
8024
8603
|
}
|
|
8025
|
-
setCurrentTrack(
|
|
8604
|
+
setCurrentTrack(index2);
|
|
8026
8605
|
},
|
|
8027
8606
|
next() {
|
|
8028
8607
|
const nextIdx = getNextIndex();
|
|
@@ -8965,8 +9544,8 @@ async function createEmbedPlayer(container, config, pluginCreators2, availableTy
|
|
|
8965
9544
|
const plugins = [pluginCreators2.hls()];
|
|
8966
9545
|
if (pluginCreators2.playlist && config.playlist?.length) {
|
|
8967
9546
|
plugins.push(pluginCreators2.playlist({
|
|
8968
|
-
items: config.playlist.map((item,
|
|
8969
|
-
id: `item-${
|
|
9547
|
+
items: config.playlist.map((item, index2) => ({
|
|
9548
|
+
id: `item-${index2}`,
|
|
8970
9549
|
src: item.src,
|
|
8971
9550
|
title: item.title,
|
|
8972
9551
|
artist: item.artist,
|