@scarlett-player/hls 1.8.1 → 1.10.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/{chunk-VD46AQJB.js → chunk-UB2TS72E.js} +240 -58
- package/dist/index.cjs +252 -78
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -9
- package/dist/light.cjs +252 -78
- package/dist/light.d.cts +3 -3
- package/dist/light.d.ts +3 -3
- package/dist/light.js +1 -9
- package/dist/{sanitize-url-DcZXD_K-.d.cts → types-DnvcTuSn.d.cts} +3 -26
- package/dist/{sanitize-url-DcZXD_K-.d.ts → types-DnvcTuSn.d.ts} +3 -26
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -32,7 +32,7 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
createHLSPlugin: () => createHLSPlugin,
|
|
34
34
|
default: () => index_default,
|
|
35
|
-
sanitizeUrl: () => sanitizeUrl
|
|
35
|
+
sanitizeUrl: () => import_core.sanitizeUrl
|
|
36
36
|
});
|
|
37
37
|
module.exports = __toCommonJS(index_exports);
|
|
38
38
|
|
|
@@ -45,7 +45,6 @@ __export(hls_loader_exports, {
|
|
|
45
45
|
isHlsJsSupported: () => isHlsJsSupported,
|
|
46
46
|
loadHlsJs: () => loadHlsJs,
|
|
47
47
|
resetLoader: () => resetLoader,
|
|
48
|
-
shouldPreferNativeHLS: () => shouldPreferNativeHLS,
|
|
49
48
|
supportsNativeHLS: () => supportsNativeHLS
|
|
50
49
|
});
|
|
51
50
|
var hlsConstructor = null;
|
|
@@ -55,13 +54,6 @@ function supportsNativeHLS() {
|
|
|
55
54
|
const video = document.createElement("video");
|
|
56
55
|
return video.canPlayType("application/vnd.apple.mpegurl") !== "";
|
|
57
56
|
}
|
|
58
|
-
function shouldPreferNativeHLS() {
|
|
59
|
-
if (!supportsNativeHLS()) return false;
|
|
60
|
-
if (typeof navigator === "undefined") return false;
|
|
61
|
-
const ua = navigator.userAgent;
|
|
62
|
-
const isSafari = /Safari/.test(ua) && !/Chrome/.test(ua) && !/CriOS/.test(ua);
|
|
63
|
-
return isSafari;
|
|
64
|
-
}
|
|
65
57
|
function isHlsJsSupported() {
|
|
66
58
|
if (hlsConstructor) {
|
|
67
59
|
return hlsConstructor.isSupported();
|
|
@@ -111,7 +103,7 @@ function resetLoader() {
|
|
|
111
103
|
}
|
|
112
104
|
|
|
113
105
|
// src/create-hls-plugin.ts
|
|
114
|
-
var
|
|
106
|
+
var import_core2 = require("@scarlett-player/core");
|
|
115
107
|
|
|
116
108
|
// src/quality.ts
|
|
117
109
|
function formatLevel(level) {
|
|
@@ -172,6 +164,9 @@ function getInitialBandwidthEstimate(overrideBps) {
|
|
|
172
164
|
return HLS_DEFAULT_ESTIMATE;
|
|
173
165
|
}
|
|
174
166
|
|
|
167
|
+
// src/sanitize-url.ts
|
|
168
|
+
var import_core = require("@scarlett-player/core");
|
|
169
|
+
|
|
175
170
|
// src/event-map.ts
|
|
176
171
|
var HLS_ERROR_TYPES = {
|
|
177
172
|
NETWORK_ERROR: "networkError",
|
|
@@ -193,15 +188,41 @@ function mapErrorType(hlsType) {
|
|
|
193
188
|
}
|
|
194
189
|
}
|
|
195
190
|
function parseHlsError(data) {
|
|
191
|
+
const frag = data.frag;
|
|
192
|
+
const response = data.response;
|
|
193
|
+
const context = data.context;
|
|
196
194
|
return {
|
|
197
195
|
type: mapErrorType(data.type),
|
|
198
196
|
details: data.details || "Unknown error",
|
|
199
197
|
fatal: data.fatal || false,
|
|
200
|
-
|
|
198
|
+
// hls.js puts the request URL in a different place per error type:
|
|
199
|
+
// playlist errors set data.url, while fragment and key load errors set
|
|
200
|
+
// data.url not at all and carry it on data.frag.url and data.response.url.
|
|
201
|
+
// Reading only data.url dropped the failing segment from every diagnostic
|
|
202
|
+
// an origin outage produces - which is the case the diagnostics exist for.
|
|
203
|
+
url: data.url ?? frag?.url ?? response?.url ?? context?.url,
|
|
201
204
|
reason: data.reason,
|
|
202
205
|
response: data.response
|
|
203
206
|
};
|
|
204
207
|
}
|
|
208
|
+
function audioTrackId(index) {
|
|
209
|
+
return `audio-${index}`;
|
|
210
|
+
}
|
|
211
|
+
function audioTrackIndex(id) {
|
|
212
|
+
if (!id) return -1;
|
|
213
|
+
const match = /^audio-(0|[1-9]\d*)$/.exec(id);
|
|
214
|
+
return match ? Number.parseInt(match[1], 10) : -1;
|
|
215
|
+
}
|
|
216
|
+
function formatAudioTrack(track, index, active) {
|
|
217
|
+
return {
|
|
218
|
+
id: audioTrackId(index),
|
|
219
|
+
// A manifest may declare neither NAME nor LANGUAGE; a numbered fallback
|
|
220
|
+
// still gives the viewer something selectable rather than a blank row.
|
|
221
|
+
label: track.name || track.lang || `Audio ${index + 1}`,
|
|
222
|
+
language: track.lang,
|
|
223
|
+
active
|
|
224
|
+
};
|
|
225
|
+
}
|
|
205
226
|
function setupHlsEventHandlers(hls, api, callbacks) {
|
|
206
227
|
const handlers = [];
|
|
207
228
|
const addHandler = (event, handler) => {
|
|
@@ -245,6 +266,24 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
245
266
|
});
|
|
246
267
|
callbacks.onLevelSwitched?.(data.level);
|
|
247
268
|
});
|
|
269
|
+
const publishAudioTracks = (tracks, activeIndex) => {
|
|
270
|
+
const audioTracks = tracks.map(
|
|
271
|
+
(track, index) => formatAudioTrack(track, index, index === activeIndex)
|
|
272
|
+
);
|
|
273
|
+
api.setState("audioTracks", audioTracks);
|
|
274
|
+
api.setState("currentAudioTrack", audioTracks[activeIndex] ?? null);
|
|
275
|
+
};
|
|
276
|
+
addHandler("hlsAudioTracksUpdated", (_event, data) => {
|
|
277
|
+
const tracks = data.audioTracks ?? [];
|
|
278
|
+
api.logger.debug("HLS audio tracks updated", { tracks: tracks.length });
|
|
279
|
+
publishAudioTracks(tracks, hls.audioTrack);
|
|
280
|
+
callbacks.onAudioTracksUpdated?.(tracks);
|
|
281
|
+
});
|
|
282
|
+
addHandler("hlsAudioTrackSwitched", (_event, data) => {
|
|
283
|
+
api.logger.debug("HLS audio track switched", { id: data.id });
|
|
284
|
+
publishAudioTracks(hls.audioTracks ?? [], data.id);
|
|
285
|
+
callbacks.onAudioTrackSwitched?.(data.id);
|
|
286
|
+
});
|
|
248
287
|
let lastBandwidthUpdate = 0;
|
|
249
288
|
addHandler("hlsFragLoaded", () => {
|
|
250
289
|
const now = Date.now();
|
|
@@ -265,16 +304,15 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
265
304
|
if (data.details?.live !== void 0) {
|
|
266
305
|
api.setState("live", data.details.live);
|
|
267
306
|
if (data.details.live) {
|
|
307
|
+
const details = data.details;
|
|
308
|
+
const start = details.fragmentStart ?? (details.fragments?.[0]?.start ?? 0);
|
|
309
|
+
const end = details.edge ?? details.totalduration ?? 0;
|
|
310
|
+
api.setState("seekableRange", { start, end });
|
|
268
311
|
const video = hls.media;
|
|
269
|
-
if (video
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
api.setState("
|
|
273
|
-
const threshold = (data.details.targetduration ?? 3) * 3;
|
|
274
|
-
const isAtLiveEdge = end - video.currentTime < threshold;
|
|
275
|
-
api.setState("liveEdge", isAtLiveEdge);
|
|
276
|
-
const latency = end - video.currentTime;
|
|
277
|
-
api.setState("liveLatency", Math.max(0, latency));
|
|
312
|
+
if (video) {
|
|
313
|
+
const latency = Math.max(0, end - video.currentTime);
|
|
314
|
+
api.setState("liveLatency", latency);
|
|
315
|
+
api.setState("liveEdge", latency < (details.targetduration ?? 3) * 3);
|
|
278
316
|
}
|
|
279
317
|
}
|
|
280
318
|
callbacks.onLiveUpdate?.();
|
|
@@ -292,14 +330,16 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
292
330
|
api.logger.error(`HLS fatal error: ${error.details} (type=${error.type})`, {
|
|
293
331
|
type: error.type,
|
|
294
332
|
details: error.details,
|
|
295
|
-
|
|
333
|
+
status: error.response?.code,
|
|
334
|
+
url: (0, import_core.sanitizeUrl)(error.url)
|
|
296
335
|
});
|
|
297
336
|
} else {
|
|
298
337
|
api.logger.warn(`HLS error: ${error.details} (type=${error.type}, fatal=${error.fatal})`, {
|
|
299
338
|
type: error.type,
|
|
300
339
|
details: error.details,
|
|
301
340
|
fatal: error.fatal,
|
|
302
|
-
|
|
341
|
+
status: error.response?.code,
|
|
342
|
+
url: (0, import_core.sanitizeUrl)(error.url)
|
|
303
343
|
});
|
|
304
344
|
}
|
|
305
345
|
callbacks.onError?.(error);
|
|
@@ -309,6 +349,8 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
309
349
|
hls.off(event, handler);
|
|
310
350
|
}
|
|
311
351
|
handlers.length = 0;
|
|
352
|
+
api.setState("audioTracks", []);
|
|
353
|
+
api.setState("currentAudioTrack", null);
|
|
312
354
|
};
|
|
313
355
|
}
|
|
314
356
|
function setupVideoEventHandlers(video, api) {
|
|
@@ -348,19 +390,27 @@ function setupVideoEventHandlers(video, api) {
|
|
|
348
390
|
addHandler("timeupdate", () => {
|
|
349
391
|
api.setState("currentTime", video.currentTime);
|
|
350
392
|
api.emit("playback:timeupdate", { currentTime: video.currentTime });
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
393
|
+
if (video.seekable && video.seekable.length > 0) {
|
|
394
|
+
if (api.getState("live") || !Number.isFinite(video.duration)) {
|
|
395
|
+
const start = video.seekable.start(0);
|
|
396
|
+
const end = video.seekable.end(video.seekable.length - 1);
|
|
397
|
+
api.setState("seekableRange", { start, end });
|
|
398
|
+
const latency = Math.max(0, end - video.currentTime);
|
|
399
|
+
api.setState("liveEdge", latency < 10);
|
|
400
|
+
api.setState("liveLatency", latency);
|
|
401
|
+
}
|
|
359
402
|
}
|
|
360
403
|
});
|
|
361
404
|
addHandler("durationchange", () => {
|
|
362
|
-
|
|
363
|
-
|
|
405
|
+
const rawDuration = video.duration;
|
|
406
|
+
const isLive = !Number.isFinite(rawDuration) || rawDuration === Infinity;
|
|
407
|
+
if (isLive) {
|
|
408
|
+
api.setState("live", true);
|
|
409
|
+
api.setState("duration", 0);
|
|
410
|
+
} else {
|
|
411
|
+
api.setState("duration", rawDuration || 0);
|
|
412
|
+
}
|
|
413
|
+
api.emit("media:loadedmetadata", { duration: api.getState("duration") });
|
|
364
414
|
});
|
|
365
415
|
addHandler("waiting", () => {
|
|
366
416
|
api.setState("waiting", true);
|
|
@@ -495,19 +545,8 @@ function createValidatingPlaylistLoader(Hls) {
|
|
|
495
545
|
};
|
|
496
546
|
}
|
|
497
547
|
|
|
498
|
-
// src/sanitize-url.ts
|
|
499
|
-
function sanitizeUrl(url) {
|
|
500
|
-
if (!url) return void 0;
|
|
501
|
-
try {
|
|
502
|
-
const parsed = new URL(url);
|
|
503
|
-
return `${parsed.origin}${parsed.pathname}`;
|
|
504
|
-
} catch {
|
|
505
|
-
return void 0;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
548
|
// src/version.ts
|
|
510
|
-
var PKG_VERSION = true ? "1.
|
|
549
|
+
var PKG_VERSION = true ? "1.10.0" : "0.0.0-dev";
|
|
511
550
|
|
|
512
551
|
// src/create-hls-plugin.ts
|
|
513
552
|
var DEFAULT_CONFIG = {
|
|
@@ -567,6 +606,11 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
567
606
|
let onlineListener = null;
|
|
568
607
|
let reconnectTriggerError = null;
|
|
569
608
|
let reconnectExhausted = false;
|
|
609
|
+
let isReconnecting = false;
|
|
610
|
+
let isLiveClassified = false;
|
|
611
|
+
let stallWatchdogTimer = null;
|
|
612
|
+
let lastStallCheckTime = 0;
|
|
613
|
+
let lastStallCheckPosition = 0;
|
|
570
614
|
const applyPoster = () => {
|
|
571
615
|
if (!video) return;
|
|
572
616
|
video.poster = api?.getState("poster") || "";
|
|
@@ -598,6 +642,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
598
642
|
clearTimeout(retryTimeout);
|
|
599
643
|
retryTimeout = null;
|
|
600
644
|
}
|
|
645
|
+
if (stallWatchdogTimer) {
|
|
646
|
+
clearTimeout(stallWatchdogTimer);
|
|
647
|
+
stallWatchdogTimer = null;
|
|
648
|
+
}
|
|
601
649
|
if (hls) {
|
|
602
650
|
hls.destroy();
|
|
603
651
|
hls = null;
|
|
@@ -658,22 +706,22 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
658
706
|
];
|
|
659
707
|
const mapFatalErrorCode = (error) => {
|
|
660
708
|
if (error.response?.text === PLAYLIST_INVALID_TEXT) {
|
|
661
|
-
return
|
|
709
|
+
return import_core2.ErrorCode.PLAYLIST_INVALID;
|
|
662
710
|
}
|
|
663
711
|
if (error.details === "bufferFullError") {
|
|
664
|
-
return
|
|
712
|
+
return import_core2.ErrorCode.MEDIA_BUFFER_FULL;
|
|
665
713
|
}
|
|
666
714
|
if (APPEND_ERROR_DETAILS.includes(error.details)) {
|
|
667
|
-
return
|
|
715
|
+
return import_core2.ErrorCode.MEDIA_APPEND_ERROR;
|
|
668
716
|
}
|
|
669
717
|
switch (error.type) {
|
|
670
718
|
case "network":
|
|
671
|
-
return
|
|
719
|
+
return import_core2.ErrorCode.MEDIA_NETWORK_ERROR;
|
|
672
720
|
case "media":
|
|
673
721
|
case "mux":
|
|
674
|
-
return
|
|
722
|
+
return import_core2.ErrorCode.MEDIA_DECODE_ERROR;
|
|
675
723
|
default:
|
|
676
|
-
return
|
|
724
|
+
return import_core2.ErrorCode.PLAYBACK_FAILED;
|
|
677
725
|
}
|
|
678
726
|
};
|
|
679
727
|
const buildErrorDetail = (error, retriesExhausted) => {
|
|
@@ -686,7 +734,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
686
734
|
if (typeof error.response?.code === "number" && error.response.code > 0) {
|
|
687
735
|
detail.httpStatus = error.response.code;
|
|
688
736
|
}
|
|
689
|
-
const url = sanitizeUrl(error.url);
|
|
737
|
+
const url = (0, import_core.sanitizeUrl)(error.url);
|
|
690
738
|
if (url) {
|
|
691
739
|
detail.url = url;
|
|
692
740
|
}
|
|
@@ -709,20 +757,20 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
709
757
|
const handleHlsError = (error) => {
|
|
710
758
|
const Hls = loader.getHlsConstructor();
|
|
711
759
|
if (!Hls || !hls) return false;
|
|
712
|
-
const now = Date.now();
|
|
713
|
-
if (now - errorWindowStart > ERROR_WINDOW_MS) {
|
|
714
|
-
errorCount = 1;
|
|
715
|
-
errorWindowStart = now;
|
|
716
|
-
} else {
|
|
717
|
-
errorCount++;
|
|
718
|
-
}
|
|
719
|
-
if (errorCount >= MAX_ERRORS_IN_WINDOW) {
|
|
720
|
-
api?.logger.error(`Too many errors (${errorCount} in ${ERROR_WINDOW_MS}ms), giving up`);
|
|
721
|
-
emitFatalError(error, true);
|
|
722
|
-
teardownPipeline(new Error(error.details));
|
|
723
|
-
return true;
|
|
724
|
-
}
|
|
725
760
|
if (error.fatal) {
|
|
761
|
+
const now = Date.now();
|
|
762
|
+
if (now - errorWindowStart > ERROR_WINDOW_MS) {
|
|
763
|
+
errorCount = 1;
|
|
764
|
+
errorWindowStart = now;
|
|
765
|
+
} else {
|
|
766
|
+
errorCount++;
|
|
767
|
+
}
|
|
768
|
+
if (errorCount >= MAX_ERRORS_IN_WINDOW) {
|
|
769
|
+
api?.logger.error(`Too many fatal errors (${errorCount} in ${ERROR_WINDOW_MS}ms), giving up`);
|
|
770
|
+
emitFatalError(error, true);
|
|
771
|
+
teardownPipeline(new Error(error.details));
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
726
774
|
api?.logger.error("Fatal HLS error", { type: error.type, details: error.details });
|
|
727
775
|
switch (error.type) {
|
|
728
776
|
case "network": {
|
|
@@ -1001,9 +1049,15 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1001
1049
|
if (resolved || session !== loadSession) return;
|
|
1002
1050
|
resolved = true;
|
|
1003
1051
|
releaseAbort();
|
|
1004
|
-
api?.logger.error(`HLS load timed out after ${timeout_ms}ms`, { src });
|
|
1052
|
+
api?.logger.error(`HLS load timed out after ${timeout_ms}ms`, { src: (0, import_core.sanitizeUrl)(src) });
|
|
1053
|
+
const timeoutError = {
|
|
1054
|
+
type: "network",
|
|
1055
|
+
details: "Video took too long to load (network timeout)",
|
|
1056
|
+
fatal: true
|
|
1057
|
+
};
|
|
1058
|
+
emitFatalError(timeoutError, true);
|
|
1005
1059
|
teardownPipeline();
|
|
1006
|
-
reject(new Error(
|
|
1060
|
+
reject(new Error(timeoutError.details));
|
|
1007
1061
|
}, timeout_ms);
|
|
1008
1062
|
}
|
|
1009
1063
|
hls.attachMedia(videoEl);
|
|
@@ -1020,6 +1074,51 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1020
1074
|
reconnectResumePosition = 0;
|
|
1021
1075
|
reconnectTriggerError = null;
|
|
1022
1076
|
reconnectExhausted = false;
|
|
1077
|
+
isReconnecting = false;
|
|
1078
|
+
};
|
|
1079
|
+
const startStallWatchdog = () => {
|
|
1080
|
+
if (!video || stallWatchdogTimer) return;
|
|
1081
|
+
lastStallCheckTime = Date.now();
|
|
1082
|
+
lastStallCheckPosition = video.currentTime;
|
|
1083
|
+
const levelTargetDuration = hls?.targetDuration ?? hls?.levels?.[hls?.currentLevel]?.details?.targetduration ?? 0;
|
|
1084
|
+
const targetDuration = Math.max(15, 4 * (levelTargetDuration || 0) || 15);
|
|
1085
|
+
const checkInterval = Math.min(targetDuration, 3e4);
|
|
1086
|
+
const check = () => {
|
|
1087
|
+
if (!video || !api) return;
|
|
1088
|
+
const isPlaying = api.getState("playing");
|
|
1089
|
+
const isSeeking = api.getState("seeking");
|
|
1090
|
+
if (!isPlaying || isSeeking) {
|
|
1091
|
+
stallWatchdogTimer = setTimeout(check, checkInterval);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
const elapsed = Date.now() - lastStallCheckTime;
|
|
1095
|
+
const positionDelta = video.currentTime - lastStallCheckPosition;
|
|
1096
|
+
if (positionDelta > 0.5) {
|
|
1097
|
+
lastStallCheckTime = Date.now();
|
|
1098
|
+
lastStallCheckPosition = video.currentTime;
|
|
1099
|
+
} else if (elapsed > targetDuration * 1e3) {
|
|
1100
|
+
api.logger.warn("Playback stall detected \u2014 no timeupdate progress", {
|
|
1101
|
+
elapsed: Math.round(elapsed),
|
|
1102
|
+
currentTime: video.currentTime,
|
|
1103
|
+
targetDuration: Math.round(targetDuration)
|
|
1104
|
+
});
|
|
1105
|
+
const stallError = {
|
|
1106
|
+
type: "network",
|
|
1107
|
+
details: "Playback stalled \u2014 no data received",
|
|
1108
|
+
fatal: true
|
|
1109
|
+
};
|
|
1110
|
+
maybeScheduleReconnect(stallError);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
stallWatchdogTimer = setTimeout(check, checkInterval);
|
|
1114
|
+
};
|
|
1115
|
+
stallWatchdogTimer = setTimeout(check, checkInterval);
|
|
1116
|
+
};
|
|
1117
|
+
const stopStallWatchdog = () => {
|
|
1118
|
+
if (stallWatchdogTimer) {
|
|
1119
|
+
clearTimeout(stallWatchdogTimer);
|
|
1120
|
+
stallWatchdogTimer = null;
|
|
1121
|
+
}
|
|
1023
1122
|
};
|
|
1024
1123
|
const emitReconnectExhausted = (elapsedMs, windowMs) => {
|
|
1025
1124
|
if (reconnectExhausted) return;
|
|
@@ -1030,7 +1129,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1030
1129
|
api?.setState("playbackState", "error");
|
|
1031
1130
|
api?.setState("buffering", false);
|
|
1032
1131
|
api?.emit("error", {
|
|
1033
|
-
code: trigger ? mapFatalErrorCode(trigger) :
|
|
1132
|
+
code: trigger ? mapFatalErrorCode(trigger) : import_core2.ErrorCode.PLAYBACK_FAILED,
|
|
1034
1133
|
message: `HLS auto-reconnect gave up after ${attempts} attempts over ${Math.round(elapsedMs / 1e3)}s`,
|
|
1035
1134
|
fatal: true,
|
|
1036
1135
|
timestamp: Date.now(),
|
|
@@ -1045,17 +1144,30 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1045
1144
|
const scheduleReconnectAttempt = () => {
|
|
1046
1145
|
if (reconnectExhausted) return;
|
|
1047
1146
|
if (reconnectTimer) return;
|
|
1048
|
-
const
|
|
1147
|
+
const isLive = (api?.getState("live") ?? false) && isLiveClassified;
|
|
1148
|
+
const configWindowMs = mergedConfig.reconnectWindowMs ?? 3e5;
|
|
1149
|
+
const window_ms = isLive ? Infinity : configWindowMs;
|
|
1049
1150
|
const elapsed_ms = Date.now() - reconnectWindowStart;
|
|
1050
1151
|
if (elapsed_ms > window_ms) {
|
|
1051
1152
|
api?.logger.warn(`Auto-reconnect window exhausted after ${reconnectAttempts} attempts`);
|
|
1052
1153
|
emitReconnectExhausted(elapsed_ms, window_ms);
|
|
1053
1154
|
return;
|
|
1054
1155
|
}
|
|
1156
|
+
const LONG_OUTAGE_MS = 6e5;
|
|
1157
|
+
if (isLive && elapsed_ms > LONG_OUTAGE_MS) {
|
|
1158
|
+
api?.emit("error:reconnecting", {
|
|
1159
|
+
attempt: reconnectAttempts + 1,
|
|
1160
|
+
delayMs: 0,
|
|
1161
|
+
elapsedMs: elapsed_ms,
|
|
1162
|
+
windowMs: window_ms,
|
|
1163
|
+
longOutage: true
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1055
1166
|
const base_delay = mergedConfig.reconnectBaseDelayMs ?? 2e3;
|
|
1056
1167
|
const max_delay = mergedConfig.reconnectMaxDelayMs ?? 3e4;
|
|
1057
1168
|
const backoff = Math.min(base_delay * Math.pow(2, reconnectAttempts), max_delay);
|
|
1058
|
-
const
|
|
1169
|
+
const isAtCap = backoff >= max_delay;
|
|
1170
|
+
const delay = isLive && isAtCap ? 3e4 : Math.round(backoff * (0.7 + Math.random() * 0.3));
|
|
1059
1171
|
api?.logger.info(`Scheduling auto-reconnect attempt ${reconnectAttempts + 1} in ${delay}ms`);
|
|
1060
1172
|
api?.emit("error:reconnecting", {
|
|
1061
1173
|
attempt: reconnectAttempts + 1,
|
|
@@ -1070,7 +1182,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1070
1182
|
};
|
|
1071
1183
|
const maybeScheduleReconnect = (error) => {
|
|
1072
1184
|
if (mergedConfig.autoReconnect === false) return;
|
|
1073
|
-
if (!
|
|
1185
|
+
if (!currentSrc) return;
|
|
1186
|
+
const isLive = (api?.getState("live") ?? false) && isLiveClassified;
|
|
1187
|
+
if (!isLive && !hasPlayedContent) return;
|
|
1074
1188
|
if (error.type !== "network" && error.type !== "media") return;
|
|
1075
1189
|
if (reconnectWindowStart === 0) {
|
|
1076
1190
|
reconnectWindowStart = Date.now();
|
|
@@ -1081,13 +1195,14 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1081
1195
|
};
|
|
1082
1196
|
const attemptReconnect = async () => {
|
|
1083
1197
|
if (!api || !currentSrc) return;
|
|
1198
|
+
isReconnecting = true;
|
|
1084
1199
|
const session = ++loadSession;
|
|
1085
1200
|
reconnectAttempts++;
|
|
1086
1201
|
const saved_src = currentSrc;
|
|
1087
1202
|
const was_live = api.getState("live");
|
|
1088
1203
|
const was_native = isNative;
|
|
1089
1204
|
const resume_position = reconnectResumePosition;
|
|
1090
|
-
api.logger.info(`Auto-reconnect attempt ${reconnectAttempts}`, { src: saved_src });
|
|
1205
|
+
api.logger.info(`Auto-reconnect attempt ${reconnectAttempts}`, { src: (0, import_core.sanitizeUrl)(saved_src) });
|
|
1091
1206
|
try {
|
|
1092
1207
|
teardownPipeline(new Error("HLS load cancelled: reconnecting"));
|
|
1093
1208
|
networkRetryCount = 0;
|
|
@@ -1113,14 +1228,21 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1113
1228
|
});
|
|
1114
1229
|
api.logger.info("Auto-reconnect succeeded");
|
|
1115
1230
|
cancelReconnect();
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1231
|
+
const wasPaused = api.getState("paused");
|
|
1232
|
+
if (!wasPaused) {
|
|
1233
|
+
try {
|
|
1234
|
+
await video?.play();
|
|
1235
|
+
} catch {
|
|
1236
|
+
}
|
|
1237
|
+
} else {
|
|
1238
|
+
api.logger.info("Stream reconnected while paused; maintaining pause at live edge");
|
|
1119
1239
|
}
|
|
1120
1240
|
} catch {
|
|
1121
1241
|
if (session !== loadSession) return;
|
|
1122
1242
|
api?.logger.warn(`Auto-reconnect attempt ${reconnectAttempts} failed`);
|
|
1123
1243
|
scheduleReconnectAttempt();
|
|
1244
|
+
} finally {
|
|
1245
|
+
isReconnecting = false;
|
|
1124
1246
|
}
|
|
1125
1247
|
};
|
|
1126
1248
|
const plugin = {
|
|
@@ -1154,6 +1276,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1154
1276
|
});
|
|
1155
1277
|
const unsubSeek = api.on("playback:seeking", ({ time }) => {
|
|
1156
1278
|
if (!video) return;
|
|
1279
|
+
if (!Number.isFinite(time)) return;
|
|
1157
1280
|
const clampedTime = Math.max(0, Math.min(time, video.duration || 0));
|
|
1158
1281
|
video.currentTime = clampedTime;
|
|
1159
1282
|
});
|
|
@@ -1206,20 +1329,43 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1206
1329
|
}
|
|
1207
1330
|
}
|
|
1208
1331
|
});
|
|
1332
|
+
const unsubAudioTrack = api.on("track:audio", ({ trackId }) => {
|
|
1333
|
+
if (!hls || isNative) {
|
|
1334
|
+
api?.logger.warn("Audio track selection not available");
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const index = audioTrackIndex(trackId);
|
|
1338
|
+
const tracks = hls.audioTracks ?? [];
|
|
1339
|
+
if (index < 0 || index >= tracks.length) {
|
|
1340
|
+
api?.logger.warn("Ignoring unknown audio track selection", { trackId });
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
hls.audioTrack = index;
|
|
1344
|
+
api?.logger.debug(`Audio: queued switch to track ${index}`);
|
|
1345
|
+
});
|
|
1209
1346
|
if (typeof window !== "undefined") {
|
|
1210
1347
|
onlineListener = () => {
|
|
1348
|
+
const hasActiveReconnect = reconnectTimer !== null || reconnectWindowStart > 0 || isReconnecting || reconnectAttempts > 0 && !reconnectExhausted;
|
|
1349
|
+
if (!hasActiveReconnect) {
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1211
1352
|
if (reconnectTimer) {
|
|
1212
1353
|
api?.logger.info("Browser back online, reconnecting immediately");
|
|
1213
1354
|
clearTimeout(reconnectTimer);
|
|
1214
1355
|
reconnectTimer = null;
|
|
1215
|
-
void attemptReconnect();
|
|
1216
1356
|
}
|
|
1357
|
+
void attemptReconnect();
|
|
1217
1358
|
};
|
|
1218
1359
|
window.addEventListener("online", onlineListener);
|
|
1219
1360
|
}
|
|
1220
1361
|
const unsubPoster = api.subscribeToState((event) => {
|
|
1221
1362
|
if (event.key === "poster") applyPoster();
|
|
1222
1363
|
});
|
|
1364
|
+
const unsubLive = api.subscribeToState((event) => {
|
|
1365
|
+
if (event.key === "live") {
|
|
1366
|
+
isLiveClassified = true;
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1223
1369
|
api.onDestroy(() => {
|
|
1224
1370
|
unsubPlay();
|
|
1225
1371
|
unsubPause();
|
|
@@ -1228,8 +1374,20 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1228
1374
|
unsubMute();
|
|
1229
1375
|
unsubRate();
|
|
1230
1376
|
unsubQuality();
|
|
1377
|
+
unsubAudioTrack();
|
|
1231
1378
|
unsubPoster();
|
|
1379
|
+
unsubLive();
|
|
1232
1380
|
});
|
|
1381
|
+
const unsubPlayState = api.subscribeToState((event) => {
|
|
1382
|
+
if (event.key === "playing") {
|
|
1383
|
+
if (event.value) {
|
|
1384
|
+
startStallWatchdog();
|
|
1385
|
+
} else {
|
|
1386
|
+
stopStallWatchdog();
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
api.onDestroy(unsubPlayState);
|
|
1233
1391
|
},
|
|
1234
1392
|
async destroy() {
|
|
1235
1393
|
api?.logger.info(`HLS plugin${variant.logSuffix} destroying`);
|
|
@@ -1248,10 +1406,12 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1248
1406
|
},
|
|
1249
1407
|
async loadSource(src) {
|
|
1250
1408
|
if (!api) throw new Error("Plugin not initialized");
|
|
1251
|
-
api.logger.info(`Loading HLS source${variant.logSuffix}`, { src });
|
|
1409
|
+
api.logger.info(`Loading HLS source${variant.logSuffix}`, { src: (0, import_core.sanitizeUrl)(src) });
|
|
1252
1410
|
const session = ++loadSession;
|
|
1253
1411
|
cancelReconnect();
|
|
1254
1412
|
hasPlayedContent = false;
|
|
1413
|
+
isLiveClassified = false;
|
|
1414
|
+
api.setState("live", false);
|
|
1255
1415
|
cleanup(new Error("HLS load cancelled: superseded by a new load"));
|
|
1256
1416
|
currentSrc = src;
|
|
1257
1417
|
applyPoster();
|
|
@@ -1301,14 +1461,24 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1301
1461
|
return isNative;
|
|
1302
1462
|
},
|
|
1303
1463
|
getLiveInfo() {
|
|
1304
|
-
if (isNative || !hls) return null;
|
|
1305
1464
|
const live = api?.getState("live") || false;
|
|
1306
1465
|
if (!live) return null;
|
|
1466
|
+
if (isNative) {
|
|
1467
|
+
return {
|
|
1468
|
+
isLive: true,
|
|
1469
|
+
latency: 0,
|
|
1470
|
+
targetLatency: 3,
|
|
1471
|
+
drift: 0,
|
|
1472
|
+
liveSyncPosition: video?.seekable?.length ? Math.max(0, video.seekable.end(video.seekable.length - 1) - 3) : void 0
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
if (!hls) return null;
|
|
1307
1476
|
return {
|
|
1308
1477
|
isLive: true,
|
|
1309
1478
|
latency: hls.latency || 0,
|
|
1310
1479
|
targetLatency: hls.targetLatency || 3,
|
|
1311
|
-
drift: hls.drift || 0
|
|
1480
|
+
drift: hls.drift || 0,
|
|
1481
|
+
liveSyncPosition: hls.liveSyncPosition ?? (video?.seekable?.length ? Math.max(0, video.seekable.end(video.seekable.length - 1) - 3) : void 0)
|
|
1312
1482
|
};
|
|
1313
1483
|
},
|
|
1314
1484
|
/**
|
|
@@ -1334,7 +1504,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1334
1504
|
const currentTime = video?.currentTime || 0;
|
|
1335
1505
|
const savedSrc = currentSrc;
|
|
1336
1506
|
const session = ++loadSession;
|
|
1507
|
+
cancelReconnect();
|
|
1337
1508
|
cleanup(new Error("HLS load cancelled: switching to native HLS"));
|
|
1509
|
+
currentSrc = savedSrc;
|
|
1338
1510
|
await loadNative(savedSrc);
|
|
1339
1511
|
if (session !== loadSession) return;
|
|
1340
1512
|
if (video && currentTime > 0) {
|
|
@@ -1371,7 +1543,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1371
1543
|
const currentTime = video?.currentTime || 0;
|
|
1372
1544
|
const savedSrc = currentSrc;
|
|
1373
1545
|
const session = ++loadSession;
|
|
1546
|
+
cancelReconnect();
|
|
1374
1547
|
cleanup(new Error("HLS load cancelled: switching to hls.js"));
|
|
1548
|
+
currentSrc = savedSrc;
|
|
1375
1549
|
await loadWithHlsJs(savedSrc);
|
|
1376
1550
|
if (session !== loadSession) return;
|
|
1377
1551
|
if (video && currentTime > 0) {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { H as HLSPluginConfig, I as IHLSPlugin } from './
|
|
2
|
-
export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel
|
|
3
|
-
|
|
1
|
+
import { H as HLSPluginConfig, I as IHLSPlugin } from './types-DnvcTuSn.cjs';
|
|
2
|
+
export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel } from './types-DnvcTuSn.cjs';
|
|
3
|
+
export { sanitizeUrl } from '@scarlett-player/core';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* HLS Provider Plugin for Scarlett Player
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { H as HLSPluginConfig, I as IHLSPlugin } from './
|
|
2
|
-
export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel
|
|
3
|
-
|
|
1
|
+
import { H as HLSPluginConfig, I as IHLSPlugin } from './types-DnvcTuSn.js';
|
|
2
|
+
export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel } from './types-DnvcTuSn.js';
|
|
3
|
+
export { sanitizeUrl } from '@scarlett-player/core';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* HLS Provider Plugin for Scarlett Player
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
__export,
|
|
3
3
|
createHLSPluginWith,
|
|
4
4
|
sanitizeUrl
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-UB2TS72E.js";
|
|
6
6
|
|
|
7
7
|
// src/hls-loader.ts
|
|
8
8
|
var hls_loader_exports = {};
|
|
@@ -13,7 +13,6 @@ __export(hls_loader_exports, {
|
|
|
13
13
|
isHlsJsSupported: () => isHlsJsSupported,
|
|
14
14
|
loadHlsJs: () => loadHlsJs,
|
|
15
15
|
resetLoader: () => resetLoader,
|
|
16
|
-
shouldPreferNativeHLS: () => shouldPreferNativeHLS,
|
|
17
16
|
supportsNativeHLS: () => supportsNativeHLS
|
|
18
17
|
});
|
|
19
18
|
var hlsConstructor = null;
|
|
@@ -23,13 +22,6 @@ function supportsNativeHLS() {
|
|
|
23
22
|
const video = document.createElement("video");
|
|
24
23
|
return video.canPlayType("application/vnd.apple.mpegurl") !== "";
|
|
25
24
|
}
|
|
26
|
-
function shouldPreferNativeHLS() {
|
|
27
|
-
if (!supportsNativeHLS()) return false;
|
|
28
|
-
if (typeof navigator === "undefined") return false;
|
|
29
|
-
const ua = navigator.userAgent;
|
|
30
|
-
const isSafari = /Safari/.test(ua) && !/Chrome/.test(ua) && !/CriOS/.test(ua);
|
|
31
|
-
return isSafari;
|
|
32
|
-
}
|
|
33
25
|
function isHlsJsSupported() {
|
|
34
26
|
if (hlsConstructor) {
|
|
35
27
|
return hlsConstructor.isSupported();
|