@blockcast/mmt-container 0.4.1 → 0.5.0-main.34c7b9c2cfa0

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.
Files changed (87) hide show
  1. package/README.md +2 -2
  2. package/dist/browser-runtime.js +3769 -364
  3. package/dist/browser-runtime.js.map +4 -4
  4. package/dist/cmcd-producer.d.ts +5 -1
  5. package/dist/cmcd-producer.d.ts.map +1 -1
  6. package/dist/cmcd-producer.js.map +2 -2
  7. package/dist/delivery-catalog.js +18 -2
  8. package/dist/delivery-catalog.js.map +2 -2
  9. package/dist/fec-manager.d.ts +21 -16
  10. package/dist/fec-manager.d.ts.map +1 -1
  11. package/dist/fec-manager.js +197 -80
  12. package/dist/fec-manager.js.map +3 -3
  13. package/dist/held-layer-admission.d.ts +58 -0
  14. package/dist/held-layer-admission.d.ts.map +1 -0
  15. package/dist/held-layer-admission.js +105 -0
  16. package/dist/held-layer-admission.js.map +7 -0
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js.map +2 -2
  20. package/dist/mmtp-container-parser.d.ts +10 -0
  21. package/dist/mmtp-container-parser.d.ts.map +1 -1
  22. package/dist/mmtp-container-parser.js +31 -1
  23. package/dist/mmtp-container-parser.js.map +2 -2
  24. package/dist/mmtp-container-parser.no-modules.js +2713 -1024
  25. package/dist/mmtp-container-parser.no-modules.js.map +4 -4
  26. package/dist/mmtp-decode-pipeline.js +31 -1
  27. package/dist/mmtp-decode-pipeline.js.map +2 -2
  28. package/dist/mmtp-fec-session.d.ts +36 -3
  29. package/dist/mmtp-fec-session.d.ts.map +1 -1
  30. package/dist/mmtp-fec-session.js +114 -5
  31. package/dist/mmtp-fec-session.js.map +3 -3
  32. package/dist/mmtp-router.d.ts +96 -29
  33. package/dist/mmtp-router.d.ts.map +1 -1
  34. package/dist/mmtp-router.js +522 -205
  35. package/dist/mmtp-router.js.map +2 -2
  36. package/dist/multicast-auth-catalog.d.ts +10 -2
  37. package/dist/multicast-auth-catalog.d.ts.map +1 -1
  38. package/dist/multicast-auth-catalog.js +422 -22
  39. package/dist/multicast-auth-catalog.js.map +4 -4
  40. package/dist/multicast-manager.d.ts +78 -11
  41. package/dist/multicast-manager.d.ts.map +1 -1
  42. package/dist/multicast-manager.js +3663 -333
  43. package/dist/multicast-manager.js.map +4 -4
  44. package/dist/multicast-transport.d.ts +4 -1
  45. package/dist/multicast-transport.d.ts.map +1 -1
  46. package/dist/multicast-transport.js +20 -3
  47. package/dist/multicast-transport.js.map +2 -2
  48. package/dist/no-modules.d.ts +1 -2
  49. package/dist/no-modules.d.ts.map +1 -1
  50. package/dist/no-modules.js +2699 -1008
  51. package/dist/no-modules.js.map +4 -4
  52. package/dist/repair-fetch.d.ts +50 -10
  53. package/dist/repair-fetch.d.ts.map +1 -1
  54. package/dist/repair-fetch.js +62 -20
  55. package/dist/repair-fetch.js.map +2 -2
  56. package/dist/wasm-mmtp-container-parser.js +31 -1
  57. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  58. package/package.json +7 -15
  59. package/src/cmcd-producer.ts +6 -1
  60. package/src/fec-manager.ts +147 -131
  61. package/src/held-layer-admission.ts +161 -0
  62. package/src/index.ts +6 -9
  63. package/src/mmtp-container-parser.ts +48 -0
  64. package/src/mmtp-fec-session.ts +76 -3
  65. package/src/mmtp-router.ts +637 -274
  66. package/src/multicast-auth-catalog.ts +81 -8
  67. package/src/multicast-manager.ts +274 -49
  68. package/src/multicast-transport.ts +6 -2
  69. package/src/no-modules.ts +1 -6
  70. package/src/repair-fetch.ts +154 -34
  71. package/test-vectors/catalog/negative/multicast-auth-padded-key.json +0 -1
  72. package/test-vectors/catalog/negative/multicast-auth-reused-track-format.json +0 -1
  73. package/test-vectors/catalog/negative/multicast-auth-root-dotted-key.json +0 -1
  74. package/test-vectors/catalog/negative/multicast-auth-trailing-pad-bits.json +0 -1
  75. package/test-vectors/repair-policy-v1.json +113 -0
  76. package/dist/manifest-auth-plan.d.ts +0 -31
  77. package/dist/manifest-auth-plan.d.ts.map +0 -1
  78. package/dist/manifest-auth-plan.js +0 -426
  79. package/dist/manifest-auth-plan.js.map +0 -7
  80. package/dist/source-auth.d.ts +0 -2
  81. package/dist/source-auth.d.ts.map +0 -1
  82. package/dist/source-auth.js +0 -3912
  83. package/dist/source-auth.js.map +0 -7
  84. package/src/manifest-auth-plan.ts +0 -55
  85. package/src/source-auth.ts +0 -1
  86. package/test-vectors/generate-source-auth-fixture.mjs +0 -114
  87. package/test-vectors/source-auth-v1.json +0 -61
