@blockcast/mmt-container 0.4.1 → 0.5.0-main.2c1f81817995
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 +2 -2
- package/dist/browser-runtime.js +3458 -356
- package/dist/browser-runtime.js.map +4 -4
- package/dist/cmcd-producer.d.ts +5 -1
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +18 -2
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/fec-manager.d.ts +21 -16
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +177 -80
- package/dist/fec-manager.js.map +3 -3
- package/dist/held-layer-admission.d.ts +58 -0
- package/dist/held-layer-admission.d.ts.map +1 -0
- package/dist/held-layer-admission.js +105 -0
- package/dist/held-layer-admission.js.map +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +2682 -1023
- package/dist/mmtp-container-parser.no-modules.js.map +4 -4
- package/dist/mmtp-fec-session.d.ts +36 -3
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +114 -5
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +72 -28
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +321 -203
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-auth-catalog.d.ts +10 -2
- package/dist/multicast-auth-catalog.d.ts.map +1 -1
- package/dist/multicast-auth-catalog.js +422 -22
- package/dist/multicast-auth-catalog.js.map +4 -4
- package/dist/multicast-manager.d.ts +48 -9
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +3406 -340
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +4 -1
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +20 -3
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.d.ts +1 -2
- package/dist/no-modules.d.ts.map +1 -1
- package/dist/no-modules.js +2668 -1007
- package/dist/no-modules.js.map +4 -4
- package/dist/repair-fetch.d.ts +10 -1
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +5 -2
- package/dist/repair-fetch.js.map +2 -2
- package/package.json +5 -14
- package/src/cmcd-producer.ts +6 -1
- package/src/fec-manager.ts +121 -131
- package/src/held-layer-admission.ts +161 -0
- package/src/index.ts +5 -9
- package/src/mmtp-fec-session.ts +76 -3
- package/src/mmtp-router.ts +390 -272
- package/src/multicast-auth-catalog.ts +81 -8
- package/src/multicast-manager.ts +189 -48
- package/src/multicast-transport.ts +6 -2
- package/src/no-modules.ts +1 -6
- package/src/repair-fetch.ts +24 -4
- package/dist/manifest-auth-plan.d.ts +0 -31
- package/dist/manifest-auth-plan.d.ts.map +0 -1
- package/dist/manifest-auth-plan.js +0 -426
- package/dist/manifest-auth-plan.js.map +0 -7
- package/dist/source-auth.d.ts +0 -2
- package/dist/source-auth.d.ts.map +0 -1
- package/dist/source-auth.js +0 -3912
- package/dist/source-auth.js.map +0 -7
- package/src/manifest-auth-plan.ts +0 -55
- package/src/source-auth.ts +0 -1
- package/test-vectors/generate-source-auth-fixture.mjs +0 -114
- package/test-vectors/source-auth-v1.json +0 -61
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockcast/mmt-container",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-main.2c1f81817995",
|
|
4
4
|
"description": "MMTP/LOC container parsing for MMT over MoQ and SSM",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,10 +14,6 @@
|
|
|
14
14
|
"import": "./dist/browser-runtime.js",
|
|
15
15
|
"types": "./dist/index.d.ts"
|
|
16
16
|
},
|
|
17
|
-
"./source-auth": {
|
|
18
|
-
"import": "./dist/source-auth.js",
|
|
19
|
-
"types": "./dist/source-auth.d.ts"
|
|
20
|
-
},
|
|
21
17
|
"./mfu": {
|
|
22
18
|
"import": "./dist/mfu-reassembler.js",
|
|
23
19
|
"types": "./dist/mfu-reassembler.d.ts"
|
|
@@ -102,10 +98,6 @@
|
|
|
102
98
|
"import": "./dist/multicast-auth-catalog.js",
|
|
103
99
|
"types": "./dist/multicast-auth-catalog.d.ts"
|
|
104
100
|
},
|
|
105
|
-
"./manifest-auth-plan": {
|
|
106
|
-
"import": "./dist/manifest-auth-plan.js",
|
|
107
|
-
"types": "./dist/manifest-auth-plan.d.ts"
|
|
108
|
-
},
|
|
109
101
|
"./catalog-validator": {
|
|
110
102
|
"import": "./dist/catalog-validator.js",
|
|
111
103
|
"types": "./dist/catalog-validator.d.ts"
|
|
@@ -166,17 +158,16 @@
|
|
|
166
158
|
"build": "tsc --emitDeclarationOnly && node build.mjs",
|
|
167
159
|
"dev": "tsc --watch",
|
|
168
160
|
"clean": "rm -rf dist",
|
|
169
|
-
"bench:source-auth": "node bench/source-auth-wasm.mjs",
|
|
170
161
|
"bench:amt-hot-path": "node --expose-gc --max-semi-space-size=64 bench/amt-hot-path.mjs",
|
|
171
162
|
"test:catalog-validator-public-api": "node test-vectors/catalog-validator-public-api.mjs",
|
|
172
163
|
"test": "vitest run",
|
|
173
164
|
"test:watch": "vitest"
|
|
174
165
|
},
|
|
175
166
|
"dependencies": {
|
|
176
|
-
"@blockcast/fec-worker": "
|
|
177
|
-
"@blockcast/mmt-base": "
|
|
178
|
-
"@blockcast/mmt-fec": "
|
|
179
|
-
"@blockcast/mmt-manifest-verify": "
|
|
167
|
+
"@blockcast/fec-worker": "0.5.0-main.2c1f81817995",
|
|
168
|
+
"@blockcast/mmt-base": "0.5.0-main.2c1f81817995",
|
|
169
|
+
"@blockcast/mmt-fec": "0.5.0-main.2c1f81817995",
|
|
170
|
+
"@blockcast/mmt-manifest-verify": "0.5.0-main.2c1f81817995",
|
|
180
171
|
"@noble/curves": "^1.9.7",
|
|
181
172
|
"@noble/hashes": "^1.8.0",
|
|
182
173
|
"ajv": "^8.17.1",
|
package/src/cmcd-producer.ts
CHANGED
|
@@ -14,13 +14,18 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { FecTrackStats } from '@blockcast/fec-worker'
|
|
17
|
-
import type { ManifestVerificationStats } from '@blockcast/mmt-manifest-verify'
|
|
18
17
|
import type { FecFailClosedReason } from './fec-manager.js'
|
|
19
18
|
|
|
20
19
|
// ---------------------------------------------------------------------------
|
|
21
20
|
// Types
|
|
22
21
|
// ---------------------------------------------------------------------------
|
|
23
22
|
|
|
23
|
+
/** Source-auth counters for CMCD: records verified, and drops per reason. */
|
|
24
|
+
export interface ManifestVerificationStats {
|
|
25
|
+
verified: number
|
|
26
|
+
failures: Record<string, number>
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
/** Transport state snapshot, built by the player wiring layer from SocketPool. */
|
|
25
30
|
export interface TransportSnapshot {
|
|
26
31
|
transport: 'native' | 'amt' | 'moq'
|
package/src/fec-manager.ts
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
type TrackKind,
|
|
47
47
|
} from './block-policy.js'
|
|
48
48
|
|
|
49
|
+
import { HeldLayerAdmission, heldLayerAnchorHorizon, type FecFailClosedReason } from './held-layer-admission.js'
|
|
49
50
|
import type { MmtpDeliveryProvenance } from './mfu-reassembler.js'
|
|
50
51
|
import type {
|
|
51
52
|
FecWorkerClient,
|
|
@@ -291,21 +292,7 @@ export interface FecRepairLayer {
|
|
|
291
292
|
repairSymbols: number
|
|
292
293
|
}
|
|
293
294
|
|
|
294
|
-
|
|
295
|
-
* Why a source track's FEC failed closed at runtime (fec-01 6.3.9): a repair
|
|
296
|
-
* packet stated an RSB_length below a held layer's total B_i on that layer's
|
|
297
|
-
* own repair track (`below_own_track_total`), or at or after the anchor, the
|
|
298
|
-
* earliest block in SS_Start serial order whose repair carried >= B_i
|
|
299
|
-
* (`below_anchored_total`). Repair for the track is refused from then on.
|
|
300
|
-
*/
|
|
301
|
-
export interface FecFailClosedReason {
|
|
302
|
-
code: 'below_own_track_total' | 'below_anchored_total'
|
|
303
|
-
layer: number
|
|
304
|
-
total: number
|
|
305
|
-
rsbLength: number
|
|
306
|
-
ssStart: number
|
|
307
|
-
anchor?: number
|
|
308
|
-
}
|
|
295
|
+
export type { FecFailClosedReason } from './held-layer-admission.js'
|
|
309
296
|
|
|
310
297
|
/** A {@link FecFailClosedReason} with the stream it closed, as reported to the fail-closed handler. */
|
|
311
298
|
export interface FecFailClosedEvent extends FecFailClosedReason {
|
|
@@ -313,26 +300,6 @@ export interface FecFailClosedEvent extends FecFailClosedReason {
|
|
|
313
300
|
catalogTrackName: string | undefined
|
|
314
301
|
}
|
|
315
302
|
|
|
316
|
-
interface HeldLayerState {
|
|
317
|
-
layer: number
|
|
318
|
-
repairTrackName: string
|
|
319
|
-
total: number
|
|
320
|
-
/** Earliest (serial order) block start whose repair carried >= total. */
|
|
321
|
-
anchor?: number
|
|
322
|
-
/** Latest (serial order) block start whose repair carried < total. */
|
|
323
|
-
latestBelow?: number
|
|
324
|
-
/** RSB_length that latestBelow's repair carried, for the fail-closed reason. */
|
|
325
|
-
latestBelowRsb?: number
|
|
326
|
-
/** Newest block start seen; anchor and latestBelow are retired behind it. */
|
|
327
|
-
newest?: number
|
|
328
|
-
/**
|
|
329
|
-
* The anchor fell behind the horizon, so no block that can still arrive
|
|
330
|
-
* precedes it: every later packet must carry >= total. Its serial is no
|
|
331
|
-
* longer compared, so SS_Start wrapping past it cannot misorder it.
|
|
332
|
-
*/
|
|
333
|
-
settled?: boolean
|
|
334
|
-
}
|
|
335
|
-
|
|
336
303
|
interface ResolvedFECTrackConfig {
|
|
337
304
|
blockProfile?: FecBlockProfile
|
|
338
305
|
algorithm?: 'raptorq' | 'reed-solomon'
|
|
@@ -544,7 +511,7 @@ export class FECManager {
|
|
|
544
511
|
// symbols its repair group carries. Same lifecycle as #blockRepairRsIds.
|
|
545
512
|
#blockRsbLengths = new Map<string, number>()
|
|
546
513
|
/** fec-01 6.3.9 anchor state per stream key, one entry per held layer. */
|
|
547
|
-
#heldLayerStates = new Map<string,
|
|
514
|
+
#heldLayerStates = new Map<string, HeldLayerAdmission>()
|
|
548
515
|
/** Streams whose FEC failed closed at runtime (fec-01 6.3.9). */
|
|
549
516
|
#fecFailClosed = new Map<string, FecFailClosedReason>()
|
|
550
517
|
#onFecFailClosed: ((event: FecFailClosedEvent) => void) | null = null
|
|
@@ -583,6 +550,9 @@ export class FECManager {
|
|
|
583
550
|
// Receiver-local first-accepted-symbol time per block, bounded like
|
|
584
551
|
// #blockPathCounts (see blockAnchorAtMs).
|
|
585
552
|
#blockAnchorsMs = new Map<string, number>()
|
|
553
|
+
// The receipt time of the packet being fed, when it is not now: a datagram
|
|
554
|
+
// held on a miss and admitted later (withReceiptTime).
|
|
555
|
+
#receiptAtMs: number | undefined = undefined
|
|
586
556
|
// Exact catalog identity observed on accepted symbols for each FEC block.
|
|
587
557
|
// Conflicting identities collapse to null so recovery fails closed.
|
|
588
558
|
#blockCatalogTrackNames = new Map<string, string | null>()
|
|
@@ -630,6 +600,9 @@ export class FECManager {
|
|
|
630
600
|
// Worker recovery emits all K source symbols. Track recovery once per block
|
|
631
601
|
// while routing only ESI slots that were absent from the direct source path.
|
|
632
602
|
#workerRecoveredBlocks = new Set<string>()
|
|
603
|
+
// Worker-recovered blocks a recovered packet of which admission refused,
|
|
604
|
+
// until the decode's final symbol (7.2.7: the block is unrecovered).
|
|
605
|
+
#refusedRecoveries = new Set<string>()
|
|
633
606
|
|
|
634
607
|
// How the multicast leg delivers symbols (see MulticastSymbolDelivery). The
|
|
635
608
|
// provider declares it per transport join; absence keeps datagram semantics.
|
|
@@ -833,7 +806,14 @@ export class FECManager {
|
|
|
833
806
|
| null = null
|
|
834
807
|
|
|
835
808
|
// Worker-path callbacks — async recovery and stats from FecWorkerClient.
|
|
836
|
-
|
|
809
|
+
/**
|
|
810
|
+
* A recovered source symbol. Return false when admission refused the
|
|
811
|
+
* packet it carries (bc-provenance v2, draft-ramadan-moq-multicast-01
|
|
812
|
+
* 7.2.7): the block is then unrecovered and stays open to its FEC deadline.
|
|
813
|
+
*/
|
|
814
|
+
onBlockRecovered:
|
|
815
|
+
| ((sbn: number, packetId: number, data: ArrayBuffer, meta: FrameMeta, catalogTrackName?: string) => boolean | void)
|
|
816
|
+
| null = null
|
|
837
817
|
onFecStatsUpdate: ((packetId: number, stats: FecTrackStats) => void) | null = null
|
|
838
818
|
|
|
839
819
|
#encodeTrackName(catalogTrackName: string): string {
|
|
@@ -978,6 +958,7 @@ export class FECManager {
|
|
|
978
958
|
this.#completedBlocks.delete(record.key)
|
|
979
959
|
this.#cleanlyCompletedBlocks.delete(record.key)
|
|
980
960
|
this.#workerRecoveredBlocks.delete(record.key)
|
|
961
|
+
this.#refusedRecoveries.delete(record.key)
|
|
981
962
|
// The policy's terminal ledgers describe the same terminal state and
|
|
982
963
|
// retire with it: a failed block must never outlive its failure record
|
|
983
964
|
// as 'complete' (that would hide a loss from the release gate).
|
|
@@ -1444,7 +1425,7 @@ export class FECManager {
|
|
|
1444
1425
|
catalogTrackName: string | undefined,
|
|
1445
1426
|
delayMs: number,
|
|
1446
1427
|
): void {
|
|
1447
|
-
const deadlineMs =
|
|
1428
|
+
const deadlineMs = this.#notEarlierUnderReceipt(this.#receiptNow() + delayMs, this.#repairDeadlineAtMs.get(key))
|
|
1448
1429
|
this.#repairDeadlineAtMs.set(key, deadlineMs)
|
|
1449
1430
|
const armed = this.#repairDeadlines.get(key)
|
|
1450
1431
|
if (armed?.handle != null) {
|
|
@@ -1464,7 +1445,7 @@ export class FECManager {
|
|
|
1464
1445
|
timer.handle = null
|
|
1465
1446
|
this.#requestRepair(sbn, packetId, catalogTrackName)
|
|
1466
1447
|
}
|
|
1467
|
-
timer.handle = setTimeout(onFire,
|
|
1448
|
+
timer.handle = setTimeout(onFire, Math.max(0, deadlineMs - performance.now()))
|
|
1468
1449
|
this.#repairDeadlines.set(key, timer)
|
|
1469
1450
|
}
|
|
1470
1451
|
|
|
@@ -1668,6 +1649,10 @@ export class FECManager {
|
|
|
1668
1649
|
ssId = sbn,
|
|
1669
1650
|
): void {
|
|
1670
1651
|
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
1652
|
+
// Arrival at FEC, not receipt (withReceiptTime): this bounds how far SS_ID
|
|
1653
|
+
// can plausibly move between symbols this instance has seen, and a held
|
|
1654
|
+
// datagram admitted late reached it no earlier than now. A block's ts is
|
|
1655
|
+
// its anchor (#blockAnchorsMs), which does take the receipt time.
|
|
1671
1656
|
const now = performance.now()
|
|
1672
1657
|
const previous = (symbol === 'source' ? this.#sourceFrontiers : this.#highestSbn).get(streamKey)
|
|
1673
1658
|
const anchor = this.#sourceAnchors.get(streamKey)
|
|
@@ -2017,7 +2002,10 @@ export class FECManager {
|
|
|
2017
2002
|
delayMs: number,
|
|
2018
2003
|
): void {
|
|
2019
2004
|
const key = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
2020
|
-
const waitAtMs =
|
|
2005
|
+
const waitAtMs = this.#notEarlierUnderReceipt(
|
|
2006
|
+
this.#receiptNow() + Math.max(1, Math.ceil(delayMs)),
|
|
2007
|
+
this.#blockWaitDeadlinesMs.get(key),
|
|
2008
|
+
)
|
|
2021
2009
|
// A held block's symbols (its own FETCH's repair symbols included) never
|
|
2022
2010
|
// carry its wait past the hold: the hold is its presentation deadline.
|
|
2023
2011
|
const holdAtMs = this.#repairHoldUntilMs.get(key)
|
|
@@ -2534,6 +2522,7 @@ export class FECManager {
|
|
|
2534
2522
|
resetSet(this.#cleanlyCompletedBlocks)
|
|
2535
2523
|
resetSet(this.#repairRequestedBlocks)
|
|
2536
2524
|
resetSet(this.#workerRecoveredBlocks)
|
|
2525
|
+
resetSet(this.#refusedRecoveries)
|
|
2537
2526
|
resetSet(this.#startupAlignedPackets)
|
|
2538
2527
|
resetSet(this.#startupPartialBlocks)
|
|
2539
2528
|
resetSet(this.#startupRepairReadyPackets)
|
|
@@ -2668,6 +2657,7 @@ export class FECManager {
|
|
|
2668
2657
|
dropSet(this.#cleanlyCompletedBlocks)
|
|
2669
2658
|
dropSet(this.#repairRequestedBlocks)
|
|
2670
2659
|
dropSet(this.#workerRecoveredBlocks)
|
|
2660
|
+
dropSet(this.#refusedRecoveries)
|
|
2671
2661
|
dropSet(this.#startupAlignedPackets)
|
|
2672
2662
|
dropSet(this.#startupPartialBlocks)
|
|
2673
2663
|
dropSet(this.#startupRepairReadyPackets)
|
|
@@ -2832,7 +2822,10 @@ export class FECManager {
|
|
|
2832
2822
|
// The block's absolute anchor (fecBlockDeadlineMs): the receiver-local
|
|
2833
2823
|
// time of its first accepted symbol, from which its presentation deadline
|
|
2834
2824
|
// is measured.
|
|
2835
|
-
|
|
2825
|
+
// A datagram held and admitted later keeps its receipt time (fec-01 9).
|
|
2826
|
+
const receivedAtMs = this.#receiptNow()
|
|
2827
|
+
const anchorMs = this.#blockAnchorsMs.get(blockKey)
|
|
2828
|
+
if (anchorMs === undefined || receivedAtMs < anchorMs) this.#blockAnchorsMs.set(blockKey, receivedAtMs)
|
|
2836
2829
|
if (source === 'multicast') counts.multicastSymbols++
|
|
2837
2830
|
else counts.moqSymbols++
|
|
2838
2831
|
this.#blockPathCounts.set(blockKey, counts)
|
|
@@ -3023,6 +3016,18 @@ export class FECManager {
|
|
|
3023
3016
|
blockAnchorAtMs(sbn: number, packetId: number, catalogTrackName?: string): number | undefined {
|
|
3024
3017
|
return this.#blockAnchorsMs.get(this.#blockKey(sbn, packetId, catalogTrackName))
|
|
3025
3018
|
}
|
|
3019
|
+
/**
|
|
3020
|
+
* How long ago (ms) the oldest open block of the stream, one with an FEC
|
|
3021
|
+
* deadline, took its first accepted symbol; undefined with none open.
|
|
3022
|
+
*/
|
|
3023
|
+
oldestOpenBlockAgeMs(packetId: number, catalogTrackName: string): number | undefined {
|
|
3024
|
+
let oldest = Infinity
|
|
3025
|
+
for (const key of this.#activeBlocks.keys()) {
|
|
3026
|
+
if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue
|
|
3027
|
+
oldest = Math.min(oldest, this.#blockAnchorsMs.get(key) ?? Infinity)
|
|
3028
|
+
}
|
|
3029
|
+
return oldest === Infinity ? undefined : performance.now() - oldest
|
|
3030
|
+
}
|
|
3026
3031
|
/** Absolute fail deadline of a still-open block, or undefined. */
|
|
3027
3032
|
blockDeadlineAtMs(sbn: number, packetId: number, catalogTrackName?: string): number | undefined {
|
|
3028
3033
|
return this.#activeBlockDeadlinesMs.get(this.#blockKey(sbn, packetId, catalogTrackName))
|
|
@@ -3687,22 +3692,6 @@ export class FECManager {
|
|
|
3687
3692
|
const blockKey = this.#blockKey(meta.sbn, packetId, recoveredCatalogTrackName)
|
|
3688
3693
|
const cleanlyCompleted = this.#cleanlyCompletedBlocks.has(blockKey)
|
|
3689
3694
|
if (!cleanlyCompleted) {
|
|
3690
|
-
if (!this.#workerRecoveredBlocks.has(blockKey)) {
|
|
3691
|
-
this.#traceLifecycle(
|
|
3692
|
-
'recovered',
|
|
3693
|
-
meta.sbn,
|
|
3694
|
-
packetId,
|
|
3695
|
-
recoveredCatalogTrackName,
|
|
3696
|
-
)
|
|
3697
|
-
this.#workerRecoveredBlocks.add(blockKey)
|
|
3698
|
-
this.#fecStats.blocksRecovered++
|
|
3699
|
-
this.#statsForPacket(packetId).blocksRecovered++
|
|
3700
|
-
if (this.#workerRecoveredBlocks.size > 1000) {
|
|
3701
|
-
dropOldest(this.#workerRecoveredBlocks, 500)
|
|
3702
|
-
}
|
|
3703
|
-
this.#noteBlockRepaired(blockKey, meta.sbn, packetId, recoveredCatalogTrackName)
|
|
3704
|
-
}
|
|
3705
|
-
|
|
3706
3695
|
// RaptorQ returns the full K-symbol block, not only erased symbols.
|
|
3707
3696
|
// The direct path already parsed every source ESI in #seenSymbols;
|
|
3708
3697
|
// routing those symbols again injects duplicate MFU fragments and can
|
|
@@ -3710,23 +3699,46 @@ export class FECManager {
|
|
|
3710
3699
|
const symbolKey = `${blockKey}:${esi}`
|
|
3711
3700
|
if (!this.#seenSymbols.has(symbolKey)) {
|
|
3712
3701
|
this.#seenSymbols.add(symbolKey)
|
|
3713
|
-
this.onBlockRecovered?.(
|
|
3702
|
+
const admitted = this.onBlockRecovered?.(
|
|
3714
3703
|
meta.sbn,
|
|
3715
3704
|
packetId,
|
|
3716
3705
|
data,
|
|
3717
3706
|
meta,
|
|
3718
3707
|
recoveredCatalogTrackName,
|
|
3719
3708
|
)
|
|
3709
|
+
if (admitted === false) {
|
|
3710
|
+
this.#refusedRecoveries.add(blockKey)
|
|
3711
|
+
if (this.#refusedRecoveries.size > 1000) dropOldest(this.#refusedRecoveries, 500)
|
|
3712
|
+
}
|
|
3720
3713
|
}
|
|
3721
3714
|
}
|
|
3722
3715
|
|
|
3723
3716
|
// The worker emits recovered source symbols in ESI order. Complete the
|
|
3724
3717
|
// block only after its final authored slot so assemblers receive every
|
|
3725
3718
|
// missing symbol before their completion callback flushes buffered media.
|
|
3719
|
+
// A decode one of whose recovered packets admission refused recovered
|
|
3720
|
+
// nothing (7.2.7): the block stays open and fails at its FEC deadline.
|
|
3721
|
+
const refused = esi >= meta.ssbLength! - 1 && this.#refusedRecoveries.delete(blockKey)
|
|
3726
3722
|
if (
|
|
3727
3723
|
esi >= meta.ssbLength! - 1 &&
|
|
3724
|
+
!refused &&
|
|
3728
3725
|
!this.#completedBlocks.has(blockKey)
|
|
3729
3726
|
) {
|
|
3727
|
+
if (!cleanlyCompleted && !this.#workerRecoveredBlocks.has(blockKey)) {
|
|
3728
|
+
this.#traceLifecycle(
|
|
3729
|
+
'recovered',
|
|
3730
|
+
meta.sbn,
|
|
3731
|
+
packetId,
|
|
3732
|
+
recoveredCatalogTrackName,
|
|
3733
|
+
)
|
|
3734
|
+
this.#workerRecoveredBlocks.add(blockKey)
|
|
3735
|
+
this.#fecStats.blocksRecovered++
|
|
3736
|
+
this.#statsForPacket(packetId).blocksRecovered++
|
|
3737
|
+
if (this.#workerRecoveredBlocks.size > 1000) {
|
|
3738
|
+
dropOldest(this.#workerRecoveredBlocks, 500)
|
|
3739
|
+
}
|
|
3740
|
+
this.#noteBlockRepaired(blockKey, meta.sbn, packetId, recoveredCatalogTrackName)
|
|
3741
|
+
}
|
|
3730
3742
|
this.cancelBlockTimeout(meta.sbn, packetId, recoveredCatalogTrackName)
|
|
3731
3743
|
if (recoveredCatalogTrackName === undefined) {
|
|
3732
3744
|
this.onBlockComplete?.(meta.sbn, packetId)
|
|
@@ -4106,77 +4118,26 @@ export class FECManager {
|
|
|
4106
4118
|
const config = this.#trackConfigs.get(packetId)
|
|
4107
4119
|
const layers = config?.repairLayers
|
|
4108
4120
|
if (layers === undefined || layers.length === 0) return true
|
|
4109
|
-
|
|
4110
|
-
|
|
4121
|
+
let admission = this.#heldLayerStates.get(streamKey)
|
|
4122
|
+
if (admission === undefined) {
|
|
4123
|
+
admission = new HeldLayerAdmission(layers, config!.heldLayers)
|
|
4124
|
+
this.#heldLayerStates.set(streamKey, admission)
|
|
4111
4125
|
}
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
// held sources, so dropping it would stall recovery after a long outage.
|
|
4128
|
-
// IsoBlockLocator.placeRepair may still refuse the same repair as `ahead`:
|
|
4129
|
-
// it measures from `newest`, which held sources move, not from this frontier.
|
|
4130
|
-
const frontier = this.#sourceFrontiers.get(streamKey)?.sbn
|
|
4131
|
-
const farAhead =
|
|
4132
|
-
frontier !== undefined && serial32Precedes(frontier, ssStart) && serial32Distance(frontier, ssStart) > horizon
|
|
4133
|
-
for (const state of states) {
|
|
4134
|
-
let code: FecFailClosedReason['code'] | undefined
|
|
4135
|
-
if (farAhead) {
|
|
4136
|
-
if (rsbLength < state.total) {
|
|
4137
|
-
if (state.layer > 0 && repairTrackName === state.repairTrackName) code = 'below_own_track_total'
|
|
4138
|
-
else if (state.anchor !== undefined || state.settled) code = 'below_anchored_total'
|
|
4139
|
-
}
|
|
4140
|
-
} else {
|
|
4141
|
-
// Serials more than `horizon` behind the newest block are retired, so
|
|
4142
|
-
// every serial still compared sits inside one half-window and SS_Start
|
|
4143
|
-
// wrapping on a long-running stream cannot reorder them.
|
|
4144
|
-
if (state.newest === undefined || serial32Precedes(state.newest, ssStart)) state.newest = ssStart
|
|
4145
|
-
const stale = (s: number) => serial32Distance(s, state.newest!) > horizon
|
|
4146
|
-
if (state.latestBelow !== undefined && stale(state.latestBelow)) {
|
|
4147
|
-
delete state.latestBelow
|
|
4148
|
-
delete state.latestBelowRsb
|
|
4149
|
-
}
|
|
4150
|
-
if (state.anchor !== undefined && stale(state.anchor)) state.settled = true
|
|
4151
|
-
if (rsbLength < state.total) {
|
|
4152
|
-
if (state.layer > 0 && repairTrackName === state.repairTrackName) code = 'below_own_track_total'
|
|
4153
|
-
else if (stale(ssStart)) continue // past retention: undecodable, anchors nothing
|
|
4154
|
-
else if (state.settled || (state.anchor !== undefined && !serial32Precedes(ssStart, state.anchor))) code = 'below_anchored_total'
|
|
4155
|
-
else if (state.latestBelow === undefined || serial32Precedes(state.latestBelow, ssStart)) {
|
|
4156
|
-
state.latestBelow = ssStart
|
|
4157
|
-
state.latestBelowRsb = rsbLength
|
|
4158
|
-
}
|
|
4159
|
-
} else if (!state.settled && !stale(ssStart)) {
|
|
4160
|
-
if (state.anchor === undefined || serial32Precedes(ssStart, state.anchor)) state.anchor = ssStart
|
|
4161
|
-
if (state.latestBelow !== undefined && !serial32Precedes(state.latestBelow, state.anchor)) {
|
|
4162
|
-
code = 'below_anchored_total'
|
|
4163
|
-
ssStart = state.latestBelow
|
|
4164
|
-
rsbLength = state.latestBelowRsb!
|
|
4165
|
-
}
|
|
4166
|
-
}
|
|
4167
|
-
}
|
|
4168
|
-
if (code !== undefined) {
|
|
4169
|
-
const reason: FecFailClosedReason = {
|
|
4170
|
-
code, layer: state.layer, total: state.total, rsbLength, ssStart,
|
|
4171
|
-
...(state.anchor === undefined ? {} : { anchor: state.anchor }),
|
|
4172
|
-
}
|
|
4173
|
-
this.#fecFailClosed.set(streamKey, reason)
|
|
4174
|
-
this.#onFecFailClosed?.({ ...reason, packetId, catalogTrackName })
|
|
4175
|
-
console.warn(
|
|
4176
|
-
`[FECManager] FEC failed closed (fec-01 6.3.9) packetId=${packetId} track=${catalogTrackName ?? '-'}: ${JSON.stringify(reason)}`,
|
|
4177
|
-
)
|
|
4178
|
-
return false
|
|
4179
|
-
}
|
|
4126
|
+
const verdict = admission.admit(
|
|
4127
|
+
repairTrackName,
|
|
4128
|
+
ssStart,
|
|
4129
|
+
rsbLength,
|
|
4130
|
+
this.#layerAnchorHorizon(packetId),
|
|
4131
|
+
this.#sourceFrontiers.get(streamKey)?.sbn,
|
|
4132
|
+
)
|
|
4133
|
+
if (verdict === 'discard') return false
|
|
4134
|
+
if (verdict !== undefined) {
|
|
4135
|
+
this.#fecFailClosed.set(streamKey, verdict)
|
|
4136
|
+
this.#onFecFailClosed?.({ ...verdict, packetId, catalogTrackName })
|
|
4137
|
+
console.warn(
|
|
4138
|
+
`[FECManager] FEC failed closed (fec-01 6.3.9) packetId=${packetId} track=${catalogTrackName ?? '-'}: ${JSON.stringify(verdict)}`,
|
|
4139
|
+
)
|
|
4140
|
+
return false
|
|
4180
4141
|
}
|
|
4181
4142
|
return true
|
|
4182
4143
|
}
|
|
@@ -4187,11 +4148,9 @@ export class FECManager {
|
|
|
4187
4148
|
* times K, capped at 2^30 so compared serials stay inside one half-window.
|
|
4188
4149
|
*/
|
|
4189
4150
|
#layerAnchorHorizon(packetId: number): number {
|
|
4190
|
-
const cap = 0x40000000
|
|
4191
4151
|
let blocks: number | undefined
|
|
4192
4152
|
try { blocks = this.isoRetentionBlocksForPacket(packetId) } catch { blocks = undefined }
|
|
4193
|
-
|
|
4194
|
-
return blocks === undefined || k <= 0 ? cap : Math.min(cap, blocks * k)
|
|
4153
|
+
return heldLayerAnchorHorizon(blocks, this.#trackConfigs.get(packetId)?.k ?? 0)
|
|
4195
4154
|
}
|
|
4196
4155
|
|
|
4197
4156
|
/**
|
|
@@ -4542,6 +4501,36 @@ export class FECManager {
|
|
|
4542
4501
|
queueMicrotask(() => this.#syncDecoderBlockOwners.delete(key))
|
|
4543
4502
|
}
|
|
4544
4503
|
|
|
4504
|
+
/**
|
|
4505
|
+
* Run `fn` with `receivedAtMs` (performance.now() clock) as the receipt
|
|
4506
|
+
* time of what it feeds. Within a consumer the bc-provenance profile
|
|
4507
|
+
* governs, a datagram held on a miss and admitted later keeps its receipt
|
|
4508
|
+
* time (fec-01 9): it anchors its block's ts and arms the block's
|
|
4509
|
+
* deadlines from then, never later than they are already armed.
|
|
4510
|
+
*/
|
|
4511
|
+
withReceiptTime(receivedAtMs: number, fn: () => void): void {
|
|
4512
|
+
const outer = this.#receiptAtMs
|
|
4513
|
+
this.#receiptAtMs = receivedAtMs
|
|
4514
|
+
try {
|
|
4515
|
+
fn()
|
|
4516
|
+
} finally {
|
|
4517
|
+
this.#receiptAtMs = outer
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
|
|
4521
|
+
#receiptNow(): number {
|
|
4522
|
+
return this.#receiptAtMs ?? performance.now()
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4525
|
+
/**
|
|
4526
|
+
* A held packet admitted late arms from its (earlier) receipt time; that
|
|
4527
|
+
* never pulls in a deadline later activity already armed. Outside a
|
|
4528
|
+
* receipt scope a re-arm replaces the deadline, as a narrower bound may.
|
|
4529
|
+
*/
|
|
4530
|
+
#notEarlierUnderReceipt(deadlineMs: number, armedMs: number | undefined): number {
|
|
4531
|
+
return this.#receiptAtMs === undefined || armedMs === undefined ? deadlineMs : Math.max(deadlineMs, armedMs)
|
|
4532
|
+
}
|
|
4533
|
+
|
|
4545
4534
|
/** Refresh deadlines after accepted block activity. */
|
|
4546
4535
|
noteBlockActivity(
|
|
4547
4536
|
sbn: number,
|
|
@@ -4778,6 +4767,7 @@ export class FECManager {
|
|
|
4778
4767
|
this.#cleanlyCompletedBlocks.clear()
|
|
4779
4768
|
this.#repairRequestedBlocks.clear()
|
|
4780
4769
|
this.#workerRecoveredBlocks.clear()
|
|
4770
|
+
this.#refusedRecoveries.clear()
|
|
4781
4771
|
this.#syncDecoderBlockOwners.clear()
|
|
4782
4772
|
this.#startupAlignedPackets.clear()
|
|
4783
4773
|
this.#startupPartialBlocks.clear()
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fec-01 6.3.9 receiver check (held-layer anchored RSB_length), in one
|
|
3
|
+
* place. FECManager and MmtpFecSession both run their repair packets through
|
|
4
|
+
* HeldLayerAdmission; neither restates the rule.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { serial32Distance, serial32Precedes } from '@blockcast/mmt-base'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Why a source track's FEC failed closed at runtime (fec-01 6.3.9): a repair
|
|
11
|
+
* packet stated an RSB_length below a held layer's total B_i on that layer's
|
|
12
|
+
* own repair track (`below_own_track_total`), or at or after the anchor, the
|
|
13
|
+
* earliest block in SS_Start serial order whose repair carried >= B_i
|
|
14
|
+
* (`below_anchored_total`). Repair for the track is refused from then on.
|
|
15
|
+
*/
|
|
16
|
+
export interface FecFailClosedReason {
|
|
17
|
+
code: 'below_own_track_total' | 'below_anchored_total'
|
|
18
|
+
layer: number
|
|
19
|
+
total: number
|
|
20
|
+
rsbLength: number
|
|
21
|
+
ssStart: number
|
|
22
|
+
anchor?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One repair layer as the catalog states it: B_i = rsIdOffset + repairSymbols. */
|
|
26
|
+
export interface HeldLayerInput {
|
|
27
|
+
layer: number
|
|
28
|
+
repairTrackName: string
|
|
29
|
+
rsIdOffset: number
|
|
30
|
+
repairSymbols: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface HeldLayerState {
|
|
34
|
+
layer: number
|
|
35
|
+
repairTrackName: string
|
|
36
|
+
total: number
|
|
37
|
+
/** Earliest (serial order) block start whose repair carried >= total. */
|
|
38
|
+
anchor?: number
|
|
39
|
+
/** Latest (serial order) block start whose repair carried < total. */
|
|
40
|
+
latestBelow?: number
|
|
41
|
+
/** RSB_length that latestBelow's repair carried, for the fail-closed reason. */
|
|
42
|
+
latestBelowRsb?: number
|
|
43
|
+
/** Newest block start seen; anchor and latestBelow are retired behind it. */
|
|
44
|
+
newest?: number
|
|
45
|
+
/**
|
|
46
|
+
* The anchor fell behind the horizon, so no block that can still arrive
|
|
47
|
+
* precedes it: every later packet must carry >= total. Its serial is no
|
|
48
|
+
* longer compared, so SS_Start wrapping past it cannot misorder it.
|
|
49
|
+
*/
|
|
50
|
+
settled?: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The largest horizon compared, so every compared serial stays in one half-window. */
|
|
54
|
+
const HORIZON_CAP = 0x40000000
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* How far (in SS_Start symbols) behind the newest block a 6.3.9 anchor or
|
|
58
|
+
* below-total block can still matter: the iso retention in blocks times K,
|
|
59
|
+
* capped at 2^30. Unknown inputs give the cap.
|
|
60
|
+
*/
|
|
61
|
+
export function heldLayerAnchorHorizon(retentionBlocks: number | undefined, k: number): number {
|
|
62
|
+
return retentionBlocks === undefined || k <= 0 ? HORIZON_CAP : Math.min(HORIZON_CAP, retentionBlocks * k)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* fec-01 6.3.9 anchored state for one FEC instance's held layers. For each
|
|
67
|
+
* held layer i with total B_i = O_i + P_i: the anchor is the earliest block,
|
|
68
|
+
* in SS_Start serial order, whose repair carried RSB_length >= B_i. The
|
|
69
|
+
* instance fails closed on (a) a packet on layer i's own repair track (i > 0)
|
|
70
|
+
* below B_i, or (b) any packet of the instance at or after the anchor below
|
|
71
|
+
* B_i. Blocks before the anchor (pre-append) stay valid at the previous total.
|
|
72
|
+
* The result is not latched here: the caller owns what failing closed means.
|
|
73
|
+
*/
|
|
74
|
+
export class HeldLayerAdmission {
|
|
75
|
+
readonly #layerNames: ReadonlySet<string>
|
|
76
|
+
readonly #states: HeldLayerState[]
|
|
77
|
+
|
|
78
|
+
constructor(layers: readonly HeldLayerInput[], heldLayers: readonly number[] = [0]) {
|
|
79
|
+
const held = new Set(heldLayers)
|
|
80
|
+
this.#layerNames = new Set(layers.map((l) => l.repairTrackName))
|
|
81
|
+
this.#states = layers
|
|
82
|
+
.filter((l) => held.has(l.layer))
|
|
83
|
+
.map((l) => ({ layer: l.layer, repairTrackName: l.repairTrackName, total: l.rsIdOffset + l.repairSymbols }))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Judges one repair packet, after the 4.5 discards and the size check and
|
|
88
|
+
* before it places a block. `repairTrackName` is the subscribed repair
|
|
89
|
+
* track, never inferred from RS_ID; without it only (b) runs. A packet on a
|
|
90
|
+
* repair track the catalog does not list is `'discard'` and anchors nothing.
|
|
91
|
+
* `sourceFrontier` is the newest source SS_ID, if any: a repair more than
|
|
92
|
+
* `horizon` past it is judged without moving the anchored state.
|
|
93
|
+
*
|
|
94
|
+
* @returns undefined to admit, `'discard'`, or the fail-closed reason.
|
|
95
|
+
*/
|
|
96
|
+
admit(
|
|
97
|
+
repairTrackName: string | undefined,
|
|
98
|
+
ssStart: number,
|
|
99
|
+
rsbLength: number,
|
|
100
|
+
horizon: number,
|
|
101
|
+
sourceFrontier?: number,
|
|
102
|
+
): FecFailClosedReason | 'discard' | undefined {
|
|
103
|
+
if (repairTrackName !== undefined && !this.#layerNames.has(repairTrackName)) return 'discard'
|
|
104
|
+
// A repair may run ahead of the media, but one more than `horizon` past the
|
|
105
|
+
// source frontier must not move the held-layer state: advancing `newest` on
|
|
106
|
+
// it would retire every real anchor and skip this check until traffic caught
|
|
107
|
+
// up. Such a repair is still judged below the total: it is on its own track,
|
|
108
|
+
// or after any anchor by definition. With no anchor yet it says nothing.
|
|
109
|
+
// It is still admitted: in iso the frontier only moves when a repair places
|
|
110
|
+
// held sources, so dropping it would stall recovery after a long outage.
|
|
111
|
+
// IsoBlockLocator.placeRepair may still refuse the same repair as `ahead`:
|
|
112
|
+
// it measures from `newest`, which held sources move, not from this frontier.
|
|
113
|
+
const farAhead =
|
|
114
|
+
sourceFrontier !== undefined &&
|
|
115
|
+
serial32Precedes(sourceFrontier, ssStart) &&
|
|
116
|
+
serial32Distance(sourceFrontier, ssStart) > horizon
|
|
117
|
+
for (const state of this.#states) {
|
|
118
|
+
let code: FecFailClosedReason['code'] | undefined
|
|
119
|
+
if (farAhead) {
|
|
120
|
+
if (rsbLength < state.total) {
|
|
121
|
+
if (state.layer > 0 && repairTrackName === state.repairTrackName) code = 'below_own_track_total'
|
|
122
|
+
else if (state.anchor !== undefined || state.settled) code = 'below_anchored_total'
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
// Serials more than `horizon` behind the newest block are retired, so
|
|
126
|
+
// every serial still compared sits inside one half-window and SS_Start
|
|
127
|
+
// wrapping on a long-running stream cannot reorder them.
|
|
128
|
+
if (state.newest === undefined || serial32Precedes(state.newest, ssStart)) state.newest = ssStart
|
|
129
|
+
const stale = (s: number) => serial32Distance(s, state.newest!) > horizon
|
|
130
|
+
if (state.latestBelow !== undefined && stale(state.latestBelow)) {
|
|
131
|
+
delete state.latestBelow
|
|
132
|
+
delete state.latestBelowRsb
|
|
133
|
+
}
|
|
134
|
+
if (state.anchor !== undefined && stale(state.anchor)) state.settled = true
|
|
135
|
+
if (rsbLength < state.total) {
|
|
136
|
+
if (state.layer > 0 && repairTrackName === state.repairTrackName) code = 'below_own_track_total'
|
|
137
|
+
else if (stale(ssStart)) continue // past retention: undecodable, anchors nothing
|
|
138
|
+
else if (state.settled || (state.anchor !== undefined && !serial32Precedes(ssStart, state.anchor))) code = 'below_anchored_total'
|
|
139
|
+
else if (state.latestBelow === undefined || serial32Precedes(state.latestBelow, ssStart)) {
|
|
140
|
+
state.latestBelow = ssStart
|
|
141
|
+
state.latestBelowRsb = rsbLength
|
|
142
|
+
}
|
|
143
|
+
} else if (!state.settled && !stale(ssStart)) {
|
|
144
|
+
if (state.anchor === undefined || serial32Precedes(ssStart, state.anchor)) state.anchor = ssStart
|
|
145
|
+
if (state.latestBelow !== undefined && !serial32Precedes(state.latestBelow, state.anchor)) {
|
|
146
|
+
code = 'below_anchored_total'
|
|
147
|
+
ssStart = state.latestBelow
|
|
148
|
+
rsbLength = state.latestBelowRsb!
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (code !== undefined) {
|
|
153
|
+
return {
|
|
154
|
+
code, layer: state.layer, total: state.total, rsbLength, ssStart,
|
|
155
|
+
...(state.anchor === undefined ? {} : { anchor: state.anchor }),
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return undefined
|
|
160
|
+
}
|
|
161
|
+
}
|