@scarlett-player/embed 0.5.2 → 1.0.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/dist/embed.audio.js +138 -9
- 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 +511 -10
- 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 +485 -7
- 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 +11 -9
package/dist/embed.js
CHANGED
|
@@ -43,6 +43,18 @@ function mapLevels(levels, _currentLevel) {
|
|
|
43
43
|
codec: level.codecSet
|
|
44
44
|
}));
|
|
45
45
|
}
|
|
46
|
+
function getInitialBandwidthEstimate(overrideBps) {
|
|
47
|
+
const HLS_DEFAULT_ESTIMATE = 5e5;
|
|
48
|
+
if (overrideBps !== void 0 && overrideBps > 0) {
|
|
49
|
+
return overrideBps;
|
|
50
|
+
}
|
|
51
|
+
const connection = navigator.connection;
|
|
52
|
+
if (connection?.downlink && connection.downlink > 0) {
|
|
53
|
+
const bps = connection.downlink * 1e6;
|
|
54
|
+
return Math.round(bps * 0.85);
|
|
55
|
+
}
|
|
56
|
+
return HLS_DEFAULT_ESTIMATE;
|
|
57
|
+
}
|
|
46
58
|
var HLS_ERROR_TYPES = {
|
|
47
59
|
NETWORK_ERROR: "networkError",
|
|
48
60
|
MEDIA_ERROR: "mediaError",
|
|
@@ -113,6 +125,14 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
113
125
|
});
|
|
114
126
|
callbacks.onLevelSwitched?.(data.level);
|
|
115
127
|
});
|
|
128
|
+
let lastBandwidthUpdate = 0;
|
|
129
|
+
addHandler("hlsFragLoaded", () => {
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
if (now - lastBandwidthUpdate >= 2e3 && hls.bandwidthEstimate) {
|
|
132
|
+
lastBandwidthUpdate = now;
|
|
133
|
+
api.setState("bandwidth", Math.round(hls.bandwidthEstimate));
|
|
134
|
+
}
|
|
135
|
+
});
|
|
116
136
|
addHandler("hlsFragBuffered", () => {
|
|
117
137
|
api.setState("buffering", false);
|
|
118
138
|
callbacks.onBufferUpdate?.();
|
|
@@ -123,12 +143,44 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
123
143
|
addHandler("hlsLevelLoaded", (_event, data) => {
|
|
124
144
|
if (data.details?.live !== void 0) {
|
|
125
145
|
api.setState("live", data.details.live);
|
|
146
|
+
if (data.details.live) {
|
|
147
|
+
const video = hls.media;
|
|
148
|
+
if (video && video.seekable && video.seekable.length > 0) {
|
|
149
|
+
const start = video.seekable.start(0);
|
|
150
|
+
const end = video.seekable.end(video.seekable.length - 1);
|
|
151
|
+
api.setState("seekableRange", { start, end });
|
|
152
|
+
const threshold = (data.details.targetduration ?? 3) * 3;
|
|
153
|
+
const isAtLiveEdge = end - video.currentTime < threshold;
|
|
154
|
+
api.setState("liveEdge", isAtLiveEdge);
|
|
155
|
+
const latency = end - video.currentTime;
|
|
156
|
+
api.setState("liveLatency", Math.max(0, latency));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
126
159
|
callbacks.onLiveUpdate?.();
|
|
127
160
|
}
|
|
128
161
|
});
|
|
129
162
|
addHandler("hlsError", (_event, data) => {
|
|
130
163
|
const error = parseHlsError(data);
|
|
131
|
-
|
|
164
|
+
const isBufferHoleSeek = !error.fatal && (error.details?.includes("bufferStalledError") || data.reason?.includes("buffer holes"));
|
|
165
|
+
if (isBufferHoleSeek) {
|
|
166
|
+
api.logger.debug(`HLS buffer recovery: ${error.reason || error.details}`, {
|
|
167
|
+
details: error.details,
|
|
168
|
+
reason: error.reason
|
|
169
|
+
});
|
|
170
|
+
} else if (error.fatal) {
|
|
171
|
+
api.logger.error(`HLS fatal error: ${error.details} (type=${error.type})`, {
|
|
172
|
+
type: error.type,
|
|
173
|
+
details: error.details,
|
|
174
|
+
url: error.url
|
|
175
|
+
});
|
|
176
|
+
} else {
|
|
177
|
+
api.logger.warn(`HLS error: ${error.details} (type=${error.type}, fatal=${error.fatal})`, {
|
|
178
|
+
type: error.type,
|
|
179
|
+
details: error.details,
|
|
180
|
+
fatal: error.fatal,
|
|
181
|
+
url: error.url
|
|
182
|
+
});
|
|
183
|
+
}
|
|
132
184
|
callbacks.onError?.(error);
|
|
133
185
|
});
|
|
134
186
|
return () => {
|
|
@@ -150,6 +202,8 @@ function setupVideoEventHandlers(video, api) {
|
|
|
150
202
|
addHandler("playing", () => {
|
|
151
203
|
api.setState("playing", true);
|
|
152
204
|
api.setState("paused", false);
|
|
205
|
+
api.setState("waiting", false);
|
|
206
|
+
api.setState("buffering", false);
|
|
153
207
|
api.setState("playbackState", "playing");
|
|
154
208
|
});
|
|
155
209
|
addHandler("pause", () => {
|
|
@@ -166,6 +220,15 @@ function setupVideoEventHandlers(video, api) {
|
|
|
166
220
|
addHandler("timeupdate", () => {
|
|
167
221
|
api.setState("currentTime", video.currentTime);
|
|
168
222
|
api.emit("playback:timeupdate", { currentTime: video.currentTime });
|
|
223
|
+
const isLive = api.getState("live");
|
|
224
|
+
if (isLive && video.seekable && video.seekable.length > 0) {
|
|
225
|
+
const start = video.seekable.start(0);
|
|
226
|
+
const end = video.seekable.end(video.seekable.length - 1);
|
|
227
|
+
api.setState("seekableRange", { start, end });
|
|
228
|
+
const isAtLiveEdge = end - video.currentTime < 10;
|
|
229
|
+
api.setState("liveEdge", isAtLiveEdge);
|
|
230
|
+
api.setState("liveLatency", Math.max(0, end - video.currentTime));
|
|
231
|
+
}
|
|
169
232
|
});
|
|
170
233
|
addHandler("durationchange", () => {
|
|
171
234
|
api.setState("duration", video.duration || 0);
|
|
@@ -312,6 +375,7 @@ var DEFAULT_CONFIG$4 = {
|
|
|
312
375
|
maxMaxBufferLength: 600,
|
|
313
376
|
backBufferLength: 30,
|
|
314
377
|
enableWorker: true,
|
|
378
|
+
capLevelToPlayerSize: true,
|
|
315
379
|
// Error recovery settings
|
|
316
380
|
maxNetworkRetries: 3,
|
|
317
381
|
maxMediaRetries: 2,
|
|
@@ -381,11 +445,13 @@ function createHLSPlugin(config) {
|
|
|
381
445
|
startPosition: mergedConfig.startPosition,
|
|
382
446
|
startLevel: -1,
|
|
383
447
|
// Auto quality selection (ABR)
|
|
448
|
+
abrEwmaDefaultEstimate: getInitialBandwidthEstimate(mergedConfig.initialBandwidthEstimate),
|
|
384
449
|
lowLatencyMode: mergedConfig.lowLatencyMode,
|
|
385
450
|
maxBufferLength: mergedConfig.maxBufferLength,
|
|
386
451
|
maxMaxBufferLength: mergedConfig.maxMaxBufferLength,
|
|
387
452
|
backBufferLength: mergedConfig.backBufferLength,
|
|
388
453
|
enableWorker: mergedConfig.enableWorker,
|
|
454
|
+
capLevelToPlayerSize: mergedConfig.capLevelToPlayerSize,
|
|
389
455
|
// Minimize hls.js internal retries - we handle retries ourselves
|
|
390
456
|
fragLoadingMaxRetry: 1,
|
|
391
457
|
manifestLoadingMaxRetry: 1,
|
|
@@ -656,7 +722,10 @@ function createHLSPlugin(config) {
|
|
|
656
722
|
currentSrc = src;
|
|
657
723
|
api.setState("playbackState", "loading");
|
|
658
724
|
api.setState("buffering", true);
|
|
659
|
-
if (
|
|
725
|
+
if (api.getState("airplayActive") && supportsNativeHLS()) {
|
|
726
|
+
api.logger.info("Using native HLS (AirPlay active)");
|
|
727
|
+
await loadNative(src);
|
|
728
|
+
} else if (isHlsJsSupported()) {
|
|
660
729
|
api.logger.info("Using hls.js for HLS playback");
|
|
661
730
|
await loadWithHlsJs(src);
|
|
662
731
|
} else if (supportsNativeHLS()) {
|
|
@@ -3198,6 +3267,37 @@ var CaptionsButton = class {
|
|
|
3198
3267
|
this.el.remove();
|
|
3199
3268
|
}
|
|
3200
3269
|
};
|
|
3270
|
+
var ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/><line x1="2" y1="2" x2="22" y2="22" stroke="currentColor" stroke-width="2"/></svg>`;
|
|
3271
|
+
var BandwidthIndicator = class {
|
|
3272
|
+
constructor(api) {
|
|
3273
|
+
this.api = api;
|
|
3274
|
+
this.el = createElement("div", { className: "sp-bandwidth-indicator" });
|
|
3275
|
+
this.el.innerHTML = ICON_SVG;
|
|
3276
|
+
this.el.setAttribute("aria-label", "Bandwidth is limiting video quality");
|
|
3277
|
+
this.el.setAttribute("title", "Bandwidth is limiting video quality");
|
|
3278
|
+
this.el.style.display = "none";
|
|
3279
|
+
}
|
|
3280
|
+
render() {
|
|
3281
|
+
return this.el;
|
|
3282
|
+
}
|
|
3283
|
+
update() {
|
|
3284
|
+
const bandwidth = this.api.getState("bandwidth");
|
|
3285
|
+
const qualities = this.api.getState("qualities");
|
|
3286
|
+
if (!bandwidth || !qualities || qualities.length === 0) {
|
|
3287
|
+
this.el.style.display = "none";
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
const highestBitrate = Math.max(...qualities.map((q) => q.bitrate));
|
|
3291
|
+
if (highestBitrate > 0 && bandwidth < highestBitrate) {
|
|
3292
|
+
this.el.style.display = "";
|
|
3293
|
+
} else {
|
|
3294
|
+
this.el.style.display = "none";
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
destroy() {
|
|
3298
|
+
this.el.remove();
|
|
3299
|
+
}
|
|
3300
|
+
};
|
|
3201
3301
|
var DEFAULT_LAYOUT = [
|
|
3202
3302
|
"play",
|
|
3203
3303
|
"skip-backward",
|
|
@@ -3205,6 +3305,7 @@ var DEFAULT_LAYOUT = [
|
|
|
3205
3305
|
"volume",
|
|
3206
3306
|
"time",
|
|
3207
3307
|
"live-indicator",
|
|
3308
|
+
"bandwidth-indicator",
|
|
3208
3309
|
"spacer",
|
|
3209
3310
|
"settings",
|
|
3210
3311
|
"captions",
|
|
@@ -3245,6 +3346,8 @@ function uiPlugin(config = {}) {
|
|
|
3245
3346
|
return new TimeDisplay(api);
|
|
3246
3347
|
case "live-indicator":
|
|
3247
3348
|
return new LiveIndicator(api);
|
|
3349
|
+
case "bandwidth-indicator":
|
|
3350
|
+
return new BandwidthIndicator(api);
|
|
3248
3351
|
case "quality":
|
|
3249
3352
|
return new QualityMenu(api);
|
|
3250
3353
|
case "settings":
|
|
@@ -4694,6 +4797,9 @@ function createAnalyticsPlugin(config) {
|
|
|
4694
4797
|
fatal: error.fatal ?? false
|
|
4695
4798
|
};
|
|
4696
4799
|
session.errors.push(errorEvent);
|
|
4800
|
+
if (session.errors.length > 100) {
|
|
4801
|
+
session.errors = session.errors.slice(-100);
|
|
4802
|
+
}
|
|
4697
4803
|
sendBeacon("error", {
|
|
4698
4804
|
errorType: errorEvent.type,
|
|
4699
4805
|
errorMessage: errorEvent.message,
|
|
@@ -4720,6 +4826,9 @@ function createAnalyticsPlugin(config) {
|
|
|
4720
4826
|
height: currentQuality.height
|
|
4721
4827
|
};
|
|
4722
4828
|
session.bitrateHistory.push(bitrateChange);
|
|
4829
|
+
if (session.bitrateHistory.length > 500) {
|
|
4830
|
+
session.bitrateHistory = session.bitrateHistory.slice(-500);
|
|
4831
|
+
}
|
|
4723
4832
|
if (currentQuality.bitrate > session.maxBitrate) {
|
|
4724
4833
|
session.maxBitrate = currentQuality.bitrate;
|
|
4725
4834
|
}
|
|
@@ -4826,7 +4935,9 @@ var DEFAULT_CONFIG$1 = {
|
|
|
4826
4935
|
persist: false,
|
|
4827
4936
|
persistKey: "scarlett-playlist",
|
|
4828
4937
|
shuffle: false,
|
|
4829
|
-
repeat: "none"
|
|
4938
|
+
repeat: "none",
|
|
4939
|
+
autoLoad: true,
|
|
4940
|
+
advanceDelay: 0
|
|
4830
4941
|
};
|
|
4831
4942
|
function generateId() {
|
|
4832
4943
|
return `track-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
@@ -4970,6 +5081,9 @@ function createPlaylistPlugin(config) {
|
|
|
4970
5081
|
}
|
|
4971
5082
|
api?.setState("mediaType", track.type || "audio");
|
|
4972
5083
|
emitChange();
|
|
5084
|
+
if (mergedConfig.autoLoad !== false && track.src) {
|
|
5085
|
+
api?.emit("media:load-request", { src: track.src, autoplay: true });
|
|
5086
|
+
}
|
|
4973
5087
|
};
|
|
4974
5088
|
const plugin = {
|
|
4975
5089
|
id: "playlist",
|
|
@@ -4984,12 +5098,20 @@ function createPlaylistPlugin(config) {
|
|
|
4984
5098
|
if (shuffle && tracks.length > 0) {
|
|
4985
5099
|
generateShuffleOrder();
|
|
4986
5100
|
}
|
|
5101
|
+
let advanceTimeout = null;
|
|
4987
5102
|
const unsubEnded = api.on("playback:ended", () => {
|
|
4988
5103
|
if (!mergedConfig.autoAdvance) return;
|
|
4989
5104
|
const nextIdx = getNextIndex();
|
|
4990
5105
|
if (nextIdx >= 0) {
|
|
4991
|
-
|
|
4992
|
-
|
|
5106
|
+
const advance = () => {
|
|
5107
|
+
api?.logger.debug("Auto-advancing to next track", { nextIdx });
|
|
5108
|
+
setCurrentTrack(nextIdx);
|
|
5109
|
+
};
|
|
5110
|
+
if (mergedConfig.advanceDelay) {
|
|
5111
|
+
advanceTimeout = setTimeout(advance, mergedConfig.advanceDelay);
|
|
5112
|
+
} else {
|
|
5113
|
+
advance();
|
|
5114
|
+
}
|
|
4993
5115
|
} else {
|
|
4994
5116
|
api?.logger.info("Playlist ended");
|
|
4995
5117
|
api?.emit("playlist:ended", void 0);
|
|
@@ -4997,6 +5119,10 @@ function createPlaylistPlugin(config) {
|
|
|
4997
5119
|
});
|
|
4998
5120
|
api.onDestroy(() => {
|
|
4999
5121
|
unsubEnded();
|
|
5122
|
+
if (advanceTimeout) {
|
|
5123
|
+
clearTimeout(advanceTimeout);
|
|
5124
|
+
advanceTimeout = null;
|
|
5125
|
+
}
|
|
5000
5126
|
persistPlaylist();
|
|
5001
5127
|
});
|
|
5002
5128
|
},
|
|
@@ -5442,6 +5568,336 @@ function createMediaSessionPlugin(config) {
|
|
|
5442
5568
|
};
|
|
5443
5569
|
return plugin;
|
|
5444
5570
|
}
|
|
5571
|
+
var POSITIONS = ["top-left", "top-right", "bottom-left", "bottom-right", "center"];
|
|
5572
|
+
var POSITION_STYLES = {
|
|
5573
|
+
"top-left": "top:10px;left:10px;",
|
|
5574
|
+
"top-right": "top:10px;right:10px;",
|
|
5575
|
+
"bottom-left": "bottom:40px;left:10px;",
|
|
5576
|
+
"bottom-right": "bottom:40px;right:10px;",
|
|
5577
|
+
"center": "top:50%;left:50%;transform:translate(-50%,-50%);"
|
|
5578
|
+
};
|
|
5579
|
+
function createWatermarkPlugin(config = {}) {
|
|
5580
|
+
let api = null;
|
|
5581
|
+
let element = null;
|
|
5582
|
+
let dynamicTimer = null;
|
|
5583
|
+
let showDelayTimer = null;
|
|
5584
|
+
let currentPosition = config.position || "bottom-right";
|
|
5585
|
+
const opacity = config.opacity ?? 0.5;
|
|
5586
|
+
const fontSize = config.fontSize ?? 14;
|
|
5587
|
+
const dynamic = config.dynamic ?? false;
|
|
5588
|
+
const dynamicInterval = config.dynamicInterval ?? 1e4;
|
|
5589
|
+
const showDelay = config.showDelay ?? 0;
|
|
5590
|
+
const createElement2 = () => {
|
|
5591
|
+
const el = document.createElement("div");
|
|
5592
|
+
el.className = "sp-watermark sp-watermark--hidden";
|
|
5593
|
+
el.style.cssText = `position:absolute;z-index:10;pointer-events:none;opacity:${opacity};font-size:${fontSize}px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,0.6);font-family:sans-serif;transition:all 0.5s ease;${POSITION_STYLES[currentPosition]}`;
|
|
5594
|
+
el.setAttribute("data-position", currentPosition);
|
|
5595
|
+
updateContent(el);
|
|
5596
|
+
return el;
|
|
5597
|
+
};
|
|
5598
|
+
const updateContent = (el, imageUrl, text) => {
|
|
5599
|
+
const img = imageUrl || config.imageUrl;
|
|
5600
|
+
const txt = text || config.text;
|
|
5601
|
+
el.innerHTML = "";
|
|
5602
|
+
if (img) {
|
|
5603
|
+
const imgEl = document.createElement("img");
|
|
5604
|
+
imgEl.src = img;
|
|
5605
|
+
imgEl.style.cssText = `max-height:${fontSize * 2}px;opacity:inherit;display:block;`;
|
|
5606
|
+
imgEl.alt = "";
|
|
5607
|
+
el.appendChild(imgEl);
|
|
5608
|
+
} else if (txt) {
|
|
5609
|
+
el.textContent = txt;
|
|
5610
|
+
}
|
|
5611
|
+
};
|
|
5612
|
+
const setPosition = (position) => {
|
|
5613
|
+
if (!element) return;
|
|
5614
|
+
currentPosition = position;
|
|
5615
|
+
element.style.top = "";
|
|
5616
|
+
element.style.right = "";
|
|
5617
|
+
element.style.bottom = "";
|
|
5618
|
+
element.style.left = "";
|
|
5619
|
+
element.style.transform = "";
|
|
5620
|
+
const styles2 = POSITION_STYLES[position];
|
|
5621
|
+
styles2.split(";").filter(Boolean).forEach((rule) => {
|
|
5622
|
+
const colonIdx = rule.indexOf(":");
|
|
5623
|
+
if (colonIdx === -1) return;
|
|
5624
|
+
const prop = rule.slice(0, colonIdx).trim();
|
|
5625
|
+
const val = rule.slice(colonIdx + 1).trim();
|
|
5626
|
+
if (prop && val) {
|
|
5627
|
+
element.style.setProperty(prop, val);
|
|
5628
|
+
}
|
|
5629
|
+
});
|
|
5630
|
+
element.setAttribute("data-position", position);
|
|
5631
|
+
const isVisible = element.classList.contains("sp-watermark--visible");
|
|
5632
|
+
const visClass = isVisible ? " sp-watermark--visible" : " sp-watermark--hidden";
|
|
5633
|
+
element.className = `sp-watermark sp-watermark--${position}${visClass}${dynamic ? " sp-watermark--dynamic" : ""}`;
|
|
5634
|
+
};
|
|
5635
|
+
const randomizePosition = () => {
|
|
5636
|
+
const available = POSITIONS.filter((p) => p !== currentPosition);
|
|
5637
|
+
const next = available[Math.floor(Math.random() * available.length)];
|
|
5638
|
+
setPosition(next);
|
|
5639
|
+
};
|
|
5640
|
+
const show = () => {
|
|
5641
|
+
if (!element) return;
|
|
5642
|
+
element.classList.remove("sp-watermark--hidden");
|
|
5643
|
+
element.classList.add("sp-watermark--visible");
|
|
5644
|
+
};
|
|
5645
|
+
const hide = () => {
|
|
5646
|
+
if (!element) return;
|
|
5647
|
+
element.classList.remove("sp-watermark--visible");
|
|
5648
|
+
element.classList.add("sp-watermark--hidden");
|
|
5649
|
+
};
|
|
5650
|
+
const startDynamic = () => {
|
|
5651
|
+
if (!dynamic || dynamicTimer) return;
|
|
5652
|
+
dynamicTimer = setInterval(randomizePosition, dynamicInterval);
|
|
5653
|
+
};
|
|
5654
|
+
const stopDynamic = () => {
|
|
5655
|
+
if (dynamicTimer) {
|
|
5656
|
+
clearInterval(dynamicTimer);
|
|
5657
|
+
dynamicTimer = null;
|
|
5658
|
+
}
|
|
5659
|
+
};
|
|
5660
|
+
const cleanup = () => {
|
|
5661
|
+
stopDynamic();
|
|
5662
|
+
if (showDelayTimer) {
|
|
5663
|
+
clearTimeout(showDelayTimer);
|
|
5664
|
+
showDelayTimer = null;
|
|
5665
|
+
}
|
|
5666
|
+
if (element?.parentNode) {
|
|
5667
|
+
element.parentNode.removeChild(element);
|
|
5668
|
+
}
|
|
5669
|
+
element = null;
|
|
5670
|
+
};
|
|
5671
|
+
return {
|
|
5672
|
+
id: "watermark",
|
|
5673
|
+
name: "Watermark",
|
|
5674
|
+
version: "1.0.0",
|
|
5675
|
+
type: "feature",
|
|
5676
|
+
description: "Anti-piracy watermark overlay with text/image support and dynamic repositioning",
|
|
5677
|
+
init(pluginApi) {
|
|
5678
|
+
api = pluginApi;
|
|
5679
|
+
api.logger.debug("Watermark plugin initialized");
|
|
5680
|
+
element = createElement2();
|
|
5681
|
+
api.container.appendChild(element);
|
|
5682
|
+
const unsubPlay = api.on("playback:play", () => {
|
|
5683
|
+
if (showDelay > 0) {
|
|
5684
|
+
showDelayTimer = setTimeout(() => {
|
|
5685
|
+
show();
|
|
5686
|
+
startDynamic();
|
|
5687
|
+
}, showDelay);
|
|
5688
|
+
} else {
|
|
5689
|
+
show();
|
|
5690
|
+
startDynamic();
|
|
5691
|
+
}
|
|
5692
|
+
});
|
|
5693
|
+
const unsubPause = api.on("playback:pause", () => {
|
|
5694
|
+
hide();
|
|
5695
|
+
stopDynamic();
|
|
5696
|
+
if (showDelayTimer) {
|
|
5697
|
+
clearTimeout(showDelayTimer);
|
|
5698
|
+
showDelayTimer = null;
|
|
5699
|
+
}
|
|
5700
|
+
});
|
|
5701
|
+
const unsubEnded = api.on("playback:ended", () => {
|
|
5702
|
+
hide();
|
|
5703
|
+
stopDynamic();
|
|
5704
|
+
});
|
|
5705
|
+
const unsubChange = api.on("playlist:change", ({ track }) => {
|
|
5706
|
+
if (!element || !track) return;
|
|
5707
|
+
const metadata = track.metadata;
|
|
5708
|
+
if (metadata) {
|
|
5709
|
+
const watermarkUrl = metadata.watermarkUrl;
|
|
5710
|
+
const watermarkText = metadata.watermarkText;
|
|
5711
|
+
if (watermarkUrl || watermarkText) {
|
|
5712
|
+
updateContent(element, watermarkUrl, watermarkText);
|
|
5713
|
+
}
|
|
5714
|
+
}
|
|
5715
|
+
});
|
|
5716
|
+
api.onDestroy(() => {
|
|
5717
|
+
unsubPlay();
|
|
5718
|
+
unsubPause();
|
|
5719
|
+
unsubEnded();
|
|
5720
|
+
unsubChange();
|
|
5721
|
+
cleanup();
|
|
5722
|
+
});
|
|
5723
|
+
},
|
|
5724
|
+
destroy() {
|
|
5725
|
+
api?.logger.debug("Watermark plugin destroyed");
|
|
5726
|
+
cleanup();
|
|
5727
|
+
api = null;
|
|
5728
|
+
},
|
|
5729
|
+
setText(text) {
|
|
5730
|
+
if (element) updateContent(element, void 0, text);
|
|
5731
|
+
},
|
|
5732
|
+
setImage(imageUrl) {
|
|
5733
|
+
if (element) updateContent(element, imageUrl);
|
|
5734
|
+
},
|
|
5735
|
+
setPosition,
|
|
5736
|
+
setOpacity(value) {
|
|
5737
|
+
if (element) element.style.opacity = String(Math.max(0, Math.min(1, value)));
|
|
5738
|
+
},
|
|
5739
|
+
show,
|
|
5740
|
+
hide,
|
|
5741
|
+
getConfig() {
|
|
5742
|
+
return { ...config, position: currentPosition, opacity: element ? parseFloat(element.style.opacity) || opacity : opacity };
|
|
5743
|
+
}
|
|
5744
|
+
};
|
|
5745
|
+
}
|
|
5746
|
+
function createCaptionsPlugin(config = {}) {
|
|
5747
|
+
let api = null;
|
|
5748
|
+
let video = null;
|
|
5749
|
+
let addedTrackElements = [];
|
|
5750
|
+
const extractFromHLS = config.extractFromHLS !== false;
|
|
5751
|
+
const autoSelect = config.autoSelect ?? false;
|
|
5752
|
+
const defaultLanguage = config.defaultLanguage ?? "en";
|
|
5753
|
+
const getVideo2 = () => {
|
|
5754
|
+
if (video) return video;
|
|
5755
|
+
video = api?.container.querySelector("video") ?? null;
|
|
5756
|
+
return video;
|
|
5757
|
+
};
|
|
5758
|
+
const cleanupTracks = () => {
|
|
5759
|
+
for (const trackEl of addedTrackElements) {
|
|
5760
|
+
trackEl.parentNode?.removeChild(trackEl);
|
|
5761
|
+
}
|
|
5762
|
+
addedTrackElements = [];
|
|
5763
|
+
api?.setState("textTracks", []);
|
|
5764
|
+
api?.setState("currentTextTrack", null);
|
|
5765
|
+
};
|
|
5766
|
+
const addTrackElement = (source) => {
|
|
5767
|
+
const videoEl = getVideo2();
|
|
5768
|
+
if (!videoEl) throw new Error("No video element");
|
|
5769
|
+
const trackEl = document.createElement("track");
|
|
5770
|
+
trackEl.kind = source.kind || "subtitles";
|
|
5771
|
+
trackEl.label = source.label;
|
|
5772
|
+
trackEl.srclang = source.language;
|
|
5773
|
+
trackEl.src = source.src;
|
|
5774
|
+
trackEl.default = false;
|
|
5775
|
+
videoEl.appendChild(trackEl);
|
|
5776
|
+
addedTrackElements.push(trackEl);
|
|
5777
|
+
if (trackEl.track) {
|
|
5778
|
+
trackEl.track.mode = "disabled";
|
|
5779
|
+
}
|
|
5780
|
+
return trackEl;
|
|
5781
|
+
};
|
|
5782
|
+
const syncTracksToState = () => {
|
|
5783
|
+
const videoEl = getVideo2();
|
|
5784
|
+
if (!videoEl) return;
|
|
5785
|
+
const tracks = [];
|
|
5786
|
+
let currentTrack = null;
|
|
5787
|
+
for (let i = 0; i < videoEl.textTracks.length; i++) {
|
|
5788
|
+
const track = videoEl.textTracks[i];
|
|
5789
|
+
if (track.kind !== "subtitles" && track.kind !== "captions") continue;
|
|
5790
|
+
const scarlettTrack = {
|
|
5791
|
+
id: `track-${i}`,
|
|
5792
|
+
label: track.label || `Track ${i + 1}`,
|
|
5793
|
+
language: track.language || "",
|
|
5794
|
+
kind: track.kind,
|
|
5795
|
+
active: track.mode === "showing"
|
|
5796
|
+
};
|
|
5797
|
+
tracks.push(scarlettTrack);
|
|
5798
|
+
if (track.mode === "showing") {
|
|
5799
|
+
currentTrack = scarlettTrack;
|
|
5800
|
+
}
|
|
5801
|
+
}
|
|
5802
|
+
api?.setState("textTracks", tracks);
|
|
5803
|
+
api?.setState("currentTextTrack", currentTrack);
|
|
5804
|
+
};
|
|
5805
|
+
const selectTrack = (trackId) => {
|
|
5806
|
+
const videoEl = getVideo2();
|
|
5807
|
+
if (!videoEl) return;
|
|
5808
|
+
for (let i = 0; i < videoEl.textTracks.length; i++) {
|
|
5809
|
+
const track = videoEl.textTracks[i];
|
|
5810
|
+
if (track.kind !== "subtitles" && track.kind !== "captions") continue;
|
|
5811
|
+
const id = `track-${i}`;
|
|
5812
|
+
if (trackId && id === trackId) {
|
|
5813
|
+
track.mode = "showing";
|
|
5814
|
+
} else {
|
|
5815
|
+
track.mode = "disabled";
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5818
|
+
syncTracksToState();
|
|
5819
|
+
};
|
|
5820
|
+
const extractHlsSubtitles = () => {
|
|
5821
|
+
if (!extractFromHLS || !api) return;
|
|
5822
|
+
const hlsPlugin = api.getPlugin("hls-provider");
|
|
5823
|
+
if (!hlsPlugin || hlsPlugin.isNativeHLS()) return;
|
|
5824
|
+
const hlsInstance = hlsPlugin.getHlsInstance();
|
|
5825
|
+
if (!hlsInstance?.subtitleTracks?.length) return;
|
|
5826
|
+
api.logger.debug("Extracting HLS subtitle tracks", {
|
|
5827
|
+
count: hlsInstance.subtitleTracks.length
|
|
5828
|
+
});
|
|
5829
|
+
for (const hlsTrack of hlsInstance.subtitleTracks) {
|
|
5830
|
+
addTrackElement({
|
|
5831
|
+
language: hlsTrack.lang || "unknown",
|
|
5832
|
+
label: hlsTrack.name || `Subtitle ${hlsTrack.id}`,
|
|
5833
|
+
src: hlsTrack.url,
|
|
5834
|
+
kind: "subtitles"
|
|
5835
|
+
});
|
|
5836
|
+
}
|
|
5837
|
+
syncTracksToState();
|
|
5838
|
+
if (autoSelect) {
|
|
5839
|
+
autoSelectTrack();
|
|
5840
|
+
}
|
|
5841
|
+
};
|
|
5842
|
+
const autoSelectTrack = () => {
|
|
5843
|
+
const tracks = api?.getState("textTracks") || [];
|
|
5844
|
+
const match = tracks.find((t) => t.language === defaultLanguage);
|
|
5845
|
+
if (match) {
|
|
5846
|
+
selectTrack(match.id);
|
|
5847
|
+
api?.logger.debug("Auto-selected caption track", { language: defaultLanguage, id: match.id });
|
|
5848
|
+
}
|
|
5849
|
+
};
|
|
5850
|
+
const initSources = () => {
|
|
5851
|
+
if (!config.sources?.length) return;
|
|
5852
|
+
for (const source of config.sources) {
|
|
5853
|
+
addTrackElement(source);
|
|
5854
|
+
}
|
|
5855
|
+
syncTracksToState();
|
|
5856
|
+
if (autoSelect) {
|
|
5857
|
+
autoSelectTrack();
|
|
5858
|
+
}
|
|
5859
|
+
};
|
|
5860
|
+
return {
|
|
5861
|
+
id: "captions",
|
|
5862
|
+
name: "Captions",
|
|
5863
|
+
version: "1.0.0",
|
|
5864
|
+
type: "feature",
|
|
5865
|
+
description: "WebVTT subtitles and closed captions with HLS extraction",
|
|
5866
|
+
init(pluginApi) {
|
|
5867
|
+
api = pluginApi;
|
|
5868
|
+
api.logger.debug("Captions plugin initialized");
|
|
5869
|
+
api.setState("textTracks", []);
|
|
5870
|
+
api.setState("currentTextTrack", null);
|
|
5871
|
+
const unsubTrackText = api.on("track:text", ({ trackId }) => {
|
|
5872
|
+
selectTrack(trackId);
|
|
5873
|
+
});
|
|
5874
|
+
const unsubLoaded = api.on("media:loaded", () => {
|
|
5875
|
+
video = null;
|
|
5876
|
+
cleanupTracks();
|
|
5877
|
+
initSources();
|
|
5878
|
+
if (extractFromHLS) {
|
|
5879
|
+
setTimeout(extractHlsSubtitles, 500);
|
|
5880
|
+
}
|
|
5881
|
+
});
|
|
5882
|
+
const unsubLoadRequest = api.on("media:load-request", () => {
|
|
5883
|
+
video = null;
|
|
5884
|
+
cleanupTracks();
|
|
5885
|
+
});
|
|
5886
|
+
api.onDestroy(() => {
|
|
5887
|
+
unsubTrackText();
|
|
5888
|
+
unsubLoaded();
|
|
5889
|
+
unsubLoadRequest();
|
|
5890
|
+
cleanupTracks();
|
|
5891
|
+
});
|
|
5892
|
+
},
|
|
5893
|
+
destroy() {
|
|
5894
|
+
api?.logger.debug("Captions plugin destroyed");
|
|
5895
|
+
cleanupTracks();
|
|
5896
|
+
video = null;
|
|
5897
|
+
api = null;
|
|
5898
|
+
}
|
|
5899
|
+
};
|
|
5900
|
+
}
|
|
5445
5901
|
class Signal {
|
|
5446
5902
|
constructor(initialValue) {
|
|
5447
5903
|
this.subscribers = /* @__PURE__ */ new Set();
|
|
@@ -6981,6 +7437,13 @@ class ScarlettPlayer {
|
|
|
6981
7437
|
await this.pluginManager.initPlugin(id);
|
|
6982
7438
|
}
|
|
6983
7439
|
}
|
|
7440
|
+
this.eventBus.on("media:load-request", async ({ src, autoplay }) => {
|
|
7441
|
+
if (this.stateManager.getValue("chromecastActive")) return;
|
|
7442
|
+
await this.load(src);
|
|
7443
|
+
if (autoplay !== false) {
|
|
7444
|
+
await this.play();
|
|
7445
|
+
}
|
|
7446
|
+
});
|
|
6984
7447
|
if (this.initialSrc) {
|
|
6985
7448
|
await this.load(this.initialSrc);
|
|
6986
7449
|
}
|
|
@@ -7188,8 +7651,9 @@ class ScarlettPlayer {
|
|
|
7188
7651
|
*/
|
|
7189
7652
|
setPlaybackRate(rate) {
|
|
7190
7653
|
this.checkDestroyed();
|
|
7191
|
-
|
|
7192
|
-
this.
|
|
7654
|
+
const clampedRate = Math.max(0.0625, Math.min(16, rate));
|
|
7655
|
+
this.stateManager.set("playbackRate", clampedRate);
|
|
7656
|
+
this.eventBus.emit("playback:ratechange", { rate: clampedRate });
|
|
7193
7657
|
}
|
|
7194
7658
|
/**
|
|
7195
7659
|
* Set autoplay state.
|
|
@@ -7318,6 +7782,13 @@ class ScarlettPlayer {
|
|
|
7318
7782
|
}
|
|
7319
7783
|
const provider = this._currentProvider;
|
|
7320
7784
|
if (typeof provider.setLevel === "function") {
|
|
7785
|
+
if (index !== -1) {
|
|
7786
|
+
const levels = this.getQualities();
|
|
7787
|
+
if (levels.length > 0 && (index < 0 || index >= levels.length)) {
|
|
7788
|
+
this.logger.warn(`Invalid quality index: ${index} (available: ${levels.length})`);
|
|
7789
|
+
return;
|
|
7790
|
+
}
|
|
7791
|
+
}
|
|
7321
7792
|
provider.setLevel(index);
|
|
7322
7793
|
this.eventBus.emit("quality:change", {
|
|
7323
7794
|
quality: index === -1 ? "auto" : `level-${index}`,
|
|
@@ -7558,18 +8029,40 @@ class ScarlettPlayer {
|
|
|
7558
8029
|
* @private
|
|
7559
8030
|
*/
|
|
7560
8031
|
detectMimeType(source) {
|
|
7561
|
-
|
|
8032
|
+
let path = source;
|
|
8033
|
+
try {
|
|
8034
|
+
path = new URL(source).pathname;
|
|
8035
|
+
} catch {
|
|
8036
|
+
const noQuery = source.split("?")[0] ?? source;
|
|
8037
|
+
path = noQuery.split("#")[0] ?? noQuery;
|
|
8038
|
+
}
|
|
8039
|
+
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
7562
8040
|
switch (ext) {
|
|
7563
8041
|
case "m3u8":
|
|
7564
8042
|
return "application/x-mpegURL";
|
|
7565
8043
|
case "mpd":
|
|
7566
8044
|
return "application/dash+xml";
|
|
7567
8045
|
case "mp4":
|
|
8046
|
+
case "m4v":
|
|
7568
8047
|
return "video/mp4";
|
|
7569
8048
|
case "webm":
|
|
7570
8049
|
return "video/webm";
|
|
7571
8050
|
case "ogg":
|
|
8051
|
+
case "ogv":
|
|
7572
8052
|
return "video/ogg";
|
|
8053
|
+
case "mov":
|
|
8054
|
+
return "video/quicktime";
|
|
8055
|
+
case "mkv":
|
|
8056
|
+
return "video/x-matroska";
|
|
8057
|
+
case "mp3":
|
|
8058
|
+
return "audio/mpeg";
|
|
8059
|
+
case "wav":
|
|
8060
|
+
return "audio/wav";
|
|
8061
|
+
case "flac":
|
|
8062
|
+
return "audio/flac";
|
|
8063
|
+
case "aac":
|
|
8064
|
+
case "m4a":
|
|
8065
|
+
return "audio/mp4";
|
|
7573
8066
|
default:
|
|
7574
8067
|
return "video/mp4";
|
|
7575
8068
|
}
|
|
@@ -7778,6 +8271,12 @@ async function createEmbedPlayer(container, config, pluginCreators2, availableTy
|
|
|
7778
8271
|
artwork: config.artwork || config.poster || config.playlist?.[0]?.artwork
|
|
7779
8272
|
}));
|
|
7780
8273
|
}
|
|
8274
|
+
if (pluginCreators2.watermark && config.watermark) {
|
|
8275
|
+
plugins.push(pluginCreators2.watermark(config.watermark));
|
|
8276
|
+
}
|
|
8277
|
+
if (pluginCreators2.captions) {
|
|
8278
|
+
plugins.push(pluginCreators2.captions(config.captions || {}));
|
|
8279
|
+
}
|
|
7781
8280
|
if (pluginCreators2.analytics && config.analytics?.beaconUrl) {
|
|
7782
8281
|
plugins.push(pluginCreators2.analytics({
|
|
7783
8282
|
beaconUrl: config.analytics.beaconUrl,
|
|
@@ -7895,7 +8394,7 @@ function setupAutoInit(pluginCreators2, availableTypes) {
|
|
|
7895
8394
|
}
|
|
7896
8395
|
}
|
|
7897
8396
|
}
|
|
7898
|
-
const VERSION = "0.3
|
|
8397
|
+
const VERSION = "0.5.3";
|
|
7899
8398
|
const AVAILABLE_TYPES = ["video", "audio", "audio-mini"];
|
|
7900
8399
|
const pluginCreators = {
|
|
7901
8400
|
hls: createHLSPlugin,
|
|
@@ -7903,7 +8402,9 @@ const pluginCreators = {
|
|
|
7903
8402
|
audioUI: createAudioUIPlugin,
|
|
7904
8403
|
analytics: createAnalyticsPlugin,
|
|
7905
8404
|
playlist: createPlaylistPlugin,
|
|
7906
|
-
mediaSession: createMediaSessionPlugin
|
|
8405
|
+
mediaSession: createMediaSessionPlugin,
|
|
8406
|
+
watermark: createWatermarkPlugin,
|
|
8407
|
+
captions: createCaptionsPlugin
|
|
7907
8408
|
};
|
|
7908
8409
|
const ScarlettPlayerAPI = createScarlettPlayerAPI(
|
|
7909
8410
|
pluginCreators,
|