@@ -22,15 +22,23 @@
22
22
  * everything.
23
23
  *
24
24
  * Priority (from the block's class, see `repairPriorityForClass`):
25
- * - P0 (keyframe / unknown) and P1 (audio) dispatch as soon as they are
26
- * enqueued, with no cap. Each has its own fail deadline, and a cap would
27
- * turn a recoverable protected block into a stall; their number is
28
- * already bounded by the blocks one fail window can hold.
29
- * - P2 (delta) dispatches only while no P0/P1 is queued or in flight,
30
- * earliest absolute fail deadline first. Enqueuing a P0/P1 aborts every
31
- * in-flight P2 (`repair-preempted`), which returns to the queue while its
32
- * block is unresolved. A P2 that never ran before its block resolved is
33
- * reported as `repair-deferred`.
25
+ * - P0 (keyframe / unknown) dispatches as soon as it is enqueued, with no
26
+ * cap. Each has its own fail deadline, and a cap would turn a
27
+ * recoverable protected block into a stall; their number is already
28
+ * bounded by the blocks one fail window can hold. A P0 block that lies
29
+ * below a RAP the receiver already holds complete is not requested at
30
+ * all (`moot-rap`), and a P0 FETCH still running when such a RAP
31
+ * arrives is cancelled (`repair-stopped`, `moot`): the receiver resumes
32
+ * from that RAP (draft-ramadan-moq-fec §11.4.2).
33
+ * - P1 (audio) dispatches, uncapped, while no P0 is queued or in flight;
34
+ * P2 (delta) only while no P0/P1 is, earliest absolute fail deadline
35
+ * first (§11.4.5 item 3).
36
+ * - Only a P0 FETCH the scheduler actually sends aborts the in-flight P1
37
+ * and P2 FETCHes (`repair-preempted`), which return to the queue while
38
+ * their blocks are unresolved: a keyframe request that is refused, moot
39
+ * or otherwise never sent cancels nothing (§11.4.5 item 3), and an audio
40
+ * request never preempts. A P2 that never ran before its block resolved
41
+ * is reported as `repair-deferred`.
34
42
  * - A block reclassified to a higher class while its P2 fetch is in flight
35
43
  * is re-issued at once under the new class's wire priority: the running
36
44
  * FETCH is aborted (FETCH_CANCEL) and a new one sent.
@@ -394,6 +402,18 @@ export type RepairSkipReason =
394
402
  | 'presentation-delay-unavailable'
395
403
  /** A lost SS_ID in an undelimited span had no §7.5 anchor to extend it from. */
396
404
  | 'range-no-anchor'
405
+ /**
406
+ * The block lost more sources than its repair group carries symbols
407
+ * (sources + RSB_length < K'): no FETCH of the group can complete it, and
408
+ * when multicast already delivered the whole group the relay could serve
409
+ * only duplicates (BLO-50082).
410
+ */
411
+ | 'group-cannot-complete'
412
+ /**
413
+ * The keyframe-bearing block lies below a RAP the receiver already holds
414
+ * complete, so its repair would be moot (draft-ramadan-moq-fec §11.4.2).
415
+ */
416
+ | 'moot-rap'
397
417
 
398
418
  /**
399
419
  * Which term set a FETCH's deadline (BLO-33597): the multicast wait, the
@@ -477,6 +497,8 @@ export interface RepairFetchHost {
477
497
  closeIsoHoles?(packetId: number, catalogTrackName: string | undefined, start: number, length: number): void
478
498
  clearRepairForBlock(sbn: number, packetId: number, catalogTrackName?: string): void
479
499
  processRepairFromMoQ(packet: Uint8Array, ownerPacketId?: number, catalogTrackName?: string): void
500
+ /** This scheduler can request unicast repair: protected flows keep pending entries for it (optional). */
501
+ setUnicastRepair?(enabled: boolean): void
480
502
  noteRepairScheduling(
481
503
  type: 'repair-preempted' | 'repair-deferred',
482
504
  sbn: number,
@@ -504,6 +526,23 @@ export interface RepairFetchHost {
504
526
  packetId: number,
505
527
  catalogTrackName?: string,
506
528
  ): void
529
+ /** A running FETCH the scheduler stopped, and why (optional). */
530
+ noteRepairStopped?(
531
+ reason: RepairStopReason,
532
+ sbn: number,
533
+ packetId: number,
534
+ catalogTrackName?: string,
535
+ ): void
536
+ /**
537
+ * Whether a complete RAP above SS_ID `ssId` already reached the stream
538
+ * (optional; absent, none ever has). Under the bc-provenance profile only
539
+ * an admitted RAP counts (multicast-01 §7.2.7).
540
+ */
541
+ rapCompleteAbove?(packetId: number, catalogTrackName: string | undefined, ssId: number): boolean
542
+ /** Observe each complete RAP reaching a stream (optional; called synchronously). */
543
+ addRapCompleteObserver?(
544
+ observer: (packetId: number, catalogTrackName: string | undefined, ssId: number) => void,
545
+ ): () => void
507
546
  /**
508
547
  * A FETCH the scheduler admitted and handed to the transport (optional):
509
548
  * every attempt, a retry or a reprioritised restart included.
@@ -517,7 +556,22 @@ export interface RepairFetchHost {
517
556
  ): void
518
557
  }
519
558
 
520
- type StopReason = 'preempted' | 'reprioritized' | 'expired' | 'resolved' | 'reset' | 'disposed'
559
+ /**
560
+ * Why the scheduler ended a running FETCH (the transport cancels it on the
561
+ * wire): preempted by a sent keyframe FETCH, reissued at a new class's
562
+ * priority, past its deadline, its block resolved, track reset or a newer
563
+ * request for the block, dispose, or `moot` (a RAP above the block arrived
564
+ * complete first, draft-ramadan-moq-fec §11.4.2).
565
+ */
566
+ export type RepairStopReason =
567
+ | 'preempted'
568
+ | 'reprioritized'
569
+ | 'expired'
570
+ | 'resolved'
571
+ | 'reset'
572
+ | 'disposed'
573
+ | 'moot'
574
+ type StopReason = RepairStopReason
521
575
 
522
576
  interface FetchRun {
523
577
  readonly controller: AbortController
@@ -592,6 +646,7 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
592
646
  ) {
593
647
  throw new Error('[RepairFetch] rangeTransport needs repairEpochReference and a host that reports undelimited losses')
594
648
  }
649
+ mc.setUnicastRepair?.(true)
595
650
  const entries = new Map<string, RepairEntry>()
596
651
  const warnedNoTarget = new Set<string>()
597
652
  let disposed = false
@@ -629,9 +684,43 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
629
684
  entry.running = null
630
685
  run.stopped = reason
631
686
  run.controller.abort()
632
- // A preempted delta returns to the queue; until it runs again nothing is
687
+ // A preempted request returns to the queue; until it runs again nothing is
633
688
  // in flight that could land, so the block goes back to its multicast wait.
634
689
  if (reason === 'preempted') releaseHold(entry)
690
+ mc.noteRepairStopped?.(reason, entry.sbn, entry.packetId, entry.catalogTrackName)
691
+ }
692
+
693
+ /** The last SS_ID the entry covers, or undefined while its block is undelimited. */
694
+ const lastSsId = (entry: RepairEntry): number | undefined => {
695
+ if (entry.range) return entry.sbn + entry.range.holeLength - 1
696
+ const length = symbolCounts(entry.sbn, entry.packetId, entry.catalogTrackName)?.authoredLength
697
+ return length === undefined ? undefined : entry.sbn + length - 1
698
+ }
699
+
700
+ /** A keyframe request whose whole block lies below a RAP already held complete. */
701
+ const moot = (entry: RepairEntry): boolean => {
702
+ if (entry.priority !== 0) return false
703
+ const last = lastSsId(entry)
704
+ if (last === undefined) return false
705
+ try {
706
+ return mc.rapCompleteAbove?.(entry.packetId, entry.catalogTrackName, last) === true
707
+ } catch {
708
+ return false
709
+ }
710
+ }
711
+
712
+ /** Drop a request no FETCH will be sent for, and say why. */
713
+ const skip = (entry: RepairEntry, reason: RepairSkipReason, rttMs: number | null, budgetMs: number): void => {
714
+ retire(entry)
715
+ releaseHold(entry)
716
+ clearEntry(entry)
717
+ mc.noteRepairSkipped(
718
+ reason,
719
+ { rttMs, budgetMs, priority: entry.priority },
720
+ entry.sbn,
721
+ entry.packetId,
722
+ entry.catalogTrackName,
723
+ )
635
724
  }
636
725
 
637
726
  /** The keyframe budget for a P0 block, or null: absent, throwing or not positive. */
@@ -656,24 +745,24 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
656
745
  }
