@scarlett-player/ui 1.1.1 → 1.2.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/README.md CHANGED
@@ -30,9 +30,14 @@ const player = new ScarlettPlayer({
30
30
 
31
31
  - Play/pause, seek, volume controls
32
32
  - Fullscreen toggle
33
+ - Picture-in-Picture toggle (disabled until media metadata is loaded; hidden
34
+ when the browser has no PiP support; Safari webkit presentation mode
35
+ supported)
33
36
  - Quality selector
34
37
  - Progress bar with buffering indicator
35
38
  - Time display (current / duration)
39
+ - Error overlay with viewer-friendly copy per error code, a Try Again action,
40
+ and a reconnecting state while the player self-heals
36
41
  - Keyboard shortcuts
37
42
  - Customizable theming
38
43
  - Auto-hide controls
package/dist/index.cjs CHANGED
@@ -1802,25 +1802,38 @@ var CastButton = class {
1802
1802
  var PipButton = class {
1803
1803
  constructor(api) {
1804
1804
  this.clickHandler = () => {
1805
- this.toggle();
1805
+ void this.toggle().catch(() => {
1806
+ });
1806
1807
  };
1807
1808
  this.api = api;
1808
- const video = document.createElement("video");
1809
- this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in video;
1809
+ const probe = document.createElement("video");
1810
+ this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in probe;
1810
1811
  this.el = createButton("sp-pip", "Picture-in-Picture", icons.pip);
1811
1812
  this.el.addEventListener("click", this.clickHandler);
1812
1813
  if (!this.supported) {
1813
1814
  this.el.style.display = "none";
1815
+ } else {
1816
+ this.el.disabled = true;
1817
+ this.el.setAttribute("aria-disabled", "true");
1814
1818
  }
1815
1819
  }
1816
1820
  render() {
1817
1821
  return this.el;
1818
1822
  }
1823
+ /** Whether the media element is ready to enter PiP (metadata loaded). */
1824
+ isMediaReady() {
1825
+ const video = getVideo(this.api.container);
1826
+ return !!video && video.readyState >= HTMLMediaElement.HAVE_METADATA;
1827
+ }
1819
1828
  update() {
1820
1829
  if (!this.supported) return;
1821
- const pip = this.api.getState("pip");
1822
- this.el.setAttribute("aria-label", pip ? "Exit Picture-in-Picture" : "Picture-in-Picture");
1823
- this.el.classList.toggle("sp-pip--active", !!pip);
1830
+ const pip = !!this.api.getState("pip");
1831
+ const enabled = pip || this.isMediaReady();
1832
+ this.el.disabled = !enabled;
1833
+ setAttr(this.el, "aria-disabled", String(!enabled));
1834
+ setHTML(this.el, pip ? icons.exitPip : icons.pip);
1835
+ setAttr(this.el, "aria-label", pip ? "Exit Picture-in-Picture" : "Picture-in-Picture");
1836
+ this.el.classList.toggle("sp-pip--active", pip);
1824
1837
  }
1825
1838
  async toggle() {
1826
1839
  const video = getVideo(this.api.container);
@@ -1828,8 +1841,14 @@ var PipButton = class {
1828
1841
  this.api.logger.warn("PiP: video element not found");
1829
1842
  return;
1830
1843
  }
1844
+ const isInPip = document.pictureInPictureElement === video || video.webkitPresentationMode === "picture-in-picture";
1845
+ if (!isInPip && video.readyState < HTMLMediaElement.HAVE_METADATA) {
1846
+ this.api.logger.debug("PiP: ignored, media not ready", {
1847
+ readyState: video.readyState
1848
+ });
1849
+ return;
1850
+ }
1831
1851
  try {
1832
- const isInPip = document.pictureInPictureElement === video || video.webkitPresentationMode === "picture-in-picture";
1833
1852
  if (isInPip) {
1834
1853
  if (document.pictureInPictureElement) {
1835
1854
  await document.exitPictureInPicture();
@@ -1846,7 +1865,8 @@ var PipButton = class {
1846
1865
  this.api.logger.debug("PiP: entered");
1847
1866
  }
1848
1867
  } catch (e) {
1849
- this.api.logger.warn("PiP: failed", { error: e.message });
1868
+ const message = e instanceof Error ? e.message : String(e);
1869
+ this.api.logger.warn("PiP: failed", { error: message });
1850
1870
  }
1851
1871
  }
1852
1872
  destroy() {
@@ -1929,6 +1949,12 @@ function getUserMessage(error) {
1929
1949
  return "Unable to load video. Please try again.";
1930
1950
  case "PLAYBACK_FAILED":
1931
1951
  return "Playback stopped unexpectedly. Please try again.";
1952
+ case "MEDIA_APPEND_ERROR":
1953
+ return "Video playback was interrupted. Please try again.";
1954
+ case "MEDIA_BUFFER_FULL":
1955
+ return "Your device is low on video memory. Close other apps or tabs and try again.";
1956
+ case "PLAYLIST_INVALID":
1957
+ return "The stream is temporarily unavailable. Please try again.";
1932
1958
  }
1933
1959
  }
1934
1960
  const msg = error.message?.toLowerCase() || "";
@@ -2730,7 +2756,12 @@ function uiPlugin(config = {}) {
2730
2756
  case " ":
2731
2757
  case "k":
2732
2758
  e.preventDefault();
2733
- video.paused ? video.play() : video.pause();
2759
+ if (video.paused) {
2760
+ video.play().catch(() => {
2761
+ });
2762
+ } else {
2763
+ video.pause();
2764
+ }
2734
2765
  break;
2735
2766
  case "m":
2736
2767
  e.preventDefault();
@@ -2739,9 +2770,11 @@ function uiPlugin(config = {}) {
2739
2770
  case "f":
2740
2771
  e.preventDefault();
2741
2772
  if (document.fullscreenElement) {
2742
- document.exitFullscreen();
2773
+ document.exitFullscreen().catch(() => {
2774
+ });
2743
2775
  } else {
2744
- api.container.requestFullscreen?.();
2776
+ api.container.requestFullscreen?.().catch(() => {
2777
+ });
2745
2778
  }
2746
2779
  break;
2747
2780
  case "ArrowLeft":
package/dist/index.js CHANGED
@@ -1771,25 +1771,38 @@ var CastButton = class {
1771
1771
  var PipButton = class {
1772
1772
  constructor(api) {
1773
1773
  this.clickHandler = () => {
1774
- this.toggle();
1774
+ void this.toggle().catch(() => {
1775
+ });
1775
1776
  };
1776
1777
  this.api = api;
1777
- const video = document.createElement("video");
1778
- this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in video;
1778
+ const probe = document.createElement("video");
1779
+ this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in probe;
1779
1780
  this.el = createButton("sp-pip", "Picture-in-Picture", icons.pip);
1780
1781
  this.el.addEventListener("click", this.clickHandler);
1781
1782
  if (!this.supported) {
1782
1783
  this.el.style.display = "none";
1784
+ } else {
1785
+ this.el.disabled = true;
1786
+ this.el.setAttribute("aria-disabled", "true");
1783
1787
  }
1784
1788
  }
1785
1789
  render() {
1786
1790
  return this.el;
1787
1791
  }
1792
+ /** Whether the media element is ready to enter PiP (metadata loaded). */
1793
+ isMediaReady() {
1794
+ const video = getVideo(this.api.container);
1795
+ return !!video && video.readyState >= HTMLMediaElement.HAVE_METADATA;
1796
+ }
1788
1797
  update() {
1789
1798
  if (!this.supported) return;
1790
- const pip = this.api.getState("pip");
1791
- this.el.setAttribute("aria-label", pip ? "Exit Picture-in-Picture" : "Picture-in-Picture");
1792
- this.el.classList.toggle("sp-pip--active", !!pip);
1799
+ const pip = !!this.api.getState("pip");
1800
+ const enabled = pip || this.isMediaReady();
1801
+ this.el.disabled = !enabled;
1802
+ setAttr(this.el, "aria-disabled", String(!enabled));
1803
+ setHTML(this.el, pip ? icons.exitPip : icons.pip);
1804
+ setAttr(this.el, "aria-label", pip ? "Exit Picture-in-Picture" : "Picture-in-Picture");
1805
+ this.el.classList.toggle("sp-pip--active", pip);
1793
1806
  }
1794
1807
  async toggle() {
1795
1808
  const video = getVideo(this.api.container);
@@ -1797,8 +1810,14 @@ var PipButton = class {
1797
1810
  this.api.logger.warn("PiP: video element not found");
1798
1811
  return;
1799
1812
  }
1813
+ const isInPip = document.pictureInPictureElement === video || video.webkitPresentationMode === "picture-in-picture";
1814
+ if (!isInPip && video.readyState < HTMLMediaElement.HAVE_METADATA) {
1815
+ this.api.logger.debug("PiP: ignored, media not ready", {
1816
+ readyState: video.readyState
1817
+ });
1818
+ return;
1819
+ }
1800
1820
  try {
1801
- const isInPip = document.pictureInPictureElement === video || video.webkitPresentationMode === "picture-in-picture";
1802
1821
  if (isInPip) {
1803
1822
  if (document.pictureInPictureElement) {
1804
1823
  await document.exitPictureInPicture();
@@ -1815,7 +1834,8 @@ var PipButton = class {
1815
1834
  this.api.logger.debug("PiP: entered");
1816
1835
  }
1817
1836
  } catch (e) {
1818
- this.api.logger.warn("PiP: failed", { error: e.message });
1837
+ const message = e instanceof Error ? e.message : String(e);
1838
+ this.api.logger.warn("PiP: failed", { error: message });
1819
1839
  }
1820
1840
  }
1821
1841
  destroy() {
@@ -1898,6 +1918,12 @@ function getUserMessage(error) {
1898
1918
  return "Unable to load video. Please try again.";
1899
1919
  case "PLAYBACK_FAILED":
1900
1920
  return "Playback stopped unexpectedly. Please try again.";
1921
+ case "MEDIA_APPEND_ERROR":
1922
+ return "Video playback was interrupted. Please try again.";
1923
+ case "MEDIA_BUFFER_FULL":
1924
+ return "Your device is low on video memory. Close other apps or tabs and try again.";
1925
+ case "PLAYLIST_INVALID":
1926
+ return "The stream is temporarily unavailable. Please try again.";
1901
1927
  }
1902
1928
  }
1903
1929
  const msg = error.message?.toLowerCase() || "";
@@ -2699,7 +2725,12 @@ function uiPlugin(config = {}) {
2699
2725
  case " ":
2700
2726
  case "k":
2701
2727
  e.preventDefault();
2702
- video.paused ? video.play() : video.pause();
2728
+ if (video.paused) {
2729
+ video.play().catch(() => {
2730
+ });
2731
+ } else {
2732
+ video.pause();
2733
+ }
2703
2734
  break;
2704
2735
  case "m":
2705
2736
  e.preventDefault();
@@ -2708,9 +2739,11 @@ function uiPlugin(config = {}) {
2708
2739
  case "f":
2709
2740
  e.preventDefault();
2710
2741
  if (document.fullscreenElement) {
2711
- document.exitFullscreen();
2742
+ document.exitFullscreen().catch(() => {
2743
+ });
2712
2744
  } else {
2713
- api.container.requestFullscreen?.();
2745
+ api.container.requestFullscreen?.().catch(() => {
2746
+ });
2714
2747
  }
2715
2748
  break;
2716
2749
  case "ArrowLeft":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scarlett-player/ui",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "UI Controls Plugin for Scarlett Player",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -29,7 +29,7 @@
29
29
  "typescript": "^5.3.0",
30
30
  "vitest": "^1.6.0",
31
31
  "jsdom": "^24.0.0",
32
- "@scarlett-player/core": "1.1.1"
32
+ "@scarlett-player/core": "1.2.0"
33
33
  },
34
34
  "keywords": [
35
35
  "video",