@blockcast/mmt-container 1.0.1 → 2.0.0-main.609e08d7e29f

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 (101) hide show
  1. package/README.md +9 -7
  2. package/dist/browser-runtime.js +6972 -6693
  3. package/dist/browser-runtime.js.map +4 -4
  4. package/dist/carp-catalog.d.ts +7 -7
  5. package/dist/carp-catalog.d.ts.map +1 -1
  6. package/dist/carp-catalog.js +65 -230
  7. package/dist/carp-catalog.js.map +3 -3
  8. package/dist/catalog-fec-params.js.map +1 -1
  9. package/dist/catalog-network-source.js.map +1 -1
  10. package/dist/delivery-catalog.js +3 -0
  11. package/dist/delivery-catalog.js.map +2 -2
  12. package/dist/delta-fallback-config.js.map +1 -1
  13. package/dist/fec-manager.d.ts +25 -1
  14. package/dist/fec-manager.d.ts.map +1 -1
  15. package/dist/fec-manager.js +208 -15
  16. package/dist/fec-manager.js.map +2 -2
  17. package/dist/fec-state.js.map +1 -1
  18. package/dist/fec-worker-setup.js.map +1 -1
  19. package/dist/held-layer-admission.d.ts +8 -0
  20. package/dist/held-layer-admission.d.ts.map +1 -1
  21. package/dist/held-layer-admission.js +22 -1
  22. package/dist/held-layer-admission.js.map +2 -2
  23. package/dist/iso-block-locator.js.map +1 -1
  24. package/dist/mfu-reassembler.js.map +1 -1
  25. package/dist/mmtp-container-parser.js.map +1 -1
  26. package/dist/mmtp-container-parser.no-modules.js +146 -332
  27. package/dist/mmtp-container-parser.no-modules.js.map +4 -4
  28. package/dist/mmtp-decode-pipeline.js.map +1 -1
  29. package/dist/mmtp-fec-session.d.ts.map +1 -1
  30. package/dist/mmtp-fec-session.js +61 -15
  31. package/dist/mmtp-fec-session.js.map +3 -3
  32. package/dist/mmtp-router.d.ts +11 -6
  33. package/dist/mmtp-router.d.ts.map +1 -1
  34. package/dist/mmtp-router.js +47 -10
  35. package/dist/mmtp-router.js.map +3 -3
  36. package/dist/multicast-auth-catalog.d.ts +8 -8
  37. package/dist/multicast-auth-catalog.d.ts.map +1 -1
  38. package/dist/multicast-auth-catalog.js +171 -75
  39. package/dist/multicast-auth-catalog.js.map +4 -4
  40. package/dist/multicast-input.d.ts +5 -5
  41. package/dist/multicast-input.d.ts.map +1 -1
  42. package/dist/multicast-input.js +126 -49
  43. package/dist/multicast-input.js.map +3 -3
  44. package/dist/multicast-manager.d.ts +11 -4
  45. package/dist/multicast-manager.d.ts.map +1 -1
  46. package/dist/multicast-manager.js +7914 -7659
  47. package/dist/multicast-manager.js.map +4 -4
  48. package/dist/multicast-transport.d.ts +16 -1
  49. package/dist/multicast-transport.d.ts.map +1 -1
  50. package/dist/multicast-transport.js +120 -5
  51. package/dist/multicast-transport.js.map +4 -4
  52. package/dist/multicast-unwrap.js +2 -2
  53. package/dist/multicast-unwrap.js.map +2 -2
  54. package/dist/no-modules.js +100 -288
  55. package/dist/no-modules.js.map +4 -4
  56. package/dist/repair-fetch.js.map +1 -1
  57. package/dist/signaling.js.map +1 -1
  58. package/dist/timestamp-codec.js.map +1 -1
  59. package/dist/wasm-mfu-reassembler.js.map +1 -1
  60. package/dist/wasm-mmtp-container-parser.js.map +1 -1
  61. package/package.json +5 -5
  62. package/schemas/catalog.schema.json +15 -7
  63. package/src/carp-catalog.ts +14 -13
  64. package/src/fec-manager.ts +89 -14
  65. package/src/held-layer-admission.ts +38 -1
  66. package/src/mmtp-fec-session.ts +20 -12
  67. package/src/mmtp-router.ts +65 -16
  68. package/src/multicast-auth-catalog.ts +43 -36
  69. package/src/multicast-input.ts +28 -8
  70. package/src/multicast-manager.ts +38 -34
  71. package/src/multicast-transport.ts +74 -7
  72. package/test-vectors/bc-provenance-v2.json +614 -175
  73. package/test-vectors/catalog/README.md +24 -8
  74. package/test-vectors/catalog/negative/multicast-auth-asm-missing-sender-address.json +0 -2
  75. package/test-vectors/catalog/negative/{multicast-auth-manifest-port-equals-port.json → multicast-auth-draft01-manifest-port.json} +1 -1
  76. package/test-vectors/catalog/negative/multicast-auth-duplicate-first-epoch.json +0 -1
  77. package/test-vectors/catalog/negative/multicast-auth-duplicate-kid.json +0 -1
  78. package/test-vectors/catalog/negative/multicast-auth-endpoint-auth-without-object-auth.json +0 -1
  79. package/test-vectors/catalog/negative/multicast-auth-endpoint-missing-bandwidth.json +0 -1
  80. package/test-vectors/catalog/negative/multicast-auth-manifest-timer-out-of-range.json +0 -1
  81. package/test-vectors/catalog/negative/multicast-auth-sender-address-on-ssm.json +0 -1
  82. package/test-vectors/catalog/negative/multicast-auth-shared-sg-port.json +73 -0
  83. package/test-vectors/catalog/negative/multicast-auth-version-1.json +0 -1
  84. package/test-vectors/catalog/negative/multicast-auth-version-3.json +0 -1
  85. package/test-vectors/catalog/negative/multicast-auth-version-missing.json +0 -1
  86. package/test-vectors/catalog/negative/multicast-endpoint-address-family-mismatch.json +55 -0
  87. package/test-vectors/catalog/negative/multicast-endpoint-address-host-name.json +55 -0
  88. package/test-vectors/catalog/negative/multicast-endpoint-address-leading-zero.json +55 -0
  89. package/test-vectors/catalog/negative/multicast-endpoint-address-ls32-leading-zero.json +55 -0
  90. package/test-vectors/catalog/negative/multicast-endpoint-address-mapped-family-mismatch.json +55 -0
  91. package/test-vectors/catalog/negative/multicast-endpoint-address-zone-id.json +55 -0
  92. package/test-vectors/catalog/positive/multicast-auth-asm-sender.json +0 -2
  93. package/test-vectors/catalog/{negative/multicast-auth-max-auth-delay-above-max.json → positive/multicast-auth-max-auth-delay-above-max-clamped.json} +0 -1
  94. package/test-vectors/catalog/{negative/multicast-auth-max-auth-delay-below-min.json → positive/multicast-auth-max-auth-delay-below-min-clamped.json} +0 -1
  95. package/test-vectors/catalog/positive/multicast-auth-max-auth-delay-string.json +55 -0
  96. package/test-vectors/catalog/positive/multicast-auth-rotation.json +0 -1
  97. package/test-vectors/catalog/positive/multicast-draft01-7-2-6-example.json +0 -1
  98. package/test-vectors/catalog/section-codes.json +8 -3
  99. package/test-vectors/catalog-multicast-auth-v1.json +0 -1
  100. package/test-vectors/catalog-validation.json +3 -3
  101. package/test-vectors/generate-bc-provenance-v2.mjs +94 -14
