@blockcast/mmt-container 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/dist/browser-runtime.js +6931 -6685
- package/dist/browser-runtime.js.map +4 -4
- package/dist/carp-catalog.d.ts +7 -7
- package/dist/carp-catalog.d.ts.map +1 -1
- package/dist/carp-catalog.js +65 -230
- package/dist/carp-catalog.js.map +3 -3
- package/dist/catalog-fec-params.js.map +1 -1
- package/dist/catalog-network-source.js.map +1 -1
- package/dist/delivery-catalog.js.map +1 -1
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +25 -1
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +208 -15
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-state.js.map +1 -1
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/held-layer-admission.d.ts +8 -0
- package/dist/held-layer-admission.d.ts.map +1 -1
- package/dist/held-layer-admission.js +22 -1
- package/dist/held-layer-admission.js.map +2 -2
- package/dist/iso-block-locator.js.map +1 -1
- package/dist/mfu-reassembler.js.map +1 -1
- package/dist/mmtp-container-parser.js.map +1 -1
- package/dist/mmtp-container-parser.no-modules.js +127 -322
- package/dist/mmtp-container-parser.no-modules.js.map +4 -4
- package/dist/mmtp-decode-pipeline.js.map +1 -1
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +61 -15
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +11 -6
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +47 -10
- package/dist/mmtp-router.js.map +3 -3
- package/dist/multicast-auth-catalog.d.ts +8 -8
- package/dist/multicast-auth-catalog.d.ts.map +1 -1
- package/dist/multicast-auth-catalog.js +171 -75
- package/dist/multicast-auth-catalog.js.map +4 -4
- package/dist/multicast-input.d.ts +5 -5
- package/dist/multicast-input.d.ts.map +1 -1
- package/dist/multicast-input.js +126 -49
- package/dist/multicast-input.js.map +3 -3
- package/dist/multicast-manager.d.ts +11 -4
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +7922 -7700
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +8 -0
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +27 -3
- package/dist/multicast-transport.js.map +3 -3
- package/dist/multicast-unwrap.js +2 -2
- package/dist/multicast-unwrap.js.map +2 -2
- package/dist/no-modules.js +81 -278
- package/dist/no-modules.js.map +4 -4
- package/dist/repair-fetch.js.map +1 -1
- package/dist/signaling.js.map +1 -1
- package/dist/timestamp-codec.js.map +1 -1
- package/dist/wasm-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js.map +1 -1
- package/package.json +5 -5
- package/schemas/catalog.schema.json +15 -7
- package/src/carp-catalog.ts +14 -13
- package/src/fec-manager.ts +89 -14
- package/src/held-layer-admission.ts +38 -1
- package/src/mmtp-fec-session.ts +20 -12
- package/src/mmtp-router.ts +65 -16
- package/src/multicast-auth-catalog.ts +43 -36
- package/src/multicast-input.ts +28 -8
- package/src/multicast-manager.ts +38 -34
- package/src/multicast-transport.ts +31 -3
- package/test-vectors/bc-provenance-v2.json +614 -175
- package/test-vectors/catalog/README.md +24 -8
- package/test-vectors/catalog/negative/multicast-auth-asm-missing-sender-address.json +0 -2
- package/test-vectors/catalog/negative/{multicast-auth-manifest-port-equals-port.json → multicast-auth-draft01-manifest-port.json} +1 -1
- package/test-vectors/catalog/negative/multicast-auth-duplicate-first-epoch.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-duplicate-kid.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-endpoint-auth-without-object-auth.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-endpoint-missing-bandwidth.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-manifest-timer-out-of-range.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-sender-address-on-ssm.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-shared-sg-port.json +73 -0
- package/test-vectors/catalog/negative/multicast-auth-version-1.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-version-3.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-version-missing.json +0 -1
- package/test-vectors/catalog/negative/multicast-endpoint-address-family-mismatch.json +55 -0
- package/test-vectors/catalog/negative/multicast-endpoint-address-host-name.json +55 -0
- package/test-vectors/catalog/negative/multicast-endpoint-address-leading-zero.json +55 -0
- package/test-vectors/catalog/negative/multicast-endpoint-address-ls32-leading-zero.json +55 -0
- package/test-vectors/catalog/negative/multicast-endpoint-address-mapped-family-mismatch.json +55 -0
- package/test-vectors/catalog/negative/multicast-endpoint-address-zone-id.json +55 -0
- package/test-vectors/catalog/positive/multicast-auth-asm-sender.json +0 -2
- 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
- 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
- package/test-vectors/catalog/positive/multicast-auth-max-auth-delay-string.json +55 -0
- package/test-vectors/catalog/positive/multicast-auth-rotation.json +0 -1
- package/test-vectors/catalog/positive/multicast-draft01-7-2-6-example.json +0 -1
- package/test-vectors/catalog/section-codes.json +8 -3
- package/test-vectors/catalog-multicast-auth-v1.json +0 -1
- package/test-vectors/catalog-validation.json +3 -3
- package/test-vectors/generate-bc-provenance-v2.mjs +94 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockcast/mmt-container",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "MMTP/LOC container parsing for MMT over MoQ and SSM",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -169,10 +169,10 @@
|
|
|
169
169
|
"test:watch": "vitest"
|
|
170
170
|
},
|
|
171
171
|
"dependencies": {
|
|
172
|
-
"@blockcast/fec-worker": "^
|
|
173
|
-
"@blockcast/mmt-base": "^
|
|
174
|
-
"@blockcast/mmt-fec": "^
|
|
175
|
-
"@blockcast/mmt-manifest-verify": "^
|
|
172
|
+
"@blockcast/fec-worker": "^2.0.0",
|
|
173
|
+
"@blockcast/mmt-base": "^2.0.0",
|
|
174
|
+
"@blockcast/mmt-fec": "^2.0.0",
|
|
175
|
+
"@blockcast/mmt-manifest-verify": "^2.0.0",
|
|
176
176
|
"@noble/curves": "^1.9.7",
|
|
177
177
|
"@noble/hashes": "^1.8.0",
|
|
178
178
|
"ajv": "^8.17.1",
|
|
@@ -332,19 +332,27 @@
|
|
|
332
332
|
"endpointAuth": {
|
|
333
333
|
"type": "object",
|
|
334
334
|
"additionalProperties": false,
|
|
335
|
-
"required": ["
|
|
336
|
-
"description": "draft-ramadan-moq-multicast-
|
|
335
|
+
"required": ["streamId"],
|
|
336
|
+
"description": "draft-ramadan-moq-multicast-02 7.2.6 (Blockcast/moqcast-draft@5f49172): marks the endpoint's flow protected; its records ride in band on the endpoint's own port (7.2.3). Required on every endpoint when multicast.auth is present and forbidden otherwise. One endpoint per (S, G, port) and the address-literal rules are multicastCatalogIssues' (a receiver ignores the endpoint).",
|
|
337
337
|
"properties": {
|
|
338
|
-
"manifestPort": { "type": "integer", "minimum": 1, "maximum": 65535 },
|
|
339
338
|
"streamId": { "type": "integer", "minimum": 0, "maximum": 4294967295 },
|
|
340
339
|
"senderAddress": {
|
|
341
340
|
"type": "string",
|
|
342
341
|
"minLength": 1,
|
|
343
342
|
"description": "S of a protected ASM endpoint; must not appear on an SSM endpoint."
|
|
344
343
|
},
|
|
345
|
-
"maxAuthDelayMs": {
|
|
346
|
-
|
|
347
|
-
|
|
344
|
+
"maxAuthDelayMs": {
|
|
345
|
+
"default": 200,
|
|
346
|
+
"description": "A publisher emits an integer from 50 to 1000. A receiver clamps a number outside that range to the nearer bound and reads a non-number, or an in-range non-integer, as absent (7.2.6), so no value here makes the endpoint ignored."
|
|
347
|
+
},
|
|
348
|
+
"manifestTimerMs": {
|
|
349
|
+
"default": 20,
|
|
350
|
+
"not": { "type": "number", "exclusiveMaximum": 1 },
|
|
351
|
+
"description": "A publisher emits an integer of at least 1. A receiver ignores the endpoint when it is a number below 1 and reads any other non-integer as absent (7.2.6)."
|
|
352
|
+
},
|
|
353
|
+
"manifestTrack": {
|
|
354
|
+
"description": "MoQ track mirroring the records (7.2.5). A receiver reads a non-string as absent (7.2.6)."
|
|
355
|
+
}
|
|
348
356
|
}
|
|
349
357
|
},
|
|
350
358
|
"networkSource": {
|
|
@@ -381,7 +389,7 @@
|
|
|
381
389
|
"frequency": { "$ref": "#/$defs/positiveInteger", "description": "RF centre frequency in kHz (e.g. 587000 for 587 MHz)." },
|
|
382
390
|
"plpId": { "type": "integer", "minimum": 0, "maximum": 63 },
|
|
383
391
|
"serviceId": { "type": "integer", "minimum": 0, "maximum": 65535 },
|
|
384
|
-
"slsUri": { "type": "string", "format": "uri", "description": "SLS of an MMTP service, MP table included (A/331:2026-04 7.2). The SLS describes the MMTP session and nothing of this profile, so
|
|
392
|
+
"slsUri": { "type": "string", "format": "uri", "description": "SLS of an MMTP service, MP table included (A/331:2026-04 7.2). The SLS describes the MMTP session and nothing of this profile, so the flow parameters and keys come from provisioning, not from the SLS." }
|
|
385
393
|
}
|
|
386
394
|
}
|
|
387
395
|
]
|
package/src/carp-catalog.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
MMTP_SIGNALING_PACKET_ID,
|
|
14
14
|
type FecBlockProfile,
|
|
15
15
|
} from "@blockcast/mmt-base";
|
|
16
|
-
import {
|
|
16
|
+
import { multicastEndpointSource } from "@blockcast/mmt-base";
|
|
17
17
|
import type { EndpointAuthConfig, MulticastAuthConfig } from "./multicast-auth-catalog.js";
|
|
18
18
|
|
|
19
19
|
// ── Track Role (Phase 14, D-02..D-05) ─────────────────────────
|
|
@@ -304,8 +304,8 @@ export type NetworkSource =
|
|
|
304
304
|
serviceId?: number;
|
|
305
305
|
/**
|
|
306
306
|
* SLS of the MMTP service, MP table included (A/331:2026-04 7.2). It
|
|
307
|
-
* describes the MMTP session and nothing of this profile:
|
|
308
|
-
* and keys come from provisioning. libmmt does not fetch it.
|
|
307
|
+
* describes the MMTP session and nothing of this profile: the flow
|
|
308
|
+
* parameters and keys come from provisioning. libmmt does not fetch it.
|
|
309
309
|
*/
|
|
310
310
|
slsUri?: string;
|
|
311
311
|
};
|
|
@@ -676,14 +676,14 @@ function validateNetworkSourceRelays(sources: unknown, path: string): void {
|
|
|
676
676
|
}
|
|
677
677
|
|
|
678
678
|
/**
|
|
679
|
-
* Validate the common CMSF shape and the MMTP required-field profile.
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
679
|
+
* Validate the common CMSF shape and the MMTP required-field profile. It never
|
|
680
|
+
* throws on a draft -02 4.1 / 7.2.6 multicast rule: pass `catalogNamespace`
|
|
681
|
+
* (the namespace of the catalog track) to `admittedMulticastEndpoints` /
|
|
682
|
+
* `multicastCatalogIssues` instead. The option is accepted and unused.
|
|
683
683
|
*/
|
|
684
684
|
export function validateCmsfMmtpCatalog(
|
|
685
685
|
value: unknown,
|
|
686
|
-
|
|
686
|
+
_options: { catalogNamespace?: string } = {},
|
|
687
687
|
): CmsfCatalog {
|
|
688
688
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
689
689
|
catalogError("catalog_object", "$", "catalog must be an object");
|
|
@@ -929,11 +929,12 @@ export function validateCmsfMmtpCatalog(
|
|
|
929
929
|
}
|
|
930
930
|
}
|
|
931
931
|
|
|
932
|
-
// draft -
|
|
933
|
-
//
|
|
934
|
-
//
|
|
935
|
-
|
|
936
|
-
|
|
932
|
+
// draft -02 4.1 / 7.2.6 (protocol and S, address literals, auth
|
|
933
|
+
// all-or-nothing, the key set, one endpoint per (S, G, port),
|
|
934
|
+
// foreign-namespace tracks) are never a catalog fault here: a receiver
|
|
935
|
+
// ignores the endpoint an issue names, and a catalog-level issue makes the
|
|
936
|
+
// multicast object yield no data (`admittedMulticastEndpoints`), while MoQ
|
|
937
|
+
// unicast stays usable. `multicastCatalogIssues` reports them.
|
|
937
938
|
|
|
938
939
|
validateArchive(catalog);
|
|
939
940
|
|
package/src/fec-manager.ts
CHANGED
|
@@ -26,6 +26,11 @@ import {
|
|
|
26
26
|
serial32Precedes,
|
|
27
27
|
ssbg1FitsSymbol,
|
|
28
28
|
type FecBlockProfile,
|
|
29
|
+
admitMmtpRepairObject,
|
|
30
|
+
parseMoqRepairFrameWithHeader,
|
|
31
|
+
type MMTPHeader,
|
|
32
|
+
type MoqRepairFrame,
|
|
33
|
+
type RepairObjectReject,
|
|
29
34
|
} from '@blockcast/mmt-base'
|
|
30
35
|
import {
|
|
31
36
|
IsoBlockLocator,
|
|
@@ -131,10 +136,22 @@ export interface FecStats {
|
|
|
131
136
|
workers: Array<FecTrackStats & { packetId: number }>
|
|
132
137
|
/** Main-thread repair symbols rejected before dispatch because their size did not match T. */
|
|
133
138
|
repairSymbolSizeMismatches: number
|
|
139
|
+
/**
|
|
140
|
+
* Repair objects discarded on admission (draft-ramadan-moq-fec-core-00 7.5,
|
|
141
|
+
* draft-ramadan-moq-mmt-02 7.3), per FEC instance and reason. The instance
|
|
142
|
+
* keeps decoding; other instances are untouched.
|
|
143
|
+
*/
|
|
144
|
+
repairObjectRejects: FecRepairObjectRejects[]
|
|
134
145
|
/** Bounded Worker error counts keyed by stable code, or message text for legacy errors. */
|
|
135
146
|
workerErrors: Record<string, number>
|
|
136
147
|
}
|
|
137
148
|
|
|
149
|
+
/** One FEC instance's repair-object admission discards, by reason. */
|
|
150
|
+
export type FecRepairObjectRejects = {
|
|
151
|
+
packetId: number
|
|
152
|
+
catalogTrackName: string | undefined
|
|
153
|
+
} & Record<RepairObjectReject, number>
|
|
154
|
+
|
|
138
155
|
export interface FecPacketStats {
|
|
139
156
|
packetId: number
|
|
140
157
|
sourcePackets: number
|
|
@@ -514,6 +531,7 @@ export class FECManager {
|
|
|
514
531
|
#heldLayerStates = new Map<string, HeldLayerAdmission>()
|
|
515
532
|
/** Streams whose FEC failed closed at runtime (fec-01 6.3.9). */
|
|
516
533
|
#fecFailClosed = new Map<string, FecFailClosedReason>()
|
|
534
|
+
#repairObjectRejects = new Map<string, FecRepairObjectRejects>()
|
|
517
535
|
#onFecFailClosed: ((event: FecFailClosedEvent) => void) | null = null
|
|
518
536
|
// Blocks already recovered — prevents late source symbols from restarting timeouts.
|
|
519
537
|
// WASM removes blocks after recovery; late symbols create empty blocks returning null,
|
|
@@ -2395,6 +2413,7 @@ export class FECManager {
|
|
|
2395
2413
|
packetId,
|
|
2396
2414
|
})),
|
|
2397
2415
|
repairSymbolSizeMismatches: this.#repairSymbolSizeMismatchCount,
|
|
2416
|
+
repairObjectRejects: [...this.#repairObjectRejects.values()].map((r) => ({ ...r })),
|
|
2398
2417
|
workerErrors: Object.fromEntries(this.#workerErrorCounts),
|
|
2399
2418
|
}
|
|
2400
2419
|
}
|
|
@@ -4140,15 +4159,7 @@ export class FECManager {
|
|
|
4140
4159
|
): boolean {
|
|
4141
4160
|
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
4142
4161
|
if (this.#fecFailClosed.has(streamKey)) return false
|
|
4143
|
-
const
|
|
4144
|
-
const layers = config?.repairLayers
|
|
4145
|
-
if (layers === undefined || layers.length === 0) return true
|
|
4146
|
-
let admission = this.#heldLayerStates.get(streamKey)
|
|
4147
|
-
if (admission === undefined) {
|
|
4148
|
-
admission = new HeldLayerAdmission(layers, config!.heldLayers)
|
|
4149
|
-
this.#heldLayerStates.set(streamKey, admission)
|
|
4150
|
-
}
|
|
4151
|
-
const verdict = admission.admit(
|
|
4162
|
+
const verdict = this.#heldLayerAdmission(packetId, streamKey)?.admit(
|
|
4152
4163
|
repairTrackName,
|
|
4153
4164
|
ssStart,
|
|
4154
4165
|
rsbLength,
|
|
@@ -4157,16 +4168,80 @@ export class FECManager {
|
|
|
4157
4168
|
)
|
|
4158
4169
|
if (verdict === 'discard') return false
|
|
4159
4170
|
if (verdict !== undefined) {
|
|
4160
|
-
this.#
|
|
4161
|
-
this.#onFecFailClosed?.({ ...verdict, packetId, catalogTrackName })
|
|
4162
|
-
console.warn(
|
|
4163
|
-
`[FECManager] FEC failed closed (fec-01 6.3.9) packetId=${packetId} track=${catalogTrackName ?? '-'}: ${JSON.stringify(verdict)}`,
|
|
4164
|
-
)
|
|
4171
|
+
this.#failClosed(streamKey, verdict, packetId, catalogTrackName)
|
|
4165
4172
|
return false
|
|
4166
4173
|
}
|
|
4167
4174
|
return true
|
|
4168
4175
|
}
|
|
4169
4176
|
|
|
4177
|
+
#heldLayerAdmission(packetId: number, streamKey: string): HeldLayerAdmission | undefined {
|
|
4178
|
+
const config = this.#trackConfigs.get(packetId)
|
|
4179
|
+
const layers = config?.repairLayers
|
|
4180
|
+
if (layers === undefined || layers.length === 0) return undefined
|
|
4181
|
+
let admission = this.#heldLayerStates.get(streamKey)
|
|
4182
|
+
if (admission === undefined) {
|
|
4183
|
+
admission = new HeldLayerAdmission(layers, config!.heldLayers)
|
|
4184
|
+
this.#heldLayerStates.set(streamKey, admission)
|
|
4185
|
+
}
|
|
4186
|
+
return admission
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
#failClosed(streamKey: string, verdict: FecFailClosedReason, packetId: number, catalogTrackName: string | undefined): void {
|
|
4190
|
+
this.#fecFailClosed.set(streamKey, verdict)
|
|
4191
|
+
this.#onFecFailClosed?.({ ...verdict, packetId, catalogTrackName })
|
|
4192
|
+
console.warn(
|
|
4193
|
+
`[FECManager] FEC failed closed (fec-01 6.3.9) packetId=${packetId} track=${catalogTrackName ?? '-'}: ${JSON.stringify(verdict)}`,
|
|
4194
|
+
)
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
/**
|
|
4198
|
+
* Admission of one iso-ssbg1-v1 repair object of `packetId`'s instance,
|
|
4199
|
+
* before it takes any part in block state (draft-ramadan-moq-fec-core-00
|
|
4200
|
+
* 7.5, draft-ramadan-moq-mmt-02 7.3): exactly 12 + 13 + T bytes, RS_ID below
|
|
4201
|
+
* RSB_length under every scheme, and the scheme's bound. The instance is the
|
|
4202
|
+
* one the repair track names (`packetId`, `catalogTrackName`), never one
|
|
4203
|
+
* inferred from the fields. A discard is counted on `repairObjectRejects`
|
|
4204
|
+
* and the instance keeps decoding; except that an object the RS_ID or
|
|
4205
|
+
* scheme discard would take whose RSB_length is below a held layer's
|
|
4206
|
+
* O_i + P_i on that layer's track or on an anchored block stops the
|
|
4207
|
+
* instance's FEC instead (core-00 7.3.2). Returns the frame to admit.
|
|
4208
|
+
*/
|
|
4209
|
+
admitRepairObject(
|
|
4210
|
+
packetId: number,
|
|
4211
|
+
catalogTrackName: string | undefined,
|
|
4212
|
+
repairTrackName: string | undefined,
|
|
4213
|
+
packet: Uint8Array,
|
|
4214
|
+
header: MMTPHeader,
|
|
4215
|
+
): MoqRepairFrame | null {
|
|
4216
|
+
const config = this.#trackConfigs.get(packetId)
|
|
4217
|
+
// No T to measure against: the parse alone (the size check refuses later).
|
|
4218
|
+
if (!config || !(config.symbolSize > 0)) return parseMoqRepairFrameWithHeader(packet, header)
|
|
4219
|
+
const admission = admitMmtpRepairObject(packet, header, config.symbolSize, config.algorithm ?? 'raptorq')
|
|
4220
|
+
if (!admission) return null
|
|
4221
|
+
if ('frame' in admission && admission.reject === undefined) return admission.frame
|
|
4222
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
4223
|
+
if ('frame' in admission && !this.#fecFailClosed.has(streamKey)) {
|
|
4224
|
+
const verdict = this.#heldLayerAdmission(packetId, streamKey)?.failsClosed(
|
|
4225
|
+
repairTrackName,
|
|
4226
|
+
admission.frame.ssStart,
|
|
4227
|
+
admission.frame.rsbLength,
|
|
4228
|
+
this.#layerAnchorHorizon(packetId),
|
|
4229
|
+
this.#sourceFrontiers.get(streamKey)?.sbn,
|
|
4230
|
+
)
|
|
4231
|
+
if (verdict) {
|
|
4232
|
+
this.#failClosed(streamKey, verdict, packetId, catalogTrackName)
|
|
4233
|
+
return null
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
let rejects = this.#repairObjectRejects.get(streamKey)
|
|
4237
|
+
if (rejects === undefined) {
|
|
4238
|
+
rejects = { packetId, catalogTrackName, repairLength: 0, repairRsId: 0, repairSchemeBound: 0 }
|
|
4239
|
+
this.#repairObjectRejects.set(streamKey, rejects)
|
|
4240
|
+
}
|
|
4241
|
+
rejects[admission.reject!]++
|
|
4242
|
+
return null
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4170
4245
|
/**
|
|
4171
4246
|
* How far (in SS_Start symbols) behind the newest block a 6.3.9 anchor or
|
|
4172
4247
|
* below-total block can still matter: the iso locator's retention in blocks
|
|
@@ -99,6 +99,17 @@ export class HeldLayerAdmission {
|
|
|
99
99
|
rsbLength: number,
|
|
100
100
|
horizon: number,
|
|
101
101
|
sourceFrontier?: number,
|
|
102
|
+
): FecFailClosedReason | 'discard' | undefined {
|
|
103
|
+
return this.#admit(repairTrackName, ssStart, rsbLength, horizon, sourceFrontier, true)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#admit(
|
|
107
|
+
repairTrackName: string | undefined,
|
|
108
|
+
ssStart: number,
|
|
109
|
+
rsbLength: number,
|
|
110
|
+
horizon: number,
|
|
111
|
+
sourceFrontier: number | undefined,
|
|
112
|
+
retroactive: boolean,
|
|
102
113
|
): FecFailClosedReason | 'discard' | undefined {
|
|
103
114
|
if (repairTrackName !== undefined && !this.#layerNames.has(repairTrackName)) return 'discard'
|
|
104
115
|
// A repair may run ahead of the media, but one more than `horizon` past the
|
|
@@ -142,7 +153,7 @@ export class HeldLayerAdmission {
|
|
|
142
153
|
}
|
|
143
154
|
} else if (!state.settled && !stale(ssStart)) {
|
|
144
155
|
if (state.anchor === undefined || serial32Precedes(ssStart, state.anchor)) state.anchor = ssStart
|
|
145
|
-
if (state.latestBelow !== undefined && !serial32Precedes(state.latestBelow, state.anchor)) {
|
|
156
|
+
if (retroactive && state.latestBelow !== undefined && !serial32Precedes(state.latestBelow, state.anchor)) {
|
|
146
157
|
code = 'below_anchored_total'
|
|
147
158
|
ssStart = state.latestBelow
|
|
148
159
|
rsbLength = state.latestBelowRsb!
|
|
@@ -158,4 +169,30 @@ export class HeldLayerAdmission {
|
|
|
158
169
|
}
|
|
159
170
|
return undefined
|
|
160
171
|
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The core-00 7.5 carve-out for a repair object about to be discarded
|
|
175
|
+
* (RS_ID or scheme bound): whether its own RSB_length, below a held layer's
|
|
176
|
+
* total on that layer's track or on an anchored block, fails the instance
|
|
177
|
+
* closed under 7.3.2. Leaves the anchored state as it was: a discarded
|
|
178
|
+
* object anchors nothing.
|
|
179
|
+
*/
|
|
180
|
+
failsClosed(
|
|
181
|
+
repairTrackName: string | undefined,
|
|
182
|
+
ssStart: number,
|
|
183
|
+
rsbLength: number,
|
|
184
|
+
horizon: number,
|
|
185
|
+
sourceFrontier?: number,
|
|
186
|
+
): FecFailClosedReason | undefined {
|
|
187
|
+
const saved = this.#states.map((state) => ({ ...state }))
|
|
188
|
+
try {
|
|
189
|
+
// No retroactive verdict: a discarded object anchors nothing, so it
|
|
190
|
+
// cannot fail an earlier below-total block, and every held layer is
|
|
191
|
+
// still judged on the object's own RSB_length.
|
|
192
|
+
const verdict = this.#admit(repairTrackName, ssStart, rsbLength, horizon, sourceFrontier, false)
|
|
193
|
+
return typeof verdict === 'object' ? verdict : undefined
|
|
194
|
+
} finally {
|
|
195
|
+
saved.forEach((state, i) => { this.#states[i] = state })
|
|
196
|
+
}
|
|
197
|
+
}
|
|
161
198
|
}
|
package/src/mmtp-fec-session.ts
CHANGED
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import {
|
|
32
|
+
admitMmtpRepairObject,
|
|
32
33
|
isoBlockContains,
|
|
33
34
|
isoBlocksBehindLowerBound,
|
|
34
35
|
parseFecBlockProfile,
|
|
35
36
|
parseMMTPHeader,
|
|
36
|
-
parseMoqRepairFrameWithHeader,
|
|
37
37
|
serial32Add,
|
|
38
38
|
serial32Distance,
|
|
39
39
|
serial32Precedes,
|
|
@@ -374,20 +374,28 @@ export class MmtpFecSession {
|
|
|
374
374
|
} catch {
|
|
375
375
|
return this.#drop('repair:malformed')
|
|
376
376
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (this.#
|
|
380
|
-
|
|
381
|
-
|
|
377
|
+
// core-00 7.5 / mmt-02 7.3: exactly 12 + 13 + T, judged before any field.
|
|
378
|
+
const admission = admitMmtpRepairObject(packet, header, this.symbolSize, this.algorithm)
|
|
379
|
+
if (!admission) return this.#drop('repair:malformed')
|
|
380
|
+
if (!('frame' in admission)) return this.#drop('repair:length')
|
|
381
|
+
const frame = admission.frame
|
|
382
|
+
if (this.#repairPacketId !== undefined && frame.packetId !== this.#repairPacketId) {
|
|
382
383
|
return this.#drop('repair:foreign-packet-id')
|
|
383
384
|
}
|
|
384
|
-
if (
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
385
|
+
if (admission.reject) {
|
|
386
|
+
// An RSB_length below a held layer's total goes to the 7.3.2 stop, not
|
|
387
|
+
// to the discard; the block state is unchanged either way.
|
|
388
|
+
const verdict = this.#heldLayers.failsClosed(
|
|
389
|
+
repairTrackName, frame.ssStart, frame.rsbLength, this.#anchorHorizon, this.#newestSource,
|
|
390
|
+
)
|
|
391
|
+
if (verdict) {
|
|
392
|
+
this.#stats.failClosed = verdict
|
|
393
|
+
return this.#drop('repair:fail-closed')
|
|
394
|
+
}
|
|
395
|
+
return this.#drop(admission.reject === 'repairRsId' ? 'repair:rs-id' : 'repair:scheme-bound')
|
|
390
396
|
}
|
|
397
|
+
this.#repairPacketId ??= frame.packetId
|
|
398
|
+
if (frame.ssbLength > this.sourceSymbols) return this.#drop('repair:ssb-length')
|
|
391
399
|
// fec-01 6.3.9, after the 4.5 discards and the size check, before a block
|
|
392
400
|
// is placed. Without a track name only the anchored rule (b) applies.
|
|
393
401
|
const verdict = this.#heldLayers.admit(
|
package/src/mmtp-router.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
serial32Precedes,
|
|
22
22
|
type FecBlockProfile,
|
|
23
23
|
type MMTPHeader,
|
|
24
|
+
type MoqRepairFrame,
|
|
24
25
|
} from "@blockcast/mmt-base";
|
|
25
26
|
import type { IsoHeldSource, WasmFecDecoder } from "./fec-manager.js";
|
|
26
27
|
import type { IsoPlacedSource, IsoRepairPlacement } from "./iso-block-locator.js";
|
|
@@ -66,9 +67,10 @@ import {
|
|
|
66
67
|
StreamDuplicateLedgers,
|
|
67
68
|
type DuplicateHorizon,
|
|
68
69
|
} from "./duplicate-ledger.js";
|
|
69
|
-
import
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
import {
|
|
71
|
+
isBcpV2RecordCandidate,
|
|
72
|
+
type BcProvenanceV2Receiver,
|
|
73
|
+
type BcpV2RecordPath,
|
|
72
74
|
} from "@blockcast/mmt-manifest-verify";
|
|
73
75
|
|
|
74
76
|
// ─── Constants ─────────────────────────────────────────────────────
|
|
@@ -176,9 +178,10 @@ export type RoutedFrameMeta = {
|
|
|
176
178
|
export interface MmtpIngressContext {
|
|
177
179
|
tracks: readonly Readonly<{ name: string; packetId: number }>[];
|
|
178
180
|
/**
|
|
179
|
-
* The bc-provenance v2 flow (draft-ramadan-moq-multicast-
|
|
181
|
+
* The bc-provenance v2 flow (draft-ramadan-moq-multicast-02 7.2.7) that
|
|
180
182
|
* delivered the packet, when its endpoint is protected: the key a receiver
|
|
181
|
-
* was registered under with `registerProvenanceFlow`.
|
|
183
|
+
* was registered under with `registerProvenanceFlow`. Its records arrive on
|
|
184
|
+
* the same ingress and `feedPacket` splits them off.
|
|
182
185
|
*/
|
|
183
186
|
flow?: string;
|
|
184
187
|
}
|
|
@@ -597,6 +600,15 @@ export interface FECManagerLike {
|
|
|
597
600
|
/** fec-01 4.5: refuse a repair header whose block length the track's scheme
|
|
598
601
|
* forbids, before it touches block state (optional). */
|
|
599
602
|
admitRepairBlockLength?(packetId: number, ssbLength: number, rsbLength: number, rsId: number): boolean;
|
|
603
|
+
/** core-00 7.5 / mmt-02 7.3 admission of one repair object of the named
|
|
604
|
+
* instance; counts a discard per instance (optional). */
|
|
605
|
+
admitRepairObject?(
|
|
606
|
+
packetId: number,
|
|
607
|
+
catalogTrackName: string | undefined,
|
|
608
|
+
repairTrackName: string | undefined,
|
|
609
|
+
packet: Uint8Array,
|
|
610
|
+
header: MMTPHeader,
|
|
611
|
+
): MoqRepairFrame | null;
|
|
600
612
|
admitRepairLayerTotal?(
|
|
601
613
|
packetId: number,
|
|
602
614
|
catalogTrackName: string | undefined,
|
|
@@ -2151,8 +2163,9 @@ export class MmtpRouter {
|
|
|
2151
2163
|
}
|
|
2152
2164
|
|
|
2153
2165
|
/**
|
|
2154
|
-
* Feed one bc-provenance record of `flow`: a datagram
|
|
2155
|
-
* (`multicast`
|
|
2166
|
+
* Feed one bc-provenance record of `flow`: a datagram on the flow's own
|
|
2167
|
+
* `port` (`multicast`; `feedPacket` splits these off on "MHM2",
|
|
2168
|
+
* multicast-02 7.2.7) or an object of the manifest track (`mirror`). Held
|
|
2156
2169
|
* datagrams the record admits are routed before this returns.
|
|
2157
2170
|
*/
|
|
2158
2171
|
feedProvenanceRecord(flow: string, record: Uint8Array, path: BcpV2RecordPath): string {
|
|
@@ -2218,6 +2231,16 @@ export class MmtpRouter {
|
|
|
2218
2231
|
ingress?: MmtpIngressContext,
|
|
2219
2232
|
): void {
|
|
2220
2233
|
if (this.#disposed) return;
|
|
2234
|
+
// multicast-02 7.2.7: on a protected flow's `port`, a datagram whose first
|
|
2235
|
+
// four octets are "MHM2" is a record candidate. The split precedes every
|
|
2236
|
+
// other read: a candidate is never parsed as MMTP, held, hashed as data,
|
|
2237
|
+
// sampled as an arrival or counted by packet_id, and one that fails step 1
|
|
2238
|
+
// is dropped there.
|
|
2239
|
+
if (source === "multicast" && ingress?.flow !== undefined && isBcpV2RecordCandidate(packet)) {
|
|
2240
|
+
if (this.#provenance.has(ingress.flow)) this.feedProvenanceRecord(ingress.flow, packet, "multicast");
|
|
2241
|
+
else this.#unregisteredFlowDrops++;
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2221
2244
|
this.#withDeferredFecPokes(() => {
|
|
2222
2245
|
this.#processMMTP(packet, source, false, ingress);
|
|
2223
2246
|
});
|
|
@@ -2290,9 +2313,6 @@ export class MmtpRouter {
|
|
|
2290
2313
|
} catch {
|
|
2291
2314
|
return;
|
|
2292
2315
|
}
|
|
2293
|
-
const parsed = parseMoqRepairFrameWithHeader(packet, header);
|
|
2294
|
-
if (!parsed) return;
|
|
2295
|
-
|
|
2296
2316
|
// A dedicated repair track can carry a packet ID that differs from the
|
|
2297
2317
|
// media packet ID. The subscribing track supplies the authoritative owner.
|
|
2298
2318
|
const repairPacketId = header.packetId;
|
|
@@ -2310,18 +2330,24 @@ export class MmtpRouter {
|
|
|
2310
2330
|
`[MmtpRouter] Dropping MoQ repair packet pid=${repairPacketId}: owner hint is required`,
|
|
2311
2331
|
);
|
|
2312
2332
|
}
|
|
2313
|
-
|
|
2333
|
+
// The frame is read only once 7.2.7 admits the datagram: admission
|
|
2334
|
+
// (core-00 7.5) counts rejects and can fail the instance closed. A
|
|
2335
|
+
// malformed held datagram is judged, and counted, when a record admits it.
|
|
2314
2336
|
// Admission keys on the repair track's own packet id: a repair layer no
|
|
2315
2337
|
// protected flow lists is an unlisted track (7.2.7), not a datagram of the
|
|
2316
2338
|
// flow that lists its owner, where it would be held until expiry. The
|
|
2317
2339
|
// flow listing it governs the owner too: a repair flow also lists its
|
|
2318
2340
|
// owners' source tracks (#provenanceFor).
|
|
2341
|
+
let unlisted = false;
|
|
2319
2342
|
if (ownerPacketId != null && !admitted) {
|
|
2320
2343
|
const admit = this.#admitUnicast(repairPacketId, packet, () =>
|
|
2321
2344
|
this.#feedRepairPacket(packet, ownerPacketIdHint, catalogTrackName, repairTrackName, true));
|
|
2322
2345
|
if (admit === "held" || admit === "dropped") return;
|
|
2323
|
-
|
|
2346
|
+
unlisted = admit === "unlisted";
|
|
2324
2347
|
}
|
|
2348
|
+
const parsed = this.#readRepairObject(ownerPacketId, catalogTrackName, repairTrackName, packet, header);
|
|
2349
|
+
if (!parsed) return;
|
|
2350
|
+
if (unlisted && this.#fecManager && this.#setAsideUnlistedRepair(ownerPacketId!, catalogTrackName, parsed)) return;
|
|
2325
2351
|
if (ownerPacketId != null && this.#fecManager) {
|
|
2326
2352
|
this.#moqRepairPidCounts[repairPacketId] =
|
|
2327
2353
|
(this.#moqRepairPidCounts[repairPacketId] || 0) + 1;
|
|
@@ -2432,6 +2458,25 @@ export class MmtpRouter {
|
|
|
2432
2458
|
return serial32Add(blockStart, index);
|
|
2433
2459
|
}
|
|
2434
2460
|
|
|
2461
|
+
/**
|
|
2462
|
+
* A repair packet's frame, admitted under its owner instance's T and scheme
|
|
2463
|
+
* when that instance is configured (draft-ramadan-moq-fec-core-00 7.5,
|
|
2464
|
+
* draft-ramadan-moq-mmt-02 7.3); otherwise only parsed.
|
|
2465
|
+
*/
|
|
2466
|
+
#readRepairObject(
|
|
2467
|
+
ownerPacketId: number | null,
|
|
2468
|
+
catalogTrackName: string | undefined,
|
|
2469
|
+
repairTrackName: string | undefined,
|
|
2470
|
+
packet: Uint8Array,
|
|
2471
|
+
header: MMTPHeader,
|
|
2472
|
+
): MoqRepairFrame | null {
|
|
2473
|
+
const fec = this.#fecManager;
|
|
2474
|
+
if (ownerPacketId != null && fec?.admitRepairObject && this.#configuredBlockProfile(ownerPacketId) !== undefined) {
|
|
2475
|
+
return fec.admitRepairObject(ownerPacketId, catalogTrackName, repairTrackName, packet, header);
|
|
2476
|
+
}
|
|
2477
|
+
return parseMoqRepairFrameWithHeader(packet, header);
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2435
2480
|
/**
|
|
2436
2481
|
* One iso-ssbg1-v1 repair symbol (ISO/IEC 23008-1 C.5.3). It states the
|
|
2437
2482
|
* block `[SS_Start, SS_Start + SSB_length)`; repair ESI = SSB_length +
|
|
@@ -3505,12 +3550,16 @@ export class MmtpRouter {
|
|
|
3505
3550
|
// Repair packet — per ISO 23008-1 §C.5.3, carries (SS_Start, SSB_length, RS_ID).
|
|
3506
3551
|
// Read from the header parsed above rather than parsing it again
|
|
3507
3552
|
// (BLO-36599).
|
|
3508
|
-
const repair = parseMoqRepairFrameWithHeader(packet, parsed.header);
|
|
3509
|
-
if (!repair) return;
|
|
3510
|
-
|
|
3511
3553
|
const ownerPacketId = this.#resolveRepairOwnerPacketId(info.packetId, ingress);
|
|
3554
|
+
// An ambiguous name attributes the packet to no instance, so it is
|
|
3555
|
+
// dropped before admission can count or fail closed against one.
|
|
3556
|
+
if (ownerPacketId != null && this.#fecManager && hasAmbiguousCatalogTrackName(ingress, ownerPacketId)) return;
|
|
3557
|
+
const repair = this.#readRepairObject(
|
|
3558
|
+
ownerPacketId, ownerPacketId == null ? undefined : catalogTrackNameForPacket(ingress, ownerPacketId),
|
|
3559
|
+
undefined, packet, parsed.header,
|
|
3560
|
+
);
|
|
3561
|
+
if (!repair) return;
|
|
3512
3562
|
if (ownerPacketId != null && this.#fecManager) {
|
|
3513
|
-
if (hasAmbiguousCatalogTrackName(ingress, ownerPacketId)) return;
|
|
3514
3563
|
// An unadmitted MoQ symbol reaching here is unlisted (#admitUnicast).
|
|
3515
3564
|
if (
|
|
3516
3565
|
source === "moq" && !manifestAdmitted &&
|