@blockcast/mmt-container 2.1.0 → 2.2.0-main.050b52fb9b00
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 +29 -17
- package/dist/browser-runtime.js +1949 -374
- package/dist/browser-runtime.js.map +4 -4
- package/dist/carp-catalog.js.map +1 -1
- package/dist/catalog-fec-params.d.ts +17 -1
- package/dist/catalog-fec-params.d.ts.map +1 -1
- package/dist/catalog-fec-params.js +15 -0
- package/dist/catalog-fec-params.js.map +2 -2
- package/dist/catalog-network-source.js.map +1 -1
- package/dist/cmcd-producer.d.ts +65 -47
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js +138 -109
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +26 -16
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +70 -12
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +403 -58
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-state.js.map +1 -1
- package/dist/fec-worker-setup.d.ts +1 -1
- package/dist/fec-worker-setup.js +1 -1
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/held-layer-admission.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.d.ts +48 -13
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +97 -25
- package/dist/iso-block-locator.js.map +2 -2
- package/dist/iso-bmff-timing.d.ts +10 -1
- package/dist/iso-bmff-timing.d.ts.map +1 -1
- package/dist/iso-bmff-timing.js +36 -2
- package/dist/iso-bmff-timing.js.map +2 -2
- package/dist/media-router.js.map +1 -1
- package/dist/mfu-reassembler.js +12 -19
- package/dist/mfu-reassembler.js.map +2 -2
- package/dist/mmtp-container-parser.d.ts +79 -10
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +219 -45
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +342 -63
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +218 -45
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-fec-session.d.ts +41 -3
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +118 -13
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +43 -9
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +2006 -95
- package/dist/mmtp-router.js.map +4 -4
- package/dist/multicast-auth-catalog.js.map +1 -1
- package/dist/multicast-input.js.map +1 -1
- package/dist/multicast-manager.d.ts +28 -5
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +16116 -15005
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +11 -3
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +281 -35
- package/dist/multicast-transport.js.map +4 -4
- package/dist/multicast-unwrap.js +12 -19
- package/dist/multicast-unwrap.js.map +2 -2
- package/dist/no-modules.js +336 -59
- package/dist/no-modules.js.map +3 -3
- package/dist/repair-fetch.d.ts +36 -8
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +285 -26
- package/dist/repair-fetch.js.map +3 -3
- package/dist/repair-layer-subscriptions.d.ts +82 -0
- package/dist/repair-layer-subscriptions.d.ts.map +1 -0
- package/dist/repair-layer-subscriptions.js +192 -0
- package/dist/repair-layer-subscriptions.js.map +7 -0
- package/dist/signaling.js.map +2 -2
- package/dist/timestamp-codec.js +12 -19
- package/dist/timestamp-codec.js.map +2 -2
- package/dist/track-session.d.ts +1 -1
- package/dist/track-session.d.ts.map +1 -1
- package/dist/track-session.js +1 -0
- package/dist/track-session.js.map +2 -2
- package/dist/wasm-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +218 -45
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +8 -5
- package/schemas/catalog.schema.json +4 -0
- package/src/catalog-fec-params.ts +36 -1
- package/src/cmcd-producer.ts +194 -179
- package/src/fec-manager.ts +433 -32
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +20 -2
- package/src/iso-block-locator.ts +133 -31
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +306 -29
- package/src/mmtp-fec-session.ts +115 -15
- package/src/mmtp-router.ts +629 -54
- package/src/multicast-manager.ts +70 -11
- package/src/multicast-transport.ts +44 -20
- package/src/repair-fetch.ts +363 -49
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +4 -1
package/src/fec-manager.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
isoBlockContains,
|
|
23
23
|
isoBlocksBehindLowerBound,
|
|
24
24
|
parseFecBlockProfile,
|
|
25
|
+
serial32Add,
|
|
25
26
|
serial32Distance,
|
|
26
27
|
serial32Precedes,
|
|
27
28
|
ssbg1FitsSymbol,
|
|
@@ -169,6 +170,17 @@ export interface FecPacketStats {
|
|
|
169
170
|
|
|
170
171
|
export type FecSymbolSource = 'multicast' | 'moq'
|
|
171
172
|
|
|
173
|
+
/**
|
|
174
|
+
* What feedRebuiltSourceSymbol did with a rebuilt source: whether its block
|
|
175
|
+
* took it, and the block the synchronous decoder recovered with it, if any.
|
|
176
|
+
*/
|
|
177
|
+
export interface RebuiltSourceFeed {
|
|
178
|
+
readonly taken: boolean
|
|
179
|
+
readonly recovered: Uint8Array | null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const NOT_TAKEN: RebuiltSourceFeed = Object.freeze({ taken: false, recovered: null })
|
|
183
|
+
|
|
172
184
|
/**
|
|
173
185
|
* One iso-ssbg1-v1 first-arrival source symbol MmtpRouter holds until a repair
|
|
174
186
|
* symbol states its block: the exact protected packet (no SS_ID, unpadded),
|
|
@@ -201,6 +213,25 @@ export interface UndelimitedIsoLoss {
|
|
|
201
213
|
readonly previousBlockStart?: number
|
|
202
214
|
}
|
|
203
215
|
|
|
216
|
+
/**
|
|
217
|
+
* A pending hole the stream ran more than the locator's horizon past before
|
|
218
|
+
* its range repair ended (BLO-52085): no repair can state its blocks any
|
|
219
|
+
* more, and the release gate no longer waits for it.
|
|
220
|
+
*/
|
|
221
|
+
export type ExpiredIsoHole = Pick<UndelimitedIsoLoss, 'packetId' | 'catalogTrackName' | 'holeStart' | 'holeLength'>
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* What a failed iso-ssbg1-v1 repair-less span was: the `length` SS_IDs from
|
|
225
|
+
* its start, between two stated blocks, of which `sources` arrived. No repair
|
|
226
|
+
* stated the blocks inside it, so the receiver cannot tell its complete
|
|
227
|
+
* blocks from the ones that lost a source: the span failed, not the block
|
|
228
|
+
* that starts it.
|
|
229
|
+
*/
|
|
230
|
+
export interface IsoSpanFailure {
|
|
231
|
+
readonly length: number
|
|
232
|
+
readonly sources: number
|
|
233
|
+
}
|
|
234
|
+
|
|
204
235
|
/**
|
|
205
236
|
* How the multicast leg hands its symbols to this manager.
|
|
206
237
|
*
|
|
@@ -510,10 +541,21 @@ export class FECManager {
|
|
|
510
541
|
// 'pending' forever, which would park every later frame of its stream in
|
|
511
542
|
// the in-order release gate. Each entry owns its handle; the callback
|
|
512
543
|
// checks it still owns the key, so a settled or reset key is never touched.
|
|
513
|
-
|
|
544
|
+
/**
|
|
545
|
+
* Per owed Worker cleanup: the bound on its acknowledgement, when its block
|
|
546
|
+
* was due, and when its cleanup was asked for (see `onCleanupComplete`).
|
|
547
|
+
*/
|
|
548
|
+
#pendingWorkerCleanupBounds = new Map<
|
|
549
|
+
string,
|
|
550
|
+
{ handle: ReturnType<typeof setTimeout>; dueAtMs: number; askedAtMs: number }
|
|
551
|
+
>()
|
|
514
552
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
515
553
|
#blockSourceCounts = new Map<string, number>()
|
|
516
554
|
#blockSourceEsis = new Map<string, Set<number>>()
|
|
555
|
+
// Source ESIs per block that another FEC instance rebuilt and fed in
|
|
556
|
+
// (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
|
|
557
|
+
// not network arrivals and never make a block clean.
|
|
558
|
+
#blockRebuiltEsis = new Map<string, Set<number>>()
|
|
517
559
|
// Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
|
|
518
560
|
// first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
|
|
519
561
|
// media units, usually before it holds K symbols; under iso-ssbg1-v1
|
|
@@ -522,6 +564,9 @@ export class FECManager {
|
|
|
522
564
|
// SSB_length source arrivals and recoverable from SSB_length received
|
|
523
565
|
// symbols; waiting for K fails every one of them.
|
|
524
566
|
#blockAuthoredLengths = new Map<string, number>()
|
|
567
|
+
// Keys whose authored length is a repair-less span's (#openIsoSpan), until
|
|
568
|
+
// a repair states the block there or the span resolves.
|
|
569
|
+
#isoSpanKeys = new Set<string>()
|
|
525
570
|
#ssbLengthConflictCount = 0
|
|
526
571
|
#blockRepairRsIds = new Map<string, Set<number>>()
|
|
527
572
|
// RSB_length per block (C.5.3 Repair FEC Payload ID): how many repair
|
|
@@ -644,6 +689,10 @@ export class FECManager {
|
|
|
644
689
|
// ── Keyframe-protected repair policy (block-policy.ts) ──
|
|
645
690
|
// One classifier per physical stream, built from that stream's geometry.
|
|
646
691
|
#classifiers = new Map<string, BlockClassifier>()
|
|
692
|
+
// A stream without catalog timing has no classifier, so the last RAP it
|
|
693
|
+
// released stands in for one: "an MPU boundary bounds its RAPs" holds until
|
|
694
|
+
// a second RAP is released inside one MPU (BLO-52085).
|
|
695
|
+
#noSpanRaps = new Map<string, { mpu: number; presentationTime: bigint; inside: boolean }>()
|
|
647
696
|
// Geometry a stream-scoped configuration gave one stream (streamKey). ABR
|
|
648
697
|
// rungs can share a packetId with a different framerate or GOP, so the
|
|
649
698
|
// packet-level geometry is only the geometry of a packet whose streams
|
|
@@ -725,13 +774,17 @@ export class FECManager {
|
|
|
725
774
|
onBlockComplete:
|
|
726
775
|
| ((sbn: number, packetId: number, catalogTrackName?: string) => void)
|
|
727
776
|
| null = null
|
|
728
|
-
/**
|
|
777
|
+
/**
|
|
778
|
+
* A block reached its fail bound. `blockClass` is its class at failure.
|
|
779
|
+
* `span` is set when what failed is a repair-less span starting at `sbn`.
|
|
780
|
+
*/
|
|
729
781
|
onBlockFailed:
|
|
730
782
|
| ((
|
|
731
783
|
sbn: number,
|
|
732
784
|
packetId: number,
|
|
733
785
|
catalogTrackName: string | undefined,
|
|
734
786
|
blockClass: BlockClass,
|
|
787
|
+
span?: IsoSpanFailure,
|
|
735
788
|
) => void)
|
|
736
789
|
| null = null
|
|
737
790
|
/**
|
|
@@ -815,6 +868,8 @@ export class FECManager {
|
|
|
815
868
|
* for it, which it must be.
|
|
816
869
|
*/
|
|
817
870
|
#onUndelimitedLoss: ((loss: UndelimitedIsoLoss) => void) | null = null
|
|
871
|
+
/** Set with the loss handler: true takes an expired hole to close (`closeIsoHoles`). */
|
|
872
|
+
#onIsoHoleExpired: ((hole: ExpiredIsoHole) => boolean) | null = null
|
|
818
873
|
/** A block every symbol of which vanished was created by SBN evidence. */
|
|
819
874
|
onBlockVanished:
|
|
820
875
|
| ((sbn: number, packetId: number, catalogTrackName: string | undefined) => void)
|
|
@@ -1469,7 +1524,12 @@ export class FECManager {
|
|
|
1469
1524
|
timer.handle = null
|
|
1470
1525
|
this.#requestRepair(sbn, packetId, catalogTrackName)
|
|
1471
1526
|
}
|
|
1472
|
-
timer.handle = setTimeout(
|
|
1527
|
+
timer.handle = setTimeout(
|
|
1528
|
+
onFire,
|
|
1529
|
+
this.#receiptAtMs === undefined
|
|
1530
|
+
? Math.max(0, deadlineMs - performance.now())
|
|
1531
|
+
: this.#receiptTimerDelayMs(deadlineMs, 0),
|
|
1532
|
+
)
|
|
1473
1533
|
this.#repairDeadlines.set(key, timer)
|
|
1474
1534
|
}
|
|
1475
1535
|
|
|
@@ -1663,6 +1723,12 @@ export class FECManager {
|
|
|
1663
1723
|
* Vanished blocks are not enumerated here: their starts are unknown until a
|
|
1664
1724
|
* repair states them. The SS_IDs between two stated blocks become one
|
|
1665
1725
|
* repair-less span when the later block's repair arrives (placeIsoRepair).
|
|
1726
|
+
*
|
|
1727
|
+
* A `held` source arrived before its block was known and is fed only now
|
|
1728
|
+
* that a repair, a span or a closed hole placed it: how far its block start
|
|
1729
|
+
* then lies behind the frontier measures how long the receiver held it, not
|
|
1730
|
+
* where the stream was, so only its SS_ID and packet_sequence_number judge
|
|
1731
|
+
* it (BLO-52085).
|
|
1666
1732
|
*/
|
|
1667
1733
|
#noteSbnSeen(
|
|
1668
1734
|
sbn: number,
|
|
@@ -1671,6 +1737,7 @@ export class FECManager {
|
|
|
1671
1737
|
symbol: 'source' | 'repair',
|
|
1672
1738
|
packetSequenceNumber?: number,
|
|
1673
1739
|
ssId = sbn,
|
|
1740
|
+
held = false,
|
|
1674
1741
|
): void {
|
|
1675
1742
|
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
1676
1743
|
// Arrival at FEC, not receipt (withReceiptTime): this bounds how far SS_ID
|
|
@@ -1706,6 +1773,7 @@ export class FECManager {
|
|
|
1706
1773
|
// The classifier compares authored order with SBN order only within
|
|
1707
1774
|
// one continuous SBN sequence.
|
|
1708
1775
|
this.#classifiers.get(streamKey)?.noteSourceDiscontinuity()
|
|
1776
|
+
this.#noSpanRaps.delete(streamKey)
|
|
1709
1777
|
this.#highestSbn.set(streamKey, { sbn, atMs: now })
|
|
1710
1778
|
this.#sourceFrontiers.set(streamKey, { sbn, atMs: now })
|
|
1711
1779
|
if (psn === undefined) this.#sourceAnchors.delete(streamKey)
|
|
@@ -1726,7 +1794,7 @@ export class FECManager {
|
|
|
1726
1794
|
}
|
|
1727
1795
|
if (!this.#blockPrecedes(previous.sbn, sbn)) {
|
|
1728
1796
|
if (symbol === 'source' && window !== null && this.#blockPrecedes(sbn, previous.sbn) && (
|
|
1729
|
-
this.#blocksBehind(sbn, previous.sbn, packetId) > this.#isoRetentionBlocks(packetId) ||
|
|
1797
|
+
(!held && this.#blocksBehind(sbn, previous.sbn, packetId) > this.#isoRetentionBlocks(packetId)) ||
|
|
1730
1798
|
(psn !== undefined && anchor !== undefined && serial32Precedes(ssId, anchor.ssId) &&
|
|
1731
1799
|
psn !== anchor.psn && !sentInOrder({ ssId, psn }, anchor))
|
|
1732
1800
|
)) {
|
|
@@ -2080,21 +2148,28 @@ export class FECManager {
|
|
|
2080
2148
|
this.#blockWaitDeadlinesMs.delete(key)
|
|
2081
2149
|
this.#repairHoldUntilMs.delete(key)
|
|
2082
2150
|
this.#traceLifecycle('timeout-queued', sbn, packetId, catalogTrackName)
|
|
2083
|
-
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName)
|
|
2151
|
+
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName, currentDeadlineMs ?? firedAtMs)
|
|
2084
2152
|
}
|
|
2085
|
-
timer.handle = setTimeout(
|
|
2153
|
+
timer.handle = setTimeout(
|
|
2154
|
+
onFire,
|
|
2155
|
+
this.#receiptAtMs === undefined
|
|
2156
|
+
? Math.max(1, Math.ceil(deadlineMs - nowMs))
|
|
2157
|
+
: this.#receiptTimerDelayMs(deadlineMs, 1),
|
|
2158
|
+
)
|
|
2086
2159
|
this.#activeBlocks.set(key, timer)
|
|
2087
2160
|
this.#indexBlockKey(key, sbn, packetId)
|
|
2088
2161
|
}
|
|
2089
2162
|
|
|
2090
2163
|
/**
|
|
2091
2164
|
* Hand a block whose delivery is over to its decoder for finalization: the
|
|
2092
|
-
* datagram deadline fired, or the ordered relay moved past it.
|
|
2165
|
+
* datagram deadline fired, or the ordered relay moved past it. `dueAtMs`
|
|
2166
|
+
* is when it was due: its deadline, or now.
|
|
2093
2167
|
*/
|
|
2094
2168
|
#finalizeExpiredBlock(
|
|
2095
2169
|
sbn: number,
|
|
2096
2170
|
packetId: number,
|
|
2097
2171
|
catalogTrackName: string | undefined,
|
|
2172
|
+
dueAtMs = performance.now(),
|
|
2098
2173
|
): void {
|
|
2099
2174
|
const key = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
2100
2175
|
// A vanished block that never received a symbol holds no decoder state
|
|
@@ -2118,7 +2193,7 @@ export class FECManager {
|
|
|
2118
2193
|
if (workerClient) {
|
|
2119
2194
|
this.#pendingWorkerCleanups.add(key)
|
|
2120
2195
|
this.#pendingWorkerCleanupClients.set(key, workerClient)
|
|
2121
|
-
this.#boundOwedCleanup(key, packetId, workerClient)
|
|
2196
|
+
this.#boundOwedCleanup(key, packetId, workerClient, dueAtMs)
|
|
2122
2197
|
workerClient.cleanup(sbn)
|
|
2123
2198
|
} else {
|
|
2124
2199
|
const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client
|
|
@@ -2289,6 +2364,9 @@ export class FECManager {
|
|
|
2289
2364
|
// sibling's change, but the first stream-scoped geometry on a packet
|
|
2290
2365
|
// does change the siblings that have none (stream-geometry-absent).
|
|
2291
2366
|
this.#bumpEpoch(packetId)
|
|
2367
|
+
for (const key of [...this.#noSpanRaps.keys()]) {
|
|
2368
|
+
if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key)
|
|
2369
|
+
}
|
|
2292
2370
|
for (const [key, classifier] of [...this.#classifiers]) {
|
|
2293
2371
|
if (!this.#belongsToPacket(key, packetId)) continue
|
|
2294
2372
|
const stream = this.#parseStreamKey(key)
|
|
@@ -2532,7 +2610,9 @@ export class FECManager {
|
|
|
2532
2610
|
}
|
|
2533
2611
|
resetMap(this.#blockSourceCounts)
|
|
2534
2612
|
resetMap(this.#blockSourceEsis)
|
|
2613
|
+
resetMap(this.#blockRebuiltEsis)
|
|
2535
2614
|
resetMap(this.#blockAuthoredLengths)
|
|
2615
|
+
resetSet(this.#isoSpanKeys)
|
|
2536
2616
|
resetMap(this.#blockRepairRsIds)
|
|
2537
2617
|
resetMap(this.#blockRsbLengths)
|
|
2538
2618
|
resetMap(this.#heldLayerStates)
|
|
@@ -2565,6 +2645,7 @@ export class FECManager {
|
|
|
2565
2645
|
resetMap(this.#isoLocators)
|
|
2566
2646
|
resetMap(this.#sourceAnchors)
|
|
2567
2647
|
this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName))
|
|
2648
|
+
this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName))
|
|
2568
2649
|
this.#bumpEpoch(packetId)
|
|
2569
2650
|
for (const [sbn, keys] of this.#blockKeysBySbn) {
|
|
2570
2651
|
for (const key of keys.keys()) {
|
|
@@ -2663,7 +2744,9 @@ export class FECManager {
|
|
|
2663
2744
|
dropMap(this.#pendingWorkerCleanupClients)
|
|
2664
2745
|
dropMap(this.#blockSourceCounts)
|
|
2665
2746
|
dropMap(this.#blockSourceEsis)
|
|
2747
|
+
dropMap(this.#blockRebuiltEsis)
|
|
2666
2748
|
dropMap(this.#blockAuthoredLengths)
|
|
2749
|
+
dropSet(this.#isoSpanKeys)
|
|
2667
2750
|
dropMap(this.#blockRepairRsIds)
|
|
2668
2751
|
dropMap(this.#blockRsbLengths)
|
|
2669
2752
|
dropMap(this.#blockPathCounts)
|
|
@@ -2679,6 +2762,7 @@ export class FECManager {
|
|
|
2679
2762
|
dropMap(this.#isoLocators)
|
|
2680
2763
|
dropMap(this.#sourceAnchors)
|
|
2681
2764
|
dropMap(this.#classifiers)
|
|
2765
|
+
dropMap(this.#noSpanRaps)
|
|
2682
2766
|
dropMap(this.#streamGeometries)
|
|
2683
2767
|
dropSet(this.#pendingWorkerCleanups)
|
|
2684
2768
|
dropSet(this.#completedBlocks)
|
|
@@ -2881,6 +2965,10 @@ export class FECManager {
|
|
|
2881
2965
|
}
|
|
2882
2966
|
}
|
|
2883
2967
|
|
|
2968
|
+
if (isSourceSymbol && source === 'multicast') {
|
|
2969
|
+
this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), (sbn + esi) >>> 0, packetId, catalogTrackName)
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2884
2972
|
return true
|
|
2885
2973
|
}
|
|
2886
2974
|
|
|
@@ -3090,8 +3178,9 @@ export class FECManager {
|
|
|
3090
3178
|
return true
|
|
3091
3179
|
}
|
|
3092
3180
|
/**
|
|
3093
|
-
* Drop a block's repair hold: its FETCH ended without delivering
|
|
3094
|
-
*
|
|
3181
|
+
* Drop a block's repair hold: its FETCH ended without delivering, or brought
|
|
3182
|
+
* back only a repair that cannot complete it. The block falls back to its
|
|
3183
|
+
* multicast wait, which may already have passed.
|
|
3095
3184
|
*/
|
|
3096
3185
|
releaseRepairHold(sbn: number, packetId: number, catalogTrackName?: string): void {
|
|
3097
3186
|
const key = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
@@ -3253,6 +3342,8 @@ export class FECManager {
|
|
|
3253
3342
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName)
|
|
3254
3343
|
const evictedBefore = locator.heldEvicted
|
|
3255
3344
|
const held = locator.hold(ssId, source)
|
|
3345
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName)
|
|
3346
|
+
if (held && source.source === 'multicast') this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName)
|
|
3256
3347
|
const evicted = locator.heldEvicted - evictedBefore
|
|
3257
3348
|
if (evicted > 0) {
|
|
3258
3349
|
const before = this.#isoHeldEvictedCount
|
|
@@ -3280,17 +3371,26 @@ export class FECManager {
|
|
|
3280
3371
|
ssStart: number,
|
|
3281
3372
|
ssbLength: number,
|
|
3282
3373
|
): IsoRepairPlacement<IsoHeldSource> {
|
|
3283
|
-
const
|
|
3374
|
+
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName)
|
|
3375
|
+
const placement = locator.placeRepair(ssStart, ssbLength)
|
|
3376
|
+
this.#settleExpiredIsoHoles(locator, packetId, catalogTrackName)
|
|
3284
3377
|
if (placement.status !== 'placed') {
|
|
3285
3378
|
this.#isoRepairRefusedCount++
|
|
3286
3379
|
if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
|
|
3287
3380
|
console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`)
|
|
3288
3381
|
}
|
|
3382
|
+
// The repair FETCH of a span's first group brought back a shorter block:
|
|
3383
|
+
// the span holds more than one, so that group cannot complete it, and
|
|
3384
|
+
// holding the span (and every frame behind it) for the FETCH buys
|
|
3385
|
+
// nothing. It falls back to its multicast wait.
|
|
3386
|
+
if (placement.ofSpan !== undefined) this.releaseRepairHold(placement.ofSpan.start, packetId, catalogTrackName)
|
|
3289
3387
|
return placement
|
|
3290
3388
|
}
|
|
3389
|
+
// A repair states the block at its SS_Start: a span the locator kept
|
|
3390
|
+
// there is now that one block (IsoBlockLocator.placeRepair).
|
|
3391
|
+
this.#isoSpanKeys.delete(this.#blockKey(placement.block.start, packetId, catalogTrackName))
|
|
3291
3392
|
if (placement.span !== undefined) this.#openIsoSpan(placement.span, packetId, catalogTrackName)
|
|
3292
3393
|
if (placement.hole !== undefined) {
|
|
3293
|
-
const locator = this.#isoLocatorFor(packetId, catalogTrackName)
|
|
3294
3394
|
const hole = placement.hole.block
|
|
3295
3395
|
const previous = locator.blockEndingAt(hole.start)
|
|
3296
3396
|
const loss: UndelimitedIsoLoss = {
|
|
@@ -3312,18 +3412,38 @@ export class FECManager {
|
|
|
3312
3412
|
* `handler`, instead of a repair-less span; null restores spans. Set by the
|
|
3313
3413
|
* range repair scheduler; applies to every stream from its next gap. The
|
|
3314
3414
|
* handler and the deferral are one setting, so no hole is ever created with
|
|
3315
|
-
* nobody to close it.
|
|
3415
|
+
* nobody to close it. `onExpired` is told of each pending hole the stream
|
|
3416
|
+
* runs past the locator's horizon (BLO-52085): returning true, it closes
|
|
3417
|
+
* the hole (`closeIsoHoles`), so the span it leaves still fails and is
|
|
3418
|
+
* reported; otherwise the hole is discarded with its sources.
|
|
3316
3419
|
*/
|
|
3317
|
-
setUndelimitedLossHandler(
|
|
3420
|
+
setUndelimitedLossHandler(
|
|
3421
|
+
handler: ((loss: UndelimitedIsoLoss) => void) | null,
|
|
3422
|
+
onExpired: ((hole: ExpiredIsoHole) => boolean) | null = null,
|
|
3423
|
+
): void {
|
|
3318
3424
|
this.#onUndelimitedLoss = handler
|
|
3425
|
+
this.#onIsoHoleExpired = handler === null ? null : onExpired
|
|
3319
3426
|
this.#deferIsoGaps = handler !== null
|
|
3320
3427
|
for (const locator of this.#isoLocators.values()) locator.deferIncompleteGaps = this.#deferIsoGaps
|
|
3321
3428
|
}
|
|
3322
3429
|
|
|
3430
|
+
/** Hand each hole the locator's horizon passed to its owner, or discard it (setUndelimitedLossHandler). */
|
|
3431
|
+
#settleExpiredIsoHoles(
|
|
3432
|
+
locator: IsoBlockLocator<IsoHeldSource>,
|
|
3433
|
+
packetId: number,
|
|
3434
|
+
catalogTrackName: string | undefined,
|
|
3435
|
+
): void {
|
|
3436
|
+
for (const hole of locator.takeExpiredHoles()) {
|
|
3437
|
+
const expired: ExpiredIsoHole = { packetId, catalogTrackName, holeStart: hole.start, holeLength: hole.length }
|
|
3438
|
+
if (this.#onIsoHoleExpired?.(expired) !== true) locator.closeHolesWithin(hole.start, hole.length)
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3323
3442
|
/**
|
|
3324
|
-
* The range repair for a hole ended: every hole
|
|
3325
|
-
*
|
|
3326
|
-
* with the held sources they placed, for the caller to feed
|
|
3443
|
+
* The range repair for a hole ended: every hole inside `[start, start +
|
|
3444
|
+
* length)`, pending or expired, becomes a repair-less span. Returns those
|
|
3445
|
+
* spans with the held sources they placed, for the caller to feed
|
|
3446
|
+
* (MmtpRouter).
|
|
3327
3447
|
*/
|
|
3328
3448
|
closeIsoHoles(
|
|
3329
3449
|
packetId: number,
|
|
@@ -3352,7 +3472,10 @@ export class FECManager {
|
|
|
3352
3472
|
catalogTrackName: string | undefined,
|
|
3353
3473
|
): void {
|
|
3354
3474
|
const key = this.#blockKey(span.block.start, packetId, catalogTrackName)
|
|
3355
|
-
if (!this.#blockAuthoredLengths.has(key))
|
|
3475
|
+
if (!this.#blockAuthoredLengths.has(key)) {
|
|
3476
|
+
this.#blockAuthoredLengths.set(key, span.block.length)
|
|
3477
|
+
this.#isoSpanKeys.add(key)
|
|
3478
|
+
}
|
|
3356
3479
|
if (span.placed.length === 0) this.#materializeBlock(span.block.start, packetId, catalogTrackName, true)
|
|
3357
3480
|
}
|
|
3358
3481
|
|
|
@@ -3470,11 +3593,14 @@ export class FECManager {
|
|
|
3470
3593
|
/**
|
|
3471
3594
|
* Whether an MPU boundary still bounds this stream's RAPs (see
|
|
3472
3595
|
* `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
|
|
3473
|
-
* it does not. A stream with no classifier yet has shown nothing against it
|
|
3596
|
+
* it does not. A stream with no classifier yet has shown nothing against it,
|
|
3597
|
+
* and neither has a stream without catalog timing until it releases two
|
|
3598
|
+
* RAPs inside one MPU.
|
|
3474
3599
|
*/
|
|
3475
3600
|
mpuBoundsRapsFor(packetId: number, catalogTrackName?: string): boolean {
|
|
3476
|
-
|
|
3477
|
-
return this.#
|
|
3601
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
3602
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true
|
|
3603
|
+
return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true
|
|
3478
3604
|
}
|
|
3479
3605
|
|
|
3480
3606
|
/** A block's class now, or frozen at its resolution. */
|
|
@@ -3510,7 +3636,15 @@ export class FECManager {
|
|
|
3510
3636
|
presentationTime: bigint,
|
|
3511
3637
|
mediaTimescale: number,
|
|
3512
3638
|
): void {
|
|
3513
|
-
if (!this.#trackConfigs.has(packetId)
|
|
3639
|
+
if (!this.#trackConfigs.has(packetId)) return
|
|
3640
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) {
|
|
3641
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
3642
|
+
const last = this.#noSpanRaps.get(streamKey)
|
|
3643
|
+
const inside = last?.inside === true ||
|
|
3644
|
+
(last?.mpu === mpu && last.presentationTime !== presentationTime)
|
|
3645
|
+
this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside })
|
|
3646
|
+
return
|
|
3647
|
+
}
|
|
3514
3648
|
this.#applyClassifierChanges(
|
|
3515
3649
|
packetId,
|
|
3516
3650
|
catalogTrackName,
|
|
@@ -3820,13 +3954,27 @@ export class FECManager {
|
|
|
3820
3954
|
|
|
3821
3955
|
client.onCleanupComplete((sbn: number) => {
|
|
3822
3956
|
if (this.#releasedWorkerClients.has(client)) return
|
|
3823
|
-
const
|
|
3957
|
+
const owed = [...this.#pendingWorkerCleanups].filter((key) => this.#pendingWorkerCleanupClients.get(key) === client)
|
|
3958
|
+
const cleanupKeys = owed.filter((key) => {
|
|
3824
3959
|
const block = this.#parseBlockKey(key)
|
|
3825
|
-
return block.packetId === packetId &&
|
|
3826
|
-
block.sbn === sbn &&
|
|
3827
|
-
this.#pendingWorkerCleanupClients.get(key) === client
|
|
3960
|
+
return block.packetId === packetId && block.sbn === sbn
|
|
3828
3961
|
})
|
|
3829
|
-
|
|
3962
|
+
// A cleanup is a queue-drain barrier: the Worker took every symbol fed
|
|
3963
|
+
// before it was asked for first, so every block already due by then
|
|
3964
|
+
// is settled by this acknowledgement too, not one barrier later. The
|
|
3965
|
+
// client sends one barrier at a time, and the blocks a repair walk
|
|
3966
|
+
// stated in one span are due together (BLO-52085). That holds only
|
|
3967
|
+
// while no block is fed a symbol after its dueAtMs, which is its
|
|
3968
|
+
// receipt-scoped deadline, not when feeding stopped: a symbol fed
|
|
3969
|
+
// between that deadline and the block's timer, after another block's
|
|
3970
|
+
// barrier was asked for, would find the block settled (failed) one
|
|
3971
|
+
// barrier early. Walks end before D-28 (walkDeadlineAt), so that window
|
|
3972
|
+
// is the timer's lateness alone; whatever feeds a block past its dueAtMs
|
|
3973
|
+
// must leave it to its own barrier.
|
|
3974
|
+
const askedAtMs = Math.max(...cleanupKeys.map((key) => this.#pendingWorkerCleanupBounds.get(key)?.askedAtMs ?? -Infinity))
|
|
3975
|
+
const due = owed.filter((key) =>
|
|
3976
|
+
!cleanupKeys.includes(key) && (this.#pendingWorkerCleanupBounds.get(key)?.dueAtMs ?? Infinity) <= askedAtMs)
|
|
3977
|
+
for (const key of [...cleanupKeys, ...due]) this.#settleOwedCleanup(key, 'cleanup-ack')
|
|
3830
3978
|
})
|
|
3831
3979
|
|
|
3832
3980
|
client.onError((msg: string, code?: FecWorkerErrorCode) => {
|
|
@@ -3908,9 +4056,11 @@ export class FECManager {
|
|
|
3908
4056
|
* fail deadline already passed, so failing it is the decision that deadline
|
|
3909
4057
|
* made. A late ack then finds nothing owed and is a no-op.
|
|
3910
4058
|
*/
|
|
3911
|
-
#boundOwedCleanup(key: string, packetId: number, client: FecWorkerClient): void {
|
|
4059
|
+
#boundOwedCleanup(key: string, packetId: number, client: FecWorkerClient, dueAtMs: number): void {
|
|
3912
4060
|
this.#clearOwedCleanupBound(key)
|
|
3913
4061
|
const bound = {
|
|
4062
|
+
dueAtMs,
|
|
4063
|
+
askedAtMs: performance.now(),
|
|
3914
4064
|
handle: setTimeout(() => {
|
|
3915
4065
|
if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return
|
|
3916
4066
|
this.#pendingWorkerCleanupBounds.delete(key)
|
|
@@ -4041,7 +4191,22 @@ export class FECManager {
|
|
|
4041
4191
|
this.#isoSourceOversizeCount++
|
|
4042
4192
|
return null
|
|
4043
4193
|
}
|
|
4044
|
-
|
|
4194
|
+
return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start)
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
/**
|
|
4198
|
+
* Hand one placed source symbol to the stream's decoder: its Worker, which
|
|
4199
|
+
* answers through onFrame (null here), or its synchronous decoder, whose
|
|
4200
|
+
* recovered block is returned. `sbn` is the SS_Start of the block a repair
|
|
4201
|
+
* stated for `ssId`.
|
|
4202
|
+
*/
|
|
4203
|
+
#feedDecoderSource(
|
|
4204
|
+
packetId: number,
|
|
4205
|
+
ssId: number,
|
|
4206
|
+
symbolData: Uint8Array,
|
|
4207
|
+
catalogTrackName: string | undefined,
|
|
4208
|
+
sbn: number,
|
|
4209
|
+
): Uint8Array | null {
|
|
4045
4210
|
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
4046
4211
|
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey)
|
|
4047
4212
|
|
|
@@ -4086,6 +4251,97 @@ export class FECManager {
|
|
|
4086
4251
|
return recovered
|
|
4087
4252
|
}
|
|
4088
4253
|
|
|
4254
|
+
/**
|
|
4255
|
+
* Feed a source symbol of this stream that another FEC instance over the
|
|
4256
|
+
* same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
|
|
4257
|
+
* caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
|
|
4258
|
+
* BLO-52085). It joins its block's decoder as a received source would, so
|
|
4259
|
+
* it reduces the erasures the block still has to solve, but it is not a
|
|
4260
|
+
* network arrival: no transport accounting, no classification evidence,
|
|
4261
|
+
* no deadline refresh, and it never makes a block clean.
|
|
4262
|
+
*
|
|
4263
|
+
* Only a block a repair stated takes one (a repair-less span has no
|
|
4264
|
+
* decoder block). A block that is terminal, or already holds that source,
|
|
4265
|
+
* does not. The symbol is the exact protected packet, unpadded, as
|
|
4266
|
+
* feedSourceSymbol takes it.
|
|
4267
|
+
*
|
|
4268
|
+
* `recovered` is the block the synchronous decoder completed with it; the
|
|
4269
|
+
* caller routes it and completes the block, as for any recovery. When
|
|
4270
|
+
* every source of the block is then present, received or rebuilt, and no
|
|
4271
|
+
* decoder returned it (a Worker answers asynchronously, and reports such a
|
|
4272
|
+
* block as clean on its own side), the block is completed here as
|
|
4273
|
+
* recovered, with nothing left to route.
|
|
4274
|
+
*/
|
|
4275
|
+
feedRebuiltSourceSymbol(
|
|
4276
|
+
packetId: number,
|
|
4277
|
+
ssId: number,
|
|
4278
|
+
symbolData: Uint8Array,
|
|
4279
|
+
catalogTrackName?: string,
|
|
4280
|
+
): RebuiltSourceFeed {
|
|
4281
|
+
if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN
|
|
4282
|
+
const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId)
|
|
4283
|
+
if (located === null || located.block.span) return NOT_TAKEN
|
|
4284
|
+
if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN
|
|
4285
|
+
const sbn = located.block.start
|
|
4286
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
4287
|
+
if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN
|
|
4288
|
+
let rebuilt = this.#blockRebuiltEsis.get(key)
|
|
4289
|
+
if (rebuilt?.has(located.esi)) return NOT_TAKEN
|
|
4290
|
+
if (rebuilt === undefined) {
|
|
4291
|
+
rebuilt = new Set()
|
|
4292
|
+
this.#blockRebuiltEsis.set(key, rebuilt)
|
|
4293
|
+
}
|
|
4294
|
+
const config = this.#trackConfigs.get(packetId)
|
|
4295
|
+
const wasShort = config !== undefined &&
|
|
4296
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== undefined
|
|
4297
|
+
rebuilt.add(located.esi)
|
|
4298
|
+
const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn)
|
|
4299
|
+
if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
|
|
4300
|
+
// The late-source cut assumed only a late source could complete this
|
|
4301
|
+
// block; with this source its group can, so it gets a full multicast
|
|
4302
|
+
// wait again, counted from now.
|
|
4303
|
+
// ponytail: from now, not from its sources, so it can close up to one
|
|
4304
|
+
// multicast wait after its uncut deadline; at most once per block, since
|
|
4305
|
+
// a rebuilt source only adds to what the block holds and the cut cannot
|
|
4306
|
+
// apply to it again. Keep the uncut deadline and re-arm to it if a lab
|
|
4307
|
+
// run shows that wait.
|
|
4308
|
+
if (wasShort && this.#activeBlocks.has(key) &&
|
|
4309
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config!) === undefined) {
|
|
4310
|
+
this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config!))
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
return { taken: true, recovered }
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
/**
|
|
4317
|
+
* A block that holds every one of its `length` sources, received or
|
|
4318
|
+
* rebuilt by another instance, with at least one rebuilt, is recovered:
|
|
4319
|
+
* every packet of it was delivered, by its own arrival or by the instance
|
|
4320
|
+
* that rebuilt it, so nothing is routed. Returns whether it completed it.
|
|
4321
|
+
*/
|
|
4322
|
+
#completeWhenSourcesPresent(
|
|
4323
|
+
key: string,
|
|
4324
|
+
sbn: number,
|
|
4325
|
+
length: number,
|
|
4326
|
+
packetId: number,
|
|
4327
|
+
catalogTrackName: string | undefined,
|
|
4328
|
+
): boolean {
|
|
4329
|
+
const rebuilt = this.#blockRebuiltEsis.get(key)
|
|
4330
|
+
if (rebuilt === undefined || this.#completedBlocks.has(key)) return false
|
|
4331
|
+
let present = rebuilt.size
|
|
4332
|
+
for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++
|
|
4333
|
+
if (present < length) return false
|
|
4334
|
+
this.#traceLifecycle('recovered', sbn, packetId, catalogTrackName)
|
|
4335
|
+
this.countRecovery(packetId, sbn, catalogTrackName)
|
|
4336
|
+
this.cancelBlockTimeout(sbn, packetId, catalogTrackName)
|
|
4337
|
+
if (catalogTrackName === undefined) {
|
|
4338
|
+
this.onBlockComplete?.(sbn, packetId)
|
|
4339
|
+
} else {
|
|
4340
|
+
this.onBlockComplete?.(sbn, packetId, catalogTrackName)
|
|
4341
|
+
}
|
|
4342
|
+
return true
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4089
4345
|
/**
|
|
4090
4346
|
* fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
|
|
4091
4347
|
* exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
|
|
@@ -4343,9 +4599,100 @@ export class FECManager {
|
|
|
4343
4599
|
this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName)
|
|
4344
4600
|
return null
|
|
4345
4601
|
}
|
|
4602
|
+
// Its K' is known now: a block this repair shows short of it waits less.
|
|
4603
|
+
if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName)
|
|
4346
4604
|
return recovered
|
|
4347
4605
|
}
|
|
4348
4606
|
|
|
4607
|
+
/**
|
|
4608
|
+
* A first-arrival multicast source, held or placed. A datagram flow keeps
|
|
4609
|
+
* its sources in send order, so one that moves the stream's newest source
|
|
4610
|
+
* position past a block leaves the sources that block still misses
|
|
4611
|
+
* reordered behind it at best: it shortens the wait of every block it
|
|
4612
|
+
* passed that only a late source can complete. MoQ is left out: its streams
|
|
4613
|
+
* can overtake one another, so a MoQ source shows nothing about the sources
|
|
4614
|
+
* sent before it.
|
|
4615
|
+
*/
|
|
4616
|
+
#noteMulticastSource(
|
|
4617
|
+
locator: IsoBlockLocator<IsoHeldSource>,
|
|
4618
|
+
ssId: number,
|
|
4619
|
+
packetId: number,
|
|
4620
|
+
catalogTrackName: string | undefined,
|
|
4621
|
+
): void {
|
|
4622
|
+
const previous = locator.newestSource
|
|
4623
|
+
if (!locator.noteSource(ssId) || previous === undefined) return
|
|
4624
|
+
// The blocks whose last SS_ID lies in [previous, ssId): this source
|
|
4625
|
+
// passed them and the previous newest one had not.
|
|
4626
|
+
for (const block of locator.blocksBetween((previous - 1) >>> 0, ssId)) {
|
|
4627
|
+
if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue
|
|
4628
|
+
this.#shortenUnrecoverableWait(
|
|
4629
|
+
this.#blockKey(block.start, packetId, catalogTrackName),
|
|
4630
|
+
block.start,
|
|
4631
|
+
packetId,
|
|
4632
|
+
catalogTrackName,
|
|
4633
|
+
)
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
|
|
4637
|
+
/**
|
|
4638
|
+
* The wait of a block that only a late source can complete (BLO-52085): a
|
|
4639
|
+
* repair stated it, its received sources plus every symbol its repair group
|
|
4640
|
+
* carries (RSB_length) are short of K' (the repair scheduler's
|
|
4641
|
+
* group-cannot-complete), and a later source of the stream has arrived, so
|
|
4642
|
+
* a source it misses is lost or reordered behind that one. The span, which
|
|
4643
|
+
* covers repair symbols paced after its sources, buys it nothing: it waits
|
|
4644
|
+
* the late-symbol allowance alone, the multicast wait less the span (the
|
|
4645
|
+
* catalog tolerance or the measured delivery gap). Undefined for any other
|
|
4646
|
+
* block.
|
|
4647
|
+
*/
|
|
4648
|
+
#lateSourceWaitMs(
|
|
4649
|
+
key: string,
|
|
4650
|
+
sbn: number,
|
|
4651
|
+
packetId: number,
|
|
4652
|
+
catalogTrackName: string | undefined,
|
|
4653
|
+
config: ResolvedFECTrackConfig,
|
|
4654
|
+
): number | undefined {
|
|
4655
|
+
const authoredLength = this.#blockAuthoredLengths.get(key)
|
|
4656
|
+
const rsbLength = this.#blockRsbLengths.get(key)
|
|
4657
|
+
if (authoredLength === undefined || rsbLength === undefined) return undefined
|
|
4658
|
+
// A source another instance rebuilt (feedRebuiltSourceSymbol) is one the
|
|
4659
|
+
// decoder holds: the group need only cover what neither delivered.
|
|
4660
|
+
let present = this.#blockSourceCounts.get(key) ?? 0
|
|
4661
|
+
const received = this.#blockSourceEsis.get(key)
|
|
4662
|
+
for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++
|
|
4663
|
+
if (present + rsbLength >= authoredLength) return undefined
|
|
4664
|
+
const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource
|
|
4665
|
+
if (newestSource === undefined || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
|
|
4666
|
+
return undefined
|
|
4667
|
+
}
|
|
4668
|
+
return this.#multicastWaitMs(config) - config.interleaveDepthMs
|
|
4669
|
+
}
|
|
4670
|
+
|
|
4671
|
+
/**
|
|
4672
|
+
* Bring an open block's wait in to its late-source wait, when it has one.
|
|
4673
|
+
* Never later than it is armed: only a source can complete such a block,
|
|
4674
|
+
* so a repair of its group (paced across the span) must not keep it open.
|
|
4675
|
+
*/
|
|
4676
|
+
#shortenUnrecoverableWait(
|
|
4677
|
+
key: string,
|
|
4678
|
+
sbn: number,
|
|
4679
|
+
packetId: number,
|
|
4680
|
+
catalogTrackName: string | undefined,
|
|
4681
|
+
): void {
|
|
4682
|
+
if (!this.#activeBlocks.has(key)) return
|
|
4683
|
+
const config = this.#trackConfigs.get(packetId)
|
|
4684
|
+
if (config === undefined) return
|
|
4685
|
+
const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config)
|
|
4686
|
+
if (waitMs === undefined) return
|
|
4687
|
+
const armedAtMs = this.#blockWaitDeadlinesMs.get(key)
|
|
4688
|
+
this.#armBlockTimeout(
|
|
4689
|
+
sbn,
|
|
4690
|
+
packetId,
|
|
4691
|
+
catalogTrackName,
|
|
4692
|
+
armedAtMs === undefined ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow()),
|
|
4693
|
+
)
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4349
4696
|
/**
|
|
4350
4697
|
* Record a block's authored SSB_length from a repair symbol. Returns false,
|
|
4351
4698
|
* leaving the block unchanged, when the symbol contradicts the block: a
|
|
@@ -4485,7 +4832,8 @@ export class FECManager {
|
|
|
4485
4832
|
* the inactivity deadline on each symbol, records the symbol's in-band
|
|
4486
4833
|
* classification evidence, and marks the block clean when all of its
|
|
4487
4834
|
* authored source symbols arrived: the SSB_length a repair symbol declared,
|
|
4488
|
-
* otherwise K.
|
|
4835
|
+
* otherwise K. `held` marks a source held unplaced since it arrived and
|
|
4836
|
+
* placed only now (see #noteSbnSeen).
|
|
4489
4837
|
*
|
|
4490
4838
|
* Throws for a packetId that was never configured, so a classified block
|
|
4491
4839
|
* never reads the manager-global K or timeout.
|
|
@@ -4496,6 +4844,7 @@ export class FECManager {
|
|
|
4496
4844
|
catalogTrackName: string | undefined,
|
|
4497
4845
|
esi: number,
|
|
4498
4846
|
evidence: SourceSymbolEvidence,
|
|
4847
|
+
held = false,
|
|
4499
4848
|
): void {
|
|
4500
4849
|
const trackConfig = this.#trackConfigs.get(packetId)
|
|
4501
4850
|
if (trackConfig === undefined) {
|
|
@@ -4517,6 +4866,7 @@ export class FECManager {
|
|
|
4517
4866
|
'source',
|
|
4518
4867
|
evidence.packetSequenceNumber,
|
|
4519
4868
|
Number.isInteger(esi) ? (sbn + esi) >>> 0 : sbn,
|
|
4869
|
+
held,
|
|
4520
4870
|
)
|
|
4521
4871
|
|
|
4522
4872
|
// A source ESI at or past the block's authored SSB_length contradicts it;
|
|
@@ -4561,6 +4911,20 @@ export class FECManager {
|
|
|
4561
4911
|
this.#completeCleanBlock(key, sbn, packetId, catalogTrackName)
|
|
4562
4912
|
return
|
|
4563
4913
|
}
|
|
4914
|
+
// A late arrival can be the last source a block lacked besides those
|
|
4915
|
+
// another instance rebuilt (feedRebuiltSourceSymbol).
|
|
4916
|
+
if (
|
|
4917
|
+
this.#blockRebuiltEsis.has(key) &&
|
|
4918
|
+
this.#completeWhenSourcesPresent(
|
|
4919
|
+
key,
|
|
4920
|
+
sbn,
|
|
4921
|
+
this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
|
|
4922
|
+
packetId,
|
|
4923
|
+
catalogTrackName,
|
|
4924
|
+
)
|
|
4925
|
+
) {
|
|
4926
|
+
return
|
|
4927
|
+
}
|
|
4564
4928
|
|
|
4565
4929
|
this.noteBlockActivity(sbn, packetId, catalogTrackName, true)
|
|
4566
4930
|
|
|
@@ -4580,7 +4944,9 @@ export class FECManager {
|
|
|
4580
4944
|
this.cancelBlockTimeout(sbn, packetId, catalogTrackName)
|
|
4581
4945
|
this.#blockSourceCounts.delete(key)
|
|
4582
4946
|
this.#blockSourceEsis.delete(key)
|
|
4947
|
+
this.#blockRebuiltEsis.delete(key)
|
|
4583
4948
|
this.#blockAuthoredLengths.delete(key)
|
|
4949
|
+
this.#isoSpanKeys.delete(key)
|
|
4584
4950
|
this.#blockRepairRsIds.delete(key)
|
|
4585
4951
|
this.#blockRsbLengths.delete(key)
|
|
4586
4952
|
// BLO-3323 L15: track which SBNs reached K source-symbol arrivals so the
|
|
@@ -4622,6 +4988,20 @@ export class FECManager {
|
|
|
4622
4988
|
return this.#receiptAtMs ?? performance.now()
|
|
4623
4989
|
}
|
|
4624
4990
|
|
|
4991
|
+
/**
|
|
4992
|
+
* The whole-ms delay of a timer for `deadlineMs` armed inside a receipt
|
|
4993
|
+
* scope: counted from the receipt time, as the timer of a symbol fed when
|
|
4994
|
+
* it was received was, less the whole ms since. So it fires on the tick
|
|
4995
|
+
* that timer would have, never later than a delay counted from now, and
|
|
4996
|
+
* its handler still re-arms a timer that fires before its deadline.
|
|
4997
|
+
* Counted from now, a deadline set from the receipt time is rounded up
|
|
4998
|
+
* from another fraction of a ms (BLO-52085).
|
|
4999
|
+
*/
|
|
5000
|
+
#receiptTimerDelayMs(deadlineMs: number, minMs: number): number {
|
|
5001
|
+
const receivedAtMs = this.#receiptAtMs!
|
|
5002
|
+
return Math.max(minMs, Math.ceil(deadlineMs - receivedAtMs) - Math.ceil(performance.now() - receivedAtMs))
|
|
5003
|
+
}
|
|
5004
|
+
|
|
4625
5005
|
/**
|
|
4626
5006
|
* A held packet admitted late arms from its (earlier) receipt time; that
|
|
4627
5007
|
* never pulls in a deadline later activity already armed. Outside a
|
|
@@ -4693,7 +5073,18 @@ export class FECManager {
|
|
|
4693
5073
|
)
|
|
4694
5074
|
}
|
|
4695
5075
|
|
|
4696
|
-
|
|
5076
|
+
// A block only a late source can complete waits the late-symbol
|
|
5077
|
+
// allowance from its own sources' activity; nothing else moves it later.
|
|
5078
|
+
if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== undefined) {
|
|
5079
|
+
this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName)
|
|
5080
|
+
return
|
|
5081
|
+
}
|
|
5082
|
+
this.#armBlockTimeout(
|
|
5083
|
+
sbn,
|
|
5084
|
+
packetId,
|
|
5085
|
+
catalogTrackName,
|
|
5086
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs,
|
|
5087
|
+
)
|
|
4697
5088
|
}
|
|
4698
5089
|
|
|
4699
5090
|
#failBlock(
|
|
@@ -4702,6 +5093,9 @@ export class FECManager {
|
|
|
4702
5093
|
catalogTrackName?: string,
|
|
4703
5094
|
): void {
|
|
4704
5095
|
const key = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
5096
|
+
const span: IsoSpanFailure | undefined = this.#isoSpanKeys.delete(key)
|
|
5097
|
+
? { length: this.#blockAuthoredLengths.get(key) ?? 0, sources: this.#blockSourceCounts.get(key) ?? 0 }
|
|
5098
|
+
: undefined
|
|
4705
5099
|
this.#traceLifecycle('failed', sbn, packetId, catalogTrackName)
|
|
4706
5100
|
this.#clearRepairDeadline(key)
|
|
4707
5101
|
this.#clearBlockTimer(key)
|
|
@@ -4711,6 +5105,7 @@ export class FECManager {
|
|
|
4711
5105
|
}
|
|
4712
5106
|
this.#blockSourceCounts.delete(key)
|
|
4713
5107
|
this.#blockSourceEsis.delete(key)
|
|
5108
|
+
this.#blockRebuiltEsis.delete(key)
|
|
4714
5109
|
this.#blockAuthoredLengths.delete(key)
|
|
4715
5110
|
this.#blockRepairRsIds.delete(key)
|
|
4716
5111
|
this.#blockRsbLengths.delete(key)
|
|
@@ -4720,7 +5115,8 @@ export class FECManager {
|
|
|
4720
5115
|
this.#markPathBlockComplete(sbn, packetId, catalogTrackName)
|
|
4721
5116
|
this.#fecStats.blocksFailed++
|
|
4722
5117
|
this.#statsForPacket(packetId).blocksFailed++
|
|
4723
|
-
this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass)
|
|
5118
|
+
if (span === undefined) this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass)
|
|
5119
|
+
else this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span)
|
|
4724
5120
|
this.onBlockResolved?.(sbn, packetId, catalogTrackName)
|
|
4725
5121
|
}
|
|
4726
5122
|
|
|
@@ -4750,7 +5146,9 @@ export class FECManager {
|
|
|
4750
5146
|
this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName)
|
|
4751
5147
|
this.#blockSourceCounts.delete(key)
|
|
4752
5148
|
this.#blockSourceEsis.delete(key)
|
|
5149
|
+
this.#blockRebuiltEsis.delete(key)
|
|
4753
5150
|
this.#blockAuthoredLengths.delete(key)
|
|
5151
|
+
this.#isoSpanKeys.delete(key)
|
|
4754
5152
|
this.#blockRepairRsIds.delete(key)
|
|
4755
5153
|
this.#blockRsbLengths.delete(key)
|
|
4756
5154
|
this.#clearBlockTimer(key)
|
|
@@ -4858,7 +5256,9 @@ export class FECManager {
|
|
|
4858
5256
|
this.#blockKeysBySbn.clear()
|
|
4859
5257
|
this.#blockSourceCounts.clear()
|
|
4860
5258
|
this.#blockSourceEsis.clear()
|
|
5259
|
+
this.#blockRebuiltEsis.clear()
|
|
4861
5260
|
this.#blockAuthoredLengths.clear()
|
|
5261
|
+
this.#isoSpanKeys.clear()
|
|
4862
5262
|
this.#blockRepairRsIds.clear()
|
|
4863
5263
|
this.#blockRsbLengths.clear()
|
|
4864
5264
|
this.#heldLayerStates.clear()
|
|
@@ -4883,6 +5283,7 @@ export class FECManager {
|
|
|
4883
5283
|
this.#blockAnchorsMs.clear()
|
|
4884
5284
|
this.#blockCatalogTrackNames.clear()
|
|
4885
5285
|
this.#classifiers.clear()
|
|
5286
|
+
this.#noSpanRaps.clear()
|
|
4886
5287
|
this.#streamGeometries.clear()
|
|
4887
5288
|
this.#failedBlocks.clear()
|
|
4888
5289
|
this.#resolvedBlockClasses.clear()
|