@blockcast/mmt-container 2.0.1 → 2.1.0-main.36792a9d6614
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-runtime.js +817 -64
- package/dist/browser-runtime.js.map +4 -4
- package/dist/cmcd-producer.d.ts +5 -0
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +2 -2
- package/dist/mmtp-router.d.ts +78 -3
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +1812 -27
- package/dist/mmtp-router.js.map +4 -4
- package/dist/multicast-manager.d.ts +33 -3
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +15362 -14626
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +11 -3
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +255 -19
- package/dist/multicast-transport.js.map +4 -4
- package/dist/repair-fetch.d.ts +10 -3
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +15 -0
- package/dist/repair-fetch.js.map +2 -2
- package/package.json +5 -5
- package/src/cmcd-producer.ts +5 -0
- package/src/index.ts +5 -0
- package/src/mmtp-router.ts +549 -28
- package/src/multicast-manager.ts +186 -22
- package/src/multicast-transport.ts +44 -20
- package/src/repair-fetch.ts +32 -2
package/dist/mmtp-router.js
CHANGED
|
@@ -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
|
|
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 &&
|
|
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 &&
|
|
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() {
|
|
@@ -8423,6 +9829,14 @@ function isRouterDebugEnabled() {
|
|
|
8423
9829
|
function debugRouter(message) {
|
|
8424
9830
|
if (isRouterDebugEnabled()) console.debug(message);
|
|
8425
9831
|
}
|
|
9832
|
+
var KEYFRAME_OVERLAY_EXTENSION_TYPE = 35585;
|
|
9833
|
+
function keyframeOverlaySsId(header) {
|
|
9834
|
+
const ext = header.extension;
|
|
9835
|
+
if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
|
|
9836
|
+
return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
|
|
9837
|
+
}
|
|
9838
|
+
var OVERLAY_OFFSET_MPUS = 16;
|
|
9839
|
+
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
8426
9840
|
function assertStreamIdentity(role, stream) {
|
|
8427
9841
|
if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
|
|
8428
9842
|
throw new Error(
|
|
@@ -8639,6 +10053,26 @@ var MmtpRouter = class {
|
|
|
8639
10053
|
#packetIdRoles = /* @__PURE__ */ new Map();
|
|
8640
10054
|
/** Explicit catalog ownership for dedicated repair-track packet IDs. */
|
|
8641
10055
|
#repairOwnersByPacketId = /* @__PURE__ */ new Map();
|
|
10056
|
+
/** Keyframe-overlay repair PID → the source its FEC instance protects
|
|
10057
|
+
* (draft-ramadan-mmt-fec-02 7.4.1). Never an owner in
|
|
10058
|
+
* #repairOwnersByPacketId: the overlay is decoded as its own instance. */
|
|
10059
|
+
#overlayLegs = /* @__PURE__ */ new Map();
|
|
10060
|
+
/** Keyframe-overlay repair track names, which name a leg too: a catalog
|
|
10061
|
+
* may state no PID for it (learned at subscribe time). */
|
|
10062
|
+
#overlayRepairNames = /* @__PURE__ */ new Map();
|
|
10063
|
+
/** Installed overlay instances, by overlay repair PID (opt-in only). */
|
|
10064
|
+
#overlays = /* @__PURE__ */ new Map();
|
|
10065
|
+
#overlayRecoveredObservers = /* @__PURE__ */ new Set();
|
|
10066
|
+
/** Streams whose recovered sequence the base path ever re-stamped. */
|
|
10067
|
+
#restampedStreams = /* @__PURE__ */ new Set();
|
|
10068
|
+
#overlayStats = {
|
|
10069
|
+
repairDropped: 0,
|
|
10070
|
+
ownerRefused: 0,
|
|
10071
|
+
recovered: 0,
|
|
10072
|
+
restarts: 0,
|
|
10073
|
+
unsequencedDropped: 0,
|
|
10074
|
+
protectedDropped: 0
|
|
10075
|
+
};
|
|
8642
10076
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
8643
10077
|
#onDeltaBlockCompleteCb = null;
|
|
8644
10078
|
// Debug counters
|
|
@@ -8835,6 +10269,7 @@ var MmtpRouter = class {
|
|
|
8835
10269
|
this.#fecManager.onSourceDiscontinuity = (packetId, catalogTrackName) => {
|
|
8836
10270
|
this.#parser.markFecDiscontinuity(packetId, catalogTrackName);
|
|
8837
10271
|
this.#requestFecPoke(packetId, catalogTrackName);
|
|
10272
|
+
this.resetKeyframeOverlays(packetId, catalogTrackName);
|
|
8838
10273
|
};
|
|
8839
10274
|
this.#fecManager.onBlockComplete = (sbn, packetId, catalogTrackName) => {
|
|
8840
10275
|
this.#mseAssemblerFor(packetId, catalogTrackName)?.completeBlock(sbn);
|
|
@@ -9157,13 +10592,287 @@ var MmtpRouter = class {
|
|
|
9157
10592
|
/** Replace catalog-derived repair ownership before a catalog reconfiguration. */
|
|
9158
10593
|
clearRepairPacketIdOwners() {
|
|
9159
10594
|
this.#repairOwnersByPacketId.clear();
|
|
10595
|
+
this.#overlayLegs.clear();
|
|
10596
|
+
this.#overlayRepairNames.clear();
|
|
9160
10597
|
}
|
|
9161
|
-
/** Bind one dedicated repair-track PID to its authoritative media PID.
|
|
10598
|
+
/** Bind one dedicated repair-track PID to its authoritative media PID. A
|
|
10599
|
+
* known keyframe-overlay PID is refused, and counted: its symbols belong to
|
|
10600
|
+
* the overlay instance, never to the source's base decoder (mmt-02 7.4.4). */
|
|
9162
10601
|
setRepairPacketIdOwner(repairPacketId, ownerPacketId) {
|
|
10602
|
+
if (this.#overlayLegs.has(repairPacketId)) {
|
|
10603
|
+
this.#overlayStats.ownerRefused++;
|
|
10604
|
+
return;
|
|
10605
|
+
}
|
|
9163
10606
|
const owners = this.#repairOwnersByPacketId.get(repairPacketId) ?? /* @__PURE__ */ new Set();
|
|
9164
10607
|
owners.add(ownerPacketId);
|
|
9165
10608
|
this.#repairOwnersByPacketId.set(repairPacketId, owners);
|
|
9166
10609
|
}
|
|
10610
|
+
/**
|
|
10611
|
+
* Record a keyframe-overlay repair leg (draft-ramadan-mmt-fec-02 7.4): its
|
|
10612
|
+
* PID, the source it protects, and its repair track name. Either one names
|
|
10613
|
+
* the leg, so a catalog that states no PID (learned at subscribe time) is
|
|
10614
|
+
* covered by the name. Overlay repair is then never resolved to a base
|
|
10615
|
+
* owner, on multicast or MoQ. Cleared with the catalog owners
|
|
10616
|
+
* (`clearRepairPacketIdOwners`).
|
|
10617
|
+
*/
|
|
10618
|
+
setKeyframeOverlayLeg(repairPacketId, leg) {
|
|
10619
|
+
if (leg.repairTrackName !== void 0) this.#overlayRepairNames.set(leg.repairTrackName, { ...leg });
|
|
10620
|
+
if (repairPacketId === void 0) return;
|
|
10621
|
+
this.#repairOwnersByPacketId.delete(repairPacketId);
|
|
10622
|
+
this.#overlayLegs.set(repairPacketId, { ...leg });
|
|
10623
|
+
}
|
|
10624
|
+
/** The keyframe-overlay leg registered on `repairPacketId`, if any. */
|
|
10625
|
+
keyframeOverlayLeg(repairPacketId) {
|
|
10626
|
+
return this.#overlayLegs.get(repairPacketId);
|
|
10627
|
+
}
|
|
10628
|
+
/**
|
|
10629
|
+
* Overlay counters: repair dropped for want of an installed instance, base
|
|
10630
|
+
* owners refused on an overlay PID, overlay blocks recovered, overlay
|
|
10631
|
+
* restarts, and recovered packets dropped for want of a wire sequence or
|
|
10632
|
+
* for a MoQ symbol on a protected flow (multicast-02 7.2.7).
|
|
10633
|
+
*/
|
|
10634
|
+
get keyframeOverlayStats() {
|
|
10635
|
+
return { ...this.#overlayStats };
|
|
10636
|
+
}
|
|
10637
|
+
#isOverlayRepair(repairPacketId, repairTrackName) {
|
|
10638
|
+
return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
|
|
10639
|
+
}
|
|
10640
|
+
/**
|
|
10641
|
+
* Install the keyframe overlay decoded on `fec` for a known overlay leg
|
|
10642
|
+
* (draft-ramadan-mmt-fec-02 7.4.4: decoded independently of the base). `fec`
|
|
10643
|
+
* holds the overlay stream under `repairPacketId` and `streamName`. Replaces
|
|
10644
|
+
* an earlier install on the same leg; the returned disposer removes only
|
|
10645
|
+
* this one.
|
|
10646
|
+
*/
|
|
10647
|
+
installKeyframeOverlay(repairPacketId, fec, k, streamName) {
|
|
10648
|
+
const leg = this.#overlayLegs.get(repairPacketId);
|
|
10649
|
+
if (!leg) throw new Error(`[MmtpRouter] packetId=${repairPacketId} is no known keyframe-overlay leg`);
|
|
10650
|
+
const inst = {
|
|
10651
|
+
repairPacketId,
|
|
10652
|
+
streamName,
|
|
10653
|
+
leg: { ...leg },
|
|
10654
|
+
fec,
|
|
10655
|
+
k,
|
|
10656
|
+
offsets: /* @__PURE__ */ new Map(),
|
|
10657
|
+
moqBlocks: /* @__PURE__ */ new Set()
|
|
10658
|
+
};
|
|
10659
|
+
this.#overlays.set(repairPacketId, inst);
|
|
10660
|
+
return () => {
|
|
10661
|
+
if (this.#overlays.get(repairPacketId) === inst) this.#overlays.delete(repairPacketId);
|
|
10662
|
+
};
|
|
10663
|
+
}
|
|
10664
|
+
/**
|
|
10665
|
+
* Observe each base block whose RAP MFU fragments a keyframe-overlay
|
|
10666
|
+
* recovery supplied, with the keyframe's FT=0 and FT=1 held
|
|
10667
|
+
* (fec-multicast-00 §6.6). The block is named by the recovered packets'
|
|
10668
|
+
* restored wire sequence through the base SS_ID offset, never by an overlay
|
|
10669
|
+
* coordinate. Called synchronously from the recovered-block route.
|
|
10670
|
+
*/
|
|
10671
|
+
addKeyframeOverlayRecoveredObserver(observer) {
|
|
10672
|
+
this.#overlayRecoveredObservers.add(observer);
|
|
10673
|
+
return () => {
|
|
10674
|
+
this.#overlayRecoveredObservers.delete(observer);
|
|
10675
|
+
};
|
|
10676
|
+
}
|
|
10677
|
+
/** Whether an overlay is installed on `repairPacketId`. */
|
|
10678
|
+
hasKeyframeOverlay(repairPacketId) {
|
|
10679
|
+
return this.#overlays.has(repairPacketId);
|
|
10680
|
+
}
|
|
10681
|
+
/**
|
|
10682
|
+
* Start a new epoch of every overlay protecting `sourcePacketId` (its
|
|
10683
|
+
* source restarted): empty overlay stream, no coverage, no offsets.
|
|
10684
|
+
*/
|
|
10685
|
+
resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
|
|
10686
|
+
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
10687
|
+
}
|
|
10688
|
+
#resetOverlay(inst) {
|
|
10689
|
+
inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
|
|
10690
|
+
inst.last = void 0;
|
|
10691
|
+
inst.offsets.clear();
|
|
10692
|
+
inst.moqBlocks.clear();
|
|
10693
|
+
}
|
|
10694
|
+
#overlaysForSource(sourcePacketId, catalogTrackName) {
|
|
10695
|
+
return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
|
|
10696
|
+
}
|
|
10697
|
+
/**
|
|
10698
|
+
* The overlay instance a repair packet belongs to, by the track it arrived
|
|
10699
|
+
* on (7.4.2: its PID or its repair track name); null for overlay repair
|
|
10700
|
+
* with no instance installed; undefined for repair of no overlay.
|
|
10701
|
+
*/
|
|
10702
|
+
#overlayRepairTarget(repairPacketId, repairTrackName) {
|
|
10703
|
+
const inst = this.#overlays.get(repairPacketId) ?? (repairTrackName === void 0 ? void 0 : [...this.#overlays.values()].find(({ leg }) => leg.repairTrackName === repairTrackName));
|
|
10704
|
+
if (inst) return inst;
|
|
10705
|
+
return this.#isOverlayRepair(repairPacketId, repairTrackName) ? null : void 0;
|
|
10706
|
+
}
|
|
10707
|
+
/** Overlay repair goes to its own instance, never to a base owner, locator
|
|
10708
|
+
* or decoder; with none installed it is dropped and counted. */
|
|
10709
|
+
#routeOverlayRepair(inst, repairPacketId, packet, header, source) {
|
|
10710
|
+
const repair = inst ? parseMoqRepairFrameWithHeader(packet, header) : null;
|
|
10711
|
+
if (!inst || !repair) {
|
|
10712
|
+
this.#overlayStats.repairDropped++;
|
|
10713
|
+
debugRouter(`[MmtpRouter] Dropping keyframe-overlay repair pid=${repairPacketId}: no overlay instance`);
|
|
10714
|
+
return;
|
|
10715
|
+
}
|
|
10716
|
+
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
10717
|
+
if (!fec.admitRepairBlockLength(pid, repair.ssbLength, repair.rsbLength, repair.rsId)) return;
|
|
10718
|
+
if (!fec.admitRepairSymbolSize(pid, repair.ssStart, repair.ssbLength, repair.rsId, repair.symbolData.byteLength)) return;
|
|
10719
|
+
const placement = fec.placeIsoRepair(pid, name, repair.ssStart, repair.ssbLength);
|
|
10720
|
+
if (placement.status !== "placed") return;
|
|
10721
|
+
const blockStart = placement.block.start;
|
|
10722
|
+
if (placement.span !== void 0) this.#replayOverlaySources(inst, placement.span.block.start, placement.span.placed);
|
|
10723
|
+
this.#replayOverlaySources(inst, blockStart, placement.placed);
|
|
10724
|
+
if (source === "moq") this.#noteOverlayMoqBlock(inst, blockStart);
|
|
10725
|
+
const alreadyCompleted = fec.isBlockCompleted(blockStart, pid, name);
|
|
10726
|
+
const recovered = fec.feedRepairSymbol(pid, repair.ssStart, repair.ssbLength, repair.rsId, repair.symbolData, name);
|
|
10727
|
+
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, blockStart, recovered);
|
|
10728
|
+
}
|
|
10729
|
+
#replayOverlaySources(inst, blockStart, placed) {
|
|
10730
|
+
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
10731
|
+
for (const { ssId, payload } of placed) {
|
|
10732
|
+
if (payload.source === "moq") this.#noteOverlayMoqBlock(inst, blockStart);
|
|
10733
|
+
if (payload.oversized) continue;
|
|
10734
|
+
const alreadyCompleted = fec.isBlockCompleted(blockStart, pid, name);
|
|
10735
|
+
const recovered = fec.feedSourceSymbol(pid, ssId, payload.symbolData, name);
|
|
10736
|
+
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, blockStart, recovered);
|
|
10737
|
+
}
|
|
10738
|
+
}
|
|
10739
|
+
/**
|
|
10740
|
+
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
10741
|
+
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
10742
|
+
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
10743
|
+
* included (7.1, 7.4.3).
|
|
10744
|
+
*/
|
|
10745
|
+
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
|
|
10746
|
+
if (this.#overlays.size === 0) return;
|
|
10747
|
+
const ssId = keyframeOverlaySsId(header);
|
|
10748
|
+
if (ssId === void 0) return;
|
|
10749
|
+
for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
|
|
10750
|
+
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
10751
|
+
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
10752
|
+
if (identity !== null) {
|
|
10753
|
+
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
10754
|
+
inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
|
|
10755
|
+
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
|
|
10756
|
+
}
|
|
10757
|
+
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
10758
|
+
const symbolData = packet.subarray(0, packet.length - 4);
|
|
10759
|
+
const oversized = !fec.isoSourceFits(pid, symbolData.length);
|
|
10760
|
+
const coords = fec.locateIsoSource(pid, name, ssId);
|
|
10761
|
+
if (coords === null) {
|
|
10762
|
+
fec.holdIsoSource(pid, name, ssId, {
|
|
10763
|
+
symbolData,
|
|
10764
|
+
source,
|
|
10765
|
+
oversized,
|
|
10766
|
+
evidence: sourceSymbolEvidence(packet, header, rapFlag, sequenceNumber)
|
|
10767
|
+
});
|
|
10768
|
+
continue;
|
|
10769
|
+
}
|
|
10770
|
+
if (source === "moq") this.#noteOverlayMoqBlock(inst, coords.sbn);
|
|
10771
|
+
if (oversized) continue;
|
|
10772
|
+
const alreadyCompleted = fec.isBlockCompleted(coords.sbn, pid, name);
|
|
10773
|
+
const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
|
|
10774
|
+
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
|
|
10775
|
+
}
|
|
10776
|
+
}
|
|
10777
|
+
#noteOverlayMoqBlock(inst, blockStart) {
|
|
10778
|
+
inst.moqBlocks.add(blockStart);
|
|
10779
|
+
const newest = inst.last?.ssId;
|
|
10780
|
+
if (newest === void 0) return;
|
|
10781
|
+
for (const start of inst.moqBlocks) {
|
|
10782
|
+
if ((newest - start | 0) > OVERLAY_MOQ_HORIZON_BLOCKS * inst.k) inst.moqBlocks.delete(start);
|
|
10783
|
+
}
|
|
10784
|
+
}
|
|
10785
|
+
/**
|
|
10786
|
+
* An overlay SS_ID inconsistent with the source's packet_sequence_number
|
|
10787
|
+
* advance since the last one is an overlay restart (an FFmpeg muxer start,
|
|
10788
|
+
* a bridge swap that keeps psn continuous): it moved the other way, or
|
|
10789
|
+
* further than the psn advance plus K_overlay. Reordering keeps the two
|
|
10790
|
+
* consistent and is no restart.
|
|
10791
|
+
*/
|
|
10792
|
+
#noteOverlaySsId(inst, ssId, psn) {
|
|
10793
|
+
const last = inst.last;
|
|
10794
|
+
if (last !== void 0) {
|
|
10795
|
+
const dSs = ssId - last.ssId | 0;
|
|
10796
|
+
const dPsn = psn - last.psn | 0;
|
|
10797
|
+
const consistent = dSs === 0 ? dPsn === 0 : Math.sign(dSs) === Math.sign(dPsn) && Math.abs(dSs) <= Math.abs(dPsn) + inst.k;
|
|
10798
|
+
if (!consistent) {
|
|
10799
|
+
this.#overlayStats.restarts++;
|
|
10800
|
+
this.#resetOverlay(inst);
|
|
10801
|
+
} else if (dPsn < 0) {
|
|
10802
|
+
return;
|
|
10803
|
+
}
|
|
10804
|
+
}
|
|
10805
|
+
inst.last = { ssId, psn };
|
|
10806
|
+
}
|
|
10807
|
+
/**
|
|
10808
|
+
* Route an overlay-recovered block's packets as source media of the
|
|
10809
|
+
* overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
|
|
10810
|
+
* base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
|
|
10811
|
+
* base re-stamp, no block advance. Each packet's wire sequence is restored
|
|
10812
|
+
* from its keyframe's own offset; without one, a stream the base never
|
|
10813
|
+
* re-stamped keeps the header's sequence and any other drops the packet.
|
|
10814
|
+
*/
|
|
10815
|
+
#routeOverlayRecovered(inst, blockStart, recovered) {
|
|
10816
|
+
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
10817
|
+
fec.markBlockCompleted(blockStart, pid, name);
|
|
10818
|
+
const sourcePacketId = inst.leg.sourcePacketId;
|
|
10819
|
+
const sourceName = inst.leg.sourceCatalogTrackName;
|
|
10820
|
+
if (this.#classifyPacketId(sourcePacketId) === null) return;
|
|
10821
|
+
const receiver = this.#provenanceFor(sourcePacketId, sourceName);
|
|
10822
|
+
if (receiver !== void 0 && inst.moqBlocks.has(blockStart)) {
|
|
10823
|
+
this.#overlayStats.protectedDropped++;
|
|
10824
|
+
return;
|
|
10825
|
+
}
|
|
10826
|
+
const blockProfile = fec.blockProfileForPacket(pid);
|
|
10827
|
+
const symbolSize = fec.symbolSizeForPacket(pid);
|
|
10828
|
+
if (blockProfile === void 0 || symbolSize === void 0 || !(symbolSize > 0)) return;
|
|
10829
|
+
const result = processFecBlock(recovered, { symbolSize, ownerPacketId: sourcePacketId, blockProfile });
|
|
10830
|
+
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
10831
|
+
const restamped = this.#restampedStreams.has(streamKey);
|
|
10832
|
+
const restored = /* @__PURE__ */ new Set();
|
|
10833
|
+
const symbols = result.symbols.flatMap((sym) => {
|
|
10834
|
+
const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
10835
|
+
if (offset !== void 0) {
|
|
10836
|
+
const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
|
|
10837
|
+
restored.add(packetSequenceNumber);
|
|
10838
|
+
return [{ ...sym, packetSequenceNumber }];
|
|
10839
|
+
}
|
|
10840
|
+
if (!restamped) return [sym];
|
|
10841
|
+
this.#overlayStats.unsequencedDropped++;
|
|
10842
|
+
return [];
|
|
10843
|
+
});
|
|
10844
|
+
this.#overlayStats.recovered++;
|
|
10845
|
+
this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
|
|
10846
|
+
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
10847
|
+
});
|
|
10848
|
+
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
10849
|
+
}
|
|
10850
|
+
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
10851
|
+
* the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
|
|
10852
|
+
* packet whose wire sequence was not restored names no base block. */
|
|
10853
|
+
#noteOverlayRecoveredRaps(packetId, catalogTrackName, streamKey, symbols) {
|
|
10854
|
+
const ssIdSeqOffset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
10855
|
+
if (ssIdSeqOffset === void 0 || this.#overlayRecoveredObservers.size === 0) return;
|
|
10856
|
+
const withMeta = (type) => new Set(symbols.filter((sym) => sym.fragmentType === type).map((sym) => sym.fragment.mpuSequenceNumber));
|
|
10857
|
+
const ft0 = withMeta(0);
|
|
10858
|
+
const ft1 = withMeta(1);
|
|
10859
|
+
const rapMfu = (fi) => new Set(symbols.filter((sym) => sym.fragmentType === 2 && sym.fragment.rapFlag && sym.fragment.fragmentationIndicator === fi).map((sym) => sym.fragment.mpuSequenceNumber));
|
|
10860
|
+
const single = rapMfu(0);
|
|
10861
|
+
const first = rapMfu(1);
|
|
10862
|
+
const last = rapMfu(3);
|
|
10863
|
+
const blocks = /* @__PURE__ */ new Set();
|
|
10864
|
+
for (const sym of symbols) {
|
|
10865
|
+
const mpu = sym.fragment.mpuSequenceNumber;
|
|
10866
|
+
if (sym.fragmentType !== 2 || !sym.fragment.rapFlag || !ft0.has(mpu) || !ft1.has(mpu)) continue;
|
|
10867
|
+
if (!single.has(mpu) && !(first.has(mpu) && last.has(mpu))) continue;
|
|
10868
|
+
const ssId = sym.packetSequenceNumber + ssIdSeqOffset >>> 0;
|
|
10869
|
+
const sbn = this.#fecManager?.locateIsoSource?.(packetId, catalogTrackName, ssId)?.sbn;
|
|
10870
|
+
if (sbn !== void 0) blocks.add(sbn);
|
|
10871
|
+
}
|
|
10872
|
+
for (const sbn of blocks) {
|
|
10873
|
+
for (const observer of this.#overlayRecoveredObservers) observer(packetId, catalogTrackName, sbn);
|
|
10874
|
+
}
|
|
10875
|
+
}
|
|
9167
10876
|
/**
|
|
9168
10877
|
* Register callback fired when a delta-track FEC block completes or fails.
|
|
9169
10878
|
*
|
|
@@ -9268,10 +10977,11 @@ var MmtpRouter = class {
|
|
|
9268
10977
|
if (!entry) return;
|
|
9269
10978
|
entry.receiver.release();
|
|
9270
10979
|
entry.protected = false;
|
|
10980
|
+
entry.userAgent = false;
|
|
9271
10981
|
this.#assistBlocks.clear();
|
|
9272
10982
|
this.#assistFailures.clear();
|
|
9273
10983
|
}
|
|
9274
|
-
/**
|
|
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. */
|
|
9275
10985
|
get unadmittedListedDropCount() {
|
|
9276
10986
|
return this.#unadmittedListedDrops;
|
|
9277
10987
|
}
|
|
@@ -9312,6 +11022,13 @@ var MmtpRouter = class {
|
|
|
9312
11022
|
}
|
|
9313
11023
|
return false;
|
|
9314
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
|
+
}
|
|
9315
11032
|
/**
|
|
9316
11033
|
* A MoQ repair symbol no protected flow lists, owned by a source track one
|
|
9317
11034
|
* does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
|
|
@@ -9402,7 +11119,7 @@ var MmtpRouter = class {
|
|
|
9402
11119
|
fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
|
|
9403
11120
|
fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
|
|
9404
11121
|
fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
9405
|
-
})) {
|
|
11122
|
+
}, true)) {
|
|
9406
11123
|
this.#unlistedAssistedRecoveries++;
|
|
9407
11124
|
this.#assistBlocks.delete(key);
|
|
9408
11125
|
return;
|
|
@@ -9587,6 +11304,11 @@ var MmtpRouter = class {
|
|
|
9587
11304
|
return;
|
|
9588
11305
|
}
|
|
9589
11306
|
const repairPacketId = header.packetId;
|
|
11307
|
+
const overlay = this.#overlayRepairTarget(repairPacketId, repairTrackName);
|
|
11308
|
+
if (overlay !== void 0) {
|
|
11309
|
+
this.#routeOverlayRepair(overlay, repairPacketId, packet, header, "moq");
|
|
11310
|
+
return;
|
|
11311
|
+
}
|
|
9590
11312
|
const hintedOwner = ownerPacketIdHint != null && this.#classifyPacketId(ownerPacketIdHint) !== null ? ownerPacketIdHint : null;
|
|
9591
11313
|
const ownerPacketId = hintedOwner ?? (this.#classifyPacketId(repairPacketId) !== null ? repairPacketId : null);
|
|
9592
11314
|
if (ownerPacketId === null) {
|
|
@@ -9876,6 +11598,8 @@ var MmtpRouter = class {
|
|
|
9876
11598
|
}
|
|
9877
11599
|
}
|
|
9878
11600
|
this.#parser.resetTrack(packetId, catalogTrackName);
|
|
11601
|
+
this.resetKeyframeOverlays(packetId, catalogTrackName);
|
|
11602
|
+
this.#restampedStreams.delete(this.#mediaStreamKey(packetId, catalogTrackName));
|
|
9879
11603
|
const assistPrefix = assistKey(packetId, catalogTrackName, 0).slice(0, -1);
|
|
9880
11604
|
for (const key of this.#assistBlocks.keys()) if (key.startsWith(assistPrefix)) this.#assistBlocks.delete(key);
|
|
9881
11605
|
for (const key of this.#assistFailures.keys()) if (key.startsWith(assistPrefix)) this.#assistFailures.delete(key);
|
|
@@ -9962,6 +11686,17 @@ var MmtpRouter = class {
|
|
|
9962
11686
|
this.#packetIdRoles.delete(packetId);
|
|
9963
11687
|
this.#catalogTrackNamesByPacketId.delete(packetId);
|
|
9964
11688
|
this.#repairOwnersByPacketId.delete(packetId);
|
|
11689
|
+
this.#overlayLegs.delete(packetId);
|
|
11690
|
+
for (const [repairPacketId, leg] of [...this.#overlayLegs]) {
|
|
11691
|
+
if (leg.sourcePacketId === packetId) this.#overlayLegs.delete(repairPacketId);
|
|
11692
|
+
}
|
|
11693
|
+
for (const [name, leg] of [...this.#overlayRepairNames]) {
|
|
11694
|
+
if (leg.sourcePacketId === packetId) this.#overlayRepairNames.delete(name);
|
|
11695
|
+
}
|
|
11696
|
+
this.#overlays.delete(packetId);
|
|
11697
|
+
for (const [repairPacketId, inst] of [...this.#overlays]) {
|
|
11698
|
+
if (inst.leg.sourcePacketId === packetId) this.#overlays.delete(repairPacketId);
|
|
11699
|
+
}
|
|
9965
11700
|
delete this.#pidCounts[packetId];
|
|
9966
11701
|
delete this.#repairPidCounts[packetId];
|
|
9967
11702
|
delete this.#moqRepairPidCounts[packetId];
|
|
@@ -10200,6 +11935,9 @@ var MmtpRouter = class {
|
|
|
10200
11935
|
this.#parserDiagnostics.clear();
|
|
10201
11936
|
this.#catalogTrackNamesByPacketId.clear();
|
|
10202
11937
|
this.#repairOwnersByPacketId.clear();
|
|
11938
|
+
this.#overlayLegs.clear();
|
|
11939
|
+
this.#overlayRepairNames.clear();
|
|
11940
|
+
this.#overlays.clear();
|
|
10203
11941
|
for (const { receiver } of this.#provenance.values()) receiver.release();
|
|
10204
11942
|
this.#provenance.clear();
|
|
10205
11943
|
this.#provenanceStreamFlow.clear();
|
|
@@ -10537,6 +12275,12 @@ var MmtpRouter = class {
|
|
|
10537
12275
|
debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
|
|
10538
12276
|
return;
|
|
10539
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
|
+
}
|
|
10540
12284
|
const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
|
|
10541
12285
|
if (verdict === "admitted") {
|
|
10542
12286
|
this.#noteFlowAdmitted(ingress.flow);
|
|
@@ -10554,7 +12298,17 @@ var MmtpRouter = class {
|
|
|
10554
12298
|
return;
|
|
10555
12299
|
}
|
|
10556
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
|
+
}
|
|
10557
12306
|
if (info.isRepair) {
|
|
12307
|
+
const overlay = this.#overlayRepairTarget(info.packetId, catalogTrackNameForPacket(ingress, info.packetId));
|
|
12308
|
+
if (overlay !== void 0) {
|
|
12309
|
+
this.#routeOverlayRepair(overlay, info.packetId, packet, parsed.header, source);
|
|
12310
|
+
return;
|
|
12311
|
+
}
|
|
10558
12312
|
const ownerPacketId = this.#resolveRepairOwnerPacketId(info.packetId, ingress);
|
|
10559
12313
|
if (ownerPacketId != null && this.#fecManager && hasAmbiguousCatalogTrackName(ingress, ownerPacketId)) return;
|
|
10560
12314
|
const repair = this.#readRepairObject(
|
|
@@ -10566,7 +12320,7 @@ var MmtpRouter = class {
|
|
|
10566
12320
|
);
|
|
10567
12321
|
if (!repair) return;
|
|
10568
12322
|
if (ownerPacketId != null && this.#fecManager) {
|
|
10569
|
-
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;
|
|
10570
12324
|
this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
|
|
10571
12325
|
const count = this.#repairPidCounts[info.packetId];
|
|
10572
12326
|
if (count <= 2 || count % 500 === 0) {
|
|
@@ -10761,6 +12515,17 @@ var MmtpRouter = class {
|
|
|
10761
12515
|
});
|
|
10762
12516
|
}
|
|
10763
12517
|
}
|
|
12518
|
+
if (!overlapCopy && this.#classifyPacketId(info.packetId) !== null) {
|
|
12519
|
+
this.#feedOverlaySource(
|
|
12520
|
+
info.packetId,
|
|
12521
|
+
catalogTrackName,
|
|
12522
|
+
packet,
|
|
12523
|
+
parsed.header,
|
|
12524
|
+
info.sequenceNumber,
|
|
12525
|
+
info.rapFlag,
|
|
12526
|
+
source
|
|
12527
|
+
);
|
|
12528
|
+
}
|
|
10764
12529
|
}
|
|
10765
12530
|
}
|
|
10766
12531
|
/**
|
|
@@ -11134,7 +12899,7 @@ var MmtpRouter = class {
|
|
|
11134
12899
|
}
|
|
11135
12900
|
// ── Private: FEC recovery routing ───────────────────────────────
|
|
11136
12901
|
/** Route WASM-recovered block through appropriate path based on container kind. */
|
|
11137
|
-
#routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete) {
|
|
12902
|
+
#routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
11138
12903
|
const packetSymbolSize = this.#symbolSizeForPacket(ownerPacketId);
|
|
11139
12904
|
if (ownerPacketId === void 0) {
|
|
11140
12905
|
throw new Error("[MmtpRouter] a recovered FEC block needs its owner packetId to read its fec.blockProfile");
|
|
@@ -11151,7 +12916,7 @@ var MmtpRouter = class {
|
|
|
11151
12916
|
ownerPacketId,
|
|
11152
12917
|
blockProfile
|
|
11153
12918
|
});
|
|
11154
|
-
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
|
|
12919
|
+
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted);
|
|
11155
12920
|
}
|
|
11156
12921
|
/**
|
|
11157
12922
|
* A recovered source packet of a protected flow is admitted on its own
|
|
@@ -11163,9 +12928,24 @@ var MmtpRouter = class {
|
|
|
11163
12928
|
* recovered packets is refused is unrecovered (7.2.7): its caller leaves it
|
|
11164
12929
|
* open, so it reaches its FEC deadline and fails by class. Always true
|
|
11165
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.
|
|
11166
12944
|
*/
|
|
11167
|
-
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete) {
|
|
11168
|
-
const
|
|
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;
|
|
11169
12949
|
let recovered = true;
|
|
11170
12950
|
if (receiver) {
|
|
11171
12951
|
const parsed = this.#stream(ownerPacketId, catalogTrackName).duplicates.parsedSymbols;
|
|
@@ -11177,6 +12957,8 @@ var MmtpRouter = class {
|
|
|
11177
12957
|
let admitted = false;
|
|
11178
12958
|
if (receiver === "ambiguous") {
|
|
11179
12959
|
this.#ambiguousRecoveredDrops++;
|
|
12960
|
+
} else if (userAgent) {
|
|
12961
|
+
admitted = true;
|
|
11180
12962
|
} else {
|
|
11181
12963
|
const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
|
|
11182
12964
|
wire.set(sym.rawSymbol);
|
|
@@ -11204,22 +12986,22 @@ var MmtpRouter = class {
|
|
|
11204
12986
|
catalogTrackName === void 0 || !byName ? byName?.values() : byName.has(catalogTrackName) ? [byName.get(catalogTrackName)] : []
|
|
11205
12987
|
);
|
|
11206
12988
|
if (flows.size > 1) return "ambiguous";
|
|
11207
|
-
for (const bound of flows) return this.#provenance.get(bound)
|
|
12989
|
+
for (const bound of flows) return this.#provenance.get(bound) ?? "ambiguous";
|
|
11208
12990
|
let match;
|
|
11209
|
-
for (const
|
|
11210
|
-
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))) {
|
|
11211
12993
|
if (match) return "ambiguous";
|
|
11212
|
-
match =
|
|
12994
|
+
match = entry;
|
|
11213
12995
|
}
|
|
11214
12996
|
}
|
|
11215
12997
|
return match;
|
|
11216
12998
|
}
|
|
11217
|
-
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName) {
|
|
12999
|
+
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
|
|
11218
13000
|
if (ownerPacketId == null) return;
|
|
11219
13001
|
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
11220
|
-
const
|
|
11221
|
-
|
|
11222
|
-
);
|
|
13002
|
+
const fecSbnOf = (index) => overlay ? {} : { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
|
|
13003
|
+
const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
|
|
13004
|
+
const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
11223
13005
|
if (ssIdSeqOffset !== void 0) {
|
|
11224
13006
|
result = {
|
|
11225
13007
|
...result,
|
|
@@ -11227,12 +13009,13 @@ var MmtpRouter = class {
|
|
|
11227
13009
|
const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
|
|
11228
13010
|
if (wire === sym.packetSequenceNumber) return sym;
|
|
11229
13011
|
this.#restampedRecoveredSeqs++;
|
|
13012
|
+
this.#restampedStreams.add(streamKey);
|
|
11230
13013
|
return { ...sym, packetSequenceNumber: wire };
|
|
11231
13014
|
})
|
|
11232
13015
|
};
|
|
11233
13016
|
}
|
|
11234
|
-
this.#stream(ownerPacketId, catalogTrackName).duplicates.advance(blockId);
|
|
11235
|
-
const deliveryProvenance = this.#fecManager?.blockDeliveryProvenance?.(
|
|
13017
|
+
if (!overlay) this.#stream(ownerPacketId, catalogTrackName).duplicates.advance(blockId);
|
|
13018
|
+
const deliveryProvenance = overlay?.deliveryProvenance ?? this.#fecManager?.blockDeliveryProvenance?.(
|
|
11236
13019
|
blockId,
|
|
11237
13020
|
ownerPacketId,
|
|
11238
13021
|
catalogTrackName
|
|
@@ -11310,7 +13093,7 @@ var MmtpRouter = class {
|
|
|
11310
13093
|
rapFlag: sym.fragment.rapFlag,
|
|
11311
13094
|
recovered: true,
|
|
11312
13095
|
deliveryProvenance,
|
|
11313
|
-
|
|
13096
|
+
...fecSbnOf(sym.index),
|
|
11314
13097
|
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
11315
13098
|
});
|
|
11316
13099
|
continue;
|
|
@@ -11422,7 +13205,7 @@ var MmtpRouter = class {
|
|
|
11422
13205
|
rapFlag: assembled.rapFlag,
|
|
11423
13206
|
recovered: true,
|
|
11424
13207
|
...assembled.deliveryProvenance === void 0 ? {} : { deliveryProvenance: assembled.deliveryProvenance },
|
|
11425
|
-
|
|
13208
|
+
...fecSbnOf(sym.index),
|
|
11426
13209
|
...recoveredCatalogTrackName === void 0 ? {} : { catalogTrackName: recoveredCatalogTrackName }
|
|
11427
13210
|
});
|
|
11428
13211
|
continue;
|
|
@@ -11559,7 +13342,7 @@ var MmtpRouter = class {
|
|
|
11559
13342
|
rapFlag: sym.fragment.rapFlag,
|
|
11560
13343
|
recovered: true,
|
|
11561
13344
|
deliveryProvenance,
|
|
11562
|
-
|
|
13345
|
+
...fecSbnOf(sym.index),
|
|
11563
13346
|
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
11564
13347
|
});
|
|
11565
13348
|
} else {
|
|
@@ -11638,7 +13421,7 @@ var MmtpRouter = class {
|
|
|
11638
13421
|
rapFlag: assembled.rapFlag,
|
|
11639
13422
|
recovered: true,
|
|
11640
13423
|
deliveryProvenance,
|
|
11641
|
-
|
|
13424
|
+
...fecSbnOf(sym.index),
|
|
11642
13425
|
...assembled.catalogTrackName === void 0 ? {} : { catalogTrackName: assembled.catalogTrackName }
|
|
11643
13426
|
});
|
|
11644
13427
|
}
|
|
@@ -11673,7 +13456,7 @@ var MmtpRouter = class {
|
|
|
11673
13456
|
...sym.fragment.sampleNumber === void 0 ? {} : { sampleNumber: sym.fragment.sampleNumber },
|
|
11674
13457
|
recovered: true,
|
|
11675
13458
|
deliveryProvenance,
|
|
11676
|
-
|
|
13459
|
+
...fecSbnOf(sym.index),
|
|
11677
13460
|
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
11678
13461
|
});
|
|
11679
13462
|
continue;
|
|
@@ -11717,6 +13500,8 @@ var MmtpRouter = class {
|
|
|
11717
13500
|
};
|
|
11718
13501
|
export {
|
|
11719
13502
|
KEYFRAME_LOST_TIMEOUT_SLACK,
|
|
11720
|
-
|
|
13503
|
+
KEYFRAME_OVERLAY_EXTENSION_TYPE,
|
|
13504
|
+
MmtpRouter,
|
|
13505
|
+
keyframeOverlaySsId
|
|
11721
13506
|
};
|
|
11722
13507
|
//# sourceMappingURL=mmtp-router.js.map
|