@blockcast/mmt-container 2.2.0-main.73af0eedcbc8 → 2.2.0-main.a16efba1d85a
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 +630 -191
- 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 +60 -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 +3 -1
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +26 -4
- 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.js.map +1 -1
- 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 +35 -2
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +97 -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 +6 -0
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +150 -44
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-auth-catalog.js.map +1 -1
- package/dist/multicast-input.js.map +1 -1
- package/dist/multicast-manager.js +176 -48
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +26 -16
- package/dist/multicast-transport.js.map +2 -2
- 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 +1 -1
- 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 +189 -179
- package/src/fec-manager.ts +26 -4
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +18 -2
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +92 -7
- package/src/mmtp-fec-session.ts +115 -15
- package/src/mmtp-router.ts +46 -0
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +1 -1
package/src/fec-manager.ts
CHANGED
|
@@ -644,6 +644,10 @@ export class FECManager {
|
|
|
644
644
|
// ── Keyframe-protected repair policy (block-policy.ts) ──
|
|
645
645
|
// One classifier per physical stream, built from that stream's geometry.
|
|
646
646
|
#classifiers = new Map<string, BlockClassifier>()
|
|
647
|
+
// A stream without catalog timing has no classifier, so the last RAP it
|
|
648
|
+
// released stands in for one: "an MPU boundary bounds its RAPs" holds until
|
|
649
|
+
// a second RAP is released inside one MPU (BLO-52085).
|
|
650
|
+
#noSpanRaps = new Map<string, { mpu: number; presentationTime: bigint; inside: boolean }>()
|
|
647
651
|
// Geometry a stream-scoped configuration gave one stream (streamKey). ABR
|
|
648
652
|
// rungs can share a packetId with a different framerate or GOP, so the
|
|
649
653
|
// packet-level geometry is only the geometry of a packet whose streams
|
|
@@ -1706,6 +1710,7 @@ export class FECManager {
|
|
|
1706
1710
|
// The classifier compares authored order with SBN order only within
|
|
1707
1711
|
// one continuous SBN sequence.
|
|
1708
1712
|
this.#classifiers.get(streamKey)?.noteSourceDiscontinuity()
|
|
1713
|
+
this.#noSpanRaps.delete(streamKey)
|
|
1709
1714
|
this.#highestSbn.set(streamKey, { sbn, atMs: now })
|
|
1710
1715
|
this.#sourceFrontiers.set(streamKey, { sbn, atMs: now })
|
|
1711
1716
|
if (psn === undefined) this.#sourceAnchors.delete(streamKey)
|
|
@@ -2289,6 +2294,9 @@ export class FECManager {
|
|
|
2289
2294
|
// sibling's change, but the first stream-scoped geometry on a packet
|
|
2290
2295
|
// does change the siblings that have none (stream-geometry-absent).
|
|
2291
2296
|
this.#bumpEpoch(packetId)
|
|
2297
|
+
for (const key of [...this.#noSpanRaps.keys()]) {
|
|
2298
|
+
if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key)
|
|
2299
|
+
}
|
|
2292
2300
|
for (const [key, classifier] of [...this.#classifiers]) {
|
|
2293
2301
|
if (!this.#belongsToPacket(key, packetId)) continue
|
|
2294
2302
|
const stream = this.#parseStreamKey(key)
|
|
@@ -2565,6 +2573,7 @@ export class FECManager {
|
|
|
2565
2573
|
resetMap(this.#isoLocators)
|
|
2566
2574
|
resetMap(this.#sourceAnchors)
|
|
2567
2575
|
this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName))
|
|
2576
|
+
this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName))
|
|
2568
2577
|
this.#bumpEpoch(packetId)
|
|
2569
2578
|
for (const [sbn, keys] of this.#blockKeysBySbn) {
|
|
2570
2579
|
for (const key of keys.keys()) {
|
|
@@ -2679,6 +2688,7 @@ export class FECManager {
|
|
|
2679
2688
|
dropMap(this.#isoLocators)
|
|
2680
2689
|
dropMap(this.#sourceAnchors)
|
|
2681
2690
|
dropMap(this.#classifiers)
|
|
2691
|
+
dropMap(this.#noSpanRaps)
|
|
2682
2692
|
dropMap(this.#streamGeometries)
|
|
2683
2693
|
dropSet(this.#pendingWorkerCleanups)
|
|
2684
2694
|
dropSet(this.#completedBlocks)
|
|
@@ -3470,11 +3480,14 @@ export class FECManager {
|
|
|
3470
3480
|
/**
|
|
3471
3481
|
* Whether an MPU boundary still bounds this stream's RAPs (see
|
|
3472
3482
|
* `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
|
|
3473
|
-
* it does not. A stream with no classifier yet has shown nothing against it
|
|
3483
|
+
* it does not. A stream with no classifier yet has shown nothing against it,
|
|
3484
|
+
* and neither has a stream without catalog timing until it releases two
|
|
3485
|
+
* RAPs inside one MPU.
|
|
3474
3486
|
*/
|
|
3475
3487
|
mpuBoundsRapsFor(packetId: number, catalogTrackName?: string): boolean {
|
|
3476
|
-
|
|
3477
|
-
return this.#
|
|
3488
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
3489
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true
|
|
3490
|
+
return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true
|
|
3478
3491
|
}
|
|
3479
3492
|
|
|
3480
3493
|
/** A block's class now, or frozen at its resolution. */
|
|
@@ -3510,7 +3523,15 @@ export class FECManager {
|
|
|
3510
3523
|
presentationTime: bigint,
|
|
3511
3524
|
mediaTimescale: number,
|
|
3512
3525
|
): void {
|
|
3513
|
-
if (!this.#trackConfigs.has(packetId)
|
|
3526
|
+
if (!this.#trackConfigs.has(packetId)) return
|
|
3527
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) {
|
|
3528
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
3529
|
+
const last = this.#noSpanRaps.get(streamKey)
|
|
3530
|
+
const inside = last?.inside === true ||
|
|
3531
|
+
(last?.mpu === mpu && last.presentationTime !== presentationTime)
|
|
3532
|
+
this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside })
|
|
3533
|
+
return
|
|
3534
|
+
}
|
|
3514
3535
|
this.#applyClassifierChanges(
|
|
3515
3536
|
packetId,
|
|
3516
3537
|
catalogTrackName,
|
|
@@ -4883,6 +4904,7 @@ export class FECManager {
|
|
|
4883
4904
|
this.#blockAnchorsMs.clear()
|
|
4884
4905
|
this.#blockCatalogTrackNames.clear()
|
|
4885
4906
|
this.#classifiers.clear()
|
|
4907
|
+
this.#noSpanRaps.clear()
|
|
4886
4908
|
this.#streamGeometries.clear()
|
|
4887
4909
|
this.#failedBlocks.clear()
|
|
4888
4910
|
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-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
|
}
|
|
@@ -365,10 +365,19 @@ export type MmtpFeedDropReason =
|
|
|
365
365
|
/** No timing resolver, or it refused the initialization segment. */
|
|
366
366
|
| 'init-timing-refused'
|
|
367
367
|
| 'fragment-unexpected-fragmentation'
|
|
368
|
+
/** FT=1 before an FT=0 has bound the stream's timing track. */
|
|
368
369
|
| 'fragment-no-resolver'
|
|
369
370
|
/** addMovieFragment refused — includes the pending-timing-sample budget. */
|
|
370
371
|
| 'fragment-timing-refused'
|
|
371
372
|
| 'unknown-fragment-type'
|
|
373
|
+
/** Stable code, no longer produced: an FT=2 with no timing (before the
|
|
374
|
+
* stream's first FT=0, or after setTrack dropped it) now waits for its
|
|
375
|
+
* timing like one whose FT=1 has not arrived. A head counts as
|
|
376
|
+
* `drop:mfu-sample-timing-missing` (parked) and a FI=2/FI=3 continuation
|
|
377
|
+
* as the `pending` record. A continuation that completes a head the
|
|
378
|
+
* reassembler buffered before the timing was dropped counts as
|
|
379
|
+
* `drop:assembled-timing-missing`. A panel keyed on this code reads zero:
|
|
380
|
+
* a re-attribution, not lost coverage. Re-key on both drop codes. */
|
|
372
381
|
| 'mfu-no-resolver'
|
|
373
382
|
| 'mfu-empty-payload'
|
|
374
383
|
| 'mfu-du-header-missing'
|
|
@@ -1089,9 +1098,30 @@ interface AwaitingTimingHead {
|
|
|
1089
1098
|
mpuSequenceNumber: number
|
|
1090
1099
|
movieFragmentSequenceNumber: number
|
|
1091
1100
|
sampleNumber: number
|
|
1101
|
+
/** MMTP packet sequence number; a recovered copy carries its source's. */
|
|
1102
|
+
sequenceNumber: number
|
|
1092
1103
|
arrivedAt: number
|
|
1093
1104
|
}
|
|
1094
1105
|
|
|
1106
|
+
/** FEC-recovered heads that went through the awaiting-timing queue. Every
|
|
1107
|
+
* count but `pending` is cumulative. A held head is pending or has left the
|
|
1108
|
+
* queue exactly once, so `held - expired - discarded - superseded - pending`
|
|
1109
|
+
* is the number fed again once their FT=1 landed. That feed can still refuse
|
|
1110
|
+
* one (size, offset, dispose from a frame callback); the refusal is counted in
|
|
1111
|
+
* `feedStats.deferredOutcomes`, not here. */
|
|
1112
|
+
export interface MmtpRecoveredHeadTimingStats {
|
|
1113
|
+
/** Held because their sample's FT=1 had not arrived yet. */
|
|
1114
|
+
held: number
|
|
1115
|
+
/** Left without timing: the reassembler-timeout deadline, or pushed out by the queue's capacity. */
|
|
1116
|
+
expired: number
|
|
1117
|
+
/** Dropped with their stream (track reconfigure, reset or retire) or by dispose(). */
|
|
1118
|
+
discarded: number
|
|
1119
|
+
/** Gave way to their arriving source copy, which takes their place in the queue. */
|
|
1120
|
+
superseded: number
|
|
1121
|
+
/** In the queue now; current occupancy, not cumulative. */
|
|
1122
|
+
pending: number
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1095
1125
|
const MAX_AWAITING_TIMING_HEADS = 8
|
|
1096
1126
|
|
|
1097
1127
|
// ── MmtpContainerParser ─────────────────────────────────────────────
|
|
@@ -1129,6 +1159,9 @@ export class MmtpContainerParser {
|
|
|
1129
1159
|
#reclaimedTimingSamples = 0
|
|
1130
1160
|
#evictedTimingSamples = 0
|
|
1131
1161
|
#rejectedMovieFragments = 0
|
|
1162
|
+
#recoveredHeadsAwaitingTiming: Omit<MmtpRecoveredHeadTimingStats, 'pending'> = {
|
|
1163
|
+
held: 0, expired: 0, discarded: 0, superseded: 0,
|
|
1164
|
+
}
|
|
1132
1165
|
#sequencers = new Map<string, { packetId: number; sequencer: FrameSequencer }>()
|
|
1133
1166
|
#fecGate: FecReleaseGate | null = null
|
|
1134
1167
|
#fecSequencer: FecReleaseSequencer | null = null
|
|
@@ -1346,6 +1379,15 @@ export class MmtpContainerParser {
|
|
|
1346
1379
|
}
|
|
1347
1380
|
}
|
|
1348
1381
|
|
|
1382
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
1383
|
+
get recoveredHeadsAwaitingTiming(): Readonly<MmtpRecoveredHeadTimingStats> {
|
|
1384
|
+
let pending = 0
|
|
1385
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
1386
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++
|
|
1387
|
+
}
|
|
1388
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending }
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1349
1391
|
get reorderStats(): Readonly<MmtpReorderStats> {
|
|
1350
1392
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0
|
|
1351
1393
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered
|
|
@@ -1541,7 +1583,7 @@ export class MmtpContainerParser {
|
|
|
1541
1583
|
const now = Date.now()
|
|
1542
1584
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
1543
1585
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0]!.arrivedAt > timeoutMs) {
|
|
1544
|
-
awaitingTiming.shift()
|
|
1586
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift()!, 'expired')
|
|
1545
1587
|
}
|
|
1546
1588
|
}
|
|
1547
1589
|
}
|
|
@@ -1711,11 +1753,16 @@ export class MmtpContainerParser {
|
|
|
1711
1753
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
1712
1754
|
return this.#drop('fragment-unexpected-fragmentation')
|
|
1713
1755
|
const resolver = this.#timingResolver(streamKey, info.packetId, false)
|
|
1714
|
-
if (!resolver) return this.#drop('fragment-no-resolver')
|
|
1756
|
+
if (!resolver?.initialized) return this.#drop('fragment-no-resolver')
|
|
1715
1757
|
const pendingBefore = resolver.pendingSampleCount
|
|
1716
1758
|
const evictedBefore = resolver.evictedSampleCount
|
|
1717
1759
|
const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples
|
|
1718
|
-
const admitted = resolver.addMovieFragment(
|
|
1760
|
+
const admitted = resolver.addMovieFragment(
|
|
1761
|
+
mpu.mpuSequenceNumber,
|
|
1762
|
+
data,
|
|
1763
|
+
remaining,
|
|
1764
|
+
options?.recovered === true,
|
|
1765
|
+
)
|
|
1719
1766
|
// Sync on BOTH paths: the resolver can displace stale timing and still
|
|
1720
1767
|
// refuse the fragment in hand, and a global count that missed those
|
|
1721
1768
|
// freed slots would keep refusing on a budget that is no longer full.
|
|
@@ -1739,7 +1786,11 @@ export class MmtpContainerParser {
|
|
|
1739
1786
|
}
|
|
1740
1787
|
if (fragmentType !== 2) return this.#drop('unknown-fragment-type')
|
|
1741
1788
|
|
|
1742
|
-
|
|
1789
|
+
// An FT=2 before the stream's first FT=0 (a join, a track reset) is taken
|
|
1790
|
+
// like one whose FT=1 has not arrived: a head waits for its timing, a
|
|
1791
|
+
// continuation parks in the reassembler, and a later or FEC-recovered
|
|
1792
|
+
// FT=0/FT=1 can still time them.
|
|
1793
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true)
|
|
1743
1794
|
if (!resolver) return this.#drop('mfu-no-resolver')
|
|
1744
1795
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
1745
1796
|
return this.#drop('mfu-empty-payload')
|
|
@@ -1761,6 +1812,7 @@ export class MmtpContainerParser {
|
|
|
1761
1812
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
1762
1813
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
1763
1814
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
1815
|
+
sequenceNumber: info.sequenceNumber,
|
|
1764
1816
|
arrivedAt: Date.now(),
|
|
1765
1817
|
})
|
|
1766
1818
|
return this.#drop('mfu-sample-timing-missing')
|
|
@@ -2146,6 +2198,18 @@ export class MmtpContainerParser {
|
|
|
2146
2198
|
this.#emitFrame(data, meta)
|
|
2147
2199
|
}
|
|
2148
2200
|
|
|
2201
|
+
/** Whether FT=1 metadata already names this sample, without claiming it. */
|
|
2202
|
+
hasAuthoredTimingFor(
|
|
2203
|
+
packetId: number,
|
|
2204
|
+
mpuSequenceNumber: number,
|
|
2205
|
+
movieFragmentSequenceNumber: number,
|
|
2206
|
+
sampleNumber: number,
|
|
2207
|
+
catalogTrackName?: string,
|
|
2208
|
+
): boolean {
|
|
2209
|
+
return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)
|
|
2210
|
+
?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2149
2213
|
/**
|
|
2150
2214
|
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
2151
2215
|
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
@@ -2216,6 +2280,9 @@ export class MmtpContainerParser {
|
|
|
2216
2280
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard()
|
|
2217
2281
|
this.#sequencers.clear()
|
|
2218
2282
|
this.#fecSequencer?.discard()
|
|
2283
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
2284
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, 'discarded')
|
|
2285
|
+
}
|
|
2219
2286
|
this.#timingStreams.clear()
|
|
2220
2287
|
this.#pendingTimingSamples = 0
|
|
2221
2288
|
this.#reliableMoqStartupGate.clear()
|
|
@@ -2241,10 +2308,27 @@ export class MmtpContainerParser {
|
|
|
2241
2308
|
}
|
|
2242
2309
|
|
|
2243
2310
|
#awaitTiming(streamKey: string, head: AwaitingTimingHead): void {
|
|
2244
|
-
const
|
|
2245
|
-
if (!
|
|
2311
|
+
const state = this.#timingStreams.get(streamKey)
|
|
2312
|
+
if (!state) return
|
|
2313
|
+
// The source copy of a held recovered head takes its place, so the frame
|
|
2314
|
+
// is delivered with source provenance once FT=1 lands.
|
|
2315
|
+
if (!head.options?.recovered) {
|
|
2316
|
+
const kept = state.awaitingTiming.filter(queued =>
|
|
2317
|
+
!queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber)
|
|
2318
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length
|
|
2319
|
+
state.awaitingTiming = kept
|
|
2320
|
+
}
|
|
2321
|
+
const awaitingTiming = state.awaitingTiming
|
|
2246
2322
|
awaitingTiming.push(head)
|
|
2247
|
-
if (
|
|
2323
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++
|
|
2324
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
2325
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift()!, 'expired')
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
2330
|
+
#leaveAwaitingTiming(head: AwaitingTimingHead, outcome: 'expired' | 'discarded'): void {
|
|
2331
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++
|
|
2248
2332
|
}
|
|
2249
2333
|
|
|
2250
2334
|
/**
|
|
@@ -2274,6 +2358,7 @@ export class MmtpContainerParser {
|
|
|
2274
2358
|
const state = this.#timingStreams.get(streamKey)
|
|
2275
2359
|
if (!state) return
|
|
2276
2360
|
this.#timingStreams.delete(streamKey)
|
|
2361
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, 'discarded')
|
|
2277
2362
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount
|
|
2278
2363
|
if (this.#pendingTimingSamples < 0) {
|
|
2279
2364
|
throw new Error('[MmtpContainerParser] pending timing sample accounting underflow')
|
package/src/mmtp-fec-session.ts
CHANGED
|
@@ -23,6 +23,15 @@
|
|
|
23
23
|
* uses. A packet below B_0 at or after the anchor fails the session closed:
|
|
24
24
|
* `stats.failClosed` reports why, and every later repair is refused.
|
|
25
25
|
*
|
|
26
|
+
* Catalog-less (draft-ramadan-moq-mmt-02 8.3, 13.2): a receiver holding
|
|
27
|
+
* layer 0 under the AL-FEC message alone builds the session from the message's
|
|
28
|
+
* fec_flow entry (`alFecFlow`) instead. K, T, the code point and the repair
|
|
29
|
+
* packet_id come from that entry, and layer 0 is held at
|
|
30
|
+
* maximum_p_for_repair_flow, standing for O_0 + P_0, by the anchored clause of
|
|
31
|
+
* draft-ramadan-moq-fec-core-00 7.3.2 alone. A later version that raises the
|
|
32
|
+
* field raises the held total (updateAlFecFlow()); the message carries no
|
|
33
|
+
* millisecond budget, so the caller states the retention (fec-core-00 9.4).
|
|
34
|
+
*
|
|
26
35
|
* Every repair packet that cannot be used is counted under the reason it was
|
|
27
36
|
* refused, and every block ends under exactly one outcome, so a stream whose
|
|
28
37
|
* FEC is doing nothing can be told apart from one with nothing to repair.
|
|
@@ -30,6 +39,8 @@
|
|
|
30
39
|
|
|
31
40
|
import {
|
|
32
41
|
admitMmtpRepairObject,
|
|
42
|
+
alFecRaptorQFlowConfig,
|
|
43
|
+
FEC_CODE_RAPTORQ,
|
|
33
44
|
isoBlockContains,
|
|
34
45
|
isoBlocksBehindLowerBound,
|
|
35
46
|
parseFecBlockProfile,
|
|
@@ -39,6 +50,8 @@ import {
|
|
|
39
50
|
serial32Precedes,
|
|
40
51
|
ssbg1FitsSymbol,
|
|
41
52
|
trimSsbg1SourceSymbol,
|
|
53
|
+
type AlFecFlow,
|
|
54
|
+
type AlFecRaptorQFlowConfig,
|
|
42
55
|
type FecBlockProfile,
|
|
43
56
|
} from '@blockcast/mmt-base'
|
|
44
57
|
import { deriveFecRetentionBlocks, fecBlockBudgetMs } from '@blockcast/mmt-fec'
|
|
@@ -105,6 +118,25 @@ export interface MmtpFecSessionConfig {
|
|
|
105
118
|
heldLayers?: readonly number[]
|
|
106
119
|
}
|
|
107
120
|
|
|
121
|
+
/**
|
|
122
|
+
* A catalog-less session: one fec_flow entry of the AL-FEC message
|
|
123
|
+
* (`parseAlFecMessage(...).flows[i]`), draft-ramadan-moq-mmt-02 8.3.
|
|
124
|
+
*/
|
|
125
|
+
export interface MmtpFecSessionMessageConfig {
|
|
126
|
+
/** The source track's entry; it must pass `alFecRaptorQFlowConfig`. */
|
|
127
|
+
alFecFlow: AlFecFlow
|
|
128
|
+
/**
|
|
129
|
+
* The decoder retention in blocks. The message carries no elapsed-time
|
|
130
|
+
* budget (mmt-02 7.5), so open-block state is bounded by local policy
|
|
131
|
+
* (fec-core-00 9.4); required, never defaulted.
|
|
132
|
+
*/
|
|
133
|
+
retentionBlocks: number
|
|
134
|
+
Decoder: MmtpFecSessionDecoderConstructor
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** What updateAlFecFlow() did with a later version of the message. */
|
|
138
|
+
export type MmtpFecSessionMessageUpdate = 'raised' | 'unchanged' | 'refused'
|
|
139
|
+
|
|
108
140
|
/**
|
|
109
141
|
* The path a symbol arrived on: its MoQT subscription, or the multicast
|
|
110
142
|
* group (native SSM or AMT) carrying the same asset. The caller states it;
|
|
@@ -197,12 +229,17 @@ export class MmtpFecSession {
|
|
|
197
229
|
readonly algorithm: 'raptorq' | 'reed-solomon'
|
|
198
230
|
readonly sourceSymbols: number
|
|
199
231
|
/** P_0 = B_0, the held total every repair packet is checked against. */
|
|
200
|
-
|
|
232
|
+
get repairSymbols(): number {
|
|
233
|
+
return this.#repairSymbols
|
|
234
|
+
}
|
|
201
235
|
readonly symbolSize: number
|
|
202
236
|
/** The decoder's retention, in blocks, derived from the catalog budget. */
|
|
203
237
|
readonly retentionBlocks: number
|
|
204
238
|
|
|
205
239
|
#decoder: MmtpFecSessionDecoder | null
|
|
240
|
+
#repairSymbols: number
|
|
241
|
+
/** The message entry a catalog-less session was built from; undefined under a catalog. */
|
|
242
|
+
#message: AlFecRaptorQFlowConfig | undefined
|
|
206
243
|
/** Blocks a repair has defined and that are not settled yet, by SS_Start. */
|
|
207
244
|
#blocks = new Map<number, BlockState>()
|
|
208
245
|
/** SS_Starts of settled blocks, so a later repair for one is refused. */
|
|
@@ -240,7 +277,26 @@ export class MmtpFecSession {
|
|
|
240
277
|
* the scheme, the block profile, K, P_0 (`repairSymbols`), T,
|
|
241
278
|
* `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
|
|
242
279
|
*/
|
|
243
|
-
constructor(config: MmtpFecSessionConfig) {
|
|
280
|
+
constructor(config: MmtpFecSessionConfig | MmtpFecSessionMessageConfig) {
|
|
281
|
+
if (config && 'alFecFlow' in config) {
|
|
282
|
+
const message = alFecRaptorQFlowConfig(config.alFecFlow)
|
|
283
|
+
if (!message) {
|
|
284
|
+
throw new Error('MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)')
|
|
285
|
+
}
|
|
286
|
+
this.#message = message
|
|
287
|
+
// Coding byte 0x14 is the iso-ssbg1-v1 profile (mmt-02 8.3).
|
|
288
|
+
this.blockProfile = parseFecBlockProfile('iso-ssbg1-v1', 'alFecFlow')
|
|
289
|
+
this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? 'raptorq' : 'reed-solomon'
|
|
290
|
+
this.sourceSymbols = message.maximumK
|
|
291
|
+
this.#repairSymbols = message.maximumP
|
|
292
|
+
this.symbolSize = message.repairSymbolSize
|
|
293
|
+
this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, 'retentionBlocks')
|
|
294
|
+
this.#repairPacketId = message.repairFlowId
|
|
295
|
+
this.#heldLayers = layerZero(message.maximumP)
|
|
296
|
+
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols)
|
|
297
|
+
this.#decoder = buildDecoder(config.Decoder, this)
|
|
298
|
+
return
|
|
299
|
+
}
|
|
244
300
|
const fec = config?.fec
|
|
245
301
|
if (!fec || typeof fec !== 'object') {
|
|
246
302
|
throw new Error('MmtpFecSession: the catalog fec instance is required')
|
|
@@ -251,7 +307,7 @@ export class MmtpFecSession {
|
|
|
251
307
|
}
|
|
252
308
|
this.algorithm = fec.scheme
|
|
253
309
|
this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, 'fec.sourceSymbols')
|
|
254
|
-
this
|
|
310
|
+
this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, 'fec.repairSymbols')
|
|
255
311
|
this.symbolSize = requirePositiveInteger(fec.symbolSize, 'fec.symbolSize')
|
|
256
312
|
const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, 'fec.interleaveDepthMs')
|
|
257
313
|
const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, 'fec.reorderToleranceMs')
|
|
@@ -271,17 +327,38 @@ export class MmtpFecSession {
|
|
|
271
327
|
config.heldLayers,
|
|
272
328
|
)
|
|
273
329
|
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
330
|
+
this.#decoder = buildDecoder(config.Decoder, this)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* A later version of the AL-FEC message, catalog-less sessions only. A
|
|
335
|
+
* maximum_p_for_repair_flow above the held total raises it (`raised`): the
|
|
336
|
+
* anchored check restarts at the new total, so a block generated under the
|
|
337
|
+
* previous one stays valid before the first block carrying the new total
|
|
338
|
+
* (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
|
|
339
|
+
* (`unchanged`): no publisher lowers it, and the check catches a wire that
|
|
340
|
+
* does. An entry with another asset, repair flow, code point, K or T, or
|
|
341
|
+
* one outside 8.3, is `refused`: that is a new source encoding, which needs
|
|
342
|
+
* a new session. A latched fail-closed stays latched either way.
|
|
343
|
+
*/
|
|
344
|
+
updateAlFecFlow(flow: AlFecFlow): MmtpFecSessionMessageUpdate {
|
|
345
|
+
const held = this.#message
|
|
346
|
+
if (!held) throw new Error('MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry')
|
|
347
|
+
const next = alFecRaptorQFlowConfig(flow)
|
|
348
|
+
if (
|
|
349
|
+
!next ||
|
|
350
|
+
next.sourcePacketIds[0] !== held.sourcePacketIds[0] ||
|
|
351
|
+
next.repairFlowId !== held.repairFlowId ||
|
|
352
|
+
next.fecCodeId !== held.fecCodeId ||
|
|
353
|
+
next.maximumK !== held.maximumK ||
|
|
354
|
+
next.repairSymbolSize !== held.repairSymbolSize
|
|
355
|
+
) {
|
|
356
|
+
return 'refused'
|
|
283
357
|
}
|
|
284
|
-
this.#
|
|
358
|
+
if (next.maximumP <= this.#repairSymbols) return 'unchanged'
|
|
359
|
+
this.#repairSymbols = next.maximumP
|
|
360
|
+
this.#heldLayers = layerZero(next.maximumP)
|
|
361
|
+
return 'raised'
|
|
285
362
|
}
|
|
286
363
|
|
|
287
364
|
/** True once release() ran. */
|
|
@@ -386,7 +463,7 @@ export class MmtpFecSession {
|
|
|
386
463
|
// An RSB_length below a held layer's total goes to the 7.3.2 stop, not
|
|
387
464
|
// to the discard; the block state is unchanged either way.
|
|
388
465
|
const verdict = this.#heldLayers.failsClosed(
|
|
389
|
-
repairTrackName, frame.ssStart, frame.rsbLength, this.#anchorHorizon, this.#newestSource,
|
|
466
|
+
this.#trackFor(repairTrackName), frame.ssStart, frame.rsbLength, this.#anchorHorizon, this.#newestSource,
|
|
390
467
|
)
|
|
391
468
|
if (verdict) {
|
|
392
469
|
this.#stats.failClosed = verdict
|
|
@@ -399,7 +476,7 @@ export class MmtpFecSession {
|
|
|
399
476
|
// fec-01 6.3.9, after the 4.5 discards and the size check, before a block
|
|
400
477
|
// is placed. Without a track name only the anchored rule (b) applies.
|
|
401
478
|
const verdict = this.#heldLayers.admit(
|
|
402
|
-
repairTrackName,
|
|
479
|
+
this.#trackFor(repairTrackName),
|
|
403
480
|
frame.ssStart,
|
|
404
481
|
frame.rsbLength,
|
|
405
482
|
this.#anchorHorizon,
|
|
@@ -464,6 +541,11 @@ export class MmtpFecSession {
|
|
|
464
541
|
return true
|
|
465
542
|
}
|
|
466
543
|
|
|
544
|
+
/** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
|
|
545
|
+
#trackFor(repairTrackName: string | undefined): string | undefined {
|
|
546
|
+
return this.#message ? undefined : repairTrackName
|
|
547
|
+
}
|
|
548
|
+
|
|
467
549
|
#drop(reason: string): Uint8Array[] {
|
|
468
550
|
this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1
|
|
469
551
|
return []
|
|
@@ -600,6 +682,24 @@ export class MmtpFecSession {
|
|
|
600
682
|
}
|
|
601
683
|
}
|
|
602
684
|
|
|
685
|
+
function layerZero(total: number): HeldLayerAdmission {
|
|
686
|
+
return new HeldLayerAdmission([{ layer: 0, repairTrackName: '', rsIdOffset: 0, repairSymbols: total }])
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function buildDecoder(Decoder: MmtpFecSessionDecoderConstructor, session: MmtpFecSession): MmtpFecSessionDecoder {
|
|
690
|
+
if (typeof Decoder !== 'function') {
|
|
691
|
+
throw new Error('MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required')
|
|
692
|
+
}
|
|
693
|
+
const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm)
|
|
694
|
+
try {
|
|
695
|
+
decoder.configure_params(session.sourceSymbols, session.symbolSize)
|
|
696
|
+
} catch (error) {
|
|
697
|
+
decoder.free()
|
|
698
|
+
throw error
|
|
699
|
+
}
|
|
700
|
+
return decoder
|
|
701
|
+
}
|
|
702
|
+
|
|
603
703
|
function requirePositiveInteger(value: unknown, path: string): number {
|
|
604
704
|
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
|
|
605
705
|
throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`)
|