@@ -25,10 +25,9 @@
25
25
  "multicast-auth-endpoint-auth-without-object-auth.json": { "outOfSection": "multicast.auth" },
26
26
  "multicast-auth-endpoint-missing-auth.json": { "outOfSection": "multicast.auth" },
27
27
  "multicast-auth-endpoint-missing-bandwidth.json": { "outOfSection": "multicast.auth" },
28
- "multicast-auth-manifest-port-equals-port.json": { "outOfSection": "multicast.auth" },
29
28
  "multicast-auth-manifest-timer-out-of-range.json": { "outOfSection": "multicast.auth" },
30
- "multicast-auth-max-auth-delay-below-min.json": { "outOfSection": "multicast.auth" },
31
- "multicast-auth-max-auth-delay-above-max.json": { "outOfSection": "multicast.auth" },
29
+ "multicast-auth-draft01-manifest-port.json": { "outOfSection": "multicast.auth" },
30
+ "multicast-auth-shared-sg-port.json": { "outOfSection": "multicast.auth" },
32
31
  "multicast-auth-padded-key.json": { "outOfSection": "multicast.auth" },
33
32
  "multicast-auth-reused-track-format.json": { "outOfSection": "multicast.auth" },
34
33
  "multicast-auth-root-dotted-key.json": { "outOfSection": "multicast.auth" },
