@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.audio.js
CHANGED
|
@@ -149,6 +149,7 @@ class StateManager {
|
|
|
149
149
|
this.signals = /* @__PURE__ */ new Map();
|
|
150
150
|
this.changeSubscribers = /* @__PURE__ */ new Set();
|
|
151
151
|
this.definedDefaults = /* @__PURE__ */ new Map();
|
|
152
|
+
this.destroyed = false;
|
|
152
153
|
this.initializeSignals(initialState);
|
|
153
154
|
}
|
|
154
155
|
/**
|
|
@@ -209,6 +210,7 @@ class StateManager {
|
|
|
209
210
|
*
|
|
210
211
|
* @param key - State property key
|
|
211
212
|
* @returns Signal for the property
|
|
213
|
+
* @throws If the manager has been destroyed, or the key was never registered
|
|
212
214
|
*
|
|
213
215
|
* @example
|
|
214
216
|
* ```ts
|
|
@@ -218,6 +220,9 @@ class StateManager {
|
|
|
218
220
|
* ```
|
|
219
221
|
*/
|
|
220
222
|
get(key) {
|
|
223
|
+
if (this.destroyed) {
|
|
224
|
+
throw new Error(`[StateManager] Manager is destroyed (reading '${key}')`);
|
|
225
|
+
}
|
|
221
226
|
const stateSignal = this.signals.get(key);
|
|
222
227
|
if (!stateSignal) {
|
|
223
228
|
throw new Error(`[StateManager] Unknown state key: ${key}`);
|
|
@@ -229,6 +234,7 @@ class StateManager {
|
|
|
229
234
|
*
|
|
230
235
|
* @param key - State property key
|
|
231
236
|
* @returns Current value
|
|
237
|
+
* @throws If the manager has been destroyed, or the key was never registered
|
|
232
238
|
*
|
|
233
239
|
* @example
|
|
234
240
|
* ```ts
|
|
@@ -396,6 +402,11 @@ class StateManager {
|
|
|
396
402
|
/**
|
|
397
403
|
* Destroy the state manager and cleanup all signals.
|
|
398
404
|
*
|
|
405
|
+
* After this, every read or write ({@link get}, {@link getValue},
|
|
406
|
+
* {@link set}) throws a destroyed-specific error. Returning last-known
|
|
407
|
+
* values instead was considered and rejected: it silently masks the
|
|
408
|
+
* lifecycle bugs this throw exposes.
|
|
409
|
+
*
|
|
399
410
|
* @example
|
|
400
411
|
* ```ts
|
|
401
412
|
* state.destroy();
|
|
@@ -405,6 +416,7 @@ class StateManager {
|
|
|
405
416
|
this.signals.forEach((stateSignal) => stateSignal.destroy());
|
|
406
417
|
this.signals.clear();
|
|
407
418
|
this.changeSubscribers.clear();
|
|
419
|
+
this.destroyed = true;
|
|
408
420
|
}
|
|
409
421
|
}
|
|
410
422
|
const DEFAULT_OPTIONS = {
|
|
@@ -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) {
|
|
@@ -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;
|
|
@@ -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 {
|
|
@@ -4220,6 +4367,272 @@ function createAudioUIPlugin(config) {
|
|
|
4220
4367
|
};
|
|
4221
4368
|
return plugin;
|
|
4222
4369
|
}
|
|
4370
|
+
var PLAYLIST_ICONS = {
|
|
4371
|
+
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>',
|
|
4372
|
+
next: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M16 6h2v12h-2V6zM6 6l8.5 6L6 18V6z"/></svg>',
|
|
4373
|
+
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>'
|
|
4374
|
+
};
|
|
4375
|
+
var PlaylistSkipButton = class {
|
|
4376
|
+
constructor(plugin, direction) {
|
|
4377
|
+
this.plugin = plugin;
|
|
4378
|
+
this.direction = direction;
|
|
4379
|
+
this.clickHandler = () => {
|
|
4380
|
+
if (this.direction === "next") {
|
|
4381
|
+
this.plugin.next();
|
|
4382
|
+
} else {
|
|
4383
|
+
this.plugin.previous();
|
|
4384
|
+
}
|
|
4385
|
+
};
|
|
4386
|
+
const label = direction === "next" ? "Next item" : "Previous item";
|
|
4387
|
+
this.el = document.createElement("button");
|
|
4388
|
+
this.el.type = "button";
|
|
4389
|
+
this.el.className = `sp-control sp-playlist-skip sp-playlist-skip--${direction}`;
|
|
4390
|
+
this.el.setAttribute("aria-label", label);
|
|
4391
|
+
this.el.setAttribute("title", label);
|
|
4392
|
+
this.el.innerHTML = PLAYLIST_ICONS[direction];
|
|
4393
|
+
this.el.addEventListener("click", this.clickHandler);
|
|
4394
|
+
}
|
|
4395
|
+
render() {
|
|
4396
|
+
return this.el;
|
|
4397
|
+
}
|
|
4398
|
+
update() {
|
|
4399
|
+
const state = this.plugin.getState();
|
|
4400
|
+
if (state.tracks.length <= 1) {
|
|
4401
|
+
this.el.style.display = "none";
|
|
4402
|
+
return;
|
|
4403
|
+
}
|
|
4404
|
+
this.el.style.display = "";
|
|
4405
|
+
const enabled = this.direction === "next" ? state.hasNext : state.hasPrevious;
|
|
4406
|
+
this.el.disabled = !enabled;
|
|
4407
|
+
}
|
|
4408
|
+
destroy() {
|
|
4409
|
+
this.el.removeEventListener("click", this.clickHandler);
|
|
4410
|
+
this.el.remove();
|
|
4411
|
+
}
|
|
4412
|
+
};
|
|
4413
|
+
var PlaylistPanel = class {
|
|
4414
|
+
constructor(plugin, options) {
|
|
4415
|
+
this.plugin = plugin;
|
|
4416
|
+
this.options = options;
|
|
4417
|
+
this.open = false;
|
|
4418
|
+
this.renderedIds = "";
|
|
4419
|
+
this.toggleHandler = (event) => {
|
|
4420
|
+
event.stopPropagation();
|
|
4421
|
+
this.setOpen(!this.open);
|
|
4422
|
+
};
|
|
4423
|
+
this.documentClickHandler = () => {
|
|
4424
|
+
if (this.open) this.setOpen(false);
|
|
4425
|
+
};
|
|
4426
|
+
this.keydownHandler = (event) => {
|
|
4427
|
+
if (event.key === "Escape" && this.open) {
|
|
4428
|
+
this.setOpen(false);
|
|
4429
|
+
this.button.focus();
|
|
4430
|
+
}
|
|
4431
|
+
};
|
|
4432
|
+
this.el = document.createElement("div");
|
|
4433
|
+
this.el.className = "sp-playlist";
|
|
4434
|
+
this.button = document.createElement("button");
|
|
4435
|
+
this.button.type = "button";
|
|
4436
|
+
this.button.className = "sp-control sp-playlist__button";
|
|
4437
|
+
this.button.setAttribute("aria-label", "Playlist");
|
|
4438
|
+
this.button.setAttribute("title", "Playlist");
|
|
4439
|
+
this.button.setAttribute("aria-haspopup", "true");
|
|
4440
|
+
this.button.setAttribute("aria-expanded", "false");
|
|
4441
|
+
this.button.innerHTML = PLAYLIST_ICONS.list;
|
|
4442
|
+
this.panel = document.createElement("div");
|
|
4443
|
+
this.panel.className = "sp-playlist__panel";
|
|
4444
|
+
this.panel.setAttribute("role", "menu");
|
|
4445
|
+
this.panel.hidden = true;
|
|
4446
|
+
this.el.appendChild(this.button);
|
|
4447
|
+
this.el.appendChild(this.panel);
|
|
4448
|
+
this.button.addEventListener("click", this.toggleHandler);
|
|
4449
|
+
document.addEventListener("click", this.documentClickHandler);
|
|
4450
|
+
document.addEventListener("keydown", this.keydownHandler);
|
|
4451
|
+
}
|
|
4452
|
+
render() {
|
|
4453
|
+
return this.el;
|
|
4454
|
+
}
|
|
4455
|
+
update() {
|
|
4456
|
+
const state = this.plugin.getState();
|
|
4457
|
+
if (state.tracks.length <= 1) {
|
|
4458
|
+
this.el.style.display = "none";
|
|
4459
|
+
return;
|
|
4460
|
+
}
|
|
4461
|
+
this.el.style.display = "";
|
|
4462
|
+
const signature = state.tracks.map((track) => track.id).join("|");
|
|
4463
|
+
if (signature !== this.renderedIds) {
|
|
4464
|
+
this.renderedIds = signature;
|
|
4465
|
+
this.renderPanel(state.tracks, state.currentIndex);
|
|
4466
|
+
return;
|
|
4467
|
+
}
|
|
4468
|
+
this.markActive(state.currentIndex);
|
|
4469
|
+
}
|
|
4470
|
+
destroy() {
|
|
4471
|
+
this.button.removeEventListener("click", this.toggleHandler);
|
|
4472
|
+
document.removeEventListener("click", this.documentClickHandler);
|
|
4473
|
+
document.removeEventListener("keydown", this.keydownHandler);
|
|
4474
|
+
this.el.remove();
|
|
4475
|
+
}
|
|
4476
|
+
setOpen(open) {
|
|
4477
|
+
this.open = open;
|
|
4478
|
+
this.panel.hidden = !open;
|
|
4479
|
+
this.button.setAttribute("aria-expanded", String(open));
|
|
4480
|
+
this.el.classList.toggle("sp-playlist--open", open);
|
|
4481
|
+
}
|
|
4482
|
+
markActive(currentIndex) {
|
|
4483
|
+
const items = this.panel.querySelectorAll(".sp-playlist__item");
|
|
4484
|
+
items.forEach((item, index) => {
|
|
4485
|
+
item.classList.toggle("sp-playlist__item--active", index === currentIndex);
|
|
4486
|
+
item.setAttribute("aria-current", index === currentIndex ? "true" : "false");
|
|
4487
|
+
});
|
|
4488
|
+
}
|
|
4489
|
+
renderPanel(tracks, currentIndex) {
|
|
4490
|
+
this.panel.textContent = "";
|
|
4491
|
+
tracks.forEach((track, index) => {
|
|
4492
|
+
const item = document.createElement("button");
|
|
4493
|
+
item.type = "button";
|
|
4494
|
+
item.className = "sp-playlist__item";
|
|
4495
|
+
item.setAttribute("role", "menuitem");
|
|
4496
|
+
item.setAttribute("aria-current", index === currentIndex ? "true" : "false");
|
|
4497
|
+
if (index === currentIndex) {
|
|
4498
|
+
item.classList.add("sp-playlist__item--active");
|
|
4499
|
+
}
|
|
4500
|
+
const position = document.createElement("span");
|
|
4501
|
+
position.className = "sp-playlist__position";
|
|
4502
|
+
position.textContent = String(index + 1);
|
|
4503
|
+
const text = document.createElement("span");
|
|
4504
|
+
text.className = "sp-playlist__text";
|
|
4505
|
+
const title = document.createElement("span");
|
|
4506
|
+
title.className = "sp-playlist__title";
|
|
4507
|
+
title.textContent = track.title ?? `Item ${index + 1}`;
|
|
4508
|
+
text.appendChild(title);
|
|
4509
|
+
if (track.artist) {
|
|
4510
|
+
const artist = document.createElement("span");
|
|
4511
|
+
artist.className = "sp-playlist__artist";
|
|
4512
|
+
artist.textContent = track.artist;
|
|
4513
|
+
text.appendChild(artist);
|
|
4514
|
+
}
|
|
4515
|
+
item.appendChild(position);
|
|
4516
|
+
item.appendChild(text);
|
|
4517
|
+
item.addEventListener("click", (event) => {
|
|
4518
|
+
event.stopPropagation();
|
|
4519
|
+
this.options.onSelect(index);
|
|
4520
|
+
this.setOpen(false);
|
|
4521
|
+
});
|
|
4522
|
+
this.panel.appendChild(item);
|
|
4523
|
+
});
|
|
4524
|
+
}
|
|
4525
|
+
};
|
|
4526
|
+
var STYLE_ID = "sp-playlist-styles";
|
|
4527
|
+
var styles = `
|
|
4528
|
+
.sp-playlist-skip[disabled] {
|
|
4529
|
+
opacity: 0.4;
|
|
4530
|
+
cursor: default;
|
|
4531
|
+
}
|
|
4532
|
+
|
|
4533
|
+
.sp-playlist {
|
|
4534
|
+
position: relative;
|
|
4535
|
+
display: inline-flex;
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
.sp-playlist__button svg,
|
|
4539
|
+
.sp-playlist-skip svg {
|
|
4540
|
+
width: 20px;
|
|
4541
|
+
height: 20px;
|
|
4542
|
+
}
|
|
4543
|
+
|
|
4544
|
+
.sp-playlist__panel {
|
|
4545
|
+
position: absolute;
|
|
4546
|
+
bottom: calc(100% + 8px);
|
|
4547
|
+
right: 0;
|
|
4548
|
+
z-index: 20;
|
|
4549
|
+
display: flex;
|
|
4550
|
+
flex-direction: column;
|
|
4551
|
+
min-width: 240px;
|
|
4552
|
+
max-width: 320px;
|
|
4553
|
+
max-height: 300px;
|
|
4554
|
+
overflow-y: auto;
|
|
4555
|
+
padding: 4px;
|
|
4556
|
+
border-radius: 8px;
|
|
4557
|
+
background: rgba(20, 20, 20, 0.96);
|
|
4558
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
.sp-playlist__panel[hidden] {
|
|
4562
|
+
display: none;
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4565
|
+
.sp-playlist__item {
|
|
4566
|
+
display: flex;
|
|
4567
|
+
gap: 10px;
|
|
4568
|
+
align-items: flex-start;
|
|
4569
|
+
width: 100%;
|
|
4570
|
+
padding: 8px 10px;
|
|
4571
|
+
border: 0;
|
|
4572
|
+
border-radius: 6px;
|
|
4573
|
+
background: transparent;
|
|
4574
|
+
color: #fff;
|
|
4575
|
+
font: inherit;
|
|
4576
|
+
text-align: left;
|
|
4577
|
+
cursor: pointer;
|
|
4578
|
+
}
|
|
4579
|
+
|
|
4580
|
+
.sp-playlist__item:hover,
|
|
4581
|
+
.sp-playlist__item:focus-visible {
|
|
4582
|
+
background: rgba(255, 255, 255, 0.12);
|
|
4583
|
+
}
|
|
4584
|
+
|
|
4585
|
+
.sp-playlist__item--active {
|
|
4586
|
+
background: rgba(255, 255, 255, 0.08);
|
|
4587
|
+
}
|
|
4588
|
+
|
|
4589
|
+
.sp-playlist__item--active .sp-playlist__title {
|
|
4590
|
+
font-weight: 600;
|
|
4591
|
+
}
|
|
4592
|
+
|
|
4593
|
+
.sp-playlist__position {
|
|
4594
|
+
flex: 0 0 auto;
|
|
4595
|
+
min-width: 18px;
|
|
4596
|
+
color: rgba(255, 255, 255, 0.7);
|
|
4597
|
+
font-variant-numeric: tabular-nums;
|
|
4598
|
+
font-size: 12px;
|
|
4599
|
+
line-height: 18px;
|
|
4600
|
+
}
|
|
4601
|
+
|
|
4602
|
+
.sp-playlist__text {
|
|
4603
|
+
display: flex;
|
|
4604
|
+
flex-direction: column;
|
|
4605
|
+
min-width: 0;
|
|
4606
|
+
}
|
|
4607
|
+
|
|
4608
|
+
.sp-playlist__title {
|
|
4609
|
+
font-size: 13px;
|
|
4610
|
+
line-height: 18px;
|
|
4611
|
+
}
|
|
4612
|
+
|
|
4613
|
+
.sp-playlist__artist {
|
|
4614
|
+
color: rgba(255, 255, 255, 0.6);
|
|
4615
|
+
font-size: 11px;
|
|
4616
|
+
line-height: 16px;
|
|
4617
|
+
}
|
|
4618
|
+
|
|
4619
|
+
@media (max-width: 480px) {
|
|
4620
|
+
.sp-playlist__panel {
|
|
4621
|
+
min-width: 200px;
|
|
4622
|
+
max-width: 76vw;
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4625
|
+
`;
|
|
4626
|
+
function injectStyles() {
|
|
4627
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID)) {
|
|
4628
|
+
return null;
|
|
4629
|
+
}
|
|
4630
|
+
const el = document.createElement("style");
|
|
4631
|
+
el.id = STYLE_ID;
|
|
4632
|
+
el.textContent = styles;
|
|
4633
|
+
document.head.appendChild(el);
|
|
4634
|
+
return el;
|
|
4635
|
+
}
|
|
4223
4636
|
var DEFAULT_CONFIG$1 = {
|
|
4224
4637
|
autoAdvance: true,
|
|
4225
4638
|
preloadNext: true,
|
|
@@ -4409,8 +4822,42 @@ function createPlaylistPlugin(config) {
|
|
|
4409
4822
|
api?.emit("playlist:ended", void 0);
|
|
4410
4823
|
}
|
|
4411
4824
|
});
|
|
4825
|
+
injectStyles();
|
|
4826
|
+
void import("./hls2.js").then(({ registerControl }) => {
|
|
4827
|
+
const self = plugin;
|
|
4828
|
+
registerControl(
|
|
4829
|
+
"playlist-previous",
|
|
4830
|
+
() => new PlaylistSkipButton(self, "previous")
|
|
4831
|
+
);
|
|
4832
|
+
registerControl("playlist-next", () => new PlaylistSkipButton(self, "next"));
|
|
4833
|
+
registerControl(
|
|
4834
|
+
"playlist",
|
|
4835
|
+
() => new PlaylistPanel(self, {
|
|
4836
|
+
onSelect: (index) => self.play(index)
|
|
4837
|
+
})
|
|
4838
|
+
);
|
|
4839
|
+
}).catch(() => {
|
|
4840
|
+
api?.logger.debug("@scarlett-player/ui not present, playlist controls not registered");
|
|
4841
|
+
});
|
|
4842
|
+
const onKeyDown = (event) => {
|
|
4843
|
+
if (!api || !api.container.contains(document.activeElement)) return;
|
|
4844
|
+
const activeEl = document.activeElement;
|
|
4845
|
+
if (activeEl instanceof HTMLInputElement || activeEl instanceof HTMLTextAreaElement || activeEl instanceof HTMLSelectElement || activeEl?.isContentEditable) {
|
|
4846
|
+
return;
|
|
4847
|
+
}
|
|
4848
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
4849
|
+
if (event.key === "n" || event.key === "N") {
|
|
4850
|
+
event.preventDefault();
|
|
4851
|
+
plugin.next();
|
|
4852
|
+
} else if (event.key === "p" || event.key === "P") {
|
|
4853
|
+
event.preventDefault();
|
|
4854
|
+
plugin.previous();
|
|
4855
|
+
}
|
|
4856
|
+
};
|
|
4857
|
+
document.addEventListener("keydown", onKeyDown);
|
|
4412
4858
|
api.onDestroy(() => {
|
|
4413
4859
|
unsubEnded();
|
|
4860
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
4414
4861
|
if (advanceTimeout) {
|
|
4415
4862
|
clearTimeout(advanceTimeout);
|
|
4416
4863
|
advanceTimeout = null;
|