@scarlett-player/embed 0.5.0 → 0.5.2

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
@@ -33,7 +33,7 @@ function formatBitrate(bitrate) {
33
33
  }
34
34
  return `${bitrate} bps`;
35
35
  }
36
- function mapLevels(levels, currentLevel) {
36
+ function mapLevels(levels, _currentLevel) {
37
37
  return levels.map((level, index) => ({
38
38
  index,
39
39
  width: level.width || 0,
@@ -1007,6 +1007,8 @@ var styles = `
1007
1007
  border-radius: 4px;
1008
1008
  transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;
1009
1009
  flex-shrink: 0;
1010
+ min-width: 44px;
1011
+ min-height: 44px;
1010
1012
  }
1011
1013
 
1012
1014
  @media (hover: hover) {
@@ -1945,6 +1947,9 @@ var ProgressBar = class {
1945
1947
  this.el.setAttribute("role", "slider");
1946
1948
  this.el.setAttribute("aria-label", "Seek");
1947
1949
  this.el.setAttribute("aria-valuemin", "0");
1950
+ this.el.setAttribute("aria-valuemax", "0");
1951
+ this.el.setAttribute("aria-valuenow", "0");
1952
+ this.el.setAttribute("aria-valuetext", "0:00");
1948
1953
  this.el.setAttribute("tabindex", "0");
1949
1954
  this.wrapper.addEventListener("mousedown", this.onMouseDown);
1950
1955
  this.wrapper.addEventListener("mousemove", this.onMouseMove);
@@ -2203,7 +2208,9 @@ var VolumeControl = class {
2203
2208
  this.btn.setAttribute("aria-label", label);
2204
2209
  const displayVolume = muted ? 0 : volume;
2205
2210
  this.level.style.width = `${displayVolume * 100}%`;
2206
- this.slider.setAttribute("aria-valuenow", String(Math.round(displayVolume * 100)));
2211
+ const volumePercent = Math.round(displayVolume * 100);
2212
+ this.slider.setAttribute("aria-valuenow", String(volumePercent));
2213
+ this.slider.setAttribute("aria-valuetext", `${volumePercent}%`);
2207
2214
  }
2208
2215
  toggleMute() {
2209
2216
  const video = getVideo(this.api.container);
@@ -2254,7 +2261,7 @@ var LiveIndicator = class {
2254
2261
  this.el.appendChild(this.dot);
2255
2262
  this.el.appendChild(this.label);
2256
2263
  this.el.setAttribute("role", "button");
2257
- this.el.setAttribute("aria-label", "Seek to live");
2264
+ this.el.setAttribute("aria-label", "Live broadcast - currently at live edge");
2258
2265
  this.el.setAttribute("tabindex", "0");
2259
2266
  this.el.addEventListener("click", this.handleClick);
2260
2267
  this.el.addEventListener("keydown", this.handleKeyDown);
@@ -2269,11 +2276,13 @@ var LiveIndicator = class {
2269
2276
  if (liveEdge) {
2270
2277
  this.el.classList.remove("sp-live--behind");
2271
2278
  this.label.textContent = "LIVE";
2272
- this.el.setAttribute("aria-label", "At live edge");
2279
+ this.dot.setAttribute("aria-hidden", "true");
2280
+ this.el.setAttribute("aria-label", "Live broadcast - currently at live edge");
2273
2281
  } else {
2274
2282
  this.el.classList.add("sp-live--behind");
2275
2283
  this.label.textContent = "GO LIVE";
2276
- this.el.setAttribute("aria-label", "Seek to live");
2284
+ this.dot.setAttribute("aria-hidden", "true");
2285
+ this.el.setAttribute("aria-label", "Live broadcast - behind live edge, click to seek to live");
2277
2286
  }
2278
2287
  }
2279
2288
  seekToLive() {
@@ -2751,6 +2760,18 @@ var SettingsMenu = class {
2751
2760
  this.close();
2752
2761
  this.btn.focus();
2753
2762
  }
2763
+ return;
2764
+ }
2765
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
2766
+ e.preventDefault();
2767
+ e.stopPropagation();
2768
+ this.navigateItems(e.key === "ArrowDown" ? 1 : -1);
2769
+ return;
2770
+ }
2771
+ if (e.key === "Tab") {
2772
+ e.preventDefault();
2773
+ e.stopPropagation();
2774
+ this.navigateItems(e.shiftKey ? -1 : 1);
2754
2775
  }
2755
2776
  };
2756
2777
  document.addEventListener("keydown", this.keyHandler);
@@ -2786,6 +2807,7 @@ var SettingsMenu = class {
2786
2807
  this.renderMainPanel();
2787
2808
  this.panel.classList.add("sp-settings-panel--open");
2788
2809
  this.btn.setAttribute("aria-expanded", "true");
2810
+ this.focusFirstItem();
2789
2811
  }
2790
2812
  close() {
2791
2813
  this.isOpen = false;
@@ -2809,6 +2831,7 @@ var SettingsMenu = class {
2809
2831
  this.renderCaptionsPanel();
2810
2832
  break;
2811
2833
  }
2834
+ this.focusFirstItem();
2812
2835
  }
2813
2836
  renderMainPanel() {
2814
2837
  this.panel.innerHTML = "";
@@ -3027,6 +3050,32 @@ var SettingsMenu = class {
3027
3050
  );
3028
3051
  });
3029
3052
  }
3053
+ getFocusableItems() {
3054
+ return Array.from(
3055
+ this.panel.querySelectorAll('[role="menuitem"]')
3056
+ );
3057
+ }
3058
+ focusFirstItem() {
3059
+ requestAnimationFrame(() => {
3060
+ const items = this.getFocusableItems();
3061
+ if (items.length > 0) {
3062
+ items[0].focus();
3063
+ }
3064
+ });
3065
+ }
3066
+ navigateItems(direction) {
3067
+ const items = this.getFocusableItems();
3068
+ if (items.length === 0) return;
3069
+ const active = document.activeElement;
3070
+ const currentIndex = items.indexOf(active);
3071
+ let nextIndex;
3072
+ if (currentIndex === -1) {
3073
+ nextIndex = direction === 1 ? 0 : items.length - 1;
3074
+ } else {
3075
+ nextIndex = (currentIndex + direction + items.length) % items.length;
3076
+ }
3077
+ items[nextIndex].focus();
3078
+ }
3030
3079
  getPanel() {
3031
3080
  return this.currentPanel;
3032
3081
  }
@@ -3690,6 +3739,8 @@ function createStyles(prefix, theme) {
3690
3739
  align-items: center;
3691
3740
  justify-content: center;
3692
3741
  transition: background 0.2s, transform 0.1s;
3742
+ min-width: 44px;
3743
+ min-height: 44px;
3693
3744
  }
3694
3745
 
3695
3746
  .${prefix}__btn:hover {
@@ -3839,6 +3890,8 @@ function createAudioUIPlugin(config) {
3839
3890
  document.head.appendChild(styleElement);
3840
3891
  container = document.createElement("div");
3841
3892
  container.className = `${prefix} ${prefix}--${layout}`;
3893
+ container.setAttribute("role", "region");
3894
+ container.setAttribute("aria-label", "Audio player");
3842
3895
  if (layout === "full") {
3843
3896
  container.innerHTML = buildFullLayout();
3844
3897
  } else if (layout === "compact") {
@@ -3873,24 +3926,24 @@ function createAudioUIPlugin(config) {
3873
3926
  ${mergedConfig.showArtist ? `<div class="${prefix}__artist">-</div>` : ""}
3874
3927
  </div>
3875
3928
  <div class="${prefix}__progress">
3876
- ${mergedConfig.showTime ? `<span class="${prefix}__time ${prefix}__time--current">0:00</span>` : ""}
3877
- <div class="${prefix}__progress-bar">
3929
+ ${mergedConfig.showTime ? `<span class="${prefix}__time ${prefix}__time--current" aria-label="Current time">0:00</span>` : ""}
3930
+ <div class="${prefix}__progress-bar" role="slider" aria-label="Seek" aria-valuemin="0" aria-valuemax="0" aria-valuenow="0" aria-valuetext="0:00" tabindex="0">
3878
3931
  <div class="${prefix}__progress-fill" style="transform: scaleX(0)"></div>
3879
3932
  </div>
3880
- ${mergedConfig.showTime ? `<span class="${prefix}__time ${prefix}__time--duration">0:00</span>` : ""}
3933
+ ${mergedConfig.showTime ? `<span class="${prefix}__time ${prefix}__time--duration" aria-label="Duration">0:00</span>` : ""}
3881
3934
  </div>
3882
- <div class="${prefix}__controls">
3883
- ${mergedConfig.showShuffle ? `<button class="${prefix}__btn ${prefix}__btn--shuffle" title="Shuffle">${ICONS.shuffle}</button>` : ""}
3884
- ${mergedConfig.showNavigation ? `<button class="${prefix}__btn ${prefix}__btn--prev" title="Previous">${ICONS.previous}</button>` : ""}
3885
- <button class="${prefix}__btn ${prefix}__btn--primary ${prefix}__btn--play" title="Play">${ICONS.play}</button>
3886
- ${mergedConfig.showNavigation ? `<button class="${prefix}__btn ${prefix}__btn--next" title="Next">${ICONS.next}</button>` : ""}
3887
- ${mergedConfig.showRepeat ? `<button class="${prefix}__btn ${prefix}__btn--repeat" title="Repeat">${ICONS.repeatOff}</button>` : ""}
3935
+ <div class="${prefix}__controls" role="group" aria-label="Playback controls">
3936
+ ${mergedConfig.showShuffle ? `<button class="${prefix}__btn ${prefix}__btn--shuffle" title="Shuffle" aria-label="Shuffle" aria-pressed="false">${ICONS.shuffle}</button>` : ""}
3937
+ ${mergedConfig.showNavigation ? `<button class="${prefix}__btn ${prefix}__btn--prev" title="Previous" aria-label="Previous track">${ICONS.previous}</button>` : ""}
3938
+ <button class="${prefix}__btn ${prefix}__btn--primary ${prefix}__btn--play" title="Play" aria-label="Play">${ICONS.play}</button>
3939
+ ${mergedConfig.showNavigation ? `<button class="${prefix}__btn ${prefix}__btn--next" title="Next" aria-label="Next track">${ICONS.next}</button>` : ""}
3940
+ ${mergedConfig.showRepeat ? `<button class="${prefix}__btn ${prefix}__btn--repeat" title="Repeat" aria-label="Repeat" aria-pressed="false">${ICONS.repeatOff}</button>` : ""}
3888
3941
  </div>
3889
3942
  ${mergedConfig.showVolume ? `
3890
3943
  <div class="${prefix}__secondary-controls">
3891
- <div class="${prefix}__volume">
3892
- <button class="${prefix}__btn ${prefix}__btn--volume" title="Volume">${ICONS.volumeHigh}</button>
3893
- <div class="${prefix}__volume-slider">
3944
+ <div class="${prefix}__volume" role="group" aria-label="Volume controls">
3945
+ <button class="${prefix}__btn ${prefix}__btn--volume" title="Volume" aria-label="Mute">${ICONS.volumeHigh}</button>
3946
+ <div class="${prefix}__volume-slider" role="slider" aria-label="Volume" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100" aria-valuetext="100%" tabindex="0">
3894
3947
  <div class="${prefix}__volume-fill" style="width: 100%"></div>
3895
3948
  </div>
3896
3949
  </div>
@@ -3909,21 +3962,21 @@ function createAudioUIPlugin(config) {
3909
3962
  ${mergedConfig.showTitle ? `<div class="${prefix}__title">-</div>` : ""}
3910
3963
  ${mergedConfig.showArtist ? `<div class="${prefix}__artist">-</div>` : ""}
3911
3964
  <div class="${prefix}__progress">
3912
- <div class="${prefix}__progress-bar">
3965
+ <div class="${prefix}__progress-bar" role="slider" aria-label="Seek" aria-valuemin="0" aria-valuemax="0" aria-valuenow="0" aria-valuetext="0:00" tabindex="0">
3913
3966
  <div class="${prefix}__progress-fill" style="transform: scaleX(0)"></div>
3914
3967
  </div>
3915
3968
  </div>
3916
3969
  </div>
3917
- <div class="${prefix}__controls">
3918
- ${mergedConfig.showNavigation ? `<button class="${prefix}__btn ${prefix}__btn--prev" title="Previous">${ICONS.previous}</button>` : ""}
3919
- <button class="${prefix}__btn ${prefix}__btn--primary ${prefix}__btn--play" title="Play">${ICONS.play}</button>
3920
- ${mergedConfig.showNavigation ? `<button class="${prefix}__btn ${prefix}__btn--next" title="Next">${ICONS.next}</button>` : ""}
3970
+ <div class="${prefix}__controls" role="group" aria-label="Playback controls">
3971
+ ${mergedConfig.showNavigation ? `<button class="${prefix}__btn ${prefix}__btn--prev" title="Previous" aria-label="Previous track">${ICONS.previous}</button>` : ""}
3972
+ <button class="${prefix}__btn ${prefix}__btn--primary ${prefix}__btn--play" title="Play" aria-label="Play">${ICONS.play}</button>
3973
+ ${mergedConfig.showNavigation ? `<button class="${prefix}__btn ${prefix}__btn--next" title="Next" aria-label="Next track">${ICONS.next}</button>` : ""}
3921
3974
  </div>
3922
3975
  `;
3923
3976
  };
3924
3977
  const buildMiniLayout = () => {
3925
3978
  return `
3926
- <button class="${prefix}__btn ${prefix}__btn--primary ${prefix}__btn--play" title="Play">${ICONS.play}</button>
3979
+ <button class="${prefix}__btn ${prefix}__btn--primary ${prefix}__btn--play" title="Play" aria-label="Play">${ICONS.play}</button>
3927
3980
  ${mergedConfig.showArtwork ? `
3928
3981
  <div class="${prefix}__artwork">
3929
3982
  <img src="${mergedConfig.defaultArtwork || ""}" alt="Album art" />
@@ -3932,7 +3985,7 @@ function createAudioUIPlugin(config) {
3932
3985
  <div class="${prefix}__info">
3933
3986
  ${mergedConfig.showTitle ? `<div class="${prefix}__title-wrapper"><div class="${prefix}__title">-</div></div>` : ""}
3934
3987
  <div class="${prefix}__progress">
3935
- <div class="${prefix}__progress-bar">
3988
+ <div class="${prefix}__progress-bar" role="slider" aria-label="Seek" aria-valuemin="0" aria-valuemax="0" aria-valuenow="0" aria-valuetext="0:00" tabindex="0">
3936
3989
  <div class="${prefix}__progress-fill" style="transform: scaleX(0)"></div>
3937
3990
  </div>
3938
3991
  </div>
@@ -3998,6 +4051,7 @@ function createAudioUIPlugin(config) {
3998
4051
  if (playPauseBtn) {
3999
4052
  playPauseBtn.innerHTML = playing ? ICONS.pause : ICONS.play;
4000
4053
  playPauseBtn.title = playing ? "Pause" : "Play";
4054
+ playPauseBtn.setAttribute("aria-label", playing ? "Pause" : "Play");
4001
4055
  }
4002
4056
  const currentTime = api.getState("currentTime") || 0;
4003
4057
  const duration = api.getState("duration") || 0;
@@ -4020,6 +4074,12 @@ function createAudioUIPlugin(config) {
4020
4074
  if (durationEl) {
4021
4075
  durationEl.textContent = formatTime(duration);
4022
4076
  }
4077
+ const progressBar = container?.querySelector(`.${prefix}__progress-bar`);
4078
+ if (progressBar) {
4079
+ progressBar.setAttribute("aria-valuemax", String(Math.floor(duration)));
4080
+ progressBar.setAttribute("aria-valuenow", String(Math.floor(currentTime)));
4081
+ progressBar.setAttribute("aria-valuetext", formatTime(currentTime));
4082
+ }
4023
4083
  const title = api.getState("title");
4024
4084
  const poster = api.getState("poster");
4025
4085
  if (titleEl && title) {
@@ -4044,21 +4104,34 @@ function createAudioUIPlugin(config) {
4044
4104
  }
4045
4105
  if (volumeBtn) {
4046
4106
  volumeBtn.innerHTML = muted || volume === 0 ? ICONS.volumeMuted : ICONS.volumeHigh;
4107
+ volumeBtn.setAttribute("aria-label", muted || volume === 0 ? "Unmute" : "Mute");
4108
+ }
4109
+ const volumeSlider = container?.querySelector(`.${prefix}__volume-slider`);
4110
+ if (volumeSlider) {
4111
+ const displayVolume = Math.round((muted ? 0 : volume) * 100);
4112
+ volumeSlider.setAttribute("aria-valuenow", String(displayVolume));
4113
+ volumeSlider.setAttribute("aria-valuetext", `${displayVolume}%`);
4047
4114
  }
4048
4115
  const playlist = api.getPlugin("playlist");
4049
4116
  if (playlist) {
4050
4117
  const state = playlist.getState();
4051
4118
  if (shuffleBtn) {
4052
4119
  shuffleBtn.classList.toggle(`${prefix}__btn--active`, state.shuffle);
4120
+ shuffleBtn.setAttribute("aria-pressed", String(state.shuffle));
4121
+ shuffleBtn.setAttribute("aria-label", state.shuffle ? "Shuffle on" : "Shuffle off");
4053
4122
  }
4054
4123
  if (repeatBtn) {
4055
4124
  repeatBtn.classList.toggle(`${prefix}__btn--active`, state.repeat !== "none");
4125
+ repeatBtn.setAttribute("aria-pressed", String(state.repeat !== "none"));
4056
4126
  if (state.repeat === "one") {
4057
4127
  repeatBtn.innerHTML = ICONS.repeatOne;
4128
+ repeatBtn.setAttribute("aria-label", "Repeat one");
4058
4129
  } else if (state.repeat === "all") {
4059
4130
  repeatBtn.innerHTML = ICONS.repeatAll;
4131
+ repeatBtn.setAttribute("aria-label", "Repeat all");
4060
4132
  } else {
4061
4133
  repeatBtn.innerHTML = ICONS.repeatOff;
4134
+ repeatBtn.setAttribute("aria-label", "Repeat off");
4062
4135
  }
4063
4136
  }
4064
4137
  }
@@ -4491,7 +4564,8 @@ function createAnalyticsPlugin(config) {
4491
4564
  const duration = nextTime - b.time;
4492
4565
  return sum + b.bitrate * duration;
4493
4566
  }, 0);
4494
- session.avgBitrate = Math.round(totalBitrateTime / session.watchTime);
4567
+ const timeSpan = now - session.bitrateHistory[0].time;
4568
+ session.avgBitrate = timeSpan > 0 ? Math.round(totalBitrateTime / timeSpan) : 0;
4495
4569
  }
4496
4570
  const state = {
4497
4571
  currentTime: api.getState("currentTime"),
@@ -6853,6 +6927,7 @@ class ScarlettPlayer {
6853
6927
  this.destroyed = false;
6854
6928
  this.seekingWhilePlaying = false;
6855
6929
  this.seekResumeTimeout = null;
6930
+ this.loadGeneration = 0;
6856
6931
  if (typeof options.container === "string") {
6857
6932
  const el = document.querySelector(options.container);
6858
6933
  if (!el || !(el instanceof HTMLElement)) {
@@ -6926,6 +7001,7 @@ class ScarlettPlayer {
6926
7001
  */
6927
7002
  async load(source) {
6928
7003
  this.checkDestroyed();
7004
+ const generation = ++this.loadGeneration;
6929
7005
  try {
6930
7006
  this.logger.info("Loading source", { source });
6931
7007
  this.stateManager.update({
@@ -6944,6 +7020,10 @@ class ScarlettPlayer {
6944
7020
  await this.pluginManager.destroyPlugin(previousProviderId);
6945
7021
  this._currentProvider = null;
6946
7022
  }
7023
+ if (generation !== this.loadGeneration) {
7024
+ this.logger.info("Load superseded by newer load call", { source });
7025
+ return;
7026
+ }
6947
7027
  const provider = this.pluginManager.selectProvider(source);
6948
7028
  if (!provider) {
6949
7029
  this.errorHandler.throw(
@@ -6959,18 +7039,28 @@ class ScarlettPlayer {
6959
7039
  this._currentProvider = provider;
6960
7040
  this.logger.info("Provider selected", { provider: provider.id });
6961
7041
  await this.pluginManager.initPlugin(provider.id);
7042
+ if (generation !== this.loadGeneration) {
7043
+ this.logger.info("Load superseded by newer load call", { source });
7044
+ return;
7045
+ }
6962
7046
  this.stateManager.set("source", { src: source, type: this.detectMimeType(source) });
6963
7047
  if (typeof provider.loadSource === "function") {
6964
7048
  await provider.loadSource(source);
6965
7049
  }
7050
+ if (generation !== this.loadGeneration) {
7051
+ this.logger.info("Load superseded by newer load call", { source });
7052
+ return;
7053
+ }
6966
7054
  if (this.stateManager.getValue("autoplay")) {
6967
7055
  await this.play();
6968
7056
  }
6969
7057
  } catch (error) {
6970
- this.errorHandler.handle(error, {
6971
- operation: "load",
6972
- source
6973
- });
7058
+ if (generation === this.loadGeneration) {
7059
+ this.errorHandler.handle(error, {
7060
+ operation: "load",
7061
+ source
7062
+ });
7063
+ }
6974
7064
  }
6975
7065
  }
6976
7066
  /**