@@ -37,6 +36,12 @@
37
36
  "multicast-auth-version-1.json": { "outOfSection": "multicast.auth" },
38
37
  "multicast-auth-version-3.json": { "outOfSection": "multicast.auth" },
39
38
  "multicast-auth-version-missing.json": { "outOfSection": "multicast.auth" },
39
+ "multicast-endpoint-address-family-mismatch.json": { "outOfSection": "multicast" },
40
+ "multicast-endpoint-address-host-name.json": { "outOfSection": "multicast" },
41
+ "multicast-endpoint-address-leading-zero.json": { "outOfSection": "multicast" },
42
+ "multicast-endpoint-address-ls32-leading-zero.json": { "outOfSection": "multicast" },
43
+ "multicast-endpoint-address-mapped-family-mismatch.json": { "outOfSection": "multicast" },
44
+ "multicast-endpoint-address-zone-id.json": { "outOfSection": "multicast" },
40
45
  "multicast-endpoint-foreign-namespace-track.json": { "outOfSection": "multicast" },
41
46
  "multicast-endpoint-protocol-amt.json": { "outOfSection": "multicast" },
42
47
  "multicast-endpoint-protocol-atsc3.json": { "outOfSection": "multicast" },
@@ -32,7 +32,6 @@
32
32
  }
33
33
  ],
34
34
  "auth": {
35
- "manifestPort": 5005,
36
35
  "streamId": 2864434397,
37
36
  "maxAuthDelayMs": 200,
38
37
  "manifestTimerMs": 20,
@@ -510,9 +510,9 @@
510
510
  }
511
511
  },
