@blockcast/mmt-container 2.2.0-main.c4fa878f4ff4 → 2.2.0-main.da2e7cd1dd5d

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 (62) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +241 -149
  3. package/dist/browser-runtime.js.map +3 -3
  4. package/dist/catalog-fec-params.d.ts +17 -1
  5. package/dist/catalog-fec-params.d.ts.map +1 -1
  6. package/dist/catalog-fec-params.js +15 -0
  7. package/dist/catalog-fec-params.js.map +2 -2
  8. package/dist/cmcd-producer.d.ts +60 -47
  9. package/dist/cmcd-producer.d.ts.map +1 -1
  10. package/dist/cmcd-producer.js +138 -109
  11. package/dist/cmcd-producer.js.map +2 -2
  12. package/dist/delivery-catalog.js +24 -15
  13. package/dist/delivery-catalog.js.map +2 -2
  14. package/dist/delta-fallback-config.js.map +1 -1
  15. package/dist/fec-manager.d.ts +3 -1
  16. package/dist/fec-manager.d.ts.map +1 -1
  17. package/dist/fec-manager.js +26 -4
  18. package/dist/fec-manager.js.map +2 -2
  19. package/dist/fec-worker-setup.d.ts +1 -1
  20. package/dist/fec-worker-setup.js +1 -1
  21. package/dist/fec-worker-setup.js.map +1 -1
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +5 -1
  25. package/dist/index.js.map +2 -2
  26. package/dist/media-router.js.map +1 -1
  27. package/dist/mmtp-container-parser.d.ts +2 -0
  28. package/dist/mmtp-container-parser.d.ts.map +1 -1
  29. package/dist/mmtp-container-parser.js +4 -0
  30. package/dist/mmtp-container-parser.js.map +2 -2
  31. package/dist/mmtp-container-parser.no-modules.js +4 -0
  32. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  33. package/dist/mmtp-decode-pipeline.js +4 -0
  34. package/dist/mmtp-decode-pipeline.js.map +2 -2
  35. package/dist/mmtp-router.d.ts.map +1 -1
  36. package/dist/mmtp-router.js +38 -15
  37. package/dist/mmtp-router.js.map +2 -2
  38. package/dist/multicast-manager.js +64 -19
  39. package/dist/multicast-manager.js.map +2 -2
  40. package/dist/multicast-transport.js +24 -15
  41. package/dist/multicast-transport.js.map +2 -2
  42. package/dist/no-modules.js +4 -0
  43. package/dist/no-modules.js.map +2 -2
  44. package/dist/repair-layer-subscriptions.d.ts +6 -2
  45. package/dist/repair-layer-subscriptions.d.ts.map +1 -1
  46. package/dist/repair-layer-subscriptions.js +6 -3
  47. package/dist/repair-layer-subscriptions.js.map +2 -2
  48. package/dist/track-session.d.ts +1 -1
  49. package/dist/track-session.js.map +1 -1
  50. package/dist/wasm-mmtp-container-parser.js +4 -0
  51. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  52. package/package.json +8 -5
  53. package/schemas/catalog.schema.json +4 -0
  54. package/src/catalog-fec-params.ts +36 -1
  55. package/src/cmcd-producer.ts +189 -179
  56. package/src/fec-manager.ts +26 -4
  57. package/src/fec-worker-setup.ts +2 -2
  58. package/src/index.ts +6 -2
  59. package/src/mmtp-container-parser.ts +12 -0
  60. package/src/mmtp-router.ts +21 -0
  61. package/src/repair-layer-subscriptions.ts +13 -5
  62. package/src/track-session.ts +1 -1
@@ -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
- if (!this.#hasCatalogTimingForPacket(packetId)) return false
3477
- return this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.mpuBoundsRaps ?? true
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) || !this.#hasCatalogTimingForPacket(packetId)) return
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()
@@ -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 `x-bc-fec-p`, because the
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 `x-bc-fec-p` are read from this. Identity on a single layer.
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
@@ -526,10 +526,13 @@ export {
526
526
  type FecStarvationDetectorConfig,
527
527
  } from './fec-starvation-detector.js'
528
528
 
