@scarlett-player/embed 1.5.0 → 1.6.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.
@@ -180,8 +180,8 @@ class StateManager {
180
180
  * Register a state key at runtime, for state a plugin owns.
181
181
  *
182
182
  * Core cannot know every plugin's keys, and {@link get} deliberately throws
183
- * for unregistered ones that throw is a useful typo-catcher and is worth
184
- * keeping so a plugin declares its keys before first use.
183
+ * for unregistered ones - that throw is a useful typo-catcher and is worth
184
+ * keeping - so a plugin declares its keys before first use.
185
185
  *
186
186
  * Idempotent by design: re-defining an existing key leaves the current value
187
187
  * untouched. Plugins commonly re-run setup after a source change, and that
@@ -1259,7 +1259,7 @@ class PluginAPI {
1259
1259
  /**
1260
1260
  * Register a state key this plugin owns, before first use.
1261
1261
  *
1262
- * Idempotent re-defining an existing key keeps its current value.
1262
+ * Idempotent - re-defining an existing key keeps its current value.
1263
1263
  * See {@link IPluginAPI.defineState}.
1264
1264
  *
1265
1265
  * @param key - State property key
@@ -3635,6 +3635,25 @@ var styles = `
3635
3635
  border-radius: inherit;
3636
3636
  }
3637
3637
 
3638
+ /* Chapter markers */
3639
+ .sp-progress__markers {
3640
+ position: absolute;
3641
+ top: 0;
3642
+ left: 0;
3643
+ width: 100%;
3644
+ height: 100%;
3645
+ pointer-events: none;
3646
+ }
3647
+
3648
+ .sp-progress__marker {
3649
+ position: absolute;
3650
+ top: 0;
3651
+ width: 2px;
3652
+ height: 100%;
3653
+ margin-left: -1px;
3654
+ background: rgba(0, 0, 0, 0.65);
3655
+ }
3656
+
3638
3657
  .sp-progress__handle {
3639
3658
  position: absolute;
3640
3659
  top: 50%;
@@ -3694,6 +3713,16 @@ var styles = `
3694
3713
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
3695
3714
  }
3696
3715
 
3716
+ .sp-progress__tooltip-chapter {
3717
+ display: block;
3718
+ max-width: 220px;
3719
+ overflow: hidden;
3720
+ color: rgba(255, 255, 255, 0.75);
3721
+ font-weight: 400;
3722
+ font-variant-numeric: normal;
3723
+ text-overflow: ellipsis;
3724
+ }
3725
+
3697
3726
  @media (hover: hover) {
3698
3727
  .sp-progress-wrapper:hover .sp-progress__tooltip {
3699
3728
  opacity: 1;
@@ -4531,6 +4560,8 @@ var ProgressBar = class {
4531
4560
  this.lastSeekTime = 0;
4532
4561
  this.seekThrottleMs = 100;
4533
4562
  this.wasPlayingBeforeDrag = false;
4563
+ this.renderedChapters = null;
4564
+ this.renderedDuration = 0;
4534
4565
  this.onMouseDown = (e) => {
4535
4566
  e.preventDefault();
4536
4567
  const video = getVideo(this.api.container);
@@ -4665,12 +4696,14 @@ var ProgressBar = class {
4665
4696
  const track = createElement("div", { className: "sp-progress__track" });
4666
4697
  this.buffered = createElement("div", { className: "sp-progress__buffered" });
4667
4698
  this.filled = createElement("div", { className: "sp-progress__filled" });
4699
+ this.markers = createElement("div", { className: "sp-progress__markers" });
4668
4700
  this.handle = createElement("div", { className: "sp-progress__handle" });
4669
4701
  this.tooltip = createElement("div", { className: "sp-progress__tooltip" });
4670
4702
  this.tooltip.textContent = "0:00";
4671
4703
  this.thumbnailPreview = new ThumbnailPreview();
4672
4704
  track.appendChild(this.buffered);
4673
4705
  track.appendChild(this.filled);
4706
+ track.appendChild(this.markers);
4674
4707
  track.appendChild(this.handle);
4675
4708
  this.el.appendChild(track);
4676
4709
  this.el.appendChild(this.thumbnailPreview.getElement());
@@ -4720,6 +4753,7 @@ var ProgressBar = class {
4720
4753
  this.thumbnailPreview.setConfig(thumbnails);
4721
4754
  }
4722
4755
  this.el.classList.toggle("sp-progress--live", !!live);
4756
+ this.updateMarkers(duration, live, seekableRange);
4723
4757
  if (live && seekableRange) {
4724
4758
  const rangeLength = seekableRange.end - seekableRange.start;
4725
4759
  if (rangeLength > 0) {
@@ -4752,6 +4786,70 @@ var ProgressBar = class {
4752
4786
  this.el.setAttribute("aria-valuetext", formatTime(currentTime));
4753
4787
  }
4754
4788
  }
4789
+ /**
4790
+ * Label of the chapter containing a point on the timeline.
4791
+ *
4792
+ * Mirrors the chapters plugin's own lookup: a start time belongs to its
4793
+ * chapter, an end time belongs to the next one, and a point in a gap between
4794
+ * sparse chapters belongs to neither.
4795
+ *
4796
+ * @param time - Position in seconds
4797
+ * @returns The chapter label, or null when the point is outside every chapter
4798
+ */
4799
+ chapterLabelAt(time) {
4800
+ const chapters = this.api.getState("chapters") ?? [];
4801
+ for (let i = chapters.length - 1; i >= 0; i--) {
4802
+ const chapter = chapters[i];
4803
+ if (time < chapter.time) continue;
4804
+ const next = chapters[i + 1];
4805
+ const end = chapter.endTime ?? (next ? next.time : Infinity);
4806
+ return time < end ? chapter.label : null;
4807
+ }
4808
+ return null;
4809
+ }
4810
+ /**
4811
+ * Paint chapter dividers along the track.
4812
+ *
4813
+ * Reads the `chapters` state that core owns, so this works whether the list
4814
+ * came from the chapters plugin or the host set it directly, and renders
4815
+ * nothing at all when there are none.
4816
+ *
4817
+ * Rebuilds only when the list or the duration actually changed. `update()`
4818
+ * runs on every time update, and rebuilding a dozen nodes 4 times a second
4819
+ * would churn the DOM for no reason.
4820
+ *
4821
+ * @param duration - Media duration in seconds
4822
+ * @param live - Whether the media is live
4823
+ * @param seekableRange - DVR window, when the media is live
4824
+ */
4825
+ updateMarkers(duration, live, seekableRange) {
4826
+ const chapters = this.api.getState("chapters") ?? [];
4827
+ const range = live ? seekableRange ? seekableRange.end - seekableRange.start : 0 : duration;
4828
+ if (chapters.length === 0 || range <= 0) {
4829
+ if (this.renderedChapters !== null) {
4830
+ this.markers.textContent = "";
4831
+ this.renderedChapters = null;
4832
+ this.renderedDuration = 0;
4833
+ }
4834
+ return;
4835
+ }
4836
+ if (chapters === this.renderedChapters && range === this.renderedDuration) {
4837
+ return;
4838
+ }
4839
+ const origin = live && seekableRange ? seekableRange.start : 0;
4840
+ this.markers.textContent = "";
4841
+ for (const chapter of chapters) {
4842
+ if (chapter.time <= origin) continue;
4843
+ const percent = (chapter.time - origin) / range * 100;
4844
+ if (percent <= 0 || percent >= 100) continue;
4845
+ const marker = createElement("div", { className: "sp-progress__marker" });
4846
+ marker.style.left = `${percent}%`;
4847
+ marker.title = chapter.label;
4848
+ this.markers.appendChild(marker);
4849
+ }
4850
+ this.renderedChapters = chapters;
4851
+ this.renderedDuration = range;
4852
+ }
4755
4853
  getTimeFromPosition(clientX) {
4756
4854
  const rect = this.el.getBoundingClientRect();
4757
4855
  const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
@@ -4776,6 +4874,12 @@ var ProgressBar = class {
4776
4874
  } else {
4777
4875
  this.tooltip.textContent = formatTime(time);
4778
4876
  }
4877
+ const chapterLabel = this.chapterLabelAt(time);
4878
+ if (chapterLabel) {
4879
+ const label = createElement("span", { className: "sp-progress__tooltip-chapter" });
4880
+ label.textContent = chapterLabel;
4881
+ this.tooltip.appendChild(label);
4882
+ }
4779
4883
  this.tooltip.style.left = `${percent * 100}%`;
4780
4884
  if (this.thumbnailPreview.isConfigured()) {
4781
4885
  this.thumbnailPreview.show(time, percent);
@@ -6187,7 +6291,15 @@ function uiPlugin(config = {}) {
6187
6291
  }
6188
6292
  hideTimeout = setTimeout(hideControls, hideDelay);
6189
6293
  };
6294
+ let last_pointer_type = null;
6295
+ const handlePointerActivity = (event) => {
6296
+ last_pointer_type = event.pointerType;
6297
+ };
6190
6298
  const handleInteraction = () => {
6299
+ if (last_pointer_type === "touch") {
6300
+ const gestures = api?.getPlugin("gestures");
6301
+ if (gestures?.ownsTapInteraction()) return;
6302
+ }
6191
6303
  showControls();
6192
6304
  };
6193
6305
  const handleMouseLeave = () => {
@@ -6321,6 +6433,8 @@ function uiPlugin(config = {}) {
6321
6433
  api.logger.debug(`Control "${id}" registered after init, rebuilding control bar`);
6322
6434
  rebuildControlBar();
6323
6435
  });
6436
+ container.addEventListener("pointerdown", handlePointerActivity, { passive: true });
6437
+ container.addEventListener("pointermove", handlePointerActivity, { passive: true });
6324
6438
  container.addEventListener("mousemove", handleInteraction);
6325
6439
  container.addEventListener("mouseenter", handleInteraction);
6326
6440
  container.addEventListener("mouseleave", handleMouseLeave);
@@ -6358,6 +6472,8 @@ function uiPlugin(config = {}) {
6358
6472
  recoveredUnsubscribe?.();
6359
6473
  recoveredUnsubscribe = null;
6360
6474
  if (api?.container) {
6475
+ api.container.removeEventListener("pointerdown", handlePointerActivity);
6476
+ api.container.removeEventListener("pointermove", handlePointerActivity);
6361
6477
  api.container.removeEventListener("mousemove", handleInteraction);
6362
6478
  api.container.removeEventListener("mouseenter", handleInteraction);
6363
6479
  api.container.removeEventListener("mouseleave", handleMouseLeave);