@blockcast/mmt-transport 0.2.2 → 0.3.0

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 (167) hide show
  1. package/README.md +44 -58
  2. package/dist/abr-controller.d.ts +34 -4
  3. package/dist/abr-controller.d.ts.map +1 -1
  4. package/dist/abr-controller.js +66 -17
  5. package/dist/abr-controller.js.map +1 -1
  6. package/dist/amt-gateway.d.ts.map +1 -1
  7. package/dist/amt-gateway.js +3 -4
  8. package/dist/amt-gateway.js.map +1 -1
  9. package/dist/driad-discovery.d.ts.map +1 -1
  10. package/dist/driad-discovery.js +3 -6
  11. package/dist/driad-discovery.js.map +1 -1
  12. package/dist/endpoint-config.d.ts +68 -0
  13. package/dist/endpoint-config.d.ts.map +1 -1
  14. package/dist/endpoint-config.js +118 -3
  15. package/dist/endpoint-config.js.map +1 -1
  16. package/dist/index.d.ts +10 -8
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +8 -11
  19. package/dist/index.js.map +1 -1
  20. package/dist/mmt-transport.no-modules.js +881 -60
  21. package/dist/mmt-transport.no-modules.js.map +4 -4
  22. package/dist/moqt-priority.d.ts +39 -0
  23. package/dist/moqt-priority.d.ts.map +1 -0
  24. package/dist/moqt-priority.js +41 -0
  25. package/dist/moqt-priority.js.map +1 -0
  26. package/dist/multicast-catalog-policy.d.ts +96 -0
  27. package/dist/multicast-catalog-policy.d.ts.map +1 -0
  28. package/dist/multicast-catalog-policy.js +660 -0
  29. package/dist/multicast-catalog-policy.js.map +1 -0
  30. package/dist/multicast-orchestrator.d.ts +24 -0
  31. package/dist/multicast-orchestrator.d.ts.map +1 -1
  32. package/dist/multicast-orchestrator.js +102 -9
  33. package/dist/multicast-orchestrator.js.map +1 -1
  34. package/dist/multicast-signaling-reconciler.d.ts +37 -0
  35. package/dist/multicast-signaling-reconciler.d.ts.map +1 -0
  36. package/dist/multicast-signaling-reconciler.js +161 -0
  37. package/dist/multicast-signaling-reconciler.js.map +1 -0
  38. package/dist/multicast-subscription-pool.d.ts.map +1 -1
  39. package/dist/multicast-subscription-pool.js +37 -10
  40. package/dist/multicast-subscription-pool.js.map +1 -1
  41. package/dist/multicast-tier.d.ts +19 -0
  42. package/dist/multicast-tier.d.ts.map +1 -1
  43. package/dist/multicast-tier.js +21 -4
  44. package/dist/multicast-tier.js.map +1 -1
  45. package/dist/no-modules.d.ts +40 -3
  46. package/dist/no-modules.d.ts.map +1 -1
  47. package/dist/no-modules.js +30 -3
  48. package/dist/no-modules.js.map +1 -1
  49. package/dist/packet-batch.d.ts +13 -0
  50. package/dist/packet-batch.d.ts.map +1 -0
  51. package/dist/packet-batch.js +14 -0
  52. package/dist/packet-batch.js.map +1 -0
  53. package/dist/stall-detector.d.ts +10 -0
  54. package/dist/stall-detector.d.ts.map +1 -1
  55. package/dist/stall-detector.js +21 -0
  56. package/dist/stall-detector.js.map +1 -1
  57. package/dist/takeover.d.ts +3 -0
  58. package/dist/takeover.d.ts.map +1 -1
  59. package/dist/takeover.js +35 -2
  60. package/dist/takeover.js.map +1 -1
  61. package/dist/transport-manager.d.ts +21 -17
  62. package/dist/transport-manager.d.ts.map +1 -1
  63. package/dist/transport-manager.js +63 -29
  64. package/dist/transport-manager.js.map +1 -1
  65. package/dist/types.d.ts +98 -123
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/types.js +12 -50
  68. package/dist/types.js.map +1 -1
  69. package/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
  70. package/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
  71. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
  72. package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
  73. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
  74. package/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
  75. package/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
  76. package/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
  77. package/node_modules/@blockcast/mmt-base/README.md +134 -0
  78. package/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
  79. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts +55 -0
  80. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts.map +1 -0
  81. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js +128 -0
  82. package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js.map +1 -0
  83. package/node_modules/@blockcast/mmt-base/dist/index.d.ts +7 -0
  84. package/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
  85. package/node_modules/@blockcast/mmt-base/dist/index.js +7 -0
  86. package/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
  87. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +292 -0
  88. package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
  89. package/node_modules/@blockcast/mmt-base/dist/mmtp.js +612 -0
  90. package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
  91. package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts +18 -0
  92. package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts.map +1 -0
  93. package/node_modules/@blockcast/mmt-base/dist/net-constants.js +18 -0
  94. package/node_modules/@blockcast/mmt-base/dist/net-constants.js.map +1 -0
  95. package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts +47 -0
  96. package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts.map +1 -0
  97. package/node_modules/@blockcast/mmt-base/dist/ntp-short.js +54 -0
  98. package/node_modules/@blockcast/mmt-base/dist/ntp-short.js.map +1 -0
  99. package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts +57 -0
  100. package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts.map +1 -0
  101. package/node_modules/@blockcast/mmt-base/dist/serial-number.js +98 -0
  102. package/node_modules/@blockcast/mmt-base/dist/serial-number.js.map +1 -0
  103. package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts +312 -0
  104. package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts.map +1 -0
  105. package/node_modules/@blockcast/mmt-base/dist/signalling.js +637 -0
  106. package/node_modules/@blockcast/mmt-base/dist/signalling.js.map +1 -0
  107. package/node_modules/@blockcast/mmt-base/package.json +49 -0
  108. package/node_modules/@blockcast/mmt-base/src/fec-block-profile.ts +145 -0
  109. package/node_modules/@blockcast/mmt-base/src/index.ts +6 -0
  110. package/node_modules/@blockcast/mmt-base/src/mmtp.ts +892 -0
  111. package/node_modules/@blockcast/mmt-base/src/net-constants.ts +17 -0
  112. package/node_modules/@blockcast/mmt-base/src/ntp-short.ts +67 -0
  113. package/node_modules/@blockcast/mmt-base/src/serial-number.ts +107 -0
  114. package/node_modules/@blockcast/mmt-base/src/signalling.ts +862 -0
  115. package/package.json +13 -6
  116. package/src/abr-controller.ts +81 -17
  117. package/src/amt-gateway.ts +3 -5
  118. package/src/driad-discovery.ts +3 -6
  119. package/src/endpoint-config.ts +157 -3
  120. package/src/index.ts +54 -33
  121. package/src/moqt-priority.ts +51 -0
  122. package/src/multicast-catalog-policy.ts +753 -0
  123. package/src/multicast-orchestrator.ts +114 -10
  124. package/src/multicast-signaling-reconciler.ts +156 -0
  125. package/src/multicast-subscription-pool.ts +46 -11
  126. package/src/multicast-tier.ts +30 -3
  127. package/src/no-modules.ts +54 -2
  128. package/src/packet-batch.ts +17 -0
  129. package/src/stall-detector.ts +37 -0
  130. package/src/takeover.ts +41 -3
  131. package/src/transport-manager.ts +64 -34
  132. package/src/types.ts +103 -160
  133. package/dist/endpoint-config.test.d.ts +0 -2
  134. package/dist/endpoint-config.test.d.ts.map +0 -1
  135. package/dist/endpoint-config.test.js +0 -39
  136. package/dist/endpoint-config.test.js.map +0 -1
  137. package/dist/fec-client.d.ts +0 -442
  138. package/dist/fec-client.d.ts.map +0 -1
  139. package/dist/fec-client.js +0 -789
  140. package/dist/fec-client.js.map +0 -1
  141. package/dist/fec-client.test.d.ts +0 -8
  142. package/dist/fec-client.test.d.ts.map +0 -1
  143. package/dist/fec-client.test.js +0 -112
  144. package/dist/fec-client.test.js.map +0 -1
  145. package/dist/multicast-subscription-pool.test.d.ts +0 -2
  146. package/dist/multicast-subscription-pool.test.d.ts.map +0 -1
  147. package/dist/multicast-subscription-pool.test.js +0 -108
  148. package/dist/multicast-subscription-pool.test.js.map +0 -1
  149. package/dist/multicast-tier.test.d.ts +0 -2
  150. package/dist/multicast-tier.test.d.ts.map +0 -1
  151. package/dist/multicast-tier.test.js +0 -75
  152. package/dist/multicast-tier.test.js.map +0 -1
  153. package/dist/stall-detector.test.d.ts +0 -2
  154. package/dist/stall-detector.test.d.ts.map +0 -1
  155. package/dist/stall-detector.test.js +0 -33
  156. package/dist/stall-detector.test.js.map +0 -1
  157. package/dist/transport-manager.test.d.ts +0 -2
  158. package/dist/transport-manager.test.d.ts.map +0 -1
  159. package/dist/transport-manager.test.js +0 -155
  160. package/dist/transport-manager.test.js.map +0 -1
  161. package/src/endpoint-config.test.ts +0 -54
  162. package/src/fec-client.test.ts +0 -140
  163. package/src/fec-client.ts +0 -1103
  164. package/src/multicast-subscription-pool.test.ts +0 -115
  165. package/src/multicast-tier.test.ts +0 -80
  166. package/src/stall-detector.test.ts +0 -34
  167. package/src/transport-manager.test.ts +0 -192
