@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.video.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 = {
|
|
|
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":
|
|
@@ -3513,6 +3616,336 @@ function uiPlugin(config = {}) {
|
|
|
3513
3616
|
}
|
|
3514
3617
|
};
|
|
3515
3618
|
}
|
|
3619
|
+
var POSITIONS = ["top-left", "top-right", "bottom-left", "bottom-right", "center"];
|
|
3620
|
+
var POSITION_STYLES = {
|
|
3621
|
+
"top-left": "top:10px;left:10px;",
|
|
3622
|
+
"top-right": "top:10px;right:10px;",
|
|
3623
|
+
"bottom-left": "bottom:40px;left:10px;",
|
|
3624
|
+
"bottom-right": "bottom:40px;right:10px;",
|
|
3625
|
+
"center": "top:50%;left:50%;transform:translate(-50%,-50%);"
|
|
3626
|
+
};
|
|
3627
|
+
function createWatermarkPlugin(config = {}) {
|
|
3628
|
+
let api = null;
|
|
3629
|
+
let element = null;
|
|
3630
|
+
let dynamicTimer = null;
|
|
3631
|
+
let showDelayTimer = null;
|
|
3632
|
+
let currentPosition = config.position || "bottom-right";
|
|
3633
|
+
const opacity = config.opacity ?? 0.5;
|
|
3634
|
+
const fontSize = config.fontSize ?? 14;
|
|
3635
|
+
const dynamic = config.dynamic ?? false;
|
|
3636
|
+
const dynamicInterval = config.dynamicInterval ?? 1e4;
|
|
3637
|
+
const showDelay = config.showDelay ?? 0;
|
|
3638
|
+
const createElement2 = () => {
|
|
3639
|
+
const el = document.createElement("div");
|
|
3640
|
+
el.className = "sp-watermark sp-watermark--hidden";
|
|
3641
|
+
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]}`;
|
|
3642
|
+
el.setAttribute("data-position", currentPosition);
|
|
3643
|
+
updateContent(el);
|
|
3644
|
+
return el;
|
|
3645
|
+
};
|
|
3646
|
+
const updateContent = (el, imageUrl, text) => {
|
|
3647
|
+
const img = imageUrl || config.imageUrl;
|
|
3648
|
+
const txt = text || config.text;
|
|
3649
|
+
el.innerHTML = "";
|
|
3650
|
+
if (img) {
|
|
3651
|
+
const imgEl = document.createElement("img");
|
|
3652
|
+
imgEl.src = img;
|
|
3653
|
+
imgEl.style.cssText = `max-height:${fontSize * 2}px;opacity:inherit;display:block;`;
|
|
3654
|
+
imgEl.alt = "";
|
|
3655
|
+
el.appendChild(imgEl);
|
|
3656
|
+
} else if (txt) {
|
|
3657
|
+
el.textContent = txt;
|
|
3658
|
+
}
|
|
3659
|
+
};
|
|
3660
|
+
const setPosition = (position) => {
|
|
3661
|
+
if (!element) return;
|
|
3662
|
+
currentPosition = position;
|
|
3663
|
+
element.style.top = "";
|
|
3664
|
+
element.style.right = "";
|
|
3665
|
+
element.style.bottom = "";
|
|
3666
|
+
element.style.left = "";
|
|
3667
|
+
element.style.transform = "";
|
|
3668
|
+
const styles2 = POSITION_STYLES[position];
|
|
3669
|
+
styles2.split(";").filter(Boolean).forEach((rule) => {
|
|
3670
|
+
const colonIdx = rule.indexOf(":");
|
|
3671
|
+
if (colonIdx === -1) return;
|
|
3672
|
+
const prop = rule.slice(0, colonIdx).trim();
|
|
3673
|
+
const val = rule.slice(colonIdx + 1).trim();
|
|
3674
|
+
if (prop && val) {
|
|
3675
|
+
element.style.setProperty(prop, val);
|
|
3676
|
+
}
|
|
3677
|
+
});
|
|
3678
|
+
element.setAttribute("data-position", position);
|
|
3679
|
+
const isVisible = element.classList.contains("sp-watermark--visible");
|
|
3680
|
+
const visClass = isVisible ? " sp-watermark--visible" : " sp-watermark--hidden";
|
|
3681
|
+
element.className = `sp-watermark sp-watermark--${position}${visClass}${dynamic ? " sp-watermark--dynamic" : ""}`;
|
|
3682
|
+
};
|
|
3683
|
+
const randomizePosition = () => {
|
|
3684
|
+
const available = POSITIONS.filter((p) => p !== currentPosition);
|
|
3685
|
+
const next = available[Math.floor(Math.random() * available.length)];
|
|
3686
|
+
setPosition(next);
|
|
3687
|
+
};
|
|
3688
|
+
const show = () => {
|
|
3689
|
+
if (!element) return;
|
|
3690
|
+
element.classList.remove("sp-watermark--hidden");
|
|
3691
|
+
element.classList.add("sp-watermark--visible");
|
|
3692
|
+
};
|
|
3693
|
+
const hide = () => {
|
|
3694
|
+
if (!element) return;
|
|
3695
|
+
element.classList.remove("sp-watermark--visible");
|
|
3696
|
+
element.classList.add("sp-watermark--hidden");
|
|
3697
|
+
};
|
|
3698
|
+
const startDynamic = () => {
|
|
3699
|
+
if (!dynamic || dynamicTimer) return;
|
|
3700
|
+
dynamicTimer = setInterval(randomizePosition, dynamicInterval);
|
|
3701
|
+
};
|
|
3702
|
+
const stopDynamic = () => {
|
|
3703
|
+
if (dynamicTimer) {
|
|
3704
|
+
clearInterval(dynamicTimer);
|
|
3705
|
+
dynamicTimer = null;
|
|
3706
|
+
}
|
|
3707
|
+
};
|
|
3708
|
+
const cleanup = () => {
|
|
3709
|
+
stopDynamic();
|
|
3710
|
+
if (showDelayTimer) {
|
|
3711
|
+
clearTimeout(showDelayTimer);
|
|
3712
|
+
showDelayTimer = null;
|
|
3713
|
+
}
|
|
3714
|
+
if (element?.parentNode) {
|
|
3715
|
+
element.parentNode.removeChild(element);
|
|
3716
|
+
}
|
|
3717
|
+
element = null;
|
|
3718
|
+
};
|
|
3719
|
+
return {
|
|
3720
|
+
id: "watermark",
|
|
3721
|
+
name: "Watermark",
|
|
3722
|
+
version: "1.0.0",
|
|
3723
|
+
type: "feature",
|
|
3724
|
+
description: "Anti-piracy watermark overlay with text/image support and dynamic repositioning",
|
|
3725
|
+
init(pluginApi) {
|
|
3726
|
+
api = pluginApi;
|
|
3727
|
+
api.logger.debug("Watermark plugin initialized");
|
|
3728
|
+
element = createElement2();
|
|
3729
|
+
api.container.appendChild(element);
|
|
3730
|
+
const unsubPlay = api.on("playback:play", () => {
|
|
3731
|
+
if (showDelay > 0) {
|
|
3732
|
+
showDelayTimer = setTimeout(() => {
|
|
3733
|
+
show();
|
|
3734
|
+
startDynamic();
|
|
3735
|
+
}, showDelay);
|
|
3736
|
+
} else {
|
|
3737
|
+
show();
|
|
3738
|
+
startDynamic();
|
|
3739
|
+
}
|
|
3740
|
+
});
|
|
3741
|
+
const unsubPause = api.on("playback:pause", () => {
|
|
3742
|
+
hide();
|
|
3743
|
+
stopDynamic();
|
|
3744
|
+
if (showDelayTimer) {
|
|
3745
|
+
clearTimeout(showDelayTimer);
|
|
3746
|
+
showDelayTimer = null;
|
|
3747
|
+
}
|
|
3748
|
+
});
|
|
3749
|
+
const unsubEnded = api.on("playback:ended", () => {
|
|
3750
|
+
hide();
|
|
3751
|
+
stopDynamic();
|
|
3752
|
+
});
|
|
3753
|
+
const unsubChange = api.on("playlist:change", ({ track }) => {
|
|
3754
|
+
if (!element || !track) return;
|
|
3755
|
+
const metadata = track.metadata;
|
|
3756
|
+
if (metadata) {
|
|
3757
|
+
const watermarkUrl = metadata.watermarkUrl;
|
|
3758
|
+
const watermarkText = metadata.watermarkText;
|
|
3759
|
+
if (watermarkUrl || watermarkText) {
|
|
3760
|
+
updateContent(element, watermarkUrl, watermarkText);
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
});
|
|
3764
|
+
api.onDestroy(() => {
|
|
3765
|
+
unsubPlay();
|
|
3766
|
+
unsubPause();
|
|
3767
|
+
unsubEnded();
|
|
3768
|
+
unsubChange();
|
|
3769
|
+
cleanup();
|
|
3770
|
+
});
|
|
3771
|
+
},
|
|
3772
|
+
destroy() {
|
|
3773
|
+
api?.logger.debug("Watermark plugin destroyed");
|
|
3774
|
+
cleanup();
|
|
3775
|
+
api = null;
|
|
3776
|
+
},
|
|
3777
|
+
setText(text) {
|
|
3778
|
+
if (element) updateContent(element, void 0, text);
|
|
3779
|
+
},
|
|
3780
|
+
setImage(imageUrl) {
|
|
3781
|
+
if (element) updateContent(element, imageUrl);
|
|
3782
|
+
},
|
|
3783
|
+
setPosition,
|
|
3784
|
+
setOpacity(value) {
|
|
3785
|
+
if (element) element.style.opacity = String(Math.max(0, Math.min(1, value)));
|
|
3786
|
+
},
|
|
3787
|
+
show,
|
|
3788
|
+
hide,
|
|
3789
|
+
getConfig() {
|
|
3790
|
+
return { ...config, position: currentPosition, opacity: element ? parseFloat(element.style.opacity) || opacity : opacity };
|
|
3791
|
+
}
|
|
3792
|
+
};
|
|
3793
|
+
}
|
|
3794
|
+
function createCaptionsPlugin(config = {}) {
|
|
3795
|
+
let api = null;
|
|
3796
|
+
let video = null;
|
|
3797
|
+
let addedTrackElements = [];
|
|
3798
|
+
const extractFromHLS = config.extractFromHLS !== false;
|
|
3799
|
+
const autoSelect = config.autoSelect ?? false;
|
|
3800
|
+
const defaultLanguage = config.defaultLanguage ?? "en";
|
|
3801
|
+
const getVideo2 = () => {
|
|
3802
|
+
if (video) return video;
|
|
3803
|
+
video = api?.container.querySelector("video") ?? null;
|
|
3804
|
+
return video;
|
|
3805
|
+
};
|
|
3806
|
+
const cleanupTracks = () => {
|
|
3807
|
+
for (const trackEl of addedTrackElements) {
|
|
3808
|
+
trackEl.parentNode?.removeChild(trackEl);
|
|
3809
|
+
}
|
|
3810
|
+
addedTrackElements = [];
|
|
3811
|
+
api?.setState("textTracks", []);
|
|
3812
|
+
api?.setState("currentTextTrack", null);
|
|
3813
|
+
};
|
|
3814
|
+
const addTrackElement = (source) => {
|
|
3815
|
+
const videoEl = getVideo2();
|
|
3816
|
+
if (!videoEl) throw new Error("No video element");
|
|
3817
|
+
const trackEl = document.createElement("track");
|
|
3818
|
+
trackEl.kind = source.kind || "subtitles";
|
|
3819
|
+
trackEl.label = source.label;
|
|
3820
|
+
trackEl.srclang = source.language;
|
|
3821
|
+
trackEl.src = source.src;
|
|
3822
|
+
trackEl.default = false;
|
|
3823
|
+
videoEl.appendChild(trackEl);
|
|
3824
|
+
addedTrackElements.push(trackEl);
|
|
3825
|
+
if (trackEl.track) {
|
|
3826
|
+
trackEl.track.mode = "disabled";
|
|
3827
|
+
}
|
|
3828
|
+
return trackEl;
|
|
3829
|
+
};
|
|
3830
|
+
const syncTracksToState = () => {
|
|
3831
|
+
const videoEl = getVideo2();
|
|
3832
|
+
if (!videoEl) return;
|
|
3833
|
+
const tracks = [];
|
|
3834
|
+
let currentTrack = null;
|
|
3835
|
+
for (let i = 0; i < videoEl.textTracks.length; i++) {
|
|
3836
|
+
const track = videoEl.textTracks[i];
|
|
3837
|
+
if (track.kind !== "subtitles" && track.kind !== "captions") continue;
|
|
3838
|
+
const scarlettTrack = {
|
|
3839
|
+
id: `track-${i}`,
|
|
3840
|
+
label: track.label || `Track ${i + 1}`,
|
|
3841
|
+
language: track.language || "",
|
|
3842
|
+
kind: track.kind,
|
|
3843
|
+
active: track.mode === "showing"
|
|
3844
|
+
};
|
|
3845
|
+
tracks.push(scarlettTrack);
|
|
3846
|
+
if (track.mode === "showing") {
|
|
3847
|
+
currentTrack = scarlettTrack;
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
api?.setState("textTracks", tracks);
|
|
3851
|
+
api?.setState("currentTextTrack", currentTrack);
|
|
3852
|
+
};
|
|
3853
|
+
const selectTrack = (trackId) => {
|
|
3854
|
+
const videoEl = getVideo2();
|
|
3855
|
+
if (!videoEl) return;
|
|
3856
|
+
for (let i = 0; i < videoEl.textTracks.length; i++) {
|
|
3857
|
+
const track = videoEl.textTracks[i];
|
|
3858
|
+
if (track.kind !== "subtitles" && track.kind !== "captions") continue;
|
|
3859
|
+
const id = `track-${i}`;
|
|
3860
|
+
if (trackId && id === trackId) {
|
|
3861
|
+
track.mode = "showing";
|
|
3862
|
+
} else {
|
|
3863
|
+
track.mode = "disabled";
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
syncTracksToState();
|
|
3867
|
+
};
|
|
3868
|
+
const extractHlsSubtitles = () => {
|
|
3869
|
+
if (!extractFromHLS || !api) return;
|
|
3870
|
+
const hlsPlugin = api.getPlugin("hls-provider");
|
|
3871
|
+
if (!hlsPlugin || hlsPlugin.isNativeHLS()) return;
|
|
3872
|
+
const hlsInstance = hlsPlugin.getHlsInstance();
|
|
3873
|
+
if (!hlsInstance?.subtitleTracks?.length) return;
|
|
3874
|
+
api.logger.debug("Extracting HLS subtitle tracks", {
|
|
3875
|
+
count: hlsInstance.subtitleTracks.length
|
|
3876
|
+
});
|
|
3877
|
+
for (const hlsTrack of hlsInstance.subtitleTracks) {
|
|
3878
|
+
addTrackElement({
|
|
3879
|
+
language: hlsTrack.lang || "unknown",
|
|
3880
|
+
label: hlsTrack.name || `Subtitle ${hlsTrack.id}`,
|
|
3881
|
+
src: hlsTrack.url,
|
|
3882
|
+
kind: "subtitles"
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
syncTracksToState();
|
|
3886
|
+
if (autoSelect) {
|
|
3887
|
+
autoSelectTrack();
|
|
3888
|
+
}
|
|
3889
|
+
};
|
|
3890
|
+
const autoSelectTrack = () => {
|
|
3891
|
+
const tracks = api?.getState("textTracks") || [];
|
|
3892
|
+
const match = tracks.find((t) => t.language === defaultLanguage);
|
|
3893
|
+
if (match) {
|
|
3894
|
+
selectTrack(match.id);
|
|
3895
|
+
api?.logger.debug("Auto-selected caption track", { language: defaultLanguage, id: match.id });
|
|
3896
|
+
}
|
|
3897
|
+
};
|
|
3898
|
+
const initSources = () => {
|
|
3899
|
+
if (!config.sources?.length) return;
|
|
3900
|
+
for (const source of config.sources) {
|
|
3901
|
+
addTrackElement(source);
|
|
3902
|
+
}
|
|
3903
|
+
syncTracksToState();
|
|
3904
|
+
if (autoSelect) {
|
|
3905
|
+
autoSelectTrack();
|
|
3906
|
+
}
|
|
3907
|
+
};
|
|
3908
|
+
return {
|
|
3909
|
+
id: "captions",
|
|
3910
|
+
name: "Captions",
|
|
3911
|
+
version: "1.0.0",
|
|
3912
|
+
type: "feature",
|
|
3913
|
+
description: "WebVTT subtitles and closed captions with HLS extraction",
|
|
3914
|
+
init(pluginApi) {
|
|
3915
|
+
api = pluginApi;
|
|
3916
|
+
api.logger.debug("Captions plugin initialized");
|
|
3917
|
+
api.setState("textTracks", []);
|
|
3918
|
+
api.setState("currentTextTrack", null);
|
|
3919
|
+
const unsubTrackText = api.on("track:text", ({ trackId }) => {
|
|
3920
|
+
selectTrack(trackId);
|
|
3921
|
+
});
|
|
3922
|
+
const unsubLoaded = api.on("media:loaded", () => {
|
|
3923
|
+
video = null;
|
|
3924
|
+
cleanupTracks();
|
|
3925
|
+
initSources();
|
|
3926
|
+
if (extractFromHLS) {
|
|
3927
|
+
setTimeout(extractHlsSubtitles, 500);
|
|
3928
|
+
}
|
|
3929
|
+
});
|
|
3930
|
+
const unsubLoadRequest = api.on("media:load-request", () => {
|
|
3931
|
+
video = null;
|
|
3932
|
+
cleanupTracks();
|
|
3933
|
+
});
|
|
3934
|
+
api.onDestroy(() => {
|
|
3935
|
+
unsubTrackText();
|
|
3936
|
+
unsubLoaded();
|
|
3937
|
+
unsubLoadRequest();
|
|
3938
|
+
cleanupTracks();
|
|
3939
|
+
});
|
|
3940
|
+
},
|
|
3941
|
+
destroy() {
|
|
3942
|
+
api?.logger.debug("Captions plugin destroyed");
|
|
3943
|
+
cleanupTracks();
|
|
3944
|
+
video = null;
|
|
3945
|
+
api = null;
|
|
3946
|
+
}
|
|
3947
|
+
};
|
|
3948
|
+
}
|
|
3516
3949
|
class Signal {
|
|
3517
3950
|
constructor(initialValue) {
|
|
3518
3951
|
this.subscribers = /* @__PURE__ */ new Set();
|
|
@@ -5052,6 +5485,13 @@ class ScarlettPlayer {
|
|
|
5052
5485
|
await this.pluginManager.initPlugin(id);
|
|
5053
5486
|
}
|
|
5054
5487
|
}
|
|
5488
|
+
this.eventBus.on("media:load-request", async ({ src, autoplay }) => {
|
|
5489
|
+
if (this.stateManager.getValue("chromecastActive")) return;
|
|
5490
|
+
await this.load(src);
|
|
5491
|
+
if (autoplay !== false) {
|
|
5492
|
+
await this.play();
|
|
5493
|
+
}
|
|
5494
|
+
});
|
|
5055
5495
|
if (this.initialSrc) {
|
|
5056
5496
|
await this.load(this.initialSrc);
|
|
5057
5497
|
}
|
|
@@ -5259,8 +5699,9 @@ class ScarlettPlayer {
|
|
|
5259
5699
|
*/
|
|
5260
5700
|
setPlaybackRate(rate) {
|
|
5261
5701
|
this.checkDestroyed();
|
|
5262
|
-
|
|
5263
|
-
this.
|
|
5702
|
+
const clampedRate = Math.max(0.0625, Math.min(16, rate));
|
|
5703
|
+
this.stateManager.set("playbackRate", clampedRate);
|
|
5704
|
+
this.eventBus.emit("playback:ratechange", { rate: clampedRate });
|
|
5264
5705
|
}
|
|
5265
5706
|
/**
|
|
5266
5707
|
* Set autoplay state.
|
|
@@ -5389,6 +5830,13 @@ class ScarlettPlayer {
|
|
|
5389
5830
|
}
|
|
5390
5831
|
const provider = this._currentProvider;
|
|
5391
5832
|
if (typeof provider.setLevel === "function") {
|
|
5833
|
+
if (index !== -1) {
|
|
5834
|
+
const levels = this.getQualities();
|
|
5835
|
+
if (levels.length > 0 && (index < 0 || index >= levels.length)) {
|
|
5836
|
+
this.logger.warn(`Invalid quality index: ${index} (available: ${levels.length})`);
|
|
5837
|
+
return;
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5392
5840
|
provider.setLevel(index);
|
|
5393
5841
|
this.eventBus.emit("quality:change", {
|
|
5394
5842
|
quality: index === -1 ? "auto" : `level-${index}`,
|
|
@@ -5629,18 +6077,40 @@ class ScarlettPlayer {
|
|
|
5629
6077
|
* @private
|
|
5630
6078
|
*/
|
|
5631
6079
|
detectMimeType(source) {
|
|
5632
|
-
|
|
6080
|
+
let path = source;
|
|
6081
|
+
try {
|
|
6082
|
+
path = new URL(source).pathname;
|
|
6083
|
+
} catch {
|
|
6084
|
+
const noQuery = source.split("?")[0] ?? source;
|
|
6085
|
+
path = noQuery.split("#")[0] ?? noQuery;
|
|
6086
|
+
}
|
|
6087
|
+
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
5633
6088
|
switch (ext) {
|
|
5634
6089
|
case "m3u8":
|
|
5635
6090
|
return "application/x-mpegURL";
|
|
5636
6091
|
case "mpd":
|
|
5637
6092
|
return "application/dash+xml";
|
|
5638
6093
|
case "mp4":
|
|
6094
|
+
case "m4v":
|
|
5639
6095
|
return "video/mp4";
|
|
5640
6096
|
case "webm":
|
|
5641
6097
|
return "video/webm";
|
|
5642
6098
|
case "ogg":
|
|
6099
|
+
case "ogv":
|
|
5643
6100
|
return "video/ogg";
|
|
6101
|
+
case "mov":
|
|
6102
|
+
return "video/quicktime";
|
|
6103
|
+
case "mkv":
|
|
6104
|
+
return "video/x-matroska";
|
|
6105
|
+
case "mp3":
|
|
6106
|
+
return "audio/mpeg";
|
|
6107
|
+
case "wav":
|
|
6108
|
+
return "audio/wav";
|
|
6109
|
+
case "flac":
|
|
6110
|
+
return "audio/flac";
|
|
6111
|
+
case "aac":
|
|
6112
|
+
case "m4a":
|
|
6113
|
+
return "audio/mp4";
|
|
5644
6114
|
default:
|
|
5645
6115
|
return "video/mp4";
|
|
5646
6116
|
}
|
|
@@ -5849,6 +6319,12 @@ async function createEmbedPlayer(container, config, pluginCreators2, availableTy
|
|
|
5849
6319
|
artwork: config.artwork || config.poster || config.playlist?.[0]?.artwork
|
|
5850
6320
|
}));
|
|
5851
6321
|
}
|
|
6322
|
+
if (pluginCreators2.watermark && config.watermark) {
|
|
6323
|
+
plugins.push(pluginCreators2.watermark(config.watermark));
|
|
6324
|
+
}
|
|
6325
|
+
if (pluginCreators2.captions) {
|
|
6326
|
+
plugins.push(pluginCreators2.captions(config.captions || {}));
|
|
6327
|
+
}
|
|
5852
6328
|
if (pluginCreators2.analytics && config.analytics?.beaconUrl) {
|
|
5853
6329
|
plugins.push(pluginCreators2.analytics({
|
|
5854
6330
|
beaconUrl: config.analytics.beaconUrl,
|
|
@@ -5966,11 +6442,13 @@ function setupAutoInit(pluginCreators2, availableTypes) {
|
|
|
5966
6442
|
}
|
|
5967
6443
|
}
|
|
5968
6444
|
}
|
|
5969
|
-
const VERSION = "0.3
|
|
6445
|
+
const VERSION = "0.5.3-video";
|
|
5970
6446
|
const AVAILABLE_TYPES = ["video"];
|
|
5971
6447
|
const pluginCreators = {
|
|
5972
6448
|
hls: createHLSPlugin,
|
|
5973
|
-
videoUI: uiPlugin
|
|
6449
|
+
videoUI: uiPlugin,
|
|
6450
|
+
watermark: createWatermarkPlugin,
|
|
6451
|
+
captions: createCaptionsPlugin
|
|
5974
6452
|
// Audio UI not available in this build
|
|
5975
6453
|
// Analytics not available in this build
|
|
5976
6454
|
// Playlist not available in this build
|