657
746
 
658
747
  /**
659
- * RTT admission: send only a FETCH the measured round trip says can land
660
- * before the entry's deadline. A refused entry is released, not queued: the
748
+ * Admission: send only a FETCH whose repair group can complete the block
749
+ * and that the measured round trip says can land before the entry's
750
+ * deadline. A refused entry is released, not queued: the
661
751
  * deadline only gets closer. Returns the admitting RTT, or null when refused.
662
752
  */
663
753
  const admit = (entry: RepairEntry, now: number): number | null => {
664
754
  const budgetMs = entry.deadlineAt - now
665
755
  const rttMs = readRtt()
666
- if (rttMs !== null && rttMs < budgetMs) return rttMs
667
- retire(entry)
668
- releaseHold(entry)
669
- clearEntry(entry)
670
- mc.noteRepairSkipped(
671
- rttMs === null ? 'rtt-unmeasured' : 'rtt-exceeds-budget',
672
- { rttMs, budgetMs, priority: entry.priority },
673
- entry.sbn,
674
- entry.packetId,
675
- entry.catalogTrackName,
676
- )
756
+ const reason: RepairSkipReason | null =
757
+ !entry.range && !groupCanComplete(entry.sbn, entry.packetId, entry.catalogTrackName)
758
+ ? 'group-cannot-complete'
759
+ : rttMs === null
760
+ ? 'rtt-unmeasured'
761
+ : rttMs < budgetMs
762
+ ? null
763
+ : 'rtt-exceeds-budget'
764
+ if (reason === null) return rttMs
765
+ skip(entry, reason, rttMs, budgetMs)
677
766
  return null
678
767
  }