package/src/takeover.ts CHANGED
@@ -28,7 +28,12 @@ export function makeSourceSwitchHandler(options: {
28
28
  };
29
29
  }
30
30
 
31
- export interface LossArcSession { lossDetected(): void; lossRecovered(): void; }
31
+ export interface LossArcSession {
32
+ /** TrackSession state. Optional so non-TrackSession hosts keep the generic API. */
33
+ readonly state?: string;
34
+ lossDetected(): void;
35
+ lossRecovered(): void;
36
+ }
32
37
  export interface FecStarvationSnapshot {
33
38
  sourceSymbols: number;
34
39
  repairSymbols: number;
@@ -57,15 +62,48 @@ export async function wireStarvationDetector(args: {
57
62
  trackName: string;
58
63
  fecInterleaveMs: number;
59
64
  fecK: number;
65
+ deliveryWindowMs?: number;
60
66
  makeDetector(config: { windowMs: number; onStarvation(): void; onRecovery(): void }): StarvationDetectorLike;
61
67
  warn?(message: string): void;
62
68
  }): Promise<StarvationDetectorLike | null> {
63
69
  if (!args.session || args.fecInterleaveMs <= 0 || args.fecK <= 0) return null;
70
+ const windowMs = args.deliveryWindowMs ?? Math.ceil(args.fecInterleaveMs * 3);
71
+ if (!Number.isFinite(windowMs) || windowMs <= 0) {
72
+ args.warn?.(`Unable to create starvation detector for ${args.trackName}: invalid delivery window ${windowMs}`);
73
+ return null;
74
+ }
64
75
  const detector = wireStallDetector({
65
76
  session: args.session,
66
- windowMs: Math.ceil(args.fecInterleaveMs * 3),
77
+ windowMs,
67
78
  makeDetector: args.makeDetector,
68
79
  });
69
80
  if (!detector) args.warn?.(`Unable to create starvation detector for ${args.trackName}`);
70
- return detector;
81
+ if (!detector || typeof args.session.state !== "string") return detector;
82
+
83
+ // Stats flow while TrackSession is still `joining`. Feeding them directly can
84
+ // age a starvation episode past its window before the first multicast RAP,
85
+ // causing the first post-cutover sample to flip a healthy session to
86
+ // `failing`. Start with a fresh baseline only once multicast/AMT is primary;
87
+ // retain the detector through `failing` so it can observe recovery.
88
+ let active = false;
89
+ return {
90
+ update(snapshot) {
91
+ const state = args.session!.state;
92
+ const shouldObserve = state === "multicast" || state === "amt" || state === "failing";
93
+ if (!shouldObserve) {
94
+ if (active) detector.reset();
95
+ active = false;
96
+ return;
97
+ }
98
+ if (!active) {
99
+ detector.reset();
100
+ active = true;
101
+ }
102
+ detector.update(snapshot);
103
+ },
104
+ reset() {
105
+ active = false;
106
+ detector.reset();
107
+ },
108
+ };
71
109
  }
@@ -22,6 +22,7 @@
22
22
 
23
23
  import type {
24
24
  TransportConfig,
25
+ TransportConnectConfig,
25
26
  TransportSubscription,
26
27
  TransportCapabilities,
27
28
  TransportState,
@@ -47,9 +48,6 @@ import { normalizeMulticastEndpoint } from "./endpoint-config.js";
47
48
  *
48
49
  * Manages transport connections with automatic mode selection:
49
50
  * WebTransport -> window.multicast -> AMT proxy -> WebSocket
50
- *
51
- * Renamed from SSMTransportManager - the old name is preserved
52
- * as a deprecated alias for backwards compatibility.
53
51
  */
54
52
  export class TransportManager {
55
53
  private state: TransportState = {
@@ -63,7 +61,8 @@ export class TransportManager {
63
61
  private capabilities: TransportCapabilities | null = null;
64
62
  private subscription: TransportSubscription | null = null;
65
63
  private driad = new DRIADDiscovery();
66
- private abr: AdaptiveBitrateController;
64
+ /** Built by `connect()` from the caller's ladder; there is no built-in one. */
65
+ private abr: AdaptiveBitrateController | null = null;
67
66
  private multicastOrchestrator: MulticastOrchestrator | null = null;
68
67
  private onOrchestratorCommand?: (command: OrchestratorCommand) => void;
69
68
  private connectionGeneration = 0;
@@ -72,10 +71,6 @@ export class TransportManager {
72
71
  private onError?: (error: Error) => void;
73
72
  private onStateChange?: (state: TransportState) => void;
74
73
 
75
- constructor() {
76
- this.abr = new AdaptiveBitrateController();
77
- }
78
-
79
74
  private getWindow(): Window | undefined {
80
75
  return typeof window === "undefined" ? undefined : window;
81
76
  }
@@ -138,11 +133,38 @@ export class TransportManager {
138
133
  * 1. window.multicast gateway bytes
139
134
  * 2. AMT proxy (specified or discovered via DRIAD)
140
135
  * 3. MOQ WebTransport (final fallback)
136
+ *
137
+ * The ABR ladder (`abrLadder`) and initial rendition (`initialQuality`)
138
+ * are checked first, before anything is torn down. A config that fails is
139
+ * refused with the same shape as an invalid endpoint, the error to the
140
+ * error callback and `false` resolved; unlike one, it is caught before the
141
+ * live subscription closes, so a refused reconnect leaves that subscription,
142
+ * its state and any connect already in flight untouched.
141
143
  */
142
- async connect(config: TransportConfig): Promise<boolean> {
144
+ async connect(config: TransportConnectConfig): Promise<boolean> {
145
+ let abr: AdaptiveBitrateController;
146
+ try {
147
+ abr = new AdaptiveBitrateController({
148
+ ladder: config.abrLadder,
149
+ initialQuality: config.initialQuality?.name,
150
+ });
151
+ } catch (error) {
152
+ // A refused config is a caller bug. Without an error callback, say so
153
+ // on the console, since `false` alone also means "no multicast path";
154
+ // with one, the callback is the report.
155
+ const refused = error instanceof Error ? error : new Error(String(error));
156
+ if (this.onError) this.onError(refused);
157
+ else console.error("[Transport] ABR config refused, not connecting:", refused);
158
+ return false;
159
+ }
143
160
  const generation = ++this.connectionGeneration;
144
161
  this.subscription?.close();
145
162
  this.subscription = null;
163
+ // The previous connection's ladder ends with it, as in disconnect(): a
164
+ // reconnect that bails below (invalid endpoint, or superseded while
165
+ // detecting capabilities) leaves no ladder, and only the success path
166
+ // installs `abr`.
167
+ this.abr = null;
146
168
  this.state = {
147
169
  mode: "disconnected",
148
170
  connected: false,
@@ -175,12 +197,11 @@ export class TransportManager {
175
197
  const caps = await this.detectCapabilities();
176
198
  if (generation !== this.connectionGeneration) return false;
177
199
 
178
- // Initialize ABR with requested quality
179
- if (config.initialQuality) {
180
- this.abr = new AdaptiveBitrateController(config.initialQuality);
181
- }
200
+ // ABR adapts over the caller's ladder (the catalog's renditions),
201
+ // validated above before anything was torn down.
202
+ this.abr = abr;
182
203
 
183
- this.abr.setCallback((quality) => {
204
+ abr.setCallback((quality) => {
184
205
  this.state.quality = quality;
185
206
  this.subscription?.onqualitychange?.(quality);
186
207
  this.onStateChange?.(this.state);
@@ -255,7 +276,7 @@ export class TransportManager {
255
276
  connected: true,
256
277
  group: config.group,
257
278
  source: config.source,
258
- quality: this.abr.getCurrentQuality(),
279
+ quality: this.requireAbr().getCurrentQuality(),
259
280
  packetsReceived: 0,
260
281
  bytesReceived: 0,
261
282
  lastPacketTime: 0,
@@ -311,8 +332,8 @@ export class TransportManager {
311
332
  private async connectAmtProxy(config: TransportConfig, amtRelay: string, generation: number): Promise<boolean> {
312
333
  try {
313
334
  const relayHost = amtRelay.includes(":") && !amtRelay.startsWith("[") ? `[${amtRelay}]` : amtRelay;
314
- const relayAuthority = config.amtRelayPort ? `${relayHost}:${config.amtRelayPort}` : relayHost;
315
- const wsUrl = new URL(`wss://${relayAuthority}/amt-proxy`);
335
+ // amtRelayPort is the relay's RFC 7450 UDP port, not the proxy's HTTPS port.
336
+ const wsUrl = new URL(`wss://${relayHost}/amt-proxy`);
316
337
  wsUrl.searchParams.set("group", config.group);
317
338
  wsUrl.searchParams.set("source", config.source || "");
318
339
  wsUrl.searchParams.set("port", String(config.port));
@@ -347,7 +368,7 @@ export class TransportManager {
347
368
  group: config.group,
348
369
  source: config.source,
349
370
  amtRelay,
350
- quality: this.abr.getCurrentQuality(),
371
+ quality: this.requireAbr().getCurrentQuality(),
351
372
  packetsReceived: 0,
352
373
  bytesReceived: 0,
353
374
  lastPacketTime: 0,
@@ -399,7 +420,8 @@ export class TransportManager {
399
420
  this.state.lastPacketTime = Date.now();
400
421
 
401
422
  // Update ABR with sample
402
- this.abr.recordSample(data.byteLength, 0);
423
+ // Packets reach here only through a connect()ed join, which built the ABR.
424
+ this.abr?.recordSample(data.byteLength, 0);
403
425
 
404
426
  // Forward to callback
405
427
  this.onPacket?.(data);
@@ -413,17 +435,28 @@ export class TransportManager {
413
435
  }
414
436
 
415
437
  /**
416
- * Get current quality level
438
+ * The rung of the live connection's `abrLadder` ABR is on; `null` when
439
+ * there is no ladder (before `connect(config)` succeeds, after
440
+ * `disconnect()`, or after a reconnect that was torn down and bailed).
417
441
  */
418
- getQuality(): QualityLevel {
419
- return this.abr.getCurrentQuality();
442
+ getQuality(): QualityLevel | null {
443
+ return this.abr?.getCurrentQuality() ?? null;
420
444
  }
421
445
 
422
446
  /**
423
- * Force quality level
447
+ * Force the rung named `name` of the live connection's `abrLadder`.
448
+ * Throws `Error` when there is no ladder (see `getQuality()`), and
449
+ * `RangeError` when the ladder has no rung of that name; it never falls
450
+ * back to another rung.
424
451
  */
425
452
  setQuality(name: string) {
426
- this.abr.setQuality(name);
453
+ this.requireAbr().setQuality(name);
454
+ }
455
+
456
+ /** The controller `connect()` built; before it there is no ladder to adapt over. */
457
+ private requireAbr(): AdaptiveBitrateController {
458
+ if (!this.abr) throw new Error("[Transport] ABR has no ladder before connect(config)");
459
+ return this.abr;
427
460
  }
428
461
 
429
462
  /** Configures multicast-only ABR while leaving the caller's MoQ ABR untouched. */
@@ -441,6 +474,10 @@ export class TransportManager {
441
474
  return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onGoodput({ bitrate }));
442
475
  }
443
476
 
477
+ recordMulticastHeadroom(healthy: boolean): OrchestratorCommand | undefined {
478
+ return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onHeadroom({ healthy }));
479
+ }
480
+
444
481
  recordMulticastTierPacket(packet: MulticastTierPacket): OrchestratorCommand | undefined {
445
482
  return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onTierPacket(packet));
446
483
  }
@@ -477,6 +514,9 @@ export class TransportManager {
477
514
  this.connectionGeneration++;
478
515
  this.subscription?.close();
479
516
  this.subscription = null;
517
+ // No ladder after a disconnect, as before the first connect():
518
+ // getQuality() is null and setQuality() throws until connect(config).
519
+ this.abr = null;
480
520
  this.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());
481
521
  this.multicastOrchestrator = null;
482
522
  this.onOrchestratorCommand = undefined;
@@ -498,13 +538,3 @@ export class TransportManager {
498
538
 
499
539
  // Export singleton instance
500
540
  export const transportManager = new TransportManager();
501
-
502
- /**
503
- * @deprecated Use `TransportManager` instead. This alias is preserved for backwards compatibility.
504
- */
505
- export const SSMTransportManager = TransportManager;
506
-
507
- /**
508
- * @deprecated Use `transportManager` instead. This alias is preserved for backwards compatibility.
509
- */
510
- export const ssmTransport = transportManager;
package/src/types.ts CHANGED
@@ -4,29 +4,85 @@
4
4
  * Shared interfaces for unified transport (WebTransport, SSM, AMT, DRIAD) and FEC.
5
5
  */
6
6
 
7
- /** window.multicast gateway config for byte subscriptions. */
8
- export interface WindowMulticastSubscribeConfig {
7
+ /** Native IP-multicast request for the window.multicast gateway. */
8
+ export interface WindowMulticastSubscribeConfigBytes {
9
9
  group: string;
10
10
  port: number;
11
11
  source?: string;
12
12
  consume: "bytes";
13
- mode?: "auto" | "native" | "tunnel";
14
- amtRelay?: string;
15
- amtRelayPort?: number;
13
+ origin?: "ip-multicast";
14
+ /** MMTP packet IDs to forward before data enters the page transport queue. */
15
+ packetIds?: readonly number[];
16
+ /** Catalog track identities corresponding to the selected packet IDs. */
17
+ tracks?: ReadonlyArray<{ name: string; packetId: number }>;
18
+ }
19
+
20
+ /** AMT relay request for the window.multicast gateway. */
21
+ export interface WindowMulticastSubscribeConfigAmt {
22
+ group: string;
23
+ port: number;
24
+ source?: string;
25
+ consume: "bytes";
26
+ origin: "amt";
27
+ relay: string;
28
+ relayPort: number;
29
+ /** MMTP packet IDs to forward before data enters the page transport queue. */
30
+ packetIds?: readonly number[];
16
31
  }
17
32
 
33
+ /** Canonical window.multicast config for byte subscriptions. */
34
+ export type WindowMulticastSubscribeConfig =
35
+ | WindowMulticastSubscribeConfigBytes
36
+ | WindowMulticastSubscribeConfigAmt;
37
+
18
38
  export type WindowMulticastSubscriptionState = "connecting" | "connected" | "reconnecting" | "closed";
19
39
 
20
40
  export interface WindowMulticastSubscriptionEventMap {
21
41
  bytes: CustomEvent<ArrayBuffer>;
42
+ /**
43
+ * Provider extension, not part of the frozen v1 contract (BLO-36599): every
44
+ * packet the provider decoded in one dispatch batch, in arrival order. Each
45
+ * element is exactly what one `bytes` event would have carried. Emitted
46
+ * only by a subscription whose `eventTypes` lists `"packets"`; see
47
+ * {@link subscriptionDeliversPacketBatches}. A consumer listens to exactly
48
+ * one of `bytes` and `packets`, because both carry the same packets.
49
+ */
50
+ packets: CustomEvent<readonly ArrayBuffer[]>;
22
51
  state: CustomEvent<WindowMulticastSubscriptionState>;
23
52
  error: CustomEvent<unknown>;
24
53
  }
25
54
 
55
+ /**
56
+ * How a window.multicast subscription hands packets to the page.
57
+ *
58
+ * - `"datagram"`: packets are independent datagrams; a pause in arrivals may
59
+ * mean loss.
60
+ * - `"ordered-relay"`: the provider relays one receiving socket over a
61
+ * reliable, ordered stream (for example a BCDS bridge WebTransport uni
62
+ * stream). The relay may drop packets (a bounded queue discarding its
63
+ * oldest on overflow) but never reorders them: what arrives is a
64
+ * subsequence of the socket's receive order, so a pause in arrivals
65
+ * carries no loss information.
66
+ *
67
+ * A provider declares `"ordered-relay"` only when every packet of the
68
+ * subscription travels that one ordered stream for the life of the
69
+ * subscription. Absent means no ordering promise, which consumers treat as
70
+ * `"datagram"`.
71
+ */
72
+ export type WindowMulticastDelivery = "datagram" | "ordered-relay";
73
+
26
74
  /** window.multicast subscription returned by the IWA gateway surface. */
27
75
  export interface WindowMulticastSubscription {
28
76
  readonly id?: string;
29
77
  readonly state?: WindowMulticastSubscriptionState;
78
+ /** How this subscription delivers packets; see WindowMulticastDelivery. */
79
+ readonly delivery?: WindowMulticastDelivery;
80
+ /**
81
+ * Events this subscription emits, when the provider advertises them. A
82
+ * provider without the field emits the v1 events only (`bytes`, `state`,
83
+ * `error`).
84
+ */
85
+ readonly eventTypes?: readonly string[];
30
86
  addEventListener<K extends keyof WindowMulticastSubscriptionEventMap>(
31
87
  type: K,
32
88
  listener: (ev: WindowMulticastSubscriptionEventMap[K]) => void,
@@ -75,19 +131,39 @@ export interface TransportConfig {
75
131
  mode?: "auto" | "native" | "tunnel";
76
132
  /** DRIAD discovery enabled */
77
133
  driadEnabled?: boolean;
78
- /** Initial bitrate quality level */
79
- initialQuality?: QualityLevel;
80
134
  }
81
135
 
82
- /** @deprecated Use TransportConfig instead */
83
- export type SSMConfig = TransportConfig;
136
+ /** What `TransportManager.connect()` needs beyond the subscription itself. */
137
+ export interface TransportConnectConfig extends TransportConfig {
138
+ /**
139
+ * The stream's renditions the ABR controller adapts over, e.g. its catalog
140
+ * video tracks. Required: there is no built-in ladder. `connect()` checks
141
+ * it (`validateQualityLadder`) and `initialQuality` before tearing anything
142
+ * down; a config that fails is refused through the error callback and
143
+ * `connect()` resolves `false`, leaving a live subscription untouched.
144
+ * `catalogQualityLadder(catalog)` builds it from a catalog.
145
+ */
146
+ abrLadder: readonly QualityLevel[];
147
+ /**
148
+ * Ladder rendition to start on, matched by `name` against `abrLadder`; the
149
+ * lowest-bitrate rung when omitted. A name `abrLadder` does not carry is
150
+ * not a fallback to that rung: `TransportManager.connect()` refuses the
151
+ * config (error callback, resolves `false`) before tearing anything down.
152
+ */
153
+ initialQuality?: QualityLevel;
154
+ }
84
155
 
85
156
  export interface QualityLevel {
86
157
  name: string;
158
+ /** Bits per second: what the ABR controller ranks and costs a rendition by. */
87
159
  bitrate: number;
88
- width: number;
89
- height: number;
90
- fps: number;
160
+ /**
161
+ * Dimensions and frame rate as the stream's catalog states them; absent
162
+ * when it does not. ABR never reads them, so none is defaulted.
163
+ */
164
+ width?: number;
165
+ height?: number;
166
+ fps?: number;
91
167
  }
92
168
 
93
169
  export interface TransportCapabilities {
@@ -123,9 +199,6 @@ export interface TransportSubscription {
123
199
  close: () => void;
124
200
  }
125
201
 
126
- /** @deprecated Use TransportSubscription instead */
127
- export type SSMSubscription = TransportSubscription;
128
-
129
202
  /** DRIAD Relay information (RFC 8777) */
130
203
  export interface DRIADRelay {
131
204
  host: string;
@@ -159,67 +232,6 @@ export interface PacketMetadata {
159
232
  timestamp: number;
160
233
  }
161
234
 
162
- /** MFU Fragment for reassembly */
163
- export interface MfuFragment {
164
- mpuSequenceNumber: number;
165
- fragmentationIndicator: number; // 0=complete, 1=first, 2=middle, 3=last
166
- fragmentCounter: number;
167
- data: Uint8Array;
168
- timestamp: bigint;
169
- }
170
-
171
- /** Reassembled MFU */
172
- export interface ReassembledMfu {
173
- mpuSequenceNumber: number;
174
- data: Uint8Array;
175
- timestamp: bigint;
176
- fragmentCount: number;
177
- recoveredViaFec: boolean;
178
- }
179
-
180
- /** FEC + MFU processing statistics */
181
- export interface ProcessingStats {
182
- // MFU stats
183
- mfuComplete: number;
184
- mfuReassembled: number;
185
- mfuTimedOut: number;
186
-
187
- // FEC stats
188
- fecSourcePackets: number;
189
- fecRepairPackets: number;
190
- fecBlocksComplete: number;
191
- fecBlocksRecovered: number;
192
- fecBlocksFailed: number;
193
- fecRecoveryRate: number;
194
-
195
- // Green-fill stats
196
- greenFillFrames: number;
197
- }
198
-
199
- /** Options for MmtFecClient */
200
- export interface MmtFecClientOptions {
201
- /** Interleave depth for FEC (default: 30 for ~1s at 30fps) */
202
- interleaveDepth?: number;
203
-
204
- /** Block timeout in milliseconds (default: 2000) */
205
- blockTimeoutMs?: number;
206
-
207
- /** MFU timeout in milliseconds (default: 1000) */
208
- mfuTimeoutMs?: number;
209
-
210
- /** Enable green-fill for failed frames (default: true) */
211
- greenFillEnabled?: boolean;
212
-
213
- /** Callback when MFU is ready */
214
- onMfuReady?: (mfu: ReassembledMfu) => void;
215
-
216
- /** Callback for green-fill frame */
217
- onGreenFill?: (timestamp: bigint) => void;
218
-
219
- /** Callback for stats update */
220
- onStatsUpdate?: (stats: ProcessingStats) => void;
221
- }
222
-
223
235
  /** Time types (microseconds) */
224
236
  export type Micro = number & { readonly __micro: unique symbol };
225
237
 
@@ -279,38 +291,11 @@ export interface MulticastSubscriptionHandle {
279
291
  close(): Promise<void>;
280
292
  }
281
293
 
282
- /** FEC Configuration for MMT streams */
283
- export interface FecConfig {
284
- enabled?: boolean;
285
- oti?: Uint8Array;
286
- interleaveDepth?: number;
287
- blockTimeoutMs?: number;
288
- greenFillEnabled?: boolean;
289
- }
290
-
291
- /** FEC Statistics */
292
- export interface FecStats {
293
- sourcePackets: number;
294
- repairPackets: number;
295
- blocksComplete: number;
296
- blocksRecovered: number;
297
- blocksFailed: number;
298
- recoveryRate: number;
299
- greenFillFrames: number;
300
- }
301
-
302
- /** Extended MulticastConfig with FEC support */
303
- export interface MulticastConfigWithFec extends MulticastConfig {
304
- fec?: FecConfig;
305
- onFecStats?: (stats: FecStats) => void;
306
- }
307
-
308
294
  /** Main Multicast Bridge API */
309
295
  export interface MulticastBridge {
310
- subscribe(config: MulticastConfig | MulticastConfigWithFec): Promise<MulticastSubscriptionHandle>;
296
+ subscribe(config: MulticastConfig): Promise<MulticastSubscriptionHandle>;
311
297
  getSubscriptions(): Promise<MulticastSubscriptionHandle[]>;
312
298
  isNativeSupported(): Promise<boolean>;
313
- getFecStats?(): FecStats | null;
314
299
  }
315
300
 
316
301
  // ============================================================
@@ -318,22 +303,17 @@ export interface MulticastBridge {
318
303
  // ============================================================
319
304
 
320
305
  /**
321
- * FEC_CONFIG message type per draft-ramadan-moq-fec-00
322
- * Used to signal FEC configuration to subscribers
323
- */
324
- export const FEC_CONFIG_MESSAGE_TYPE = 0x50;
325
-
326
- /**
327
- * Repair track suffix per draft-ramadan-moq-fec-00 Section 5.1
328
- * Repair tracks are named by appending this suffix to the source track path
329
- */
330
- export const REPAIR_TRACK_SUFFIX = "repair";
331
-
332
- /**
333
- * Get the repair track path for a given source track
306
+ * Build a conventional repair track path by appending "repair" to a source
307
+ * track path.
334
308
  *
335
- * Per draft-ramadan-moq-fec-00 Section 5.1, the repair track is named
336
- * by appending "repair" to the source track path.
309
+ * Track names carry no protocol meaning. A repair track's layer, its source
310
+ * and its symbol budget are resolved from the catalog via `resolveRepairLayers`
311
+ * in `@blockcast/mmt-container`, never from a name or a name suffix. This
312
+ * helper is a naming convenience for producers only.
313
+ *
314
+ * @deprecated Naming convenience only. Never classify a track or resolve a
315
+ * repair layer from its path; use `resolveRepairLayers` from
316
+ * `@blockcast/mmt-container`.
337
317
  *
338
318
  * @param sourceTrack - The source track path (e.g., "live/video")
339
319
  * @returns The repair track path (e.g., "live/video/repair")
@@ -344,53 +324,16 @@ export const REPAIR_TRACK_SUFFIX = "repair";
344
324
  */
345
325
  export function repairTrackPath(sourceTrack: string): string {
346
326
  if (!sourceTrack || sourceTrack.length === 0) {
347
- return REPAIR_TRACK_SUFFIX;
348
- }
349
- return `${sourceTrack}/${REPAIR_TRACK_SUFFIX}`;
350
- }
351
-
352
- /**
353
- * Check if a track path is a repair track
354
- *
355
- * @param path - The track path to check
356
- * @returns True if the path ends with the repair track suffix
357
- *
358
- * @example
359
- * isRepairTrack("live/video/repair") // returns true
360
- * isRepairTrack("repair") // returns true
361
- * isRepairTrack("live/video") // returns false
362
- */
363
- export function isRepairTrack(path: string): boolean {
364
- return path === REPAIR_TRACK_SUFFIX || path.endsWith(`/${REPAIR_TRACK_SUFFIX}`);
365
- }
366
-
367
- /**
368
- * Get the source track path from a repair track path
369
- *
370
- * @param repairTrack - The repair track path
371
- * @returns The source track path, or null if not a valid repair track
372
- *
373
- * @example
374
- * sourceTrackFromRepair("live/video/repair") // returns "live/video"
375
- * sourceTrackFromRepair("repair") // returns ""
376
- * sourceTrackFromRepair("live/video") // returns null
377
- */
378
- export function sourceTrackFromRepair(repairTrack: string): string | null {
379
- if (repairTrack === REPAIR_TRACK_SUFFIX) {
380
- return "";
381
- }
382
- const suffix = `/${REPAIR_TRACK_SUFFIX}`;
383
- if (repairTrack.endsWith(suffix)) {
384
- return repairTrack.slice(0, -suffix.length);
327
+ return "repair";
385
328
  }
386
- return null;
329
+ return `${sourceTrack}/repair`;
387
330
  }
388
331
 
389
332
  /**
390
333
  * MoQ FEC Configuration per draft-ramadan-moq-fec-00
391
334
  *
392
- * This configuration is sent via FEC_CONFIG message after SUBSCRIBE_OK
393
- * to inform subscribers about FEC parameters.
335
+ * These parameters are signalled in each track's catalog `fec` block;
336
+ * no FEC_CONFIG control message is sent or parsed.
394
337
  */
395
338
  export interface MoqFecConfig {
396
339
  /** Subscribe ID this config applies to */
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=endpoint-config.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"endpoint-config.test.d.ts","sourceRoot":"","sources":["../src/endpoint-config.test.ts"],"names":[],"mappings":""}
@@ -1,39 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { isMulticastExplicitlyDisabled, multicastEndpointKey, normalizeMulticastEndpoint, readUrlMulticastConfig, } from "./endpoint-config.js";
3
- describe("multicast endpoint config", () => {
4
- it("normalizes endpoint identity", () => {
5
- const endpoint = normalizeMulticastEndpoint({
6
- groupAddress: " 232.99.0.10 ",
7
- sourceAddress: " 69.25.95.101 ",
8
- port: 5050,
9
- });
10
- expect(endpoint).toEqual({
11
- groupAddress: "232.99.0.10",
12
- sourceAddress: "69.25.95.101",
13
- port: 5050,
14
- });
15
- expect(multicastEndpointKey(endpoint)).toBe('["69.25.95.101","232.99.0.10",5050,"auto","",""]');
16
- });
17
- it("does not alias endpoint fields containing key delimiters", () => {
18
- const base = { groupAddress: "232.99.0.10", port: 5050, mode: "tunnel" };
19
- expect(multicastEndpointKey({ ...base, amtRelay: "relay|2268" })).not.toBe(multicastEndpointKey({ ...base, amtRelay: "relay", amtRelayPort: 2268 }));
20
- });
21
- it("rejects invalid groups and ports", () => {
22
- expect(normalizeMulticastEndpoint({ groupAddress: "127.0.0.1", port: 5050 })).toBeUndefined();
23
- expect(normalizeMulticastEndpoint({ groupAddress: "232.1.1.1", port: 0 })).toBeUndefined();
24
- });
25
- it("preserves explicit AMT URL configuration", () => {
26
- expect(readUrlMulticastConfig("?mc_group=232.99.0.10&mc_source=69.25.95.101&mc_port=5050&mc_amt_relay=relay.example&mc_amt_relay_port=2268")).toEqual({
27
- groupAddress: "232.99.0.10",
28
- sourceAddress: "69.25.95.101",
29
- port: 5050,
30
- mode: "tunnel",
31
- amtRelay: "relay.example",
32
- amtRelayPort: 2268,
33
- });
34
- });
35
- it("recognizes explicit MoQ-only selection", () => {
36
- expect(isMulticastExplicitlyDisabled("?transport=moq-only")).toBe(true);
37
- });
38
- });
39
- //# sourceMappingURL=endpoint-config.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"endpoint-config.test.js","sourceRoot":"","sources":["../src/endpoint-config.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EACN,6BAA6B,EAC7B,oBAAoB,EACpB,0BAA0B,EAC1B,sBAAsB,GACtB,MAAM,sBAAsB,CAAC;AAE9B,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IAC1C,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACvC,MAAM,QAAQ,GAAG,0BAA0B,CAAC;YAC3C,YAAY,EAAE,eAAe;YAC7B,aAAa,EAAE,gBAAgB;YAC/B,IAAI,EAAE,IAAI;SACV,CAAC,CAAC;QACH,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC;YACxB,YAAY,EAAE,aAAa;YAC3B,aAAa,EAAE,cAAc;YAC7B,IAAI,EAAE,IAAI;SACV,CAAC,CAAC;QACH,MAAM,CAAC,oBAAoB,CAAC,QAAS,CAAC,CAAC,CAAC,IAAI,CAC3C,kDAAkD,CAClD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QACnE,MAAM,IAAI,GAAG,EAAE,YAAY,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAiB,EAAE,CAAC;QAClF,MAAM,CAAC,oBAAoB,CAAC,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CACzE,oBAAoB,CAAC,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CACxE,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC3C,MAAM,CAAC,0BAA0B,CAAC,EAAE,YAAY,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;QAC9F,MAAM,CAAC,0BAA0B,CAAC,EAAE,YAAY,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;IAC5F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,sBAAsB,CAC5B,6GAA6G,CAC7G,CAAC,CAAC,OAAO,CAAC;YACV,YAAY,EAAE,aAAa;YAC3B,aAAa,EAAE,cAAc;YAC7B,IAAI,EAAE,IAAI;YACV,IAAI,EAAE,QAAQ;YACd,QAAQ,EAAE,eAAe;YACzB,YAAY,EAAE,IAAI;SAClB,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QACjD,MAAM,CAAC,6BAA6B,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzE,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}