@scarlett-player/hls 1.11.0 → 1.13.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/README.md +75 -1
- package/dist/{chunk-NVPSLRJY.js → chunk-3KFYKCR2.js} +361 -57
- package/dist/index.cjs +363 -57
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -1
- package/dist/light.cjs +363 -57
- package/dist/light.d.cts +2 -2
- package/dist/light.d.ts +2 -2
- package/dist/light.js +5 -1
- package/dist/live-metrics-CoYkWhl5.d.cts +388 -0
- package/dist/live-metrics-CoYkWhl5.d.ts +388 -0
- package/package.json +2 -2
- package/dist/types-DnvcTuSn.d.cts +0 -143
- package/dist/types-DnvcTuSn.d.ts +0 -143
package/dist/light.cjs
CHANGED
|
@@ -30,6 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/light.ts
|
|
31
31
|
var light_exports = {};
|
|
32
32
|
__export(light_exports, {
|
|
33
|
+
DEFAULT_TARGET_LATENCY: () => DEFAULT_TARGET_LATENCY,
|
|
34
|
+
computeLiveMetrics: () => computeLiveMetrics,
|
|
33
35
|
createHLSPlugin: () => createHLSPlugin,
|
|
34
36
|
default: () => light_default,
|
|
35
37
|
sanitizeUrl: () => import_core.sanitizeUrl
|
|
@@ -167,6 +169,107 @@ function getInitialBandwidthEstimate(overrideBps) {
|
|
|
167
169
|
// src/sanitize-url.ts
|
|
168
170
|
var import_core = require("@scarlett-player/core");
|
|
169
171
|
|
|
172
|
+
// src/live-metrics.ts
|
|
173
|
+
var DEFAULT_TARGET_LATENCY = 3;
|
|
174
|
+
var MIN_EDGE_TOLERANCE = 1.5;
|
|
175
|
+
var NATIVE_EDGE_TOLERANCE = 7;
|
|
176
|
+
var EPSILON = 0.05;
|
|
177
|
+
function finite(value) {
|
|
178
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
179
|
+
}
|
|
180
|
+
function rangeFromDetails(details) {
|
|
181
|
+
if (!details) return null;
|
|
182
|
+
const start = finite(details.fragmentStart) ?? finite(details.fragments?.[0]?.start) ?? 0;
|
|
183
|
+
const total = finite(details.totalduration);
|
|
184
|
+
const end = finite(details.edge) ?? (total === null ? null : start + total);
|
|
185
|
+
if (end === null) return null;
|
|
186
|
+
return { start, end };
|
|
187
|
+
}
|
|
188
|
+
function rangeFromMedia(media) {
|
|
189
|
+
const seekable = media?.seekable;
|
|
190
|
+
if (!seekable || seekable.length === 0) return null;
|
|
191
|
+
const start = seekable.start(0);
|
|
192
|
+
const end = seekable.end(seekable.length - 1);
|
|
193
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
|
194
|
+
return { start, end };
|
|
195
|
+
}
|
|
196
|
+
function targetFromDetails(details) {
|
|
197
|
+
if (!details) return null;
|
|
198
|
+
const targetduration = finite(details.targetduration);
|
|
199
|
+
return finite(details.partHoldBack) ?? finite(details.holdBack) ?? (targetduration !== null ? targetduration * 3 : null);
|
|
200
|
+
}
|
|
201
|
+
function edgeTolerance(details, targetLatency) {
|
|
202
|
+
const targetduration = finite(details?.targetduration);
|
|
203
|
+
const half = targetduration !== null ? targetduration / 2 : targetLatency / 2;
|
|
204
|
+
return Math.max(MIN_EDGE_TOLERANCE, finite(details?.partTarget) ?? half);
|
|
205
|
+
}
|
|
206
|
+
function computeLiveMetrics(source) {
|
|
207
|
+
if (source.kind === "hls") {
|
|
208
|
+
const { hls, details } = source;
|
|
209
|
+
const media2 = hls.media;
|
|
210
|
+
const seekableRange2 = rangeFromDetails(details) ?? rangeFromMedia(media2);
|
|
211
|
+
const targetLatency2 = finite(hls.targetLatency) ?? targetFromDetails(details) ?? DEFAULT_TARGET_LATENCY;
|
|
212
|
+
const latency2 = finite(hls.latency) ?? (seekableRange2 && media2 ? Math.max(0, seekableRange2.end - media2.currentTime) : null);
|
|
213
|
+
if (latency2 === null && seekableRange2 === null) return null;
|
|
214
|
+
const resolvedLatency = latency2 ?? 0;
|
|
215
|
+
return {
|
|
216
|
+
latency: resolvedLatency,
|
|
217
|
+
targetLatency: targetLatency2,
|
|
218
|
+
atEdge: resolvedLatency <= targetLatency2 + edgeTolerance(details, targetLatency2),
|
|
219
|
+
seekableRange: seekableRange2,
|
|
220
|
+
// Effective LL, not requested LL: the manifest has to carry parts or
|
|
221
|
+
// advertise blocking reloads, AND the host has to have asked for it.
|
|
222
|
+
lowLatency: source.lowLatencyRequested !== false && (!!details?.partList?.length || details?.canBlockReload === true)
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const { media } = source;
|
|
226
|
+
const seekableRange = rangeFromMedia(media);
|
|
227
|
+
if (!seekableRange) return null;
|
|
228
|
+
const targetLatency = source.targetLatency ?? DEFAULT_TARGET_LATENCY;
|
|
229
|
+
const tolerance = source.targetLatency === void 0 ? NATIVE_EDGE_TOLERANCE : Math.max(MIN_EDGE_TOLERANCE, source.targetLatency / 2);
|
|
230
|
+
const latency = Math.max(0, seekableRange.end - media.currentTime);
|
|
231
|
+
return {
|
|
232
|
+
latency,
|
|
233
|
+
targetLatency,
|
|
234
|
+
atEdge: latency <= targetLatency + tolerance,
|
|
235
|
+
seekableRange,
|
|
236
|
+
lowLatency: source.lowLatency === true
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function applyLiveMetrics(api, metrics) {
|
|
240
|
+
if (!metrics) return;
|
|
241
|
+
const previousLatency = api.getState("liveLatency");
|
|
242
|
+
if (Math.abs(previousLatency - metrics.latency) > EPSILON) {
|
|
243
|
+
api.setState("liveLatency", metrics.latency);
|
|
244
|
+
api.emit("live:latency", { latency: metrics.latency });
|
|
245
|
+
}
|
|
246
|
+
if (api.getState("liveEdge") !== metrics.atEdge) {
|
|
247
|
+
api.setState("liveEdge", metrics.atEdge);
|
|
248
|
+
api.emit("live:edgechange", { atEdge: metrics.atEdge });
|
|
249
|
+
}
|
|
250
|
+
const range = metrics.seekableRange;
|
|
251
|
+
if (range) {
|
|
252
|
+
const previous = api.getState("seekableRange");
|
|
253
|
+
if (!previous || Math.abs(previous.start - range.start) > EPSILON || Math.abs(previous.end - range.end) > EPSILON) {
|
|
254
|
+
api.setState("seekableRange", { start: range.start, end: range.end });
|
|
255
|
+
api.emit("live:seekablerange", { start: range.start, end: range.end });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (api.getState("lowLatencyMode") !== metrics.lowLatency) {
|
|
259
|
+
api.setState("lowLatencyMode", metrics.lowLatency);
|
|
260
|
+
api.emit("live:lowlatency", { enabled: metrics.lowLatency });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function resetLiveMetrics(api) {
|
|
264
|
+
if (api.getState("lowLatencyMode")) {
|
|
265
|
+
api.setState("lowLatencyMode", false);
|
|
266
|
+
api.emit("live:lowlatency", { enabled: false });
|
|
267
|
+
}
|
|
268
|
+
api.setState("liveLatency", 0);
|
|
269
|
+
api.setState("liveEdge", false);
|
|
270
|
+
api.setState("seekableRange", null);
|
|
271
|
+
}
|
|
272
|
+
|
|
170
273
|
// src/event-map.ts
|
|
171
274
|
var HLS_ERROR_TYPES = {
|
|
172
275
|
NETWORK_ERROR: "networkError",
|
|
@@ -243,7 +346,7 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
243
346
|
api.emit("quality:levels", {
|
|
244
347
|
levels: levels.map((l) => ({ id: l.id, label: l.label }))
|
|
245
348
|
});
|
|
246
|
-
callbacks.onManifestParsed?.(data.levels);
|
|
349
|
+
callbacks.onManifestParsed?.(data.levels, data);
|
|
247
350
|
});
|
|
248
351
|
addHandler("hlsLevelSwitched", (_event, data) => {
|
|
249
352
|
const level = hls.levels[data.level];
|
|
@@ -304,16 +407,18 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
304
407
|
if (data.details?.live !== void 0) {
|
|
305
408
|
api.setState("live", data.details.live);
|
|
306
409
|
if (data.details.live) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
410
|
+
callbacks.onLevelDetails?.(data.details);
|
|
411
|
+
applyLiveMetrics(
|
|
412
|
+
api,
|
|
413
|
+
computeLiveMetrics({
|
|
414
|
+
kind: "hls",
|
|
415
|
+
hls,
|
|
416
|
+
details: data.details,
|
|
417
|
+
lowLatencyRequested: callbacks.isLowLatencyRequested?.() ?? true
|
|
418
|
+
})
|
|
419
|
+
);
|
|
420
|
+
} else {
|
|
421
|
+
resetLiveMetrics(api);
|
|
317
422
|
}
|
|
318
423
|
callbacks.onLiveUpdate?.();
|
|
319
424
|
}
|
|
@@ -353,7 +458,7 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
353
458
|
api.setState("currentAudioTrack", null);
|
|
354
459
|
};
|
|
355
460
|
}
|
|
356
|
-
function setupVideoEventHandlers(video, api) {
|
|
461
|
+
function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
357
462
|
const handlers = [];
|
|
358
463
|
const addHandler = (event, handler) => {
|
|
359
464
|
video.addEventListener(event, handler);
|
|
@@ -390,15 +495,11 @@ function setupVideoEventHandlers(video, api) {
|
|
|
390
495
|
addHandler("timeupdate", () => {
|
|
391
496
|
api.setState("currentTime", video.currentTime);
|
|
392
497
|
api.emit("playback:timeupdate", { currentTime: video.currentTime });
|
|
393
|
-
if (
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const latency = Math.max(0, end - video.currentTime);
|
|
399
|
-
api.setState("liveEdge", latency < 10);
|
|
400
|
-
api.setState("liveLatency", latency);
|
|
401
|
-
}
|
|
498
|
+
if (api.getState("live") || !Number.isFinite(video.duration)) {
|
|
499
|
+
applyLiveMetrics(
|
|
500
|
+
api,
|
|
501
|
+
getLiveMetrics ? getLiveMetrics() : computeLiveMetrics({ kind: "media", media: video })
|
|
502
|
+
);
|
|
402
503
|
}
|
|
403
504
|
});
|
|
404
505
|
addHandler("durationchange", () => {
|
|
@@ -454,12 +555,6 @@ function setupVideoEventHandlers(video, api) {
|
|
|
454
555
|
});
|
|
455
556
|
addHandler("loadedmetadata", () => {
|
|
456
557
|
api.setState("duration", video.duration);
|
|
457
|
-
api.setState("mediaType", video.videoWidth > 0 ? "video" : "audio");
|
|
458
|
-
});
|
|
459
|
-
addHandler("loadeddata", () => {
|
|
460
|
-
if (video.videoWidth > 0) {
|
|
461
|
-
api.setState("mediaType", "video");
|
|
462
|
-
}
|
|
463
558
|
});
|
|
464
559
|
addHandler("error", () => {
|
|
465
560
|
const error = video.error;
|
|
@@ -501,6 +596,119 @@ function setupVideoEventHandlers(video, api) {
|
|
|
501
596
|
};
|
|
502
597
|
}
|
|
503
598
|
|
|
599
|
+
// src/media-type.ts
|
|
600
|
+
var ELEMENT_EVENTS = ["loadedmetadata", "loadeddata", "resize", "playing"];
|
|
601
|
+
var TRACK_LIST_EVENTS = ["addtrack", "removetrack", "change"];
|
|
602
|
+
var HAVE_METADATA = 1;
|
|
603
|
+
function isTrackList(list) {
|
|
604
|
+
return typeof list === "object" && list !== null && typeof list.length === "number";
|
|
605
|
+
}
|
|
606
|
+
function createMediaTypeClassifier(api) {
|
|
607
|
+
let source = null;
|
|
608
|
+
let videoConfirmed = false;
|
|
609
|
+
let audioConfirmed = false;
|
|
610
|
+
let elementReported = false;
|
|
611
|
+
let element = null;
|
|
612
|
+
let elementDisposers = [];
|
|
613
|
+
let published = null;
|
|
614
|
+
let destroyed = false;
|
|
615
|
+
const decide = () => {
|
|
616
|
+
if (videoConfirmed) return "video";
|
|
617
|
+
if (audioConfirmed) return "audio";
|
|
618
|
+
return "unknown";
|
|
619
|
+
};
|
|
620
|
+
const publish = () => {
|
|
621
|
+
const next = decide();
|
|
622
|
+
if (next === published) return;
|
|
623
|
+
published = next;
|
|
624
|
+
api.setState("mediaType", next);
|
|
625
|
+
};
|
|
626
|
+
const readElement = () => {
|
|
627
|
+
const video = element;
|
|
628
|
+
if (!video) return;
|
|
629
|
+
if (elementReported && video.videoWidth > 0) {
|
|
630
|
+
videoConfirmed = true;
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (video.readyState < HAVE_METADATA) return;
|
|
634
|
+
const videoTracks = video.videoTracks;
|
|
635
|
+
const audioTracks = video.audioTracks;
|
|
636
|
+
if (!isTrackList(videoTracks)) return;
|
|
637
|
+
if (videoTracks.length > 0) {
|
|
638
|
+
videoConfirmed = true;
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (isTrackList(audioTracks) && audioTracks.length > 0) {
|
|
642
|
+
audioConfirmed = true;
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
const evaluate = () => {
|
|
646
|
+
if (destroyed) return;
|
|
647
|
+
readElement();
|
|
648
|
+
publish();
|
|
649
|
+
};
|
|
650
|
+
const detachElement = () => {
|
|
651
|
+
for (const off of elementDisposers) off();
|
|
652
|
+
elementDisposers = [];
|
|
653
|
+
element = null;
|
|
654
|
+
};
|
|
655
|
+
return {
|
|
656
|
+
beginSource(src) {
|
|
657
|
+
if (destroyed) return;
|
|
658
|
+
if (source === src) return;
|
|
659
|
+
source = src;
|
|
660
|
+
videoConfirmed = false;
|
|
661
|
+
audioConfirmed = false;
|
|
662
|
+
elementReported = false;
|
|
663
|
+
published = null;
|
|
664
|
+
publish();
|
|
665
|
+
},
|
|
666
|
+
attach(video) {
|
|
667
|
+
if (destroyed) return;
|
|
668
|
+
detachElement();
|
|
669
|
+
element = video;
|
|
670
|
+
elementReported = false;
|
|
671
|
+
for (const event of ELEMENT_EVENTS) {
|
|
672
|
+
const handler = () => {
|
|
673
|
+
elementReported = true;
|
|
674
|
+
evaluate();
|
|
675
|
+
};
|
|
676
|
+
video.addEventListener(event, handler);
|
|
677
|
+
elementDisposers.push(() => video.removeEventListener(event, handler));
|
|
678
|
+
}
|
|
679
|
+
for (const list of [element.videoTracks, element.audioTracks]) {
|
|
680
|
+
if (!isTrackList(list) || typeof list.addEventListener !== "function") continue;
|
|
681
|
+
for (const event of TRACK_LIST_EVENTS) {
|
|
682
|
+
const handler = () => evaluate();
|
|
683
|
+
list.addEventListener(event, handler);
|
|
684
|
+
elementDisposers.push(() => list.removeEventListener?.(event, handler));
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
evaluate();
|
|
688
|
+
},
|
|
689
|
+
noteManifestParsed(data) {
|
|
690
|
+
if (destroyed || !data) return;
|
|
691
|
+
const hasVideo = typeof data.video === "boolean" ? data.video : null;
|
|
692
|
+
const hasAudio = typeof data.audio === "boolean" ? data.audio : null;
|
|
693
|
+
if (hasVideo === true) {
|
|
694
|
+
videoConfirmed = true;
|
|
695
|
+
} else if (hasVideo === false && hasAudio === true) {
|
|
696
|
+
audioConfirmed = true;
|
|
697
|
+
}
|
|
698
|
+
publish();
|
|
699
|
+
},
|
|
700
|
+
evaluate,
|
|
701
|
+
current() {
|
|
702
|
+
return decide();
|
|
703
|
+
},
|
|
704
|
+
destroy() {
|
|
705
|
+
if (destroyed) return;
|
|
706
|
+
destroyed = true;
|
|
707
|
+
detachElement();
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
504
712
|
// src/playlist-validation.ts
|
|
505
713
|
var PLAYLIST_INVALID_TEXT = "Invalid playlist document";
|
|
506
714
|
var MEDIA_PLAYLIST_CONTEXTS = ["level", "audioTrack", "subtitleTrack"];
|
|
@@ -546,7 +754,7 @@ function createValidatingPlaylistLoader(Hls) {
|
|
|
546
754
|
}
|
|
547
755
|
|
|
548
756
|
// src/version.ts
|
|
549
|
-
var PKG_VERSION = true ? "1.
|
|
757
|
+
var PKG_VERSION = true ? "1.12.0" : "0.0.0-dev";
|
|
550
758
|
|
|
551
759
|
// src/create-hls-plugin.ts
|
|
552
760
|
var DEFAULT_CONFIG = {
|
|
@@ -574,6 +782,7 @@ var DEFAULT_CONFIG = {
|
|
|
574
782
|
// Never index a malformed live playlist refresh blindly
|
|
575
783
|
validatePlaylists: true
|
|
576
784
|
};
|
|
785
|
+
var LL_CATCH_UP_PLAYBACK_RATE = 1.1;
|
|
577
786
|
var MANIFEST_PHASE_ERRORS = [
|
|
578
787
|
"manifestLoadError",
|
|
579
788
|
"manifestLoadTimeOut",
|
|
@@ -589,6 +798,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
589
798
|
let cleanupHlsEvents = null;
|
|
590
799
|
let cleanupVideoEvents = null;
|
|
591
800
|
let isAutoQuality = true;
|
|
801
|
+
let mediaTypeClassifier = null;
|
|
802
|
+
let lastLevelDetails = null;
|
|
803
|
+
let lastLiveMetrics = null;
|
|
804
|
+
let lastKnownTargetLatency = null;
|
|
592
805
|
let loadSession = 0;
|
|
593
806
|
let abortPendingLoad = null;
|
|
594
807
|
let networkRetryCount = 0;
|
|
@@ -611,6 +824,24 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
611
824
|
let stallWatchdogTimer = null;
|
|
612
825
|
let lastStallCheckTime = 0;
|
|
613
826
|
let lastStallCheckPosition = 0;
|
|
827
|
+
const readLiveMetrics = () => {
|
|
828
|
+
const metrics = hls && !isNative ? computeLiveMetrics({
|
|
829
|
+
kind: "hls",
|
|
830
|
+
hls,
|
|
831
|
+
details: lastLevelDetails,
|
|
832
|
+
lowLatencyRequested: mergedConfig.lowLatencyMode === true
|
|
833
|
+
}) : video ? computeLiveMetrics({
|
|
834
|
+
kind: "media",
|
|
835
|
+
media: video,
|
|
836
|
+
targetLatency: lastKnownTargetLatency ?? void 0,
|
|
837
|
+
lowLatency: lastLiveMetrics?.lowLatency
|
|
838
|
+
}) : null;
|
|
839
|
+
if (metrics) {
|
|
840
|
+
lastLiveMetrics = metrics;
|
|
841
|
+
if (hls && !isNative) lastKnownTargetLatency = metrics.targetLatency;
|
|
842
|
+
}
|
|
843
|
+
return metrics;
|
|
844
|
+
};
|
|
614
845
|
const applyPoster = () => {
|
|
615
846
|
if (!video) return;
|
|
616
847
|
video.poster = api?.getState("poster") || "";
|
|
@@ -660,6 +891,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
660
891
|
mediaRetryCount = 0;
|
|
661
892
|
errorCount = 0;
|
|
662
893
|
errorWindowStart = 0;
|
|
894
|
+
lastLevelDetails = null;
|
|
895
|
+
lastLiveMetrics = null;
|
|
896
|
+
lastKnownTargetLatency = null;
|
|
897
|
+
if (api) resetLiveMetrics(api);
|
|
663
898
|
};
|
|
664
899
|
const buildHlsConfig = () => {
|
|
665
900
|
const config2 = buildBaseHlsConfig();
|
|
@@ -671,27 +906,58 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
671
906
|
}
|
|
672
907
|
return config2;
|
|
673
908
|
};
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
909
|
+
const buildLiveHlsConfig = () => {
|
|
910
|
+
const live = {};
|
|
911
|
+
const set = (key, value) => {
|
|
912
|
+
if (value !== void 0) live[key] = value;
|
|
913
|
+
};
|
|
914
|
+
const syncDuration = mergedConfig.liveSyncDuration;
|
|
915
|
+
const syncCount = mergedConfig.liveSyncDurationCount;
|
|
916
|
+
const maxDuration = mergedConfig.liveMaxLatencyDuration;
|
|
917
|
+
const maxCount = mergedConfig.liveMaxLatencyDurationCount;
|
|
918
|
+
const mixed = (syncDuration !== void 0 || maxDuration !== void 0) && (syncCount !== void 0 || maxCount !== void 0);
|
|
919
|
+
const dropCount = mixed && syncDuration !== void 0;
|
|
920
|
+
const dropDuration = mixed && !dropCount;
|
|
921
|
+
if (mixed) {
|
|
922
|
+
api?.logger.warn(
|
|
923
|
+
`Ignoring ${dropCount ? "liveSyncDurationCount/liveMaxLatencyDurationCount" : "liveSyncDuration/liveMaxLatencyDuration"}: hls.js rejects a config mixing seconds-based and count-based live latency options`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
set("liveSyncDuration", dropDuration ? void 0 : syncDuration);
|
|
927
|
+
set("liveSyncDurationCount", dropCount ? void 0 : syncCount);
|
|
928
|
+
set("liveMaxLatencyDuration", dropDuration ? void 0 : maxDuration);
|
|
929
|
+
set("liveMaxLatencyDurationCount", dropCount ? void 0 : maxCount);
|
|
930
|
+
set("liveDurationInfinity", mergedConfig.liveDurationInfinity);
|
|
931
|
+
set(
|
|
932
|
+
"maxLiveSyncPlaybackRate",
|
|
933
|
+
mergedConfig.maxLiveSyncPlaybackRate ?? (mergedConfig.lowLatencyMode === true ? LL_CATCH_UP_PLAYBACK_RATE : void 0)
|
|
934
|
+
);
|
|
935
|
+
return live;
|
|
936
|
+
};
|
|
937
|
+
const buildBaseHlsConfig = () => {
|
|
938
|
+
return {
|
|
939
|
+
debug: mergedConfig.debug,
|
|
940
|
+
autoStartLoad: mergedConfig.autoStartLoad,
|
|
941
|
+
startPosition: mergedConfig.startPosition,
|
|
942
|
+
startLevel: -1,
|
|
943
|
+
// Auto quality selection (ABR)
|
|
944
|
+
abrEwmaDefaultEstimate: getInitialBandwidthEstimate(mergedConfig.initialBandwidthEstimate),
|
|
945
|
+
lowLatencyMode: mergedConfig.lowLatencyMode,
|
|
946
|
+
maxBufferLength: mergedConfig.maxBufferLength,
|
|
947
|
+
maxMaxBufferLength: mergedConfig.maxMaxBufferLength,
|
|
948
|
+
backBufferLength: mergedConfig.backBufferLength,
|
|
949
|
+
enableWorker: mergedConfig.enableWorker,
|
|
950
|
+
capLevelToPlayerSize: mergedConfig.capLevelToPlayerSize,
|
|
951
|
+
// Minimize hls.js internal retries - we handle retries ourselves
|
|
952
|
+
fragLoadingMaxRetry: 1,
|
|
953
|
+
manifestLoadingMaxRetry: 1,
|
|
954
|
+
levelLoadingMaxRetry: 1,
|
|
955
|
+
fragLoadingRetryDelay: 500,
|
|
956
|
+
manifestLoadingRetryDelay: 500,
|
|
957
|
+
levelLoadingRetryDelay: 500,
|
|
958
|
+
...buildLiveHlsConfig()
|
|
959
|
+
};
|
|
960
|
+
};
|
|
695
961
|
const getRetryDelay = (retryCount) => {
|
|
696
962
|
const baseDelay = mergedConfig.retryDelayMs ?? 1e3;
|
|
697
963
|
const backoffFactor = mergedConfig.retryBackoffFactor ?? 2;
|
|
@@ -888,8 +1154,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
888
1154
|
const videoEl = getOrCreateVideo();
|
|
889
1155
|
isNative = true;
|
|
890
1156
|
if (api) {
|
|
891
|
-
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api);
|
|
1157
|
+
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics);
|
|
892
1158
|
}
|
|
1159
|
+
mediaTypeClassifier?.beginSource(src);
|
|
1160
|
+
mediaTypeClassifier?.attach(videoEl);
|
|
893
1161
|
return new Promise((resolve, reject) => {
|
|
894
1162
|
let watchdog = null;
|
|
895
1163
|
let settled = false;
|
|
@@ -981,8 +1249,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
981
1249
|
isNative = false;
|
|
982
1250
|
hls = loader.createHlsInstance(buildHlsConfig());
|
|
983
1251
|
if (api) {
|
|
984
|
-
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api);
|
|
1252
|
+
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics);
|
|
985
1253
|
}
|
|
1254
|
+
mediaTypeClassifier?.beginSource(src);
|
|
1255
|
+
mediaTypeClassifier?.attach(videoEl);
|
|
986
1256
|
return new Promise((resolve, reject) => {
|
|
987
1257
|
if (!hls || !api) {
|
|
988
1258
|
reject(new Error("HLS not initialized"));
|
|
@@ -1009,8 +1279,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1009
1279
|
}
|
|
1010
1280
|
};
|
|
1011
1281
|
cleanupHlsEvents = setupHlsEventHandlers(hls, api, {
|
|
1012
|
-
onManifestParsed: () => {
|
|
1282
|
+
onManifestParsed: (_levels, manifestData) => {
|
|
1013
1283
|
if (session !== loadSession) return;
|
|
1284
|
+
mediaTypeClassifier?.noteManifestParsed(manifestData);
|
|
1014
1285
|
if (!resolved) {
|
|
1015
1286
|
resolved = true;
|
|
1016
1287
|
releaseAbort();
|
|
@@ -1023,6 +1294,12 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1023
1294
|
},
|
|
1024
1295
|
onLevelSwitched: () => {
|
|
1025
1296
|
},
|
|
1297
|
+
onLevelDetails: (details) => {
|
|
1298
|
+
if (session !== loadSession) return;
|
|
1299
|
+
lastLevelDetails = details;
|
|
1300
|
+
readLiveMetrics();
|
|
1301
|
+
},
|
|
1302
|
+
isLowLatencyRequested: () => mergedConfig.lowLatencyMode === true,
|
|
1026
1303
|
onError: (error) => {
|
|
1027
1304
|
if (session !== loadSession) return;
|
|
1028
1305
|
const terminal = handleHlsError(error);
|
|
@@ -1263,6 +1540,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1263
1540
|
async init(pluginApi) {
|
|
1264
1541
|
api = pluginApi;
|
|
1265
1542
|
api.logger.info(`HLS plugin${variant.logSuffix} initialized`);
|
|
1543
|
+
mediaTypeClassifier = createMediaTypeClassifier(api);
|
|
1266
1544
|
const unsubPlay = api.on("playback:play", async () => {
|
|
1267
1545
|
if (!video) return;
|
|
1268
1546
|
try {
|
|
@@ -1398,6 +1676,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1398
1676
|
onlineListener = null;
|
|
1399
1677
|
}
|
|
1400
1678
|
cleanup(new Error("HLS load cancelled: player destroyed"));
|
|
1679
|
+
mediaTypeClassifier?.destroy();
|
|
1680
|
+
mediaTypeClassifier = null;
|
|
1401
1681
|
if (video?.parentNode) {
|
|
1402
1682
|
video.parentNode.removeChild(video);
|
|
1403
1683
|
}
|
|
@@ -1460,25 +1740,45 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1460
1740
|
isNativeHLS() {
|
|
1461
1741
|
return isNative;
|
|
1462
1742
|
},
|
|
1743
|
+
/**
|
|
1744
|
+
* Report the live state of the stream.
|
|
1745
|
+
*
|
|
1746
|
+
* `latency` and `targetLatency` come from hls.js on the MSE path, where
|
|
1747
|
+
* they are measured against `EXT-X-PROGRAM-DATE-TIME` drift when the
|
|
1748
|
+
* manifest carries it. On the native path there is no latency API, so both
|
|
1749
|
+
* are approximations: latency is the distance to `seekable.end`, and the
|
|
1750
|
+
* target is whatever an earlier hls.js session on this source measured
|
|
1751
|
+
* (an AirPlay handoff) before it falls back to 3 seconds. Parking a viewer
|
|
1752
|
+
* of a 2-second-target stream 3 seconds back was the previous behaviour,
|
|
1753
|
+
* and it is a full target latency of drift.
|
|
1754
|
+
*
|
|
1755
|
+
* @returns Live info, or null for VOD and before a pipeline exists
|
|
1756
|
+
*/
|
|
1463
1757
|
getLiveInfo() {
|
|
1464
1758
|
const live = api?.getState("live") || false;
|
|
1465
1759
|
if (!live) return null;
|
|
1760
|
+
const metrics = readLiveMetrics();
|
|
1466
1761
|
if (isNative) {
|
|
1762
|
+
const targetLatency2 = metrics?.targetLatency ?? DEFAULT_TARGET_LATENCY;
|
|
1763
|
+
const seekableEnd = video?.seekable?.length ? video.seekable.end(video.seekable.length - 1) : void 0;
|
|
1467
1764
|
return {
|
|
1468
1765
|
isLive: true,
|
|
1469
|
-
latency: 0,
|
|
1470
|
-
targetLatency:
|
|
1766
|
+
latency: metrics?.latency ?? 0,
|
|
1767
|
+
targetLatency: targetLatency2,
|
|
1471
1768
|
drift: 0,
|
|
1472
|
-
liveSyncPosition:
|
|
1769
|
+
liveSyncPosition: seekableEnd !== void 0 ? Math.max(0, seekableEnd - targetLatency2) : void 0,
|
|
1770
|
+
lowLatency: metrics?.lowLatency ?? false
|
|
1473
1771
|
};
|
|
1474
1772
|
}
|
|
1475
1773
|
if (!hls) return null;
|
|
1774
|
+
const targetLatency = hls.targetLatency || metrics?.targetLatency || DEFAULT_TARGET_LATENCY;
|
|
1476
1775
|
return {
|
|
1477
1776
|
isLive: true,
|
|
1478
1777
|
latency: hls.latency || 0,
|
|
1479
|
-
targetLatency
|
|
1778
|
+
targetLatency,
|
|
1480
1779
|
drift: hls.drift || 0,
|
|
1481
|
-
liveSyncPosition: hls.liveSyncPosition ?? (video?.seekable?.length ? Math.max(0, video.seekable.end(video.seekable.length - 1) -
|
|
1780
|
+
liveSyncPosition: hls.liveSyncPosition ?? (video?.seekable?.length ? Math.max(0, video.seekable.end(video.seekable.length - 1) - targetLatency) : void 0),
|
|
1781
|
+
lowLatency: metrics?.lowLatency ?? false
|
|
1482
1782
|
};
|
|
1483
1783
|
},
|
|
1484
1784
|
/**
|
|
@@ -1503,10 +1803,12 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1503
1803
|
const wasPlaying = api?.getState("playing") || false;
|
|
1504
1804
|
const currentTime = video?.currentTime || 0;
|
|
1505
1805
|
const savedSrc = currentSrc;
|
|
1806
|
+
const savedTargetLatency = lastKnownTargetLatency;
|
|
1506
1807
|
const session = ++loadSession;
|
|
1507
1808
|
cancelReconnect();
|
|
1508
1809
|
cleanup(new Error("HLS load cancelled: switching to native HLS"));
|
|
1509
1810
|
currentSrc = savedSrc;
|
|
1811
|
+
lastKnownTargetLatency = savedTargetLatency;
|
|
1510
1812
|
await loadNative(savedSrc);
|
|
1511
1813
|
if (session !== loadSession) return;
|
|
1512
1814
|
if (video && currentTime > 0) {
|
|
@@ -1542,10 +1844,12 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1542
1844
|
const wasPlaying = api?.getState("playing") || false;
|
|
1543
1845
|
const currentTime = video?.currentTime || 0;
|
|
1544
1846
|
const savedSrc = currentSrc;
|
|
1847
|
+
const savedTargetLatency = lastKnownTargetLatency;
|
|
1545
1848
|
const session = ++loadSession;
|
|
1546
1849
|
cancelReconnect();
|
|
1547
1850
|
cleanup(new Error("HLS load cancelled: switching to hls.js"));
|
|
1548
1851
|
currentSrc = savedSrc;
|
|
1852
|
+
lastKnownTargetLatency = savedTargetLatency;
|
|
1549
1853
|
await loadWithHlsJs(savedSrc);
|
|
1550
1854
|
if (session !== loadSession) return;
|
|
1551
1855
|
if (video && currentTime > 0) {
|
|
@@ -1580,6 +1884,8 @@ function createHLSPlugin(config) {
|
|
|
1580
1884
|
var light_default = createHLSPlugin;
|
|
1581
1885
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1582
1886
|
0 && (module.exports = {
|
|
1887
|
+
DEFAULT_TARGET_LATENCY,
|
|
1888
|
+
computeLiveMetrics,
|
|
1583
1889
|
createHLSPlugin,
|
|
1584
1890
|
sanitizeUrl
|
|
1585
1891
|
});
|
package/dist/light.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { H as HLSPluginConfig, I as IHLSPlugin } from './
|
|
2
|
-
export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel } from './
|
|
1
|
+
import { H as HLSPluginConfig, I as IHLSPlugin } from './live-metrics-CoYkWhl5.cjs';
|
|
2
|
+
export { D as DEFAULT_TARGET_LATENCY, b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel, f as HlsLevelDetails, L as LiveMetrics, e as LiveMetricsSource, d as computeLiveMetrics } from './live-metrics-CoYkWhl5.cjs';
|
|
3
3
|
export { sanitizeUrl } from '@scarlett-player/core';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/light.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { H as HLSPluginConfig, I as IHLSPlugin } from './
|
|
2
|
-
export { b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel } from './
|
|
1
|
+
import { H as HLSPluginConfig, I as IHLSPlugin } from './live-metrics-CoYkWhl5.js';
|
|
2
|
+
export { D as DEFAULT_TARGET_LATENCY, b as HLSError, c as HLSLiveInfo, a as HLSQualityLevel, f as HlsLevelDetails, L as LiveMetrics, e as LiveMetricsSource, d as computeLiveMetrics } from './live-metrics-CoYkWhl5.js';
|
|
3
3
|
export { sanitizeUrl } from '@scarlett-player/core';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/light.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_TARGET_LATENCY,
|
|
2
3
|
__export,
|
|
4
|
+
computeLiveMetrics,
|
|
3
5
|
createHLSPluginWith,
|
|
4
6
|
sanitizeUrl
|
|
5
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-3KFYKCR2.js";
|
|
6
8
|
|
|
7
9
|
// src/hls-loader-light.ts
|
|
8
10
|
var hls_loader_light_exports = {};
|
|
@@ -85,6 +87,8 @@ function createHLSPlugin(config) {
|
|
|
85
87
|
}
|
|
86
88
|
var light_default = createHLSPlugin;
|
|
87
89
|
export {
|
|
90
|
+
DEFAULT_TARGET_LATENCY,
|
|
91
|
+
computeLiveMetrics,
|
|
88
92
|
createHLSPlugin,
|
|
89
93
|
light_default as default,
|
|
90
94
|
sanitizeUrl
|