@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.
@@ -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.12.0" : "0.0.0-dev";
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?.pause();
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.12.0" : "0.0.0-dev";
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?.pause();
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
@@ -4,7 +4,7 @@ import {
4
4
  computeLiveMetrics,
5
5
  createHLSPluginWith,
6
6
  sanitizeUrl
7
- } from "./chunk-47MU7IVX.js";
7
+ } from "./chunk-MT5VV3QN.js";
8
8
 
9
9
  // src/hls-loader.ts
10
10
  var hls_loader_exports = {};
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.12.0" : "0.0.0-dev";
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?.pause();
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
@@ -4,7 +4,7 @@ import {
4
4
  computeLiveMetrics,
5
5
  createHLSPluginWith,
6
6
  sanitizeUrl
7
- } from "./chunk-47MU7IVX.js";
7
+ } from "./chunk-MT5VV3QN.js";
8
8
 
9
9
  // src/hls-loader-light.ts
10
10
  var hls_loader_light_exports = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scarlett-player/hls",
3
- "version": "1.12.0",
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.12.0"
40
+ "@scarlett-player/core": "1.14.0"
41
41
  },
42
42
  "keywords": [
43
43
  "video",