@blockcast/mmt-container 2.1.0 → 2.2.0-main.079cd7664c0b
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 +1271 -247
- 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 +33 -1
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +213 -6
- 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 +5 -4
- 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 +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/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 +37 -2
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +98 -28
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +222 -45
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +97 -28
- 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 +25 -2
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +1718 -54
- 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 +15 -2
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +15652 -14939
- 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 +215 -42
- package/dist/no-modules.js.map +3 -3
- package/dist/repair-fetch.js.map +1 -1
- 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.js.map +1 -1
- package/dist/wasm-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +97 -28
- 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 +256 -6
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +18 -2
- package/src/iso-block-locator.ts +19 -0
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +96 -8
- package/src/mmtp-fec-session.ts +115 -15
- package/src/mmtp-router.ts +344 -27
- package/src/multicast-manager.ts +26 -5
- package/src/multicast-transport.ts +44 -20
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +1 -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
|
|
@@ -644,6 +660,10 @@ export class FECManager {
|
|
|
644
660
|
// ── Keyframe-protected repair policy (block-policy.ts) ──
|
|
645
661
|
// One classifier per physical stream, built from that stream's geometry.
|
|
646
662
|
#classifiers = new Map<string, BlockClassifier>()
|
|
663
|
+
// A stream without catalog timing has no classifier, so the last RAP it
|
|
664
|
+
// released stands in for one: "an MPU boundary bounds its RAPs" holds until
|
|
665
|
+
// a second RAP is released inside one MPU (BLO-52085).
|
|
666
|
+
#noSpanRaps = new Map<string, { mpu: number; presentationTime: bigint; inside: boolean }>()
|
|
647
667
|
// Geometry a stream-scoped configuration gave one stream (streamKey). ABR
|
|
648
668
|
// rungs can share a packetId with a different framerate or GOP, so the
|
|
649
669
|
// packet-level geometry is only the geometry of a packet whose streams
|
|
@@ -1706,6 +1726,7 @@ export class FECManager {
|
|
|
1706
1726
|
// The classifier compares authored order with SBN order only within
|
|
1707
1727
|
// one continuous SBN sequence.
|
|
1708
1728
|
this.#classifiers.get(streamKey)?.noteSourceDiscontinuity()
|
|
1729
|
+
this.#noSpanRaps.delete(streamKey)
|
|
1709
1730
|
this.#highestSbn.set(streamKey, { sbn, atMs: now })
|
|
1710
1731
|
this.#sourceFrontiers.set(streamKey, { sbn, atMs: now })
|
|
1711
1732
|
if (psn === undefined) this.#sourceAnchors.delete(streamKey)
|
|
@@ -2289,6 +2310,9 @@ export class FECManager {
|
|
|
2289
2310
|
// sibling's change, but the first stream-scoped geometry on a packet
|
|
2290
2311
|
// does change the siblings that have none (stream-geometry-absent).
|
|
2291
2312
|
this.#bumpEpoch(packetId)
|
|
2313
|
+
for (const key of [...this.#noSpanRaps.keys()]) {
|
|
2314
|
+
if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key)
|
|
2315
|
+
}
|
|
2292
2316
|
for (const [key, classifier] of [...this.#classifiers]) {
|
|
2293
2317
|
if (!this.#belongsToPacket(key, packetId)) continue
|
|
2294
2318
|
const stream = this.#parseStreamKey(key)
|
|
@@ -2532,6 +2556,7 @@ export class FECManager {
|
|
|
2532
2556
|
}
|
|
2533
2557
|
resetMap(this.#blockSourceCounts)
|
|
2534
2558
|
resetMap(this.#blockSourceEsis)
|
|
2559
|
+
resetMap(this.#blockRebuiltEsis)
|
|
2535
2560
|
resetMap(this.#blockAuthoredLengths)
|
|
2536
2561
|
resetMap(this.#blockRepairRsIds)
|
|
2537
2562
|
resetMap(this.#blockRsbLengths)
|
|
@@ -2565,6 +2590,7 @@ export class FECManager {
|
|
|
2565
2590
|
resetMap(this.#isoLocators)
|
|
2566
2591
|
resetMap(this.#sourceAnchors)
|
|
2567
2592
|
this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName))
|
|
2593
|
+
this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName))
|
|
2568
2594
|
this.#bumpEpoch(packetId)
|
|
2569
2595
|
for (const [sbn, keys] of this.#blockKeysBySbn) {
|
|
2570
2596
|
for (const key of keys.keys()) {
|
|
@@ -2663,6 +2689,7 @@ export class FECManager {
|
|
|
2663
2689
|
dropMap(this.#pendingWorkerCleanupClients)
|
|
2664
2690
|
dropMap(this.#blockSourceCounts)
|
|
2665
2691
|
dropMap(this.#blockSourceEsis)
|
|
2692
|
+
dropMap(this.#blockRebuiltEsis)
|
|
2666
2693
|
dropMap(this.#blockAuthoredLengths)
|
|
2667
2694
|
dropMap(this.#blockRepairRsIds)
|
|
2668
2695
|
dropMap(this.#blockRsbLengths)
|
|
@@ -2679,6 +2706,7 @@ export class FECManager {
|
|
|
2679
2706
|
dropMap(this.#isoLocators)
|
|
2680
2707
|
dropMap(this.#sourceAnchors)
|
|
2681
2708
|
dropMap(this.#classifiers)
|
|
2709
|
+
dropMap(this.#noSpanRaps)
|
|
2682
2710
|
dropMap(this.#streamGeometries)
|
|
2683
2711
|
dropSet(this.#pendingWorkerCleanups)
|
|
2684
2712
|
dropSet(this.#completedBlocks)
|
|
@@ -2881,6 +2909,10 @@ export class FECManager {
|
|
|
2881
2909
|
}
|
|
2882
2910
|
}
|
|
2883
2911
|
|
|
2912
|
+
if (isSourceSymbol && source === 'multicast') {
|
|
2913
|
+
this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), (sbn + esi) >>> 0, packetId, catalogTrackName)
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2884
2916
|
return true
|
|
2885
2917
|
}
|
|
2886
2918
|
|
|
@@ -3253,6 +3285,7 @@ export class FECManager {
|
|
|
3253
3285
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName)
|
|
3254
3286
|
const evictedBefore = locator.heldEvicted
|
|
3255
3287
|
const held = locator.hold(ssId, source)
|
|
3288
|
+
if (held && source.source === 'multicast') this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName)
|
|
3256
3289
|
const evicted = locator.heldEvicted - evictedBefore
|
|
3257
3290
|
if (evicted > 0) {
|
|
3258
3291
|
const before = this.#isoHeldEvictedCount
|
|
@@ -3470,11 +3503,14 @@ export class FECManager {
|
|
|
3470
3503
|
/**
|
|
3471
3504
|
* Whether an MPU boundary still bounds this stream's RAPs (see
|
|
3472
3505
|
* `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
|
|
3473
|
-
* it does not. A stream with no classifier yet has shown nothing against it
|
|
3506
|
+
* it does not. A stream with no classifier yet has shown nothing against it,
|
|
3507
|
+
* and neither has a stream without catalog timing until it releases two
|
|
3508
|
+
* RAPs inside one MPU.
|
|
3474
3509
|
*/
|
|
3475
3510
|
mpuBoundsRapsFor(packetId: number, catalogTrackName?: string): boolean {
|
|
3476
|
-
|
|
3477
|
-
return this.#
|
|
3511
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
3512
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true
|
|
3513
|
+
return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true
|
|
3478
3514
|
}
|
|
3479
3515
|
|
|
3480
3516
|
/** A block's class now, or frozen at its resolution. */
|
|
@@ -3510,7 +3546,15 @@ export class FECManager {
|
|
|
3510
3546
|
presentationTime: bigint,
|
|
3511
3547
|
mediaTimescale: number,
|
|
3512
3548
|
): void {
|
|
3513
|
-
if (!this.#trackConfigs.has(packetId)
|
|
3549
|
+
if (!this.#trackConfigs.has(packetId)) return
|
|
3550
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) {
|
|
3551
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
3552
|
+
const last = this.#noSpanRaps.get(streamKey)
|
|
3553
|
+
const inside = last?.inside === true ||
|
|
3554
|
+
(last?.mpu === mpu && last.presentationTime !== presentationTime)
|
|
3555
|
+
this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside })
|
|
3556
|
+
return
|
|
3557
|
+
}
|
|
3514
3558
|
this.#applyClassifierChanges(
|
|
3515
3559
|
packetId,
|
|
3516
3560
|
catalogTrackName,
|
|
@@ -4041,7 +4085,22 @@ export class FECManager {
|
|
|
4041
4085
|
this.#isoSourceOversizeCount++
|
|
4042
4086
|
return null
|
|
4043
4087
|
}
|
|
4044
|
-
|
|
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 {
|
|
4045
4104
|
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
4046
4105
|
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey)
|
|
4047
4106
|
|
|
@@ -4086,6 +4145,81 @@ export class FECManager {
|
|
|
4086
4145
|
return recovered
|
|
4087
4146
|
}
|
|
4088
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
|
+
rebuilt.add(located.esi)
|
|
4189
|
+
const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn)
|
|
4190
|
+
if (recovered === null) this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)
|
|
4191
|
+
return { taken: true, recovered }
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
/**
|
|
4195
|
+
* A block that holds every one of its `length` sources, received or
|
|
4196
|
+
* rebuilt by another instance, with at least one rebuilt, is recovered:
|
|
4197
|
+
* every packet of it was delivered, by its own arrival or by the instance
|
|
4198
|
+
* that rebuilt it, so nothing is routed. Returns whether it completed it.
|
|
4199
|
+
*/
|
|
4200
|
+
#completeWhenSourcesPresent(
|
|
4201
|
+
key: string,
|
|
4202
|
+
sbn: number,
|
|
4203
|
+
length: number,
|
|
4204
|
+
packetId: number,
|
|
4205
|
+
catalogTrackName: string | undefined,
|
|
4206
|
+
): boolean {
|
|
4207
|
+
const rebuilt = this.#blockRebuiltEsis.get(key)
|
|
4208
|
+
if (rebuilt === undefined || this.#completedBlocks.has(key)) return false
|
|
4209
|
+
let present = rebuilt.size
|
|
4210
|
+
for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++
|
|
4211
|
+
if (present < length) return false
|
|
4212
|
+
this.#traceLifecycle('recovered', sbn, packetId, catalogTrackName)
|
|
4213
|
+
this.countRecovery(packetId, sbn, catalogTrackName)
|
|
4214
|
+
this.cancelBlockTimeout(sbn, packetId, catalogTrackName)
|
|
4215
|
+
if (catalogTrackName === undefined) {
|
|
4216
|
+
this.onBlockComplete?.(sbn, packetId)
|
|
4217
|
+
} else {
|
|
4218
|
+
this.onBlockComplete?.(sbn, packetId, catalogTrackName)
|
|
4219
|
+
}
|
|
4220
|
+
return true
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4089
4223
|
/**
|
|
4090
4224
|
* fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
|
|
4091
4225
|
* exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
|
|
@@ -4343,9 +4477,95 @@ export class FECManager {
|
|
|
4343
4477
|
this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName)
|
|
4344
4478
|
return null
|
|
4345
4479
|
}
|
|
4480
|
+
// Its K' is known now: a block this repair shows short of it waits less.
|
|
4481
|
+
if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName)
|
|
4346
4482
|
return recovered
|
|
4347
4483
|
}
|
|
4348
4484
|
|
|
4485
|
+
/**
|
|
4486
|
+
* A first-arrival multicast source, held or placed. A datagram flow keeps
|
|
4487
|
+
* its sources in send order, so one that moves the stream's newest source
|
|
4488
|
+
* position past a block leaves the sources that block still misses
|
|
4489
|
+
* reordered behind it at best: it shortens the wait of every block it
|
|
4490
|
+
* passed that only a late source can complete. MoQ is left out: its streams
|
|
4491
|
+
* can overtake one another, so a MoQ source shows nothing about the sources
|
|
4492
|
+
* sent before it.
|
|
4493
|
+
*/
|
|
4494
|
+
#noteMulticastSource(
|
|
4495
|
+
locator: IsoBlockLocator<IsoHeldSource>,
|
|
4496
|
+
ssId: number,
|
|
4497
|
+
packetId: number,
|
|
4498
|
+
catalogTrackName: string | undefined,
|
|
4499
|
+
): void {
|
|
4500
|
+
const previous = locator.newestSource
|
|
4501
|
+
if (!locator.noteSource(ssId) || previous === undefined) return
|
|
4502
|
+
// The blocks whose last SS_ID lies in [previous, ssId): this source
|
|
4503
|
+
// passed them and the previous newest one had not.
|
|
4504
|
+
for (const block of locator.blocksBetween((previous - 1) >>> 0, ssId)) {
|
|
4505
|
+
if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue
|
|
4506
|
+
this.#shortenUnrecoverableWait(
|
|
4507
|
+
this.#blockKey(block.start, packetId, catalogTrackName),
|
|
4508
|
+
block.start,
|
|
4509
|
+
packetId,
|
|
4510
|
+
catalogTrackName,
|
|
4511
|
+
)
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
|
|
4515
|
+
/**
|
|
4516
|
+
* The wait of a block that only a late source can complete (BLO-52085): a
|
|
4517
|
+
* repair stated it, its received sources plus every symbol its repair group
|
|
4518
|
+
* carries (RSB_length) are short of K' (the repair scheduler's
|
|
4519
|
+
* group-cannot-complete), and a later source of the stream has arrived, so
|
|
4520
|
+
* a source it misses is lost or reordered behind that one. The span, which
|
|
4521
|
+
* covers repair symbols paced after its sources, buys it nothing: it waits
|
|
4522
|
+
* the late-symbol allowance alone, the multicast wait less the span (the
|
|
4523
|
+
* catalog tolerance or the measured delivery gap). Undefined for any other
|
|
4524
|
+
* block.
|
|
4525
|
+
*/
|
|
4526
|
+
#lateSourceWaitMs(
|
|
4527
|
+
key: string,
|
|
4528
|
+
sbn: number,
|
|
4529
|
+
packetId: number,
|
|
4530
|
+
catalogTrackName: string | undefined,
|
|
4531
|
+
config: ResolvedFECTrackConfig,
|
|
4532
|
+
): number | undefined {
|
|
4533
|
+
const authoredLength = this.#blockAuthoredLengths.get(key)
|
|
4534
|
+
const rsbLength = this.#blockRsbLengths.get(key)
|
|
4535
|
+
if (authoredLength === undefined || rsbLength === undefined) return undefined
|
|
4536
|
+
if ((this.#blockSourceCounts.get(key) ?? 0) + rsbLength >= authoredLength) return undefined
|
|
4537
|
+
const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource
|
|
4538
|
+
if (newestSource === undefined || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
|
|
4539
|
+
return undefined
|
|
4540
|
+
}
|
|
4541
|
+
return this.#multicastWaitMs(config) - config.interleaveDepthMs
|
|
4542
|
+
}
|
|
4543
|
+
|
|
4544
|
+
/**
|
|
4545
|
+
* Bring an open block's wait in to its late-source wait, when it has one.
|
|
4546
|
+
* Never later than it is armed: only a source can complete such a block,
|
|
4547
|
+
* so a repair of its group (paced across the span) must not keep it open.
|
|
4548
|
+
*/
|
|
4549
|
+
#shortenUnrecoverableWait(
|
|
4550
|
+
key: string,
|
|
4551
|
+
sbn: number,
|
|
4552
|
+
packetId: number,
|
|
4553
|
+
catalogTrackName: string | undefined,
|
|
4554
|
+
): void {
|
|
4555
|
+
if (!this.#activeBlocks.has(key)) return
|
|
4556
|
+
const config = this.#trackConfigs.get(packetId)
|
|
4557
|
+
if (config === undefined) return
|
|
4558
|
+
const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config)
|
|
4559
|
+
if (waitMs === undefined) return
|
|
4560
|
+
const armedAtMs = this.#blockWaitDeadlinesMs.get(key)
|
|
4561
|
+
this.#armBlockTimeout(
|
|
4562
|
+
sbn,
|
|
4563
|
+
packetId,
|
|
4564
|
+
catalogTrackName,
|
|
4565
|
+
armedAtMs === undefined ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow()),
|
|
4566
|
+
)
|
|
4567
|
+
}
|
|
4568
|
+
|
|
4349
4569
|
/**
|
|
4350
4570
|
* Record a block's authored SSB_length from a repair symbol. Returns false,
|
|
4351
4571
|
* leaving the block unchanged, when the symbol contradicts the block: a
|
|
@@ -4561,6 +4781,20 @@ export class FECManager {
|
|
|
4561
4781
|
this.#completeCleanBlock(key, sbn, packetId, catalogTrackName)
|
|
4562
4782
|
return
|
|
4563
4783
|
}
|
|
4784
|
+
// A late arrival can be the last source a block lacked besides those
|
|
4785
|
+
// another instance rebuilt (feedRebuiltSourceSymbol).
|
|
4786
|
+
if (
|
|
4787
|
+
this.#blockRebuiltEsis.has(key) &&
|
|
4788
|
+
this.#completeWhenSourcesPresent(
|
|
4789
|
+
key,
|
|
4790
|
+
sbn,
|
|
4791
|
+
this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
|
|
4792
|
+
packetId,
|
|
4793
|
+
catalogTrackName,
|
|
4794
|
+
)
|
|
4795
|
+
) {
|
|
4796
|
+
return
|
|
4797
|
+
}
|
|
4564
4798
|
|
|
4565
4799
|
this.noteBlockActivity(sbn, packetId, catalogTrackName, true)
|
|
4566
4800
|
|
|
@@ -4580,6 +4814,7 @@ export class FECManager {
|
|
|
4580
4814
|
this.cancelBlockTimeout(sbn, packetId, catalogTrackName)
|
|
4581
4815
|
this.#blockSourceCounts.delete(key)
|
|
4582
4816
|
this.#blockSourceEsis.delete(key)
|
|
4817
|
+
this.#blockRebuiltEsis.delete(key)
|
|
4583
4818
|
this.#blockAuthoredLengths.delete(key)
|
|
4584
4819
|
this.#blockRepairRsIds.delete(key)
|
|
4585
4820
|
this.#blockRsbLengths.delete(key)
|
|
@@ -4693,7 +4928,18 @@ export class FECManager {
|
|
|
4693
4928
|
)
|
|
4694
4929
|
}
|
|
4695
4930
|
|
|
4696
|
-
|
|
4931
|
+
// A block only a late source can complete waits the late-symbol
|
|
4932
|
+
// allowance from its own sources' activity; nothing else moves it later.
|
|
4933
|
+
if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== undefined) {
|
|
4934
|
+
this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName)
|
|
4935
|
+
return
|
|
4936
|
+
}
|
|
4937
|
+
this.#armBlockTimeout(
|
|
4938
|
+
sbn,
|
|
4939
|
+
packetId,
|
|
4940
|
+
catalogTrackName,
|
|
4941
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs,
|
|
4942
|
+
)
|
|
4697
4943
|
}
|
|
4698
4944
|
|
|
4699
4945
|
#failBlock(
|
|
@@ -4711,6 +4957,7 @@ export class FECManager {
|
|
|
4711
4957
|
}
|
|
4712
4958
|
this.#blockSourceCounts.delete(key)
|
|
4713
4959
|
this.#blockSourceEsis.delete(key)
|
|
4960
|
+
this.#blockRebuiltEsis.delete(key)
|
|
4714
4961
|
this.#blockAuthoredLengths.delete(key)
|
|
4715
4962
|
this.#blockRepairRsIds.delete(key)
|
|
4716
4963
|
this.#blockRsbLengths.delete(key)
|
|
@@ -4750,6 +4997,7 @@ export class FECManager {
|
|
|
4750
4997
|
this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName)
|
|
4751
4998
|
this.#blockSourceCounts.delete(key)
|
|
4752
4999
|
this.#blockSourceEsis.delete(key)
|
|
5000
|
+
this.#blockRebuiltEsis.delete(key)
|
|
4753
5001
|
this.#blockAuthoredLengths.delete(key)
|
|
4754
5002
|
this.#blockRepairRsIds.delete(key)
|
|
4755
5003
|
this.#blockRsbLengths.delete(key)
|
|
@@ -4858,6 +5106,7 @@ export class FECManager {
|
|
|
4858
5106
|
this.#blockKeysBySbn.clear()
|
|
4859
5107
|
this.#blockSourceCounts.clear()
|
|
4860
5108
|
this.#blockSourceEsis.clear()
|
|
5109
|
+
this.#blockRebuiltEsis.clear()
|
|
4861
5110
|
this.#blockAuthoredLengths.clear()
|
|
4862
5111
|
this.#blockRepairRsIds.clear()
|
|
4863
5112
|
this.#blockRsbLengths.clear()
|
|
@@ -4883,6 +5132,7 @@ export class FECManager {
|
|
|
4883
5132
|
this.#blockAnchorsMs.clear()
|
|
4884
5133
|
this.#blockCatalogTrackNames.clear()
|
|
4885
5134
|
this.#classifiers.clear()
|
|
5135
|
+
this.#noSpanRaps.clear()
|
|
4886
5136
|
this.#streamGeometries.clear()
|
|
4887
5137
|
this.#failedBlocks.clear()
|
|
4888
5138
|
this.#resolvedBlockClasses.clear()
|
package/src/fec-worker-setup.ts
CHANGED
|
@@ -64,7 +64,7 @@ export interface FecWorkerSetupOptions {
|
|
|
64
64
|
/**
|
|
65
65
|
* Repair symbols per block this receiver actually joined: the sum of
|
|
66
66
|
* `repairSymbols` over the joined BASE layers (draft-ramadan-moq-fec 6.3).
|
|
67
|
-
* Reported as P by the worker snapshot and CMCD `
|
|
67
|
+
* Reported as P by the worker snapshot and CMCD `com.blockcast-fp`, because the
|
|
68
68
|
* catalog total is not what this receiver joined. Falls back to
|
|
69
69
|
* `fecRepairCount`, which is the same value on a single-layer track.
|
|
70
70
|
*/
|
|
@@ -252,7 +252,7 @@ export async function setupFecWorkerForTrack(
|
|
|
252
252
|
blockProfile,
|
|
253
253
|
k: opts.fecK,
|
|
254
254
|
// P as this receiver joined it, not the catalog total: the worker snapshot
|
|
255
|
-
// and CMCD `
|
|
255
|
+
// and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
|
|
256
256
|
repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
|
|
257
257
|
symbolSize: opts.fecSymbolSize,
|
|
258
258
|
interleaveDepth: interleaveDepthBlocks,
|
package/src/index.ts
CHANGED
|
@@ -299,6 +299,7 @@ export {
|
|
|
299
299
|
type MmtpFeedOutcomeKey,
|
|
300
300
|
type TrackInfo,
|
|
301
301
|
type MmtpReorderStats,
|
|
302
|
+
type MmtpRecoveredHeadTimingStats,
|
|
302
303
|
type MmtpReorderFrameDiagnostics,
|
|
303
304
|
type MmtpReorderStreamDiagnostics,
|
|
304
305
|
type FrameMeta as ParserFrameMeta,
|
|
@@ -526,10 +527,13 @@ export {
|
|
|
526
527
|
type FecStarvationDetectorConfig,
|
|
527
528
|
} from './fec-starvation-detector.js'
|
|
528
529
|
|
|
529
|
-
// CMCD
|
|
530
|
+
// CMCD v2 Event-mode producer (CTA-5004-B, com.blockcast-* custom keys)
|
|
530
531
|
export {
|
|
531
532
|
CmcdProducer,
|
|
532
|
-
|
|
533
|
+
encodeCmcdRecord,
|
|
534
|
+
type CmcdRecord,
|
|
535
|
+
type CmcdToken,
|
|
536
|
+
type CmcdValue,
|
|
533
537
|
type CmcdProducerOptions,
|
|
534
538
|
type SnapshotData,
|
|
535
539
|
type TransportSnapshot,
|
|
@@ -546,6 +550,7 @@ export {
|
|
|
546
550
|
type TrackFecParams,
|
|
547
551
|
type TrackFecUnavailable,
|
|
548
552
|
catalogTrackFecUnavailable,
|
|
553
|
+
catalogTrackDeliveryMode,
|
|
549
554
|
catalogGroupDurationMs,
|
|
550
555
|
catalogKeyframeCadenceMs,
|
|
551
556
|
catalogFecMediaUnitsPerSecond,
|
|
@@ -602,11 +607,22 @@ export {
|
|
|
602
607
|
type MmtpFecSessionConfig,
|
|
603
608
|
type MmtpFecSessionDecoder,
|
|
604
609
|
type MmtpFecSessionDecoderConstructor,
|
|
610
|
+
type MmtpFecSessionMessageConfig,
|
|
611
|
+
type MmtpFecSessionMessageUpdate,
|
|
605
612
|
type MmtpFecSessionMixedPathStats,
|
|
606
613
|
type MmtpFecSessionPath,
|
|
607
614
|
type MmtpFecSessionPathCounts,
|
|
608
615
|
type MmtpFecSessionStats,
|
|
609
616
|
} from "./mmtp-fec-session.js";
|
|
617
|
+
// MoQ repair-layer subscriptions by measured loss (BLO-35456)
|
|
618
|
+
export {
|
|
619
|
+
RepairLayerSubscriptions,
|
|
620
|
+
type MoqRepairDelivery,
|
|
621
|
+
type RepairLayerSource,
|
|
622
|
+
type RepairLayerSubscriptionsOptions,
|
|
623
|
+
type RepairLayerSubscriptionsSnapshot,
|
|
624
|
+
type RepairTrackSubscription,
|
|
625
|
+
} from "./repair-layer-subscriptions.js";
|
|
610
626
|
|
|
611
627
|
export {
|
|
612
628
|
catalogAmtNetworkSource,
|
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
|
}
|