@blockcast/mmt-container 2.2.0-main.73af0eedcbc8 → 2.2.0-main.75b39a2cdddf

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.
Files changed (93) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1001 -215
  3. package/dist/browser-runtime.js.map +4 -4
  4. package/dist/carp-catalog.js.map +1 -1
  5. package/dist/catalog-fec-params.d.ts +17 -1
  6. package/dist/catalog-fec-params.d.ts.map +1 -1
  7. package/dist/catalog-fec-params.js +15 -0
  8. package/dist/catalog-fec-params.js.map +2 -2
  9. package/dist/catalog-network-source.js.map +1 -1
  10. package/dist/cmcd-producer.d.ts +60 -47
  11. package/dist/cmcd-producer.d.ts.map +1 -1
  12. package/dist/cmcd-producer.js +138 -109
  13. package/dist/cmcd-producer.js.map +2 -2
  14. package/dist/delivery-catalog.js +26 -16
  15. package/dist/delivery-catalog.js.map +2 -2
  16. package/dist/delta-fallback-config.js.map +1 -1
  17. package/dist/fec-manager.d.ts +33 -1
  18. package/dist/fec-manager.d.ts.map +1 -1
  19. package/dist/fec-manager.js +222 -6
  20. package/dist/fec-manager.js.map +2 -2
  21. package/dist/fec-state.js.map +1 -1
  22. package/dist/fec-worker-setup.d.ts +1 -1
  23. package/dist/fec-worker-setup.js +1 -1
  24. package/dist/fec-worker-setup.js.map +1 -1
  25. package/dist/held-layer-admission.js.map +1 -1
  26. package/dist/index.d.ts +5 -4
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -1
  29. package/dist/index.js.map +2 -2
  30. package/dist/iso-block-locator.d.ts +9 -0
  31. package/dist/iso-block-locator.d.ts.map +1 -1
  32. package/dist/iso-block-locator.js +17 -0
  33. package/dist/iso-block-locator.js.map +2 -2
  34. package/dist/iso-bmff-timing.d.ts +10 -1
  35. package/dist/iso-bmff-timing.d.ts.map +1 -1
  36. package/dist/iso-bmff-timing.js +36 -2
  37. package/dist/iso-bmff-timing.js.map +2 -2
  38. package/dist/media-router.js.map +1 -1
  39. package/dist/mfu-reassembler.js +12 -19
  40. package/dist/mfu-reassembler.js.map +2 -2
  41. package/dist/mmtp-container-parser.d.ts +58 -10
  42. package/dist/mmtp-container-parser.d.ts.map +1 -1
  43. package/dist/mmtp-container-parser.js +105 -34
  44. package/dist/mmtp-container-parser.js.map +2 -2
  45. package/dist/mmtp-container-parser.no-modules.js +229 -51
  46. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  47. package/dist/mmtp-decode-pipeline.js +104 -34
  48. package/dist/mmtp-decode-pipeline.js.map +2 -2
  49. package/dist/mmtp-fec-session.d.ts +41 -3
  50. package/dist/mmtp-fec-session.d.ts.map +1 -1
  51. package/dist/mmtp-fec-session.js +118 -13
  52. package/dist/mmtp-fec-session.js.map +3 -3
  53. package/dist/mmtp-router.d.ts +18 -1
  54. package/dist/mmtp-router.d.ts.map +1 -1
  55. package/dist/mmtp-router.js +323 -64
  56. package/dist/mmtp-router.js.map +2 -2
  57. package/dist/multicast-auth-catalog.js.map +1 -1
  58. package/dist/multicast-input.js.map +1 -1
  59. package/dist/multicast-manager.js +547 -72
  60. package/dist/multicast-manager.js.map +2 -2
  61. package/dist/multicast-transport.js +28 -18
  62. package/dist/multicast-transport.js.map +2 -2
  63. package/dist/multicast-unwrap.js +12 -19
  64. package/dist/multicast-unwrap.js.map +2 -2
  65. package/dist/no-modules.js +222 -48
  66. package/dist/no-modules.js.map +3 -3
  67. package/dist/repair-fetch.js.map +1 -1
  68. package/dist/repair-layer-subscriptions.d.ts +82 -0
  69. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  70. package/dist/repair-layer-subscriptions.js +192 -0
  71. package/dist/repair-layer-subscriptions.js.map +7 -0
  72. package/dist/signaling.js.map +1 -1
  73. package/dist/timestamp-codec.js +12 -19
  74. package/dist/timestamp-codec.js.map +2 -2
  75. package/dist/track-session.d.ts +1 -1
  76. package/dist/track-session.js.map +1 -1
  77. package/dist/wasm-mfu-reassembler.js.map +1 -1
  78. package/dist/wasm-mmtp-container-parser.js +104 -34
  79. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  80. package/package.json +8 -5
  81. package/schemas/catalog.schema.json +4 -0
  82. package/src/catalog-fec-params.ts +36 -1
  83. package/src/cmcd-producer.ts +189 -179
  84. package/src/fec-manager.ts +277 -6
  85. package/src/fec-worker-setup.ts +2 -2
  86. package/src/index.ts +18 -2
  87. package/src/iso-block-locator.ts +19 -0
  88. package/src/iso-bmff-timing.ts +40 -1
  89. package/src/mmtp-container-parser.ts +125 -19
  90. package/src/mmtp-fec-session.ts +115 -15
  91. package/src/mmtp-router.ts +350 -13
  92. package/src/repair-layer-subscriptions.ts +261 -0
  93. package/src/track-session.ts +1 -1
