@blockcast/mmt-container 0.4.1 → 0.5.0-main.34c7b9c2cfa0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/browser-runtime.js +3769 -364
- package/dist/browser-runtime.js.map +4 -4
- package/dist/cmcd-producer.d.ts +5 -1
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +18 -2
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/fec-manager.d.ts +21 -16
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +197 -80
- package/dist/fec-manager.js.map +3 -3
- package/dist/held-layer-admission.d.ts +58 -0
- package/dist/held-layer-admission.d.ts.map +1 -0
- package/dist/held-layer-admission.js +105 -0
- package/dist/held-layer-admission.js.map +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/mmtp-container-parser.d.ts +10 -0
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +31 -1
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +2713 -1024
- package/dist/mmtp-container-parser.no-modules.js.map +4 -4
- package/dist/mmtp-decode-pipeline.js +31 -1
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-fec-session.d.ts +36 -3
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +114 -5
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +96 -29
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +522 -205
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-auth-catalog.d.ts +10 -2
- package/dist/multicast-auth-catalog.d.ts.map +1 -1
- package/dist/multicast-auth-catalog.js +422 -22
- package/dist/multicast-auth-catalog.js.map +4 -4
- package/dist/multicast-manager.d.ts +78 -11
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +3663 -333
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +4 -1
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +20 -3
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.d.ts +1 -2
- package/dist/no-modules.d.ts.map +1 -1
- package/dist/no-modules.js +2699 -1008
- package/dist/no-modules.js.map +4 -4
- package/dist/repair-fetch.d.ts +50 -10
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +62 -20
- package/dist/repair-fetch.js.map +2 -2
- package/dist/wasm-mmtp-container-parser.js +31 -1
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +7 -15
- package/src/cmcd-producer.ts +6 -1
- package/src/fec-manager.ts +147 -131
- package/src/held-layer-admission.ts +161 -0
- package/src/index.ts +6 -9
- package/src/mmtp-container-parser.ts +48 -0
- package/src/mmtp-fec-session.ts +76 -3
- package/src/mmtp-router.ts +637 -274
- package/src/multicast-auth-catalog.ts +81 -8
- package/src/multicast-manager.ts +274 -49
- package/src/multicast-transport.ts +6 -2
- package/src/no-modules.ts +1 -6
- package/src/repair-fetch.ts +154 -34
- package/test-vectors/catalog/negative/multicast-auth-padded-key.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-reused-track-format.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-root-dotted-key.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-trailing-pad-bits.json +0 -1
- package/test-vectors/repair-policy-v1.json +113 -0
- package/dist/manifest-auth-plan.d.ts +0 -31
- package/dist/manifest-auth-plan.d.ts.map +0 -1
- package/dist/manifest-auth-plan.js +0 -426
- package/dist/manifest-auth-plan.js.map +0 -7
- package/dist/source-auth.d.ts +0 -2
- package/dist/source-auth.d.ts.map +0 -1
- package/dist/source-auth.js +0 -3912
- package/dist/source-auth.js.map +0 -7
- package/src/manifest-auth-plan.ts +0 -55
- package/src/source-auth.ts +0 -1
- package/test-vectors/generate-source-auth-fixture.mjs +0 -114
- package/test-vectors/source-auth-v1.json +0 -61
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fec-01 6.3.9 receiver check (held-layer anchored RSB_length), in one
|
|
3
|
+
* place. FECManager and MmtpFecSession both run their repair packets through
|
|
4
|
+
* HeldLayerAdmission; neither restates the rule.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { serial32Distance, serial32Precedes } from '@blockcast/mmt-base'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Why a source track's FEC failed closed at runtime (fec-01 6.3.9): a repair
|
|
11
|
+
* packet stated an RSB_length below a held layer's total B_i on that layer's
|
|
12
|
+
* own repair track (`below_own_track_total`), or at or after the anchor, the
|
|
13
|
+
* earliest block in SS_Start serial order whose repair carried >= B_i
|
|
14
|
+
* (`below_anchored_total`). Repair for the track is refused from then on.
|
|
15
|
+
*/
|
|
16
|
+
export interface FecFailClosedReason {
|
|
17
|
+
code: 'below_own_track_total' | 'below_anchored_total'
|
|
18
|
+
layer: number
|
|
19
|
+
total: number
|
|
20
|
+
rsbLength: number
|
|
21
|
+
ssStart: number
|
|
22
|
+
anchor?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One repair layer as the catalog states it: B_i = rsIdOffset + repairSymbols. */
|
|
26
|
+
export interface HeldLayerInput {
|
|
27
|
+
layer: number
|
|
28
|
+
repairTrackName: string
|
|
29
|
+
rsIdOffset: number
|
|
30
|
+
repairSymbols: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface HeldLayerState {
|
|
34
|
+
layer: number
|
|
35
|
+
repairTrackName: string
|
|
36
|
+
total: number
|
|
37
|
+
/** Earliest (serial order) block start whose repair carried >= total. */
|
|
38
|
+
anchor?: number
|
|
39
|
+
/** Latest (serial order) block start whose repair carried < total. */
|
|
40
|
+
latestBelow?: number
|
|
41
|
+
/** RSB_length that latestBelow's repair carried, for the fail-closed reason. */
|
|
42
|
+
latestBelowRsb?: number
|
|
43
|
+
/** Newest block start seen; anchor and latestBelow are retired behind it. */
|
|
44
|
+
newest?: number
|
|
45
|
+
/**
|
|
46
|
+
* The anchor fell behind the horizon, so no block that can still arrive
|
|
47
|
+
* precedes it: every later packet must carry >= total. Its serial is no
|
|
48
|
+
* longer compared, so SS_Start wrapping past it cannot misorder it.
|
|
49
|
+
*/
|
|
50
|
+
settled?: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The largest horizon compared, so every compared serial stays in one half-window. */
|
|
54
|
+
const HORIZON_CAP = 0x40000000
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* How far (in SS_Start symbols) behind the newest block a 6.3.9 anchor or
|
|
58
|
+
* below-total block can still matter: the iso retention in blocks times K,
|
|
59
|
+
* capped at 2^30. Unknown inputs give the cap.
|
|
60
|
+
*/
|
|
61
|
+
export function heldLayerAnchorHorizon(retentionBlocks: number | undefined, k: number): number {
|
|
62
|
+
return retentionBlocks === undefined || k <= 0 ? HORIZON_CAP : Math.min(HORIZON_CAP, retentionBlocks * k)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* fec-01 6.3.9 anchored state for one FEC instance's held layers. For each
|
|
67
|
+
* held layer i with total B_i = O_i + P_i: the anchor is the earliest block,
|
|
68
|
+
* in SS_Start serial order, whose repair carried RSB_length >= B_i. The
|
|
69
|
+
* instance fails closed on (a) a packet on layer i's own repair track (i > 0)
|
|
70
|
+
* below B_i, or (b) any packet of the instance at or after the anchor below
|
|
71
|
+
* B_i. Blocks before the anchor (pre-append) stay valid at the previous total.
|
|
72
|
+
* The result is not latched here: the caller owns what failing closed means.
|
|
73
|
+
*/
|
|
74
|
+
export class HeldLayerAdmission {
|
|
75
|
+
readonly #layerNames: ReadonlySet<string>
|
|
76
|
+
readonly #states: HeldLayerState[]
|
|
77
|
+
|
|
78
|
+
constructor(layers: readonly HeldLayerInput[], heldLayers: readonly number[] = [0]) {
|
|
79
|
+
const held = new Set(heldLayers)
|
|
80
|
+
this.#layerNames = new Set(layers.map((l) => l.repairTrackName))
|
|
81
|
+
this.#states = layers
|
|
82
|
+
.filter((l) => held.has(l.layer))
|
|
83
|
+
.map((l) => ({ layer: l.layer, repairTrackName: l.repairTrackName, total: l.rsIdOffset + l.repairSymbols }))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Judges one repair packet, after the 4.5 discards and the size check and
|
|
88
|
+
* before it places a block. `repairTrackName` is the subscribed repair
|
|
89
|
+
* track, never inferred from RS_ID; without it only (b) runs. A packet on a
|
|
90
|
+
* repair track the catalog does not list is `'discard'` and anchors nothing.
|
|
91
|
+
* `sourceFrontier` is the newest source SS_ID, if any: a repair more than
|
|
92
|
+
* `horizon` past it is judged without moving the anchored state.
|
|
93
|
+
*
|
|
94
|
+
* @returns undefined to admit, `'discard'`, or the fail-closed reason.
|
|
95
|
+
*/
|
|
96
|
+
admit(
|
|
97
|
+
repairTrackName: string | undefined,
|
|
98
|
+
ssStart: number,
|
|
99
|
+
rsbLength: number,
|
|
100
|
+
horizon: number,
|
|
101
|
+
sourceFrontier?: number,
|
|
102
|
+
): FecFailClosedReason | 'discard' | undefined {
|
|
103
|
+
if (repairTrackName !== undefined && !this.#layerNames.has(repairTrackName)) return 'discard'
|
|
104
|
+
// A repair may run ahead of the media, but one more than `horizon` past the
|
|
105
|
+
// source frontier must not move the held-layer state: advancing `newest` on
|
|
106
|
+
// it would retire every real anchor and skip this check until traffic caught
|
|
107
|
+
// up. Such a repair is still judged below the total: it is on its own track,
|
|
108
|
+
// or after any anchor by definition. With no anchor yet it says nothing.
|
|
109
|
+
// It is still admitted: in iso the frontier only moves when a repair places
|
|
110
|
+
// held sources, so dropping it would stall recovery after a long outage.
|
|
111
|
+
// IsoBlockLocator.placeRepair may still refuse the same repair as `ahead`:
|
|
112
|
+
// it measures from `newest`, which held sources move, not from this frontier.
|
|
113
|
+
const farAhead =
|
|
114
|
+
sourceFrontier !== undefined &&
|
|
115
|
+
serial32Precedes(sourceFrontier, ssStart) &&
|
|
116
|
+
serial32Distance(sourceFrontier, ssStart) > horizon
|
|
117
|
+
for (const state of this.#states) {
|
|
118
|
+
let code: FecFailClosedReason['code'] | undefined
|
|
119
|
+
if (farAhead) {
|
|
120
|
+
if (rsbLength < state.total) {
|
|
121
|
+
if (state.layer > 0 && repairTrackName === state.repairTrackName) code = 'below_own_track_total'
|
|
122
|
+
else if (state.anchor !== undefined || state.settled) code = 'below_anchored_total'
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
// Serials more than `horizon` behind the newest block are retired, so
|
|
126
|
+
// every serial still compared sits inside one half-window and SS_Start
|
|
127
|
+
// wrapping on a long-running stream cannot reorder them.
|
|
128
|
+
if (state.newest === undefined || serial32Precedes(state.newest, ssStart)) state.newest = ssStart
|
|
129
|
+
const stale = (s: number) => serial32Distance(s, state.newest!) > horizon
|
|
130
|
+
if (state.latestBelow !== undefined && stale(state.latestBelow)) {
|
|
131
|
+
delete state.latestBelow
|
|
132
|
+
delete state.latestBelowRsb
|
|
133
|
+
}
|
|
134
|
+
if (state.anchor !== undefined && stale(state.anchor)) state.settled = true
|
|
135
|
+
if (rsbLength < state.total) {
|
|
136
|
+
if (state.layer > 0 && repairTrackName === state.repairTrackName) code = 'below_own_track_total'
|
|
137
|
+
else if (stale(ssStart)) continue // past retention: undecodable, anchors nothing
|
|
138
|
+
else if (state.settled || (state.anchor !== undefined && !serial32Precedes(ssStart, state.anchor))) code = 'below_anchored_total'
|
|
139
|
+
else if (state.latestBelow === undefined || serial32Precedes(state.latestBelow, ssStart)) {
|
|
140
|
+
state.latestBelow = ssStart
|
|
141
|
+
state.latestBelowRsb = rsbLength
|
|
142
|
+
}
|
|
143
|
+
} else if (!state.settled && !stale(ssStart)) {
|
|
144
|
+
if (state.anchor === undefined || serial32Precedes(ssStart, state.anchor)) state.anchor = ssStart
|
|
145
|
+
if (state.latestBelow !== undefined && !serial32Precedes(state.latestBelow, state.anchor)) {
|
|
146
|
+
code = 'below_anchored_total'
|
|
147
|
+
ssStart = state.latestBelow
|
|
148
|
+
rsbLength = state.latestBelowRsb!
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (code !== undefined) {
|
|
153
|
+
return {
|
|
154
|
+
code, layer: state.layer, total: state.total, rsbLength, ssStart,
|
|
155
|
+
...(state.anchor === undefined ? {} : { anchor: state.anchor }),
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return undefined
|
|
160
|
+
}
|
|
161
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -39,15 +39,11 @@ export {
|
|
|
39
39
|
export { isCompleteFmp4DataUnit } from './fmp4-completion.js'
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
// from the eager index barrel
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// "Failed to resolve module specifier". No browser player uses source-auth.)
|
|
48
|
-
// Consumers that need it import the dedicated subpath: '@blockcast/mmt-container/source-auth'.
|
|
49
|
-
// The same rule covers the @blockcast/mmt-manifest-verify surface (ManifestVerifier et al.):
|
|
50
|
-
// import it from that package (or the subpath above) directly, never from this barrel.
|
|
42
|
+
// The bc-provenance v2 surface (@blockcast/mmt-manifest-verify) is intentionally
|
|
43
|
+
// NOT re-exported from the eager index barrel: it imports @noble/curves and
|
|
44
|
+
// @noble/hashes, bare specifiers the moq-lib / shred / moqtail players (which
|
|
45
|
+
// load the barrel as raw ESM via an import map) cannot resolve. Import it from
|
|
46
|
+
// that package directly. The router's provenance hooks take it by type only.
|
|
51
47
|
|
|
52
48
|
// H.264/AVC + H.265/HEVC Codec Utilities
|
|
53
49
|
export {
|
|
@@ -653,6 +649,7 @@ export {
|
|
|
653
649
|
type RepairPriorityReason,
|
|
654
650
|
type RepairSendMeasurement,
|
|
655
651
|
type RepairSkipReason,
|
|
652
|
+
type RepairStopReason,
|
|
656
653
|
type RepairSubscribeCallback,
|
|
657
654
|
type WireRepairFetchOptions,
|
|
658
655
|
} from './repair-fetch.js'
|
|
@@ -234,6 +234,14 @@ export interface FecReleaseGate {
|
|
|
234
234
|
): FrameGapDecision
|
|
235
235
|
/** Every frame released in order from a gated stream. */
|
|
236
236
|
onReleased?(data: Uint8Array, meta: FrameMeta): void
|
|
237
|
+
/**
|
|
238
|
+
* Whether a complete frame is a RAP (draft-ramadan-moq-fec 11.4.2). Asked
|
|
239
|
+
* for each video frame with a Source FEC Payload ID that enters the gate,
|
|
240
|
+
* held or released; absent, no frame counts as a RAP.
|
|
241
|
+
*/
|
|
242
|
+
isRap?(data: Uint8Array, meta: FrameMeta): boolean
|
|
243
|
+
/** A complete RAP entered the gate at SS_ID `ssId` (see `isRap`). */
|
|
244
|
+
onRap?(packetId: number, catalogTrackName: string | undefined, ssId: number): void
|
|
237
245
|
}
|
|
238
246
|
|
|
239
247
|
/** Typed ISO-BMFF timing-sample budget counters. See `timingBudgetStats`. */
|
|
@@ -692,6 +700,15 @@ function authoredGap(before: FrameMeta, after: FrameMeta): boolean {
|
|
|
692
700
|
return b.decodeTime > a.decodeTime + BigInt(a.durationTicks)
|
|
693
701
|
}
|
|
694
702
|
|
|
703
|
+
/**
|
|
704
|
+
* Serial-number order on the uint32 SS_ID (RFC 1982 style): `a` follows `b`
|
|
705
|
+
* within half the space, so a 2^32 wrap does not read a post-wrap block as
|
|
706
|
+
* below a pre-wrap RAP.
|
|
707
|
+
*/
|
|
708
|
+
function ssIdAfter(a: number, b: number): boolean {
|
|
709
|
+
return ((a - b) | 0) > 0
|
|
710
|
+
}
|
|
711
|
+
|
|
695
712
|
interface GatedFrame extends PendingFrame {
|
|
696
713
|
/** performance.now() at push; measures the hold against the gate's limit. */
|
|
697
714
|
arrivedAtMs: number
|
|
@@ -704,6 +721,8 @@ interface GatedStream {
|
|
|
704
721
|
last: PendingFrame | null
|
|
705
722
|
lastSbn: number | undefined
|
|
706
723
|
discontinuity: boolean
|
|
724
|
+
/** Latest (serial order) SS_ID of a complete RAP that entered the gate since the last reset. */
|
|
725
|
+
rapSsId: number | undefined
|
|
707
726
|
}
|
|
708
727
|
|
|
709
728
|
/**
|
|
@@ -797,11 +816,17 @@ class FecReleaseSequencer {
|
|
|
797
816
|
stream.last = null
|
|
798
817
|
stream.lastSbn = undefined
|
|
799
818
|
stream.discontinuity = false
|
|
819
|
+
stream.rapSsId = undefined
|
|
800
820
|
}
|
|
801
821
|
if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
|
|
802
822
|
this.#recordStat('droppedStale')
|
|
803
823
|
return
|
|
804
824
|
}
|
|
825
|
+
const ssId = meta.fecSbn
|
|
826
|
+
if (meta.trackType === 'video' && ssId !== undefined && this.#gate.isRap?.(data, meta) === true) {
|
|
827
|
+
if (stream.rapSsId === undefined || ssIdAfter(ssId, stream.rapSsId)) stream.rapSsId = ssId
|
|
828
|
+
this.#gate.onRap?.(stream.packetId, stream.catalogTrackName, ssId)
|
|
829
|
+
}
|
|
805
830
|
if (stream.last === null) {
|
|
806
831
|
// The first release establishes decoder sync: video must start on a
|
|
807
832
|
// RAP; earlier deltas are undecodable and are dropped.
|
|
@@ -841,6 +866,23 @@ class FecReleaseSequencer {
|
|
|
841
866
|
return earliest
|
|
842
867
|
}
|
|
843
868
|
|
|
869
|
+
/**
|
|
870
|
+
* Whether a complete RAP above SS_ID `ssId` entered the gate of a stream of
|
|
871
|
+
* `packetId` (draft-ramadan-moq-fec 11.4.2: repair of a block below it is
|
|
872
|
+
* moot). Its SS_ID is the one its frame completed in; a frame's packets
|
|
873
|
+
* hold consecutive SS_IDs in authored order, so a complete RAP with a
|
|
874
|
+
* packet above `ssId` has every packet of it at or above the first SS_ID
|
|
875
|
+
* any loss at or below `ssId` could have taken.
|
|
876
|
+
*/
|
|
877
|
+
rapAbove(packetId: number, catalogTrackName: string | undefined, ssId: number): boolean {
|
|
878
|
+
for (const stream of this.#streams.values()) {
|
|
879
|
+
if (stream.packetId !== packetId) continue
|
|
880
|
+
if (catalogTrackName !== undefined && stream.catalogTrackName !== catalogTrackName) continue
|
|
881
|
+
if (stream.rapSsId !== undefined && ssIdAfter(stream.rapSsId, ssId)) return true
|
|
882
|
+
}
|
|
883
|
+
return false
|
|
884
|
+
}
|
|
885
|
+
|
|
844
886
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
845
887
|
poke(streamKey?: string): void {
|
|
846
888
|
if (streamKey !== undefined) {
|
|
@@ -946,6 +988,7 @@ class FecReleaseSequencer {
|
|
|
946
988
|
last: null,
|
|
947
989
|
lastSbn: undefined,
|
|
948
990
|
discontinuity: false,
|
|
991
|
+
rapSsId: undefined,
|
|
949
992
|
}
|
|
950
993
|
this.#streams.set(streamKey, stream)
|
|
951
994
|
}
|
|
@@ -1346,6 +1389,11 @@ export class MmtpContainerParser {
|
|
|
1346
1389
|
return this.#fecSequencer?.earliestHeldArrivalMs(packetId, catalogTrackName)
|
|
1347
1390
|
}
|
|
1348
1391
|
|
|
1392
|
+
/** See FecReleaseSequencer.rapAbove; false when no stream is FEC-gated. */
|
|
1393
|
+
fecReleaseRapAbove(packetId: number, catalogTrackName: string | undefined, ssId: number): boolean {
|
|
1394
|
+
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1349
1397
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
1350
1398
|
pokeFecRelease(packetId?: number, catalogTrackName?: string): void {
|
|
1351
1399
|
if (!this.#fecSequencer) return
|
package/src/mmtp-fec-session.ts
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
* The retention is derived from the catalog's `fecBlockBudgetMs`, and a
|
|
19
19
|
* catalog missing either input throws rather than defaulting.
|
|
20
20
|
*
|
|
21
|
+
* Every repair packet runs the fec-01 6.3.9 held-layer check (layer 0, B_0 =
|
|
22
|
+
* catalog `fec.repairSymbols`) through the same HeldLayerAdmission FECManager
|
|
23
|
+
* uses. A packet below B_0 at or after the anchor fails the session closed:
|
|
24
|
+
* `stats.failClosed` reports why, and every later repair is refused.
|
|
25
|
+
*
|
|
21
26
|
* Every repair packet that cannot be used is counted under the reason it was
|
|
22
27
|
* refused, and every block ends under exactly one outcome, so a stream whose
|
|
23
28
|
* FEC is doing nothing can be told apart from one with nothing to repair.
|
|
@@ -37,6 +42,14 @@ import {
|
|
|
37
42
|
type FecBlockProfile,
|
|
38
43
|
} from '@blockcast/mmt-base'
|
|
39
44
|
import { deriveFecRetentionBlocks, fecBlockBudgetMs } from '@blockcast/mmt-fec'
|
|
45
|
+
import {
|
|
46
|
+
HeldLayerAdmission,
|
|
47
|
+
heldLayerAnchorHorizon,
|
|
48
|
+
type FecFailClosedReason,
|
|
49
|
+
type HeldLayerInput,
|
|
50
|
+
} from './held-layer-admission.js'
|
|
51
|
+
|
|
52
|
+
export type { FecFailClosedReason, HeldLayerInput } from './held-layer-admission.js'
|
|
40
53
|
|
|
41
54
|
/** The mmt-wasm `MmtFecDecoder` members the session drives. */
|
|
42
55
|
export interface MmtpFecSessionDecoder {
|
|
@@ -67,6 +80,10 @@ export interface MmtpFecSessionCatalogFec {
|
|
|
67
80
|
algorithm?: unknown
|
|
68
81
|
blockProfile?: unknown
|
|
69
82
|
sourceSymbols?: unknown
|
|
83
|
+
/** P_0: layer 0's repair symbols per block, the fec-01 6.3.9 total B_0. */
|
|
84
|
+
repairSymbols?: unknown
|
|
85
|
+
/** Layer 0's repair track; names it for addRepair()'s `repairTrackName`. */
|
|
86
|
+
repairTrack?: unknown
|
|
70
87
|
symbolSize?: unknown
|
|
71
88
|
interleaveDepthMs?: unknown
|
|
72
89
|
reorderToleranceMs?: unknown
|
|
@@ -77,6 +94,14 @@ export interface MmtpFecSessionConfig {
|
|
|
77
94
|
fec: MmtpFecSessionCatalogFec
|
|
78
95
|
/** Builds the RaptorQ decoder, normally `wasm_bindgen.MmtFecDecoder`. */
|
|
79
96
|
Decoder: MmtpFecSessionDecoderConstructor
|
|
97
|
+
/**
|
|
98
|
+
* The source's repair layers as the catalog resolves them
|
|
99
|
+
* (`TrackFecParams.repairLayers`), for a caller that subscribes more than
|
|
100
|
+
* layer 0. Absent: layer 0 alone, B_0 = `fec.repairSymbols`.
|
|
101
|
+
*/
|
|
102
|
+
repairLayers?: readonly HeldLayerInput[]
|
|
103
|
+
/** The layers the caller holds (fec-01 6.3.9). Absent: [0]. */
|
|
104
|
+
heldLayers?: readonly number[]
|
|
80
105
|
}
|
|
81
106
|
|
|
82
107
|
/**
|
|
@@ -139,6 +164,16 @@ export interface MmtpFecSessionStats {
|
|
|
139
164
|
* contributed nothing to its block and do not make it mixed.
|
|
140
165
|
*/
|
|
141
166
|
mixedPath: MmtpFecSessionMixedPathStats
|
|
167
|
+
/**
|
|
168
|
+
* fec-01 6.3.9: null until a repair packet at or after the anchor (the
|
|
169
|
+
* earliest block, in SS_Start serial order, whose repair carried
|
|
170
|
+
* RSB_length >= B_0) states an RSB_length below B_0; then why. Terminal:
|
|
171
|
+
* every later repair is refused under `repair:fail-closed` (the packet that
|
|
172
|
+
* failed it is counted there too), and block state stays as it was. Sources
|
|
173
|
+
* are still taken and can still complete a block from repair admitted
|
|
174
|
+
* before the failure.
|
|
175
|
+
*/
|
|
176
|
+
failClosed: FecFailClosedReason | null
|
|
142
177
|
}
|
|
143
178
|
|
|
144
179
|
interface BlockState {
|
|
@@ -160,6 +195,8 @@ export class MmtpFecSession {
|
|
|
160
195
|
readonly blockProfile: FecBlockProfile
|
|
161
196
|
readonly algorithm: 'raptorq' | 'reed-solomon'
|
|
162
197
|
readonly sourceSymbols: number
|
|
198
|
+
/** P_0 = B_0, the held total every repair packet is checked against. */
|
|
199
|
+
readonly repairSymbols: number
|
|
163
200
|
readonly symbolSize: number
|
|
164
201
|
/** The decoder's retention, in blocks, derived from the catalog budget. */
|
|
165
202
|
readonly retentionBlocks: number
|
|
@@ -178,6 +215,9 @@ export class MmtpFecSession {
|
|
|
178
215
|
/** The newest SS_ID when the remembered SS_IDs were last pruned. */
|
|
179
216
|
#prunedAt: number | undefined
|
|
180
217
|
#repairPacketId: number | undefined
|
|
218
|
+
#newestSource: number | undefined
|
|
219
|
+
#heldLayers: HeldLayerAdmission
|
|
220
|
+
#anchorHorizon: number
|
|
181
221
|
#stats: MmtpFecSessionStats = {
|
|
182
222
|
sourceSymbols: 0,
|
|
183
223
|
repairSymbols: 0,
|
|
@@ -191,11 +231,12 @@ export class MmtpFecSession {
|
|
|
191
231
|
sourceSymbolsByPath: { moq: 0, multicast: 0 },
|
|
192
232
|
repairSymbolsByPath: { moq: 0, multicast: 0 },
|
|
193
233
|
mixedPath: { clean: 0, recovered: 0, failed: 0, released: 0 },
|
|
234
|
+
failClosed: null,
|
|
194
235
|
}
|
|
195
236
|
|
|
196
237
|
/**
|
|
197
238
|
* @throws when the catalog block lacks or misstates a field the session
|
|
198
|
-
* needs: the block profile, K, T, `interleaveDepthMs` or
|
|
239
|
+
* needs: the block profile, K, P_0 (`repairSymbols`), T, `interleaveDepthMs` or
|
|
199
240
|
* `reorderToleranceMs`. None is defaulted.
|
|
200
241
|
*/
|
|
201
242
|
constructor(config: MmtpFecSessionConfig) {
|
|
@@ -209,6 +250,7 @@ export class MmtpFecSession {
|
|
|
209
250
|
}
|
|
210
251
|
this.algorithm = fec.algorithm === 'reed-solomon' ? 'reed-solomon' : 'raptorq'
|
|
211
252
|
this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, 'fec.sourceSymbols')
|
|
253
|
+
this.repairSymbols = requirePositiveInteger(fec.repairSymbols, 'fec.repairSymbols')
|
|
212
254
|
this.symbolSize = requirePositiveInteger(fec.symbolSize, 'fec.symbolSize')
|
|
213
255
|
const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, 'fec.interleaveDepthMs')
|
|
214
256
|
const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, 'fec.reorderToleranceMs')
|
|
@@ -216,6 +258,18 @@ export class MmtpFecSession {
|
|
|
216
258
|
fecBlockBudgetMs(interleaveDepthMs, reorderToleranceMs),
|
|
217
259
|
interleaveDepthMs,
|
|
218
260
|
)
|
|
261
|
+
this.#heldLayers = new HeldLayerAdmission(
|
|
262
|
+
config.repairLayers ?? [
|
|
263
|
+
{
|
|
264
|
+
layer: 0,
|
|
265
|
+
repairTrackName: typeof fec.repairTrack === 'string' ? fec.repairTrack : '',
|
|
266
|
+
rsIdOffset: 0,
|
|
267
|
+
repairSymbols: this.repairSymbols,
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
config.heldLayers,
|
|
271
|
+
)
|
|
272
|
+
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols)
|
|
219
273
|
if (typeof config.Decoder !== 'function') {
|
|
220
274
|
throw new Error('MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required')
|
|
221
275
|
}
|
|
@@ -241,6 +295,7 @@ export class MmtpFecSession {
|
|
|
241
295
|
sourceSymbolsByPath: { ...this.#stats.sourceSymbolsByPath },
|
|
242
296
|
repairSymbolsByPath: { ...this.#stats.repairSymbolsByPath },
|
|
243
297
|
mixedPath: { ...this.#stats.mixedPath },
|
|
298
|
+
failClosed: this.#stats.failClosed && { ...this.#stats.failClosed },
|
|
244
299
|
}
|
|
245
300
|
}
|
|
246
301
|
|
|
@@ -266,6 +321,7 @@ export class MmtpFecSession {
|
|
|
266
321
|
this.#stats.sourceSymbols++
|
|
267
322
|
this.#stats.sourceSymbolsByPath[path]++
|
|
268
323
|
this.#noteNewest(ssId)
|
|
324
|
+
if (this.#newestSource === undefined || serial32Precedes(this.#newestSource, ssId)) this.#newestSource = ssId
|
|
269
325
|
|
|
270
326
|
const start = this.#blockStartOf(ssId)
|
|
271
327
|
const block = start === undefined ? undefined : this.#blocks.get(start)
|
|
@@ -298,14 +354,17 @@ export class MmtpFecSession {
|
|
|
298
354
|
* Feeds one object of the repair track: a whole MMTP repair packet (ISO/IEC
|
|
299
355
|
* 23008-1 C.5.3). A repair track carries one repair flow, so the first
|
|
300
356
|
* repair packet fixes its packet ID and a packet of any other is refused.
|
|
301
|
-
* `path` is where it was received, as for addSource().
|
|
357
|
+
* `path` is where it was received, as for addSource(). `repairTrackName`
|
|
358
|
+
* is the repair track it was received on, for the 6.3.9 own-track rule; a
|
|
359
|
+
* track the layers do not list is refused as `repair:unlisted-track`.
|
|
302
360
|
*
|
|
303
361
|
* @returns the protected packets this repair symbol recovered, as for
|
|
304
362
|
* addSource().
|
|
305
363
|
*/
|
|
306
|
-
addRepair(packet: Uint8Array, path: MmtpFecSessionPath = 'moq'): Uint8Array[] {
|
|
364
|
+
addRepair(packet: Uint8Array, path: MmtpFecSessionPath = 'moq', repairTrackName?: string): Uint8Array[] {
|
|
307
365
|
const decoder = this.#decoder
|
|
308
366
|
if (!decoder) return this.#drop('repair:released')
|
|
367
|
+
if (this.#stats.failClosed) return this.#drop('repair:fail-closed')
|
|
309
368
|
const pathBit = PATH_BITS[path]
|
|
310
369
|
if (!pathBit) return this.#drop('repair:unknown-path')
|
|
311
370
|
let header
|
|
@@ -328,6 +387,20 @@ export class MmtpFecSession {
|
|
|
328
387
|
if (this.algorithm === 'reed-solomon' && frame.ssbLength + frame.rsbLength > 255) {
|
|
329
388
|
return this.#drop('repair:rs-block-length')
|
|
330
389
|
}
|
|
390
|
+
// fec-01 6.3.9, after the 4.5 discards and the size check, before a block
|
|
391
|
+
// is placed. Without a track name only the anchored rule (b) applies.
|
|
392
|
+
const verdict = this.#heldLayers.admit(
|
|
393
|
+
repairTrackName,
|
|
394
|
+
frame.ssStart,
|
|
395
|
+
frame.rsbLength,
|
|
396
|
+
this.#anchorHorizon,
|
|
397
|
+
this.#newestSource,
|
|
398
|
+
)
|
|
399
|
+
if (verdict === 'discard') return this.#drop('repair:unlisted-track')
|
|
400
|
+
if (verdict !== undefined) {
|
|
401
|
+
this.#stats.failClosed = verdict
|
|
402
|
+
return this.#drop('repair:fail-closed')
|
|
403
|
+
}
|
|
331
404
|
|
|
332
405
|
const start = frame.ssStart
|
|
333
406
|
let block = this.#blocks.get(start)
|