679
768
 
@@ -682,16 +771,21 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
682
771
  if (entry.expiry !== undefined) clearTimeout(entry.expiry)
683
772
  }
684
773
 
685
- const urgentOutstanding = (): boolean => {
774
+ /** Whether any entry of priority below `priority` is queued or in flight. */
775
+ const outstandingBelow = (priority: RepairPriority): boolean => {
686
776
  for (const entry of entries.values()) {
687
- if (entry.priority < 2) return true
777
+ if (entry.priority < priority) return true
688
778
  }
689
779
  return false
690
780
  }
691
781
 
692
- const preemptDeltas = (): void => {
782
+ /**
783
+ * A keyframe FETCH is being sent: cancel every in-flight audio and delta
784
+ * FETCH (§11.4.5 item 3). Called only for a FETCH actually sent.
785
+ */
786
+ const preemptBestEffort = (): void => {
693
787
  for (const entry of entries.values()) {
694
- if (entry.priority !== 2 || !entry.running) continue
788
+ if (entry.priority === 0 || !entry.running) continue
695
789
  stop(entry, 'preempted')
696
790
  note('repair-preempted', entry)
697
791
  }
@@ -721,8 +815,14 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
721
815
  /** `counted` is false for a reprioritisation restart, which is no retry. */
722
816
  const start = (entry: RepairEntry, counted = true): void => {
723
817
  const now = performance.now()
818
+ if (moot(entry)) {
819
+ skip(entry, 'moot-rap', readRtt(), entry.deadlineAt - now)
820
+ return
821
+ }
724
822
  const rttMs = admit(entry, now)
725
823
  if (rttMs === null) return
824
+ // Before the transport sees this request, so the cancels go out first.
825
+ if (entry.priority === 0) preemptBestEffort()
726
826
  if (counted) entry.attempts++
727
827
  mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
728
828
  rttMs,
@@ -756,7 +856,7 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
756
856
  // A fetch still running at its block's fail bound can no longer land
757
857
  // in time. Cancel it here rather than trust the transport or the FEC
758
858
  // layer's resolution to end it: a hung P0/P1 left running would keep
759
- // urgentOutstanding() true and gate every P2 for the session.
859
+ // outstandingBelow() true and gate every P1/P2 for the session.
760
860
  if (entry.deadlineAt <= now) {
761
861
  stop(entry, 'expired')
762
862
  retire(entry)
@@ -775,9 +875,13 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
775
875
  queued.push(entry)
776
876
  }
777
877
  for (const entry of queued) {
778
- if (entry.priority < 2) start(entry)
878
+ if (entry.priority === 0) start(entry)
779
879
  }
780
- if (urgentOutstanding()) return
880
+ if (outstandingBelow(1)) return
881
+ for (const entry of queued) {
882
+ if (entry.priority === 1) start(entry)
883
+ }
884
+ if (outstandingBelow(2)) return
781
885
  queued
782
886
  .filter((entry) => entry.priority === 2)
783
887
  .sort((a, b) => a.deadlineAt - b.deadlineAt || a.order - b.order)
@@ -961,7 +1065,6 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
961
1065
  stop(entry, 'reprioritized')
962
1066
  start(entry, false)
963
1067
  }
964
- preemptDeltas()
965
1068
  dispatch()
966
1069
  return
967
1070
  }
@@ -1137,7 +1240,6 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
1137
1240
  }
1138
1241
  entries.set(key, entry)
1139
1242
  noteBudget(entry, deadline.extensionMs)
1140
- if (entry.priority < 2) preemptDeltas()
1141
1243
  dispatch()
1142
1244
  })
