@blockcast/mmt-container 2.1.0 → 2.2.0-main.7a01a10f6629

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.
@@ -377,6 +377,1412 @@ var RUNTIME_FEC_RECEIVER = Object.freeze({
377
377
  overlays: true
378
378
  });
379
379
 
380
+ // node_modules/@blockcast/mmt-transport/dist/multicast-tier.js
381
+ function endpointKey(endpoint) {
382
+ return `${endpoint.sourceAddress ?? ""}|${endpoint.groupAddress}|${endpoint.port}`;
383
+ }
384
+ function carriesMedia(endpoint) {
385
+ return endpoint.carriesMedia !== false;
386
+ }
387
+ function selectMulticastTier(endpoints, estimate) {
388
+ const media = endpoints.filter(carriesMedia);
389
+ if (media.length <= 1)
390
+ return media[0];
391
+ const ranked = media.filter((endpoint) => typeof endpoint.bandwidth === "number" && endpoint.bandwidth > 0).sort((a, b) => a.bandwidth - b.bandwidth);
392
+ if (ranked.length < 2)
393
+ return media[0];
394
+ if (estimate === void 0)
395
+ return ranked[0];
396
+ const budget = estimate * 0.8;
397
+ let selected;
398
+ for (const endpoint of ranked) {
399
+ if (endpoint.bandwidth <= budget)
400
+ selected = endpoint;
401
+ }
402
+ return selected ?? ranked[0];
403
+ }
404
+
405
+ // node_modules/@blockcast/mmt-transport/dist/abr-controller.js
406
+ var QUALITY_LEVELS = Object.freeze([
407
+ { name: "240p", bitrate: 3e5, width: 426, height: 240, fps: 15 },
408
+ { name: "360p", bitrate: 5e5, width: 640, height: 360, fps: 24 },
409
+ { name: "480p", bitrate: 1e6, width: 854, height: 480, fps: 30 },
410
+ { name: "720p", bitrate: 25e5, width: 1280, height: 720, fps: 30 },
411
+ { name: "1080p", bitrate: 5e6, width: 1920, height: 1080, fps: 30 },
412
+ { name: "1440p", bitrate: 8e6, width: 2560, height: 1440, fps: 30 },
413
+ { name: "4K", bitrate: 15e6, width: 3840, height: 2160, fps: 30 }
414
+ ].map((level) => Object.freeze(level)));
415
+ function positiveFinite(value) {
416
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
417
+ }
418
+ function validateQualityLadder(ladder) {
419
+ if (!Array.isArray(ladder) || ladder.length === 0) {
420
+ throw new TypeError("[ABR] ladder must list at least one rendition (e.g. the catalog's video tracks)");
421
+ }
422
+ const names = /* @__PURE__ */ new Set();
423
+ for (const level of ladder) {
424
+ if (typeof level?.name !== "string" || level.name.length === 0) {
425
+ throw new TypeError("[ABR] every ladder rendition needs a name");
426
+ }
427
+ if (!positiveFinite(level.bitrate)) {
428
+ throw new RangeError(`[ABR] rendition ${level.name}: bitrate must be a positive finite number, got ${String(level.bitrate)}`);
429
+ }
430
+ for (const field of ["width", "height", "fps"]) {
431
+ if (level[field] !== void 0 && !positiveFinite(level[field])) {
432
+ throw new RangeError(`[ABR] rendition ${level.name}: ${field} must be a positive finite number when stated, got ${String(level[field])}`);
433
+ }
434
+ }
435
+ if (names.has(level.name))
436
+ throw new RangeError(`[ABR] rendition ${level.name} appears twice in the ladder`);
437
+ names.add(level.name);
438
+ }
439
+ return Object.freeze([...ladder].sort((a, b) => a.bitrate - b.bitrate).map((level) => Object.freeze({ ...level })));
440
+ }
441
+ var AdaptiveBitrateController = class {
442
+ /** The caller's renditions, ascending by bitrate (`validateQualityLadder`). */
443
+ ladder;
444
+ currentQualityIndex;
445
+ history = [];
446
+ historyWindow = 1e4;
447
+ // 10 second window
448
+ upgradeThreshold = 0.8;
449
+ // 80% of target bitrate sustained
450
+ downgradeThreshold = 0.5;
451
+ // 50% of target bitrate triggers downgrade
452
+ stabilityPeriod = 5e3;
453
+ // Wait 5s before upgrading
454
+ lastQualityChange = 0;
455
+ onQualityChange;
456
+ // FEC stats for effective loss rate calculation
457
+ fecStats = {
458
+ rawLossRate: 0,
459
+ effectiveLossRate: 0,
460
+ recoveryRate: 1,
461
+ fecEnabled: false
462
+ };
463
+ constructor(options) {
464
+ if (!options || typeof options !== "object") {
465
+ throw new TypeError("[ABR] AdaptiveBitrateController needs { ladder, initialQuality }");
466
+ }
467
+ this.ladder = validateQualityLadder(options.ladder);
468
+ this.currentQualityIndex = options.initialQuality === void 0 ? 0 : this.ladder.findIndex((q) => q.name === options.initialQuality);
469
+ if (this.currentQualityIndex < 0) {
470
+ throw new RangeError(`[ABR] initial rendition ${String(options.initialQuality)} is not in the ladder`);
471
+ }
472
+ }
473
+ setCallback(callback) {
474
+ this.onQualityChange = callback;
475
+ }
476
+ getCurrentQuality() {
477
+ return this.ladder[this.currentQualityIndex];
478
+ }
479
+ /** Selects the multicast (S,G) tier without replacing a player's unicast ABR manager. */
480
+ selectMulticastTier(endpoints, estimate = this.getMetrics().measuredBitrate || void 0) {
481
+ return selectMulticastTier(endpoints, estimate);
482
+ }
483
+ /**
484
+ * Record network sample for bitrate adaptation
485
+ *
486
+ * @param bytesReceived - Bytes received in this sample
487
+ * @param packetsLost - Packets lost (raw, before FEC recovery)
488
+ */
489
+ recordSample(bytesReceived, packetsLost) {
490
+ const now = Date.now();
491
+ this.history.push({ timestamp: now, bytesReceived, packetsLost });
492
+ this.history = this.history.filter((s) => now - s.timestamp < this.historyWindow);
493
+ const { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();
494
+ const currentQuality = this.getCurrentQuality();
495
+ const effectiveLossRate = this.fecStats.fecEnabled ? this.fecStats.effectiveLossRate : rawLossRate;
496
+ if (now - this.lastQualityChange < this.stabilityPeriod) {
497
+ return;
498
+ }
499
+ if (effectiveLossRate > 0.05 || measuredBitrate < currentQuality.bitrate * this.downgradeThreshold) {
500
+ this.downgrade();
501
+ return;
502
+ }
503
+ if (measuredBitrate > currentQuality.bitrate * this.upgradeThreshold && effectiveLossRate < 0.01 && this.currentQualityIndex < this.ladder.length - 1) {
504
+ this.upgrade();
505
+ }
506
+ }
507
+ /**
508
+ * Update FEC statistics for ABR decisions
509
+ *
510
+ * ABR uses the effective loss rate (after FEC recovery) rather than the
511
+ * raw transport loss rate.
512
+ *
513
+ * @param stats - FEC statistics from the FEC decoder
514
+ */
515
+ setFecStats(stats) {
516
+ this.fecStats = stats;
517
+ }
518
+ /**
519
+ * Get current FEC statistics
520
+ */
521
+ getFecStats() {
522
+ return { ...this.fecStats };
523
+ }
524
+ calculateMetrics() {
525
+ if (this.history.length < 2) {
526
+ return { measuredBitrate: 0, lossRate: 0 };
527
+ }
528
+ const totalBytes = this.history.reduce((sum, s) => sum + s.bytesReceived, 0);
529
+ const totalLost = this.history.reduce((sum, s) => sum + s.packetsLost, 0);
530
+ const duration = (this.history[this.history.length - 1].timestamp - this.history[0].timestamp) / 1e3;
531
+ const measuredBitrate = duration > 0 ? totalBytes * 8 / duration : 0;
532
+ const lossRate = totalLost / (this.history.length + totalLost);
533
+ return { measuredBitrate, lossRate };
534
+ }
535
+ upgrade() {
536
+ if (this.currentQualityIndex >= this.ladder.length - 1)
537
+ return;
538
+ this.currentQualityIndex++;
539
+ this.lastQualityChange = Date.now();
540
+ const newQuality = this.getCurrentQuality();
541
+ console.log(`[ABR] Upgrading to ${newQuality.name} (${newQuality.bitrate / 1e6}Mbps)`);
542
+ this.onQualityChange?.(newQuality);
543
+ }
544
+ downgrade() {
545
+ if (this.currentQualityIndex <= 0)
546
+ return;
547
+ this.currentQualityIndex--;
548
+ this.lastQualityChange = Date.now();
549
+ const newQuality = this.getCurrentQuality();
550
+ console.log(`[ABR] Downgrading to ${newQuality.name} (${newQuality.bitrate / 1e6}Mbps)`);
551
+ this.onQualityChange?.(newQuality);
552
+ }
553
+ /**
554
+ * Select a ladder rendition: the caller switched to it (a manual override,
555
+ * or a switch the caller made). Throws for a name the ladder does not carry.
556
+ */
557
+ setQuality(name) {
558
+ const index = this.ladder.findIndex((q) => q.name === name);
559
+ if (index < 0)
560
+ throw new RangeError(`[ABR] rendition ${name} is not in the ladder`);
561
+ this.currentQualityIndex = index;
562
+ this.lastQualityChange = Date.now();
563
+ this.onQualityChange?.(this.getCurrentQuality());
564
+ }
565
+ /**
566
+ * Get current metrics including both raw and effective loss rates
567
+ */
568
+ getMetrics() {
569
+ const { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();
570
+ return {
571
+ measuredBitrate,
572
+ rawLossRate,
573
+ effectiveLossRate: this.fecStats.fecEnabled ? this.fecStats.effectiveLossRate : rawLossRate,
574
+ fecEnabled: this.fecStats.fecEnabled,
575
+ sampleCount: this.history.length
576
+ };
577
+ }
578
+ /**
579
+ * Reset history and start fresh
580
+ */
581
+ reset() {
582
+ this.history = [];
583
+ this.lastQualityChange = 0;
584
+ }
585
+ };
586
+
587
+ // node_modules/@blockcast/mmt-base/dist/mmtp.js
588
+ var MPU_HEADER_SIZE2 = 8;
589
+ var MPU_LENGTH_FIELD_SIZE2 = 2;
590
+ var MPU_HEADER_REMAINDER_SIZE2 = MPU_HEADER_SIZE2 - MPU_LENGTH_FIELD_SIZE2;
591
+ var MMTP_SIGNALING_PACKET_ID2 = 0;
592
+ var MMTP_PACKET_ID_MAX = 65535;
593
+
594
+ // node_modules/@blockcast/mmt-base/dist/net-constants.js
595
+ var DEFAULT_AMT_RELAY_PORT = 2268;
596
+
597
+ // node_modules/@blockcast/mmt-base/dist/ntp-short.js
598
+ var NTP_SHORT_BITS2 = 32;
599
+ var NTP_SHORT_MODULUS2 = 2 ** NTP_SHORT_BITS2;
600
+ var NTP_SHORT_HALF_RANGE2 = 2 ** (NTP_SHORT_BITS2 - 1);
601
+ function isNtpShort(value) {
602
+ return Number.isInteger(value) && value >= 0 && value < NTP_SHORT_MODULUS2;
603
+ }
604
+ function compareNtpShort(a, b) {
605
+ if (!isNtpShort(a) || !isNtpShort(b)) {
606
+ throw new RangeError(`NTP-short values must be integers in [0, 2^${NTP_SHORT_BITS2}), got ${String(a)} and ${String(b)}`);
607
+ }
608
+ if (a === b)
609
+ return "equal";
610
+ const forward = (a - b + NTP_SHORT_MODULUS2) % NTP_SHORT_MODULUS2;
611
+ if (forward < NTP_SHORT_HALF_RANGE2)
612
+ return "after";
613
+ if (forward > NTP_SHORT_HALF_RANGE2)
614
+ return "before";
615
+ return "unordered";
616
+ }
617
+
618
+ // node_modules/@blockcast/mmt-base/dist/fec-block-profile.js
619
+ var FEC_BLOCK_PROFILES2 = Object.freeze([
620
+ "iso-ssbg1-v1"
621
+ ]);
622
+
623
+ // node_modules/@blockcast/mmt-base/dist/fec-bindings.js
624
+ var RUNTIME_FEC_RECEIVER2 = Object.freeze({
625
+ schemes: Object.freeze(["raptorq", "reed-solomon"]),
626
+ blockProfiles: Object.freeze(["iso-ssbg1-v1"]),
627
+ overlays: true
628
+ });
629
+
630
+ // node_modules/@blockcast/mmt-transport/dist/multicast-orchestrator.js
631
+ var DEFAULT_DWELL_MS = 3e3;
632
+ var DEFAULT_JOIN_DEADLINE_MS = 5e3;
633
+ var MulticastOrchestrator = class {
634
+ options;
635
+ #state = "stable";
636
+ #current;
637
+ #target;
638
+ #estimate;
639
+ #favored;
640
+ #joinStartedAt = 0;
641
+ /**
642
+ * Latest active-tier send time (NTP short), or undefined before the first.
643
+ * "Latest" is serial order, not the numerically largest value: after the
644
+ * field wraps, the largest value is the stale one.
645
+ */
646
+ #highWater;
647
+ #dwellMs;
648
+ #joinDeadlineMs;
649
+ constructor(options) {
650
+ this.options = options;
651
+ this.#dwellMs = options.dwellMs ?? DEFAULT_DWELL_MS;
652
+ this.#joinDeadlineMs = options.joinDeadlineMs ?? DEFAULT_JOIN_DEADLINE_MS;
653
+ if (!Number.isFinite(this.#dwellMs) || !Number.isFinite(this.#joinDeadlineMs) || this.#dwellMs <= 0 || this.#joinDeadlineMs <= 0) {
654
+ throw new Error("Multicast dwell and join deadline must be positive");
655
+ }
656
+ }
657
+ get state() {
658
+ return this.#state;
659
+ }
660
+ get current() {
661
+ return this.#current;
662
+ }
663
+ get target() {
664
+ return this.#target;
665
+ }
666
+ get estimate() {
667
+ return this.#estimate;
668
+ }
669
+ start() {
670
+ this.#current ??= this.options.initialEndpoint ?? selectMulticastTier(this.options.endpoints, void 0);
671
+ return this.#current;
672
+ }
673
+ onGoodput(sample) {
674
+ const bitrate = sample.bitrate;
675
+ if (!this.#current)
676
+ return void 0;
677
+ if (this.#state === "joining")
678
+ return this.poll();
679
+ if (this.options.adaptive === false)
680
+ return void 0;
681
+ if (bitrate === void 0 || !Number.isFinite(bitrate))
682
+ return void 0;
683
+ this.#estimate = bitrate;
684
+ const desired = selectMulticastTier(this.options.endpoints, bitrate);
685
+ if (!desired)
686
+ return void 0;
687
+ const desiredKey = endpointKey(desired);
688
+ if (desiredKey === endpointKey(this.#current)) {
689
+ if (this.#favored?.source === "goodput")
690
+ this.#favored = void 0;
691
+ return void 0;
692
+ }
693
+ if (typeof desired.bandwidth !== "number" || typeof this.#current.bandwidth !== "number" || desired.bandwidth >= this.#current.bandwidth) {
694
+ if (this.#favored?.source === "goodput")
695
+ this.#favored = void 0;
696
+ return void 0;
697
+ }
698
+ const now = this.#now();
699
+ if (!this.#favored || this.#favored.key !== desiredKey || this.#favored.source !== "goodput") {
700
+ this.#favored = { key: desiredKey, since: now, source: "goodput" };
701
+ return void 0;
702
+ }
703
+ if (now - this.#favored.since < this.#dwellMs)
704
+ return void 0;
705
+ this.#state = "joining";
706
+ this.#target = desired;
707
+ this.#joinStartedAt = now;
708
+ this.#favored = void 0;
709
+ return { kind: "join-target", current: this.#current, target: desired };
710
+ }
711
+ onHeadroom(sample) {
712
+ if (!this.#current || this.options.adaptive === false || this.#state === "joining")
713
+ return void 0;
714
+ if (!sample.healthy) {
715
+ if (this.#favored?.source === "headroom")
716
+ this.#favored = void 0;
717
+ return void 0;
718
+ }
719
+ if (this.#favored?.source === "goodput")
720
+ return void 0;
721
+ const currentBandwidth = this.#current.bandwidth;
722
+ if (typeof currentBandwidth !== "number")
723
+ return void 0;
724
+ const desired = this.options.endpoints.filter((endpoint) => carriesMedia(endpoint) && typeof endpoint.bandwidth === "number" && endpoint.bandwidth > currentBandwidth).sort((a, b) => a.bandwidth - b.bandwidth)[0];
725
+ if (!desired) {
726
+ this.#favored = void 0;
727
+ return void 0;
728
+ }
729
+ const desiredKey = endpointKey(desired);
730
+ if (desiredKey === endpointKey(this.#current)) {
731
+ this.#favored = void 0;
732
+ return void 0;
733
+ }
734
+ const now = this.#now();
735
+ if (!this.#favored || this.#favored.key !== desiredKey) {
736
+ this.#favored = { key: desiredKey, since: now, source: "headroom" };
737
+ return void 0;
738
+ }
739
+ if (now - this.#favored.since < this.#dwellMs)
740
+ return void 0;
741
+ this.#state = "joining";
742
+ this.#target = desired;
743
+ this.#joinStartedAt = now;
744
+ this.#favored = void 0;
745
+ return { kind: "join-target", current: this.#current, target: desired };
746
+ }
747
+ /**
748
+ * Join `target` because the viewer selected it, not because goodput or
749
+ * headroom ranked it. It runs whether or not the orchestrator adapts, and
750
+ * takes the path an adaptive join takes: commit on the target's RAP at or
751
+ * beyond the high-water mark, abandon at the join deadline. While it is
752
+ * pending nothing re-ranks the ladder; goodput only advances the deadline.
753
+ *
754
+ * Returns undefined, changing nothing, while another join is pending (cancel
755
+ * it first), when `target` is the current tier, or when it carries no media.
756
+ */
757
+ select(target) {
758
+ if (!this.#current || this.#state === "joining" || !carriesMedia(target))
759
+ return void 0;
760
+ if (endpointKey(target) === endpointKey(this.#current))
761
+ return void 0;
762
+ this.#state = "joining";
763
+ this.#target = target;
764
+ this.#joinStartedAt = this.#now();
765
+ this.#favored = void 0;
766
+ return { kind: "join-target", current: this.#current, target };
767
+ }
768
+ /**
769
+ * Feed a packet from the active tier (to advance the high-water mark) or
770
+ * from the in-flight target (to find a committable RAP).
771
+ *
772
+ * Serial order is only the true order while the compared instants are less
773
+ * than `NTP_SHORT_HALF_RANGE` (2^15 s, ~9.1 h) apart. Feeding every
774
+ * active-tier packet keeps the mark that close to the live stream. A mark
775
+ * refreshed only by commits can fall further behind than that, and a
776
+ * target RAP may then compare as older, so the join is abandoned.
777
+ */
778
+ onTierPacket(packet) {
779
+ if (!this.#current)
780
+ return void 0;
781
+ const sentAt = tierPacketSendTime(packet);
782
+ if (packet.endpointKey === endpointKey(this.#current) && sentAt !== void 0 && (this.#highWater === void 0 || compareNtpShort(sentAt, this.#highWater) === "after")) {
783
+ this.#highWater = sentAt;
784
+ }
785
+ if (this.#state !== "joining" || !this.#target)
786
+ return void 0;
787
+ const expired = this.poll();
788
+ if (expired)
789
+ return expired;
790
+ if (packet.endpointKey !== endpointKey(this.#target) || !packet.keyframe || !packet.hasValidCodecConfig || sentAt === void 0 || !this.#reachesHighWater(sentAt))
791
+ return void 0;
792
+ const command = {
793
+ kind: "commit-swap",
794
+ target: this.#target,
795
+ old: this.#current
796
+ };
797
+ this.#highWater = sentAt;
798
+ this.#current = this.#target;
799
+ this.#target = void 0;
800
+ this.#state = "stable";
801
+ return command;
802
+ }
803
+ poll() {
804
+ if (this.#state !== "joining" || !this.#target || !this.#current)
805
+ return void 0;
806
+ if (this.#now() - this.#joinStartedAt < this.#joinDeadlineMs)
807
+ return void 0;
808
+ return this.cancel();
809
+ }
810
+ cancel() {
811
+ if (this.#state !== "joining" || !this.#target || !this.#current)
812
+ return void 0;
813
+ const command = {
814
+ kind: "abandon-join",
815
+ current: this.#current,
816
+ target: this.#target
817
+ };
818
+ this.#target = void 0;
819
+ this.#state = "stable";
820
+ return command;
821
+ }
822
+ /**
823
+ * Whether a target RAP sent at `sentAt` is at or beyond the active tier's
824
+ * high-water mark. A pair exactly half the NTP-short space apart has no
825
+ * serial order, so it cannot show that and does not commit.
826
+ */
827
+ #reachesHighWater(sentAt) {
828
+ if (this.#highWater === void 0)
829
+ return true;
830
+ const order = compareNtpShort(sentAt, this.#highWater);
831
+ return order === "after" || order === "equal";
832
+ }
833
+ #now() {
834
+ const now = this.options.now();
835
+ if (!Number.isFinite(now))
836
+ throw new Error("Multicast orchestrator clock must be finite");
837
+ return now;
838
+ }
839
+ };
840
+ function tierPacketSendTime(packet) {
841
+ if (!Number.isFinite(packet.timestamp))
842
+ return void 0;
843
+ if (!isNtpShort(packet.timestamp)) {
844
+ throw new RangeError(`Multicast tier packet timestamp must be an MMTP NTP-short send time, got ${packet.timestamp}`);
845
+ }
846
+ return packet.timestamp;
847
+ }
848
+
849
+ // node_modules/@blockcast/mmt-transport/dist/endpoint-config.js
850
+ function toWindowMulticastSubscribeConfig(input) {
851
+ if (input.mode !== void 0 && !["auto", "native", "tunnel"].includes(input.mode)) {
852
+ throw new TypeError(`Unsupported multicast mode: ${String(input.mode)}`);
853
+ }
854
+ if (input.packetIds !== void 0 && !Array.isArray(input.packetIds)) {
855
+ throw new TypeError(`multicast packetIds must be an array, got ${input.packetIds === null ? "null" : typeof input.packetIds}`);
856
+ }
857
+ if (input.packetIds?.some((id) => !isMulticastTrackPacketId(id)) || input.tracks?.some((track) => !isMulticastTrackPacketId(track.packetId))) {
858
+ throw new RangeError(`multicast packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}; packetId 0 is the MMTP signaling flow and is never a track's`);
859
+ }
860
+ const source = input.source?.trim() || void 0;
861
+ const senderAddress = input.senderAddress?.trim() || void 0;
862
+ if (source && senderAddress) {
863
+ throw new TypeError("multicast senderAddress is an ASM filter and never appears beside source");
864
+ }
865
+ const base = {
866
+ group: input.group,
867
+ port: input.port,
868
+ ...source ? { source } : {},
869
+ consume: "bytes",
870
+ ...input.packetIds?.length ? { packetIds: [...input.packetIds] } : {}
871
+ };
872
+ const filter = senderAddress ? { senderAddress } : {};
873
+ if (input.atsc3) {
874
+ if (senderAddress)
875
+ throw new TypeError("an ATSC3 tuner request carries no senderAddress");
876
+ const { frequency, plpId, serviceId } = input.atsc3;
877
+ if (!Number.isInteger(frequency) || frequency < 1) {
878
+ throw new TypeError(`ATSC3 tuning requires a positive integer frequency in kHz, got ${String(frequency)}`);
879
+ }
880
+ return {
881
+ ...base,
882
+ origin: "tuner",
883
+ standard: "atsc3",
884
+ frequency,
885
+ ...plpId !== void 0 ? { plpId } : {},
886
+ ...serviceId !== void 0 ? { serviceId } : {}
887
+ };
888
+ }
889
+ const relay = input.amtRelay?.trim();
890
+ const driad = !relay && input.amtDiscovery === "driad";
891
+ const wantsAmt = input.mode === "tunnel" || input.mode !== "native" && (relay !== void 0 && relay !== "" || driad);
892
+ if (!wantsAmt) {
893
+ const native = {
894
+ ...base,
895
+ ...filter,
896
+ origin: "ip-multicast",
897
+ ...input.tracks?.length ? { tracks: input.tracks.map(({ name, packetId }) => ({ name, packetId })) } : {}
898
+ };
899
+ return native;
900
+ }
901
+ const relayPort = input.amtRelayPort;
902
+ if (driad) {
903
+ if (!base.source)
904
+ throw new TypeError("AMT DRIAD discovery requires a source address");
905
+ if (relayPort !== void 0 && !isPositivePort(relayPort)) {
906
+ throw new TypeError("AMT DRIAD discovery requires a valid relay port when one is given");
907
+ }
908
+ return {
909
+ ...base,
910
+ source: base.source,
911
+ origin: "amt",
912
+ discovery: "driad",
913
+ ...relayPort !== void 0 ? { relayPort } : {}
914
+ };
915
+ }
916
+ if (!relay || relayPort === void 0 || !isPositivePort(relayPort)) {
917
+ throw new TypeError("AMT requires a resolved relay and a valid relay port");
918
+ }
919
+ return {
920
+ ...base,
921
+ ...filter,
922
+ origin: "amt",
923
+ relay,
924
+ relayPort
925
+ };
926
+ }
927
+ function isValidMulticastGroupAddress(address) {
928
+ const value = address?.trim().toLowerCase();
929
+ if (!value)
930
+ return false;
931
+ if (value.startsWith("ff"))
932
+ return isValidIpv6MulticastAddress(value);
933
+ const parts = value.split(".");
934
+ if (parts.length !== 4)
935
+ return false;
936
+ const octets = parts.map((part) => {
937
+ if (!/^\d+$/.test(part))
938
+ return Number.NaN;
939
+ const octet = Number(part);
940
+ return Number.isInteger(octet) && octet >= 0 && octet <= 255 ? octet : Number.NaN;
941
+ });
942
+ return octets.every(Number.isInteger) && octets[0] >= 224 && octets[0] <= 239;
943
+ }
944
+ function normalizeMulticastEndpoint(endpoint) {
945
+ const groupAddress = endpoint.groupAddress.trim().toLowerCase();
946
+ const sourceAddress = endpoint.sourceAddress?.trim().toLowerCase() || void 0;
947
+ const senderAddress = endpoint.senderAddress?.trim().toLowerCase() || void 0;
948
+ if (sourceAddress && senderAddress)
949
+ return void 0;
950
+ const amtRelay = endpoint.amtRelay?.trim().toLowerCase() || void 0;
951
+ if (!isValidMulticastGroupAddress(groupAddress) || !isPositivePort(endpoint.port))
952
+ return void 0;
953
+ if (endpoint.mode && !["auto", "native", "tunnel"].includes(endpoint.mode))
954
+ return void 0;
955
+ if (endpoint.amtRelayPort !== void 0 && !isPositivePort(endpoint.amtRelayPort))
956
+ return void 0;
957
+ return {
958
+ groupAddress,
959
+ ...sourceAddress ? { sourceAddress } : {},
960
+ ...senderAddress ? { senderAddress } : {},
961
+ port: endpoint.port,
962
+ ...endpoint.mode ? { mode: endpoint.mode } : {},
963
+ ...amtRelay ? { amtRelay } : {},
964
+ ...endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {},
965
+ ...endpoint.amtDiscovery === "driad" ? { amtDiscovery: "driad" } : {}
966
+ };
967
+ }
968
+ function multicastEndpointKey(endpoint) {
969
+ const normalized = normalizeMulticastEndpoint(endpoint);
970
+ if (!normalized)
971
+ throw new Error("Invalid multicast endpoint");
972
+ return JSON.stringify([
973
+ normalized.sourceAddress ?? "",
974
+ normalized.groupAddress,
975
+ normalized.port,
976
+ normalized.mode ?? "auto",
977
+ normalized.amtRelay ?? "",
978
+ normalized.amtRelayPort ?? "",
979
+ // A DRIAD join is a different gateway subscription from a native one.
980
+ ...normalized.amtDiscovery ? [normalized.amtDiscovery] : [],
981
+ // An ASM sender filter is a different subscription from the unfiltered join.
982
+ ...normalized.senderAddress ? [{ senderAddress: normalized.senderAddress }] : []
983
+ ]);
984
+ }
985
+ function isMulticastTrackPacketId(value) {
986
+ return Number.isInteger(value) && value > MMTP_SIGNALING_PACKET_ID2 && value <= MMTP_PACKET_ID_MAX;
987
+ }
988
+ function isValidIpv6MulticastAddress(value) {
989
+ if (!/^[0-9a-f:]+$/.test(value))
990
+ return false;
991
+ const compressed = value.includes("::");
992
+ if (value.indexOf("::") !== value.lastIndexOf("::"))
993
+ return false;
994
+ const groups = value.split(":").filter(Boolean);
995
+ if (groups.length === 0 || groups.length > 8)
996
+ return false;
997
+ if (!compressed && groups.length !== 8)
998
+ return false;
999
+ return groups.every((group) => /^[0-9a-f]{1,4}$/.test(group));
1000
+ }
1001
+ function isPositivePort(port) {
1002
+ return Number.isInteger(port) && port > 0 && port <= 65535;
1003
+ }
1004
+
1005
+ // node_modules/@blockcast/mmt-transport/dist/packet-batch.js
1006
+ function subscriptionDeliversPacketBatches(subscription) {
1007
+ return subscription.eventTypes?.includes("packets") === true;
1008
+ }
1009
+
1010
+ // node_modules/@blockcast/mmt-transport/dist/multicast-subscription-pool.js
1011
+ var MulticastSubscriptionPool = class {
1012
+ #entries = /* @__PURE__ */ new Map();
1013
+ #gatewayIds = /* @__PURE__ */ new WeakMap();
1014
+ #nextGatewayId = 1;
1015
+ async acquire(gateway, config, consumer) {
1016
+ const endpoint = normalizeMulticastEndpoint({
1017
+ groupAddress: config.group,
1018
+ sourceAddress: config.source,
1019
+ port: config.port,
1020
+ mode: config.mode,
1021
+ amtRelay: config.amtRelay,
1022
+ amtRelayPort: config.amtRelayPort,
1023
+ amtDiscovery: config.amtDiscovery
1024
+ });
1025
+ if (!endpoint)
1026
+ throw new Error("Invalid multicast endpoint");
1027
+ if (typeof gateway.subscribe !== "function")
1028
+ throw new Error("window.multicast.subscribe unavailable");
1029
+ const key = multicastEndpointKey(endpoint);
1030
+ const internalKey = `${this.#gatewayId(gateway)}|${key}`;
1031
+ let record = this.#entries.get(internalKey);
1032
+ if (!record) {
1033
+ record = { claims: 0, entry: Promise.resolve(null) };
1034
+ const createdRecord = record;
1035
+ record.entry = this.#createEntry(gateway, endpoint, (entry2) => {
1036
+ if (this.#entries.get(internalKey) === createdRecord)
1037
+ this.#entries.delete(internalKey);
1038
+ this.#detach(entry2);
1039
+ }).catch((error) => {
1040
+ if (this.#entries.get(internalKey) === createdRecord)
1041
+ this.#entries.delete(internalKey);
1042
+ throw error;
1043
+ });
1044
+ this.#entries.set(internalKey, record);
1045
+ }
1046
+ record.claims++;
1047
+ let entry;
1048
+ try {
1049
+ entry = await record.entry;
1050
+ } catch (error) {
1051
+ record.claims--;
1052
+ throw error;
1053
+ }
1054
+ if (entry.closed) {
1055
+ record.claims--;
1056
+ return this.acquire(gateway, config, consumer);
1057
+ }
1058
+ const token = Symbol(key);
1059
+ entry.consumers.set(token, consumer);
1060
+ let released = false;
1061
+ return {
1062
+ key,
1063
+ release: async () => {
1064
+ if (released)
1065
+ return;
1066
+ released = true;
1067
+ entry.consumers.delete(token);
1068
+ record.claims--;
1069
+ if (record.claims > 0)
1070
+ return;
1071
+ if (this.#entries.get(internalKey) === record)
1072
+ this.#entries.delete(internalKey);
1073
+ this.#detach(entry);
1074
+ if (!entry.closed)
1075
+ await entry.subscription.unsubscribe();
1076
+ }
1077
+ };
1078
+ }
1079
+ get size() {
1080
+ return this.#entries.size;
1081
+ }
1082
+ async #createEntry(gateway, endpoint, onClosed) {
1083
+ const subscription = await gateway.subscribe(toWindowMulticastSubscribeConfig({
1084
+ group: endpoint.groupAddress,
1085
+ ...endpoint.sourceAddress ? { source: endpoint.sourceAddress } : {},
1086
+ port: endpoint.port,
1087
+ ...endpoint.mode ? { mode: endpoint.mode } : {},
1088
+ ...endpoint.amtRelay ? { amtRelay: endpoint.amtRelay } : {},
1089
+ ...endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {},
1090
+ ...endpoint.amtDiscovery ? { amtDiscovery: endpoint.amtDiscovery } : {}
1091
+ }));
1092
+ const consumers = /* @__PURE__ */ new Map();
1093
+ let entry;
1094
+ const packetListener = subscriptionDeliversPacketBatches(subscription) ? {
1095
+ type: "packets",
1096
+ listener: (event) => {
1097
+ for (const data of event.detail) {
1098
+ this.#dispatch(consumers, (consumer) => consumer.onBytes(data));
1099
+ }
1100
+ }
1101
+ } : {
1102
+ type: "bytes",
1103
+ listener: (event) => {
1104
+ this.#dispatch(consumers, (consumer) => consumer.onBytes(event.detail));
1105
+ }
1106
+ };
1107
+ const stateListener = (event) => {
1108
+ this.#dispatch(consumers, (consumer) => consumer.onState?.(event.detail));
1109
+ if (event.detail === "closed" && !entry.closed) {
1110
+ entry.closed = true;
1111
+ onClosed(entry);
1112
+ }
1113
+ };
1114
+ const errorListener = (event) => {
1115
+ const error = event.detail instanceof Error ? event.detail : new Error(String(event.detail));
1116
+ this.#dispatch(consumers, (consumer) => consumer.onError?.(error));
1117
+ };
1118
+ entry = { subscription, consumers, packetListener, stateListener, errorListener, closed: false };
1119
+ if (packetListener.type === "packets") {
1120
+ subscription.addEventListener("packets", packetListener.listener);
1121
+ } else {
1122
+ subscription.addEventListener("bytes", packetListener.listener);
1123
+ }
1124
+ subscription.addEventListener("state", stateListener);
1125
+ subscription.addEventListener("error", errorListener);
1126
+ return entry;
1127
+ }
1128
+ #gatewayId(gateway) {
1129
+ const object = gateway;
1130
+ let id = this.#gatewayIds.get(object);
1131
+ if (!id) {
1132
+ id = this.#nextGatewayId++;
1133
+ this.#gatewayIds.set(object, id);
1134
+ }
1135
+ return id;
1136
+ }
1137
+ #detach(entry) {
1138
+ const { packetListener } = entry;
1139
+ if (packetListener.type === "packets") {
1140
+ entry.subscription.removeEventListener("packets", packetListener.listener);
1141
+ } else {
1142
+ entry.subscription.removeEventListener("bytes", packetListener.listener);
1143
+ }
1144
+ entry.subscription.removeEventListener("state", entry.stateListener);
1145
+ entry.subscription.removeEventListener("error", entry.errorListener);
1146
+ }
1147
+ #dispatch(consumers, callback) {
1148
+ for (const consumer of consumers.values()) {
1149
+ try {
1150
+ callback(consumer);
1151
+ } catch (error) {
1152
+ console.error("[Transport] multicast consumer callback failed", error);
1153
+ }
1154
+ }
1155
+ }
1156
+ };
1157
+ var multicastSubscriptionPool = new MulticastSubscriptionPool();
1158
+
1159
+ // node_modules/@blockcast/mmt-transport/dist/transport-manager.js
1160
+ var TransportManager = class {
1161
+ state = {
1162
+ mode: "disconnected",
1163
+ connected: false,
1164
+ packetsReceived: 0,
1165
+ bytesReceived: 0,
1166
+ lastPacketTime: 0
1167
+ };
1168
+ capabilities = null;
1169
+ subscription = null;
1170
+ /** Built by `connect()` from the caller's ladder; there is no built-in one. */
1171
+ abr = null;
1172
+ multicastOrchestrator = null;
1173
+ onOrchestratorCommand;
1174
+ connectionGeneration = 0;
1175
+ onPacket;
1176
+ onError;
1177
+ onStateChange;
1178
+ getWindow() {
1179
+ return typeof window === "undefined" ? void 0 : window;
1180
+ }
1181
+ getWindowMulticastGateway() {
1182
+ return this.getWindow()?.multicast;
1183
+ }
1184
+ async getGatewayCapabilities(gateway) {
1185
+ if (!gateway?.capabilities)
1186
+ return void 0;
1187
+ try {
1188
+ return typeof gateway.capabilities === "function" ? await gateway.capabilities() : gateway.capabilities;
1189
+ } catch (error) {
1190
+ console.warn("[Transport] window.multicast capabilities failed:", error);
1191
+ return void 0;
1192
+ }
1193
+ }
1194
+ async gatewaySupportsBytes(gateway) {
1195
+ if (typeof gateway?.subscribe !== "function")
1196
+ return false;
1197
+ if (!gateway.capabilities)
1198
+ return true;
1199
+ const capabilities = await this.getGatewayCapabilities(gateway);
1200
+ if (!capabilities)
1201
+ return false;
1202
+ const consumeModes = capabilities.consumeModes;
1203
+ return !consumeModes || consumeModes.includes("bytes");
1204
+ }
1205
+ /**
1206
+ * Detect available transport capabilities
1207
+ */
1208
+ async detectCapabilities() {
1209
+ if (this.capabilities)
1210
+ return this.capabilities;
1211
+ const gateway = this.getWindowMulticastGateway();
1212
+ const hasGatewayBytes = await this.gatewaySupportsBytes(gateway);
1213
+ const win = this.getWindow();
1214
+ this.capabilities = {
1215
+ nativeSSM: hasGatewayBytes,
1216
+ nativeIGMP: hasGatewayBytes,
1217
+ directSockets: typeof win?.UDPSocket !== "undefined",
1218
+ amt: false,
1219
+ driad: hasGatewayBytes,
1220
+ // the gateway runs RFC 8777 discovery
1221
+ fecDecode: false
1222
+ };
1223
+ console.log("[Transport] Capabilities:", this.capabilities);
1224
+ return this.capabilities;
1225
+ }
1226
+ /**
1227
+ * Connect using best available transport
1228
+ *
1229
+ * Promotion hierarchy:
1230
+ * 1. window.multicast gateway bytes
1231
+ * 2. AMT: a configured relay via the AMT proxy, else gateway DRIAD discovery
1232
+ * 3. MOQ WebTransport (final fallback)
1233
+ *
1234
+ * The ABR ladder (`abrLadder`) and initial rendition (`initialQuality`)
1235
+ * are checked first, before anything is torn down. A config that fails is
1236
+ * refused with the same shape as an invalid endpoint, the error to the
1237
+ * error callback and `false` resolved; unlike one, it is caught before the
1238
+ * live subscription closes, so a refused reconnect leaves that subscription,
1239
+ * its state and any connect already in flight untouched.
1240
+ */
1241
+ async connect(config) {
1242
+ let abr;
1243
+ try {
1244
+ abr = new AdaptiveBitrateController({
1245
+ ladder: config.abrLadder,
1246
+ initialQuality: config.initialQuality?.name
1247
+ });
1248
+ } catch (error) {
1249
+ const refused = error instanceof Error ? error : new Error(String(error));
1250
+ if (this.onError)
1251
+ this.onError(refused);
1252
+ else
1253
+ console.error("[Transport] ABR config refused, not connecting:", refused);
1254
+ return false;
1255
+ }
1256
+ const generation = ++this.connectionGeneration;
1257
+ this.subscription?.close();
1258
+ this.subscription = null;
1259
+ this.abr = null;
1260
+ this.state = {
1261
+ mode: "disconnected",
1262
+ connected: false,
1263
+ packetsReceived: 0,
1264
+ bytesReceived: 0,
1265
+ lastPacketTime: 0
1266
+ };
1267
+ this.onStateChange?.(this.state);
1268
+ const endpoint = normalizeMulticastEndpoint({
1269
+ groupAddress: config.group,
1270
+ sourceAddress: config.source,
1271
+ port: config.port,
1272
+ mode: config.mode,
1273
+ amtRelay: config.amtRelay,
1274
+ amtRelayPort: config.amtRelayPort
1275
+ });
1276
+ if (!endpoint) {
1277
+ const error = new Error("Invalid multicast endpoint configuration");
1278
+ this.onError?.(error);
1279
+ return false;
1280
+ }
1281
+ config = {
1282
+ ...config,
1283
+ group: endpoint.groupAddress,
1284
+ source: endpoint.sourceAddress ?? "",
1285
+ mode: endpoint.mode,
1286
+ amtRelay: endpoint.amtRelay,
1287
+ amtRelayPort: endpoint.amtRelayPort
1288
+ };
1289
+ const caps = await this.detectCapabilities();
1290
+ if (generation !== this.connectionGeneration)
1291
+ return false;
1292
+ this.abr = abr;
1293
+ abr.setCallback((quality) => {
1294
+ this.state.quality = quality;
1295
+ this.subscription?.onqualitychange?.(quality);
1296
+ this.onStateChange?.(this.state);
1297
+ });
1298
+ let connected = false;
1299
+ if (caps.nativeSSM || caps.nativeIGMP) {
1300
+ console.log("[Transport] Attempting window.multicast...");
1301
+ connected = await this.connectWindowMulticast(config, generation);
1302
+ if (connected) {
1303
+ if (generation !== this.connectionGeneration)
1304
+ return false;
1305
+ this.state.mode = "multicast";
1306
+ return true;
1307
+ }
1308
+ }
1309
+ if (!connected) {
1310
+ console.log("[Transport] Attempting AMT tunnel...");
1311
+ connected = await this.connectAmt(config, generation);
1312
+ if (connected) {
1313
+ if (generation !== this.connectionGeneration)
1314
+ return false;
1315
+ this.state.mode = "amt";
1316
+ return true;
1317
+ }
1318
+ }
1319
+ console.log("[Transport] All multicast transports failed, falling back to MOQ");
1320
+ return false;
1321
+ }
1322
+ /**
1323
+ * Connect via the lowercase window.multicast bytes gateway.
1324
+ */
1325
+ async connectWindowMulticast(config, generation, mode = "multicast") {
1326
+ try {
1327
+ const gateway = this.getWindowMulticastGateway();
1328
+ if (typeof gateway?.subscribe !== "function")
1329
+ return false;
1330
+ const lease = await multicastSubscriptionPool.acquire(gateway, config, {
1331
+ onBytes: (data) => {
1332
+ if (generation === this.connectionGeneration)
1333
+ this.handlePacket(data);
1334
+ },
1335
+ onState: (state) => {
1336
+ if (generation !== this.connectionGeneration)
1337
+ return;
1338
+ this.state.connected = state !== "closed";
1339
+ this.onStateChange?.(this.state);
1340
+ },
1341
+ onError: (error) => {
1342
+ if (generation !== this.connectionGeneration)
1343
+ return;
1344
+ console.error("[Transport] window.multicast error:", error);
1345
+ this.onError?.(error);
1346
+ }
1347
+ });
1348
+ if (generation !== this.connectionGeneration) {
1349
+ await lease.release();
1350
+ return false;
1351
+ }
1352
+ this.subscription = {
1353
+ id: lease.key,
1354
+ config,
1355
+ close: () => {
1356
+ void lease.release();
1357
+ }
1358
+ };
1359
+ this.state = {
1360
+ mode,
1361
+ connected: true,
1362
+ group: config.group,
1363
+ source: config.source,
1364
+ quality: this.requireAbr().getCurrentQuality(),
1365
+ packetsReceived: 0,
1366
+ bytesReceived: 0,
1367
+ lastPacketTime: 0
1368
+ };
1369
+ console.log(`[Transport] Connected via window.multicast: (${config.source}, ${config.group})`);
1370
+ this.onStateChange?.(this.state);
1371
+ return true;
1372
+ } catch (error) {
1373
+ console.warn("[Transport] window.multicast failed:", error);
1374
+ return false;
1375
+ }
1376
+ }
1377
+ /**
1378
+ * Connect via AMT: a configured relay goes through the AMT proxy; with no
1379
+ * relay, the window.multicast gateway discovers one per RFC 8777 (DRIAD).
1380
+ */
1381
+ async connectAmt(config, generation) {
1382
+ try {
1383
+ if (config.amtRelay)
1384
+ return await this.connectAmtProxy(config, config.amtRelay, generation);
1385
+ if (config.driadEnabled === false || !config.source || config.mode === "native") {
1386
+ console.warn("[Transport] No AMT relay available (DRIAD requires source address for SSM)");
1387
+ return false;
1388
+ }
1389
+ console.log(`[Transport] Asking the gateway for a DRIAD relay for source ${config.source}...`);
1390
+ return await this.connectWindowMulticast({ ...config, amtDiscovery: "driad" }, generation, "amt");
1391
+ } catch (error) {
1392
+ console.warn("[Transport] AMT connection failed:", error);
1393
+ return false;
1394
+ }
1395
+ }
1396
+ /**
1397
+ * Connect via WebSocket-based AMT proxy (browser fallback)
1398
+ */
1399
+ async connectAmtProxy(config, amtRelay, generation) {
1400
+ try {
1401
+ const relayHost = amtRelay.includes(":") && !amtRelay.startsWith("[") ? `[${amtRelay}]` : amtRelay;
1402
+ const wsUrl = new URL(`wss://${relayHost}/amt-proxy`);
1403
+ wsUrl.searchParams.set("group", config.group);
1404
+ wsUrl.searchParams.set("source", config.source || "");
1405
+ wsUrl.searchParams.set("port", String(config.port));
1406
+ const ws = new WebSocket(wsUrl);
1407
+ return new Promise((resolve, reject) => {
1408
+ ws.binaryType = "arraybuffer";
1409
+ const timeout = setTimeout(() => {
1410
+ ws.close();
1411
+ reject(new Error("AMT proxy connection timeout"));
1412
+ }, 1e4);
1413
+ ws.onopen = () => {
1414
+ clearTimeout(timeout);
1415
+ if (generation !== this.connectionGeneration) {
1416
+ ws.close();
1417
+ resolve(false);
1418
+ return;
1419
+ }
1420
+ console.log(`[Transport] Connected to AMT proxy: ${amtRelay}`);
1421
+ this.subscription = {
1422
+ id: `amt-proxy-${Date.now()}`,
1423
+ config,
1424
+ close: () => ws.close()
1425
+ };
1426
+ this.state = {
1427
+ mode: "amt",
1428
+ connected: true,
1429
+ group: config.group,
1430
+ source: config.source,
1431
+ amtRelay,
1432
+ quality: this.requireAbr().getCurrentQuality(),
1433
+ packetsReceived: 0,
1434
+ bytesReceived: 0,
1435
+ lastPacketTime: 0
1436
+ };
1437
+ this.onStateChange?.(this.state);
1438
+ resolve(true);
1439
+ };
1440
+ ws.onmessage = (event) => {
1441
+ if (generation === this.connectionGeneration && event.data instanceof ArrayBuffer) {
1442
+ this.handlePacket(event.data);
1443
+ }
1444
+ };
1445
+ ws.onerror = (error) => {
1446
+ clearTimeout(timeout);
1447
+ if (generation !== this.connectionGeneration) {
1448
+ resolve(false);
1449
+ return;
1450
+ }
1451
+ console.error("[Transport] AMT proxy error:", error);
1452
+ reject(error);
1453
+ };
1454
+ ws.onclose = () => {
1455
+ clearTimeout(timeout);
1456
+ if (generation !== this.connectionGeneration) {
1457
+ resolve(false);
1458
+ return;
1459
+ }
1460
+ this.state.connected = false;
1461
+ this.onStateChange?.(this.state);
1462
+ resolve(false);
1463
+ };
1464
+ });
1465
+ } catch (error) {
1466
+ console.warn("[Transport] AMT proxy failed:", error);
1467
+ return false;
1468
+ }
1469
+ }
1470
+ /**
1471
+ * Handle received packet
1472
+ */
1473
+ handlePacket(data) {
1474
+ this.state.packetsReceived++;
1475
+ this.state.bytesReceived += data.byteLength;
1476
+ this.state.lastPacketTime = Date.now();
1477
+ this.abr?.recordSample(data.byteLength, 0);
1478
+ this.onPacket?.(data);
1479
+ }
1480
+ /**
1481
+ * Get current transport state
1482
+ */
1483
+ getState() {
1484
+ return { ...this.state };
1485
+ }
1486
+ /**
1487
+ * The rung of the live connection's `abrLadder` ABR is on; `null` when
1488
+ * there is no ladder (before `connect(config)` succeeds, after
1489
+ * `disconnect()`, or after a reconnect that was torn down and bailed).
1490
+ */
1491
+ getQuality() {
1492
+ return this.abr?.getCurrentQuality() ?? null;
1493
+ }
1494
+ /**
1495
+ * Force the rung named `name` of the live connection's `abrLadder`.
1496
+ * Throws `Error` when there is no ladder (see `getQuality()`), and
1497
+ * `RangeError` when the ladder has no rung of that name; it never falls
1498
+ * back to another rung.
1499
+ */
1500
+ setQuality(name) {
1501
+ this.requireAbr().setQuality(name);
1502
+ }
1503
+ /** The controller `connect()` built; before it there is no ladder to adapt over. */
1504
+ requireAbr() {
1505
+ if (!this.abr)
1506
+ throw new Error("[Transport] ABR has no ladder before connect(config)");
1507
+ return this.abr;
1508
+ }
1509
+ /** Configures multicast-only ABR while leaving the caller's MoQ ABR untouched. */
1510
+ configureMulticastTiers(options, onCommand) {
1511
+ this.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());
1512
+ this.multicastOrchestrator = new MulticastOrchestrator(options);
1513
+ this.onOrchestratorCommand = onCommand;
1514
+ return this.multicastOrchestrator.start();
1515
+ }
1516
+ recordMulticastGoodput(bitrate) {
1517
+ return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onGoodput({ bitrate }));
1518
+ }
1519
+ recordMulticastHeadroom(healthy) {
1520
+ return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onHeadroom({ healthy }));
1521
+ }
1522
+ recordMulticastTierPacket(packet) {
1523
+ return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onTierPacket(packet));
1524
+ }
1525
+ pollMulticastTierSwitch() {
1526
+ return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.poll());
1527
+ }
1528
+ /** Abandon the in-flight tier join, e.g. because the host could not join the target. */
1529
+ cancelMulticastTierJoin() {
1530
+ return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());
1531
+ }
1532
+ /**
1533
+ * Set packet callback
1534
+ */
1535
+ setPacketCallback(callback) {
1536
+ this.onPacket = callback;
1537
+ }
1538
+ /**
1539
+ * Set error callback
1540
+ */
1541
+ setErrorCallback(callback) {
1542
+ this.onError = callback;
1543
+ }
1544
+ /**
1545
+ * Set state change callback
1546
+ */
1547
+ setStateCallback(callback) {
1548
+ this.onStateChange = callback;
1549
+ }
1550
+ /**
1551
+ * Disconnect
1552
+ */
1553
+ disconnect() {
1554
+ this.connectionGeneration++;
1555
+ this.subscription?.close();
1556
+ this.subscription = null;
1557
+ this.abr = null;
1558
+ this.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());
1559
+ this.multicastOrchestrator = null;
1560
+ this.onOrchestratorCommand = void 0;
1561
+ this.state = {
1562
+ mode: "disconnected",
1563
+ connected: false,
1564
+ packetsReceived: 0,
1565
+ bytesReceived: 0,
1566
+ lastPacketTime: 0
1567
+ };
1568
+ this.onStateChange?.(this.state);
1569
+ }
1570
+ dispatchOrchestratorCommand(command) {
1571
+ if (command)
1572
+ this.onOrchestratorCommand?.(command);
1573
+ return command;
1574
+ }
1575
+ };
1576
+ var transportManager = new TransportManager();
1577
+
1578
+ // node_modules/@blockcast/mmt-transport/dist/multicast-receiver-gateway.js
1579
+ var USER_AGENT_VERIFIED = /* @__PURE__ */ Symbol.for("@blockcast/mmt-transport/user-agent-verified/v1");
1580
+
1581
+ // node_modules/@blockcast/mmt-transport/dist/moqt-priority.js
1582
+ var MAX_MOQT_PRIORITY = 255;
1583
+ function moqtPriority(value, field) {
1584
+ if (!Number.isInteger(value) || value < 0 || value > MAX_MOQT_PRIORITY) {
1585
+ throw new RangeError(`${field} must be an integer in [0, ${MAX_MOQT_PRIORITY}], got ${value}`);
1586
+ }
1587
+ return value;
1588
+ }
1589
+ function moqtSubscriberPriority(value) {
1590
+ return moqtPriority(value, "MOQT subscriber priority");
1591
+ }
1592
+ function moqtPublisherPriority(value) {
1593
+ return moqtPriority(value, "MOQT publisher priority");
1594
+ }
1595
+ var MOQT_SUBSCRIBER_PRIORITY = Object.freeze({
1596
+ catalog: moqtSubscriberPriority(0),
1597
+ /**
1598
+ * @deprecated A repair FETCH's SUBSCRIBER_PRIORITY is derived per request
1599
+ * from the catalog source and repair priorities (draft-ramadan-moq-fec
1600
+ * §11.4.5), not a player constant. Use the `subscriberPriority` that
1601
+ * `@blockcast/mmt-container`'s `wireRepairFetch` puts on each
1602
+ * `RepairFetchRequest` (`repairFetchSubscriberPriority`).
1603
+ */
1604
+ deadlineRepair: moqtSubscriberPriority(0),
1605
+ audio: moqtSubscriberPriority(1),
1606
+ video: moqtSubscriberPriority(2),
1607
+ /**
1608
+ * @deprecated See `deadlineRepair`: use the request's catalog-derived
1609
+ * `RepairFetchRequest.subscriberPriority` (draft-ramadan-moq-fec §11.4.5).
1610
+ */
1611
+ backgroundRepair: moqtSubscriberPriority(3)
1612
+ });
1613
+ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
1614
+ catalog: moqtPublisherPriority(32),
1615
+ source: moqtPublisherPriority(128),
1616
+ repair: moqtPublisherPriority(240)
1617
+ });
1618
+
1619
+ // node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
1620
+ var AMTGatewayManager = class {
1621
+ subscriptions = /* @__PURE__ */ new Map();
1622
+ messageHandler;
1623
+ pendingPackets = /* @__PURE__ */ new Map();
1624
+ constructor() {
1625
+ this.messageHandler = (event) => {
1626
+ const data = event.data;
1627
+ console.log(`[AMT] SW message received:`, data.type);
1628
+ if (data.type === "UDP_PACKET") {
1629
+ this.handlePacket(data);
1630
+ } else if (data.type === "SOCKET_ERROR") {
1631
+ this.handleSocketError(data);
1632
+ } else if (data.type === "DEBUG_LOG") {
1633
+ console.log(`[AMT SW Debug] ${data.message}`);
1634
+ }
1635
+ };
1636
+ if (typeof navigator !== "undefined" && navigator.serviceWorker) {
1637
+ navigator.serviceWorker.addEventListener("message", this.messageHandler);
1638
+ console.log("[AMT] Registered service worker message handler");
1639
+ } else {
1640
+ console.warn("[AMT] Service worker not available");
1641
+ }
1642
+ }
1643
+ /**
1644
+ * Subscribe via AMT tunnel
1645
+ */
1646
+ async subscribe(config) {
1647
+ const { key } = config;
1648
+ if (config.protected) {
1649
+ throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
1650
+ }
1651
+ const keyStr = this.getKeyString(key);
1652
+ if (this.subscriptions.has(keyStr)) {
1653
+ console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
1654
+ return keyStr;
1655
+ }
1656
+ try {
1657
+ const relayAddress = config.relayAddress;
1658
+ const relayPort = config.relayPort || DEFAULT_AMT_RELAY_PORT;
1659
+ if (!relayAddress) {
1660
+ throw new Error(`AMT relay address required: this manager does no relay discovery. For RFC 8777 DRIAD discovery, join through window.multicast with amtDiscovery: "driad".`);
1661
+ }
1662
+ console.log(`[AMT] Using configured relay ${relayAddress}:${relayPort}`);
1663
+ const subscriptionId = keyStr;
1664
+ this.pendingPackets.set(subscriptionId, []);
1665
+ const subscription = {
1666
+ key,
1667
+ subscriptionId,
1668
+ config,
1669
+ handshakeComplete: false,
1670
+ relay: {
1671
+ address: relayAddress,
1672
+ port: relayPort,
1673
+ discoveryMethod: "manual",
1674
+ discoveredAt: Date.now()
1675
+ }
1676
+ };
1677
+ this.subscriptions.set(keyStr, subscription);
1678
+ console.log(`[AMT] Subscription created for ${keyStr} via ${relayAddress}:${relayPort}`);
1679
+ return keyStr;
1680
+ } catch (error) {
1681
+ const subscription = this.subscriptions.get(keyStr);
1682
+ if (subscription) {
1683
+ this.subscriptions.delete(keyStr);
1684
+ this.pendingPackets.delete(subscription.subscriptionId);
1685
+ }
1686
+ throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
1687
+ }
1688
+ }
1689
+ /**
1690
+ * Unsubscribe from AMT tunnel
1691
+ */
1692
+ async unsubscribe(handle) {
1693
+ const subscription = this.subscriptions.get(handle);
1694
+ if (!subscription) {
1695
+ throw new Error(`Subscription ${handle} not found`);
1696
+ }
1697
+ try {
1698
+ if (subscription.keepAliveTimer) {
1699
+ clearInterval(subscription.keepAliveTimer);
1700
+ }
1701
+ console.log(`[AMT] Unsubscribed from ${handle}`);
1702
+ } catch (error) {
1703
+ console.error("[AMT] Error during unsubscribe:", error);
1704
+ } finally {
1705
+ this.subscriptions.delete(handle);
1706
+ this.pendingPackets.delete(subscription.subscriptionId);
1707
+ }
1708
+ }
1709
+ /**
1710
+ * Get list of active subscriptions
1711
+ */
1712
+ getSubscriptions() {
1713
+ return Array.from(this.subscriptions.keys());
1714
+ }
1715
+ /**
1716
+ * Handle packet from service worker
1717
+ */
1718
+ handlePacket(message) {
1719
+ const { subscriptionId, data } = message;
1720
+ if (!subscriptionId)
1721
+ return;
1722
+ const subscription = this.subscriptions.get(subscriptionId);
1723
+ if (!subscription) {
1724
+ console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
1725
+ return;
1726
+ }
1727
+ if (!subscription.handshakeComplete) {
1728
+ const queue = this.pendingPackets.get(subscriptionId);
1729
+ const packetData2 = data;
1730
+ if (queue) {
1731
+ queue.push({
1732
+ data: packetData2.data,
1733
+ remoteAddress: packetData2.remoteAddress,
1734
+ remotePort: packetData2.remotePort
1735
+ });
1736
+ }
1737
+ return;
1738
+ }
1739
+ const packetData = data;
1740
+ const metadata = {
1741
+ sourceAddress: subscription.relay?.address || "unknown",
1742
+ sourcePort: subscription.relay?.port || 0,
1743
+ transport: "tunnel",
1744
+ timestamp: performance.now()
1745
+ };
1746
+ subscription.config.onPacket(packetData.data, metadata);
1747
+ }
1748
+ /**
1749
+ * Handle socket error from service worker
1750
+ */
1751
+ handleSocketError(message) {
1752
+ const { subscriptionId, error } = message;
1753
+ if (!subscriptionId)
1754
+ return;
1755
+ const subscription = this.subscriptions.get(subscriptionId);
1756
+ if (!subscription) {
1757
+ console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
1758
+ return;
1759
+ }
1760
+ subscription.config.onError(new Error(error || "Unknown error"));
1761
+ }
1762
+ /**
1763
+ * Cleanup on destroy
1764
+ */
1765
+ destroy() {
1766
+ if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
1767
+ navigator.serviceWorker.removeEventListener("message", this.messageHandler);
1768
+ }
1769
+ const handles = Array.from(this.subscriptions.keys());
1770
+ for (const handle of handles) {
1771
+ this.unsubscribe(handle).catch((err) => {
1772
+ console.error(`[AMT] Error unsubscribing ${handle}:`, err);
1773
+ });
1774
+ }
1775
+ }
1776
+ /**
1777
+ * Get key string for HashMap
1778
+ */
1779
+ getKeyString(key) {
1780
+ const source = key.source ? `@${key.source}` : "";
1781
+ return `${key.group}:${key.port}${source}`;
1782
+ }
1783
+ };
1784
+ var amtGatewayManager = new AMTGatewayManager();
1785
+
380
1786
  // src/mfu-reassembler.ts
381
1787
  function fragmentProvenance(fragment) {
382
1788
  if (fragment.deliveryProvenance !== void 0) return fragment.deliveryProvenance;
@@ -8183,7 +9589,7 @@ function decideGap(input) {
8183
9589
 
8184
9590
  // src/duplicate-ledger.ts
8185
9591
  var COUNT_BOUNDED_GENERATION = 2e3;
8186
- function serial32Precedes2(a, b) {
9592
+ function serial32Precedes3(a, b) {
8187
9593
  const distance = b - a >>> 0;
8188
9594
  return distance !== 0 && distance < 2147483648;
8189
9595
  }
@@ -8359,12 +9765,12 @@ var StreamDuplicateLedgers = class {
8359
9765
  predatesHorizon(packetSequenceNumber, ssId) {
8360
9766
  if (this.#retentionMs === null) return false;
8361
9767
  const seqFloor = this.sourceSeqs.horizonFloor;
8362
- if (seqFloor !== void 0 && serial32Precedes2(packetSequenceNumber, seqFloor) && !this.sourceSeqs.has(packetSequenceNumber)) {
9768
+ if (seqFloor !== void 0 && serial32Precedes3(packetSequenceNumber, seqFloor) && !this.sourceSeqs.has(packetSequenceNumber)) {
8363
9769
  return true;
8364
9770
  }
8365
9771
  if (ssId === void 0) return false;
8366
9772
  const ssIdFloor = this.parsedSymbols.horizonFloor;
8367
- return ssIdFloor !== void 0 && serial32Precedes2(ssId, ssIdFloor) && !this.parsedSymbols.has(ssId);
9773
+ return ssIdFloor !== void 0 && serial32Precedes3(ssId, ssIdFloor) && !this.parsedSymbols.has(ssId);
8368
9774
  }
8369
9775
  /** Whether the stream keeps ledgers, for its FEC horizon or by count. */
8370
9776
  get retaining() {
@@ -9571,10 +10977,11 @@ var MmtpRouter = class {
9571
10977
  if (!entry) return;
9572
10978
  entry.receiver.release();
9573
10979
  entry.protected = false;
10980
+ entry.userAgent = false;
9574
10981
  this.#assistBlocks.clear();
9575
10982
  this.#assistFailures.clear();
9576
10983
  }
9577
- /** MoQ packets of a track a protected flow lists, dropped on its unicast path: duplicate, overflow, or listed by more than one flow. */
10984
+ /** Packets of a track a protected flow lists, dropped unread: a MoQ copy (duplicate, overflow, or listed by more than one flow) or a multicast source datagram that names no flow, of a track a user-agent-admitted flow lists. */
9578
10985
  get unadmittedListedDropCount() {
9579
10986
  return this.#unadmittedListedDrops;
9580
10987
  }
@@ -9615,6 +11022,13 @@ var MmtpRouter = class {
9615
11022
  }
9616
11023
  return false;
9617
11024
  }
11025
+ /** Whether a protected flow the user agent admitted lists `packetId`. */
11026
+ #userAgentGoverns(packetId) {
11027
+ for (const entry of this.#provenance.values()) {
11028
+ if (entry.protected && entry.userAgent && ingressHasPacketId(entry.ingress, packetId)) return true;
11029
+ }
11030
+ return false;
11031
+ }
9618
11032
  /**
9619
11033
  * A MoQ repair symbol no protected flow lists, owned by a source track one
9620
11034
  * does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
@@ -9705,7 +11119,7 @@ var MmtpRouter = class {
9705
11119
  fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
9706
11120
  fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
9707
11121
  fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
9708
- })) {
11122
+ }, true)) {
9709
11123
  this.#unlistedAssistedRecoveries++;
9710
11124
  this.#assistBlocks.delete(key);
9711
11125
  return;
@@ -10861,6 +12275,12 @@ var MmtpRouter = class {
10861
12275
  debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
10862
12276
  return;
10863
12277
  }
12278
+ if (ingress.admittedBy === USER_AGENT_VERIFIED) {
12279
+ this.#noteFlowAdmitted(ingress.flow);
12280
+ protectedFlow.userAgent = true;
12281
+ this.#processMMTP(packet, source, true, ingress, overlapCopy);
12282
+ return;
12283
+ }
10864
12284
  const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
10865
12285
  if (verdict === "admitted") {
10866
12286
  this.#noteFlowAdmitted(ingress.flow);
@@ -10878,6 +12298,11 @@ var MmtpRouter = class {
10878
12298
  return;
10879
12299
  }
10880
12300
  }
12301
+ const flowless = source === "multicast" && !manifestAdmitted && ingress?.flow === void 0;
12302
+ if (flowless && !info.isRepair && this.#userAgentGoverns(info.packetId)) {
12303
+ this.#unadmittedListedDrops++;
12304
+ return;
12305
+ }
10881
12306
  if (info.isRepair) {
10882
12307
  const overlay = this.#overlayRepairTarget(info.packetId, catalogTrackNameForPacket(ingress, info.packetId));
10883
12308
  if (overlay !== void 0) {
@@ -10895,7 +12320,7 @@ var MmtpRouter = class {
10895
12320
  );
10896
12321
  if (!repair) return;
10897
12322
  if (ownerPacketId != null && this.#fecManager) {
10898
- if (source === "moq" && !manifestAdmitted && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
12323
+ if ((flowless && this.#userAgentGoverns(ownerPacketId) || source === "moq" && !manifestAdmitted) && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
10899
12324
  this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
10900
12325
  const count = this.#repairPidCounts[info.packetId];
10901
12326
  if (count <= 2 || count % 500 === 0) {
@@ -11474,7 +12899,7 @@ var MmtpRouter = class {
11474
12899
  }
11475
12900
  // ── Private: FEC recovery routing ───────────────────────────────
11476
12901
  /** Route WASM-recovered block through appropriate path based on container kind. */
11477
- #routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete) {
12902
+ #routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete, assisted = false) {
11478
12903
  const packetSymbolSize = this.#symbolSizeForPacket(ownerPacketId);
11479
12904
  if (ownerPacketId === void 0) {
11480
12905
  throw new Error("[MmtpRouter] a recovered FEC block needs its owner packetId to read its fec.blockProfile");
@@ -11491,7 +12916,7 @@ var MmtpRouter = class {
11491
12916
  ownerPacketId,
11492
12917
  blockProfile
11493
12918
  });
11494
- return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
12919
+ return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted);
11495
12920
  }
11496
12921
  /**
11497
12922
  * A recovered source packet of a protected flow is admitted on its own
@@ -11503,9 +12928,24 @@ var MmtpRouter = class {
11503
12928
  * recovered packets is refused is unrecovered (7.2.7): its caller leaves it
11504
12929
  * open, so it reaches its FEC deadline and fails by class. Always true
11505
12930
  * outside a protected flow.
12931
+ *
12932
+ * On a flow the user agent admitted, the live decoder's recoveries are kept
12933
+ * without a record: while the flow is protected that decoder takes only
12934
+ * symbols the user agent or a record verified (a flow-less multicast
12935
+ * datagram of a listed track is dropped or set aside in #processMMTP), so
12936
+ * what it recovers is as verified as they are. An `assisted` (scratch-decoder) recovery mixes in
12937
+ * unlisted MoQ repair and still needs a record.
12938
+ * ponytail: per-flow flag; per-block provenance only if a protected flow
12939
+ * ever mixes verified and unverified live inputs.
12940
+ * ponytail: a worker frame computed before the protection reset can still
12941
+ * arrive after it (frames carry no epoch); its inputs are MoQ copies read
12942
+ * and played before protection, so it admits nothing new. Drop pre-reset
12943
+ * frames by epoch only if pre-protection input ever matters.
11506
12944
  */
11507
- #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete) {
11508
- const receiver = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
12945
+ #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
12946
+ const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
12947
+ const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
12948
+ const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
11509
12949
  let recovered = true;
11510
12950
  if (receiver) {
11511
12951
  const parsed = this.#stream(ownerPacketId, catalogTrackName).duplicates.parsedSymbols;
@@ -11517,6 +12957,8 @@ var MmtpRouter = class {
11517
12957
  let admitted = false;
11518
12958
  if (receiver === "ambiguous") {
11519
12959
  this.#ambiguousRecoveredDrops++;
12960
+ } else if (userAgent) {
12961
+ admitted = true;
11520
12962
  } else {
11521
12963
  const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
11522
12964
  wire.set(sym.rawSymbol);
@@ -11544,12 +12986,12 @@ var MmtpRouter = class {
11544
12986
  catalogTrackName === void 0 || !byName ? byName?.values() : byName.has(catalogTrackName) ? [byName.get(catalogTrackName)] : []
11545
12987
  );
11546
12988
  if (flows.size > 1) return "ambiguous";
11547
- for (const bound of flows) return this.#provenance.get(bound)?.receiver ?? "ambiguous";
12989
+ for (const bound of flows) return this.#provenance.get(bound) ?? "ambiguous";
11548
12990
  let match;
11549
- for (const { receiver, ingress } of this.#provenance.values()) {
11550
- if (ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
12991
+ for (const entry of this.#provenance.values()) {
12992
+ if (entry.ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
11551
12993
  if (match) return "ambiguous";
11552
- match = receiver;
12994
+ match = entry;
11553
12995
  }
11554
12996
  }
11555
12997
  return match;