@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.video.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
|
}
|
|
@@ -4949,6 +5041,7 @@ class ScarlettPlayer {
|
|
|
4949
5041
|
this.destroyed = false;
|
|
4950
5042
|
this.seekingWhilePlaying = false;
|
|
4951
5043
|
this.seekResumeTimeout = null;
|
|
5044
|
+
this.loadGeneration = 0;
|
|
4952
5045
|
if (typeof options.container === "string") {
|
|
4953
5046
|
const el = document.querySelector(options.container);
|
|
4954
5047
|
if (!el || !(el instanceof HTMLElement)) {
|
|
@@ -5022,6 +5115,7 @@ class ScarlettPlayer {
|
|
|
5022
5115
|
*/
|
|
5023
5116
|
async load(source) {
|
|
5024
5117
|
this.checkDestroyed();
|
|
5118
|
+
const generation = ++this.loadGeneration;
|
|
5025
5119
|
try {
|
|
5026
5120
|
this.logger.info("Loading source", { source });
|
|
5027
5121
|
this.stateManager.update({
|
|
@@ -5040,6 +5134,10 @@ class ScarlettPlayer {
|
|
|
5040
5134
|
await this.pluginManager.destroyPlugin(previousProviderId);
|
|
5041
5135
|
this._currentProvider = null;
|
|
5042
5136
|
}
|
|
5137
|
+
if (generation !== this.loadGeneration) {
|
|
5138
|
+
this.logger.info("Load superseded by newer load call", { source });
|
|
5139
|
+
return;
|
|
5140
|
+
}
|
|
5043
5141
|
const provider = this.pluginManager.selectProvider(source);
|
|
5044
5142
|
if (!provider) {
|
|
5045
5143
|
this.errorHandler.throw(
|
|
@@ -5055,18 +5153,28 @@ class ScarlettPlayer {
|
|
|
5055
5153
|
this._currentProvider = provider;
|
|
5056
5154
|
this.logger.info("Provider selected", { provider: provider.id });
|
|
5057
5155
|
await this.pluginManager.initPlugin(provider.id);
|
|
5156
|
+
if (generation !== this.loadGeneration) {
|
|
5157
|
+
this.logger.info("Load superseded by newer load call", { source });
|
|
5158
|
+
return;
|
|
5159
|
+
}
|
|
5058
5160
|
this.stateManager.set("source", { src: source, type: this.detectMimeType(source) });
|
|
5059
5161
|
if (typeof provider.loadSource === "function") {
|
|
5060
5162
|
await provider.loadSource(source);
|
|
5061
5163
|
}
|
|
5164
|
+
if (generation !== this.loadGeneration) {
|
|
5165
|
+
this.logger.info("Load superseded by newer load call", { source });
|
|
5166
|
+
return;
|
|
5167
|
+
}
|
|
5062
5168
|
if (this.stateManager.getValue("autoplay")) {
|
|
5063
5169
|
await this.play();
|
|
5064
5170
|
}
|
|
5065
5171
|
} catch (error) {
|
|
5066
|
-
this.
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5172
|
+
if (generation === this.loadGeneration) {
|
|
5173
|
+
this.errorHandler.handle(error, {
|
|
5174
|
+
operation: "load",
|
|
5175
|
+
source
|
|
5176
|
+
});
|
|
5177
|
+
}
|
|
5070
5178
|
}
|
|
5071
5179
|
}
|
|
5072
5180
|
/**
|
|
@@ -5194,8 +5302,9 @@ class ScarlettPlayer {
|
|
|
5194
5302
|
*/
|
|
5195
5303
|
setPlaybackRate(rate) {
|
|
5196
5304
|
this.checkDestroyed();
|
|
5197
|
-
|
|
5198
|
-
this.
|
|
5305
|
+
const clampedRate = Math.max(0.0625, Math.min(16, rate));
|
|
5306
|
+
this.stateManager.set("playbackRate", clampedRate);
|
|
5307
|
+
this.eventBus.emit("playback:ratechange", { rate: clampedRate });
|
|
5199
5308
|
}
|
|
5200
5309
|
/**
|
|
5201
5310
|
* Set autoplay state.
|
|
@@ -5324,6 +5433,13 @@ class ScarlettPlayer {
|
|
|
5324
5433
|
}
|
|
5325
5434
|
const provider = this._currentProvider;
|
|
5326
5435
|
if (typeof provider.setLevel === "function") {
|
|
5436
|
+
if (index !== -1) {
|
|
5437
|
+
const levels = this.getQualities();
|
|
5438
|
+
if (levels.length > 0 && (index < 0 || index >= levels.length)) {
|
|
5439
|
+
this.logger.warn(`Invalid quality index: ${index} (available: ${levels.length})`);
|
|
5440
|
+
return;
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5327
5443
|
provider.setLevel(index);
|
|
5328
5444
|
this.eventBus.emit("quality:change", {
|
|
5329
5445
|
quality: index === -1 ? "auto" : `level-${index}`,
|
|
@@ -5564,18 +5680,40 @@ class ScarlettPlayer {
|
|
|
5564
5680
|
* @private
|
|
5565
5681
|
*/
|
|
5566
5682
|
detectMimeType(source) {
|
|
5567
|
-
|
|
5683
|
+
let path = source;
|
|
5684
|
+
try {
|
|
5685
|
+
path = new URL(source).pathname;
|
|
5686
|
+
} catch {
|
|
5687
|
+
const noQuery = source.split("?")[0] ?? source;
|
|
5688
|
+
path = noQuery.split("#")[0] ?? noQuery;
|
|
5689
|
+
}
|
|
5690
|
+
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
5568
5691
|
switch (ext) {
|
|
5569
5692
|
case "m3u8":
|
|
5570
5693
|
return "application/x-mpegURL";
|
|
5571
5694
|
case "mpd":
|
|
5572
5695
|
return "application/dash+xml";
|
|
5573
5696
|
case "mp4":
|
|
5697
|
+
case "m4v":
|
|
5574
5698
|
return "video/mp4";
|
|
5575
5699
|
case "webm":
|
|
5576
5700
|
return "video/webm";
|
|
5577
5701
|
case "ogg":
|
|
5702
|
+
case "ogv":
|
|
5578
5703
|
return "video/ogg";
|
|
5704
|
+
case "mov":
|
|
5705
|
+
return "video/quicktime";
|
|
5706
|
+
case "mkv":
|
|
5707
|
+
return "video/x-matroska";
|
|
5708
|
+
case "mp3":
|
|
5709
|
+
return "audio/mpeg";
|
|
5710
|
+
case "wav":
|
|
5711
|
+
return "audio/wav";
|
|
5712
|
+
case "flac":
|
|
5713
|
+
return "audio/flac";
|
|
5714
|
+
case "aac":
|
|
5715
|
+
case "m4a":
|
|
5716
|
+
return "audio/mp4";
|
|
5579
5717
|
default:
|
|
5580
5718
|
return "video/mp4";
|
|
5581
5719
|
}
|