@@ -0,0 +1,261 @@
1
+ /**
2
+ * MoQ repair-layer subscriptions by measured loss (BLO-35456).
3
+ *
4
+ * MoQ is per-track subscribe, so a repair layer the receiver does not
5
+ * subscribe costs nothing past the relay. This is the mechanism that decides
6
+ * which of a source's repair tracks to hold: it measures loss from the MMTP
7
+ * `packet_sequence_number` gaps the receiver already sees on the source track
8
+ * (no new signalling), holds base layers 0..n-1 (draft-ramadan-moq-fec 6.3,
9
+ * `resolveRepairLayers`), and holds the keyframe overlay track while any base
10
+ * layer is held. The policy here is deliberately the simplest per-layer
11
+ * add/drop; the join policy proper is BLO-35457.
12
+ *
13
+ * Every repair packet is handed to `feed` with the repair track it arrived on,
14
+ * so libmmt routes an overlay packet to the overlay instance and never to the
15
+ * base (`MmtpRouter.feedRepairPacket(..., repairTrackName)`).
16
+ *
17
+ * `stream` delivery means QUIC retransmits: no repair track is ever
18
+ * subscribed (FEC only for unreliable transports). The mode is the source's
19
+ * `deliveryMode` (`catalogTrackDeliveryMode`); absent is `datagram`. It is
20
+ * read from the current source, so on a catalog refresh re-feed the updated
21
+ * source through `switchSource` for a changed `deliveryMode` to take effect.
22
+ *
23
+ * ABR switch R -> R' (`switchSource`): R's subscriptions are closed, the layer
24
+ * count carries to R', and any packet R's subscriptions deliver after the
25
+ * switch is dropped, never fed.
26
+ */
27
+
28
+ import { serial32Distance } from '@blockcast/mmt-base'
29
+ import type { RepairLayerParams, RepairOverlayParams } from './repair-layers.js'
30
+
31
+ export type MoqRepairDelivery = 'datagram' | 'stream'
32
+
33
+ /** The source's FEC shape, as `fecParams` resolves it. */
34
+ export interface RepairLayerSource {
35
+ /** K: source symbols per base block. */
36
+ fecK: number
37
+ /** Base layers, layer 0 first (`TrackFecParams.repairLayers`). */
38
+ repairLayers?: readonly RepairLayerParams[]
39
+ /** Keyframe overlays (`TrackFecParams.repairOverlays`); at most one is used. */
40
+ repairOverlays?: readonly RepairOverlayParams[]
41
+ /** The publisher's MoQ delivery mode; absent is `datagram`. */
42
+ deliveryMode?: MoqRepairDelivery
43
+ }
44
+
45
+ export interface RepairTrackSubscription {
46
+ close(): void
47
+ }
48
+
49
+ export interface RepairLayerSubscriptionsOptions {
50
+ source: RepairLayerSource
51
+ /** Subscribe one repair track; every object payload goes to `onPacket`. */
52
+ subscribe(
53
+ repairTrackName: string,
54
+ priority: number,
55
+ onPacket: (packet: Uint8Array) => void,
56
+ ): RepairTrackSubscription
57
+ /** Hand one repair packet to the decoder, e.g. `processRepairFromMoQ`. */
58
+ feed(packet: Uint8Array, repairTrackName: string): void
59
+ /** Expected source packets per loss window. Default 500. */
60
+ windowPackets?: number
61
+ /** Repair held per unit of measured loss. Default 2. */
62
+ margin?: number
63
+ /** Consecutive windows wanting fewer layers before one is dropped. Default 3. */
64
+ dropAfterWindows?: number
65
+ }
66
+
67
+ export interface RepairLayerSubscriptionsSnapshot {
68
+ delivery: MoqRepairDelivery
69
+ layerCount: number
70
+ subscribedTracks: string[]
71
+ repairPackets: number
72
+ repairBytes: number
73
+ /** Packets a closed subscription delivered after its switch or drop. */
74
+ staleDropped: number
75
+ /** Loss rate of the last closed window; null before the first. */
76
+ lossRate: number | null
77
+ }
78
+
79
+ const SERIAL32_HALF = 0x80000000
80
+
81
+ export class RepairLayerSubscriptions {
82
+ readonly #opts: RepairLayerSubscriptionsOptions
83
+ readonly #windowPackets: number
84
+ readonly #margin: number
85
+ readonly #dropAfterWindows: number
86
+ #source: RepairLayerSource
87
+ #layerCount = 0
88
+ #subs = new Map<string, { sub: RepairTrackSubscription }>()
89
+ #disposed = false
90
+ #lastSeq: number | undefined
91
+ #expected = 0
92
+ #lost = 0
93
+ #wantFewer = 0
94
+ #lossRate: number | null = null
95
+ #repairPackets = 0
96
+ #repairBytes = 0
97
+ #staleDropped = 0
98
+
99
+ constructor(opts: RepairLayerSubscriptionsOptions) {
100
+ this.#opts = opts
101
+ this.#source = opts.source
102
+ this.#windowPackets = opts.windowPackets ?? 500
103
+ this.#margin = opts.margin ?? 2
104
+ this.#dropAfterWindows = opts.dropAfterWindows ?? 3
105
+ }
106
+
107
+ get layerCount(): number {
108
+ return this.#layerCount
109
+ }
110
+
111
+ /**
112
+ * One source packet's MMTP `packet_sequence_number`, in arrival order.
113
+ * The sequence is per `packet_id`: pass the packets of ONE packet_id only,
114
+ * or interleaved sequences read as loss.
115
+ */
116
+ noteSourceSequence(seq: number): void {
117
+ if (this.#disposed || this.#delivery() === 'stream') return
118
+ seq >>>= 0
119
+ if (this.#lastSeq === undefined) {
120
+ this.#lastSeq = seq
121
+ return
122
+ }
123
+ const d = serial32Distance(this.#lastSeq, seq)
124
+ if (d === 0) return
125
+ const back = d >= SERIAL32_HALF ? 2 ** 32 - d : 0
126
+ if (d > this.#windowPackets && (back === 0 || back > this.#windowPackets)) {
127
+ // A step wider than one window, either way, is a discontinuity (a
128
+ // publisher restart), not loss or reorder: restart the window from here.
129
+ // ponytail: a real outage longer than a window is also read as a restart;
130
+ // FEC cannot repair that anyway.
131
+ this.#lastSeq = seq
132
+ this.#expected = 0
133
+ this.#lost = 0
134
+ return
135
+ }
136
+ if (back === 0) {
137
+ this.#expected += d
138
+ this.#lost += d - 1
139
+ this.#lastSeq = seq
140
+ } else if (this.#lost > 0) {
141
+ // A late packet fills a hole counted as lost. Holes are not tracked
142
+ // individually, so one whose window already closed cancels a hole in
143
+ // this window instead: a bias toward lower loss of at most the reorder
144
+ // depth per window.
145
+ this.#lost -= 1
146
+ }
147
+ if (this.#expected >= this.#windowPackets) this.#closeWindow()
148
+ }
149
+
150
+ /** ABR switch R -> R': close R's tracks, carry the layer count to R'. */
151
+ switchSource(next: RepairLayerSource): void {
152
+ if (this.#disposed) return
153
+ this.#closeAll()
154
+ this.#source = next
155
+ this.#lastSeq = undefined
156
+ this.#expected = 0
157
+ this.#lost = 0
158
+ this.#wantFewer = 0
159
+ this.#layerCount = this.#delivery() === 'stream' ? 0 : Math.min(this.#layerCount, this.#layers().length)
160
+ this.#reconcile()
161
+ }
162
+
163
+ snapshot(): RepairLayerSubscriptionsSnapshot {
164
+ return {
165
+ delivery: this.#delivery(),
166
+ layerCount: this.#layerCount,
167
+ subscribedTracks: [...this.#subs.keys()],
168
+ repairPackets: this.#repairPackets,
169
+ repairBytes: this.#repairBytes,
170
+ staleDropped: this.#staleDropped,
171
+ lossRate: this.#lossRate,
172
+ }
173
+ }
174
+
175
+ dispose(): void {
176
+ if (this.#disposed) return
177
+ this.#closeAll()
178
+ this.#disposed = true
179
+ }
180
+
181
+ #delivery(): MoqRepairDelivery {
182
+ return this.#source.deliveryMode === 'stream' ? 'stream' : 'datagram'
183
+ }
184
+
185
+ #layers(): readonly RepairLayerParams[] {
186
+ return this.#source.repairLayers ?? []
187
+ }
188
+
189
+ #closeWindow(): void {
190
+ const rate = this.#lost / this.#expected
191
+ this.#lossRate = rate
192
+ this.#expected = 0
193
+ this.#lost = 0
194
+ const target = this.#targetLayers(rate)
195
+ if (target > this.#layerCount) {
196
+ this.#wantFewer = 0
197
+ this.#layerCount = target
198
+ } else if (target < this.#layerCount) {
199
+ this.#wantFewer += 1
200
+ if (this.#wantFewer >= this.#dropAfterWindows) {
201
+ this.#wantFewer = 0
202
+ this.#layerCount -= 1
203
+ }
204
+ } else {
205
+ this.#wantFewer = 0
206
+ }
207
+ this.#reconcile()
208
+ }
209
+
210
+ /** Fewest base layers whose summed P covers margin x the measured loss of K. */
211
+ #targetLayers(rate: number): number {
212
+ if (rate <= 0) return 0
213
+ const needed = Math.ceil(rate * this.#source.fecK * this.#margin)
214
+ const layers = this.#layers()
215
+ let held = 0
216
+ for (let n = 0; n < layers.length; n++) {
217
+ held += layers[n]!.repairSymbols
218
+ if (held >= needed) return n + 1
219
+ }
220
+ return layers.length
221
+ }
222
+
223
+ /** Stream delivery never leaves layer count 0, so it never subscribes. */
224
+ #reconcile(): void {
225
+ const want = new Map<string, number>()
226
+ for (const layer of this.#layers().slice(0, this.#layerCount)) {
227
+ want.set(layer.repairTrackName, layer.priority)
228
+ }
229
+ const overlay = this.#source.repairOverlays?.[0]
230
+ if (this.#layerCount > 0 && overlay) want.set(overlay.repairTrackName, overlay.priority)
231
+
232
+ for (const [name, entry] of this.#subs) {
233
+ if (!want.has(name)) {
234
+ entry.sub.close()
235
+ this.#subs.delete(name)
236
+ }
237
+ }
238
+ for (const [name, priority] of want) {
239
+ if (this.#subs.has(name)) continue
240
+ const entry = {} as { sub: RepairTrackSubscription }
241
+ entry.sub = this.#opts.subscribe(name, priority, (packet) => this.#onPacket(name, entry, packet))
242
+ this.#subs.set(name, entry)
243
+ }
244
+ }
245
+
246
+ /** Fenced on the entry itself, so a closed subscription is stale even if its track is re-added. */
247
+ #onPacket(name: string, entry: { sub: RepairTrackSubscription }, packet: Uint8Array): void {
248
+ if (this.#disposed || this.#subs.get(name) !== entry) {
249
+ this.#staleDropped += 1
250
+ return
251
+ }
252
+ this.#repairPackets += 1
253
+ this.#repairBytes += packet.byteLength
254
+ this.#opts.feed(packet, name)
255
+ }
256
+
257
+ #closeAll(): void {
258
+ for (const entry of this.#subs.values()) entry.sub.close()
259
+ this.#subs.clear()
260
+ }
261
+ }
@@ -58,7 +58,7 @@ export interface TrackSessionConfig {
58
58
  * This is runtime join state, not catalog geometry: a receiver on a clean
59
59
  * path joins layer 0 only while a lossy one adds layers, so the catalog's
60
60
  * total P is not what this receiver is receiving. Diagnostics that report P
61
- * (the worker snapshot, CMCD `x-bc-fec-p`) must read this.
61
+ * (the worker snapshot, CMCD `com.blockcast-fp`) must read this.
62
62
  *
63
63
  * Absent when the caller joins no layers explicitly, in which case P_0 is
64
64
  * what was joined. That is an identity on a single-layer track, not a