1143
1245
 
@@ -1226,15 +1328,33 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
1226
1328
  }
1227
1329
  entries.set(entry.key, entry)
1228
1330
  entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1)
1229
- preemptDeltas()
1230
1331
  dispatch()
1231
1332
  })
1232
1333
  }
1233
1334
 
1335
+ // A RAP arrived complete: a keyframe FETCH below it is moot (§11.4.2). The
1336
+ // observer runs inside the frame push, so the cancel waits for a microtask
1337
+ // rather than re-entering the FEC layer through the hold release.
1338
+ const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {
1339
+ queueMicrotask(() => {
1340
+ if (disposed) return
1341
+ for (const entry of [...entries.values()]) {
1342
+ if (entry.packetId !== packetId || !entry.running || !moot(entry)) continue
1343
+ stop(entry, 'moot')
1344
+ retire(entry)
1345
+ releaseHold(entry)
1346
+ clearEntry(entry)
1347
+ }
1348
+ dispatch()
1349
+ })
1350
+ })
1351
+
1234
1352
  return () => {
1235
1353
  disposed = true
1354
+ mc.setUnicastRepair?.(false)
1236
1355
  removeResetObserver()
1237
1356
  removeLifecycleObserver()
1357
+ removeRapObserver?.()
1238
1358
  for (const entry of [...entries.values()]) {
1239
1359
  clearEntry(entry)
1240
1360
  if (entry.running) releaseHold(entry)
@@ -20,7 +20,6 @@
20
20
  ],
21
21
  "multicast.auth": {
22
22
  "scheme": "bc-provenance",
23
- "hash": "blake3",
24
23
  "sig": "ed25519",
25
24
  "publicKey": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=",
26
25
  "keyId": "2026-08",
@@ -30,7 +30,6 @@
30
30
  ],
31
31
  "multicast.auth": {
32
32
  "scheme": "bc-provenance",
33
- "hash": "blake3",
34
33
  "sig": "ed25519",
35
34
  "publicKey": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg",
36
35
  "keyId": "2026-08",
@@ -30,7 +30,6 @@
30
30
  ],
31
31
  "multicast.auth": {
32
32
  "scheme": "bc-provenance",
33
- "hash": "blake3",
34
33
  "sig": "ed25519",
35
34
  "publicKey": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg",
36
35
  "keyId": "2026-08",
@@ -20,7 +20,6 @@
20
20
  ],
21
21
  "multicast.auth": {
22
22
  "scheme": "bc-provenance",
23
- "hash": "blake3",
24
23
  "sig": "ed25519",
25
24
  "publicKey": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbh",
26
25
  "keyId": "2026-08",
@@ -0,0 +1,113 @@
1
+ {
2
+ "version": 1,
3
+ "description": "Receiver repair policy (draft-ramadan-moq-fec-01 Section 11.4.2 and Section 11.4.5 item 3, Blockcast/moqcast-draft PR #2 @ 3162ef93). Which repair FETCHes a receiver sends, which it cancels, and which it never sends. Hand-authored from the specification, NOT generated by any implementation (BLO-49776).",
4
+ "scope": [
5
+ "Section 11.4 applies only to a multicast or AMT leg with a MoQ session (fec-01 11.4). These cases describe that leg; a receiver with no repair FETCH path records ABSENT, not a pass.",
6
+ "schedulerCases drive a repair scheduler. Every block spans K' = 4 SS_IDs, so block B covers SS_IDs B..B+3. Track video is a source track (packetId 1, keyframe or delta blocks); track audio is packetId 2 (audio blocks). A block is named track/SS_Start.",
7
+ "Best-effort (delta and audio) requests in schedulerCases are presumed covered by unexpired credit under fec-01 11.4.5 item 2; that credit is a precondition of each case, not something the case exercises.",
8
+ "A RAP 'held complete at ssId' is a keyframe access unit at SS_ID ssId that the receiver holds complete: admitted complete under the multicast-01 7.2.7 profile, received or recovered complete outside it. A block lies below it when the block's last SS_ID is less than ssId; a RAP at or inside the block does not make the block moot.",
9
+ "fetched lists every FETCH the receiver sends, in order. cancelled lists every sent FETCH it then cancels (FETCH_CANCEL on the wire). skipped names requests made but not sent, with the receiver's reason; stopped names sent FETCHes stopped, with the stop reason. A list not given is expected empty.",
10
+ "assistedCases drive one block of a protected flow (multicast-01 7.2.7) that is one symbol short of SSB_length, with unlisted repair symbols pooled for it. Each unlisted symbol triggers one unlisted-assisted decode in a scratch decoder; outcome 'fail' is a failed decode, 'recover' a decode whose recovered data unit admission accepts, 'recover-unadmitted' a decode whose recovered data unit admission refuses.",
11
+ "status names where a case's rule comes from: 'fec-01' is a -01 requirement; 'libmmt interpretation' is our reading where -01 is silent; 'regression' rows were already required at moqcast-draft main 615a9a8 and are kept to pin standing behaviour.",
12
+ "A keyframe request refused before its FETCH is sent (libmmt's reliable-path RTT check) is libmmt-local and deviates from the fec-01 11.4.2 MUST, so it is pinned in libmmt's own suite (repair-fetch-keyframe-budget.test.ts), not here. The 11.4.5 item 3 rule that a block 11.4.2 does not request cancels nothing is exercised by keyframe-below-complete-rap-not-requested."
13
+ ],
14
+ "schedulerCases": [
15
+ {
16
+ "name": "keyframe-below-complete-rap-not-requested",
17
+ "status": "fec-01",
18
+ "rule": "fec-01 11.4.2: a keyframe-bearing block at its FEC deadline is requested unless it lies below a RAP already held complete; 11.4.5 item 3: a block whose repair 11.4.2 does not request cancels nothing (case a).",
19
+ "ops": [
20
+ { "op": "fire", "track": "video", "block": 20, "class": "delta" },
21
+ { "op": "rapHeld", "ssId": 4 },
22
+ { "op": "fire", "track": "video", "block": 0, "class": "keyframe" }
23
+ ],
24
+ "expect": {
25
+ "fetched": ["video/20"],
26
+ "skipped": [{ "block": "video/0", "reason": "moot-rap" }]
27
+ }
28
+ },
29
+ {
30
+ "name": "control-rap-inside-block-is-requested",
31
+ "status": "libmmt interpretation",
32
+ "rule": "Control for the case above. fec-01 does not define 'lies below'; libmmt reads it so that a RAP at the block's own last SS_ID is part of the block, so the block is not below it and is requested.",
33
+ "ops": [
34
+ { "op": "rapHeld", "ssId": 3 },
35
+ { "op": "fire", "track": "video", "block": 0, "class": "keyframe" }
36
+ ],
37
+ "expect": { "fetched": ["video/0"] }
38
+ },
39
+ {
40
+ "name": "rap-completes-mid-fetch-cancels",
41
+ "status": "regression",
42
+ "rule": "fec-01 11.4.2 (already a SHOULD at main 615a9a8): a RAP completing while a keyframe repair is outstanding makes that repair moot and the receiver cancels it; a block above the RAP keeps its FETCH (case b).",
43
+ "ops": [
44
+ { "op": "fire", "track": "video", "block": 0, "class": "keyframe" },
45
+ { "op": "fire", "track": "video", "block": 8, "class": "keyframe" },
46
+ { "op": "rapComplete", "ssId": 5 }
47
+ ],
48
+ "expect": {
49
+ "fetched": ["video/0", "video/8"],
50
+ "cancelled": ["video/0"],
51
+ "stopped": [{ "block": "video/0", "reason": "moot" }]
52
+ }
53
+ },
54
+ {
55
+ "name": "audio-request-preempts-nothing",
56
+ "status": "fec-01",
57
+ "rule": "fec-01 11.4.5 item 3: only the keyframe repair 11.4.2 requests cancels best-effort FETCHes; an audio request cancels nothing.",
58
+ "ops": [
59
+ { "op": "fire", "track": "video", "block": 20, "class": "delta" },
60
+ { "op": "fire", "track": "audio", "block": 0, "class": "audio" }
61
+ ],
62
+ "expect": { "fetched": ["video/20", "audio/0"] }
63
+ },
64
+ {
65
+ "name": "sent-keyframe-repair-cancels-delta-and-audio",
66
+ "status": "regression",
67
+ "rule": "fec-01 11.4.5 item 3 (standing at main 615a9a8): a keyframe repair FETCH that is sent cancels the outstanding delta and audio FETCHes (case c-pos).",
68
+ "ops": [
69
+ { "op": "fire", "track": "video", "block": 20, "class": "delta" },
70
+ { "op": "fire", "track": "audio", "block": 0, "class": "audio" },
71
+ { "op": "fire", "track": "video", "block": 40, "class": "keyframe" }
72
+ ],
73
+ "expect": {
74
+ "fetched": ["video/20", "audio/0", "video/40"],
75
+ "cancelled": ["video/20", "audio/0"],
76
+ "stopped": [
77
+ { "block": "video/20", "reason": "preempted" },
78
+ { "block": "audio/0", "reason": "preempted" }
79
+ ]
80
+ }
81
+ }
82
+ ],
83
+ "assistedCases": [
84
+ {
85
+ "name": "two-failed-assisted-decodes-then-refused",
86
+ "status": "fec-01",
87
+ "rule": "multicast-01 7.2.7 step 2, referenced by fec-01 11.4: at most two unlisted-assisted decodes may fail per block; a failed decode's pooled symbol is discarded and a fresh one retried; a third is refused (case d).",
88
+ "decodes": ["fail", "fail", "fail"],
89
+ "expect": { "scratchDecodes": 2, "refused": 1, "recovered": false }
90
+ },
91
+ {
92
+ "name": "assisted-decode-recovers-on-retry",
93
+ "status": "fec-01",
94
+ "rule": "Control for the case above: after one failed decode, the retry on a fresh pooled symbol recovers the block (case d).",
95
+ "decodes": ["fail", "recover"],
96
+ "expect": { "scratchDecodes": 2, "refused": 0, "recovered": true }
97
+ },
98
+ {
99
+ "name": "recovered-but-unadmitted-is-unrecovered",
100
+ "status": "fec-01",
101
+ "rule": "multicast-01 7.2.7 / fec-01 11.4.2: a data unit recovered but refused by admission does not complete the block; the block stays unrecovered and is still due its repair at the FEC deadline (case e).",
102
+ "decodes": ["recover-unadmitted"],
103
+ "expect": { "scratchDecodes": 1, "refused": 0, "recovered": false }
104
+ },
105
+ {
106
+ "name": "unadmitted-recovery-counts-toward-decode-cap",
107
+ "status": "fec-01",
108
+ "rule": "multicast-01 7.2.7: a decode none of whose recovered packets is admitted recovers nothing, so it counts toward the two-decode cap like a failed decode (case e).",
109
+ "decodes": ["recover-unadmitted", "fail", "fail"],
110
+ "expect": { "scratchDecodes": 2, "refused": 1, "recovered": false }
111
+ }
112
+ ]
113
+ }
@@ -1,31 +0,0 @@
1
- import type { ManifestKey } from '@blockcast/mmt-manifest-verify';
2
- import { type ResolvedMulticastAuthFlow } from './multicast-auth-catalog.js';
3
- /**
4
- * The bc-provenance version 2 plan a receiver binds from one catalog
5
- * (draft-ramadan-moq-multicast-01 7.2.6): the key set, `minEpoch`, and one
6
- * flow per protected endpoint. The record verifier and the 7.2.7 state machine
7
- * consume it; this plan holds no verification state.
8
- */
9
- export interface ManifestAuthPlan {
10
- keys: readonly ManifestKey[];
11
- minEpoch: number;
12
- flows: readonly ResolvedMulticastAuthFlow[];
13
- /** Every `manifestTrack` a flow names, for a unicast receiver to subscribe. */
14
- manifestTracks: readonly string[];
15
- /** The key that signs a record of `kid` at `epoch`; `undefined` drops it.
16
- * Key selection only: it does not apply `minEpoch`, which the record
17
- * verifier checks (BLO-49771). */
18
- keyFor(kid: number, epoch: number): Uint8Array | undefined;
19
- /** The flow whose records arrive on `(S, G, manifestPort)`. */
20
- flowForManifestPort(source: string, groupAddress: string, manifestPort: number): ResolvedMulticastAuthFlow | undefined;
21
- /** The flow a manifest track mirrors. */
22
- flowForManifestTrack(manifestTrack: string): ResolvedMulticastAuthFlow | undefined;
23
- }
24
- /** Build the plan, or `undefined` for an unprotected catalog. Throws where
25
- * `resolveMulticastAuthCatalog` does: no key set, or authentication pinned
26
- * by `requireAuth` on a catalog without `multicast.auth`. */
27
- export declare function createManifestAuthPlan(catalog: unknown, options?: {
28
- requireAuth?: boolean;
29
- catalogNamespace?: string;
30
- }): ManifestAuthPlan | undefined;
31
- //# sourceMappingURL=manifest-auth-plan.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"manifest-auth-plan.d.ts","sourceRoot":"","sources":["../src/manifest-auth-plan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAA;AACjE,OAAO,EAIL,KAAK,yBAAyB,EAC/B,MAAM,6BAA6B,CAAA;AAEpC;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,SAAS,WAAW,EAAE,CAAA;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,SAAS,yBAAyB,EAAE,CAAA;IAC3C,+EAA+E;IAC/E,cAAc,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC;;sCAEkC;IAClC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAA;IAC1D,+DAA+D;IAC/D,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,yBAAyB,GAAG,SAAS,CAAA;IACtH,yCAAyC;IACzC,oBAAoB,CAAC,aAAa,EAAE,MAAM,GAAG,yBAAyB,GAAG,SAAS,CAAA;CACnF;AAED;;6DAE6D;AAC7D,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAO,GACjE,gBAAgB,GAAG,SAAS,CAkB9B"}