@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.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 index = this.handlers.indexOf(handler);
924
- if (index !== -1) {
925
- this.handlers.splice(index, 1);
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(index) {
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 (index !== -1) {
2009
+ if (index2 !== -1) {
1995
2010
  const levels = this.getQualities();
1996
- if (levels.length > 0 && (index < 0 || index >= levels.length)) {
1997
- this.logger.warn(`Invalid quality index: ${index} (available: ${levels.length})`);
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(index);
2016
+ provider.setLevel(index2);
2002
2017
  this.eventBus.emit("quality:change", {
2003
- quality: index === -1 ? "auto" : `level-${index}`,
2004
- auto: index === -1
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, index) => ({
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, index) => ({
2383
- id: `level-${index}`,
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: index === hls.currentLevel
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
- emitFatalError(hls_error, false);
3049
+ handleNativeFatalError(hls_error);
2949
3050
  };
2950
3051
  videoEl.addEventListener("error", onFatalVideoError);
2951
- const removeFatalListener = () => videoEl.removeEventListener("error", onFatalVideoError);
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
- if (Date.now() - reconnectWindowStart > window_ms) {
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", { attempt: reconnectAttempts + 1, delayMs: delay });
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", void 0);
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(index) {
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 = index;
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 index = Math.floor(time / interval);
4508
- const col = index % columns;
4509
- const row = Math.floor(index / columns);
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 = (index) => {
7875
- if (index < 0 || index >= tracks.length) {
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[index];
7880
- currentIndex = index;
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 index = tracks.length;
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(index, track) {
8535
+ insert(index2, track) {
7957
8536
  const normalizedTrack = { ...track, id: track.id || generateId() };
7958
- const clampedIndex = Math.max(0, Math.min(index, tracks.length));
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 index;
8551
+ let index2;
7973
8552
  if (typeof idOrIndex === "string") {
7974
- index = tracks.findIndex((t) => t.id === idOrIndex);
7975
- if (index === -1) {
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
- index = idOrIndex;
8559
+ index2 = idOrIndex;
7981
8560
  }
7982
- if (index < 0 || index >= tracks.length) {
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(index, 1);
7987
- if (index < currentIndex) {
8565
+ const [removedTrack] = tracks.splice(index2, 1);
8566
+ if (index2 < currentIndex) {
7988
8567
  currentIndex--;
7989
- } else if (index === currentIndex) {
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 !== index).map((i) => i > index ? i - 1 : 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 index;
8588
+ let index2;
8010
8589
  if (idOrIndex === void 0) {
8011
- index = currentIndex >= 0 ? currentIndex : shuffle ? getActualIndex(0) : 0;
8590
+ index2 = currentIndex >= 0 ? currentIndex : shuffle ? getActualIndex(0) : 0;
8012
8591
  } else if (typeof idOrIndex === "string") {
8013
- index = tracks.findIndex((t) => t.id === idOrIndex);
8014
- if (index === -1) {
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
- index = idOrIndex;
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(index);
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, index) => ({
8969
- id: `item-${index}`,
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,