@scarlett-player/hls 1.12.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/dist/{chunk-47MU7IVX.js → chunk-3KFYKCR2.js} +124 -8
- package/dist/index.cjs +124 -8
- package/dist/index.js +1 -1
- package/dist/light.cjs +124 -8
- 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];
|
|
@@ -455,12 +455,6 @@ function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
|
455
455
|
});
|
|
456
456
|
addHandler("loadedmetadata", () => {
|
|
457
457
|
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
458
|
});
|
|
465
459
|
addHandler("error", () => {
|
|
466
460
|
const error = video.error;
|
|
@@ -502,6 +496,119 @@ function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
|
502
496
|
};
|
|
503
497
|
}
|
|
504
498
|
|
|
499
|
+
// src/media-type.ts
|
|
500
|
+
var ELEMENT_EVENTS = ["loadedmetadata", "loadeddata", "resize", "playing"];
|
|
501
|
+
var TRACK_LIST_EVENTS = ["addtrack", "removetrack", "change"];
|
|
502
|
+
var HAVE_METADATA = 1;
|
|
503
|
+
function isTrackList(list) {
|
|
504
|
+
return typeof list === "object" && list !== null && typeof list.length === "number";
|
|
505
|
+
}
|
|
506
|
+
function createMediaTypeClassifier(api) {
|
|
507
|
+
let source = null;
|
|
508
|
+
let videoConfirmed = false;
|
|
509
|
+
let audioConfirmed = false;
|
|
510
|
+
let elementReported = false;
|
|
511
|
+
let element = null;
|
|
512
|
+
let elementDisposers = [];
|
|
513
|
+
let published = null;
|
|
514
|
+
let destroyed = false;
|
|
515
|
+
const decide = () => {
|
|
516
|
+
if (videoConfirmed) return "video";
|
|
517
|
+
if (audioConfirmed) return "audio";
|
|
518
|
+
return "unknown";
|
|
519
|
+
};
|
|
520
|
+
const publish = () => {
|
|
521
|
+
const next = decide();
|
|
522
|
+
if (next === published) return;
|
|
523
|
+
published = next;
|
|
524
|
+
api.setState("mediaType", next);
|
|
525
|
+
};
|
|
526
|
+
const readElement = () => {
|
|
527
|
+
const video = element;
|
|
528
|
+
if (!video) return;
|
|
529
|
+
if (elementReported && video.videoWidth > 0) {
|
|
530
|
+
videoConfirmed = true;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (video.readyState < HAVE_METADATA) return;
|
|
534
|
+
const videoTracks = video.videoTracks;
|
|
535
|
+
const audioTracks = video.audioTracks;
|
|
536
|
+
if (!isTrackList(videoTracks)) return;
|
|
537
|
+
if (videoTracks.length > 0) {
|
|
538
|
+
videoConfirmed = true;
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (isTrackList(audioTracks) && audioTracks.length > 0) {
|
|
542
|
+
audioConfirmed = true;
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
const evaluate = () => {
|
|
546
|
+
if (destroyed) return;
|
|
547
|
+
readElement();
|
|
548
|
+
publish();
|
|
549
|
+
};
|
|
550
|
+
const detachElement = () => {
|
|
551
|
+
for (const off of elementDisposers) off();
|
|
552
|
+
elementDisposers = [];
|
|
553
|
+
element = null;
|
|
554
|
+
};
|
|
555
|
+
return {
|
|
556
|
+
beginSource(src) {
|
|
557
|
+
if (destroyed) return;
|
|
558
|
+
if (source === src) return;
|
|
559
|
+
source = src;
|
|
560
|
+
videoConfirmed = false;
|
|
561
|
+
audioConfirmed = false;
|
|
562
|
+
elementReported = false;
|
|
563
|
+
published = null;
|
|
564
|
+
publish();
|
|
565
|
+
},
|
|
566
|
+
attach(video) {
|
|
567
|
+
if (destroyed) return;
|
|
568
|
+
detachElement();
|
|
569
|
+
element = video;
|
|
570
|
+
elementReported = false;
|
|
571
|
+
for (const event of ELEMENT_EVENTS) {
|
|
572
|
+
const handler = () => {
|
|
573
|
+
elementReported = true;
|
|
574
|
+
evaluate();
|
|
575
|
+
};
|
|
576
|
+
video.addEventListener(event, handler);
|
|
577
|
+
elementDisposers.push(() => video.removeEventListener(event, handler));
|
|
578
|
+
}
|
|
579
|
+
for (const list of [element.videoTracks, element.audioTracks]) {
|
|
580
|
+
if (!isTrackList(list) || typeof list.addEventListener !== "function") continue;
|
|
581
|
+
for (const event of TRACK_LIST_EVENTS) {
|
|
582
|
+
const handler = () => evaluate();
|
|
583
|
+
list.addEventListener(event, handler);
|
|
584
|
+
elementDisposers.push(() => list.removeEventListener?.(event, handler));
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
evaluate();
|
|
588
|
+
},
|
|
589
|
+
noteManifestParsed(data) {
|
|
590
|
+
if (destroyed || !data) return;
|
|
591
|
+
const hasVideo = typeof data.video === "boolean" ? data.video : null;
|
|
592
|
+
const hasAudio = typeof data.audio === "boolean" ? data.audio : null;
|
|
593
|
+
if (hasVideo === true) {
|
|
594
|
+
videoConfirmed = true;
|
|
595
|
+
} else if (hasVideo === false && hasAudio === true) {
|
|
596
|
+
audioConfirmed = true;
|
|
597
|
+
}
|
|
598
|
+
publish();
|
|
599
|
+
},
|
|
600
|
+
evaluate,
|
|
601
|
+
current() {
|
|
602
|
+
return decide();
|
|
603
|
+
},
|
|
604
|
+
destroy() {
|
|
605
|
+
if (destroyed) return;
|
|
606
|
+
destroyed = true;
|
|
607
|
+
detachElement();
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
505
612
|
// src/playlist-validation.ts
|
|
506
613
|
var PLAYLIST_INVALID_TEXT = "Invalid playlist document";
|
|
507
614
|
var MEDIA_PLAYLIST_CONTEXTS = ["level", "audioTrack", "subtitleTrack"];
|
|
@@ -591,6 +698,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
591
698
|
let cleanupHlsEvents = null;
|
|
592
699
|
let cleanupVideoEvents = null;
|
|
593
700
|
let isAutoQuality = true;
|
|
701
|
+
let mediaTypeClassifier = null;
|
|
594
702
|
let lastLevelDetails = null;
|
|
595
703
|
let lastLiveMetrics = null;
|
|
596
704
|
let lastKnownTargetLatency = null;
|
|
@@ -948,6 +1056,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
948
1056
|
if (api) {
|
|
949
1057
|
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics);
|
|
950
1058
|
}
|
|
1059
|
+
mediaTypeClassifier?.beginSource(src);
|
|
1060
|
+
mediaTypeClassifier?.attach(videoEl);
|
|
951
1061
|
return new Promise((resolve, reject) => {
|
|
952
1062
|
let watchdog = null;
|
|
953
1063
|
let settled = false;
|
|
@@ -1041,6 +1151,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1041
1151
|
if (api) {
|
|
1042
1152
|
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics);
|
|
1043
1153
|
}
|
|
1154
|
+
mediaTypeClassifier?.beginSource(src);
|
|
1155
|
+
mediaTypeClassifier?.attach(videoEl);
|
|
1044
1156
|
return new Promise((resolve, reject) => {
|
|
1045
1157
|
if (!hls || !api) {
|
|
1046
1158
|
reject(new Error("HLS not initialized"));
|
|
@@ -1067,8 +1179,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1067
1179
|
}
|
|
1068
1180
|
};
|
|
1069
1181
|
cleanupHlsEvents = setupHlsEventHandlers(hls, api, {
|
|
1070
|
-
onManifestParsed: () => {
|
|
1182
|
+
onManifestParsed: (_levels, manifestData) => {
|
|
1071
1183
|
if (session !== loadSession) return;
|
|
1184
|
+
mediaTypeClassifier?.noteManifestParsed(manifestData);
|
|
1072
1185
|
if (!resolved) {
|
|
1073
1186
|
resolved = true;
|
|
1074
1187
|
releaseAbort();
|
|
@@ -1327,6 +1440,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1327
1440
|
async init(pluginApi) {
|
|
1328
1441
|
api = pluginApi;
|
|
1329
1442
|
api.logger.info(`HLS plugin${variant.logSuffix} initialized`);
|
|
1443
|
+
mediaTypeClassifier = createMediaTypeClassifier(api);
|
|
1330
1444
|
const unsubPlay = api.on("playback:play", async () => {
|
|
1331
1445
|
if (!video) return;
|
|
1332
1446
|
try {
|
|
@@ -1462,6 +1576,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1462
1576
|
onlineListener = null;
|
|
1463
1577
|
}
|
|
1464
1578
|
cleanup(new Error("HLS load cancelled: player destroyed"));
|
|
1579
|
+
mediaTypeClassifier?.destroy();
|
|
1580
|
+
mediaTypeClassifier = null;
|
|
1465
1581
|
if (video?.parentNode) {
|
|
1466
1582
|
video.parentNode.removeChild(video);
|
|
1467
1583
|
}
|
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];
|
|
@@ -555,12 +555,6 @@ function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
|
555
555
|
});
|
|
556
556
|
addHandler("loadedmetadata", () => {
|
|
557
557
|
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
558
|
});
|
|
565
559
|
addHandler("error", () => {
|
|
566
560
|
const error = video.error;
|
|
@@ -602,6 +596,119 @@ function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
|
602
596
|
};
|
|
603
597
|
}
|
|
604
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
|
+
|
|
605
712
|
// src/playlist-validation.ts
|
|
606
713
|
var PLAYLIST_INVALID_TEXT = "Invalid playlist document";
|
|
607
714
|
var MEDIA_PLAYLIST_CONTEXTS = ["level", "audioTrack", "subtitleTrack"];
|
|
@@ -691,6 +798,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
691
798
|
let cleanupHlsEvents = null;
|
|
692
799
|
let cleanupVideoEvents = null;
|
|
693
800
|
let isAutoQuality = true;
|
|
801
|
+
let mediaTypeClassifier = null;
|
|
694
802
|
let lastLevelDetails = null;
|
|
695
803
|
let lastLiveMetrics = null;
|
|
696
804
|
let lastKnownTargetLatency = null;
|
|
@@ -1048,6 +1156,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1048
1156
|
if (api) {
|
|
1049
1157
|
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics);
|
|
1050
1158
|
}
|
|
1159
|
+
mediaTypeClassifier?.beginSource(src);
|
|
1160
|
+
mediaTypeClassifier?.attach(videoEl);
|
|
1051
1161
|
return new Promise((resolve, reject) => {
|
|
1052
1162
|
let watchdog = null;
|
|
1053
1163
|
let settled = false;
|
|
@@ -1141,6 +1251,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1141
1251
|
if (api) {
|
|
1142
1252
|
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics);
|
|
1143
1253
|
}
|
|
1254
|
+
mediaTypeClassifier?.beginSource(src);
|
|
1255
|
+
mediaTypeClassifier?.attach(videoEl);
|
|
1144
1256
|
return new Promise((resolve, reject) => {
|
|
1145
1257
|
if (!hls || !api) {
|
|
1146
1258
|
reject(new Error("HLS not initialized"));
|
|
@@ -1167,8 +1279,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1167
1279
|
}
|
|
1168
1280
|
};
|
|
1169
1281
|
cleanupHlsEvents = setupHlsEventHandlers(hls, api, {
|
|
1170
|
-
onManifestParsed: () => {
|
|
1282
|
+
onManifestParsed: (_levels, manifestData) => {
|
|
1171
1283
|
if (session !== loadSession) return;
|
|
1284
|
+
mediaTypeClassifier?.noteManifestParsed(manifestData);
|
|
1172
1285
|
if (!resolved) {
|
|
1173
1286
|
resolved = true;
|
|
1174
1287
|
releaseAbort();
|
|
@@ -1427,6 +1540,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1427
1540
|
async init(pluginApi) {
|
|
1428
1541
|
api = pluginApi;
|
|
1429
1542
|
api.logger.info(`HLS plugin${variant.logSuffix} initialized`);
|
|
1543
|
+
mediaTypeClassifier = createMediaTypeClassifier(api);
|
|
1430
1544
|
const unsubPlay = api.on("playback:play", async () => {
|
|
1431
1545
|
if (!video) return;
|
|
1432
1546
|
try {
|
|
@@ -1562,6 +1676,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1562
1676
|
onlineListener = null;
|
|
1563
1677
|
}
|
|
1564
1678
|
cleanup(new Error("HLS load cancelled: player destroyed"));
|
|
1679
|
+
mediaTypeClassifier?.destroy();
|
|
1680
|
+
mediaTypeClassifier = null;
|
|
1565
1681
|
if (video?.parentNode) {
|
|
1566
1682
|
video.parentNode.removeChild(video);
|
|
1567
1683
|
}
|
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];
|
|
@@ -555,12 +555,6 @@ function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
|
555
555
|
});
|
|
556
556
|
addHandler("loadedmetadata", () => {
|
|
557
557
|
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
558
|
});
|
|
565
559
|
addHandler("error", () => {
|
|
566
560
|
const error = video.error;
|
|
@@ -602,6 +596,119 @@ function setupVideoEventHandlers(video, api, getLiveMetrics) {
|
|
|
602
596
|
};
|
|
603
597
|
}
|
|
604
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
|
+
|
|
605
712
|
// src/playlist-validation.ts
|
|
606
713
|
var PLAYLIST_INVALID_TEXT = "Invalid playlist document";
|
|
607
714
|
var MEDIA_PLAYLIST_CONTEXTS = ["level", "audioTrack", "subtitleTrack"];
|
|
@@ -691,6 +798,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
691
798
|
let cleanupHlsEvents = null;
|
|
692
799
|
let cleanupVideoEvents = null;
|
|
693
800
|
let isAutoQuality = true;
|
|
801
|
+
let mediaTypeClassifier = null;
|
|
694
802
|
let lastLevelDetails = null;
|
|
695
803
|
let lastLiveMetrics = null;
|
|
696
804
|
let lastKnownTargetLatency = null;
|
|
@@ -1048,6 +1156,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1048
1156
|
if (api) {
|
|
1049
1157
|
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics);
|
|
1050
1158
|
}
|
|
1159
|
+
mediaTypeClassifier?.beginSource(src);
|
|
1160
|
+
mediaTypeClassifier?.attach(videoEl);
|
|
1051
1161
|
return new Promise((resolve, reject) => {
|
|
1052
1162
|
let watchdog = null;
|
|
1053
1163
|
let settled = false;
|
|
@@ -1141,6 +1251,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1141
1251
|
if (api) {
|
|
1142
1252
|
cleanupVideoEvents = setupVideoEventHandlers(videoEl, api, readLiveMetrics);
|
|
1143
1253
|
}
|
|
1254
|
+
mediaTypeClassifier?.beginSource(src);
|
|
1255
|
+
mediaTypeClassifier?.attach(videoEl);
|
|
1144
1256
|
return new Promise((resolve, reject) => {
|
|
1145
1257
|
if (!hls || !api) {
|
|
1146
1258
|
reject(new Error("HLS not initialized"));
|
|
@@ -1167,8 +1279,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1167
1279
|
}
|
|
1168
1280
|
};
|
|
1169
1281
|
cleanupHlsEvents = setupHlsEventHandlers(hls, api, {
|
|
1170
|
-
onManifestParsed: () => {
|
|
1282
|
+
onManifestParsed: (_levels, manifestData) => {
|
|
1171
1283
|
if (session !== loadSession) return;
|
|
1284
|
+
mediaTypeClassifier?.noteManifestParsed(manifestData);
|
|
1172
1285
|
if (!resolved) {
|
|
1173
1286
|
resolved = true;
|
|
1174
1287
|
releaseAbort();
|
|
@@ -1427,6 +1540,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1427
1540
|
async init(pluginApi) {
|
|
1428
1541
|
api = pluginApi;
|
|
1429
1542
|
api.logger.info(`HLS plugin${variant.logSuffix} initialized`);
|
|
1543
|
+
mediaTypeClassifier = createMediaTypeClassifier(api);
|
|
1430
1544
|
const unsubPlay = api.on("playback:play", async () => {
|
|
1431
1545
|
if (!video) return;
|
|
1432
1546
|
try {
|
|
@@ -1562,6 +1676,8 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1562
1676
|
onlineListener = null;
|
|
1563
1677
|
}
|
|
1564
1678
|
cleanup(new Error("HLS load cancelled: player destroyed"));
|
|
1679
|
+
mediaTypeClassifier?.destroy();
|
|
1680
|
+
mediaTypeClassifier = null;
|
|
1565
1681
|
if (video?.parentNode) {
|
|
1566
1682
|
video.parentNode.removeChild(video);
|
|
1567
1683
|
}
|
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.13.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.13.0"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"video",
|