512
512
  {
513
- "name": "multicast.auth other than bc-provenance version 2 is rejected (draft -01 7.2.6)",
514
- "valid": false,
515
- "error_code": "multicast_auth",
513
+ "name": "multicast.auth other than bc-provenance version 2 admits no multicast endpoint, and the catalog parses (multicast-02 7.2.6)",
514
+ "valid": true,
515
+ "admitted_multicast_endpoints": 0,
516
516
  "catalog": {
517
517
  "version": 1,
518
518
  "tracks": [
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // bc-provenance version 2 vectors, draft-ramadan-moq-multicast-01 (7.2.3,
2
+ // bc-provenance version 2 vectors, draft-ramadan-moq-multicast-02 (7.2.3,
3
3
  // 7.2.4, 7.2.7 step 1, 7.2.10, Appendix A). Case "appendix-a" reproduces the
4
4
  // draft's Appendix A byte for byte; every other case is labelled beyond-draft.
5
+ // No published vector covers m >= 2 FEC instances or an empty fec array, so
6
+ // for those the TS, Rust and Go cells are each other's cross-check.
5
7
  // Only node:crypto is used (sha-256, Ed25519), so the output is deterministic.
6
8
  // Step 1 is read at join (no record of the flow verified yet), so the
7
9
  // previous-epoch deadline of 7.2.7 (the bound of 7.2.5 plus maxAuthDelayMs +
@@ -14,7 +16,16 @@ import { dirname, join } from 'node:path';
14
16
  import { fileURLToPath } from 'node:url';
15
17
 
16
18
  const here = dirname(fileURLToPath(import.meta.url));
17
- const DRAFT_COMMIT = 'abf183bfb83e616909453872c4d788db4b3acd19';
19
+ const DRAFT_COMMIT = '5f4917291e2f0e58bdee9e4a813ff248537695e2';
20
+ // The 196-octet record of draft-ramadan-moq-multicast-01 Appendix A @ abf183bf.
21
+ const APPENDIX_A_01_RECORD = Buffer.from(
22
+ '4d484d320000000100000011000000000000000100000000000003e800020100' +
23
+ '6ade9800fe68177dd329ee6fdd668cb43ebdeb6d2e4c2ae351fd25050ab68dd9' +
24
+ '8af192147ead4d8be7d24e08d7075bbac8fbe7779ab3b8f1c47d3f8be4d2a514' +
25
+ 'f2ec20696f7d9007e81b73c9f54aff8c2f66bd6e8956bfa7e001789b986c328f' +
26
+ '9ed787519d5154a8e6beb7c472a09fa1e1d3d426b6042519343728835ed5c941' +
27
+ '6e615aad7ad335c0f0403aa8985e6b6979e5918d3d28e787ca29d55447930777' +
28
+ 'c380d808', 'hex');
18
29
 
19
30
  const H = (...parts) => createHash('sha256').update(Buffer.concat(parts)).digest();
20
31
  const u8 = (n) => Buffer.from([n]);
@@ -45,12 +56,17 @@ const pseudoheader = (ep, len) =>
45
56
  Buffer.concat([ip(ep.S), ip(ep.G), u8(0), u8(0x11), u16(len), u16(ep.port), u16(ep.port), u32(ep.streamId)]);
46
57
  const digest = (ep, payload) => H(pseudoheader(ep, payload.length), payload);
47
58
  const signedPrefix = (ep) =>
48
- Buffer.concat([Buffer.from('mcast-manifest-v2\0'), u32(ep.streamId), ip(ep.S), ip(ep.G), u16(ep.manifestPort)]);
59
+ Buffer.concat([Buffer.from('mcast-manifest-v2\0'), u32(ep.streamId), ip(ep.S), ip(ep.G), u16(ep.port)]);
49
60
 
61
+ // 7.2.4: one tuple per instance of the entry's fec array in ascending
62
+ // instanceId, no count and no instanceId; none (absent or empty) is
63
+ // lp16("none") then four u32(0), and "none" is no scheme.
50
64
  function paramsHash(p) {
51
- const fec = p.fec
52
- ? Buffer.concat([lp16(p.fec.algorithm), u32(p.fec.sourceSymbols), u32(p.fec.repairSymbols),
53
- u32(p.fec.symbolSize), u32(p.fec.interleaveDepthMs ?? 0)])
65
+ const instances = [...(p.fec ?? [])].sort((a, b) => (a.instanceId ?? 0) - (b.instanceId ?? 0));
66
+ if (instances.some((f) => f.scheme === 'none')) throw new Error('scheme "none" is reserved');
67
+ const fec = instances.length
68
+ ? Buffer.concat(instances.map((f) => Buffer.concat([lp16(f.scheme), u32(f.sourceSymbols), u32(f.repairSymbols),
69
+ u32(f.symbolSize), u32(f.interleaveDepthMs ?? 0)])))
54
70
  : Buffer.concat([lp16('none'), u32(0), u32(0), u32(0), u32(0)]);
55
71
  return H(Buffer.from('BCPV2-params'), lp16(p.codec), u32(p.timescale ?? 0), u32(p.groupDurationTicks ?? 0), fec);
56
72
  }
@@ -71,15 +87,18 @@ function record(ep, h, digests) {
71
87
  return Buffer.concat([body, sign(null, Buffer.concat([signedPrefix(ep), body]), privateKey)]);
72
88
  }
73
89
 
74
- const anchorEp = { S: '198.51.100.7', G: '232.10.20.30', port: 5004, manifestPort: 5005, streamId: 0xaabbccdd };
90
+ const anchorEp = { S: '198.51.100.7', G: '232.10.20.30', port: 5004, streamId: 0xaabbccdd };
75
91
  const anchorCtx = {
76
92
  namespace: ['example.arena'],
77
93
  packetIds: [1, 2],
78
94
  params: {
79
95
  codec: 'avc1.640028', timescale: 90000, groupDurationTicks: 3000,
80
- fec: { algorithm: 'raptorq', sourceSymbols: 32, repairSymbols: 16, symbolSize: 1312, interleaveDepthMs: 133 },
96
+ fec: [{ scheme: 'raptorq', sourceSymbols: 32, repairSymbols: 16, symbolSize: 1312, interleaveDepthMs: 133 }],
81
97
  },
82
98
  };
99
+ // Base plus keyframe overlay instance (draft-ramadan-moq-mmt-02 7.4.1).
100
+ const baseInstance = { instanceId: 0, ...anchorCtx.params.fec[0] };
101
+ const overlayInstance = { instanceId: 1, scheme: 'raptorq', sourceSymbols: 256, repairSymbols: 32, symbolSize: 1312, interleaveDepthMs: 1000 };
83
102
  const anchorHdr = { kid: 1, epoch: 17, mseq: 1, firstSeq: 1000, flags: 0, sent: 1792972800 };
84
103
 
85
104
  // A synthetic FEC Type 1 source block (ISO/IEC 23008-1 C.5.2), replacing the
@@ -114,7 +133,7 @@ function positive(id, source, note, { ep = anchorEp, ctx = anchorCtx, hdr = {},
114
133
 
115
134
  const anchorDatagrams = [Buffer.alloc(1400, 0x11), Buffer.alloc(1400, 0x22)];
116
135
  const cases = [
117
- positive('appendix-a', 'appendix-a', 'draft-ramadan-moq-multicast-01 Appendix A, byte for byte',
136
+ positive('appendix-a', 'appendix-a', 'draft-ramadan-moq-multicast-02 Appendix A, byte for byte',
118
137
  { datagrams: anchorDatagrams }),
119
138
  positive('twin', 'appendix-a', 'the twin of a record is byte-identical to it (7.2.5); equals appendix-a',
120
139
  { datagrams: anchorDatagrams }),
@@ -128,11 +147,21 @@ const cases = [
128
147
  { datagrams: anchorDatagrams, hdr: { mseq: 2, flags: 1 } }),
129
148
  positive('ipv6', 'beyond-draft', 'IPv6 S and G, 44-octet pseudoheader and 50-octet signed_message prefix',
130
149
  { ep: { ...anchorEp, S: '2001:db8::7', G: 'ff3e::8000:1e' }, datagrams: anchorDatagrams }),
131
- positive('params-no-fec', 'beyond-draft', 'catalog entry with no fec object: lp16("none") then four u32(0)',
132
- { ctx: { ...anchorCtx, params: { codec: 'avc1.640028', timescale: 90000, groupDurationTicks: 3000, fec: null } },
150
+ positive('params-no-fec', 'beyond-draft', 'catalog entry with no fec member: lp16("none") then four u32(0)',
151
+ { ctx: { ...anchorCtx, params: { codec: 'avc1.640028', timescale: 90000, groupDurationTicks: 3000 } },
133
152
  datagrams: anchorDatagrams }),
134
- positive('params-no-interleave', 'beyond-draft', 'fec object without interleaveDepthMs: u32(0)',
135
- { ctx: { ...anchorCtx, params: { ...anchorCtx.params, fec: { ...anchorCtx.params.fec, interleaveDepthMs: undefined } } },
153
+ positive('params-empty-fec-array', 'beyond-draft', 'empty fec array: hashes as the no-FEC encoding, params_hash equals params-no-fec',
154
+ { ctx: { ...anchorCtx, params: { codec: 'avc1.640028', timescale: 90000, groupDurationTicks: 3000, fec: [] } },
155
+ datagrams: anchorDatagrams }),
156
+ positive('params-no-interleave', 'beyond-draft', 'fec instance without interleaveDepthMs: u32(0)',
157
+ { ctx: { ...anchorCtx, params: { ...anchorCtx.params, fec: [{ ...anchorCtx.params.fec[0], interleaveDepthMs: undefined }] } },
158
+ datagrams: anchorDatagrams }),
159
+ positive('params-two-instances', 'beyond-draft', 'base and keyframe overlay instance (mmt-02 7.4.1): two tuples in ascending instanceId',
160
+ { ctx: { ...anchorCtx, params: { ...anchorCtx.params, fec: [baseInstance, overlayInstance] } },
161
+ datagrams: anchorDatagrams }),
162
+ positive('params-two-instances-listed-reversed', 'beyond-draft',
163
+ 'the same instances listed instanceId 1 first: hashed in ascending instanceId, so params_hash equals params-two-instances; array order would not',
164
+ { ctx: { ...anchorCtx, params: { ...anchorCtx.params, fec: [overlayInstance, baseInstance] } },
136
165
  datagrams: anchorDatagrams }),
137
166
  positive('catalog-less', 'beyond-draft', 'flow carried without a MoQ catalog: ctx is 32 zero octets',
138
167
  { ctx: null, datagrams: anchorDatagrams }),
@@ -208,6 +237,9 @@ const negatives = [
208
237
  negative('ctx-mismatch', 'ctx', { receiver: { catalogCtxHex: hex(Buffer.alloc(32, 0xff)) } }),
209
238
  negative('budget-empty', 'budget', { receiver: { budgetTokens: 0 } }),
210
239
  negative('signature', 'signature', { mutate: (r) => { const b = Buffer.from(r); b[b.length - 1] ^= 1; return b; } }),
240
+ // multicast-01 Appendix A @ abf183b: signed under the -01 prefix, which bound
241
+ // u16(manifestPort) = 5005; under the -02 prefix (u16(port)) it must fail.
242
+ negative('appendix-a-01-record', 'signature', { mutate: () => APPENDIX_A_01_RECORD }),
211
243
  ];
212
244
 
213
245
  function boundary(id, receiver) {
@@ -236,11 +268,57 @@ boundaries.push(boundary('epoch-kid-range-top', {
236
268
  keys: [...baseReceiver.keys, { kid: 2, publicKeyHex: hex(publicKey), firstEpoch: 17 }], minEpoch: 16,
237
269
  }));
238
270
  boundaries[3].recordHex = hex(record(anchorEp, { ...anchorHdr, ctx: c0, epoch: 16 }, anchorDigests));
271
+ // Highest mseq below the window: 0 + 64 > 10, so mseq 0 is above the floor.
272
+ // An unsigned highest - 64 wraps and would drop it (7.2.1, 7.2.7 step 1).
273
+ boundaries.push(boundary('mseq-floor-below-window', { highestMseq: { kid: 1, epoch: 17, mseq: 10 } }));
274
+ boundaries[4].recordHex = hex(record(anchorEp, { ...anchorHdr, ctx: c0, mseq: 0 }, anchorDigests));
275
+
276
+ // "none" is reserved (fec-core-00 15.2): an instance naming it is rejected,
277
+ // never hashed as the no-FEC encoding.
278
+ const paramsRejections = [
279
+ { id: 'scheme-none', note: 'a fec instance whose scheme is "none" is rejected',
280
+ params: { ...anchorCtx.params, fec: [{ ...anchorCtx.params.fec[0], scheme: 'none' }] } },
281
+ ];
282
+ for (const r of paramsRejections) {
283
+ let threw = false;
284
+ try { paramsHash(r.params); } catch { threw = true; }
285
+ if (!threw) throw new Error(`${r.id} hashed`);
286
+ }
287
+
288
+ // 7.2.4 repair-only endpoint: params_hash comes from the source track of the
289
+ // endpoint's lowest-packetId repair track, named by its `source` or, absent
290
+ // that, by any instance (not only layer 0's) naming it as `repairTrack`.
291
+ const catalogBase = (repair) => ({
292
+ version: 1,
293
+ streamingFormat: 'mmtp',
294
+ tracks: [
295
+ { name: 'video', packaging: 'mmtp', codec: 'avc1.640028', mmtpMode: 'mfu', timescale: 90000, groupDurationTicks: 3000,
296
+ fec: [
297
+ { instanceId: 1, scope: 'keyframe', scheme: 'raptorq', blockProfile: 'iso-ssbg1-v1', sourceSymbols: 256, repairSymbols: 32,
298
+ symbolSize: 1312, interleaveDepthMs: 1000, repairTrack: 'video/keyframe-repair' },
299
+ { instanceId: 0, scheme: 'raptorq', blockProfile: 'iso-ssbg1-v1', sourceSymbols: 32, repairSymbols: 16,
300
+ symbolSize: 1312, interleaveDepthMs: 133, reorderToleranceMs: 40, repairTrack: 'video/repair' },
301
+ ] },
302
+ { name: 'video/repair', packaging: 'fec-repair', source: 'video', instanceId: 0, priority: 240 },
303
+ repair,
304
+ ],
305
+ });
306
+ const resolvedVideo = { ...anchorCtx.params, fec: [baseInstance, overlayInstance] };
307
+ const catalogResolutions = [
308
+ { id: 'repair-only-via-source', note: 'the repair track names its source track',
309
+ catalog: catalogBase({ name: 'video/keyframe-repair', packaging: 'fec-repair', source: 'video', instanceId: 1, priority: 242 }),
310
+ endpointTracks: [{ name: 'video/keyframe-repair', packetId: 9 }, { name: 'video/repair', packetId: 12 }],
311
+ resolvedParams: resolvedVideo },
312
+ { id: 'repair-only-via-instance-repair-track', note: 'the repair track carries no source; the instance 1 (keyframe overlay) repairTrack names it',
313
+ catalog: catalogBase({ name: 'video/keyframe-repair', packaging: 'fec-repair', instanceId: 1, priority: 242 }),
314
+ endpointTracks: [{ name: 'video/keyframe-repair', packetId: 9 }],
315
+ resolvedParams: resolvedVideo },
316
+ ].map((r) => ({ ...r, paramsHashHex: hex(paramsHash(r.resolvedParams)) }));
239
317
 
240
318
  const out = {
241
319
  version: 2,
242
320
  profile: 'bc-provenance',
243
- draft: 'draft-ramadan-moq-multicast-01',
321
+ draft: 'draft-ramadan-moq-multicast-02',
244
322
  draftCommit: DRAFT_COMMIT,
245
323
  hashAlg: 1,
246
324
  key: { seedHex: hex(seed), publicKeyHex: hex(publicKey) },
@@ -248,5 +326,7 @@ const out = {
248
326
  recovered: recoveredCase,
249
327
  negatives,
250
328
  boundaries,
329
+ paramsRejections,
330
+ catalogResolutions,
251
331
  };
252
332
  writeFileSync(join(here, 'bc-provenance-v2.json'), `${JSON.stringify(out, null, 2)}\n`);