@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockcast/mmt-transport",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Unified transport manager for MoQ with WebTransport, SSM multicast, AMT, DRIAD, and FEC support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,10 +10,6 @@
10
10
  "import": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts"
12
12
  },
13
- "./fec": {
14
- "import": "./dist/fec-client.js",
15
- "types": "./dist/fec-client.d.ts"
16
- },
17
13
  "./clock": {
18
14
  "import": "./dist/clock.js",
19
15
  "types": "./dist/clock.d.ts"
@@ -24,7 +20,12 @@
24
20
  },
25
21
  "files": [
26
22
  "dist",
27
- "src"
23
+ "src",
24
+ "!src/**/*.test.ts",
25
+ "!src/**/*.spec.ts",
26
+ "!src/**/__tests__",
27
+ "!dist/**/*.test.*",
28
+ "!dist/**/*.spec.*"
28
29
  ],
29
30
  "scripts": {
30
31
  "build": "tsc && node build.mjs",
@@ -34,6 +35,12 @@
34
35
  "test": "vitest run",
35
36
  "test:watch": "vitest"
36
37
  },
38
+ "dependencies": {
39
+ "@blockcast/mmt-base": "^0.3.0"
40
+ },
41
+ "bundledDependencies": [
42
+ "@blockcast/mmt-base"
43
+ ],
37
44
  "devDependencies": {
38
45
  "esbuild": "^0.21.5",
39
46
  "typescript": "^5.9.3",
@@ -27,9 +27,15 @@ export interface FecStatsForAbr {
27
27
  }
28
28
 
29
29
  /**
30
- * Standard quality levels for progressive ABR
30
+ * A generic 240p..4K ladder, kept as public API only.
31
+ *
32
+ * @deprecated Not a stream's ladder and never a default: the controller
33
+ * adapts over the ladder its caller supplies (`AdaptiveBitrateControllerOptions.ladder`,
34
+ * e.g. the catalog's video renditions). Walking this list is how a player on a
35
+ * 360p/480p/720p catalog logged upgrades to 1080p and 1440p it could never
36
+ * switch to (BLO-35597).
31
37
  */
32
- export const QUALITY_LEVELS: QualityLevel[] = [
38
+ export const QUALITY_LEVELS: readonly QualityLevel[] = Object.freeze([
33
39
  { name: "240p", bitrate: 300_000, width: 426, height: 240, fps: 15 },
34
40
  { name: "360p", bitrate: 500_000, width: 640, height: 360, fps: 24 },
35
41
  { name: "480p", bitrate: 1_000_000, width: 854, height: 480, fps: 30 },
@@ -37,7 +43,57 @@ export const QUALITY_LEVELS: QualityLevel[] = [
37
43
  { name: "1080p", bitrate: 5_000_000, width: 1920, height: 1080, fps: 30 },
38
44
  { name: "1440p", bitrate: 8_000_000, width: 2560, height: 1440, fps: 30 },
39
45
  { name: "4K", bitrate: 15_000_000, width: 3840, height: 2160, fps: 30 },
40
- ];
46
+ ].map((level) => Object.freeze(level)));
47
+
48
+ /** What the controller adapts over and where it starts; both from the caller. */
49
+ export interface AdaptiveBitrateControllerOptions {
50
+ /**
51
+ * The renditions this stream carries (e.g. the catalog's video tracks), in
52
+ * any order: a unique name and a bitrate (bps) each, plus dimensions and
53
+ * fps when the catalog states them. Renditions may share a bitrate.
54
+ */
55
+ ladder: readonly QualityLevel[];
56
+ /** Name of the ladder rendition presented at construction; the lowest
57
+ * rung when omitted. A name that matches no rung is refused. */
58
+ initialQuality?: string;
59
+ }
60
+
61
+ function positiveFinite(value: unknown): value is number {
62
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
63
+ }
64
+
65
+ /**
66
+ * Validate a caller ladder and return it frozen, ascending by bitrate. Throws
67
+ * on an empty ladder, a rung without a name or a positive finite bitrate, a
68
+ * width, height or fps that is present but not positive finite, or a name that
69
+ * appears twice: a name is how `setQuality` and the initial rendition find a
70
+ * rung. A shared bitrate is legal (e.g. 720p30 and 720p60, or two codecs);
71
+ * `Array.prototype.sort` is stable, so equal bitrates keep the caller's order.
72
+ */
73
+ export function validateQualityLadder(ladder: readonly QualityLevel[]): readonly QualityLevel[] {
74
+ if (!Array.isArray(ladder) || ladder.length === 0) {
75
+ throw new TypeError("[ABR] ladder must list at least one rendition (e.g. the catalog's video tracks)");
76
+ }
77
+ const names = new Set<string>();
78
+ for (const level of ladder) {
79
+ if (typeof level?.name !== "string" || level.name.length === 0) {
80
+ throw new TypeError("[ABR] every ladder rendition needs a name");
81
+ }
82
+ if (!positiveFinite(level.bitrate)) {
83
+ throw new RangeError(`[ABR] rendition ${level.name}: bitrate must be a positive finite number, got ${String(level.bitrate)}`);
84
+ }
85
+ for (const field of ["width", "height", "fps"] as const) {
86
+ if (level[field] !== undefined && !positiveFinite(level[field])) {
87
+ throw new RangeError(`[ABR] rendition ${level.name}: ${field} must be a positive finite number when stated, got ${String(level[field])}`);
88
+ }
89
+ }
90
+ if (names.has(level.name)) throw new RangeError(`[ABR] rendition ${level.name} appears twice in the ladder`);
91
+ names.add(level.name);
92
+ }
93
+ return Object.freeze(
94
+ [...ladder].sort((a, b) => a.bitrate - b.bitrate).map((level) => Object.freeze({ ...level })),
95
+ );
96
+ }
41
97
 
42
98
  /**
43
99
  * Progressive Adaptive Bitrate Controller
@@ -50,6 +106,8 @@ export const QUALITY_LEVELS: QualityLevel[] = [
50
106
  * - Fast quality downgrades on congestion
51
107
  */
52
108
  export class AdaptiveBitrateController {
109
+ /** The caller's renditions, ascending by bitrate (`validateQualityLadder`). */
110
+ readonly ladder: readonly QualityLevel[];
53
111
  private currentQualityIndex: number;
54
112
  private history: { timestamp: number; bytesReceived: number; packetsLost: number }[] = [];
55
113
  private readonly historyWindow = 10_000; // 10 second window
@@ -68,10 +126,16 @@ export class AdaptiveBitrateController {
68
126
  fecEnabled: false,
69
127
  };
70
128
 
71
- constructor(initialQuality?: QualityLevel) {
72
- this.currentQualityIndex = initialQuality ? QUALITY_LEVELS.findIndex((q) => q.name === initialQuality.name) : 0;
73
-
74
- if (this.currentQualityIndex < 0) this.currentQualityIndex = 0;
129
+ constructor(options: AdaptiveBitrateControllerOptions) {
130
+ if (!options || typeof options !== "object") {
131
+ throw new TypeError("[ABR] AdaptiveBitrateController needs { ladder, initialQuality }");
132
+ }
133
+ this.ladder = validateQualityLadder(options.ladder);
134
+ this.currentQualityIndex =
135
+ options.initialQuality === undefined ? 0 : this.ladder.findIndex((q) => q.name === options.initialQuality);
136
+ if (this.currentQualityIndex < 0) {
137
+ throw new RangeError(`[ABR] initial rendition ${String(options.initialQuality)} is not in the ladder`);
138
+ }
75
139
  }
76
140
 
77
141
  setCallback(callback: (quality: QualityLevel) => void) {
@@ -79,7 +143,7 @@ export class AdaptiveBitrateController {
79
143
  }
80
144
 
81
145
  getCurrentQuality(): QualityLevel {
82
- return QUALITY_LEVELS[this.currentQualityIndex];
146
+ return this.ladder[this.currentQualityIndex];
83
147
  }
84
148
 
85
149
  /** Selects the multicast (S,G) tier without replacing a player's unicast ABR manager. */
@@ -130,7 +194,7 @@ export class AdaptiveBitrateController {
130
194
  if (
131
195
  measuredBitrate > currentQuality.bitrate * this.upgradeThreshold &&
132
196
  effectiveLossRate < 0.01 &&
133
- this.currentQualityIndex < QUALITY_LEVELS.length - 1
197
+ this.currentQualityIndex < this.ladder.length - 1
134
198
  ) {
135
199
  this.upgrade();
136
200
  }
@@ -171,7 +235,7 @@ export class AdaptiveBitrateController {
171
235
  }
172
236
 
173
237
  private upgrade() {
174
- if (this.currentQualityIndex >= QUALITY_LEVELS.length - 1) return;
238
+ if (this.currentQualityIndex >= this.ladder.length - 1) return;
175
239
 
176
240
  this.currentQualityIndex++;
177
241
  this.lastQualityChange = Date.now();
@@ -195,15 +259,15 @@ export class AdaptiveBitrateController {
195
259
  }
196
260
 
197
261
  /**
198
- * Force quality level (for testing)
262
+ * Select a ladder rendition: the caller switched to it (a manual override,
263
+ * or a switch the caller made). Throws for a name the ladder does not carry.
199
264
  */
200
265
  setQuality(name: string) {
201
- const index = QUALITY_LEVELS.findIndex((q) => q.name === name);
202
- if (index >= 0) {
203
- this.currentQualityIndex = index;
204
- this.lastQualityChange = Date.now();
205
- this.onQualityChange?.(this.getCurrentQuality());
206
- }
266
+ const index = this.ladder.findIndex((q) => q.name === name);
267
+ if (index < 0) throw new RangeError(`[ABR] rendition ${name} is not in the ladder`);
268
+ this.currentQualityIndex = index;
269
+ this.lastQualityChange = Date.now();
270
+ this.onQualityChange?.(this.getCurrentQuality());
207
271
  }
208
272
 
209
273
  /**
@@ -9,9 +9,7 @@
9
9
 
10
10
  import type { GroupKey, PacketMetadata, AmtRelayInfo } from "./types.js";
11
11
  import { DRIADDiscovery } from "./driad-discovery.js";
12
-
13
- /** Default AMT port (RFC 7450) */
14
- const DEFAULT_AMT_PORT = 2268;
12
+ import { DEFAULT_AMT_RELAY_PORT } from "@blockcast/mmt-base";
15
13
 
16
14
  /** AMT message types */
17
15
  const AMT_MESSAGE_TYPES = {
@@ -133,7 +131,7 @@ export class AMTGatewayManager {
133
131
  try {
134
132
  let relayAddress: string;
135
133
  let discoveryMethod: "driad" | "manual";
136
- const relayPort = config.relayPort || DEFAULT_AMT_PORT;
134
+ const relayPort = config.relayPort || DEFAULT_AMT_RELAY_PORT;
137
135
 
138
136
  if (config.relayAddress) {
139
137
  relayAddress = config.relayAddress;
@@ -352,7 +350,7 @@ export class AMTGateway {
352
350
 
353
351
  constructor(options: AMTGatewayOptions) {
354
352
  this.relayAddress = options.relayAddress;
355
- this.relayPort = options.relayPort || DEFAULT_AMT_PORT;
353
+ this.relayPort = options.relayPort || DEFAULT_AMT_RELAY_PORT;
356
354
 
357
355
  this.onPacket = options.onPacket || (() => {});
358
356
  this.onStateChange = options.onStateChange || (() => {});
@@ -10,6 +10,7 @@
10
10
  * Query: 10.95.25.69.amt.in-addr.arpa (source-based, NOT group-based)
11
11
  */
12
12
 
13
+ import { DEFAULT_AMT_RELAY_PORT } from "@blockcast/mmt-base";
13
14
  import type { DRIADRelay } from "./types.js";
14
15
 
15
16
  /**
@@ -89,11 +90,7 @@ export class DRIADDiscovery {
89
90
  return [dohRelay];
90
91
  }
91
92
 
92
- // Final fallback to well-known AMT relays
93
- return [
94
- { host: "amt-relay.m2icast.net", port: 2268, priority: 10, weight: 100, asn: 0 },
95
- { host: "amt.akamaistream.net", port: 2268, priority: 20, weight: 50, asn: 0 },
96
- ];
93
+ return [];
97
94
  }
98
95
 
99
96
  /**
@@ -121,7 +118,7 @@ export class DRIADDiscovery {
121
118
  if (result.Answer && result.Answer.length > 0) {
122
119
  return {
123
120
  host: result.Answer[0].data,
124
- port: 2268,
121
+ port: DEFAULT_AMT_RELAY_PORT,
125
122
  priority: 10,
126
123
  weight: 100,
127
124
  asn: 0,
@@ -1,5 +1,13 @@
1
+ import { MMTP_PACKET_ID_MAX, MMTP_SIGNALING_PACKET_ID } from "@blockcast/mmt-base";
2
+ import type {
3
+ WindowMulticastSubscribeConfig,
4
+ WindowMulticastSubscribeConfigBytes,
5
+ } from "./types.js";
6
+
1
7
  export type MulticastTransportPreference = "auto" | "native" | "tunnel";
2
8
 
9
+ export { DEFAULT_AMT_RELAY_PORT } from "@blockcast/mmt-base";
10
+
3
11
  export interface MulticastEndpointConfig {
4
12
  groupAddress: string;
5
13
  sourceAddress?: string;
@@ -9,6 +17,94 @@ export interface MulticastEndpointConfig {
9
17
  amtRelayPort?: number;
10
18
  }
11
19
 
20
+ /**
21
+ * Input accepted by the canonical window.multicast request converter.
22
+ *
23
+ * `mode` and the `amtRelay*` fields are internal endpoint-selection state;
24
+ * they are deliberately not emitted on the page-facing request. The
25
+ * resulting `origin` is the only acquisition selector understood by the
26
+ * gateway.
27
+ */
28
+ export interface WindowMulticastEndpointInput {
29
+ group: string;
30
+ port: number;
31
+ source?: string;
32
+ mode?: MulticastTransportPreference;
33
+ amtRelay?: string;
34
+ amtRelayPort?: number;
35
+ packetIds?: readonly number[];
36
+ tracks?: ReadonlyArray<{ name: string; packetId: number }>;
37
+ }
38
+
39
+ /**
40
+ * Convert internal endpoint-selection state to the canonical page contract.
41
+ *
42
+ * Native is the default and an explicit `native` selection always wins over
43
+ * stale relay metadata. Tunnel/AMT requests must carry the resolved relay
44
+ * and port; there is no implicit relay-port default at this boundary.
45
+ *
46
+ * That rule is deliberate and is NOT the place to fix a relay that names no
47
+ * port: this is the last hop before the page-facing request, so a default
48
+ * applied here would be invisible to every caller and to the join it produces.
49
+ * A catalog relay with no port gets `DEFAULT_AMT_RELAY_PORT` upstream, in the
50
+ * readers that resolve it, and arrives here explicit like any other
51
+ * (BLO-38176).
52
+ */
53
+ export function toWindowMulticastSubscribeConfig(
54
+ input: WindowMulticastEndpointInput,
55
+ ): WindowMulticastSubscribeConfig {
56
+ if (input.mode !== undefined && !["auto", "native", "tunnel"].includes(input.mode)) {
57
+ throw new TypeError(`Unsupported multicast mode: ${String(input.mode)}`);
58
+ }
59
+ // An explicit non-array (the null "endpoint unusable" sentinel from
60
+ // selectEndpointPacketIdsForTracks) must not degrade into "no filter".
61
+ if (input.packetIds !== undefined && !Array.isArray(input.packetIds)) {
62
+ throw new TypeError(
63
+ `multicast packetIds must be an array, got ${input.packetIds === null ? "null" : typeof input.packetIds}`,
64
+ );
65
+ }
66
+ // Every join request passes through here, so the track packet_id range is
67
+ // enforced here rather than trusted from each caller.
68
+ if (input.packetIds?.some(id => !isMulticastTrackPacketId(id)) ||
69
+ input.tracks?.some(track => !isMulticastTrackPacketId(track.packetId))) {
70
+ throw new RangeError(
71
+ `multicast packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}; ` +
72
+ "packetId 0 is the MMTP signaling flow and is never a track's",
73
+ );
74
+ }
75
+
76
+ const base = {
77
+ group: input.group,
78
+ port: input.port,
79
+ ...(input.source?.trim() ? { source: input.source.trim() } : {}),
80
+ consume: "bytes" as const,
81
+ ...(input.packetIds?.length ? { packetIds: [...input.packetIds] } : {}),
82
+ };
83
+
84
+ const relay = input.amtRelay?.trim();
85
+ const wantsAmt = input.mode === "tunnel" ||
86
+ (input.mode !== "native" && relay !== undefined && relay !== "");
87
+ if (!wantsAmt) {
88
+ const native: WindowMulticastSubscribeConfigBytes = {
89
+ ...base,
90
+ origin: "ip-multicast",
91
+ ...(input.tracks?.length ? { tracks: input.tracks.map(({ name, packetId }) => ({ name, packetId })) } : {}),
92
+ };
93
+ return native;
94
+ }
95
+
96
+ const relayPort = input.amtRelayPort;
97
+ if (!relay || relayPort === undefined || !isPositivePort(relayPort)) {
98
+ throw new TypeError("AMT requires a resolved relay and a valid relay port");
99
+ }
100
+ return {
101
+ ...base,
102
+ origin: "amt",
103
+ relay,
104
+ relayPort,
105
+ };
106
+ }
107
+
12
108
  export function isValidMulticastGroupAddress(address: string | undefined): boolean {
13
109
  const value = address?.trim().toLowerCase();
14
110
  if (!value) return false;
@@ -58,6 +154,19 @@ export function multicastEndpointKey(endpoint: MulticastEndpointConfig): string
58
154
  ]);
59
155
  }
60
156
 
157
+ /**
158
+ * True when `value` may be advertised as a multicast track's MMTP `packet_id`:
159
+ * an integer in 1..MMTP_PACKET_ID_MAX. packet_id 0 (MMTP_SIGNALING_PACKET_ID)
160
+ * is the MMTP signaling flow -- the FFmpeg `moq_mmt` muxer sends the AL-FEC
161
+ * message (0x0203) on it into the same group as the media -- so it is never a
162
+ * track's. Only a number qualifies: a string, boolean or null is not coerced.
163
+ */
164
+ export function isMulticastTrackPacketId(value: unknown): value is number {
165
+ return Number.isInteger(value) &&
166
+ (value as number) > MMTP_SIGNALING_PACKET_ID &&
167
+ (value as number) <= MMTP_PACKET_ID_MAX;
168
+ }
169
+
61
170
  export function isUsableMulticastEndpoint(
62
171
  endpoint: { groupAddress?: string; port?: unknown } | undefined,
63
172
  ): boolean {
@@ -65,6 +174,46 @@ export function isUsableMulticastEndpoint(
65
174
  return isValidMulticastGroupAddress(endpoint?.groupAddress) && isPositivePort(port);
66
175
  }
67
176
 
177
+ /**
178
+ * Unprefixed aliases of the `mc_` URL override keys, retired in BLO-37507, each
179
+ * paired with the key that replaces it.
180
+ */
181
+ const RETIRED_MULTICAST_URL_PARAMS = [
182
+ ["amt_relay", "mc_amt_relay"],
183
+ ["amt_relay_port", "mc_amt_relay_port"],
184
+ ] as const;
185
+
186
+ /**
187
+ * Reject a URL multicast override that still carries a retired unprefixed alias.
188
+ *
189
+ * Both URL readers call this only once a valid override is established (a valid
190
+ * multicast `mc_group` and a valid `mc_port`; `readUrlMulticastConfig` also
191
+ * requires the whole endpoint to normalize), which is where they used to read
192
+ * the aliases: a link that
193
+ * relied on one fails here with its replacement named, instead of silently
194
+ * losing its AMT relay (and, in `readUrlMulticastConfig`, its tunnel mode).
195
+ * Presence is the error, whatever the value and whether or not the `mc_` form
196
+ * is also present: quietly preferring one of two keys is the tolerant read being
197
+ * retired. Outside an override the readers read no override key at all, the
198
+ * `mc_` forms included, so the bare names are left to the page; they are live
199
+ * page parameters elsewhere, and keeping clear of those is what the `mc_`
200
+ * namespace is for.
201
+ */
202
+ export function assertNoRetiredMulticastUrlParams(params: URLSearchParams): void {
203
+ for (const [retired, replacement] of RETIRED_MULTICAST_URL_PARAMS) {
204
+ if (params.has(retired)) {
205
+ throw new Error(
206
+ `multicast URL override: query parameter "${retired}" is retired; use "${replacement}"`,
207
+ );
208
+ }
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Read an explicit multicast endpoint override from `mc_*` URL parameters.
214
+ * Throws when an override carries a retired unprefixed alias
215
+ * (`assertNoRetiredMulticastUrlParams`).
216
+ */
68
217
  export function readUrlMulticastConfig(
69
218
  search: string | URLSearchParams = typeof location === "undefined" ? "" : location.search,
70
219
  ): MulticastEndpointConfig | undefined {
@@ -73,12 +222,12 @@ export function readUrlMulticastConfig(
73
222
  const port = positivePort(params.get("mc_port"));
74
223
  if (!groupAddress || !port) return undefined;
75
224
 
76
- const amtRelay = params.get("mc_amt_relay") || params.get("amt_relay") || undefined;
77
- const amtRelayPort = positivePort(params.get("mc_amt_relay_port") || params.get("amt_relay_port"));
225
+ const amtRelay = params.get("mc_amt_relay") || undefined;
226
+ const amtRelayPort = positivePort(params.get("mc_amt_relay_port"));
78
227
  const requestedMode = params.get("mc_mode");
79
228
  const mode = amtRelay ? "tunnel" : multicastMode(requestedMode);
80
229
 
81
- return normalizeMulticastEndpoint({
230
+ const endpoint = normalizeMulticastEndpoint({
82
231
  groupAddress,
83
232
  sourceAddress: params.get("mc_source") || undefined,
84
233
  port,
@@ -86,6 +235,11 @@ export function readUrlMulticastConfig(
86
235
  ...(amtRelay ? { amtRelay } : {}),
87
236
  ...(amtRelayPort ? { amtRelayPort } : {}),
88
237
  });
238
+ // An endpoint that does not normalize (e.g. a non-multicast mc_group) is no
239
+ // override, so the page keeps its own bare names; the same scope
240
+ // readPlayerMulticastConfig applies by validating the group first.
241
+ if (endpoint) assertNoRetiredMulticastUrlParams(params);
242
+ return endpoint;
89
243
  }
90
244
 
91
245
  export function isMulticastExplicitlyDisabled(
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@
26
26
  // Types (new generic names)
27
27
  export type {
28
28
  TransportConfig,
29
+ TransportConnectConfig,
29
30
  TransportSubscription,
30
31
  QualityLevel,
31
32
  TransportCapabilities,
@@ -35,16 +36,15 @@ export type {
35
36
  AmtRelayInfo,
36
37
  GroupKey,
37
38
  PacketMetadata,
38
- MfuFragment,
39
- ReassembledMfu,
40
- ProcessingStats,
41
- MmtFecClientOptions,
42
39
  Micro,
43
40
  SyncDecision,
44
41
  WindowMulticastSubscribeConfig,
42
+ WindowMulticastSubscribeConfigBytes,
43
+ WindowMulticastSubscribeConfigAmt,
45
44
  WindowMulticastSubscriptionState,
46
45
  WindowMulticastSubscriptionEventMap,
47
46
  WindowMulticastSubscription,
47
+ WindowMulticastDelivery,
48
48
  WindowMulticastCapabilities,
49
49
  WindowMulticastGateway,
50
50
  // Consolidated from @blockcast/multicast
@@ -54,40 +54,28 @@ export type {
54
54
  SubscriptionState,
55
55
  SubscriptionStats,
56
56
  MulticastSubscriptionHandle,
57
- FecConfig,
58
- FecStats,
59
- MulticastConfigWithFec,
60
57
  MulticastBridge,
61
58
  // FEC types per draft-ramadan-moq-fec-00
62
59
  MoqFecConfig,
63
60
  RepairObjectHeader,
64
- // @deprecated - use TransportConfig and TransportSubscription instead
65
- SSMConfig,
66
- SSMSubscription,
67
61
  } from "./types.js";
68
62
 
69
63
  // FEC constants and utilities per draft-ramadan-moq-fec-00
70
64
  export {
71
- FEC_CONFIG_MESSAGE_TYPE,
72
- REPAIR_TRACK_SUFFIX,
73
65
  repairTrackPath,
74
- isRepairTrack,
75
- sourceTrackFromRepair,
76
66
  MoqFecAlgorithm,
77
67
  } from "./types.js";
78
68
 
79
- // Transport Manager (renamed from SSMTransportManager)
80
69
  export { TransportManager, transportManager } from "./transport-manager.js";
81
70
 
82
- // @deprecated - Use TransportManager and transportManager instead
83
- export { SSMTransportManager, ssmTransport } from "./transport-manager.js";
84
-
85
71
  // DRIAD Discovery
86
72
  export { DRIADDiscovery, driadDiscovery } from "./driad-discovery.js";
87
73
 
88
74
  // Adaptive Bitrate Controller
89
75
  export {
90
76
  AdaptiveBitrateController,
77
+ validateQualityLadder,
78
+ type AdaptiveBitrateControllerOptions,
91
79
  QUALITY_LEVELS,
92
80
  type FecStatsForAbr,
93
81
  } from "./abr-controller.js";
@@ -104,23 +92,59 @@ export {
104
92
  DEFAULT_JOIN_DEADLINE_MS,
105
93
  goodputFromStats,
106
94
  type GoodputSample,
95
+ type HeadroomSample,
107
96
  type MulticastTierPacket,
108
97
  type OrchestratorCommand,
109
98
  type OrchestratorOptions,
110
99
  type OrchestratorState,
111
100
  } from "./multicast-orchestrator.js";
112
101
 
102
+ export {
103
+ createMulticastSignalingReconciler,
104
+ normalizeCatalogEndpoints,
105
+ type MulticastSignalingEndpoint,
106
+ } from "./multicast-signaling-reconciler.js";
107
+
108
+ export {
109
+ catalogTierEndpoints,
110
+ endpointCarriesMedia,
111
+ endpointToMulticastConfig,
112
+ hasMulticastCatalogBaseline,
113
+ hasUnicastMediaRenditions,
114
+ isMulticastOnlyCatalog,
115
+ isRecoverableMulticastSignalReadError,
116
+ mergeUrlMulticastPolicy,
117
+ normalizeMulticastConfig,
118
+ readPlayerMulticastConfig,
119
+ requiresMulticastCapability,
120
+ sameMulticastConfig,
121
+ selectEndpointPacketIds,
122
+ selectEndpointPacketIdsForTracks,
123
+ selectMulticastCandidate,
124
+ selectMulticastCandidateForTracks,
125
+ urlOverrideBlocksMulticastCandidate,
126
+ withMulticastEndpoints,
127
+ catalogQualityLadder,
128
+ type PlayerMulticastConfig,
129
+ type MulticastTrackSelection,
130
+ } from "./multicast-catalog-policy.js";
131
+
113
132
  export {
114
133
  isMulticastExplicitlyDisabled,
134
+ isMulticastTrackPacketId,
115
135
  isUsableMulticastEndpoint,
116
136
  isValidMulticastGroupAddress,
117
137
  multicastEndpointKey,
118
138
  normalizeMulticastEndpoint,
119
139
  readUrlMulticastConfig,
140
+ toWindowMulticastSubscribeConfig,
120
141
  type MulticastEndpointConfig,
121
142
  type MulticastTransportPreference,
143
+ type WindowMulticastEndpointInput,
122
144
  } from "./endpoint-config.js";
123
145
 
146
+ export { subscriptionDeliversPacketBatches } from "./packet-batch.js";
147
+
124
148
  export {
125
149
  MulticastSubscriptionPool,
126
150
  multicastSubscriptionPool,
@@ -129,8 +153,11 @@ export {
129
153
  } from "./multicast-subscription-pool.js";
130
154
 
131
155
  export {
156
+ classifyStall,
132
157
  TransportStallDetector,
133
158
  type FecProgressSnapshot,
159
+ type PlayerStallClassification,
160
+ type PlayerStallSnapshot,
134
161
  type StallDetectorConfig,
135
162
  } from "./stall-detector.js";
136
163
 
@@ -158,6 +185,15 @@ export {
158
185
  type FallbackTrack,
159
186
  } from "./unicast-fallback.js";
160
187
 
188
+ export {
189
+ MOQT_PUBLISHER_PRIORITY,
190
+ MOQT_SUBSCRIBER_PRIORITY,
191
+ moqtPublisherPriority,
192
+ type MoqtPublisherPriority,
193
+ moqtSubscriberPriority,
194
+ type MoqtSubscriberPriority,
195
+ } from "./moqt-priority.js";
196
+
161
197
  // AMT Gateway
162
198
  export {
163
199
  AMTGatewayManager,
@@ -167,21 +203,6 @@ export {
167
203
  type AMTGatewayOptions,
168
204
  } from "./amt-gateway.js";
169
205
 
170
- // FEC Client
171
- export {
172
- MmtFecClient,
173
- createMulticastFecHandler,
174
- setupMmtFec,
175
- // MoQ FEC Client (for WebTransport)
176
- MoqFecClient,
177
- createMoqFecClient,
178
- loadMmtWasm,
179
- type MoqFecClientConfig,
180
- type MoqFecClientStats,
181
- // FEC decoder configuration (RFC 6330 OTI parameters)
182
- type FecDecoderConfig,
183
- } from "./fec-client.js";
184
-
185
206
  // A/V Sync Clock
186
207
  export {
187
208
  SharedClock,
@@ -0,0 +1,51 @@
1
+ const MAX_MOQT_PRIORITY = 0xff;
2
+
3
+ declare const subscriberPriorityBrand: unique symbol;
4
+ declare const publisherPriorityBrand: unique symbol;
5
+
6
+ export type MoqtSubscriberPriority = number & { readonly [subscriberPriorityBrand]: true };
7
+ export type MoqtPublisherPriority = number & { readonly [publisherPriorityBrand]: true };
8
+
9
+ function moqtPriority(value: number, field: string): number {
10
+ if (!Number.isInteger(value) || value < 0 || value > MAX_MOQT_PRIORITY) {
11
+ throw new RangeError(`${field} must be an integer in [0, ${MAX_MOQT_PRIORITY}], got ${value}`);
12
+ }
13
+ return value;
14
+ }
15
+
16
+ /** Validate a protocol-native MOQT subscriber priority without transforming it. */
17
+ export function moqtSubscriberPriority(value: number): MoqtSubscriberPriority {
18
+ return moqtPriority(value, "MOQT subscriber priority") as MoqtSubscriberPriority;
19
+ }
20
+
21
+ /** Validate a protocol-native MOQT publisher priority without transforming it. */
22
+ export function moqtPublisherPriority(value: number): MoqtPublisherPriority {
23
+ return moqtPriority(value, "MOQT publisher priority") as MoqtPublisherPriority;
24
+ }
25
+
26
+ /** Shared media subscription policy on MOQT's native lower-number-wins scale. */
27
+ export const MOQT_SUBSCRIBER_PRIORITY = Object.freeze({
28
+ catalog: moqtSubscriberPriority(0),
29
+ /**
30
+ * @deprecated A repair FETCH's SUBSCRIBER_PRIORITY is derived per request
31
+ * from the catalog source and repair priorities (draft-ramadan-moq-fec
32
+ * §11.4.5), not a player constant. Use the `subscriberPriority` that
33
+ * `@blockcast/mmt-container`'s `wireRepairFetch` puts on each
34
+ * `RepairFetchRequest` (`repairFetchSubscriberPriority`).
35
+ */
36
+ deadlineRepair: moqtSubscriberPriority(0),
37
+ audio: moqtSubscriberPriority(1),
38
+ video: moqtSubscriberPriority(2),
39
+ /**
40
+ * @deprecated See `deadlineRepair`: use the request's catalog-derived
41
+ * `RepairFetchRequest.subscriberPriority` (draft-ramadan-moq-fec §11.4.5).
42
+ */
43
+ backgroundRepair: moqtSubscriberPriority(3),
44
+ });
45
+
46
+ /** Publisher priority bands from draft-ramadan-moq-fec section 10. */
47
+ export const MOQT_PUBLISHER_PRIORITY = Object.freeze({
48
+ catalog: moqtPublisherPriority(32),
49
+ source: moqtPublisherPriority(128),
50
+ repair: moqtPublisherPriority(240),
51
+ });