529
- // CMCD Producer (x-bc-* vendor key telemetry, BLO-1044)
529
+ // CMCD v2 Event-mode producer (CTA-5004-B, com.blockcast-* custom keys)
530
530
  export {
531
531
  CmcdProducer,
532
- type CmcdParams,
532
+ encodeCmcdRecord,
533
+ type CmcdRecord,
534
+ type CmcdToken,
535
+ type CmcdValue,
533
536
  type CmcdProducerOptions,
534
537
  type SnapshotData,
535
538
  type TransportSnapshot,
@@ -546,6 +549,7 @@ export {
546
549
  type TrackFecParams,
547
550
  type TrackFecUnavailable,
548
551
  catalogTrackFecUnavailable,
552
+ catalogTrackDeliveryMode,
549
553
  catalogGroupDurationMs,
550
554
  catalogKeyframeCadenceMs,
551
555
  catalogFecMediaUnitsPerSecond,
@@ -2146,6 +2146,18 @@ export class MmtpContainerParser {
2146
2146
  this.#emitFrame(data, meta)
2147
2147
  }
2148
2148
 
2149
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
2150
+ hasAuthoredTimingFor(
2151
+ packetId: number,
2152
+ mpuSequenceNumber: number,
2153
+ movieFragmentSequenceNumber: number,
2154
+ sampleNumber: number,
2155
+ catalogTrackName?: string,
2156
+ ): boolean {
2157
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)
2158
+ ?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null
2159
+ }
2160
+
2149
2161
  /**
2150
2162
  * Claim and return the authored timing for a recovered sample. Recovery does
2151
2163
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -5206,6 +5206,27 @@ export class MmtpRouter {
5206
5206
  );
5207
5207
  }
5208
5208
 
5209
+ // A recovered head whose sample FT=1 has not named yet (an overlay that
5210
+ // outran the source path) would complete its data unit untimed: the unit
5211
+ // is dropped, marked completed, and the timed source copy is then refused
5212
+ // (BLO-53342). Leave it out; its recovered continuations park and complete
5213
+ // with the source head. A head that is truly lost ends as it did before.
5214
+ result = {
5215
+ ...result,
5216
+ symbols: result.symbols.filter((sym) =>
5217
+ sym.fragmentType !== 2 ||
5218
+ sym.fragment.fragmentationIndicator !== 1 ||
5219
+ sym.fragment.movieFragmentSequenceNumber === undefined ||
5220
+ sym.fragment.sampleNumber === undefined ||
5221
+ this.#parser.hasAuthoredTimingFor(
5222
+ ownerPacketId,
5223
+ sym.fragment.mpuSequenceNumber,
5224
+ sym.fragment.movieFragmentSequenceNumber,
5225
+ sym.fragment.sampleNumber,
5226
+ catalogTrackName,
5227
+ )),
5228
+ };
5229
+
5209
5230
  // MSE assembler path: route recovered block as pre-assembled frames (skip FT=0)
5210
5231
  // BLO-3035 layer 3: recovered symbols are RAW MFU fragments, not complete
5211
5232
  // frames. A keyframe can span multiple packets (FI=1/2/3); pushing each
@@ -15,7 +15,10 @@
15
15
  * base (`MmtpRouter.feedRepairPacket(..., repairTrackName)`).
16
16
  *
17
17
  * `stream` delivery means QUIC retransmits: no repair track is ever
18
- * subscribed (FEC only for unreliable transports).
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.
19
22
  *
20
23
  * ABR switch R -> R' (`switchSource`): R's subscriptions are closed, the layer
21
24
  * count carries to R', and any packet R's subscriptions deliver after the
@@ -35,6 +38,8 @@ export interface RepairLayerSource {
35
38
  repairLayers?: readonly RepairLayerParams[]
36
39
  /** Keyframe overlays (`TrackFecParams.repairOverlays`); at most one is used. */
37
40
  repairOverlays?: readonly RepairOverlayParams[]
41
+ /** The publisher's MoQ delivery mode; absent is `datagram`. */
42
+ deliveryMode?: MoqRepairDelivery
38
43
  }
39
44
 
40
45
  export interface RepairTrackSubscription {
@@ -43,7 +48,6 @@ export interface RepairTrackSubscription {
43
48
 
44
49
  export interface RepairLayerSubscriptionsOptions {
45
50
  source: RepairLayerSource
46
- delivery: MoqRepairDelivery
47
51
  /** Subscribe one repair track; every object payload goes to `onPacket`. */
48
52
  subscribe(
49
53
  repairTrackName: string,
@@ -110,7 +114,7 @@ export class RepairLayerSubscriptions {
110
114
  * or interleaved sequences read as loss.
111
115
  */
112
116
  noteSourceSequence(seq: number): void {
113
- if (this.#disposed || this.#opts.delivery === 'stream') return
117
+ if (this.#disposed || this.#delivery() === 'stream') return
114
118
  seq >>>= 0
115
119
  if (this.#lastSeq === undefined) {
116
120
  this.#lastSeq = seq
@@ -152,13 +156,13 @@ export class RepairLayerSubscriptions {
152
156
  this.#expected = 0
153
157
  this.#lost = 0
154
158
  this.#wantFewer = 0
155
- this.#layerCount = Math.min(this.#layerCount, this.#layers().length)
159
+ this.#layerCount = this.#delivery() === 'stream' ? 0 : Math.min(this.#layerCount, this.#layers().length)
156
160
  this.#reconcile()
157
161
  }
158
162
 
159
163
  snapshot(): RepairLayerSubscriptionsSnapshot {
160
164
  return {
161
- delivery: this.#opts.delivery,
165
+ delivery: this.#delivery(),
162
166
  layerCount: this.#layerCount,
163
167
  subscribedTracks: [...this.#subs.keys()],
164
168
  repairPackets: this.#repairPackets,
@@ -174,6 +178,10 @@ export class RepairLayerSubscriptions {
174
178
  this.#disposed = true
175
179
  }
176
180
 
181
+ #delivery(): MoqRepairDelivery {
182
+ return this.#source.deliveryMode === 'stream' ? 'stream' : 'datagram'
183
+ }
184
+
177
185
  #layers(): readonly RepairLayerParams[] {
178
186
  return this.#source.repairLayers ?? []
179
187
  }
@@ -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