@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 +5 -0
- package/dist/index.cjs +44 -11
- package/dist/index.js +44 -11
- package/package.json +2 -2
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
|
|
1809
|
-
this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in
|
|
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
|
-
|
|
1823
|
-
this.el.
|
|
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
|
-
|
|
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
|
|
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
|
|
1778
|
-
this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in
|
|
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
|
-
|
|
1792
|
-
this.el.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
32
|
+
"@scarlett-player/core": "1.2.0"
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|
|
35
35
|
"video",
|