@scarlett-player/hls 1.12.0 → 1.14.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-47MU7IVX.js → chunk-MT5VV3QN.js} +160 -13
- package/dist/index.cjs +160 -13
- package/dist/index.js +1 -1
- package/dist/light.cjs +160 -13
- package/dist/light.js +1 -1
- package/package.json +2 -2
|
@@ -246,7 +246,7 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
246
246
|
api.emit("quality:levels", {
|
|
247
247
|
levels: levels.map((l) => ({ id: l.id, label: l.label }))
|
|
248
248
|
});
|
|
249
|
-
callbacks.onManifestParsed?.(data.levels);
|
|
249
|
+
callbacks.onManifestParsed?.(data.levels, data);
|
|
250
250
|
});
|
|
251
251
|
addHandler("hlsLevelSwitched", (_event, data) => {
|
|
252
252
|
const level = hls.levels[data.level];
|
|
@@ -358,7 +358,7 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
358
358
|
api.setState("currentAudioTrack", null);
|
|
359
359
|
};
|
|
360
360
|
}
|
|
361
|
-
function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
361
|
+
function setupVideoEventHandlers(video, api, getLiveMetrics, gate) {
|
|
362
362
|
const handlers = [];
|
|
363
363
|
const addHandler = (event, handler) => {
|
|
364
364
|
video.addEventListener(event, handler);
|
|
@@ -380,11 +380,23 @@ function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
|
380
380
|
api.setState("buffering", false);
|
|
381
381
|
api.setState("playbackState", "playing");
|
|
382
382
|
syncEndedFromElement();
|
|
383
|
+
if (gate) gate.corePauseRequested = false;
|
|
384
|
+
if (gate?.corePlayRequested) {
|
|
385
|
+
gate.corePlayRequested = false;
|
|
386
|
+
} else {
|
|
387
|
+
api.emit("playback:play", void 0);
|
|
388
|
+
}
|
|
383
389
|
});
|
|
384
390
|
addHandler("pause", () => {
|
|
385
391
|
api.setState("playing", false);
|
|
386
392
|
api.setState("paused", true);
|
|
387
393
|
api.setState("playbackState", "paused");
|
|
394
|
+
if (gate) gate.corePlayRequested = false;
|
|
395
|
+
if (gate?.corePauseRequested) {
|
|
396
|
+
gate.corePauseRequested = false;
|
|
397
|
+
} else {
|
|
398
|
+
api.emit("playback:pause", void 0);
|
|
399
|
+
}
|
|
388
400
|
});
|
|
389
401
|
addHandler("ended", () => {
|
|
390
402
|
api.setState("playing", false);
|
|
@@ -455,12 +467,6 @@ function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
|
455
467
|
});
|
|
456
468
|
addHandler("loadedmetadata", () => {
|
|
457
469
|
api.setState("duration", video.duration);
|
|
458
|
-
api.setState("mediaType", video.videoWidth > 0 ? "video" : "audio");
|
|
459
|
-
});
|
|
460
|
-
addHandler("loadeddata", () => {
|
|
461
|
-
if (video.videoWidth > 0) {
|
|
462
|
-
api.setState("mediaType", "video");
|
|
463
|
-
}
|
|
464
470
|
});
|
|
465
471
|
addHandler("error", () => {
|
|
466
472
|
const error = video.error;
|
|
@@ -502,6 +508,119 @@ function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
|
502
508
|
};
|
|
503
509
|
}
|
|
504
510
|
|
|
511
|
+
// src/media-type.ts
|
|
512
|
+
var ELEMENT_EVENTS = ["loadedmetadata", "loadeddata", "resize", "playing"];
|
|
513
|
+
var TRACK_LIST_EVENTS = ["addtrack", "removetrack", "change"];
|
|
514
|
+
var HAVE_METADATA = 1;
|
|
515
|
+
function isTrackList(list) {
|
|
516
|
+
return typeof list === "object" && list !== null && typeof list.length === "number";
|
|
517
|
+
}
|
|
518
|
+
function createMediaTypeClassifier(api) {
|
|
519
|
+
let source = null;
|
|
520
|
+
let videoConfirmed = false;
|
|
521
|
+
let audioConfirmed = false;
|
|
522
|
+
let elementReported = false;
|
|
523
|
+
let element = null;
|
|
524
|
+
let elementDisposers = [];
|
|
525
|
+
let published = null;
|
|
526
|
+
let destroyed = false;
|
|
527
|
+
const decide = () => {
|
|
528
|
+
if (videoConfirmed) return "video";
|
|
529
|
+
if (audioConfirmed) return "audio";
|
|
530
|
+
return "unknown";
|
|
531
|
+
};
|
|
532
|
+
const publish = () => {
|
|
533
|
+
const next = decide();
|
|
534
|
+
if (next === published) return;
|
|
535
|
+
published = next;
|
|
536
|
+
api.setState("mediaType", next);
|
|
537
|
+
};
|
|
538
|
+
const readElement = () => {
|
|
539
|
+
const video = element;
|
|
540
|
+
if (!video) return;
|
|
541
|
+
if (elementReported && video.videoWidth > 0) {
|
|
542
|
+
videoConfirmed = true;
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (video.readyState < HAVE_METADATA) return;
|
|
546
|
+
const videoTracks = video.videoTracks;
|
|
547
|
+
const audioTracks = video.audioTracks;
|
|
548
|
+
if (!isTrackList(videoTracks)) return;
|
|
549
|
+
if (videoTracks.length > 0) {
|
|
550
|
+
videoConfirmed = true;
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (isTrackList(audioTracks) && audioTracks.length > 0) {
|
|
554
|
+
audioConfirmed = true;
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
const evaluate = () => {
|
|
558
|
+
if (destroyed) return;
|
|
559
|
+
readElement();
|
|
560
|
+
publish();
|
|
561
|
+
};
|
|
562
|
+
const detachElement = () => {
|
|
563
|
+
for (const off of elementDisposers) off();
|
|
564
|
+
elementDisposers = [];
|
|
565
|
+
element = null;
|
|
566
|
+
};
|
|
567
|
+
return {
|
|
568
|
+
beginSource(src) {
|
|
569
|
+
if (destroyed) return;
|
|
570
|
+
if (source === src) return;
|
|
571
|
+
source = src;
|
|
572
|
+
videoConfirmed = false;
|
|
573
|
+
audioConfirmed = false;
|
|
574
|
+
elementReported = false;
|
|
575
|
+
published = null;
|
|
576
|
+
publish();
|
|
577
|
+
},
|
|
578
|
+
attach(video) {
|
|
579
|
+
if (destroyed) return;
|
|
580
|
+
detachElement();
|
|
581
|
+
element = video;
|
|
582
|
+
elementReported = false;
|
|
583
|
+
for (const event of ELEMENT_EVENTS) {
|
|
584
|
+
const handler = () => {
|
|
585
|
+
elementReported = true;
|
|
586
|
+
evaluate();
|
|
587
|
+
};
|
|
588
|
+
video.addEventListener(event, handler);
|
|
589
|
+
elementDisposers.push(() => video.removeEventListener(event, handler));
|
|
590
|
+
}
|
|
591
|
+
for (const list of [element.videoTracks, element.audioTracks]) {
|
|
592
|
+
if (!isTrackList(list) || typeof list.addEventListener !== "function") continue;
|
|
593
|
+
for (const event of TRACK_LIST_EVENTS) {
|
|
594
|
+
const handler = () => evaluate();
|
|
595
|
+
list.addEventListener(event, handler);
|
|
596
|
+
elementDisposers.push(() => list.removeEventListener?.(event, handler));
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
evaluate();
|
|
600
|
+
},
|
|
601
|
+
noteManifestParsed(data) {
|
|
602
|
+
if (destroyed || !data) return;
|
|
603
|
+
const hasVideo = typeof data.video === "boolean" ? data.video : null;
|
|
604
|
+
const hasAudio = typeof data.audio === "boolean" ? data.audio : null;
|
|
605
|
+
if (hasVideo === true) {
|
|
606
|
+
videoConfirmed = true;
|
|
607
|
+
} else if (hasVideo === false && hasAudio === true) {
|
|
608
|
+
audioConfirmed = true;
|
|
609
|
+
}
|
|
610
|
+
publish();
|
|
611
|
+
},
|
|
612
|
+
evaluate,
|
|
613
|
+
current() {
|
|
614
|
+
return decide();
|
|
615
|
+
},
|
|
616
|
+
destroy() {
|
|
617
|
+
if (destroyed) return;
|
|
618
|
+
destroyed = true;
|
|
619
|
+
detachElement();
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
505
624
|
// src/playlist-validation.ts
|
|
506
625
|
var PLAYLIST_INVALID_TEXT = "Invalid playlist document";
|
|
507
626
|
var MEDIA_PLAYLIST_CONTEXTS = ["level", "audioTrack", "subtitleTrack"];
|
|
@@ -547,7 +666,7 @@ function createValidatingPlaylistLoader(Hls) {
|
|
|
547
666
|
}
|
|
548
667
|
|
|
549
668
|
// src/version.ts
|
|
550
|
-
var PKG_VERSION = true ? "1.
|
|
669
|
+
var PKG_VERSION = true ? "1.13.0" : "0.0.0-dev";
|
|
551
670
|
|
|
552
671
|
// src/create-hls-plugin.ts
|
|
553
672
|
var DEFAULT_CONFIG = {
|
|
@@ -591,6 +710,11 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
591
710
|
let cleanupHlsEvents = null;
|
|
592
711
|
let cleanupVideoEvents = null;
|
|
593
712
|
let isAutoQuality = true;
|
|
713
|
+
const playbackGate = {
|
|
714
|
+
corePlayRequested: false,
|
|
715
|
+
corePauseRequested: false
|
|
716
|
+
};
|
|
717
|
+
let mediaTypeClassifier = null;
|
|
594
718
|
let lastLevelDetails = null;
|
|
595
719
|
let lastLiveMetrics = null;
|
|
596
720
|
let lastKnownTargetLatency = null;
|
|
@@ -946,8 +1070,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
946
1070
|
const videoEl = getOrCreateVideo();
|
|
947
1071
|
isNative = true;
|
|
948
1072
|
if (api) {
|
|
949
|
-
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics);
|
|
1073
|
+
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics, playbackGate);
|
|
950
1074
|
}
|
|
1075
|
+
mediaTypeClassifier?.beginSource(src);
|
|
1076
|
+
mediaTypeClassifier?.attach(videoEl);
|
|
951
1077
|
return new Promise((resolve, reject) => {
|
|
952
1078
|
let watchdog = null;
|
|
953
1079
|
let settled = false;
|
|
@@ -1039,8 +1165,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1039
1165
|
isNative = false;
|
|
1040
1166
|
hls = loader.createHlsInstance(buildHlsConfig());
|
|
1041
1167
|
if (api) {
|
|
1042
|
-
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics);
|
|
1168
|
+
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics, playbackGate);
|
|
1043
1169
|
}
|
|
1170
|
+
mediaTypeClassifier?.beginSource(src);
|
|
1171
|
+
mediaTypeClassifier?.attach(videoEl);
|
|
1044
1172
|
return new Promise((resolve, reject) => {
|
|
1045
1173
|
if (!hls || !api) {
|
|
1046
1174
|
reject(new Error("HLS not initialized"));
|
|
@@ -1067,8 +1195,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1067
1195
|
}
|
|
1068
1196
|
};
|
|
1069
1197
|
cleanupHlsEvents = setupHlsEventHandlers(hls, api, {
|
|
1070
|
-
onManifestParsed: () => {
|
|
1198
|
+
onManifestParsed: (_levels, manifestData) => {
|
|
1071
1199
|
if (session !== loadSession) return;
|
|
1200
|
+
mediaTypeClassifier?.noteManifestParsed(manifestData);
|
|
1072
1201
|
if (!resolved) {
|
|
1073
1202
|
resolved = true;
|
|
1074
1203
|
releaseAbort();
|
|
@@ -1327,16 +1456,30 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1327
1456
|
async init(pluginApi) {
|
|
1328
1457
|
api = pluginApi;
|
|
1329
1458
|
api.logger.info(`HLS plugin${variant.logSuffix} initialized`);
|
|
1459
|
+
mediaTypeClassifier = createMediaTypeClassifier(api);
|
|
1330
1460
|
const unsubPlay = api.on("playback:play", async () => {
|
|
1331
1461
|
if (!video) return;
|
|
1462
|
+
if (!video.paused) return;
|
|
1332
1463
|
try {
|
|
1464
|
+
playbackGate.corePlayRequested = true;
|
|
1333
1465
|
await video.play();
|
|
1334
1466
|
} catch (e) {
|
|
1467
|
+
playbackGate.corePlayRequested = false;
|
|
1335
1468
|
api?.logger.error("Play failed", e);
|
|
1336
1469
|
}
|
|
1337
1470
|
});
|
|
1338
1471
|
const unsubPause = api.on("playback:pause", () => {
|
|
1339
|
-
video
|
|
1472
|
+
if (!video) return;
|
|
1473
|
+
if (video.paused) {
|
|
1474
|
+
if (playbackGate.corePlayRequested) {
|
|
1475
|
+
playbackGate.corePlayRequested = false;
|
|
1476
|
+
video.pause();
|
|
1477
|
+
}
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
playbackGate.corePauseRequested = true;
|
|
1481
|
+
playbackGate.corePlayRequested = false;
|
|
1482
|
+
video.pause();
|
|
1340
1483
|
});
|
|
1341
1484
|
const unsubSeek = api.on("playback:seeking", ({ time }) => {
|
|
1342
1485
|
if (!video) return;
|
|
@@ -1462,6 +1605,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1462
1605
|
onlineListener = null;
|
|
1463
1606
|
}
|
|
1464
1607
|
cleanup(new Error("HLS load cancelled: player destroyed"));
|
|
1608
|
+
mediaTypeClassifier?.destroy();
|
|
1609
|
+
mediaTypeClassifier = null;
|
|
1465
1610
|
if (video?.parentNode) {
|
|
1466
1611
|
video.parentNode.removeChild(video);
|
|
1467
1612
|
}
|
|
@@ -1478,6 +1623,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1478
1623
|
api.setState("live", false);
|
|
1479
1624
|
cleanup(new Error("HLS load cancelled: superseded by a new load"));
|
|
1480
1625
|
currentSrc = src;
|
|
1626
|
+
playbackGate.corePlayRequested = false;
|
|
1627
|
+
playbackGate.corePauseRequested = false;
|
|
1481
1628
|
applyPoster();
|
|
1482
1629
|
api.setState("playbackState", "loading");
|
|
1483
1630
|
api.setState("buffering", true);
|
package/dist/index.cjs
CHANGED
|
@@ -346,7 +346,7 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
346
346
|
api.emit("quality:levels", {
|
|
347
347
|
levels: levels.map((l) => ({ id: l.id, label: l.label }))
|
|
348
348
|
});
|
|
349
|
-
callbacks.onManifestParsed?.(data.levels);
|
|
349
|
+
callbacks.onManifestParsed?.(data.levels, data);
|
|
350
350
|
});
|
|
351
351
|
addHandler("hlsLevelSwitched", (_event, data) => {
|
|
352
352
|
const level = hls.levels[data.level];
|
|
@@ -458,7 +458,7 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
458
458
|
api.setState("currentAudioTrack", null);
|
|
459
459
|
};
|
|
460
460
|
}
|
|
461
|
-
function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
461
|
+
function setupVideoEventHandlers(video, api, getLiveMetrics, gate) {
|
|
462
462
|
const handlers = [];
|
|
463
463
|
const addHandler = (event, handler) => {
|
|
464
464
|
video.addEventListener(event, handler);
|
|
@@ -480,11 +480,23 @@ function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
|
480
480
|
api.setState("buffering", false);
|
|
481
481
|
api.setState("playbackState", "playing");
|
|
482
482
|
syncEndedFromElement();
|
|
483
|
+
if (gate) gate.corePauseRequested = false;
|
|
484
|
+
if (gate?.corePlayRequested) {
|
|
485
|
+
gate.corePlayRequested = false;
|
|
486
|
+
} else {
|
|
487
|
+
api.emit("playback:play", void 0);
|
|
488
|
+
}
|
|
483
489
|
});
|
|
484
490
|
addHandler("pause", () => {
|
|
485
491
|
api.setState("playing", false);
|
|
486
492
|
api.setState("paused", true);
|
|
487
493
|
api.setState("playbackState", "paused");
|
|
494
|
+
if (gate) gate.corePlayRequested = false;
|
|
495
|
+
if (gate?.corePauseRequested) {
|
|
496
|
+
gate.corePauseRequested = false;
|
|
497
|
+
} else {
|
|
498
|
+
api.emit("playback:pause", void 0);
|
|
499
|
+
}
|
|
488
500
|
});
|
|
489
501
|
addHandler("ended", () => {
|
|
490
502
|
api.setState("playing", false);
|
|
@@ -555,12 +567,6 @@ function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
|
555
567
|
});
|
|
556
568
|
addHandler("loadedmetadata", () => {
|
|
557
569
|
api.setState("duration", video.duration);
|
|
558
|
-
api.setState("mediaType", video.videoWidth > 0 ? "video" : "audio");
|
|
559
|
-
});
|
|
560
|
-
addHandler("loadeddata", () => {
|
|
561
|
-
if (video.videoWidth > 0) {
|
|
562
|
-
api.setState("mediaType", "video");
|
|
563
|
-
}
|
|
564
570
|
});
|
|
565
571
|
addHandler("error", () => {
|
|
566
572
|
const error = video.error;
|
|
@@ -602,6 +608,119 @@ function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
|
602
608
|
};
|
|
603
609
|
}
|
|
604
610
|
|
|
611
|
+
// src/media-type.ts
|
|
612
|
+
var ELEMENT_EVENTS = ["loadedmetadata", "loadeddata", "resize", "playing"];
|
|
613
|
+
var TRACK_LIST_EVENTS = ["addtrack", "removetrack", "change"];
|
|
614
|
+
var HAVE_METADATA = 1;
|
|
615
|
+
function isTrackList(list) {
|
|
616
|
+
return typeof list === "object" && list !== null && typeof list.length === "number";
|
|
617
|
+
}
|
|
618
|
+
function createMediaTypeClassifier(api) {
|
|
619
|
+
let source = null;
|
|
620
|
+
let videoConfirmed = false;
|
|
621
|
+
let audioConfirmed = false;
|
|
622
|
+
let elementReported = false;
|
|
623
|
+
let element = null;
|
|
624
|
+
let elementDisposers = [];
|
|
625
|
+
let published = null;
|
|
626
|
+
let destroyed = false;
|
|
627
|
+
const decide = () => {
|
|
628
|
+
if (videoConfirmed) return "video";
|
|
629
|
+
if (audioConfirmed) return "audio";
|
|
630
|
+
return "unknown";
|
|
631
|
+
};
|
|
632
|
+
const publish = () => {
|
|
633
|
+
const next = decide();
|
|
634
|
+
if (next === published) return;
|
|
635
|
+
published = next;
|
|
636
|
+
api.setState("mediaType", next);
|
|
637
|
+
};
|
|
638
|
+
const readElement = () => {
|
|
639
|
+
const video = element;
|
|
640
|
+
if (!video) return;
|
|
641
|
+
if (elementReported && video.videoWidth > 0) {
|
|
642
|
+
videoConfirmed = true;
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (video.readyState < HAVE_METADATA) return;
|
|
646
|
+
const videoTracks = video.videoTracks;
|
|
647
|
+
const audioTracks = video.audioTracks;
|
|
648
|
+
if (!isTrackList(videoTracks)) return;
|
|
649
|
+
if (videoTracks.length > 0) {
|
|
650
|
+
videoConfirmed = true;
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (isTrackList(audioTracks) && audioTracks.length > 0) {
|
|
654
|
+
audioConfirmed = true;
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
const evaluate = () => {
|
|
658
|
+
if (destroyed) return;
|
|
659
|
+
readElement();
|
|
660
|
+
publish();
|
|
661
|
+
};
|
|
662
|
+
const detachElement = () => {
|
|
663
|
+
for (const off of elementDisposers) off();
|
|
664
|
+
elementDisposers = [];
|
|
665
|
+
element = null;
|
|
666
|
+
};
|
|
667
|
+
return {
|
|
668
|
+
beginSource(src) {
|
|
669
|
+
if (destroyed) return;
|
|
670
|
+
if (source === src) return;
|
|
671
|
+
source = src;
|
|
672
|
+
videoConfirmed = false;
|
|
673
|
+
audioConfirmed = false;
|
|
674
|
+
elementReported = false;
|
|
675
|
+
published = null;
|
|
676
|
+
publish();
|
|
677
|
+
},
|
|
678
|
+
attach(video) {
|
|
679
|
+
if (destroyed) return;
|
|
680
|
+
detachElement();
|
|
681
|
+
element = video;
|
|
682
|
+
elementReported = false;
|
|
683
|
+
for (const event of ELEMENT_EVENTS) {
|
|
684
|
+
const handler = () => {
|
|
685
|
+
elementReported = true;
|
|
686
|
+
evaluate();
|
|
687
|
+
};
|
|
688
|
+
video.addEventListener(event, handler);
|
|
689
|
+
elementDisposers.push(() => video.removeEventListener(event, handler));
|
|
690
|
+
}
|
|
691
|
+
for (const list of [element.videoTracks, element.audioTracks]) {
|
|
692
|
+
if (!isTrackList(list) || typeof list.addEventListener !== "function") continue;
|
|
693
|
+
for (const event of TRACK_LIST_EVENTS) {
|
|
694
|
+
const handler = () => evaluate();
|
|
695
|
+
list.addEventListener(event, handler);
|
|
696
|
+
elementDisposers.push(() => list.removeEventListener?.(event, handler));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
evaluate();
|
|
700
|
+
},
|
|
701
|
+
noteManifestParsed(data) {
|
|
702
|
+
if (destroyed || !data) return;
|
|
703
|
+
const hasVideo = typeof data.video === "boolean" ? data.video : null;
|
|
704
|
+
const hasAudio = typeof data.audio === "boolean" ? data.audio : null;
|
|
705
|
+
if (hasVideo === true) {
|
|
706
|
+
videoConfirmed = true;
|
|
707
|
+
} else if (hasVideo === false && hasAudio === true) {
|
|
708
|
+
audioConfirmed = true;
|
|
709
|
+
}
|
|
710
|
+
publish();
|
|
711
|
+
},
|
|
712
|
+
evaluate,
|
|
713
|
+
current() {
|
|
714
|
+
return decide();
|
|
715
|
+
},
|
|
716
|
+
destroy() {
|
|
717
|
+
if (destroyed) return;
|
|
718
|
+
destroyed = true;
|
|
719
|
+
detachElement();
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
605
724
|
// src/playlist-validation.ts
|
|
606
725
|
var PLAYLIST_INVALID_TEXT = "Invalid playlist document";
|
|
607
726
|
var MEDIA_PLAYLIST_CONTEXTS = ["level", "audioTrack", "subtitleTrack"];
|
|
@@ -647,7 +766,7 @@ function createValidatingPlaylistLoader(Hls) {
|
|
|
647
766
|
}
|
|
648
767
|
|
|
649
768
|
// src/version.ts
|
|
650
|
-
var PKG_VERSION = true ? "1.
|
|
769
|
+
var PKG_VERSION = true ? "1.13.0" : "0.0.0-dev";
|
|
651
770
|
|
|
652
771
|
// src/create-hls-plugin.ts
|
|
653
772
|
var DEFAULT_CONFIG = {
|
|
@@ -691,6 +810,11 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
691
810
|
let cleanupHlsEvents = null;
|
|
692
811
|
let cleanupVideoEvents = null;
|
|
693
812
|
let isAutoQuality = true;
|
|
813
|
+
const playbackGate = {
|
|
814
|
+
corePlayRequested: false,
|
|
815
|
+
corePauseRequested: false
|
|
816
|
+
};
|
|
817
|
+
let mediaTypeClassifier = null;
|
|
694
818
|
let lastLevelDetails = null;
|
|
695
819
|
let lastLiveMetrics = null;
|
|
696
820
|
let lastKnownTargetLatency = null;
|
|
@@ -1046,8 +1170,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1046
1170
|
const videoEl = getOrCreateVideo();
|
|
1047
1171
|
isNative = true;
|
|
1048
1172
|
if (api) {
|
|
1049
|
-
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics);
|
|
1173
|
+
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics, playbackGate);
|
|
1050
1174
|
}
|
|
1175
|
+
mediaTypeClassifier?.beginSource(src);
|
|
1176
|
+
mediaTypeClassifier?.attach(videoEl);
|
|
1051
1177
|
return new Promise((resolve, reject) => {
|
|
1052
1178
|
let watchdog = null;
|
|
1053
1179
|
let settled = false;
|
|
@@ -1139,8 +1265,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1139
1265
|
isNative = false;
|
|
1140
1266
|
hls = loader.createHlsInstance(buildHlsConfig());
|
|
1141
1267
|
if (api) {
|
|
1142
|
-
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics);
|
|
1268
|
+
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics, playbackGate);
|
|
1143
1269
|
}
|
|
1270
|
+
mediaTypeClassifier?.beginSource(src);
|
|
1271
|
+
mediaTypeClassifier?.attach(videoEl);
|
|
1144
1272
|
return new Promise((resolve, reject) => {
|
|
1145
1273
|
if (!hls || !api) {
|
|
1146
1274
|
reject(new Error("HLS not initialized"));
|
|
@@ -1167,8 +1295,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1167
1295
|
}
|
|
1168
1296
|
};
|
|
1169
1297
|
cleanupHlsEvents = setupHlsEventHandlers(hls, api, {
|
|
1170
|
-
onManifestParsed: () => {
|
|
1298
|
+
onManifestParsed: (_levels, manifestData) => {
|
|
1171
1299
|
if (session !== loadSession) return;
|
|
1300
|
+
mediaTypeClassifier?.noteManifestParsed(manifestData);
|
|
1172
1301
|
if (!resolved) {
|
|
1173
1302
|
resolved = true;
|
|
1174
1303
|
releaseAbort();
|
|
@@ -1427,16 +1556,30 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1427
1556
|
async init(pluginApi) {
|
|
1428
1557
|
api = pluginApi;
|
|
1429
1558
|
api.logger.info(`HLS plugin${variant.logSuffix} initialized`);
|
|
1559
|
+
mediaTypeClassifier = createMediaTypeClassifier(api);
|
|
1430
1560
|
const unsubPlay = api.on("playback:play", async () => {
|
|
1431
1561
|
if (!video) return;
|
|
1562
|
+
if (!video.paused) return;
|
|
1432
1563
|
try {
|
|
1564
|
+
playbackGate.corePlayRequested = true;
|
|
1433
1565
|
await video.play();
|
|
1434
1566
|
} catch (e) {
|
|
1567
|
+
playbackGate.corePlayRequested = false;
|
|
1435
1568
|
api?.logger.error("Play failed", e);
|
|
1436
1569
|
}
|
|
1437
1570
|
});
|
|
1438
1571
|
const unsubPause = api.on("playback:pause", () => {
|
|
1439
|
-
video
|
|
1572
|
+
if (!video) return;
|
|
1573
|
+
if (video.paused) {
|
|
1574
|
+
if (playbackGate.corePlayRequested) {
|
|
1575
|
+
playbackGate.corePlayRequested = false;
|
|
1576
|
+
video.pause();
|
|
1577
|
+
}
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
playbackGate.corePauseRequested = true;
|
|
1581
|
+
playbackGate.corePlayRequested = false;
|
|
1582
|
+
video.pause();
|
|
1440
1583
|
});
|
|
1441
1584
|
const unsubSeek = api.on("playback:seeking", ({ time }) => {
|
|
1442
1585
|
if (!video) return;
|
|
@@ -1562,6 +1705,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1562
1705
|
onlineListener = null;
|
|
1563
1706
|
}
|
|
1564
1707
|
cleanup(new Error("HLS load cancelled: player destroyed"));
|
|
1708
|
+
mediaTypeClassifier?.destroy();
|
|
1709
|
+
mediaTypeClassifier = null;
|
|
1565
1710
|
if (video?.parentNode) {
|
|
1566
1711
|
video.parentNode.removeChild(video);
|
|
1567
1712
|
}
|
|
@@ -1578,6 +1723,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1578
1723
|
api.setState("live", false);
|
|
1579
1724
|
cleanup(new Error("HLS load cancelled: superseded by a new load"));
|
|
1580
1725
|
currentSrc = src;
|
|
1726
|
+
playbackGate.corePlayRequested = false;
|
|
1727
|
+
playbackGate.corePauseRequested = false;
|
|
1581
1728
|
applyPoster();
|
|
1582
1729
|
api.setState("playbackState", "loading");
|
|
1583
1730
|
api.setState("buffering", true);
|
package/dist/index.js
CHANGED
package/dist/light.cjs
CHANGED
|
@@ -346,7 +346,7 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
346
346
|
api.emit("quality:levels", {
|
|
347
347
|
levels: levels.map((l) => ({ id: l.id, label: l.label }))
|
|
348
348
|
});
|
|
349
|
-
callbacks.onManifestParsed?.(data.levels);
|
|
349
|
+
callbacks.onManifestParsed?.(data.levels, data);
|
|
350
350
|
});
|
|
351
351
|
addHandler("hlsLevelSwitched", (_event, data) => {
|
|
352
352
|
const level = hls.levels[data.level];
|
|
@@ -458,7 +458,7 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
458
458
|
api.setState("currentAudioTrack", null);
|
|
459
459
|
};
|
|
460
460
|
}
|
|
461
|
-
function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
461
|
+
function setupVideoEventHandlers(video, api, getLiveMetrics, gate) {
|
|
462
462
|
const handlers = [];
|
|
463
463
|
const addHandler = (event, handler) => {
|
|
464
464
|
video.addEventListener(event, handler);
|
|
@@ -480,11 +480,23 @@ function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
|
480
480
|
api.setState("buffering", false);
|
|
481
481
|
api.setState("playbackState", "playing");
|
|
482
482
|
syncEndedFromElement();
|
|
483
|
+
if (gate) gate.corePauseRequested = false;
|
|
484
|
+
if (gate?.corePlayRequested) {
|
|
485
|
+
gate.corePlayRequested = false;
|
|
486
|
+
} else {
|
|
487
|
+
api.emit("playback:play", void 0);
|
|
488
|
+
}
|
|
483
489
|
});
|
|
484
490
|
addHandler("pause", () => {
|
|
485
491
|
api.setState("playing", false);
|
|
486
492
|
api.setState("paused", true);
|
|
487
493
|
api.setState("playbackState", "paused");
|
|
494
|
+
if (gate) gate.corePlayRequested = false;
|
|
495
|
+
if (gate?.corePauseRequested) {
|
|
496
|
+
gate.corePauseRequested = false;
|
|
497
|
+
} else {
|
|
498
|
+
api.emit("playback:pause", void 0);
|
|
499
|
+
}
|
|
488
500
|
});
|
|
489
501
|
addHandler("ended", () => {
|
|
490
502
|
api.setState("playing", false);
|
|
@@ -555,12 +567,6 @@ function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
|
555
567
|
});
|
|
556
568
|
addHandler("loadedmetadata", () => {
|
|
557
569
|
api.setState("duration", video.duration);
|
|
558
|
-
api.setState("mediaType", video.videoWidth > 0 ? "video" : "audio");
|
|
559
|
-
});
|
|
560
|
-
addHandler("loadeddata", () => {
|
|
561
|
-
if (video.videoWidth > 0) {
|
|
562
|
-
api.setState("mediaType", "video");
|
|
563
|
-
}
|
|
564
570
|
});
|
|
565
571
|
addHandler("error", () => {
|
|
566
572
|
const error = video.error;
|
|
@@ -602,6 +608,119 @@ function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
|
602
608
|
};
|
|
603
609
|
}
|
|
604
610
|
|
|
611
|
+
// src/media-type.ts
|
|
612
|
+
var ELEMENT_EVENTS = ["loadedmetadata", "loadeddata", "resize", "playing"];
|
|
613
|
+
var TRACK_LIST_EVENTS = ["addtrack", "removetrack", "change"];
|
|
614
|
+
var HAVE_METADATA = 1;
|
|
615
|
+
function isTrackList(list) {
|
|
616
|
+
return typeof list === "object" && list !== null && typeof list.length === "number";
|
|
617
|
+
}
|
|
618
|
+
function createMediaTypeClassifier(api) {
|
|
619
|
+
let source = null;
|
|
620
|
+
let videoConfirmed = false;
|
|
621
|
+
let audioConfirmed = false;
|
|
622
|
+
let elementReported = false;
|
|
623
|
+
let element = null;
|
|
624
|
+
let elementDisposers = [];
|
|
625
|
+
let published = null;
|
|
626
|
+
let destroyed = false;
|
|
627
|
+
const decide = () => {
|
|
628
|
+
if (videoConfirmed) return "video";
|
|
629
|
+
if (audioConfirmed) return "audio";
|
|
630
|
+
return "unknown";
|
|
631
|
+
};
|
|
632
|
+
const publish = () => {
|
|
633
|
+
const next = decide();
|
|
634
|
+
if (next === published) return;
|
|
635
|
+
published = next;
|
|
636
|
+
api.setState("mediaType", next);
|
|
637
|
+
};
|
|
638
|
+
const readElement = () => {
|
|
639
|
+
const video = element;
|
|
640
|
+
if (!video) return;
|
|
641
|
+
if (elementReported && video.videoWidth > 0) {
|
|
642
|
+
videoConfirmed = true;
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (video.readyState < HAVE_METADATA) return;
|
|
646
|
+
const videoTracks = video.videoTracks;
|
|
647
|
+
const audioTracks = video.audioTracks;
|
|
648
|
+
if (!isTrackList(videoTracks)) return;
|
|
649
|
+
if (videoTracks.length > 0) {
|
|
650
|
+
videoConfirmed = true;
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (isTrackList(audioTracks) && audioTracks.length > 0) {
|
|
654
|
+
audioConfirmed = true;
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
const evaluate = () => {
|
|
658
|
+
if (destroyed) return;
|
|
659
|
+
readElement();
|
|
660
|
+
publish();
|
|
661
|
+
};
|
|
662
|
+
const detachElement = () => {
|
|
663
|
+
for (const off of elementDisposers) off();
|
|
664
|
+
elementDisposers = [];
|
|
665
|
+
element = null;
|
|
666
|
+
};
|
|
667
|
+
return {
|
|
668
|
+
beginSource(src) {
|
|
669
|
+
if (destroyed) return;
|
|
670
|
+
if (source === src) return;
|
|
671
|
+
source = src;
|
|
672
|
+
videoConfirmed = false;
|
|
673
|
+
audioConfirmed = false;
|
|
674
|
+
elementReported = false;
|
|
675
|
+
published = null;
|
|
676
|
+
publish();
|
|
677
|
+
},
|
|
678
|
+
attach(video) {
|
|
679
|
+
if (destroyed) return;
|
|
680
|
+
detachElement();
|
|
681
|
+
element = video;
|
|
682
|
+
elementReported = false;
|
|
683
|
+
for (const event of ELEMENT_EVENTS) {
|
|
684
|
+
const handler = () => {
|
|
685
|
+
elementReported = true;
|
|
686
|
+
evaluate();
|
|
687
|
+
};
|
|
688
|
+
video.addEventListener(event, handler);
|
|
689
|
+
elementDisposers.push(() => video.removeEventListener(event, handler));
|
|
690
|
+
}
|
|
691
|
+
for (const list of [element.videoTracks, element.audioTracks]) {
|
|
692
|
+
if (!isTrackList(list) || typeof list.addEventListener !== "function") continue;
|
|
693
|
+
for (const event of TRACK_LIST_EVENTS) {
|
|
694
|
+
const handler = () => evaluate();
|
|
695
|
+
list.addEventListener(event, handler);
|
|
696
|
+
elementDisposers.push(() => list.removeEventListener?.(event, handler));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
evaluate();
|
|
700
|
+
},
|
|
701
|
+
noteManifestParsed(data) {
|
|
702
|
+
if (destroyed || !data) return;
|
|
703
|
+
const hasVideo = typeof data.video === "boolean" ? data.video : null;
|
|
704
|
+
const hasAudio = typeof data.audio === "boolean" ? data.audio : null;
|
|
705
|
+
if (hasVideo === true) {
|
|
706
|
+
videoConfirmed = true;
|
|
707
|
+
} else if (hasVideo === false && hasAudio === true) {
|
|
708
|
+
audioConfirmed = true;
|
|
709
|
+
}
|
|
710
|
+
publish();
|
|
711
|
+
},
|
|
712
|
+
evaluate,
|
|
713
|
+
current() {
|
|
714
|
+
return decide();
|
|
715
|
+
},
|
|
716
|
+
destroy() {
|
|
717
|
+
if (destroyed) return;
|
|
718
|
+
destroyed = true;
|
|
719
|
+
detachElement();
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
605
724
|
// src/playlist-validation.ts
|
|
606
725
|
var PLAYLIST_INVALID_TEXT = "Invalid playlist document";
|
|
607
726
|
var MEDIA_PLAYLIST_CONTEXTS = ["level", "audioTrack", "subtitleTrack"];
|
|
@@ -647,7 +766,7 @@ function createValidatingPlaylistLoader(Hls) {
|
|
|
647
766
|
}
|
|
648
767
|
|
|
649
768
|
// src/version.ts
|
|
650
|
-
var PKG_VERSION = true ? "1.
|
|
769
|
+
var PKG_VERSION = true ? "1.13.0" : "0.0.0-dev";
|
|
651
770
|
|
|
652
771
|
// src/create-hls-plugin.ts
|
|
653
772
|
var DEFAULT_CONFIG = {
|
|
@@ -691,6 +810,11 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
691
810
|
let cleanupHlsEvents = null;
|
|
692
811
|
let cleanupVideoEvents = null;
|
|
693
812
|
let isAutoQuality = true;
|
|
813
|
+
const playbackGate = {
|
|
814
|
+
corePlayRequested: false,
|
|
815
|
+
corePauseRequested: false
|
|
816
|
+
};
|
|
817
|
+
let mediaTypeClassifier = null;
|
|
694
818
|
let lastLevelDetails = null;
|
|
695
819
|
let lastLiveMetrics = null;
|
|
696
820
|
let lastKnownTargetLatency = null;
|
|
@@ -1046,8 +1170,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1046
1170
|
const videoEl = getOrCreateVideo();
|
|
1047
1171
|
isNative = true;
|
|
1048
1172
|
if (api) {
|
|
1049
|
-
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics);
|
|
1173
|
+
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics, playbackGate);
|
|
1050
1174
|
}
|
|
1175
|
+
mediaTypeClassifier?.beginSource(src);
|
|
1176
|
+
mediaTypeClassifier?.attach(videoEl);
|
|
1051
1177
|
return new Promise((resolve, reject) => {
|
|
1052
1178
|
let watchdog = null;
|
|
1053
1179
|
let settled = false;
|
|
@@ -1139,8 +1265,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1139
1265
|
isNative = false;
|
|
1140
1266
|
hls = loader.createHlsInstance(buildHlsConfig());
|
|
1141
1267
|
if (api) {
|
|
1142
|
-
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics);
|
|
1268
|
+
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics, playbackGate);
|
|
1143
1269
|
}
|
|
1270
|
+
mediaTypeClassifier?.beginSource(src);
|
|
1271
|
+
mediaTypeClassifier?.attach(videoEl);
|
|
1144
1272
|
return new Promise((resolve, reject) => {
|
|
1145
1273
|
if (!hls || !api) {
|
|
1146
1274
|
reject(new Error("HLS not initialized"));
|
|
@@ -1167,8 +1295,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1167
1295
|
}
|
|
1168
1296
|
};
|
|
1169
1297
|
cleanupHlsEvents = setupHlsEventHandlers(hls, api, {
|
|
1170
|
-
onManifestParsed: () => {
|
|
1298
|
+
onManifestParsed: (_levels, manifestData) => {
|
|
1171
1299
|
if (session !== loadSession) return;
|
|
1300
|
+
mediaTypeClassifier?.noteManifestParsed(manifestData);
|
|
1172
1301
|
if (!resolved) {
|
|
1173
1302
|
resolved = true;
|
|
1174
1303
|
releaseAbort();
|
|
@@ -1427,16 +1556,30 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1427
1556
|
async init(pluginApi) {
|
|
1428
1557
|
api = pluginApi;
|
|
1429
1558
|
api.logger.info(`HLS plugin${variant.logSuffix} initialized`);
|
|
1559
|
+
mediaTypeClassifier = createMediaTypeClassifier(api);
|
|
1430
1560
|
const unsubPlay = api.on("playback:play", async () => {
|
|
1431
1561
|
if (!video) return;
|
|
1562
|
+
if (!video.paused) return;
|
|
1432
1563
|
try {
|
|
1564
|
+
playbackGate.corePlayRequested = true;
|
|
1433
1565
|
await video.play();
|
|
1434
1566
|
} catch (e) {
|
|
1567
|
+
playbackGate.corePlayRequested = false;
|
|
1435
1568
|
api?.logger.error("Play failed", e);
|
|
1436
1569
|
}
|
|
1437
1570
|
});
|
|
1438
1571
|
const unsubPause = api.on("playback:pause", () => {
|
|
1439
|
-
video
|
|
1572
|
+
if (!video) return;
|
|
1573
|
+
if (video.paused) {
|
|
1574
|
+
if (playbackGate.corePlayRequested) {
|
|
1575
|
+
playbackGate.corePlayRequested = false;
|
|
1576
|
+
video.pause();
|
|
1577
|
+
}
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
playbackGate.corePauseRequested = true;
|
|
1581
|
+
playbackGate.corePlayRequested = false;
|
|
1582
|
+
video.pause();
|
|
1440
1583
|
});
|
|
1441
1584
|
const unsubSeek = api.on("playback:seeking", ({ time }) => {
|
|
1442
1585
|
if (!video) return;
|
|
@@ -1562,6 +1705,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1562
1705
|
onlineListener = null;
|
|
1563
1706
|
}
|
|
1564
1707
|
cleanup(new Error("HLS load cancelled: player destroyed"));
|
|
1708
|
+
mediaTypeClassifier?.destroy();
|
|
1709
|
+
mediaTypeClassifier = null;
|
|
1565
1710
|
if (video?.parentNode) {
|
|
1566
1711
|
video.parentNode.removeChild(video);
|
|
1567
1712
|
}
|
|
@@ -1578,6 +1723,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1578
1723
|
api.setState("live", false);
|
|
1579
1724
|
cleanup(new Error("HLS load cancelled: superseded by a new load"));
|
|
1580
1725
|
currentSrc = src;
|
|
1726
|
+
playbackGate.corePlayRequested = false;
|
|
1727
|
+
playbackGate.corePauseRequested = false;
|
|
1581
1728
|
applyPoster();
|
|
1582
1729
|
api.setState("playbackState", "loading");
|
|
1583
1730
|
api.setState("buffering", true);
|
package/dist/light.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scarlett-player/hls",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "HLS Provider Plugin for Scarlett Player",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"vitest": "^1.6.0",
|
|
38
38
|
"@vitest/coverage-v8": "^1.6.0",
|
|
39
39
|
"jsdom": "^24.0.0",
|
|
40
|
-
"@scarlett-player/core": "1.
|
|
40
|
+
"@scarlett-player/core": "1.14.0"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"video",
|