@blockcast/mmt-container 2.2.0-main.da2e7cd1dd5d → 2.2.0-main.e75bbd7576a6
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/dist/browser-runtime.js +472 -36
- package/dist/browser-runtime.js.map +2 -2
- package/dist/delivery-catalog.js +2 -1
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/fec-manager.d.ts +30 -0
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +196 -2
- package/dist/fec-manager.js.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.d.ts +9 -0
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +17 -0
- 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/mmtp-container-parser.d.ts +35 -2
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +82 -9
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +85 -11
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +81 -9
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +18 -1
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +276 -34
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-manager.js +472 -36
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +2 -1
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.js +81 -9
- package/dist/no-modules.js.map +2 -2
- package/dist/wasm-mmtp-container-parser.js +81 -9
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +5 -5
- package/src/fec-manager.ts +251 -2
- package/src/index.ts +1 -0
- package/src/iso-block-locator.ts +19 -0
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +84 -8
- package/src/mmtp-router.ts +346 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockcast/mmt-container",
|
|
3
|
-
"version": "2.2.0-main.
|
|
3
|
+
"version": "2.2.0-main.e75bbd7576a6",
|
|
4
4
|
"description": "MMTP/LOC container parsing for MMT over MoQ and SSM",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -169,10 +169,10 @@
|
|
|
169
169
|
"test:watch": "vitest"
|
|
170
170
|
},
|
|
171
171
|
"dependencies": {
|
|
172
|
-
"@blockcast/fec-worker": "2.2.0-main.
|
|
173
|
-
"@blockcast/mmt-base": "2.2.0-main.
|
|
174
|
-
"@blockcast/mmt-fec": "2.2.0-main.
|
|
175
|
-
"@blockcast/mmt-manifest-verify": "2.2.0-main.
|
|
172
|
+
"@blockcast/fec-worker": "2.2.0-main.e75bbd7576a6",
|
|
173
|
+
"@blockcast/mmt-base": "2.2.0-main.e75bbd7576a6",
|
|
174
|
+
"@blockcast/mmt-fec": "2.2.0-main.e75bbd7576a6",
|
|
175
|
+
"@blockcast/mmt-manifest-verify": "2.2.0-main.e75bbd7576a6",
|
|
176
176
|
"@noble/curves": "^1.9.7",
|
|
177
177
|
"@noble/hashes": "^1.8.0",
|
|
178
178
|
"ajv": "^8.17.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),
|
|
@@ -514,6 +526,10 @@ export class FECManager {
|
|
|
514
526
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
515
527
|
#blockSourceCounts = new Map<string, number>()
|
|
516
528
|
#blockSourceEsis = new Map<string, Set<number>>()
|
|
529
|
+
// Source ESIs per block that another FEC instance rebuilt and fed in
|
|
530
|
+
// (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
|
|
531
|
+
// not network arrivals and never make a block clean.
|
|
532
|
+
#blockRebuiltEsis = new Map<string, Set<number>>()
|
|
517
533
|
// Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
|
|
518
534
|
// first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
|
|
519
535
|
// media units, usually before it holds K symbols; under iso-ssbg1-v1
|
|
@@ -2540,6 +2556,7 @@ export class FECManager {
|
|
|
2540
2556
|
}
|
|
2541
2557
|
resetMap(this.#blockSourceCounts)
|
|
2542
2558
|
resetMap(this.#blockSourceEsis)
|
|
2559
|
+
resetMap(this.#blockRebuiltEsis)
|
|
2543
2560
|
resetMap(this.#blockAuthoredLengths)
|
|
2544
2561
|
resetMap(this.#blockRepairRsIds)
|
|
2545
2562
|
resetMap(this.#blockRsbLengths)
|
|
@@ -2672,6 +2689,7 @@ export class FECManager {
|
|
|
2672
2689
|
dropMap(this.#pendingWorkerCleanupClients)
|
|
2673
2690
|
dropMap(this.#blockSourceCounts)
|
|
2674
2691
|
dropMap(this.#blockSourceEsis)
|
|
2692
|
+
dropMap(this.#blockRebuiltEsis)
|
|
2675
2693
|
dropMap(this.#blockAuthoredLengths)
|
|
2676
2694
|
dropMap(this.#blockRepairRsIds)
|
|
2677
2695
|
dropMap(this.#blockRsbLengths)
|
|
@@ -2891,6 +2909,10 @@ export class FECManager {
|
|
|
2891
2909
|
}
|
|
2892
2910
|
}
|
|
2893
2911
|
|
|
2912
|
+
if (isSourceSymbol && source === 'multicast') {
|
|
2913
|
+
this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), (sbn + esi) >>> 0, packetId, catalogTrackName)
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2894
2916
|
return true
|
|
2895
2917
|
}
|
|
2896
2918
|
|
|
@@ -3263,6 +3285,7 @@ export class FECManager {
|
|
|
3263
3285
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName)
|
|
3264
3286
|
const evictedBefore = locator.heldEvicted
|
|
3265
3287
|
const held = locator.hold(ssId, source)
|
|
3288
|
+
if (held && source.source === 'multicast') this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName)
|
|
3266
3289
|
const evicted = locator.heldEvicted - evictedBefore
|
|
3267
3290
|
if (evicted > 0) {
|
|
3268
3291
|
const before = this.#isoHeldEvictedCount
|
|
@@ -4062,7 +4085,22 @@ export class FECManager {
|
|
|
4062
4085
|
this.#isoSourceOversizeCount++
|
|
4063
4086
|
return null
|
|
4064
4087
|
}
|
|
4065
|
-
|
|
4088
|
+
return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start)
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
/**
|
|
4092
|
+
* Hand one placed source symbol to the stream's decoder: its Worker, which
|
|
4093
|
+
* answers through onFrame (null here), or its synchronous decoder, whose
|
|
4094
|
+
* recovered block is returned. `sbn` is the SS_Start of the block a repair
|
|
4095
|
+
* stated for `ssId`.
|
|
4096
|
+
*/
|
|
4097
|
+
#feedDecoderSource(
|
|
4098
|
+
packetId: number,
|
|
4099
|
+
ssId: number,
|
|
4100
|
+
symbolData: Uint8Array,
|
|
4101
|
+
catalogTrackName: string | undefined,
|
|
4102
|
+
sbn: number,
|
|
4103
|
+
): Uint8Array | null {
|
|
4066
4104
|
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
4067
4105
|
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey)
|
|
4068
4106
|
|
|
@@ -4107,6 +4145,97 @@ export class FECManager {
|
|
|
4107
4145
|
return recovered
|
|
4108
4146
|
}
|
|
4109
4147
|
|
|
4148
|
+
/**
|
|
4149
|
+
* Feed a source symbol of this stream that another FEC instance over the
|
|
4150
|
+
* same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
|
|
4151
|
+
* caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
|
|
4152
|
+
* BLO-52085). It joins its block's decoder as a received source would, so
|
|
4153
|
+
* it reduces the erasures the block still has to solve, but it is not a
|
|
4154
|
+
* network arrival: no transport accounting, no classification evidence,
|
|
4155
|
+
* no deadline refresh, and it never makes a block clean.
|
|
4156
|
+
*
|
|
4157
|
+
* Only a block a repair stated takes one (a repair-less span has no
|
|
4158
|
+
* decoder block). A block that is terminal, or already holds that source,
|
|
4159
|
+
* does not. The symbol is the exact protected packet, unpadded, as
|
|
4160
|
+
* feedSourceSymbol takes it.
|
|
4161
|
+
*
|
|
4162
|
+
* `recovered` is the block the synchronous decoder completed with it; the
|
|
4163
|
+
* caller routes it and completes the block, as for any recovery. When
|
|
4164
|
+
* every source of the block is then present, received or rebuilt, and no
|
|
4165
|
+
* decoder returned it (a Worker answers asynchronously, and reports such a
|
|
4166
|
+
* block as clean on its own side), the block is completed here as
|
|
4167
|
+
* recovered, with nothing left to route.
|
|
4168
|
+
*/
|
|
4169
|
+
feedRebuiltSourceSymbol(
|
|
4170
|
+
packetId: number,
|
|
4171
|
+
ssId: number,
|
|
4172
|
+
symbolData: Uint8Array,
|
|
4173
|
+
catalogTrackName?: string,
|
|
4174
|
+
): RebuiltSourceFeed {
|
|
4175
|
+
if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN
|
|
4176
|
+
const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId)
|
|
4177
|
+
if (located === null || located.block.span) return NOT_TAKEN
|
|
4178
|
+
if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN
|
|
4179
|
+
const sbn = located.block.start
|
|
4180
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
4181
|
+
if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN
|
|
4182
|
+
let rebuilt = this.#blockRebuiltEsis.get(key)
|
|
4183
|
+
if (rebuilt?.has(located.esi)) return NOT_TAKEN
|
|
4184
|
+
if (rebuilt === undefined) {
|
|
4185
|
+
rebuilt = new Set()
|
|
4186
|
+
this.#blockRebuiltEsis.set(key, rebuilt)
|
|
4187
|
+
}
|
|
4188
|
+
const config = this.#trackConfigs.get(packetId)
|
|
4189
|
+
const wasShort = config !== undefined &&
|
|
4190
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== undefined
|
|
4191
|
+
rebuilt.add(located.esi)
|
|
4192
|
+
const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn)
|
|
4193
|
+
if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
|
|
4194
|
+
// The late-source cut assumed only a late source could complete this
|
|
4195
|
+
// block; with this source its group can, so it gets a full multicast
|
|
4196
|
+
// wait again, counted from now.
|
|
4197
|
+
// ponytail: from now, not from its sources, so it can close up to one
|
|
4198
|
+
// multicast wait after its uncut deadline; at most once per block, since
|
|
4199
|
+
// a rebuilt source only adds to what the block holds and the cut cannot
|
|
4200
|
+
// apply to it again. Keep the uncut deadline and re-arm to it if a lab
|
|
4201
|
+
// run shows that wait.
|
|
4202
|
+
if (wasShort && this.#activeBlocks.has(key) &&
|
|
4203
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config!) === undefined) {
|
|
4204
|
+
this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config!))
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
return { taken: true, recovered }
|
|
4208
|
+
}
|
|
4209
|
+
|
|
4210
|
+
/**
|
|
4211
|
+
* A block that holds every one of its `length` sources, received or
|
|
4212
|
+
* rebuilt by another instance, with at least one rebuilt, is recovered:
|
|
4213
|
+
* every packet of it was delivered, by its own arrival or by the instance
|
|
4214
|
+
* that rebuilt it, so nothing is routed. Returns whether it completed it.
|
|
4215
|
+
*/
|
|
4216
|
+
#completeWhenSourcesPresent(
|
|
4217
|
+
key: string,
|
|
4218
|
+
sbn: number,
|
|
4219
|
+
length: number,
|
|
4220
|
+
packetId: number,
|
|
4221
|
+
catalogTrackName: string | undefined,
|
|
4222
|
+
): boolean {
|
|
4223
|
+
const rebuilt = this.#blockRebuiltEsis.get(key)
|
|
4224
|
+
if (rebuilt === undefined || this.#completedBlocks.has(key)) return false
|
|
4225
|
+
let present = rebuilt.size
|
|
4226
|
+
for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++
|
|
4227
|
+
if (present < length) return false
|
|
4228
|
+
this.#traceLifecycle('recovered', sbn, packetId, catalogTrackName)
|
|
4229
|
+
this.countRecovery(packetId, sbn, catalogTrackName)
|
|
4230
|
+
this.cancelBlockTimeout(sbn, packetId, catalogTrackName)
|
|
4231
|
+
if (catalogTrackName === undefined) {
|
|
4232
|
+
this.onBlockComplete?.(sbn, packetId)
|
|
4233
|
+
} else {
|
|
4234
|
+
this.onBlockComplete?.(sbn, packetId, catalogTrackName)
|
|
4235
|
+
}
|
|
4236
|
+
return true
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4110
4239
|
/**
|
|
4111
4240
|
* fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
|
|
4112
4241
|
* exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
|
|
@@ -4364,9 +4493,100 @@ export class FECManager {
|
|
|
4364
4493
|
this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName)
|
|
4365
4494
|
return null
|
|
4366
4495
|
}
|
|
4496
|
+
// Its K' is known now: a block this repair shows short of it waits less.
|
|
4497
|
+
if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName)
|
|
4367
4498
|
return recovered
|
|
4368
4499
|
}
|
|
4369
4500
|
|
|
4501
|
+
/**
|
|
4502
|
+
* A first-arrival multicast source, held or placed. A datagram flow keeps
|
|
4503
|
+
* its sources in send order, so one that moves the stream's newest source
|
|
4504
|
+
* position past a block leaves the sources that block still misses
|
|
4505
|
+
* reordered behind it at best: it shortens the wait of every block it
|
|
4506
|
+
* passed that only a late source can complete. MoQ is left out: its streams
|
|
4507
|
+
* can overtake one another, so a MoQ source shows nothing about the sources
|
|
4508
|
+
* sent before it.
|
|
4509
|
+
*/
|
|
4510
|
+
#noteMulticastSource(
|
|
4511
|
+
locator: IsoBlockLocator<IsoHeldSource>,
|
|
4512
|
+
ssId: number,
|
|
4513
|
+
packetId: number,
|
|
4514
|
+
catalogTrackName: string | undefined,
|
|
4515
|
+
): void {
|
|
4516
|
+
const previous = locator.newestSource
|
|
4517
|
+
if (!locator.noteSource(ssId) || previous === undefined) return
|
|
4518
|
+
// The blocks whose last SS_ID lies in [previous, ssId): this source
|
|
4519
|
+
// passed them and the previous newest one had not.
|
|
4520
|
+
for (const block of locator.blocksBetween((previous - 1) >>> 0, ssId)) {
|
|
4521
|
+
if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue
|
|
4522
|
+
this.#shortenUnrecoverableWait(
|
|
4523
|
+
this.#blockKey(block.start, packetId, catalogTrackName),
|
|
4524
|
+
block.start,
|
|
4525
|
+
packetId,
|
|
4526
|
+
catalogTrackName,
|
|
4527
|
+
)
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
/**
|
|
4532
|
+
* The wait of a block that only a late source can complete (BLO-52085): a
|
|
4533
|
+
* repair stated it, its received sources plus every symbol its repair group
|
|
4534
|
+
* carries (RSB_length) are short of K' (the repair scheduler's
|
|
4535
|
+
* group-cannot-complete), and a later source of the stream has arrived, so
|
|
4536
|
+
* a source it misses is lost or reordered behind that one. The span, which
|
|
4537
|
+
* covers repair symbols paced after its sources, buys it nothing: it waits
|
|
4538
|
+
* the late-symbol allowance alone, the multicast wait less the span (the
|
|
4539
|
+
* catalog tolerance or the measured delivery gap). Undefined for any other
|
|
4540
|
+
* block.
|
|
4541
|
+
*/
|
|
4542
|
+
#lateSourceWaitMs(
|
|
4543
|
+
key: string,
|
|
4544
|
+
sbn: number,
|
|
4545
|
+
packetId: number,
|
|
4546
|
+
catalogTrackName: string | undefined,
|
|
4547
|
+
config: ResolvedFECTrackConfig,
|
|
4548
|
+
): number | undefined {
|
|
4549
|
+
const authoredLength = this.#blockAuthoredLengths.get(key)
|
|
4550
|
+
const rsbLength = this.#blockRsbLengths.get(key)
|
|
4551
|
+
if (authoredLength === undefined || rsbLength === undefined) return undefined
|
|
4552
|
+
// A source another instance rebuilt (feedRebuiltSourceSymbol) is one the
|
|
4553
|
+
// decoder holds: the group need only cover what neither delivered.
|
|
4554
|
+
let present = this.#blockSourceCounts.get(key) ?? 0
|
|
4555
|
+
const received = this.#blockSourceEsis.get(key)
|
|
4556
|
+
for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++
|
|
4557
|
+
if (present + rsbLength >= authoredLength) return undefined
|
|
4558
|
+
const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource
|
|
4559
|
+
if (newestSource === undefined || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
|
|
4560
|
+
return undefined
|
|
4561
|
+
}
|
|
4562
|
+
return this.#multicastWaitMs(config) - config.interleaveDepthMs
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4565
|
+
/**
|
|
4566
|
+
* Bring an open block's wait in to its late-source wait, when it has one.
|
|
4567
|
+
* Never later than it is armed: only a source can complete such a block,
|
|
4568
|
+
* so a repair of its group (paced across the span) must not keep it open.
|
|
4569
|
+
*/
|
|
4570
|
+
#shortenUnrecoverableWait(
|
|
4571
|
+
key: string,
|
|
4572
|
+
sbn: number,
|
|
4573
|
+
packetId: number,
|
|
4574
|
+
catalogTrackName: string | undefined,
|
|
4575
|
+
): void {
|
|
4576
|
+
if (!this.#activeBlocks.has(key)) return
|
|
4577
|
+
const config = this.#trackConfigs.get(packetId)
|
|
4578
|
+
if (config === undefined) return
|
|
4579
|
+
const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config)
|
|
4580
|
+
if (waitMs === undefined) return
|
|
4581
|
+
const armedAtMs = this.#blockWaitDeadlinesMs.get(key)
|
|
4582
|
+
this.#armBlockTimeout(
|
|
4583
|
+
sbn,
|
|
4584
|
+
packetId,
|
|
4585
|
+
catalogTrackName,
|
|
4586
|
+
armedAtMs === undefined ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow()),
|
|
4587
|
+
)
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4370
4590
|
/**
|
|
4371
4591
|
* Record a block's authored SSB_length from a repair symbol. Returns false,
|
|
4372
4592
|
* leaving the block unchanged, when the symbol contradicts the block: a
|
|
@@ -4582,6 +4802,20 @@ export class FECManager {
|
|
|
4582
4802
|
this.#completeCleanBlock(key, sbn, packetId, catalogTrackName)
|
|
4583
4803
|
return
|
|
4584
4804
|
}
|
|
4805
|
+
// A late arrival can be the last source a block lacked besides those
|
|
4806
|
+
// another instance rebuilt (feedRebuiltSourceSymbol).
|
|
4807
|
+
if (
|
|
4808
|
+
this.#blockRebuiltEsis.has(key) &&
|
|
4809
|
+
this.#completeWhenSourcesPresent(
|
|
4810
|
+
key,
|
|
4811
|
+
sbn,
|
|
4812
|
+
this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
|
|
4813
|
+
packetId,
|
|
4814
|
+
catalogTrackName,
|
|
4815
|
+
)
|
|
4816
|
+
) {
|
|
4817
|
+
return
|
|
4818
|
+
}
|
|
4585
4819
|
|
|
4586
4820
|
this.noteBlockActivity(sbn, packetId, catalogTrackName, true)
|
|
4587
4821
|
|
|
@@ -4601,6 +4835,7 @@ export class FECManager {
|
|
|
4601
4835
|
this.cancelBlockTimeout(sbn, packetId, catalogTrackName)
|
|
4602
4836
|
this.#blockSourceCounts.delete(key)
|
|
4603
4837
|
this.#blockSourceEsis.delete(key)
|
|
4838
|
+
this.#blockRebuiltEsis.delete(key)
|
|
4604
4839
|
this.#blockAuthoredLengths.delete(key)
|
|
4605
4840
|
this.#blockRepairRsIds.delete(key)
|
|
4606
4841
|
this.#blockRsbLengths.delete(key)
|
|
@@ -4714,7 +4949,18 @@ export class FECManager {
|
|
|
4714
4949
|
)
|
|
4715
4950
|
}
|
|
4716
4951
|
|
|
4717
|
-
|
|
4952
|
+
// A block only a late source can complete waits the late-symbol
|
|
4953
|
+
// allowance from its own sources' activity; nothing else moves it later.
|
|
4954
|
+
if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== undefined) {
|
|
4955
|
+
this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName)
|
|
4956
|
+
return
|
|
4957
|
+
}
|
|
4958
|
+
this.#armBlockTimeout(
|
|
4959
|
+
sbn,
|
|
4960
|
+
packetId,
|
|
4961
|
+
catalogTrackName,
|
|
4962
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs,
|
|
4963
|
+
)
|
|
4718
4964
|
}
|
|
4719
4965
|
|
|
4720
4966
|
#failBlock(
|
|
@@ -4732,6 +4978,7 @@ export class FECManager {
|
|
|
4732
4978
|
}
|
|
4733
4979
|
this.#blockSourceCounts.delete(key)
|
|
4734
4980
|
this.#blockSourceEsis.delete(key)
|
|
4981
|
+
this.#blockRebuiltEsis.delete(key)
|
|
4735
4982
|
this.#blockAuthoredLengths.delete(key)
|
|
4736
4983
|
this.#blockRepairRsIds.delete(key)
|
|
4737
4984
|
this.#blockRsbLengths.delete(key)
|
|
@@ -4771,6 +5018,7 @@ export class FECManager {
|
|
|
4771
5018
|
this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName)
|
|
4772
5019
|
this.#blockSourceCounts.delete(key)
|
|
4773
5020
|
this.#blockSourceEsis.delete(key)
|
|
5021
|
+
this.#blockRebuiltEsis.delete(key)
|
|
4774
5022
|
this.#blockAuthoredLengths.delete(key)
|
|
4775
5023
|
this.#blockRepairRsIds.delete(key)
|
|
4776
5024
|
this.#blockRsbLengths.delete(key)
|
|
@@ -4879,6 +5127,7 @@ export class FECManager {
|
|
|
4879
5127
|
this.#blockKeysBySbn.clear()
|
|
4880
5128
|
this.#blockSourceCounts.clear()
|
|
4881
5129
|
this.#blockSourceEsis.clear()
|
|
5130
|
+
this.#blockRebuiltEsis.clear()
|
|
4882
5131
|
this.#blockAuthoredLengths.clear()
|
|
4883
5132
|
this.#blockRepairRsIds.clear()
|
|
4884
5133
|
this.#blockRsbLengths.clear()
|
package/src/index.ts
CHANGED
package/src/iso-block-locator.ts
CHANGED
|
@@ -85,6 +85,7 @@ export class IsoBlockLocator<T> {
|
|
|
85
85
|
#retentionBlocks: number
|
|
86
86
|
#heldEvicted = 0
|
|
87
87
|
#newest: number | undefined
|
|
88
|
+
#newestSource: number | undefined
|
|
88
89
|
/** Pending holes (see `deferIncompleteGaps`): unstated, sources still held. */
|
|
89
90
|
readonly #holes: IsoBlockRange[] = []
|
|
90
91
|
/**
|
|
@@ -152,6 +153,23 @@ export class IsoBlockLocator<T> {
|
|
|
152
153
|
if (this.#newest === undefined || serial32Precedes(this.#newest, position)) this.#newest = position
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
/**
|
|
157
|
+
* The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
|
|
158
|
+
* `newest`, a repair never moves it: a repair can run ahead of its sources,
|
|
159
|
+
* a source cannot run ahead of the sources sent before it except by
|
|
160
|
+
* reordering.
|
|
161
|
+
*/
|
|
162
|
+
get newestSource(): number | undefined {
|
|
163
|
+
return this.#newestSource
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Note a received source's SS_ID; true when it is the newest one yet. */
|
|
167
|
+
noteSource(ssId: number): boolean {
|
|
168
|
+
if (this.#newestSource !== undefined && !serial32Precedes(this.#newestSource, ssId)) return false
|
|
169
|
+
this.#newestSource = ssId
|
|
170
|
+
return true
|
|
171
|
+
}
|
|
172
|
+
|
|
155
173
|
/** Held sources evicted by `heldCapacity` (never by the horizon itself). */
|
|
156
174
|
get heldEvicted(): number {
|
|
157
175
|
return this.#heldEvicted
|
|
@@ -412,6 +430,7 @@ export class IsoBlockLocator<T> {
|
|
|
412
430
|
this.#held.clear()
|
|
413
431
|
this.#coverageEnd = undefined
|
|
414
432
|
this.#newest = undefined
|
|
433
|
+
this.#newestSource = undefined
|
|
415
434
|
}
|
|
416
435
|
|
|
417
436
|
#claim(block: IsoBlockRange): IsoPlacedSource<T>[] {
|
package/src/iso-bmff-timing.ts
CHANGED
|
@@ -581,6 +581,14 @@ type PendingMovieFragment = Array<IsoBmffSampleTiming | null>
|
|
|
581
581
|
export class IsoBmffTimingResolver {
|
|
582
582
|
#track: IsoBmffTrackTiming | null = null
|
|
583
583
|
#fragments = new Map<string, PendingMovieFragment>()
|
|
584
|
+
/** First sample of each pending fragment, kept in `#delivered` with its sample count. */
|
|
585
|
+
#firstSamples = new Map<string, IsoBmffSampleTiming>()
|
|
586
|
+
/**
|
|
587
|
+
* First sample and sample count of the newest fragments whose every slot was
|
|
588
|
+
* cleared. Abandonment counts: `releaseSample` clears a slot too, which
|
|
589
|
+
* matches the combiner, since it retires an abandoned sample as well.
|
|
590
|
+
*/
|
|
591
|
+
#delivered = new Map<string, { first: IsoBmffSampleTiming, sampleCount: number }>()
|
|
584
592
|
#pendingSamples = 0
|
|
585
593
|
#evictedSamples = 0
|
|
586
594
|
#lastMovieFragmentRejection: IsoBmffMovieFragmentRejection | null = null
|
|
@@ -591,6 +599,11 @@ export class IsoBmffTimingResolver {
|
|
|
591
599
|
}
|
|
592
600
|
}
|
|
593
601
|
|
|
602
|
+
/** True once an FT=0 has bound the track that FT=1 timing is read against. */
|
|
603
|
+
get initialized(): boolean {
|
|
604
|
+
return this.#track !== null
|
|
605
|
+
}
|
|
606
|
+
|
|
594
607
|
get pendingSampleCount(): number {
|
|
595
608
|
return this.#pendingSamples
|
|
596
609
|
}
|
|
@@ -618,10 +631,18 @@ export class IsoBmffTimingResolver {
|
|
|
618
631
|
return true
|
|
619
632
|
}
|
|
620
633
|
|
|
634
|
+
/**
|
|
635
|
+
* `recovered` marks an FEC-recovered copy. A recovered block hands back
|
|
636
|
+
* every FT=1 it holds, the received ones included, so a recovered copy of a
|
|
637
|
+
* fragment whose samples are all done is a duplicate: admitting it again
|
|
638
|
+
* would charge stale timing and could displace the live MPU's. A source
|
|
639
|
+
* copy is still admitted again (a rendition switch re-arms timing that way).
|
|
640
|
+
*/
|
|
621
641
|
addMovieFragment(
|
|
622
642
|
mpuSequenceNumber: number,
|
|
623
643
|
data: Uint8Array,
|
|
624
644
|
additionalSampleLimit = this.maxPendingSamples,
|
|
645
|
+
recovered = false,
|
|
625
646
|
): boolean {
|
|
626
647
|
this.#lastMovieFragmentRejection = null
|
|
627
648
|
if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
|
|
@@ -648,6 +669,9 @@ export class IsoBmffTimingResolver {
|
|
|
648
669
|
(sample, index) => sample !== null && sameSample(sample, parsed.samples[index]!),
|
|
649
670
|
)
|
|
650
671
|
}
|
|
672
|
+
const delivered = recovered ? this.#delivered.get(key) : undefined
|
|
673
|
+
if (delivered && delivered.sampleCount === parsed.samples.length &&
|
|
674
|
+
sameSample(delivered.first, parsed.samples[0]!)) return true
|
|
651
675
|
// Sliding window, not a one-way ratchet. `consumeSample` and
|
|
652
676
|
// `releaseSample` between them cover every sample some component observed
|
|
653
677
|
// -- but a sample whose data units never arrive at all is observed by
|
|
@@ -683,6 +707,7 @@ export class IsoBmffTimingResolver {
|
|
|
683
707
|
return false
|
|
684
708
|
}
|
|
685
709
|
this.#fragments.set(key, [...parsed.samples])
|
|
710
|
+
this.#firstSamples.set(key, parsed.samples[0]!)
|
|
686
711
|
this.#pendingSamples += parsed.samples.length
|
|
687
712
|
return true
|
|
688
713
|
}
|
|
@@ -758,6 +783,7 @@ export class IsoBmffTimingResolver {
|
|
|
758
783
|
let live = 0
|
|
759
784
|
for (const sample of samples) if (sample !== null) live++
|
|
760
785
|
this.#fragments.delete(oldest)
|
|
786
|
+
this.#firstSamples.delete(oldest)
|
|
761
787
|
this.#pendingSamples -= live
|
|
762
788
|
this.#evictedSamples += live
|
|
763
789
|
return live
|
|
@@ -779,13 +805,26 @@ export class IsoBmffTimingResolver {
|
|
|
779
805
|
if (!samples || !samples[index]) return false
|
|
780
806
|
samples[index] = null
|
|
781
807
|
this.#pendingSamples--
|
|
782
|
-
if (samples.every(value => value === null))
|
|
808
|
+
if (samples.every(value => value === null)) {
|
|
809
|
+
this.#fragments.delete(key)
|
|
810
|
+
const first = this.#firstSamples.get(key)!
|
|
811
|
+
this.#firstSamples.delete(key)
|
|
812
|
+
this.#delivered.delete(key)
|
|
813
|
+
this.#delivered.set(key, { first, sampleCount: samples.length })
|
|
814
|
+
// ponytail: the dedupe memory is as wide as the timing budget, in fragments;
|
|
815
|
+
// a recovered copy older than that is admitted again, as before.
|
|
816
|
+
if (this.#delivered.size > this.maxPendingSamples) {
|
|
817
|
+
this.#delivered.delete(this.#delivered.keys().next().value!)
|
|
818
|
+
}
|
|
819
|
+
}
|
|
783
820
|
return true
|
|
784
821
|
}
|
|
785
822
|
|
|
786
823
|
reset(): void {
|
|
787
824
|
this.#track = null
|
|
788
825
|
this.#fragments.clear()
|
|
826
|
+
this.#firstSamples.clear()
|
|
827
|
+
this.#delivered.clear()
|
|
789
828
|
this.#pendingSamples = 0
|
|
790
829
|
}
|
|
791
830
|
}
|