@scarlett-player/embed 0.5.1 → 0.5.3
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.audio.js +140 -27
- package/dist/embed.audio.js.map +1 -1
- package/dist/embed.audio.umd.cjs +1 -1
- package/dist/embed.audio.umd.cjs.map +1 -1
- package/dist/embed.js +201 -32
- package/dist/embed.js.map +1 -1
- package/dist/embed.umd.cjs +1 -1
- package/dist/embed.umd.cjs.map +1 -1
- package/dist/embed.video.js +150 -12
- package/dist/embed.video.js.map +1 -1
- package/dist/embed.video.umd.cjs +1 -1
- package/dist/embed.video.umd.cjs.map +1 -1
- package/package.json +9 -9
package/dist/embed.js
CHANGED
|
@@ -123,12 +123,44 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
123
123
|
addHandler("hlsLevelLoaded", (_event, data) => {
|
|
124
124
|
if (data.details?.live !== void 0) {
|
|
125
125
|
api.setState("live", data.details.live);
|
|
126
|
+
if (data.details.live) {
|
|
127
|
+
const video = hls.media;
|
|
128
|
+
if (video && video.seekable && video.seekable.length > 0) {
|
|
129
|
+
const start = video.seekable.start(0);
|
|
130
|
+
const end = video.seekable.end(video.seekable.length - 1);
|
|
131
|
+
api.setState("seekableRange", { start, end });
|
|
132
|
+
const threshold = (data.details.targetduration ?? 3) * 3;
|
|
133
|
+
const isAtLiveEdge = end - video.currentTime < threshold;
|
|
134
|
+
api.setState("liveEdge", isAtLiveEdge);
|
|
135
|
+
const latency = end - video.currentTime;
|
|
136
|
+
api.setState("liveLatency", Math.max(0, latency));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
126
139
|
callbacks.onLiveUpdate?.();
|
|
127
140
|
}
|
|
128
141
|
});
|
|
129
142
|
addHandler("hlsError", (_event, data) => {
|
|
130
143
|
const error = parseHlsError(data);
|
|
131
|
-
|
|
144
|
+
const isBufferHoleSeek = !error.fatal && (error.details?.includes("bufferStalledError") || data.reason?.includes("buffer holes"));
|
|
145
|
+
if (isBufferHoleSeek) {
|
|
146
|
+
api.logger.debug(`HLS buffer recovery: ${error.reason || error.details}`, {
|
|
147
|
+
details: error.details,
|
|
148
|
+
reason: error.reason
|
|
149
|
+
});
|
|
150
|
+
} else if (error.fatal) {
|
|
151
|
+
api.logger.error(`HLS fatal error: ${error.details} (type=${error.type})`, {
|
|
152
|
+
type: error.type,
|
|
153
|
+
details: error.details,
|
|
154
|
+
url: error.url
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
api.logger.warn(`HLS error: ${error.details} (type=${error.type}, fatal=${error.fatal})`, {
|
|
158
|
+
type: error.type,
|
|
159
|
+
details: error.details,
|
|
160
|
+
fatal: error.fatal,
|
|
161
|
+
url: error.url
|
|
162
|
+
});
|
|
163
|
+
}
|
|
132
164
|
callbacks.onError?.(error);
|
|
133
165
|
});
|
|
134
166
|
return () => {
|
|
@@ -150,6 +182,8 @@ function setupVideoEventHandlers(video, api) {
|
|
|
150
182
|
addHandler("playing", () => {
|
|
151
183
|
api.setState("playing", true);
|
|
152
184
|
api.setState("paused", false);
|
|
185
|
+
api.setState("waiting", false);
|
|
186
|
+
api.setState("buffering", false);
|
|
153
187
|
api.setState("playbackState", "playing");
|
|
154
188
|
});
|
|
155
189
|
addHandler("pause", () => {
|
|
@@ -166,6 +200,15 @@ function setupVideoEventHandlers(video, api) {
|
|
|
166
200
|
addHandler("timeupdate", () => {
|
|
167
201
|
api.setState("currentTime", video.currentTime);
|
|
168
202
|
api.emit("playback:timeupdate", { currentTime: video.currentTime });
|
|
203
|
+
const isLive = api.getState("live");
|
|
204
|
+
if (isLive && video.seekable && video.seekable.length > 0) {
|
|
205
|
+
const start = video.seekable.start(0);
|
|
206
|
+
const end = video.seekable.end(video.seekable.length - 1);
|
|
207
|
+
api.setState("seekableRange", { start, end });
|
|
208
|
+
const isAtLiveEdge = end - video.currentTime < 10;
|
|
209
|
+
api.setState("liveEdge", isAtLiveEdge);
|
|
210
|
+
api.setState("liveLatency", Math.max(0, end - video.currentTime));
|
|
211
|
+
}
|
|
169
212
|
});
|
|
170
213
|
addHandler("durationchange", () => {
|
|
171
214
|
api.setState("duration", video.duration || 0);
|
|
@@ -1007,6 +1050,8 @@ var styles = `
|
|
|
1007
1050
|
border-radius: 4px;
|
|
1008
1051
|
transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;
|
|
1009
1052
|
flex-shrink: 0;
|
|
1053
|
+
min-width: 44px;
|
|
1054
|
+
min-height: 44px;
|
|
1010
1055
|
}
|
|
1011
1056
|
|
|
1012
1057
|
@media (hover: hover) {
|
|
@@ -1945,6 +1990,9 @@ var ProgressBar = class {
|
|
|
1945
1990
|
this.el.setAttribute("role", "slider");
|
|
1946
1991
|
this.el.setAttribute("aria-label", "Seek");
|
|
1947
1992
|
this.el.setAttribute("aria-valuemin", "0");
|
|
1993
|
+
this.el.setAttribute("aria-valuemax", "0");
|
|
1994
|
+
this.el.setAttribute("aria-valuenow", "0");
|
|
1995
|
+
this.el.setAttribute("aria-valuetext", "0:00");
|
|
1948
1996
|
this.el.setAttribute("tabindex", "0");
|
|
1949
1997
|
this.wrapper.addEventListener("mousedown", this.onMouseDown);
|
|
1950
1998
|
this.wrapper.addEventListener("mousemove", this.onMouseMove);
|
|
@@ -2203,7 +2251,9 @@ var VolumeControl = class {
|
|
|
2203
2251
|
this.btn.setAttribute("aria-label", label);
|
|
2204
2252
|
const displayVolume = muted ? 0 : volume;
|
|
2205
2253
|
this.level.style.width = `${displayVolume * 100}%`;
|
|
2206
|
-
|
|
2254
|
+
const volumePercent = Math.round(displayVolume * 100);
|
|
2255
|
+
this.slider.setAttribute("aria-valuenow", String(volumePercent));
|
|
2256
|
+
this.slider.setAttribute("aria-valuetext", `${volumePercent}%`);
|
|
2207
2257
|
}
|
|
2208
2258
|
toggleMute() {
|
|
2209
2259
|
const video = getVideo(this.api.container);
|
|
@@ -2254,7 +2304,7 @@ var LiveIndicator = class {
|
|
|
2254
2304
|
this.el.appendChild(this.dot);
|
|
2255
2305
|
this.el.appendChild(this.label);
|
|
2256
2306
|
this.el.setAttribute("role", "button");
|
|
2257
|
-
this.el.setAttribute("aria-label", "
|
|
2307
|
+
this.el.setAttribute("aria-label", "Live broadcast - currently at live edge");
|
|
2258
2308
|
this.el.setAttribute("tabindex", "0");
|
|
2259
2309
|
this.el.addEventListener("click", this.handleClick);
|
|
2260
2310
|
this.el.addEventListener("keydown", this.handleKeyDown);
|
|
@@ -2269,11 +2319,13 @@ var LiveIndicator = class {
|
|
|
2269
2319
|
if (liveEdge) {
|
|
2270
2320
|
this.el.classList.remove("sp-live--behind");
|
|
2271
2321
|
this.label.textContent = "LIVE";
|
|
2272
|
-
this.
|
|
2322
|
+
this.dot.setAttribute("aria-hidden", "true");
|
|
2323
|
+
this.el.setAttribute("aria-label", "Live broadcast - currently at live edge");
|
|
2273
2324
|
} else {
|
|
2274
2325
|
this.el.classList.add("sp-live--behind");
|
|
2275
2326
|
this.label.textContent = "GO LIVE";
|
|
2276
|
-
this.
|
|
2327
|
+
this.dot.setAttribute("aria-hidden", "true");
|
|
2328
|
+
this.el.setAttribute("aria-label", "Live broadcast - behind live edge, click to seek to live");
|
|
2277
2329
|
}
|
|
2278
2330
|
}
|
|
2279
2331
|
seekToLive() {
|
|
@@ -2751,6 +2803,18 @@ var SettingsMenu = class {
|
|
|
2751
2803
|
this.close();
|
|
2752
2804
|
this.btn.focus();
|
|
2753
2805
|
}
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
2809
|
+
e.preventDefault();
|
|
2810
|
+
e.stopPropagation();
|
|
2811
|
+
this.navigateItems(e.key === "ArrowDown" ? 1 : -1);
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
if (e.key === "Tab") {
|
|
2815
|
+
e.preventDefault();
|
|
2816
|
+
e.stopPropagation();
|
|
2817
|
+
this.navigateItems(e.shiftKey ? -1 : 1);
|
|
2754
2818
|
}
|
|
2755
2819
|
};
|
|
2756
2820
|
document.addEventListener("keydown", this.keyHandler);
|
|
@@ -2786,6 +2850,7 @@ var SettingsMenu = class {
|
|
|
2786
2850
|
this.renderMainPanel();
|
|
2787
2851
|
this.panel.classList.add("sp-settings-panel--open");
|
|
2788
2852
|
this.btn.setAttribute("aria-expanded", "true");
|
|
2853
|
+
this.focusFirstItem();
|
|
2789
2854
|
}
|
|
2790
2855
|
close() {
|
|
2791
2856
|
this.isOpen = false;
|
|
@@ -2809,6 +2874,7 @@ var SettingsMenu = class {
|
|
|
2809
2874
|
this.renderCaptionsPanel();
|
|
2810
2875
|
break;
|
|
2811
2876
|
}
|
|
2877
|
+
this.focusFirstItem();
|
|
2812
2878
|
}
|
|
2813
2879
|
renderMainPanel() {
|
|
2814
2880
|
this.panel.innerHTML = "";
|
|
@@ -3027,6 +3093,32 @@ var SettingsMenu = class {
|
|
|
3027
3093
|
);
|
|
3028
3094
|
});
|
|
3029
3095
|
}
|
|
3096
|
+
getFocusableItems() {
|
|
3097
|
+
return Array.from(
|
|
3098
|
+
this.panel.querySelectorAll('[role="menuitem"]')
|
|
3099
|
+
);
|
|
3100
|
+
}
|
|
3101
|
+
focusFirstItem() {
|
|
3102
|
+
requestAnimationFrame(() => {
|
|
3103
|
+
const items = this.getFocusableItems();
|
|
3104
|
+
if (items.length > 0) {
|
|
3105
|
+
items[0].focus();
|
|
3106
|
+
}
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
navigateItems(direction) {
|
|
3110
|
+
const items = this.getFocusableItems();
|
|
3111
|
+
if (items.length === 0) return;
|
|
3112
|
+
const active = document.activeElement;
|
|
3113
|
+
const currentIndex = items.indexOf(active);
|
|
3114
|
+
let nextIndex;
|
|
3115
|
+
if (currentIndex === -1) {
|
|
3116
|
+
nextIndex = direction === 1 ? 0 : items.length - 1;
|
|
3117
|
+
} else {
|
|
3118
|
+
nextIndex = (currentIndex + direction + items.length) % items.length;
|
|
3119
|
+
}
|
|
3120
|
+
items[nextIndex].focus();
|
|
3121
|
+
}
|
|
3030
3122
|
getPanel() {
|
|
3031
3123
|
return this.currentPanel;
|
|
3032
3124
|
}
|
|
@@ -3690,6 +3782,8 @@ function createStyles(prefix, theme) {
|
|
|
3690
3782
|
align-items: center;
|
|
3691
3783
|
justify-content: center;
|
|
3692
3784
|
transition: background 0.2s, transform 0.1s;
|
|
3785
|
+
min-width: 44px;
|
|
3786
|
+
min-height: 44px;
|
|
3693
3787
|
}
|
|
3694
3788
|
|
|
3695
3789
|
.${prefix}__btn:hover {
|
|
@@ -3839,6 +3933,8 @@ function createAudioUIPlugin(config) {
|
|
|
3839
3933
|
document.head.appendChild(styleElement);
|
|
3840
3934
|
container = document.createElement("div");
|
|
3841
3935
|
container.className = `${prefix} ${prefix}--${layout}`;
|
|
3936
|
+
container.setAttribute("role", "region");
|
|
3937
|
+
container.setAttribute("aria-label", "Audio player");
|
|
3842
3938
|
if (layout === "full") {
|
|
3843
3939
|
container.innerHTML = buildFullLayout();
|
|
3844
3940
|
} else if (layout === "compact") {
|
|
@@ -3873,24 +3969,24 @@ function createAudioUIPlugin(config) {
|
|
|
3873
3969
|
${mergedConfig.showArtist ? `<div class="${prefix}__artist">-</div>` : ""}
|
|
3874
3970
|
</div>
|
|
3875
3971
|
<div class="${prefix}__progress">
|
|
3876
|
-
${mergedConfig.showTime ? `<span class="${prefix}__time ${prefix}__time--current">0:00</span>` : ""}
|
|
3877
|
-
<div class="${prefix}__progress-bar">
|
|
3972
|
+
${mergedConfig.showTime ? `<span class="${prefix}__time ${prefix}__time--current" aria-label="Current time">0:00</span>` : ""}
|
|
3973
|
+
<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
3974
|
<div class="${prefix}__progress-fill" style="transform: scaleX(0)"></div>
|
|
3879
3975
|
</div>
|
|
3880
|
-
${mergedConfig.showTime ? `<span class="${prefix}__time ${prefix}__time--duration">0:00</span>` : ""}
|
|
3976
|
+
${mergedConfig.showTime ? `<span class="${prefix}__time ${prefix}__time--duration" aria-label="Duration">0:00</span>` : ""}
|
|
3881
3977
|
</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>` : ""}
|
|
3978
|
+
<div class="${prefix}__controls" role="group" aria-label="Playback controls">
|
|
3979
|
+
${mergedConfig.showShuffle ? `<button class="${prefix}__btn ${prefix}__btn--shuffle" title="Shuffle" aria-label="Shuffle" aria-pressed="false">${ICONS.shuffle}</button>` : ""}
|
|
3980
|
+
${mergedConfig.showNavigation ? `<button class="${prefix}__btn ${prefix}__btn--prev" title="Previous" aria-label="Previous track">${ICONS.previous}</button>` : ""}
|
|
3981
|
+
<button class="${prefix}__btn ${prefix}__btn--primary ${prefix}__btn--play" title="Play" aria-label="Play">${ICONS.play}</button>
|
|
3982
|
+
${mergedConfig.showNavigation ? `<button class="${prefix}__btn ${prefix}__btn--next" title="Next" aria-label="Next track">${ICONS.next}</button>` : ""}
|
|
3983
|
+
${mergedConfig.showRepeat ? `<button class="${prefix}__btn ${prefix}__btn--repeat" title="Repeat" aria-label="Repeat" aria-pressed="false">${ICONS.repeatOff}</button>` : ""}
|
|
3888
3984
|
</div>
|
|
3889
3985
|
${mergedConfig.showVolume ? `
|
|
3890
3986
|
<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">
|
|
3987
|
+
<div class="${prefix}__volume" role="group" aria-label="Volume controls">
|
|
3988
|
+
<button class="${prefix}__btn ${prefix}__btn--volume" title="Volume" aria-label="Mute">${ICONS.volumeHigh}</button>
|
|
3989
|
+
<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
3990
|
<div class="${prefix}__volume-fill" style="width: 100%"></div>
|
|
3895
3991
|
</div>
|
|
3896
3992
|
</div>
|
|
@@ -3909,21 +4005,21 @@ function createAudioUIPlugin(config) {
|
|
|
3909
4005
|
${mergedConfig.showTitle ? `<div class="${prefix}__title">-</div>` : ""}
|
|
3910
4006
|
${mergedConfig.showArtist ? `<div class="${prefix}__artist">-</div>` : ""}
|
|
3911
4007
|
<div class="${prefix}__progress">
|
|
3912
|
-
<div class="${prefix}__progress-bar">
|
|
4008
|
+
<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
4009
|
<div class="${prefix}__progress-fill" style="transform: scaleX(0)"></div>
|
|
3914
4010
|
</div>
|
|
3915
4011
|
</div>
|
|
3916
4012
|
</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>` : ""}
|
|
4013
|
+
<div class="${prefix}__controls" role="group" aria-label="Playback controls">
|
|
4014
|
+
${mergedConfig.showNavigation ? `<button class="${prefix}__btn ${prefix}__btn--prev" title="Previous" aria-label="Previous track">${ICONS.previous}</button>` : ""}
|
|
4015
|
+
<button class="${prefix}__btn ${prefix}__btn--primary ${prefix}__btn--play" title="Play" aria-label="Play">${ICONS.play}</button>
|
|
4016
|
+
${mergedConfig.showNavigation ? `<button class="${prefix}__btn ${prefix}__btn--next" title="Next" aria-label="Next track">${ICONS.next}</button>` : ""}
|
|
3921
4017
|
</div>
|
|
3922
4018
|
`;
|
|
3923
4019
|
};
|
|
3924
4020
|
const buildMiniLayout = () => {
|
|
3925
4021
|
return `
|
|
3926
|
-
<button class="${prefix}__btn ${prefix}__btn--primary ${prefix}__btn--play" title="Play">${ICONS.play}</button>
|
|
4022
|
+
<button class="${prefix}__btn ${prefix}__btn--primary ${prefix}__btn--play" title="Play" aria-label="Play">${ICONS.play}</button>
|
|
3927
4023
|
${mergedConfig.showArtwork ? `
|
|
3928
4024
|
<div class="${prefix}__artwork">
|
|
3929
4025
|
<img src="${mergedConfig.defaultArtwork || ""}" alt="Album art" />
|
|
@@ -3932,7 +4028,7 @@ function createAudioUIPlugin(config) {
|
|
|
3932
4028
|
<div class="${prefix}__info">
|
|
3933
4029
|
${mergedConfig.showTitle ? `<div class="${prefix}__title-wrapper"><div class="${prefix}__title">-</div></div>` : ""}
|
|
3934
4030
|
<div class="${prefix}__progress">
|
|
3935
|
-
<div class="${prefix}__progress-bar">
|
|
4031
|
+
<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
4032
|
<div class="${prefix}__progress-fill" style="transform: scaleX(0)"></div>
|
|
3937
4033
|
</div>
|
|
3938
4034
|
</div>
|
|
@@ -3998,6 +4094,7 @@ function createAudioUIPlugin(config) {
|
|
|
3998
4094
|
if (playPauseBtn) {
|
|
3999
4095
|
playPauseBtn.innerHTML = playing ? ICONS.pause : ICONS.play;
|
|
4000
4096
|
playPauseBtn.title = playing ? "Pause" : "Play";
|
|
4097
|
+
playPauseBtn.setAttribute("aria-label", playing ? "Pause" : "Play");
|
|
4001
4098
|
}
|
|
4002
4099
|
const currentTime = api.getState("currentTime") || 0;
|
|
4003
4100
|
const duration = api.getState("duration") || 0;
|
|
@@ -4020,6 +4117,12 @@ function createAudioUIPlugin(config) {
|
|
|
4020
4117
|
if (durationEl) {
|
|
4021
4118
|
durationEl.textContent = formatTime(duration);
|
|
4022
4119
|
}
|
|
4120
|
+
const progressBar = container?.querySelector(`.${prefix}__progress-bar`);
|
|
4121
|
+
if (progressBar) {
|
|
4122
|
+
progressBar.setAttribute("aria-valuemax", String(Math.floor(duration)));
|
|
4123
|
+
progressBar.setAttribute("aria-valuenow", String(Math.floor(currentTime)));
|
|
4124
|
+
progressBar.setAttribute("aria-valuetext", formatTime(currentTime));
|
|
4125
|
+
}
|
|
4023
4126
|
const title = api.getState("title");
|
|
4024
4127
|
const poster = api.getState("poster");
|
|
4025
4128
|
if (titleEl && title) {
|
|
@@ -4044,21 +4147,34 @@ function createAudioUIPlugin(config) {
|
|
|
4044
4147
|
}
|
|
4045
4148
|
if (volumeBtn) {
|
|
4046
4149
|
volumeBtn.innerHTML = muted || volume === 0 ? ICONS.volumeMuted : ICONS.volumeHigh;
|
|
4150
|
+
volumeBtn.setAttribute("aria-label", muted || volume === 0 ? "Unmute" : "Mute");
|
|
4151
|
+
}
|
|
4152
|
+
const volumeSlider = container?.querySelector(`.${prefix}__volume-slider`);
|
|
4153
|
+
if (volumeSlider) {
|
|
4154
|
+
const displayVolume = Math.round((muted ? 0 : volume) * 100);
|
|
4155
|
+
volumeSlider.setAttribute("aria-valuenow", String(displayVolume));
|
|
4156
|
+
volumeSlider.setAttribute("aria-valuetext", `${displayVolume}%`);
|
|
4047
4157
|
}
|
|
4048
4158
|
const playlist = api.getPlugin("playlist");
|
|
4049
4159
|
if (playlist) {
|
|
4050
4160
|
const state = playlist.getState();
|
|
4051
4161
|
if (shuffleBtn) {
|
|
4052
4162
|
shuffleBtn.classList.toggle(`${prefix}__btn--active`, state.shuffle);
|
|
4163
|
+
shuffleBtn.setAttribute("aria-pressed", String(state.shuffle));
|
|
4164
|
+
shuffleBtn.setAttribute("aria-label", state.shuffle ? "Shuffle on" : "Shuffle off");
|
|
4053
4165
|
}
|
|
4054
4166
|
if (repeatBtn) {
|
|
4055
4167
|
repeatBtn.classList.toggle(`${prefix}__btn--active`, state.repeat !== "none");
|
|
4168
|
+
repeatBtn.setAttribute("aria-pressed", String(state.repeat !== "none"));
|
|
4056
4169
|
if (state.repeat === "one") {
|
|
4057
4170
|
repeatBtn.innerHTML = ICONS.repeatOne;
|
|
4171
|
+
repeatBtn.setAttribute("aria-label", "Repeat one");
|
|
4058
4172
|
} else if (state.repeat === "all") {
|
|
4059
4173
|
repeatBtn.innerHTML = ICONS.repeatAll;
|
|
4174
|
+
repeatBtn.setAttribute("aria-label", "Repeat all");
|
|
4060
4175
|
} else {
|
|
4061
4176
|
repeatBtn.innerHTML = ICONS.repeatOff;
|
|
4177
|
+
repeatBtn.setAttribute("aria-label", "Repeat off");
|
|
4062
4178
|
}
|
|
4063
4179
|
}
|
|
4064
4180
|
}
|
|
@@ -4491,7 +4607,8 @@ function createAnalyticsPlugin(config) {
|
|
|
4491
4607
|
const duration = nextTime - b.time;
|
|
4492
4608
|
return sum + b.bitrate * duration;
|
|
4493
4609
|
}, 0);
|
|
4494
|
-
|
|
4610
|
+
const timeSpan = now - session.bitrateHistory[0].time;
|
|
4611
|
+
session.avgBitrate = timeSpan > 0 ? Math.round(totalBitrateTime / timeSpan) : 0;
|
|
4495
4612
|
}
|
|
4496
4613
|
const state = {
|
|
4497
4614
|
currentTime: api.getState("currentTime"),
|
|
@@ -4620,6 +4737,9 @@ function createAnalyticsPlugin(config) {
|
|
|
4620
4737
|
fatal: error.fatal ?? false
|
|
4621
4738
|
};
|
|
4622
4739
|
session.errors.push(errorEvent);
|
|
4740
|
+
if (session.errors.length > 100) {
|
|
4741
|
+
session.errors = session.errors.slice(-100);
|
|
4742
|
+
}
|
|
4623
4743
|
sendBeacon("error", {
|
|
4624
4744
|
errorType: errorEvent.type,
|
|
4625
4745
|
errorMessage: errorEvent.message,
|
|
@@ -4646,6 +4766,9 @@ function createAnalyticsPlugin(config) {
|
|
|
4646
4766
|
height: currentQuality.height
|
|
4647
4767
|
};
|
|
4648
4768
|
session.bitrateHistory.push(bitrateChange);
|
|
4769
|
+
if (session.bitrateHistory.length > 500) {
|
|
4770
|
+
session.bitrateHistory = session.bitrateHistory.slice(-500);
|
|
4771
|
+
}
|
|
4649
4772
|
if (currentQuality.bitrate > session.maxBitrate) {
|
|
4650
4773
|
session.maxBitrate = currentQuality.bitrate;
|
|
4651
4774
|
}
|
|
@@ -6853,6 +6976,7 @@ class ScarlettPlayer {
|
|
|
6853
6976
|
this.destroyed = false;
|
|
6854
6977
|
this.seekingWhilePlaying = false;
|
|
6855
6978
|
this.seekResumeTimeout = null;
|
|
6979
|
+
this.loadGeneration = 0;
|
|
6856
6980
|
if (typeof options.container === "string") {
|
|
6857
6981
|
const el = document.querySelector(options.container);
|
|
6858
6982
|
if (!el || !(el instanceof HTMLElement)) {
|
|
@@ -6926,6 +7050,7 @@ class ScarlettPlayer {
|
|
|
6926
7050
|
*/
|
|
6927
7051
|
async load(source) {
|
|
6928
7052
|
this.checkDestroyed();
|
|
7053
|
+
const generation = ++this.loadGeneration;
|
|
6929
7054
|
try {
|
|
6930
7055
|
this.logger.info("Loading source", { source });
|
|
6931
7056
|
this.stateManager.update({
|
|
@@ -6944,6 +7069,10 @@ class ScarlettPlayer {
|
|
|
6944
7069
|
await this.pluginManager.destroyPlugin(previousProviderId);
|
|
6945
7070
|
this._currentProvider = null;
|
|
6946
7071
|
}
|
|
7072
|
+
if (generation !== this.loadGeneration) {
|
|
7073
|
+
this.logger.info("Load superseded by newer load call", { source });
|
|
7074
|
+
return;
|
|
7075
|
+
}
|
|
6947
7076
|
const provider = this.pluginManager.selectProvider(source);
|
|
6948
7077
|
if (!provider) {
|
|
6949
7078
|
this.errorHandler.throw(
|
|
@@ -6959,18 +7088,28 @@ class ScarlettPlayer {
|
|
|
6959
7088
|
this._currentProvider = provider;
|
|
6960
7089
|
this.logger.info("Provider selected", { provider: provider.id });
|
|
6961
7090
|
await this.pluginManager.initPlugin(provider.id);
|
|
7091
|
+
if (generation !== this.loadGeneration) {
|
|
7092
|
+
this.logger.info("Load superseded by newer load call", { source });
|
|
7093
|
+
return;
|
|
7094
|
+
}
|
|
6962
7095
|
this.stateManager.set("source", { src: source, type: this.detectMimeType(source) });
|
|
6963
7096
|
if (typeof provider.loadSource === "function") {
|
|
6964
7097
|
await provider.loadSource(source);
|
|
6965
7098
|
}
|
|
7099
|
+
if (generation !== this.loadGeneration) {
|
|
7100
|
+
this.logger.info("Load superseded by newer load call", { source });
|
|
7101
|
+
return;
|
|
7102
|
+
}
|
|
6966
7103
|
if (this.stateManager.getValue("autoplay")) {
|
|
6967
7104
|
await this.play();
|
|
6968
7105
|
}
|
|
6969
7106
|
} catch (error) {
|
|
6970
|
-
this.
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
7107
|
+
if (generation === this.loadGeneration) {
|
|
7108
|
+
this.errorHandler.handle(error, {
|
|
7109
|
+
operation: "load",
|
|
7110
|
+
source
|
|
7111
|
+
});
|
|
7112
|
+
}
|
|
6974
7113
|
}
|
|
6975
7114
|
}
|
|
6976
7115
|
/**
|
|
@@ -7098,8 +7237,9 @@ class ScarlettPlayer {
|
|
|
7098
7237
|
*/
|
|
7099
7238
|
setPlaybackRate(rate) {
|
|
7100
7239
|
this.checkDestroyed();
|
|
7101
|
-
|
|
7102
|
-
this.
|
|
7240
|
+
const clampedRate = Math.max(0.0625, Math.min(16, rate));
|
|
7241
|
+
this.stateManager.set("playbackRate", clampedRate);
|
|
7242
|
+
this.eventBus.emit("playback:ratechange", { rate: clampedRate });
|
|
7103
7243
|
}
|
|
7104
7244
|
/**
|
|
7105
7245
|
* Set autoplay state.
|
|
@@ -7228,6 +7368,13 @@ class ScarlettPlayer {
|
|
|
7228
7368
|
}
|
|
7229
7369
|
const provider = this._currentProvider;
|
|
7230
7370
|
if (typeof provider.setLevel === "function") {
|
|
7371
|
+
if (index !== -1) {
|
|
7372
|
+
const levels = this.getQualities();
|
|
7373
|
+
if (levels.length > 0 && (index < 0 || index >= levels.length)) {
|
|
7374
|
+
this.logger.warn(`Invalid quality index: ${index} (available: ${levels.length})`);
|
|
7375
|
+
return;
|
|
7376
|
+
}
|
|
7377
|
+
}
|
|
7231
7378
|
provider.setLevel(index);
|
|
7232
7379
|
this.eventBus.emit("quality:change", {
|
|
7233
7380
|
quality: index === -1 ? "auto" : `level-${index}`,
|
|
@@ -7468,18 +7615,40 @@ class ScarlettPlayer {
|
|
|
7468
7615
|
* @private
|
|
7469
7616
|
*/
|
|
7470
7617
|
detectMimeType(source) {
|
|
7471
|
-
|
|
7618
|
+
let path = source;
|
|
7619
|
+
try {
|
|
7620
|
+
path = new URL(source).pathname;
|
|
7621
|
+
} catch {
|
|
7622
|
+
const noQuery = source.split("?")[0] ?? source;
|
|
7623
|
+
path = noQuery.split("#")[0] ?? noQuery;
|
|
7624
|
+
}
|
|
7625
|
+
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
7472
7626
|
switch (ext) {
|
|
7473
7627
|
case "m3u8":
|
|
7474
7628
|
return "application/x-mpegURL";
|
|
7475
7629
|
case "mpd":
|
|
7476
7630
|
return "application/dash+xml";
|
|
7477
7631
|
case "mp4":
|
|
7632
|
+
case "m4v":
|
|
7478
7633
|
return "video/mp4";
|
|
7479
7634
|
case "webm":
|
|
7480
7635
|
return "video/webm";
|
|
7481
7636
|
case "ogg":
|
|
7637
|
+
case "ogv":
|
|
7482
7638
|
return "video/ogg";
|
|
7639
|
+
case "mov":
|
|
7640
|
+
return "video/quicktime";
|
|
7641
|
+
case "mkv":
|
|
7642
|
+
return "video/x-matroska";
|
|
7643
|
+
case "mp3":
|
|
7644
|
+
return "audio/mpeg";
|
|
7645
|
+
case "wav":
|
|
7646
|
+
return "audio/wav";
|
|
7647
|
+
case "flac":
|
|
7648
|
+
return "audio/flac";
|
|
7649
|
+
case "aac":
|
|
7650
|
+
case "m4a":
|
|
7651
|
+
return "audio/mp4";
|
|
7483
7652
|
default:
|
|
7484
7653
|
return "video/mp4";
|
|
7485
7654
|
}
|