@blockcast/mmt-container 0.4.1 → 0.5.0-main.34c7b9c2cfa0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/browser-runtime.js +3769 -364
- 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 +197 -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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/mmtp-container-parser.d.ts +10 -0
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +31 -1
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +2713 -1024
- package/dist/mmtp-container-parser.no-modules.js.map +4 -4
- package/dist/mmtp-decode-pipeline.js +31 -1
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- 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 +96 -29
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +522 -205
- 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 +78 -11
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +3663 -333
- 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 +2699 -1008
- package/dist/no-modules.js.map +4 -4
- package/dist/repair-fetch.d.ts +50 -10
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +62 -20
- package/dist/repair-fetch.js.map +2 -2
- package/dist/wasm-mmtp-container-parser.js +31 -1
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +7 -15
- package/src/cmcd-producer.ts +6 -1
- package/src/fec-manager.ts +147 -131
- package/src/held-layer-admission.ts +161 -0
- package/src/index.ts +6 -9
- package/src/mmtp-container-parser.ts +48 -0
- package/src/mmtp-fec-session.ts +76 -3
- package/src/mmtp-router.ts +637 -274
- package/src/multicast-auth-catalog.ts +81 -8
- package/src/multicast-manager.ts +274 -49
- package/src/multicast-transport.ts +6 -2
- package/src/no-modules.ts +1 -6
- package/src/repair-fetch.ts +154 -34
- package/test-vectors/catalog/negative/multicast-auth-padded-key.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-reused-track-format.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-root-dotted-key.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-trailing-pad-bits.json +0 -1
- package/test-vectors/repair-policy-v1.json +113 -0
- 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.34c7b9c2cfa0",
|
|
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"
|
|
@@ -149,7 +141,8 @@
|
|
|
149
141
|
"./catalog.schema.json": "./schemas/catalog.schema.json",
|
|
150
142
|
"./test-vectors/catalog-validation.json": "./test-vectors/catalog-validation.json",
|
|
151
143
|
"./test-vectors/align.json": "./test-vectors/align.json",
|
|
152
|
-
"./test-vectors/catalog-timing.json": "./test-vectors/catalog-timing.json"
|
|
144
|
+
"./test-vectors/catalog-timing.json": "./test-vectors/catalog-timing.json",
|
|
145
|
+
"./test-vectors/repair-policy-v1.json": "./test-vectors/repair-policy-v1.json"
|
|
153
146
|
},
|
|
154
147
|
"files": [
|
|
155
148
|
"dist",
|
|
@@ -166,17 +159,16 @@
|
|
|
166
159
|
"build": "tsc --emitDeclarationOnly && node build.mjs",
|
|
167
160
|
"dev": "tsc --watch",
|
|
168
161
|
"clean": "rm -rf dist",
|
|
169
|
-
"bench:source-auth": "node bench/source-auth-wasm.mjs",
|
|
170
162
|
"bench:amt-hot-path": "node --expose-gc --max-semi-space-size=64 bench/amt-hot-path.mjs",
|
|
171
163
|
"test:catalog-validator-public-api": "node test-vectors/catalog-validator-public-api.mjs",
|
|
172
164
|
"test": "vitest run",
|
|
173
165
|
"test:watch": "vitest"
|
|
174
166
|
},
|
|
175
167
|
"dependencies": {
|
|
176
|
-
"@blockcast/fec-worker": "
|
|
177
|
-
"@blockcast/mmt-base": "
|
|
178
|
-
"@blockcast/mmt-fec": "
|
|
179
|
-
"@blockcast/mmt-manifest-verify": "
|
|
168
|
+
"@blockcast/fec-worker": "0.5.0-main.34c7b9c2cfa0",
|
|
169
|
+
"@blockcast/mmt-base": "0.5.0-main.34c7b9c2cfa0",
|
|
170
|
+
"@blockcast/mmt-fec": "0.5.0-main.34c7b9c2cfa0",
|
|
171
|
+
"@blockcast/mmt-manifest-verify": "0.5.0-main.34c7b9c2cfa0",
|
|
180
172
|
"@noble/curves": "^1.9.7",
|
|
181
173
|
"@noble/hashes": "^1.8.0",
|
|
182
174
|
"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,14 @@ 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>()
|
|
606
|
+
// Worker-recovered blocks: the next ESI the decode's ordered emit owes. The
|
|
607
|
+
// Worker emits every ESI 0..SSB_length-1 in order and drops only a symbol it
|
|
608
|
+
// refuses (a malformed ssbg_mode1 symbol), so a skipped ESI is a dropped
|
|
609
|
+
// recovered packet and the decode recovered nothing.
|
|
610
|
+
#workerRecoveredNextEsi = new Map<string, number>()
|
|
633
611
|
|
|
634
612
|
// How the multicast leg delivers symbols (see MulticastSymbolDelivery). The
|
|
635
613
|
// provider declares it per transport join; absence keeps datagram semantics.
|
|
@@ -833,7 +811,14 @@ export class FECManager {
|
|
|
833
811
|
| null = null
|
|
834
812
|
|
|
835
813
|
// Worker-path callbacks — async recovery and stats from FecWorkerClient.
|
|
836
|
-
|
|
814
|
+
/**
|
|
815
|
+
* A recovered source symbol. Return false when admission refused the
|
|
816
|
+
* packet it carries (bc-provenance v2, draft-ramadan-moq-multicast-01
|
|
817
|
+
* 7.2.7): the block is then unrecovered and stays open to its FEC deadline.
|
|
818
|
+
*/
|
|
819
|
+
onBlockRecovered:
|
|
820
|
+
| ((sbn: number, packetId: number, data: ArrayBuffer, meta: FrameMeta, catalogTrackName?: string) => boolean | void)
|
|
821
|
+
| null = null
|
|
837
822
|
onFecStatsUpdate: ((packetId: number, stats: FecTrackStats) => void) | null = null
|
|
838
823
|
|
|
839
824
|
#encodeTrackName(catalogTrackName: string): string {
|
|
@@ -978,6 +963,8 @@ export class FECManager {
|
|
|
978
963
|
this.#completedBlocks.delete(record.key)
|
|
979
964
|
this.#cleanlyCompletedBlocks.delete(record.key)
|
|
980
965
|
this.#workerRecoveredBlocks.delete(record.key)
|
|
966
|
+
this.#refusedRecoveries.delete(record.key)
|
|
967
|
+
this.#workerRecoveredNextEsi.delete(record.key)
|
|
981
968
|
// The policy's terminal ledgers describe the same terminal state and
|
|
982
969
|
// retire with it: a failed block must never outlive its failure record
|
|
983
970
|
// as 'complete' (that would hide a loss from the release gate).
|
|
@@ -1444,7 +1431,7 @@ export class FECManager {
|
|
|
1444
1431
|
catalogTrackName: string | undefined,
|
|
1445
1432
|
delayMs: number,
|
|
1446
1433
|
): void {
|
|
1447
|
-
const deadlineMs =
|
|
1434
|
+
const deadlineMs = this.#notEarlierUnderReceipt(this.#receiptNow() + delayMs, this.#repairDeadlineAtMs.get(key))
|
|
1448
1435
|
this.#repairDeadlineAtMs.set(key, deadlineMs)
|
|
1449
1436
|
const armed = this.#repairDeadlines.get(key)
|
|
1450
1437
|
if (armed?.handle != null) {
|
|
@@ -1464,7 +1451,7 @@ export class FECManager {
|
|
|
1464
1451
|
timer.handle = null
|
|
1465
1452
|
this.#requestRepair(sbn, packetId, catalogTrackName)
|
|
1466
1453
|
}
|
|
1467
|
-
timer.handle = setTimeout(onFire,
|
|
1454
|
+
timer.handle = setTimeout(onFire, Math.max(0, deadlineMs - performance.now()))
|
|
1468
1455
|
this.#repairDeadlines.set(key, timer)
|
|
1469
1456
|
}
|
|
1470
1457
|
|
|
@@ -1668,6 +1655,12 @@ export class FECManager {
|
|
|
1668
1655
|
ssId = sbn,
|
|
1669
1656
|
): void {
|
|
1670
1657
|
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
1658
|
+
// Arrival at FEC, not receipt (withReceiptTime): this bounds how far SS_ID
|
|
1659
|
+
// can plausibly move between symbols this instance has seen, and a held
|
|
1660
|
+
// datagram admitted late reached it no earlier than now. A block's ts is
|
|
1661
|
+
// its anchor (#blockAnchorsMs), which does take the receipt time.
|
|
1662
|
+
// Receipt times are also not monotonic: a datagram held from t0 and
|
|
1663
|
+
// admitted after one received at t1 > t0 would move the frontier back.
|
|
1671
1664
|
const now = performance.now()
|
|
1672
1665
|
const previous = (symbol === 'source' ? this.#sourceFrontiers : this.#highestSbn).get(streamKey)
|
|
1673
1666
|
const anchor = this.#sourceAnchors.get(streamKey)
|
|
@@ -2017,7 +2010,10 @@ export class FECManager {
|
|
|
2017
2010
|
delayMs: number,
|
|
2018
2011
|
): void {
|
|
2019
2012
|
const key = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
2020
|
-
const waitAtMs =
|
|
2013
|
+
const waitAtMs = this.#notEarlierUnderReceipt(
|
|
2014
|
+
this.#receiptNow() + Math.max(1, Math.ceil(delayMs)),
|
|
2015
|
+
this.#blockWaitDeadlinesMs.get(key),
|
|
2016
|
+
)
|
|
2021
2017
|
// A held block's symbols (its own FETCH's repair symbols included) never
|
|
2022
2018
|
// carry its wait past the hold: the hold is its presentation deadline.
|
|
2023
2019
|
const holdAtMs = this.#repairHoldUntilMs.get(key)
|
|
@@ -2534,6 +2530,8 @@ export class FECManager {
|
|
|
2534
2530
|
resetSet(this.#cleanlyCompletedBlocks)
|
|
2535
2531
|
resetSet(this.#repairRequestedBlocks)
|
|
2536
2532
|
resetSet(this.#workerRecoveredBlocks)
|
|
2533
|
+
resetSet(this.#refusedRecoveries)
|
|
2534
|
+
resetMap(this.#workerRecoveredNextEsi)
|
|
2537
2535
|
resetSet(this.#startupAlignedPackets)
|
|
2538
2536
|
resetSet(this.#startupPartialBlocks)
|
|
2539
2537
|
resetSet(this.#startupRepairReadyPackets)
|
|
@@ -2668,6 +2666,8 @@ export class FECManager {
|
|
|
2668
2666
|
dropSet(this.#cleanlyCompletedBlocks)
|
|
2669
2667
|
dropSet(this.#repairRequestedBlocks)
|
|
2670
2668
|
dropSet(this.#workerRecoveredBlocks)
|
|
2669
|
+
dropSet(this.#refusedRecoveries)
|
|
2670
|
+
dropMap(this.#workerRecoveredNextEsi)
|
|
2671
2671
|
dropSet(this.#startupAlignedPackets)
|
|
2672
2672
|
dropSet(this.#startupPartialBlocks)
|
|
2673
2673
|
dropSet(this.#startupRepairReadyPackets)
|
|
@@ -2832,7 +2832,10 @@ export class FECManager {
|
|
|
2832
2832
|
// The block's absolute anchor (fecBlockDeadlineMs): the receiver-local
|
|
2833
2833
|
// time of its first accepted symbol, from which its presentation deadline
|
|
2834
2834
|
// is measured.
|
|
2835
|
-
|
|
2835
|
+
// A datagram held and admitted later keeps its receipt time (fec-01 9).
|
|
2836
|
+
const receivedAtMs = this.#receiptNow()
|
|
2837
|
+
const anchorMs = this.#blockAnchorsMs.get(blockKey)
|
|
2838
|
+
if (anchorMs === undefined || receivedAtMs < anchorMs) this.#blockAnchorsMs.set(blockKey, receivedAtMs)
|
|
2836
2839
|
if (source === 'multicast') counts.multicastSymbols++
|
|
2837
2840
|
else counts.moqSymbols++
|
|
2838
2841
|
this.#blockPathCounts.set(blockKey, counts)
|
|
@@ -3023,6 +3026,18 @@ export class FECManager {
|
|
|
3023
3026
|
blockAnchorAtMs(sbn: number, packetId: number, catalogTrackName?: string): number | undefined {
|
|
3024
3027
|
return this.#blockAnchorsMs.get(this.#blockKey(sbn, packetId, catalogTrackName))
|
|
3025
3028
|
}
|
|
3029
|
+
/**
|
|
3030
|
+
* How long ago (ms) the oldest open block of the stream, one with an FEC
|
|
3031
|
+
* deadline, took its first accepted symbol; undefined with none open.
|
|
3032
|
+
*/
|
|
3033
|
+
oldestOpenBlockAgeMs(packetId: number, catalogTrackName: string): number | undefined {
|
|
3034
|
+
let oldest = Infinity
|
|
3035
|
+
for (const key of this.#activeBlocks.keys()) {
|
|
3036
|
+
if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue
|
|
3037
|
+
oldest = Math.min(oldest, this.#blockAnchorsMs.get(key) ?? Infinity)
|
|
3038
|
+
}
|
|
3039
|
+
return oldest === Infinity ? undefined : performance.now() - oldest
|
|
3040
|
+
}
|
|
3026
3041
|
/** Absolute fail deadline of a still-open block, or undefined. */
|
|
3027
3042
|
blockDeadlineAtMs(sbn: number, packetId: number, catalogTrackName?: string): number | undefined {
|
|
3028
3043
|
return this.#activeBlockDeadlinesMs.get(this.#blockKey(sbn, packetId, catalogTrackName))
|
|
@@ -3686,23 +3701,22 @@ export class FECManager {
|
|
|
3686
3701
|
if (!this.#claimDecoderBlock(client, meta.sbn, recoveredCatalogTrackName)) return
|
|
3687
3702
|
const blockKey = this.#blockKey(meta.sbn, packetId, recoveredCatalogTrackName)
|
|
3688
3703
|
const cleanlyCompleted = this.#cleanlyCompletedBlocks.has(blockKey)
|
|
3704
|
+
if (esi !== (this.#workerRecoveredNextEsi.get(blockKey) ?? 0)) {
|
|
3705
|
+
this.#refusedRecoveries.add(blockKey)
|
|
3706
|
+
if (this.#refusedRecoveries.size > 1000) dropOldest(this.#refusedRecoveries, 500)
|
|
3707
|
+
}
|
|
3708
|
+
if (esi >= meta.ssbLength! - 1) {
|
|
3709
|
+
// Safe to forget the block here: the Worker never re-emits a
|
|
3710
|
+
// terminal block (packages/fec-worker/src/fec-worker.ts guards
|
|
3711
|
+
// source and repair feeds with isTerminalBlock).
|
|
3712
|
+
this.#workerRecoveredNextEsi.delete(blockKey)
|
|
3713
|
+
} else {
|
|
3714
|
+
this.#workerRecoveredNextEsi.set(blockKey, esi + 1)
|
|
3715
|
+
// Eviction fails closed: an evicted mid-block entry reads 0 and
|
|
3716
|
+
// refuses the rest of that decode rather than admitting a gap.
|
|
3717
|
+
if (this.#workerRecoveredNextEsi.size > 1000) dropOldest(this.#workerRecoveredNextEsi, 500)
|
|
3718
|
+
}
|
|
3689
3719
|
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
3720
|
// RaptorQ returns the full K-symbol block, not only erased symbols.
|
|
3707
3721
|
// The direct path already parsed every source ESI in #seenSymbols;
|
|
3708
3722
|
// routing those symbols again injects duplicate MFU fragments and can
|
|
@@ -3710,23 +3724,46 @@ export class FECManager {
|
|
|
3710
3724
|
const symbolKey = `${blockKey}:${esi}`
|
|
3711
3725
|
if (!this.#seenSymbols.has(symbolKey)) {
|
|
3712
3726
|
this.#seenSymbols.add(symbolKey)
|
|
3713
|
-
this.onBlockRecovered?.(
|
|
3727
|
+
const admitted = this.onBlockRecovered?.(
|
|
3714
3728
|
meta.sbn,
|
|
3715
3729
|
packetId,
|
|
3716
3730
|
data,
|
|
3717
3731
|
meta,
|
|
3718
3732
|
recoveredCatalogTrackName,
|
|
3719
3733
|
)
|
|
3734
|
+
if (admitted === false) {
|
|
3735
|
+
this.#refusedRecoveries.add(blockKey)
|
|
3736
|
+
if (this.#refusedRecoveries.size > 1000) dropOldest(this.#refusedRecoveries, 500)
|
|
3737
|
+
}
|
|
3720
3738
|
}
|
|
3721
3739
|
}
|
|
3722
3740
|
|
|
3723
3741
|
// The worker emits recovered source symbols in ESI order. Complete the
|
|
3724
3742
|
// block only after its final authored slot so assemblers receive every
|
|
3725
3743
|
// missing symbol before their completion callback flushes buffered media.
|
|
3744
|
+
// A decode one of whose recovered packets admission refused recovered
|
|
3745
|
+
// nothing (7.2.7): the block stays open and fails at its FEC deadline.
|
|
3746
|
+
const refused = esi >= meta.ssbLength! - 1 && this.#refusedRecoveries.delete(blockKey)
|
|
3726
3747
|
if (
|
|
3727
3748
|
esi >= meta.ssbLength! - 1 &&
|
|
3749
|
+
!refused &&
|
|
3728
3750
|
!this.#completedBlocks.has(blockKey)
|
|
3729
3751
|
) {
|
|
3752
|
+
if (!cleanlyCompleted && !this.#workerRecoveredBlocks.has(blockKey)) {
|
|
3753
|
+
this.#traceLifecycle(
|
|
3754
|
+
'recovered',
|
|
3755
|
+
meta.sbn,
|
|
3756
|
+
packetId,
|
|
3757
|
+
recoveredCatalogTrackName,
|
|
3758
|
+
)
|
|
3759
|
+
this.#workerRecoveredBlocks.add(blockKey)
|
|
3760
|
+
this.#fecStats.blocksRecovered++
|
|
3761
|
+
this.#statsForPacket(packetId).blocksRecovered++
|
|
3762
|
+
if (this.#workerRecoveredBlocks.size > 1000) {
|
|
3763
|
+
dropOldest(this.#workerRecoveredBlocks, 500)
|
|
3764
|
+
}
|
|
3765
|
+
this.#noteBlockRepaired(blockKey, meta.sbn, packetId, recoveredCatalogTrackName)
|
|
3766
|
+
}
|
|
3730
3767
|
this.cancelBlockTimeout(meta.sbn, packetId, recoveredCatalogTrackName)
|
|
3731
3768
|
if (recoveredCatalogTrackName === undefined) {
|
|
3732
3769
|
this.onBlockComplete?.(meta.sbn, packetId)
|
|
@@ -4106,77 +4143,26 @@ export class FECManager {
|
|
|
4106
4143
|
const config = this.#trackConfigs.get(packetId)
|
|
4107
4144
|
const layers = config?.repairLayers
|
|
4108
4145
|
if (layers === undefined || layers.length === 0) return true
|
|
4109
|
-
|
|
4110
|
-
|
|
4146
|
+
let admission = this.#heldLayerStates.get(streamKey)
|
|
4147
|
+
if (admission === undefined) {
|
|
4148
|
+
admission = new HeldLayerAdmission(layers, config!.heldLayers)
|
|
4149
|
+
this.#heldLayerStates.set(streamKey, admission)
|
|
4111
4150
|
}
|
|
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
|
-
}
|
|
4151
|
+
const verdict = admission.admit(
|
|
4152
|
+
repairTrackName,
|
|
4153
|
+
ssStart,
|
|
4154
|
+
rsbLength,
|
|
4155
|
+
this.#layerAnchorHorizon(packetId),
|
|
4156
|
+
this.#sourceFrontiers.get(streamKey)?.sbn,
|
|
4157
|
+
)
|
|
4158
|
+
if (verdict === 'discard') return false
|
|
4159
|
+
if (verdict !== undefined) {
|
|
4160
|
+
this.#fecFailClosed.set(streamKey, verdict)
|
|
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
|
+
)
|
|
4165
|
+
return false
|
|
4180
4166
|
}
|
|
4181
4167
|
return true
|
|
4182
4168
|
}
|
|
@@ -4187,11 +4173,9 @@ export class FECManager {
|
|
|
4187
4173
|
* times K, capped at 2^30 so compared serials stay inside one half-window.
|
|
4188
4174
|
*/
|
|
4189
4175
|
#layerAnchorHorizon(packetId: number): number {
|
|
4190
|
-
const cap = 0x40000000
|
|
4191
4176
|
let blocks: number | undefined
|
|
4192
4177
|
try { blocks = this.isoRetentionBlocksForPacket(packetId) } catch { blocks = undefined }
|
|
4193
|
-
|
|
4194
|
-
return blocks === undefined || k <= 0 ? cap : Math.min(cap, blocks * k)
|
|
4178
|
+
return heldLayerAnchorHorizon(blocks, this.#trackConfigs.get(packetId)?.k ?? 0)
|
|
4195
4179
|
}
|
|
4196
4180
|
|
|
4197
4181
|
/**
|
|
@@ -4542,6 +4526,36 @@ export class FECManager {
|
|
|
4542
4526
|
queueMicrotask(() => this.#syncDecoderBlockOwners.delete(key))
|
|
4543
4527
|
}
|
|
4544
4528
|
|
|
4529
|
+
/**
|
|
4530
|
+
* Run `fn` with `receivedAtMs` (performance.now() clock) as the receipt
|
|
4531
|
+
* time of what it feeds. Within a consumer the bc-provenance profile
|
|
4532
|
+
* governs, a datagram held on a miss and admitted later keeps its receipt
|
|
4533
|
+
* time (fec-01 9): it anchors its block's ts and arms the block's
|
|
4534
|
+
* deadlines from then, never later than they are already armed.
|
|
4535
|
+
*/
|
|
4536
|
+
withReceiptTime(receivedAtMs: number, fn: () => void): void {
|
|
4537
|
+
const outer = this.#receiptAtMs
|
|
4538
|
+
this.#receiptAtMs = receivedAtMs
|
|
4539
|
+
try {
|
|
4540
|
+
fn()
|
|
4541
|
+
} finally {
|
|
4542
|
+
this.#receiptAtMs = outer
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
|
|
4546
|
+
#receiptNow(): number {
|
|
4547
|
+
return this.#receiptAtMs ?? performance.now()
|
|
4548
|
+
}
|
|
4549
|
+
|
|
4550
|
+
/**
|
|
4551
|
+
* A held packet admitted late arms from its (earlier) receipt time; that
|
|
4552
|
+
* never pulls in a deadline later activity already armed. Outside a
|
|
4553
|
+
* receipt scope a re-arm replaces the deadline, as a narrower bound may.
|
|
4554
|
+
*/
|
|
4555
|
+
#notEarlierUnderReceipt(deadlineMs: number, armedMs: number | undefined): number {
|
|
4556
|
+
return this.#receiptAtMs === undefined || armedMs === undefined ? deadlineMs : Math.max(deadlineMs, armedMs)
|
|
4557
|
+
}
|
|
4558
|
+
|
|
4545
4559
|
/** Refresh deadlines after accepted block activity. */
|
|
4546
4560
|
noteBlockActivity(
|
|
4547
4561
|
sbn: number,
|
|
@@ -4778,6 +4792,8 @@ export class FECManager {
|
|
|
4778
4792
|
this.#cleanlyCompletedBlocks.clear()
|
|
4779
4793
|
this.#repairRequestedBlocks.clear()
|
|
4780
4794
|
this.#workerRecoveredBlocks.clear()
|
|
4795
|
+
this.#refusedRecoveries.clear()
|
|
4796
|
+
this.#workerRecoveredNextEsi.clear()
|
|
4781
4797
|
this.#syncDecoderBlockOwners.clear()
|
|
4782
4798
|
this.#startupAlignedPackets.clear()
|
|
4783
4799
|
this.#startupPartialBlocks.clear()
|