@scarlett-player/embed 1.5.1 → 1.7.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 +454 -7
- 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 +638 -59
- 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 +270 -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/dist/hls2.js +12 -0
- package/dist/hls2.js.map +1 -0
- package/package.json +10 -10
package/dist/embed.video.js
CHANGED
|
@@ -149,6 +149,7 @@ class StateManager {
|
|
|
149
149
|
this.signals = /* @__PURE__ */ new Map();
|
|
150
150
|
this.changeSubscribers = /* @__PURE__ */ new Set();
|
|
151
151
|
this.definedDefaults = /* @__PURE__ */ new Map();
|
|
152
|
+
this.destroyed = false;
|
|
152
153
|
this.initializeSignals(initialState);
|
|
153
154
|
}
|
|
154
155
|
/**
|
|
@@ -209,6 +210,7 @@ class StateManager {
|
|
|
209
210
|
*
|
|
210
211
|
* @param key - State property key
|
|
211
212
|
* @returns Signal for the property
|
|
213
|
+
* @throws If the manager has been destroyed, or the key was never registered
|
|
212
214
|
*
|
|
213
215
|
* @example
|
|
214
216
|
* ```ts
|
|
@@ -218,6 +220,9 @@ class StateManager {
|
|
|
218
220
|
* ```
|
|
219
221
|
*/
|
|
220
222
|
get(key) {
|
|
223
|
+
if (this.destroyed) {
|
|
224
|
+
throw new Error(`[StateManager] Manager is destroyed (reading '${key}')`);
|
|
225
|
+
}
|
|
221
226
|
const stateSignal = this.signals.get(key);
|
|
222
227
|
if (!stateSignal) {
|
|
223
228
|
throw new Error(`[StateManager] Unknown state key: ${key}`);
|
|
@@ -229,6 +234,7 @@ class StateManager {
|
|
|
229
234
|
*
|
|
230
235
|
* @param key - State property key
|
|
231
236
|
* @returns Current value
|
|
237
|
+
* @throws If the manager has been destroyed, or the key was never registered
|
|
232
238
|
*
|
|
233
239
|
* @example
|
|
234
240
|
* ```ts
|
|
@@ -396,6 +402,11 @@ class StateManager {
|
|
|
396
402
|
/**
|
|
397
403
|
* Destroy the state manager and cleanup all signals.
|
|
398
404
|
*
|
|
405
|
+
* After this, every read or write ({@link get}, {@link getValue},
|
|
406
|
+
* {@link set}) throws a destroyed-specific error. Returning last-known
|
|
407
|
+
* values instead was considered and rejected: it silently masks the
|
|
408
|
+
* lifecycle bugs this throw exposes.
|
|
409
|
+
*
|
|
399
410
|
* @example
|
|
400
411
|
* ```ts
|
|
401
412
|
* state.destroy();
|
|
@@ -405,6 +416,7 @@ class StateManager {
|
|
|
405
416
|
this.signals.forEach((stateSignal) => stateSignal.destroy());
|
|
406
417
|
this.signals.clear();
|
|
407
418
|
this.changeSubscribers.clear();
|
|
419
|
+
this.destroyed = true;
|
|
408
420
|
}
|
|
409
421
|
}
|
|
410
422
|
const DEFAULT_OPTIONS = {
|
|
@@ -1629,6 +1641,7 @@ class ScarlettPlayer {
|
|
|
1629
1641
|
this.eventBus.on("media:load-request", async ({ src, autoplay }) => {
|
|
1630
1642
|
if (this.stateManager.getValue("chromecastActive")) return;
|
|
1631
1643
|
await this.load(src);
|
|
1644
|
+
if (this.destroyed) return;
|
|
1632
1645
|
if (autoplay !== false) {
|
|
1633
1646
|
await this.play();
|
|
1634
1647
|
}
|
|
@@ -1637,12 +1650,14 @@ class ScarlettPlayer {
|
|
|
1637
1650
|
const was_live = this.stateManager.getValue("live");
|
|
1638
1651
|
const resume_at = this.stateManager.getValue("currentTime");
|
|
1639
1652
|
await this.load(src);
|
|
1653
|
+
if (this.destroyed) return;
|
|
1640
1654
|
if (this.stateManager.getValue("error")) return;
|
|
1641
1655
|
if (was_live) {
|
|
1642
1656
|
this.seekToLive();
|
|
1643
1657
|
} else if (resume_at > 0) {
|
|
1644
1658
|
this.seek(resume_at);
|
|
1645
1659
|
}
|
|
1660
|
+
if (this.destroyed) return;
|
|
1646
1661
|
await this.play();
|
|
1647
1662
|
});
|
|
1648
1663
|
if (this.initialSrc) {
|
|
@@ -2140,6 +2155,7 @@ class ScarlettPlayer {
|
|
|
2140
2155
|
return;
|
|
2141
2156
|
}
|
|
2142
2157
|
this.logger.info("Destroying player");
|
|
2158
|
+
this.loadGeneration++;
|
|
2143
2159
|
if (this.seekResumeTimeout !== null) {
|
|
2144
2160
|
clearTimeout(this.seekResumeTimeout);
|
|
2145
2161
|
this.seekResumeTimeout = null;
|
|
@@ -2287,6 +2303,15 @@ var __export = (target, all) => {
|
|
|
2287
2303
|
for (var name in all)
|
|
2288
2304
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
2289
2305
|
};
|
|
2306
|
+
function sanitizeUrl(url) {
|
|
2307
|
+
if (!url) return void 0;
|
|
2308
|
+
try {
|
|
2309
|
+
const parsed = new URL(url);
|
|
2310
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
2311
|
+
} catch {
|
|
2312
|
+
return void 0;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2290
2315
|
function formatLevel(level) {
|
|
2291
2316
|
if (level.name) {
|
|
2292
2317
|
return level.name;
|
|
@@ -2708,6 +2733,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2708
2733
|
let reconnectWindowStart = 0;
|
|
2709
2734
|
let reconnectResumePosition = 0;
|
|
2710
2735
|
let onlineListener = null;
|
|
2736
|
+
let reconnectTriggerError = null;
|
|
2737
|
+
let reconnectExhausted = false;
|
|
2711
2738
|
const getOrCreateVideo = () => {
|
|
2712
2739
|
if (video) return video;
|
|
2713
2740
|
const existing = api?.container.querySelector("video");
|
|
@@ -2816,6 +2843,22 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2816
2843
|
return ErrorCode.PLAYBACK_FAILED;
|
|
2817
2844
|
}
|
|
2818
2845
|
};
|
|
2846
|
+
const buildErrorDetail = (error, retriesExhausted) => {
|
|
2847
|
+
const attempts = error.type === "network" ? networkRetryCount : error.type === "media" ? mediaRetryCount : 0;
|
|
2848
|
+
const detail = {
|
|
2849
|
+
type: error.type,
|
|
2850
|
+
retriesExhausted,
|
|
2851
|
+
attempts
|
|
2852
|
+
};
|
|
2853
|
+
if (typeof error.response?.code === "number" && error.response.code > 0) {
|
|
2854
|
+
detail.httpStatus = error.response.code;
|
|
2855
|
+
}
|
|
2856
|
+
const url = sanitizeUrl(error.url);
|
|
2857
|
+
if (url) {
|
|
2858
|
+
detail.url = url;
|
|
2859
|
+
}
|
|
2860
|
+
return detail;
|
|
2861
|
+
};
|
|
2819
2862
|
const emitFatalError = (error, retriesExhausted) => {
|
|
2820
2863
|
const message = retriesExhausted ? `HLS error: ${error.details} (max retries exceeded)` : `HLS error: ${error.details}`;
|
|
2821
2864
|
api?.logger.error(message, { type: error.type, details: error.details });
|
|
@@ -2825,7 +2868,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2825
2868
|
code: mapFatalErrorCode(error),
|
|
2826
2869
|
message,
|
|
2827
2870
|
fatal: true,
|
|
2828
|
-
timestamp: Date.now()
|
|
2871
|
+
timestamp: Date.now(),
|
|
2872
|
+
detail: buildErrorDetail(error, retriesExhausted)
|
|
2829
2873
|
});
|
|
2830
2874
|
maybeScheduleReconnect(error);
|
|
2831
2875
|
};
|
|
@@ -2902,6 +2946,62 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2902
2946
|
}
|
|
2903
2947
|
return false;
|
|
2904
2948
|
};
|
|
2949
|
+
const handleNativeFatalError = (error, resumePosition) => {
|
|
2950
|
+
const is_network = error.type === "network";
|
|
2951
|
+
const max_retries = is_network ? mergedConfig.maxNetworkRetries ?? 3 : mergedConfig.maxMediaRetries ?? 2;
|
|
2952
|
+
const used = is_network ? networkRetryCount : mediaRetryCount;
|
|
2953
|
+
if (!currentSrc || used >= max_retries) {
|
|
2954
|
+
emitFatalError(error, used >= max_retries);
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
const resume_position = resumePosition ?? video?.currentTime ?? 0;
|
|
2958
|
+
if (is_network) {
|
|
2959
|
+
networkRetryCount++;
|
|
2960
|
+
} else {
|
|
2961
|
+
mediaRetryCount++;
|
|
2962
|
+
}
|
|
2963
|
+
const attempt = used + 1;
|
|
2964
|
+
const delay = getRetryDelay(attempt - 1);
|
|
2965
|
+
api?.logger.info(
|
|
2966
|
+
`Attempting native ${error.type} error recovery (attempt ${attempt}/${max_retries}) in ${delay}ms`
|
|
2967
|
+
);
|
|
2968
|
+
api?.emit(is_network ? "error:network" : "error:media", {
|
|
2969
|
+
error: new Error(error.details)
|
|
2970
|
+
});
|
|
2971
|
+
if (retryTimeout) {
|
|
2972
|
+
clearTimeout(retryTimeout);
|
|
2973
|
+
}
|
|
2974
|
+
const retry_session = loadSession;
|
|
2975
|
+
retryTimeout = setTimeout(() => {
|
|
2976
|
+
if (retry_session !== loadSession) return;
|
|
2977
|
+
void recoverNative(error, resume_position);
|
|
2978
|
+
}, delay);
|
|
2979
|
+
};
|
|
2980
|
+
const recoverNative = async (error, resumePosition) => {
|
|
2981
|
+
if (!currentSrc) return;
|
|
2982
|
+
const session = ++loadSession;
|
|
2983
|
+
const saved_src = currentSrc;
|
|
2984
|
+
const was_live = api?.getState("live") ?? false;
|
|
2985
|
+
try {
|
|
2986
|
+
teardownPipeline(new Error("HLS load cancelled: native error recovery"));
|
|
2987
|
+
api?.setState("playbackState", "loading");
|
|
2988
|
+
await loadNative(saved_src);
|
|
2989
|
+
if (session !== loadSession) return;
|
|
2990
|
+
if (!was_live && video && resumePosition > 0) {
|
|
2991
|
+
video.currentTime = resumePosition;
|
|
2992
|
+
}
|
|
2993
|
+
api?.setState("playbackState", "ready");
|
|
2994
|
+
api?.setState("buffering", false);
|
|
2995
|
+
try {
|
|
2996
|
+
await video?.play();
|
|
2997
|
+
} catch {
|
|
2998
|
+
}
|
|
2999
|
+
} catch {
|
|
3000
|
+
if (session !== loadSession) return;
|
|
3001
|
+
api?.logger.warn("Native error recovery attempt failed");
|
|
3002
|
+
handleNativeFatalError(error, resumePosition);
|
|
3003
|
+
}
|
|
3004
|
+
};
|
|
2905
3005
|
const loadNative = async (src) => {
|
|
2906
3006
|
const session = loadSession;
|
|
2907
3007
|
const videoEl = getOrCreateVideo();
|
|
@@ -2943,12 +3043,24 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
2943
3043
|
const media_error = videoEl.error;
|
|
2944
3044
|
const hls_error = {
|
|
2945
3045
|
type: media_error?.code === MediaError.MEDIA_ERR_NETWORK ? "network" : "media",
|
|
2946
|
-
details: media_error?.message || "Native HLS playback error"
|
|
3046
|
+
details: media_error?.message || "Native HLS playback error",
|
|
3047
|
+
fatal: true
|
|
2947
3048
|
};
|
|
2948
|
-
|
|
3049
|
+
handleNativeFatalError(hls_error);
|
|
2949
3050
|
};
|
|
2950
3051
|
videoEl.addEventListener("error", onFatalVideoError);
|
|
2951
|
-
const
|
|
3052
|
+
const onPlayingResetBudget = () => {
|
|
3053
|
+
if (networkRetryCount > 0 || mediaRetryCount > 0) {
|
|
3054
|
+
api?.logger.debug("Native playback recovered, resetting retry budgets");
|
|
3055
|
+
networkRetryCount = 0;
|
|
3056
|
+
mediaRetryCount = 0;
|
|
3057
|
+
}
|
|
3058
|
+
};
|
|
3059
|
+
videoEl.addEventListener("playing", onPlayingResetBudget);
|
|
3060
|
+
const removeFatalListener = () => {
|
|
3061
|
+
videoEl.removeEventListener("error", onFatalVideoError);
|
|
3062
|
+
videoEl.removeEventListener("playing", onPlayingResetBudget);
|
|
3063
|
+
};
|
|
2952
3064
|
const previous_cleanup = cleanupVideoEvents;
|
|
2953
3065
|
cleanupVideoEvents = () => {
|
|
2954
3066
|
removeFatalListener();
|
|
@@ -3073,12 +3185,38 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3073
3185
|
reconnectAttempts = 0;
|
|
3074
3186
|
reconnectWindowStart = 0;
|
|
3075
3187
|
reconnectResumePosition = 0;
|
|
3188
|
+
reconnectTriggerError = null;
|
|
3189
|
+
reconnectExhausted = false;
|
|
3190
|
+
};
|
|
3191
|
+
const emitReconnectExhausted = (elapsedMs, windowMs) => {
|
|
3192
|
+
if (reconnectExhausted) return;
|
|
3193
|
+
reconnectExhausted = true;
|
|
3194
|
+
const attempts = reconnectAttempts;
|
|
3195
|
+
const trigger = reconnectTriggerError;
|
|
3196
|
+
api?.emit("error:reconnect-exhausted", { attempts, elapsedMs, windowMs });
|
|
3197
|
+
api?.setState("playbackState", "error");
|
|
3198
|
+
api?.setState("buffering", false);
|
|
3199
|
+
api?.emit("error", {
|
|
3200
|
+
code: trigger ? mapFatalErrorCode(trigger) : ErrorCode.PLAYBACK_FAILED,
|
|
3201
|
+
message: `HLS auto-reconnect gave up after ${attempts} attempts over ${Math.round(elapsedMs / 1e3)}s`,
|
|
3202
|
+
fatal: true,
|
|
3203
|
+
timestamp: Date.now(),
|
|
3204
|
+
detail: {
|
|
3205
|
+
type: trigger?.type ?? "other",
|
|
3206
|
+
retriesExhausted: true,
|
|
3207
|
+
attempts,
|
|
3208
|
+
reconnectExhausted: true
|
|
3209
|
+
}
|
|
3210
|
+
});
|
|
3076
3211
|
};
|
|
3077
3212
|
const scheduleReconnectAttempt = () => {
|
|
3213
|
+
if (reconnectExhausted) return;
|
|
3078
3214
|
if (reconnectTimer) return;
|
|
3079
3215
|
const window_ms = mergedConfig.reconnectWindowMs ?? 3e5;
|
|
3080
|
-
|
|
3216
|
+
const elapsed_ms = Date.now() - reconnectWindowStart;
|
|
3217
|
+
if (elapsed_ms > window_ms) {
|
|
3081
3218
|
api?.logger.warn(`Auto-reconnect window exhausted after ${reconnectAttempts} attempts`);
|
|
3219
|
+
emitReconnectExhausted(elapsed_ms, window_ms);
|
|
3082
3220
|
return;
|
|
3083
3221
|
}
|
|
3084
3222
|
const base_delay = mergedConfig.reconnectBaseDelayMs ?? 2e3;
|
|
@@ -3086,7 +3224,12 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3086
3224
|
const backoff = Math.min(base_delay * Math.pow(2, reconnectAttempts), max_delay);
|
|
3087
3225
|
const delay = Math.round(backoff * (0.7 + Math.random() * 0.3));
|
|
3088
3226
|
api?.logger.info(`Scheduling auto-reconnect attempt ${reconnectAttempts + 1} in ${delay}ms`);
|
|
3089
|
-
api?.emit("error:reconnecting", {
|
|
3227
|
+
api?.emit("error:reconnecting", {
|
|
3228
|
+
attempt: reconnectAttempts + 1,
|
|
3229
|
+
delayMs: delay,
|
|
3230
|
+
elapsedMs: elapsed_ms,
|
|
3231
|
+
windowMs: window_ms
|
|
3232
|
+
});
|
|
3090
3233
|
reconnectTimer = setTimeout(() => {
|
|
3091
3234
|
reconnectTimer = null;
|
|
3092
3235
|
void attemptReconnect();
|
|
@@ -3099,6 +3242,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3099
3242
|
if (reconnectWindowStart === 0) {
|
|
3100
3243
|
reconnectWindowStart = Date.now();
|
|
3101
3244
|
reconnectResumePosition = video?.currentTime ?? 0;
|
|
3245
|
+
reconnectTriggerError = error;
|
|
3102
3246
|
}
|
|
3103
3247
|
scheduleReconnectAttempt();
|
|
3104
3248
|
};
|
|
@@ -3130,7 +3274,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
3130
3274
|
}
|
|
3131
3275
|
api.setState("playbackState", "ready");
|
|
3132
3276
|
api.setState("buffering", false);
|
|
3133
|
-
api.emit("error:recovered",
|
|
3277
|
+
api.emit("error:recovered", {
|
|
3278
|
+
attempt: reconnectAttempts,
|
|
3279
|
+
elapsedMs: Date.now() - reconnectWindowStart
|
|
3280
|
+
});
|
|
3134
3281
|
api.logger.info("Auto-reconnect succeeded");
|
|
3135
3282
|
cancelReconnect();
|
|
3136
3283
|
try {
|
|
@@ -3635,6 +3782,25 @@ var styles = `
|
|
|
3635
3782
|
border-radius: inherit;
|
|
3636
3783
|
}
|
|
3637
3784
|
|
|
3785
|
+
/* Chapter markers */
|
|
3786
|
+
.sp-progress__markers {
|
|
3787
|
+
position: absolute;
|
|
3788
|
+
top: 0;
|
|
3789
|
+
left: 0;
|
|
3790
|
+
width: 100%;
|
|
3791
|
+
height: 100%;
|
|
3792
|
+
pointer-events: none;
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
.sp-progress__marker {
|
|
3796
|
+
position: absolute;
|
|
3797
|
+
top: 0;
|
|
3798
|
+
width: 2px;
|
|
3799
|
+
height: 100%;
|
|
3800
|
+
margin-left: -1px;
|
|
3801
|
+
background: rgba(0, 0, 0, 0.65);
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3638
3804
|
.sp-progress__handle {
|
|
3639
3805
|
position: absolute;
|
|
3640
3806
|
top: 50%;
|
|
@@ -3694,6 +3860,16 @@ var styles = `
|
|
|
3694
3860
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
3695
3861
|
}
|
|
3696
3862
|
|
|
3863
|
+
.sp-progress__tooltip-chapter {
|
|
3864
|
+
display: block;
|
|
3865
|
+
max-width: 220px;
|
|
3866
|
+
overflow: hidden;
|
|
3867
|
+
color: rgba(255, 255, 255, 0.75);
|
|
3868
|
+
font-weight: 400;
|
|
3869
|
+
font-variant-numeric: normal;
|
|
3870
|
+
text-overflow: ellipsis;
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3697
3873
|
@media (hover: hover) {
|
|
3698
3874
|
.sp-progress-wrapper:hover .sp-progress__tooltip {
|
|
3699
3875
|
opacity: 1;
|
|
@@ -4531,6 +4707,8 @@ var ProgressBar = class {
|
|
|
4531
4707
|
this.lastSeekTime = 0;
|
|
4532
4708
|
this.seekThrottleMs = 100;
|
|
4533
4709
|
this.wasPlayingBeforeDrag = false;
|
|
4710
|
+
this.renderedChapters = null;
|
|
4711
|
+
this.renderedDuration = 0;
|
|
4534
4712
|
this.onMouseDown = (e) => {
|
|
4535
4713
|
e.preventDefault();
|
|
4536
4714
|
const video = getVideo(this.api.container);
|
|
@@ -4665,12 +4843,14 @@ var ProgressBar = class {
|
|
|
4665
4843
|
const track = createElement("div", { className: "sp-progress__track" });
|
|
4666
4844
|
this.buffered = createElement("div", { className: "sp-progress__buffered" });
|
|
4667
4845
|
this.filled = createElement("div", { className: "sp-progress__filled" });
|
|
4846
|
+
this.markers = createElement("div", { className: "sp-progress__markers" });
|
|
4668
4847
|
this.handle = createElement("div", { className: "sp-progress__handle" });
|
|
4669
4848
|
this.tooltip = createElement("div", { className: "sp-progress__tooltip" });
|
|
4670
4849
|
this.tooltip.textContent = "0:00";
|
|
4671
4850
|
this.thumbnailPreview = new ThumbnailPreview();
|
|
4672
4851
|
track.appendChild(this.buffered);
|
|
4673
4852
|
track.appendChild(this.filled);
|
|
4853
|
+
track.appendChild(this.markers);
|
|
4674
4854
|
track.appendChild(this.handle);
|
|
4675
4855
|
this.el.appendChild(track);
|
|
4676
4856
|
this.el.appendChild(this.thumbnailPreview.getElement());
|
|
@@ -4720,6 +4900,7 @@ var ProgressBar = class {
|
|
|
4720
4900
|
this.thumbnailPreview.setConfig(thumbnails);
|
|
4721
4901
|
}
|
|
4722
4902
|
this.el.classList.toggle("sp-progress--live", !!live);
|
|
4903
|
+
this.updateMarkers(duration, live, seekableRange);
|
|
4723
4904
|
if (live && seekableRange) {
|
|
4724
4905
|
const rangeLength = seekableRange.end - seekableRange.start;
|
|
4725
4906
|
if (rangeLength > 0) {
|
|
@@ -4752,6 +4933,70 @@ var ProgressBar = class {
|
|
|
4752
4933
|
this.el.setAttribute("aria-valuetext", formatTime(currentTime));
|
|
4753
4934
|
}
|
|
4754
4935
|
}
|
|
4936
|
+
/**
|
|
4937
|
+
* Label of the chapter containing a point on the timeline.
|
|
4938
|
+
*
|
|
4939
|
+
* Mirrors the chapters plugin's own lookup: a start time belongs to its
|
|
4940
|
+
* chapter, an end time belongs to the next one, and a point in a gap between
|
|
4941
|
+
* sparse chapters belongs to neither.
|
|
4942
|
+
*
|
|
4943
|
+
* @param time - Position in seconds
|
|
4944
|
+
* @returns The chapter label, or null when the point is outside every chapter
|
|
4945
|
+
*/
|
|
4946
|
+
chapterLabelAt(time) {
|
|
4947
|
+
const chapters = this.api.getState("chapters") ?? [];
|
|
4948
|
+
for (let i = chapters.length - 1; i >= 0; i--) {
|
|
4949
|
+
const chapter = chapters[i];
|
|
4950
|
+
if (time < chapter.time) continue;
|
|
4951
|
+
const next = chapters[i + 1];
|
|
4952
|
+
const end = chapter.endTime ?? (next ? next.time : Infinity);
|
|
4953
|
+
return time < end ? chapter.label : null;
|
|
4954
|
+
}
|
|
4955
|
+
return null;
|
|
4956
|
+
}
|
|
4957
|
+
/**
|
|
4958
|
+
* Paint chapter dividers along the track.
|
|
4959
|
+
*
|
|
4960
|
+
* Reads the `chapters` state that core owns, so this works whether the list
|
|
4961
|
+
* came from the chapters plugin or the host set it directly, and renders
|
|
4962
|
+
* nothing at all when there are none.
|
|
4963
|
+
*
|
|
4964
|
+
* Rebuilds only when the list or the duration actually changed. `update()`
|
|
4965
|
+
* runs on every time update, and rebuilding a dozen nodes 4 times a second
|
|
4966
|
+
* would churn the DOM for no reason.
|
|
4967
|
+
*
|
|
4968
|
+
* @param duration - Media duration in seconds
|
|
4969
|
+
* @param live - Whether the media is live
|
|
4970
|
+
* @param seekableRange - DVR window, when the media is live
|
|
4971
|
+
*/
|
|
4972
|
+
updateMarkers(duration, live, seekableRange) {
|
|
4973
|
+
const chapters = this.api.getState("chapters") ?? [];
|
|
4974
|
+
const range = live ? seekableRange ? seekableRange.end - seekableRange.start : 0 : duration;
|
|
4975
|
+
if (chapters.length === 0 || range <= 0) {
|
|
4976
|
+
if (this.renderedChapters !== null) {
|
|
4977
|
+
this.markers.textContent = "";
|
|
4978
|
+
this.renderedChapters = null;
|
|
4979
|
+
this.renderedDuration = 0;
|
|
4980
|
+
}
|
|
4981
|
+
return;
|
|
4982
|
+
}
|
|
4983
|
+
if (chapters === this.renderedChapters && range === this.renderedDuration) {
|
|
4984
|
+
return;
|
|
4985
|
+
}
|
|
4986
|
+
const origin = live && seekableRange ? seekableRange.start : 0;
|
|
4987
|
+
this.markers.textContent = "";
|
|
4988
|
+
for (const chapter of chapters) {
|
|
4989
|
+
if (chapter.time <= origin) continue;
|
|
4990
|
+
const percent = (chapter.time - origin) / range * 100;
|
|
4991
|
+
if (percent <= 0 || percent >= 100) continue;
|
|
4992
|
+
const marker = createElement("div", { className: "sp-progress__marker" });
|
|
4993
|
+
marker.style.left = `${percent}%`;
|
|
4994
|
+
marker.title = chapter.label;
|
|
4995
|
+
this.markers.appendChild(marker);
|
|
4996
|
+
}
|
|
4997
|
+
this.renderedChapters = chapters;
|
|
4998
|
+
this.renderedDuration = range;
|
|
4999
|
+
}
|
|
4755
5000
|
getTimeFromPosition(clientX) {
|
|
4756
5001
|
const rect = this.el.getBoundingClientRect();
|
|
4757
5002
|
const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
@@ -4776,6 +5021,12 @@ var ProgressBar = class {
|
|
|
4776
5021
|
} else {
|
|
4777
5022
|
this.tooltip.textContent = formatTime(time);
|
|
4778
5023
|
}
|
|
5024
|
+
const chapterLabel = this.chapterLabelAt(time);
|
|
5025
|
+
if (chapterLabel) {
|
|
5026
|
+
const label = createElement("span", { className: "sp-progress__tooltip-chapter" });
|
|
5027
|
+
label.textContent = chapterLabel;
|
|
5028
|
+
this.tooltip.appendChild(label);
|
|
5029
|
+
}
|
|
4779
5030
|
this.tooltip.style.left = `${percent * 100}%`;
|
|
4780
5031
|
if (this.thumbnailPreview.isConfigured()) {
|
|
4781
5032
|
this.thumbnailPreview.show(time, percent);
|
|
@@ -6187,7 +6438,15 @@ function uiPlugin(config = {}) {
|
|
|
6187
6438
|
}
|
|
6188
6439
|
hideTimeout = setTimeout(hideControls, hideDelay);
|
|
6189
6440
|
};
|
|
6441
|
+
let last_pointer_type = null;
|
|
6442
|
+
const handlePointerActivity = (event) => {
|
|
6443
|
+
last_pointer_type = event.pointerType;
|
|
6444
|
+
};
|
|
6190
6445
|
const handleInteraction = () => {
|
|
6446
|
+
if (last_pointer_type === "touch") {
|
|
6447
|
+
const gestures = api?.getPlugin("gestures");
|
|
6448
|
+
if (gestures?.ownsTapInteraction()) return;
|
|
6449
|
+
}
|
|
6191
6450
|
showControls();
|
|
6192
6451
|
};
|
|
6193
6452
|
const handleMouseLeave = () => {
|
|
@@ -6321,6 +6580,8 @@ function uiPlugin(config = {}) {
|
|
|
6321
6580
|
api.logger.debug(`Control "${id}" registered after init, rebuilding control bar`);
|
|
6322
6581
|
rebuildControlBar();
|
|
6323
6582
|
});
|
|
6583
|
+
container.addEventListener("pointerdown", handlePointerActivity, { passive: true });
|
|
6584
|
+
container.addEventListener("pointermove", handlePointerActivity, { passive: true });
|
|
6324
6585
|
container.addEventListener("mousemove", handleInteraction);
|
|
6325
6586
|
container.addEventListener("mouseenter", handleInteraction);
|
|
6326
6587
|
container.addEventListener("mouseleave", handleMouseLeave);
|
|
@@ -6358,6 +6619,8 @@ function uiPlugin(config = {}) {
|
|
|
6358
6619
|
recoveredUnsubscribe?.();
|
|
6359
6620
|
recoveredUnsubscribe = null;
|
|
6360
6621
|
if (api?.container) {
|
|
6622
|
+
api.container.removeEventListener("pointerdown", handlePointerActivity);
|
|
6623
|
+
api.container.removeEventListener("pointermove", handlePointerActivity);
|
|
6361
6624
|
api.container.removeEventListener("mousemove", handleInteraction);
|
|
6362
6625
|
api.container.removeEventListener("mouseenter", handleInteraction);
|
|
6363
6626
|
api.container.removeEventListener("mouseleave", handleMouseLeave);
|