@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.
- package/README.md +44 -58
- package/dist/abr-controller.d.ts +34 -4
- package/dist/abr-controller.d.ts.map +1 -1
- package/dist/abr-controller.js +66 -17
- package/dist/abr-controller.js.map +1 -1
- package/dist/amt-gateway.d.ts.map +1 -1
- package/dist/amt-gateway.js +3 -4
- package/dist/amt-gateway.js.map +1 -1
- package/dist/driad-discovery.d.ts.map +1 -1
- package/dist/driad-discovery.js +3 -6
- package/dist/driad-discovery.js.map +1 -1
- package/dist/endpoint-config.d.ts +68 -0
- package/dist/endpoint-config.d.ts.map +1 -1
- package/dist/endpoint-config.js +118 -3
- package/dist/endpoint-config.js.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -11
- package/dist/index.js.map +1 -1
- package/dist/mmt-transport.no-modules.js +881 -60
- package/dist/mmt-transport.no-modules.js.map +4 -4
- package/dist/moqt-priority.d.ts +39 -0
- package/dist/moqt-priority.d.ts.map +1 -0
- package/dist/moqt-priority.js +41 -0
- package/dist/moqt-priority.js.map +1 -0
- package/dist/multicast-catalog-policy.d.ts +96 -0
- package/dist/multicast-catalog-policy.d.ts.map +1 -0
- package/dist/multicast-catalog-policy.js +660 -0
- package/dist/multicast-catalog-policy.js.map +1 -0
- package/dist/multicast-orchestrator.d.ts +24 -0
- package/dist/multicast-orchestrator.d.ts.map +1 -1
- package/dist/multicast-orchestrator.js +102 -9
- package/dist/multicast-orchestrator.js.map +1 -1
- package/dist/multicast-signaling-reconciler.d.ts +37 -0
- package/dist/multicast-signaling-reconciler.d.ts.map +1 -0
- package/dist/multicast-signaling-reconciler.js +161 -0
- package/dist/multicast-signaling-reconciler.js.map +1 -0
- package/dist/multicast-subscription-pool.d.ts.map +1 -1
- package/dist/multicast-subscription-pool.js +37 -10
- package/dist/multicast-subscription-pool.js.map +1 -1
- package/dist/multicast-tier.d.ts +19 -0
- package/dist/multicast-tier.d.ts.map +1 -1
- package/dist/multicast-tier.js +21 -4
- package/dist/multicast-tier.js.map +1 -1
- package/dist/no-modules.d.ts +40 -3
- package/dist/no-modules.d.ts.map +1 -1
- package/dist/no-modules.js +30 -3
- package/dist/no-modules.js.map +1 -1
- package/dist/packet-batch.d.ts +13 -0
- package/dist/packet-batch.d.ts.map +1 -0
- package/dist/packet-batch.js +14 -0
- package/dist/packet-batch.js.map +1 -0
- package/dist/stall-detector.d.ts +10 -0
- package/dist/stall-detector.d.ts.map +1 -1
- package/dist/stall-detector.js +21 -0
- package/dist/stall-detector.js.map +1 -1
- package/dist/takeover.d.ts +3 -0
- package/dist/takeover.d.ts.map +1 -1
- package/dist/takeover.js +35 -2
- package/dist/takeover.js.map +1 -1
- package/dist/transport-manager.d.ts +21 -17
- package/dist/transport-manager.d.ts.map +1 -1
- package/dist/transport-manager.js +63 -29
- package/dist/transport-manager.js.map +1 -1
- package/dist/types.d.ts +98 -123
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +12 -50
- package/dist/types.js.map +1 -1
- package/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
- package/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
- package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
- package/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
- package/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
- package/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
- package/node_modules/@blockcast/mmt-base/README.md +134 -0
- package/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts +55 -0
- package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js +128 -0
- package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/index.d.ts +7 -0
- package/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/index.js +7 -0
- package/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +292 -0
- package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/mmtp.js +612 -0
- package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts +18 -0
- package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/net-constants.js +18 -0
- package/node_modules/@blockcast/mmt-base/dist/net-constants.js.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts +47 -0
- package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/ntp-short.js +54 -0
- package/node_modules/@blockcast/mmt-base/dist/ntp-short.js.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts +57 -0
- package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/serial-number.js +98 -0
- package/node_modules/@blockcast/mmt-base/dist/serial-number.js.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts +312 -0
- package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/signalling.js +637 -0
- package/node_modules/@blockcast/mmt-base/dist/signalling.js.map +1 -0
- package/node_modules/@blockcast/mmt-base/package.json +49 -0
- package/node_modules/@blockcast/mmt-base/src/fec-block-profile.ts +145 -0
- package/node_modules/@blockcast/mmt-base/src/index.ts +6 -0
- package/node_modules/@blockcast/mmt-base/src/mmtp.ts +892 -0
- package/node_modules/@blockcast/mmt-base/src/net-constants.ts +17 -0
- package/node_modules/@blockcast/mmt-base/src/ntp-short.ts +67 -0
- package/node_modules/@blockcast/mmt-base/src/serial-number.ts +107 -0
- package/node_modules/@blockcast/mmt-base/src/signalling.ts +862 -0
- package/package.json +13 -6
- package/src/abr-controller.ts +81 -17
- package/src/amt-gateway.ts +3 -5
- package/src/driad-discovery.ts +3 -6
- package/src/endpoint-config.ts +157 -3
- package/src/index.ts +54 -33
- package/src/moqt-priority.ts +51 -0
- package/src/multicast-catalog-policy.ts +753 -0
- package/src/multicast-orchestrator.ts +114 -10
- package/src/multicast-signaling-reconciler.ts +156 -0
- package/src/multicast-subscription-pool.ts +46 -11
- package/src/multicast-tier.ts +30 -3
- package/src/no-modules.ts +54 -2
- package/src/packet-batch.ts +17 -0
- package/src/stall-detector.ts +37 -0
- package/src/takeover.ts +41 -3
- package/src/transport-manager.ts +64 -34
- package/src/types.ts +103 -160
- package/dist/endpoint-config.test.d.ts +0 -2
- package/dist/endpoint-config.test.d.ts.map +0 -1
- package/dist/endpoint-config.test.js +0 -39
- package/dist/endpoint-config.test.js.map +0 -1
- package/dist/fec-client.d.ts +0 -442
- package/dist/fec-client.d.ts.map +0 -1
- package/dist/fec-client.js +0 -789
- package/dist/fec-client.js.map +0 -1
- package/dist/fec-client.test.d.ts +0 -8
- package/dist/fec-client.test.d.ts.map +0 -1
- package/dist/fec-client.test.js +0 -112
- package/dist/fec-client.test.js.map +0 -1
- package/dist/multicast-subscription-pool.test.d.ts +0 -2
- package/dist/multicast-subscription-pool.test.d.ts.map +0 -1
- package/dist/multicast-subscription-pool.test.js +0 -108
- package/dist/multicast-subscription-pool.test.js.map +0 -1
- package/dist/multicast-tier.test.d.ts +0 -2
- package/dist/multicast-tier.test.d.ts.map +0 -1
- package/dist/multicast-tier.test.js +0 -75
- package/dist/multicast-tier.test.js.map +0 -1
- package/dist/stall-detector.test.d.ts +0 -2
- package/dist/stall-detector.test.d.ts.map +0 -1
- package/dist/stall-detector.test.js +0 -33
- package/dist/stall-detector.test.js.map +0 -1
- package/dist/transport-manager.test.d.ts +0 -2
- package/dist/transport-manager.test.d.ts.map +0 -1
- package/dist/transport-manager.test.js +0 -155
- package/dist/transport-manager.test.js.map +0 -1
- package/src/endpoint-config.test.ts +0 -54
- package/src/fec-client.test.ts +0 -140
- package/src/fec-client.ts +0 -1103
- package/src/multicast-subscription-pool.test.ts +0 -115
- package/src/multicast-tier.test.ts +0 -80
- package/src/stall-detector.test.ts +0 -34
- package/src/transport-manager.test.ts +0 -192
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
function endpointKey(endpoint) {
|
|
5
5
|
return `${endpoint.sourceAddress ?? ""}|${endpoint.groupAddress}|${endpoint.port}`;
|
|
6
6
|
}
|
|
7
|
+
function carriesMedia(endpoint) {
|
|
8
|
+
return endpoint.carriesMedia !== false;
|
|
9
|
+
}
|
|
7
10
|
function selectMulticastTier(endpoints, estimate) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
const media = endpoints.filter(carriesMedia);
|
|
12
|
+
if (media.length <= 1) return media[0];
|
|
13
|
+
const ranked = media.filter((endpoint) => typeof endpoint.bandwidth === "number" && endpoint.bandwidth > 0).sort((a, b) => a.bandwidth - b.bandwidth);
|
|
14
|
+
if (ranked.length < 2) return media[0];
|
|
11
15
|
if (estimate === void 0) return ranked[0];
|
|
12
16
|
const budget = estimate * 0.8;
|
|
13
17
|
let selected;
|
|
@@ -18,7 +22,7 @@
|
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
// src/abr-controller.ts
|
|
21
|
-
var QUALITY_LEVELS = [
|
|
25
|
+
var QUALITY_LEVELS = Object.freeze([
|
|
22
26
|
{ name: "240p", bitrate: 3e5, width: 426, height: 240, fps: 15 },
|
|
23
27
|
{ name: "360p", bitrate: 5e5, width: 640, height: 360, fps: 24 },
|
|
24
28
|
{ name: "480p", bitrate: 1e6, width: 854, height: 480, fps: 30 },
|
|
@@ -26,8 +30,37 @@
|
|
|
26
30
|
{ name: "1080p", bitrate: 5e6, width: 1920, height: 1080, fps: 30 },
|
|
27
31
|
{ name: "1440p", bitrate: 8e6, width: 2560, height: 1440, fps: 30 },
|
|
28
32
|
{ name: "4K", bitrate: 15e6, width: 3840, height: 2160, fps: 30 }
|
|
29
|
-
];
|
|
33
|
+
].map((level) => Object.freeze(level)));
|
|
34
|
+
function positiveFinite(value) {
|
|
35
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
36
|
+
}
|
|
37
|
+
function validateQualityLadder(ladder) {
|
|
38
|
+
if (!Array.isArray(ladder) || ladder.length === 0) {
|
|
39
|
+
throw new TypeError("[ABR] ladder must list at least one rendition (e.g. the catalog's video tracks)");
|
|
40
|
+
}
|
|
41
|
+
const names = /* @__PURE__ */ new Set();
|
|
42
|
+
for (const level of ladder) {
|
|
43
|
+
if (typeof level?.name !== "string" || level.name.length === 0) {
|
|
44
|
+
throw new TypeError("[ABR] every ladder rendition needs a name");
|
|
45
|
+
}
|
|
46
|
+
if (!positiveFinite(level.bitrate)) {
|
|
47
|
+
throw new RangeError(`[ABR] rendition ${level.name}: bitrate must be a positive finite number, got ${String(level.bitrate)}`);
|
|
48
|
+
}
|
|
49
|
+
for (const field of ["width", "height", "fps"]) {
|
|
50
|
+
if (level[field] !== void 0 && !positiveFinite(level[field])) {
|
|
51
|
+
throw new RangeError(`[ABR] rendition ${level.name}: ${field} must be a positive finite number when stated, got ${String(level[field])}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (names.has(level.name)) throw new RangeError(`[ABR] rendition ${level.name} appears twice in the ladder`);
|
|
55
|
+
names.add(level.name);
|
|
56
|
+
}
|
|
57
|
+
return Object.freeze(
|
|
58
|
+
[...ladder].sort((a, b) => a.bitrate - b.bitrate).map((level) => Object.freeze({ ...level }))
|
|
59
|
+
);
|
|
60
|
+
}
|
|
30
61
|
var AdaptiveBitrateController = class {
|
|
62
|
+
/** The caller's renditions, ascending by bitrate (`validateQualityLadder`). */
|
|
63
|
+
ladder;
|
|
31
64
|
currentQualityIndex;
|
|
32
65
|
history = [];
|
|
33
66
|
historyWindow = 1e4;
|
|
@@ -47,15 +80,21 @@
|
|
|
47
80
|
recoveryRate: 1,
|
|
48
81
|
fecEnabled: false
|
|
49
82
|
};
|
|
50
|
-
constructor(
|
|
51
|
-
|
|
52
|
-
|
|
83
|
+
constructor(options) {
|
|
84
|
+
if (!options || typeof options !== "object") {
|
|
85
|
+
throw new TypeError("[ABR] AdaptiveBitrateController needs { ladder, initialQuality }");
|
|
86
|
+
}
|
|
87
|
+
this.ladder = validateQualityLadder(options.ladder);
|
|
88
|
+
this.currentQualityIndex = options.initialQuality === void 0 ? 0 : this.ladder.findIndex((q) => q.name === options.initialQuality);
|
|
89
|
+
if (this.currentQualityIndex < 0) {
|
|
90
|
+
throw new RangeError(`[ABR] initial rendition ${String(options.initialQuality)} is not in the ladder`);
|
|
91
|
+
}
|
|
53
92
|
}
|
|
54
93
|
setCallback(callback) {
|
|
55
94
|
this.onQualityChange = callback;
|
|
56
95
|
}
|
|
57
96
|
getCurrentQuality() {
|
|
58
|
-
return
|
|
97
|
+
return this.ladder[this.currentQualityIndex];
|
|
59
98
|
}
|
|
60
99
|
/** Selects the multicast (S,G) tier without replacing a player's unicast ABR manager. */
|
|
61
100
|
selectMulticastTier(endpoints, estimate = this.getMetrics().measuredBitrate || void 0) {
|
|
@@ -81,7 +120,7 @@
|
|
|
81
120
|
this.downgrade();
|
|
82
121
|
return;
|
|
83
122
|
}
|
|
84
|
-
if (measuredBitrate > currentQuality.bitrate * this.upgradeThreshold && effectiveLossRate < 0.01 && this.currentQualityIndex <
|
|
123
|
+
if (measuredBitrate > currentQuality.bitrate * this.upgradeThreshold && effectiveLossRate < 0.01 && this.currentQualityIndex < this.ladder.length - 1) {
|
|
85
124
|
this.upgrade();
|
|
86
125
|
}
|
|
87
126
|
}
|
|
@@ -114,7 +153,7 @@
|
|
|
114
153
|
return { measuredBitrate, lossRate };
|
|
115
154
|
}
|
|
116
155
|
upgrade() {
|
|
117
|
-
if (this.currentQualityIndex >=
|
|
156
|
+
if (this.currentQualityIndex >= this.ladder.length - 1) return;
|
|
118
157
|
this.currentQualityIndex++;
|
|
119
158
|
this.lastQualityChange = Date.now();
|
|
120
159
|
const newQuality = this.getCurrentQuality();
|
|
@@ -130,15 +169,15 @@
|
|
|
130
169
|
this.onQualityChange?.(newQuality);
|
|
131
170
|
}
|
|
132
171
|
/**
|
|
133
|
-
*
|
|
172
|
+
* Select a ladder rendition: the caller switched to it (a manual override,
|
|
173
|
+
* or a switch the caller made). Throws for a name the ladder does not carry.
|
|
134
174
|
*/
|
|
135
175
|
setQuality(name) {
|
|
136
|
-
const index =
|
|
137
|
-
if (index
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
176
|
+
const index = this.ladder.findIndex((q) => q.name === name);
|
|
177
|
+
if (index < 0) throw new RangeError(`[ABR] rendition ${name} is not in the ladder`);
|
|
178
|
+
this.currentQualityIndex = index;
|
|
179
|
+
this.lastQualityChange = Date.now();
|
|
180
|
+
this.onQualityChange?.(this.getCurrentQuality());
|
|
142
181
|
}
|
|
143
182
|
/**
|
|
144
183
|
* Get current metrics including both raw and effective loss rates
|
|
@@ -162,7 +201,91 @@
|
|
|
162
201
|
}
|
|
163
202
|
};
|
|
164
203
|
|
|
204
|
+
// node_modules/@blockcast/mmt-alta-parse/dist/index.js
|
|
205
|
+
var MPU_HEADER_SIZE = 8;
|
|
206
|
+
var MPU_LENGTH_FIELD_SIZE = 2;
|
|
207
|
+
var MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE;
|
|
208
|
+
|
|
209
|
+
// node_modules/@blockcast/mmt-base/dist/mmtp.js
|
|
210
|
+
var MPU_HEADER_SIZE2 = 8;
|
|
211
|
+
var MPU_LENGTH_FIELD_SIZE2 = 2;
|
|
212
|
+
var MPU_HEADER_REMAINDER_SIZE2 = MPU_HEADER_SIZE2 - MPU_LENGTH_FIELD_SIZE2;
|
|
213
|
+
var MMTP_SIGNALING_PACKET_ID = 0;
|
|
214
|
+
var MMTP_PACKET_ID_MAX = 65535;
|
|
215
|
+
|
|
216
|
+
// node_modules/@blockcast/mmt-base/dist/net-constants.js
|
|
217
|
+
var DEFAULT_AMT_RELAY_PORT = 2268;
|
|
218
|
+
|
|
219
|
+
// node_modules/@blockcast/mmt-base/dist/ntp-short.js
|
|
220
|
+
var NTP_SHORT_BITS = 32;
|
|
221
|
+
var NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS;
|
|
222
|
+
var NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1);
|
|
223
|
+
function isNtpShort(value) {
|
|
224
|
+
return Number.isInteger(value) && value >= 0 && value < NTP_SHORT_MODULUS;
|
|
225
|
+
}
|
|
226
|
+
function compareNtpShort(a, b) {
|
|
227
|
+
if (!isNtpShort(a) || !isNtpShort(b)) {
|
|
228
|
+
throw new RangeError(`NTP-short values must be integers in [0, 2^${NTP_SHORT_BITS}), got ${String(a)} and ${String(b)}`);
|
|
229
|
+
}
|
|
230
|
+
if (a === b)
|
|
231
|
+
return "equal";
|
|
232
|
+
const forward = (a - b + NTP_SHORT_MODULUS) % NTP_SHORT_MODULUS;
|
|
233
|
+
if (forward < NTP_SHORT_HALF_RANGE)
|
|
234
|
+
return "after";
|
|
235
|
+
if (forward > NTP_SHORT_HALF_RANGE)
|
|
236
|
+
return "before";
|
|
237
|
+
return "unordered";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// node_modules/@blockcast/mmt-base/dist/fec-block-profile.js
|
|
241
|
+
var FEC_BLOCK_PROFILES = Object.freeze([
|
|
242
|
+
"k-padded-v1",
|
|
243
|
+
"iso-ssbg1-v1"
|
|
244
|
+
]);
|
|
245
|
+
|
|
165
246
|
// src/endpoint-config.ts
|
|
247
|
+
function toWindowMulticastSubscribeConfig(input) {
|
|
248
|
+
if (input.mode !== void 0 && !["auto", "native", "tunnel"].includes(input.mode)) {
|
|
249
|
+
throw new TypeError(`Unsupported multicast mode: ${String(input.mode)}`);
|
|
250
|
+
}
|
|
251
|
+
if (input.packetIds !== void 0 && !Array.isArray(input.packetIds)) {
|
|
252
|
+
throw new TypeError(
|
|
253
|
+
`multicast packetIds must be an array, got ${input.packetIds === null ? "null" : typeof input.packetIds}`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (input.packetIds?.some((id) => !isMulticastTrackPacketId(id)) || input.tracks?.some((track) => !isMulticastTrackPacketId(track.packetId))) {
|
|
257
|
+
throw new RangeError(
|
|
258
|
+
`multicast packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}; packetId 0 is the MMTP signaling flow and is never a track's`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
const base = {
|
|
262
|
+
group: input.group,
|
|
263
|
+
port: input.port,
|
|
264
|
+
...input.source?.trim() ? { source: input.source.trim() } : {},
|
|
265
|
+
consume: "bytes",
|
|
266
|
+
...input.packetIds?.length ? { packetIds: [...input.packetIds] } : {}
|
|
267
|
+
};
|
|
268
|
+
const relay = input.amtRelay?.trim();
|
|
269
|
+
const wantsAmt = input.mode === "tunnel" || input.mode !== "native" && relay !== void 0 && relay !== "";
|
|
270
|
+
if (!wantsAmt) {
|
|
271
|
+
const native = {
|
|
272
|
+
...base,
|
|
273
|
+
origin: "ip-multicast",
|
|
274
|
+
...input.tracks?.length ? { tracks: input.tracks.map(({ name, packetId }) => ({ name, packetId })) } : {}
|
|
275
|
+
};
|
|
276
|
+
return native;
|
|
277
|
+
}
|
|
278
|
+
const relayPort = input.amtRelayPort;
|
|
279
|
+
if (!relay || relayPort === void 0 || !isPositivePort(relayPort)) {
|
|
280
|
+
throw new TypeError("AMT requires a resolved relay and a valid relay port");
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
...base,
|
|
284
|
+
origin: "amt",
|
|
285
|
+
relay,
|
|
286
|
+
relayPort
|
|
287
|
+
};
|
|
288
|
+
}
|
|
166
289
|
function isValidMulticastGroupAddress(address) {
|
|
167
290
|
const value = address?.trim().toLowerCase();
|
|
168
291
|
if (!value) return false;
|
|
@@ -204,20 +327,36 @@
|
|
|
204
327
|
normalized.amtRelayPort ?? ""
|
|
205
328
|
]);
|
|
206
329
|
}
|
|
330
|
+
function isMulticastTrackPacketId(value) {
|
|
331
|
+
return Number.isInteger(value) && value > MMTP_SIGNALING_PACKET_ID && value <= MMTP_PACKET_ID_MAX;
|
|
332
|
+
}
|
|
207
333
|
function isUsableMulticastEndpoint(endpoint) {
|
|
208
334
|
const port = Number(endpoint?.port);
|
|
209
335
|
return isValidMulticastGroupAddress(endpoint?.groupAddress) && isPositivePort(port);
|
|
210
336
|
}
|
|
337
|
+
var RETIRED_MULTICAST_URL_PARAMS = [
|
|
338
|
+
["amt_relay", "mc_amt_relay"],
|
|
339
|
+
["amt_relay_port", "mc_amt_relay_port"]
|
|
340
|
+
];
|
|
341
|
+
function assertNoRetiredMulticastUrlParams(params) {
|
|
342
|
+
for (const [retired, replacement] of RETIRED_MULTICAST_URL_PARAMS) {
|
|
343
|
+
if (params.has(retired)) {
|
|
344
|
+
throw new Error(
|
|
345
|
+
`multicast URL override: query parameter "${retired}" is retired; use "${replacement}"`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
211
350
|
function readUrlMulticastConfig(search = typeof location === "undefined" ? "" : location.search) {
|
|
212
351
|
const params = typeof search === "string" ? new URLSearchParams(search) : search;
|
|
213
352
|
const groupAddress = params.get("mc_group") || "";
|
|
214
353
|
const port = positivePort(params.get("mc_port"));
|
|
215
354
|
if (!groupAddress || !port) return void 0;
|
|
216
|
-
const amtRelay = params.get("mc_amt_relay") ||
|
|
217
|
-
const amtRelayPort = positivePort(params.get("mc_amt_relay_port")
|
|
355
|
+
const amtRelay = params.get("mc_amt_relay") || void 0;
|
|
356
|
+
const amtRelayPort = positivePort(params.get("mc_amt_relay_port"));
|
|
218
357
|
const requestedMode = params.get("mc_mode");
|
|
219
358
|
const mode = amtRelay ? "tunnel" : multicastMode(requestedMode);
|
|
220
|
-
|
|
359
|
+
const endpoint = normalizeMulticastEndpoint({
|
|
221
360
|
groupAddress,
|
|
222
361
|
sourceAddress: params.get("mc_source") || void 0,
|
|
223
362
|
port,
|
|
@@ -225,6 +364,8 @@
|
|
|
225
364
|
...amtRelay ? { amtRelay } : {},
|
|
226
365
|
...amtRelayPort ? { amtRelayPort } : {}
|
|
227
366
|
});
|
|
367
|
+
if (endpoint) assertNoRetiredMulticastUrlParams(params);
|
|
368
|
+
return endpoint;
|
|
228
369
|
}
|
|
229
370
|
function isMulticastExplicitlyDisabled(search = typeof location === "undefined" ? "" : location.search) {
|
|
230
371
|
const params = typeof search === "string" ? new URLSearchParams(search) : search;
|
|
@@ -270,7 +411,12 @@
|
|
|
270
411
|
#estimate;
|
|
271
412
|
#favored;
|
|
272
413
|
#joinStartedAt = 0;
|
|
273
|
-
|
|
414
|
+
/**
|
|
415
|
+
* Latest active-tier send time (NTP short), or undefined before the first.
|
|
416
|
+
* "Latest" is serial order, not the numerically largest value: after the
|
|
417
|
+
* field wraps, the largest value is the stale one.
|
|
418
|
+
*/
|
|
419
|
+
#highWater;
|
|
274
420
|
#dwellMs;
|
|
275
421
|
#joinDeadlineMs;
|
|
276
422
|
get state() {
|
|
@@ -291,19 +437,57 @@
|
|
|
291
437
|
}
|
|
292
438
|
onGoodput(sample) {
|
|
293
439
|
const bitrate = sample.bitrate;
|
|
294
|
-
this.#estimate = bitrate;
|
|
295
440
|
if (!this.#current || this.options.adaptive === false) return void 0;
|
|
296
441
|
if (this.#state === "joining") return this.poll();
|
|
442
|
+
if (bitrate === void 0 || !Number.isFinite(bitrate)) return void 0;
|
|
443
|
+
this.#estimate = bitrate;
|
|
297
444
|
const desired = selectMulticastTier(this.options.endpoints, bitrate);
|
|
298
445
|
if (!desired) return void 0;
|
|
299
446
|
const desiredKey = endpointKey(desired);
|
|
447
|
+
if (desiredKey === endpointKey(this.#current)) {
|
|
448
|
+
if (this.#favored?.source === "goodput") this.#favored = void 0;
|
|
449
|
+
return void 0;
|
|
450
|
+
}
|
|
451
|
+
if (typeof desired.bandwidth !== "number" || typeof this.#current.bandwidth !== "number" || desired.bandwidth >= this.#current.bandwidth) {
|
|
452
|
+
if (this.#favored?.source === "goodput") this.#favored = void 0;
|
|
453
|
+
return void 0;
|
|
454
|
+
}
|
|
455
|
+
const now = this.#now();
|
|
456
|
+
if (!this.#favored || this.#favored.key !== desiredKey || this.#favored.source !== "goodput") {
|
|
457
|
+
this.#favored = { key: desiredKey, since: now, source: "goodput" };
|
|
458
|
+
return void 0;
|
|
459
|
+
}
|
|
460
|
+
if (now - this.#favored.since < this.#dwellMs) return void 0;
|
|
461
|
+
this.#state = "joining";
|
|
462
|
+
this.#target = desired;
|
|
463
|
+
this.#joinStartedAt = now;
|
|
464
|
+
this.#favored = void 0;
|
|
465
|
+
return { kind: "join-target", current: this.#current, target: desired };
|
|
466
|
+
}
|
|
467
|
+
onHeadroom(sample) {
|
|
468
|
+
if (!this.#current || this.options.adaptive === false || this.#state === "joining") return void 0;
|
|
469
|
+
if (!sample.healthy) {
|
|
470
|
+
if (this.#favored?.source === "headroom") this.#favored = void 0;
|
|
471
|
+
return void 0;
|
|
472
|
+
}
|
|
473
|
+
if (this.#favored?.source === "goodput") return void 0;
|
|
474
|
+
const currentBandwidth = this.#current.bandwidth;
|
|
475
|
+
if (typeof currentBandwidth !== "number") return void 0;
|
|
476
|
+
const desired = this.options.endpoints.filter(
|
|
477
|
+
(endpoint) => carriesMedia(endpoint) && typeof endpoint.bandwidth === "number" && endpoint.bandwidth > currentBandwidth
|
|
478
|
+
).sort((a, b) => a.bandwidth - b.bandwidth)[0];
|
|
479
|
+
if (!desired) {
|
|
480
|
+
this.#favored = void 0;
|
|
481
|
+
return void 0;
|
|
482
|
+
}
|
|
483
|
+
const desiredKey = endpointKey(desired);
|
|
300
484
|
if (desiredKey === endpointKey(this.#current)) {
|
|
301
485
|
this.#favored = void 0;
|
|
302
486
|
return void 0;
|
|
303
487
|
}
|
|
304
488
|
const now = this.#now();
|
|
305
489
|
if (!this.#favored || this.#favored.key !== desiredKey) {
|
|
306
|
-
this.#favored = { key: desiredKey, since: now };
|
|
490
|
+
this.#favored = { key: desiredKey, since: now, source: "headroom" };
|
|
307
491
|
return void 0;
|
|
308
492
|
}
|
|
309
493
|
if (now - this.#favored.since < this.#dwellMs) return void 0;
|
|
@@ -313,21 +497,32 @@
|
|
|
313
497
|
this.#favored = void 0;
|
|
314
498
|
return { kind: "join-target", current: this.#current, target: desired };
|
|
315
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* Feed a packet from the active tier (to advance the high-water mark) or
|
|
502
|
+
* from the in-flight target (to find a committable RAP).
|
|
503
|
+
*
|
|
504
|
+
* Serial order is only the true order while the compared instants are less
|
|
505
|
+
* than `NTP_SHORT_HALF_RANGE` (2^15 s, ~9.1 h) apart. Feeding every
|
|
506
|
+
* active-tier packet keeps the mark that close to the live stream. A mark
|
|
507
|
+
* refreshed only by commits can fall further behind than that, and a
|
|
508
|
+
* target RAP may then compare as older, so the join is abandoned.
|
|
509
|
+
*/
|
|
316
510
|
onTierPacket(packet) {
|
|
317
511
|
if (!this.#current) return void 0;
|
|
318
|
-
|
|
319
|
-
|
|
512
|
+
const sentAt = tierPacketSendTime(packet);
|
|
513
|
+
if (packet.endpointKey === endpointKey(this.#current) && sentAt !== void 0 && (this.#highWater === void 0 || compareNtpShort(sentAt, this.#highWater) === "after")) {
|
|
514
|
+
this.#highWater = sentAt;
|
|
320
515
|
}
|
|
321
516
|
if (this.#state !== "joining" || !this.#target) return void 0;
|
|
322
517
|
const expired = this.poll();
|
|
323
518
|
if (expired) return expired;
|
|
324
|
-
if (packet.endpointKey !== endpointKey(this.#target) || !packet.keyframe || !packet.hasValidCodecConfig ||
|
|
519
|
+
if (packet.endpointKey !== endpointKey(this.#target) || !packet.keyframe || !packet.hasValidCodecConfig || sentAt === void 0 || !this.#reachesHighWater(sentAt)) return void 0;
|
|
325
520
|
const command = {
|
|
326
521
|
kind: "commit-swap",
|
|
327
522
|
target: this.#target,
|
|
328
523
|
old: this.#current
|
|
329
524
|
};
|
|
330
|
-
this.#
|
|
525
|
+
this.#highWater = sentAt;
|
|
331
526
|
this.#current = this.#target;
|
|
332
527
|
this.#target = void 0;
|
|
333
528
|
this.#state = "stable";
|
|
@@ -349,12 +544,481 @@
|
|
|
349
544
|
this.#state = "stable";
|
|
350
545
|
return command;
|
|
351
546
|
}
|
|
547
|
+
/**
|
|
548
|
+
* Whether a target RAP sent at `sentAt` is at or beyond the active tier's
|
|
549
|
+
* high-water mark. A pair exactly half the NTP-short space apart has no
|
|
550
|
+
* serial order, so it cannot show that and does not commit.
|
|
551
|
+
*/
|
|
552
|
+
#reachesHighWater(sentAt) {
|
|
553
|
+
if (this.#highWater === void 0) return true;
|
|
554
|
+
const order = compareNtpShort(sentAt, this.#highWater);
|
|
555
|
+
return order === "after" || order === "equal";
|
|
556
|
+
}
|
|
352
557
|
#now() {
|
|
353
558
|
const now = this.options.now();
|
|
354
559
|
if (!Number.isFinite(now)) throw new Error("Multicast orchestrator clock must be finite");
|
|
355
560
|
return now;
|
|
356
561
|
}
|
|
357
562
|
};
|
|
563
|
+
function tierPacketSendTime(packet) {
|
|
564
|
+
if (!Number.isFinite(packet.timestamp)) return void 0;
|
|
565
|
+
if (!isNtpShort(packet.timestamp)) {
|
|
566
|
+
throw new RangeError(
|
|
567
|
+
`Multicast tier packet timestamp must be an MMTP NTP-short send time, got ${packet.timestamp}`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
return packet.timestamp;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/multicast-signaling-reconciler.ts
|
|
574
|
+
function createMulticastSignalingReconciler(catalog, options = {}) {
|
|
575
|
+
const initial = normalizeCatalogEndpoints(catalog);
|
|
576
|
+
const endpoints = new Map(initial.map((endpoint) => [endpoint.endpointId, copyEndpoint(endpoint)]));
|
|
577
|
+
const versions = new Map(initial.map((endpoint) => [endpoint.endpointId, endpoint.endpointVersion]));
|
|
578
|
+
const seen = /* @__PURE__ */ new Set();
|
|
579
|
+
const dormant = /* @__PURE__ */ new Set();
|
|
580
|
+
let catalogVersion = Number(catalog?.multicast?.version ?? catalog?.version ?? 0) || 0;
|
|
581
|
+
const snapshot = (now = Date.now()) => ({
|
|
582
|
+
catalogVersion,
|
|
583
|
+
endpoints: [...endpoints.values()].filter((endpoint) => !dormant.has(endpoint.endpointId) && !isExpired(endpoint, now)).map(copyEndpoint),
|
|
584
|
+
dormantEndpointIds: [...dormant].sort()
|
|
585
|
+
});
|
|
586
|
+
return {
|
|
587
|
+
apply(signal, now = Date.now()) {
|
|
588
|
+
if (!isObject(signal) || typeof signal.type !== "string" || typeof signal.msgId !== "string") return { applied: false, reason: "invalid-message" };
|
|
589
|
+
if (seen.has(signal.msgId)) return { applied: false, reason: "duplicate-message" };
|
|
590
|
+
if (signal.expiresAt != null && Number(signal.expiresAt) < now) return { applied: false, reason: "expired" };
|
|
591
|
+
const signalCatalogVersion = Number(signal.catalogVersion ?? 0);
|
|
592
|
+
if (!Number.isFinite(signalCatalogVersion)) return { applied: false, reason: "invalid-message" };
|
|
593
|
+
if (signalCatalogVersion < catalogVersion) return { applied: false, reason: "stale-catalog" };
|
|
594
|
+
const result = applyFresh(signal, now);
|
|
595
|
+
if (result.applied) {
|
|
596
|
+
seen.add(signal.msgId);
|
|
597
|
+
if (signal.type !== "heartbeat") {
|
|
598
|
+
catalogVersion = Math.max(catalogVersion, signalCatalogVersion);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return result;
|
|
602
|
+
},
|
|
603
|
+
snapshot,
|
|
604
|
+
toCatalog(now = Date.now()) {
|
|
605
|
+
const current = snapshot(now);
|
|
606
|
+
return { ...catalog, multicast: { ...catalog.multicast, version: current.catalogVersion, endpoints: current.endpoints } };
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
function applyFresh(signal, now) {
|
|
610
|
+
switch (signal.type) {
|
|
611
|
+
case "snapshot": {
|
|
612
|
+
const next = signal.payload?.endpoints;
|
|
613
|
+
if (!Array.isArray(next)) return { applied: false, reason: "invalid-message" };
|
|
614
|
+
const normalized = next.map((endpoint, index) => normalizeEndpoint(endpoint, index));
|
|
615
|
+
if (normalized.some((endpoint) => !endpoint || !isValidEndpoint(endpoint, options.catalog))) return { applied: false, reason: "invalid-endpoint" };
|
|
616
|
+
const ids = new Set(normalized.map((endpoint) => endpoint.endpointId));
|
|
617
|
+
if (ids.size !== normalized.length) return { applied: false, reason: "duplicate-endpoint" };
|
|
618
|
+
endpoints.clear();
|
|
619
|
+
versions.clear();
|
|
620
|
+
dormant.clear();
|
|
621
|
+
for (const endpoint of normalized) {
|
|
622
|
+
versions.set(endpoint.endpointId, endpoint.endpointVersion);
|
|
623
|
+
if (!isExpired(endpoint, now)) endpoints.set(endpoint.endpointId, copyEndpoint(endpoint));
|
|
624
|
+
}
|
|
625
|
+
return { applied: true, action: "snapshot" };
|
|
626
|
+
}
|
|
627
|
+
case "add":
|
|
628
|
+
return upsert(signal.payload?.endpoint, now);
|
|
629
|
+
case "update": {
|
|
630
|
+
const id = signal.payload?.endpointId;
|
|
631
|
+
const fields = signal.payload?.fields;
|
|
632
|
+
if (!endpoints.has(id)) return { applied: false, reason: "unknown-endpoint" };
|
|
633
|
+
if (isStale(id, signal.payload?.endpointVersion)) return { applied: false, reason: "stale-endpoint" };
|
|
634
|
+
if (!isObject(fields) || ["protocol", "sourceAddress", "groupAddress", "port"].some((key) => Object.hasOwn(fields, key))) return { applied: false, reason: "invalid-update" };
|
|
635
|
+
return upsert({ ...endpoints.get(id), ...fields, endpointVersion: signal.payload.endpointVersion }, now);
|
|
636
|
+
}
|
|
637
|
+
case "remove": {
|
|
638
|
+
const id = signal.payload?.endpointId;
|
|
639
|
+
if (!endpoints.has(id)) return { applied: false, reason: "unknown-endpoint" };
|
|
640
|
+
const endpointVersion = Number(signal.payload?.endpointVersion);
|
|
641
|
+
if (!Number.isFinite(endpointVersion)) return { applied: false, reason: "invalid-message" };
|
|
642
|
+
if (isStale(id, endpointVersion)) return { applied: false, reason: "stale-endpoint" };
|
|
643
|
+
endpoints.delete(id);
|
|
644
|
+
dormant.delete(id);
|
|
645
|
+
versions.set(id, endpointVersion);
|
|
646
|
+
return { applied: true, action: "drop" };
|
|
647
|
+
}
|
|
648
|
+
case "invalidate": {
|
|
649
|
+
const id = signal.payload?.endpointId;
|
|
650
|
+
if (!endpoints.has(id)) return { applied: false, reason: "unknown-endpoint" };
|
|
651
|
+
dormant.add(id);
|
|
652
|
+
return { applied: true, action: "drop" };
|
|
653
|
+
}
|
|
654
|
+
case "heartbeat":
|
|
655
|
+
return { applied: true, action: "heartbeat" };
|
|
656
|
+
default:
|
|
657
|
+
return { applied: false, reason: "unknown-message-type" };
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function upsert(value, now) {
|
|
661
|
+
const endpoint = normalizeEndpoint(value, endpoints.size);
|
|
662
|
+
if (!endpoint || !isValidEndpoint(endpoint, options.catalog)) return { applied: false, reason: "invalid-endpoint" };
|
|
663
|
+
if (isStale(endpoint.endpointId, endpoint.endpointVersion)) return { applied: false, reason: "stale-endpoint" };
|
|
664
|
+
versions.set(endpoint.endpointId, endpoint.endpointVersion);
|
|
665
|
+
dormant.delete(endpoint.endpointId);
|
|
666
|
+
if (isExpired(endpoint, now)) endpoints.delete(endpoint.endpointId);
|
|
667
|
+
else endpoints.set(endpoint.endpointId, copyEndpoint(endpoint));
|
|
668
|
+
return { applied: true, action: isExpired(endpoint, now) ? "drop" : "candidate" };
|
|
669
|
+
}
|
|
670
|
+
function isStale(id, version) {
|
|
671
|
+
const current = versions.get(id);
|
|
672
|
+
return current !== void 0 && Number(version) <= current;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function normalizeCatalogEndpoints(catalog) {
|
|
676
|
+
const endpoints = catalog?.multicast?.endpoints;
|
|
677
|
+
if (!Array.isArray(endpoints)) return [];
|
|
678
|
+
return endpoints.map(normalizeEndpoint).filter((endpoint) => !!endpoint && isValidEndpoint(endpoint, catalog));
|
|
679
|
+
}
|
|
680
|
+
function normalizeEndpoint(value, index) {
|
|
681
|
+
if (!isObject(value)) return void 0;
|
|
682
|
+
const endpoint = value;
|
|
683
|
+
const tracks = Array.isArray(endpoint.tracks) ? endpoint.tracks.map((track) => typeof track === "string" ? { name: track } : { ...track }).filter((track) => track.name) : [];
|
|
684
|
+
return { ...endpoint, endpointId: endpoint.endpointId || `${endpoint.sourceAddress || ""}|${endpoint.groupAddress || ""}|${endpoint.port || ""}|${index}`, endpointVersion: Number(endpoint.endpointVersion ?? 0), tracks };
|
|
685
|
+
}
|
|
686
|
+
function isValidEndpoint(endpoint, catalog) {
|
|
687
|
+
if (!endpoint.endpointId || !Number.isFinite(endpoint.endpointVersion)) return false;
|
|
688
|
+
if (!["ssm", "asm", "amt"].includes(endpoint.protocol)) return false;
|
|
689
|
+
if (endpoint.protocol === "ssm" && (typeof endpoint.sourceAddress !== "string" || endpoint.sourceAddress.trim().length === 0)) return false;
|
|
690
|
+
if (!isValidMulticastGroupAddress(endpoint.groupAddress) || !Number.isInteger(Number(endpoint.port)) || endpoint.port <= 0 || endpoint.port > 65535 || !endpoint.tracks.length) return false;
|
|
691
|
+
const catalogTracks = /* @__PURE__ */ new Set([...Object.keys(catalog?.video?.renditions ?? {}), ...Object.keys(catalog?.audio?.renditions ?? {})]);
|
|
692
|
+
return endpoint.tracks.every((track) => !!track.name && isMulticastTrackPacketId(track.packetId) && (catalogTracks.size === 0 || catalogTracks.has(track.name) || catalogTracks.has(track.name.replace(/\/repair$/, ""))));
|
|
693
|
+
}
|
|
694
|
+
function copyEndpoint(endpoint) {
|
|
695
|
+
return { ...endpoint, tracks: endpoint.tracks.map((track) => ({ ...track })) };
|
|
696
|
+
}
|
|
697
|
+
function isExpired(endpoint, now) {
|
|
698
|
+
return endpoint.validUntil != null && Number(endpoint.validUntil) < now;
|
|
699
|
+
}
|
|
700
|
+
function isObject(value) {
|
|
701
|
+
return value !== null && typeof value === "object";
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/multicast-catalog-policy.ts
|
|
705
|
+
function hasMulticastCatalogBaseline(catalog) {
|
|
706
|
+
const multicast = catalog?.multicast;
|
|
707
|
+
return normalizeCatalogEndpoints(catalog).length > 0 || Boolean(multicast?.groupAddress && multicast?.port);
|
|
708
|
+
}
|
|
709
|
+
function hasUnicastMediaRenditions(catalog) {
|
|
710
|
+
return Object.keys(catalog?.video?.renditions ?? {}).length > 0 || Object.keys(catalog?.audio?.renditions ?? {}).length > 0;
|
|
711
|
+
}
|
|
712
|
+
function isMulticastOnlyCatalog(catalog) {
|
|
713
|
+
return hasMulticastCatalogBaseline(catalog) && !hasUnicastMediaRenditions(catalog);
|
|
714
|
+
}
|
|
715
|
+
function requiresMulticastCapability(catalog, capabilities = {}) {
|
|
716
|
+
return isMulticastOnlyCatalog(catalog) && !capabilities.multicast;
|
|
717
|
+
}
|
|
718
|
+
function isRecoverableMulticastSignalReadError(error) {
|
|
719
|
+
const candidate = error;
|
|
720
|
+
const name = String(candidate?.name ?? "");
|
|
721
|
+
const message = String(candidate?.message ?? error ?? "");
|
|
722
|
+
return name === "SyntaxError" || /not valid JSON|unexpected token|JSON\.parse|encoded data was not valid|malformed/i.test(message);
|
|
723
|
+
}
|
|
724
|
+
function withMulticastEndpoints(catalog, endpoints, version) {
|
|
725
|
+
if (!catalog?.multicast) return catalog;
|
|
726
|
+
return {
|
|
727
|
+
...catalog,
|
|
728
|
+
multicast: {
|
|
729
|
+
...catalog.multicast,
|
|
730
|
+
version,
|
|
731
|
+
endpoints: endpoints.map(copyEndpoint2)
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function selectMulticastCandidate(catalog, endpoints = normalizeCatalogEndpoints(catalog), search = currentSearchParams()) {
|
|
736
|
+
const params = search instanceof URLSearchParams ? search : new URLSearchParams(search);
|
|
737
|
+
return selectMulticastCandidateForTracks(catalog, endpoints, {
|
|
738
|
+
containerKind: params.get("packaging") || params.get("container") || "mmtp",
|
|
739
|
+
rendition: params.get("rendition") || void 0
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
function selectMulticastCandidateForTracks(catalog, endpoints = normalizeCatalogEndpoints(catalog), selection = {}) {
|
|
743
|
+
const packaging = selection.containerKind || "mmtp";
|
|
744
|
+
const renditionTrack = selection.rendition || "";
|
|
745
|
+
const preferredTrack = packaging === "mmtp" ? "video" : `video.${packaging}`;
|
|
746
|
+
const usableEndpoints = endpoints.filter(isUsableMulticastEndpoint);
|
|
747
|
+
const defaultRenditionTrack = renditionTrack ? "" : lowestServedVideoRendition(catalog, usableEndpoints);
|
|
748
|
+
return renditionTrack && usableEndpoints.find((endpoint) => endpoint.tracks?.some((track) => matchesPreferredTrack(track.name, renditionTrack))) || defaultRenditionTrack && usableEndpoints.find((endpoint) => endpoint.tracks?.some((track) => matchesPreferredTrack(track.name, defaultRenditionTrack))) || usableEndpoints.find((endpoint) => endpoint.tracks?.some((track) => matchesPreferredTrack(track.name, preferredTrack))) || usableEndpoints[0] || null;
|
|
749
|
+
}
|
|
750
|
+
function endpointToMulticastConfig(endpoint, catalog, selection) {
|
|
751
|
+
if (!endpoint || !isUsableMulticastEndpoint(endpoint) || advertisesInvalidPacketId(endpoint)) return null;
|
|
752
|
+
const config = {
|
|
753
|
+
group: endpoint.groupAddress,
|
|
754
|
+
source: endpoint.sourceAddress || "",
|
|
755
|
+
port: Number(endpoint.port)
|
|
756
|
+
};
|
|
757
|
+
const networkSource = endpoint.networkSource ?? catalog?.multicast?.networkSource;
|
|
758
|
+
const amt = findAmtRelay(networkSource);
|
|
759
|
+
if (amt) {
|
|
760
|
+
config.mode = "tunnel";
|
|
761
|
+
config.amtRelay = amt.relay;
|
|
762
|
+
config.amtRelayPort = amt.port;
|
|
763
|
+
} else if (endpoint.protocol === "amt") {
|
|
764
|
+
const amtEntries = amtNetworkSourceEntries(networkSource);
|
|
765
|
+
if (amtEntries.length > 0) config.amtNetworkSource = amtEntries;
|
|
766
|
+
}
|
|
767
|
+
const packetIds = selection === void 0 ? selectEndpointPacketIds(endpoint, catalog) : selectEndpointPacketIdsForTracks(endpoint, catalog, selection);
|
|
768
|
+
if (packetIds === null) return null;
|
|
769
|
+
if (packetIds.length > 0) config.packetIds = packetIds;
|
|
770
|
+
return config;
|
|
771
|
+
}
|
|
772
|
+
function selectEndpointPacketIds(endpoint, catalog, search = currentSearchParams()) {
|
|
773
|
+
const params = search instanceof URLSearchParams ? search : new URLSearchParams(search);
|
|
774
|
+
return selectEndpointPacketIdsForTracks(endpoint, catalog, {
|
|
775
|
+
containerKind: params.get("packaging") || params.get("container") || "mmtp",
|
|
776
|
+
rendition: params.get("rendition") || void 0
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
function selectEndpointPacketIdsForTracks(endpoint, catalog, selection = {}) {
|
|
780
|
+
if (!endpoint || !Array.isArray(endpoint.tracks)) return [];
|
|
781
|
+
if (advertisesInvalidPacketId(endpoint)) return null;
|
|
782
|
+
const tracks = endpoint.tracks.map((track) => ({
|
|
783
|
+
name: typeof track?.name === "string" ? track.name : "",
|
|
784
|
+
packetId: validPacketId(track?.packetId)
|
|
785
|
+
})).filter((track) => track.name.length > 0 && track.packetId !== void 0);
|
|
786
|
+
if (tracks.length === 0) return [];
|
|
787
|
+
const packaging = selection.containerKind || "mmtp";
|
|
788
|
+
const renditionTrack = selection.rendition || "";
|
|
789
|
+
const videoRenditions = catalogRenditions(catalog, "video");
|
|
790
|
+
const audioRenditions = catalogRenditions(catalog, "audio");
|
|
791
|
+
const videoTracks = tracks.filter((track) => Object.hasOwn(videoRenditions, track.name));
|
|
792
|
+
const audioTracks = tracks.filter((track) => Object.hasOwn(audioRenditions, track.name));
|
|
793
|
+
const preferredVideo = packaging === "mmtp" ? "video" : `video.${packaging}`;
|
|
794
|
+
const preferredAudio = packaging === "mmtp" ? "audio" : `audio.${packaging}`;
|
|
795
|
+
const video = (renditionTrack ? videoTracks.find((track) => track.name === renditionTrack) : void 0) ?? videoTracks.find((track) => track.name === preferredVideo) ?? [...videoTracks].sort((a, b) => videoRenditionRank(videoRenditions, a.name) - videoRenditionRank(videoRenditions, b.name))[0];
|
|
796
|
+
const audio = audioTracks.find((track) => track.name === preferredAudio) ?? audioTracks[0];
|
|
797
|
+
const selected = [video, audio].filter(
|
|
798
|
+
(track) => track !== void 0
|
|
799
|
+
);
|
|
800
|
+
if (selected.length === 0) return normalizePacketIds(tracks.map((track) => track.packetId));
|
|
801
|
+
const selectedRepairNames = new Set(selected.flatMap((track) => catalogRepairTracks(catalog, track.name)));
|
|
802
|
+
const selectedRepairTracks = tracks.filter((track) => selectedRepairNames.has(track.name));
|
|
803
|
+
return normalizePacketIds([...selected, ...selectedRepairTracks].map((track) => track.packetId));
|
|
804
|
+
}
|
|
805
|
+
function readPlayerMulticastConfig(search = currentSearchParams()) {
|
|
806
|
+
const params = search instanceof URLSearchParams ? search : new URLSearchParams(search || "");
|
|
807
|
+
const group = params.get("mc_group") || "";
|
|
808
|
+
if (!isValidMulticastGroupAddress(group)) return null;
|
|
809
|
+
const port = positivePort2(params.get("mc_port"));
|
|
810
|
+
if (!port) return null;
|
|
811
|
+
assertNoRetiredMulticastUrlParams(params);
|
|
812
|
+
const mode = params.get("mc_mode") || "";
|
|
813
|
+
const amtRelay = params.get("mc_amt_relay") || "";
|
|
814
|
+
const amtRelayPort = positivePort2(params.get("mc_amt_relay_port"));
|
|
815
|
+
const config = {
|
|
816
|
+
group,
|
|
817
|
+
source: params.get("mc_source") || "",
|
|
818
|
+
port
|
|
819
|
+
};
|
|
820
|
+
if (mode) config.mode = mode;
|
|
821
|
+
if (amtRelay) {
|
|
822
|
+
config.amtRelay = amtRelay;
|
|
823
|
+
if (amtRelay.trim()) config.mode = "tunnel";
|
|
824
|
+
}
|
|
825
|
+
if (amtRelayPort) config.amtRelayPort = amtRelayPort;
|
|
826
|
+
return config;
|
|
827
|
+
}
|
|
828
|
+
function normalizeMulticastConfig(config) {
|
|
829
|
+
if (!config?.group) return null;
|
|
830
|
+
if (Array.isArray(config.packetIds) && !config.packetIds.every(isMulticastTrackPacketId)) return null;
|
|
831
|
+
const amt = findAmtRelay(config.amtNetworkSource);
|
|
832
|
+
const amtRelay = config.amtRelay || amt?.relay || "";
|
|
833
|
+
const dialableRelay = amtRelay.trim();
|
|
834
|
+
const amtRelayPort = positivePort2(String(config.amtRelayPort ?? "")) ?? (dialableRelay ? amt?.port ?? DEFAULT_AMT_RELAY_PORT : void 0);
|
|
835
|
+
const packetIds = normalizePacketIds(config.packetIds);
|
|
836
|
+
const pendingAmt = dialableRelay ? [] : amtNetworkSourceEntries(config.amtNetworkSource);
|
|
837
|
+
return {
|
|
838
|
+
group: config.group,
|
|
839
|
+
source: config.source || "",
|
|
840
|
+
port: Number(config.port),
|
|
841
|
+
mode: dialableRelay ? "tunnel" : config.mode || "auto",
|
|
842
|
+
amtRelay,
|
|
843
|
+
...amtRelayPort ? { amtRelayPort } : {},
|
|
844
|
+
// Relay-pending DRIAD survives normalization while it is still pending:
|
|
845
|
+
// dropping it here would erase, one hop before the join, the only thing
|
|
846
|
+
// telling the call site "AMT was asked for and is unresolved" apart from
|
|
847
|
+
// "this endpoint was always native" (BLO-38184). Once a relay resolves
|
|
848
|
+
// the entries have done their job and are not carried. `amtNetworkSource`
|
|
849
|
+
// is deliberately not part of `sameMulticastConfig`: it is provenance for
|
|
850
|
+
// the relay, not endpoint identity, so it must not report a change.
|
|
851
|
+
//
|
|
852
|
+
// That holds only while no caller acts on the field — today a pending
|
|
853
|
+
// DRIAD entry and a native endpoint both join `ip-multicast`, so
|
|
854
|
+
// comparing equal reports the truth. Once a caller resolves DRIAD and
|
|
855
|
+
// tunnels, a catalog swapping native for DRIAD-AMT becomes a real
|
|
856
|
+
// join-behaviour change that would compare equal and never re-join:
|
|
857
|
+
// whoever wires that resolution adds `amtNetworkSource` to
|
|
858
|
+
// `sameMulticastConfig` in the same change.
|
|
859
|
+
...pendingAmt.length > 0 ? { amtNetworkSource: pendingAmt } : {},
|
|
860
|
+
...packetIds.length > 0 ? { packetIds } : {}
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
function sameMulticastConfig(a, b) {
|
|
864
|
+
const left = normalizeMulticastConfig(a);
|
|
865
|
+
const right = normalizeMulticastConfig(b);
|
|
866
|
+
return Boolean(left && right && left.group === right.group && left.source === right.source && left.port === right.port && left.mode === right.mode && left.amtRelay === right.amtRelay && (left.amtRelayPort ?? 0) === (right.amtRelayPort ?? 0) && JSON.stringify(left.packetIds ?? []) === JSON.stringify(right.packetIds ?? []));
|
|
867
|
+
}
|
|
868
|
+
function urlOverrideBlocksMulticastCandidate(candidateConfig, urlConfig = readPlayerMulticastConfig()) {
|
|
869
|
+
if (!urlConfig) return false;
|
|
870
|
+
const candidate = normalizeMulticastConfig(candidateConfig);
|
|
871
|
+
const explicit = normalizeMulticastConfig(urlConfig);
|
|
872
|
+
return !candidate || !explicit || candidate.group !== explicit.group || candidate.source !== explicit.source || candidate.port !== explicit.port;
|
|
873
|
+
}
|
|
874
|
+
function mergeUrlMulticastPolicy(candidateConfig, urlConfig = readPlayerMulticastConfig()) {
|
|
875
|
+
const candidate = normalizeMulticastConfig(candidateConfig);
|
|
876
|
+
if (!candidate || !urlConfig || urlOverrideBlocksMulticastCandidate(candidate, urlConfig)) return candidate;
|
|
877
|
+
const merged = { ...candidate };
|
|
878
|
+
if (urlConfig.mode !== void 0) {
|
|
879
|
+
merged.mode = urlConfig.mode;
|
|
880
|
+
if (urlConfig.mode !== "tunnel" && urlConfig.amtRelay === void 0) {
|
|
881
|
+
merged.amtRelay = "";
|
|
882
|
+
delete merged.amtRelayPort;
|
|
883
|
+
delete merged.amtNetworkSource;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (urlConfig.amtRelay !== void 0) {
|
|
887
|
+
merged.amtRelay = urlConfig.amtRelay;
|
|
888
|
+
delete merged.amtRelayPort;
|
|
889
|
+
if (urlConfig.amtRelay.trim()) {
|
|
890
|
+
merged.mode = "tunnel";
|
|
891
|
+
delete merged.amtNetworkSource;
|
|
892
|
+
} else if (urlConfig.mode === void 0 && candidate.amtRelay?.trim()) {
|
|
893
|
+
merged.mode = "auto";
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (urlConfig.amtRelayPort !== void 0) merged.amtRelayPort = urlConfig.amtRelayPort;
|
|
897
|
+
return normalizeMulticastConfig(merged);
|
|
898
|
+
}
|
|
899
|
+
function currentSearchParams() {
|
|
900
|
+
return typeof location === "undefined" ? new URLSearchParams() : new URLSearchParams(location.search);
|
|
901
|
+
}
|
|
902
|
+
function positivePort2(value) {
|
|
903
|
+
const port = Number(value);
|
|
904
|
+
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : null;
|
|
905
|
+
}
|
|
906
|
+
function copyEndpoint2(endpoint) {
|
|
907
|
+
return { ...endpoint, tracks: endpoint.tracks.map((track) => ({ ...track })) };
|
|
908
|
+
}
|
|
909
|
+
function amtNetworkSourceEntries(networkSource) {
|
|
910
|
+
const sources = Array.isArray(networkSource) ? networkSource : networkSource ? [networkSource] : [];
|
|
911
|
+
return sources.filter((entry) => entry?.type === "amt").map((entry) => ({ ...entry }));
|
|
912
|
+
}
|
|
913
|
+
function findAmtRelay(networkSource) {
|
|
914
|
+
const source = amtNetworkSourceEntries(networkSource).find((candidate) => candidate?.discovery !== "driad" && typeof candidate?.relay === "string" && candidate.relay.trim());
|
|
915
|
+
if (!source) return void 0;
|
|
916
|
+
const port = positivePort2(String(source.port ?? ""));
|
|
917
|
+
return { relay: source.relay.trim(), port: port ?? DEFAULT_AMT_RELAY_PORT };
|
|
918
|
+
}
|
|
919
|
+
function validPacketId(value) {
|
|
920
|
+
return isMulticastTrackPacketId(value) ? value : void 0;
|
|
921
|
+
}
|
|
922
|
+
function advertisesInvalidPacketId(endpoint) {
|
|
923
|
+
return Array.isArray(endpoint.tracks) && endpoint.tracks.some((track) => track?.packetId !== void 0 && !isMulticastTrackPacketId(track.packetId));
|
|
924
|
+
}
|
|
925
|
+
function normalizePacketIds(packetIds) {
|
|
926
|
+
if (!Array.isArray(packetIds)) return [];
|
|
927
|
+
return [...new Set(packetIds.map(validPacketId).filter((id) => id !== void 0))].sort(
|
|
928
|
+
(a, b) => a - b
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
function matchesPreferredTrack(trackName, preferredTrack) {
|
|
932
|
+
return trackName === preferredTrack || trackName.startsWith(`${preferredTrack}/`);
|
|
933
|
+
}
|
|
934
|
+
function catalogRenditions(catalog, role) {
|
|
935
|
+
const renditions = { ...catalog?.[role]?.renditions ?? {} };
|
|
936
|
+
for (const track of Array.isArray(catalog?.tracks) ? catalog.tracks : []) {
|
|
937
|
+
if (track?.role === role && typeof track.name === "string" && track.name.length > 0) {
|
|
938
|
+
renditions[track.name] = track;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return renditions;
|
|
942
|
+
}
|
|
943
|
+
function assertNoRetiredTrackFields(catalog) {
|
|
944
|
+
if (catalog && typeof catalog === "object" && "commonTrackFields" in catalog) {
|
|
945
|
+
throw new Error("multicast catalog policy: $.commonTrackFields is retired; every track must declare its own packaging");
|
|
946
|
+
}
|
|
947
|
+
const tracks = Array.isArray(catalog?.tracks) ? catalog.tracks : [];
|
|
948
|
+
tracks.forEach((track, index) => {
|
|
949
|
+
if (track && typeof track === "object" && "selectionParams" in track) {
|
|
950
|
+
throw new Error(`multicast catalog policy: $.tracks[${index}].selectionParams is retired; selection fields are flat on the track`);
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
function catalogRepairTracks(catalog, name) {
|
|
955
|
+
assertNoRetiredTrackFields(catalog);
|
|
956
|
+
const tracks = Array.isArray(catalog?.tracks) ? catalog.tracks : [];
|
|
957
|
+
const track = tracks.find((candidate) => candidate?.name === name);
|
|
958
|
+
const rendition = catalog?.video?.renditions?.[name] ?? catalog?.audio?.renditions?.[name];
|
|
959
|
+
const fec = track?.fec ?? rendition?.fec;
|
|
960
|
+
if (!fec || fec.algorithm === "none") return [];
|
|
961
|
+
const names = /* @__PURE__ */ new Set();
|
|
962
|
+
if (typeof fec.repairTrack === "string" && fec.repairTrack.length > 0) names.add(fec.repairTrack);
|
|
963
|
+
const dependsOn = (entry) => Array.isArray(entry?.depends) && entry.depends.length === 1 && entry.depends[0] === name;
|
|
964
|
+
for (const candidate of tracks) {
|
|
965
|
+
const packaging = candidate?.packaging;
|
|
966
|
+
if (packaging === "fec-repair" && typeof candidate?.name === "string" && candidate.name.length > 0 && dependsOn(candidate)) {
|
|
967
|
+
names.add(candidate.name);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
const repairRenditions = catalog?.repair?.renditions;
|
|
971
|
+
if (repairRenditions && typeof repairRenditions === "object") {
|
|
972
|
+
for (const [repairName, entry] of Object.entries(repairRenditions)) {
|
|
973
|
+
if (repairName.length > 0 && dependsOn(entry)) names.add(repairName);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return [...names];
|
|
977
|
+
}
|
|
978
|
+
function catalogQualityLadder(catalog, role = "video") {
|
|
979
|
+
return Object.entries(catalogRenditions(catalog, role)).map(([name, config]) => {
|
|
980
|
+
const { bitrate, codedWidth, codedHeight, framerate } = config ?? {};
|
|
981
|
+
if (!isPositiveFinite(bitrate)) {
|
|
982
|
+
throw new RangeError(`[ABR] catalog ${role} rendition ${name} states no positive bitrate to cost it by`);
|
|
983
|
+
}
|
|
984
|
+
const rung = { name, bitrate };
|
|
985
|
+
if (isPositiveFinite(codedWidth)) rung.width = codedWidth;
|
|
986
|
+
if (isPositiveFinite(codedHeight)) rung.height = codedHeight;
|
|
987
|
+
if (isPositiveFinite(framerate)) rung.fps = framerate;
|
|
988
|
+
return rung;
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
function isPositiveFinite(value) {
|
|
992
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
993
|
+
}
|
|
994
|
+
function videoRenditionRank(config, name) {
|
|
995
|
+
if (typeof config?.bitrate === "number" && config.bitrate > 0) return config.bitrate;
|
|
996
|
+
const width = typeof config?.codedWidth === "number" ? config.codedWidth : 0;
|
|
997
|
+
const height = typeof config?.codedHeight === "number" ? config.codedHeight : 0;
|
|
998
|
+
if (width > 0 && height > 0) return width * height;
|
|
999
|
+
const match = /(?:^|[/_.-])(\d{3,4})p(?:$|[/_.-])/i.exec(name);
|
|
1000
|
+
return match ? Number(match[1]) : Number.POSITIVE_INFINITY;
|
|
1001
|
+
}
|
|
1002
|
+
function lowestServedVideoRendition(catalog, endpoints) {
|
|
1003
|
+
const renditions = catalog?.video?.renditions ?? {};
|
|
1004
|
+
const servedTracks = new Set(endpoints.flatMap((endpoint) => endpoint.tracks?.map((track) => track.name) ?? []));
|
|
1005
|
+
let selected = "";
|
|
1006
|
+
let selectedRank = Number.POSITIVE_INFINITY;
|
|
1007
|
+
for (const [name, config] of Object.entries(renditions)) {
|
|
1008
|
+
if (!name || !servedTracks.has(name)) continue;
|
|
1009
|
+
const rank = videoRenditionRank(config, name);
|
|
1010
|
+
if (!selected || rank < selectedRank) {
|
|
1011
|
+
selected = name;
|
|
1012
|
+
selectedRank = rank;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return selected;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// src/packet-batch.ts
|
|
1019
|
+
function subscriptionDeliversPacketBatches(subscription) {
|
|
1020
|
+
return subscription.eventTypes?.includes("packets") === true;
|
|
1021
|
+
}
|
|
358
1022
|
|
|
359
1023
|
// src/multicast-subscription-pool.ts
|
|
360
1024
|
var MulticastSubscriptionPool = class {
|
|
@@ -420,19 +1084,28 @@
|
|
|
420
1084
|
return this.#entries.size;
|
|
421
1085
|
}
|
|
422
1086
|
async #createEntry(gateway, endpoint, onClosed) {
|
|
423
|
-
const subscription = await gateway.subscribe({
|
|
1087
|
+
const subscription = await gateway.subscribe(toWindowMulticastSubscribeConfig({
|
|
424
1088
|
group: endpoint.groupAddress,
|
|
425
1089
|
...endpoint.sourceAddress ? { source: endpoint.sourceAddress } : {},
|
|
426
1090
|
port: endpoint.port,
|
|
427
|
-
consume: "bytes",
|
|
428
1091
|
...endpoint.mode ? { mode: endpoint.mode } : {},
|
|
429
1092
|
...endpoint.amtRelay ? { amtRelay: endpoint.amtRelay } : {},
|
|
430
1093
|
...endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {}
|
|
431
|
-
});
|
|
1094
|
+
}));
|
|
432
1095
|
const consumers = /* @__PURE__ */ new Map();
|
|
433
1096
|
let entry;
|
|
434
|
-
const
|
|
435
|
-
|
|
1097
|
+
const packetListener = subscriptionDeliversPacketBatches(subscription) ? {
|
|
1098
|
+
type: "packets",
|
|
1099
|
+
listener: (event) => {
|
|
1100
|
+
for (const data of event.detail) {
|
|
1101
|
+
this.#dispatch(consumers, (consumer) => consumer.onBytes(data));
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
} : {
|
|
1105
|
+
type: "bytes",
|
|
1106
|
+
listener: (event) => {
|
|
1107
|
+
this.#dispatch(consumers, (consumer) => consumer.onBytes(event.detail));
|
|
1108
|
+
}
|
|
436
1109
|
};
|
|
437
1110
|
const stateListener = (event) => {
|
|
438
1111
|
this.#dispatch(consumers, (consumer) => consumer.onState?.(event.detail));
|
|
@@ -445,8 +1118,12 @@
|
|
|
445
1118
|
const error = event.detail instanceof Error ? event.detail : new Error(String(event.detail));
|
|
446
1119
|
this.#dispatch(consumers, (consumer) => consumer.onError?.(error));
|
|
447
1120
|
};
|
|
448
|
-
entry = { subscription, consumers,
|
|
449
|
-
|
|
1121
|
+
entry = { subscription, consumers, packetListener, stateListener, errorListener, closed: false };
|
|
1122
|
+
if (packetListener.type === "packets") {
|
|
1123
|
+
subscription.addEventListener("packets", packetListener.listener);
|
|
1124
|
+
} else {
|
|
1125
|
+
subscription.addEventListener("bytes", packetListener.listener);
|
|
1126
|
+
}
|
|
450
1127
|
subscription.addEventListener("state", stateListener);
|
|
451
1128
|
subscription.addEventListener("error", errorListener);
|
|
452
1129
|
return entry;
|
|
@@ -461,7 +1138,12 @@
|
|
|
461
1138
|
return id;
|
|
462
1139
|
}
|
|
463
1140
|
#detach(entry) {
|
|
464
|
-
|
|
1141
|
+
const { packetListener } = entry;
|
|
1142
|
+
if (packetListener.type === "packets") {
|
|
1143
|
+
entry.subscription.removeEventListener("packets", packetListener.listener);
|
|
1144
|
+
} else {
|
|
1145
|
+
entry.subscription.removeEventListener("bytes", packetListener.listener);
|
|
1146
|
+
}
|
|
465
1147
|
entry.subscription.removeEventListener("state", entry.stateListener);
|
|
466
1148
|
entry.subscription.removeEventListener("error", entry.errorListener);
|
|
467
1149
|
}
|
|
@@ -477,7 +1159,70 @@
|
|
|
477
1159
|
};
|
|
478
1160
|
var multicastSubscriptionPool = new MulticastSubscriptionPool();
|
|
479
1161
|
|
|
1162
|
+
// src/moqt-priority.ts
|
|
1163
|
+
var MAX_MOQT_PRIORITY = 255;
|
|
1164
|
+
function moqtPriority(value, field) {
|
|
1165
|
+
if (!Number.isInteger(value) || value < 0 || value > MAX_MOQT_PRIORITY) {
|
|
1166
|
+
throw new RangeError(`${field} must be an integer in [0, ${MAX_MOQT_PRIORITY}], got ${value}`);
|
|
1167
|
+
}
|
|
1168
|
+
return value;
|
|
1169
|
+
}
|
|
1170
|
+
function moqtSubscriberPriority(value) {
|
|
1171
|
+
return moqtPriority(value, "MOQT subscriber priority");
|
|
1172
|
+
}
|
|
1173
|
+
function moqtPublisherPriority(value) {
|
|
1174
|
+
return moqtPriority(value, "MOQT publisher priority");
|
|
1175
|
+
}
|
|
1176
|
+
var MOQT_SUBSCRIBER_PRIORITY = Object.freeze({
|
|
1177
|
+
catalog: moqtSubscriberPriority(0),
|
|
1178
|
+
/**
|
|
1179
|
+
* @deprecated A repair FETCH's SUBSCRIBER_PRIORITY is derived per request
|
|
1180
|
+
* from the catalog source and repair priorities (draft-ramadan-moq-fec
|
|
1181
|
+
* §11.4.5), not a player constant. Use the `subscriberPriority` that
|
|
1182
|
+
* `@blockcast/mmt-container`'s `wireRepairFetch` puts on each
|
|
1183
|
+
* `RepairFetchRequest` (`repairFetchSubscriberPriority`).
|
|
1184
|
+
*/
|
|
1185
|
+
deadlineRepair: moqtSubscriberPriority(0),
|
|
1186
|
+
audio: moqtSubscriberPriority(1),
|
|
1187
|
+
video: moqtSubscriberPriority(2),
|
|
1188
|
+
/**
|
|
1189
|
+
* @deprecated See `deadlineRepair`: use the request's catalog-derived
|
|
1190
|
+
* `RepairFetchRequest.subscriberPriority` (draft-ramadan-moq-fec §11.4.5).
|
|
1191
|
+
*/
|
|
1192
|
+
backgroundRepair: moqtSubscriberPriority(3)
|
|
1193
|
+
});
|
|
1194
|
+
var MOQT_PUBLISHER_PRIORITY = Object.freeze({
|
|
1195
|
+
catalog: moqtPublisherPriority(32),
|
|
1196
|
+
source: moqtPublisherPriority(128),
|
|
1197
|
+
repair: moqtPublisherPriority(240)
|
|
1198
|
+
});
|
|
1199
|
+
|
|
480
1200
|
// src/stall-detector.ts
|
|
1201
|
+
function classifyStall({
|
|
1202
|
+
liveQ = null,
|
|
1203
|
+
decodeCallsDelta = null,
|
|
1204
|
+
fps = null
|
|
1205
|
+
} = {}) {
|
|
1206
|
+
if (liveQ != null && liveQ >= 20) {
|
|
1207
|
+
return {
|
|
1208
|
+
verdict: "DECODE-BOUND",
|
|
1209
|
+
reason: `decode queue backlogged now (liveQ=${liveQ})`
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
if (liveQ != null && liveQ <= 2 && decodeCallsDelta != null && decodeCallsDelta <= 1) {
|
|
1213
|
+
return {
|
|
1214
|
+
verdict: "PIPELINE-STARVED",
|
|
1215
|
+
reason: `queue empty (liveQ=${liveQ}) and decode flatlined (\u0394calls=${decodeCallsDelta}) \u2014 nothing reaching the decoder; suspect delivery/FEC/failback, not decode perf`
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
if (fps != null && fps < 45) {
|
|
1219
|
+
return {
|
|
1220
|
+
verdict: "STARVED-OR-SLOW",
|
|
1221
|
+
reason: `low decode rate (fps=${fps}) without a queue/flatline signal to disambiguate`
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
return { verdict: "INDETERMINATE", reason: "no signal supports a verdict" };
|
|
1225
|
+
}
|
|
481
1226
|
var TransportStallDetector = class {
|
|
482
1227
|
constructor(config) {
|
|
483
1228
|
this.config = config;
|
|
@@ -553,13 +1298,39 @@
|
|
|
553
1298
|
}
|
|
554
1299
|
async function wireStarvationDetector(args) {
|
|
555
1300
|
if (!args.session || args.fecInterleaveMs <= 0 || args.fecK <= 0) return null;
|
|
1301
|
+
const windowMs = args.deliveryWindowMs ?? Math.ceil(args.fecInterleaveMs * 3);
|
|
1302
|
+
if (!Number.isFinite(windowMs) || windowMs <= 0) {
|
|
1303
|
+
args.warn?.(`Unable to create starvation detector for ${args.trackName}: invalid delivery window ${windowMs}`);
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
556
1306
|
const detector = wireStallDetector({
|
|
557
1307
|
session: args.session,
|
|
558
|
-
windowMs
|
|
1308
|
+
windowMs,
|
|
559
1309
|
makeDetector: args.makeDetector
|
|
560
1310
|
});
|
|
561
1311
|
if (!detector) args.warn?.(`Unable to create starvation detector for ${args.trackName}`);
|
|
562
|
-
return detector;
|
|
1312
|
+
if (!detector || typeof args.session.state !== "string") return detector;
|
|
1313
|
+
let active = false;
|
|
1314
|
+
return {
|
|
1315
|
+
update(snapshot) {
|
|
1316
|
+
const state = args.session.state;
|
|
1317
|
+
const shouldObserve = state === "multicast" || state === "amt" || state === "failing";
|
|
1318
|
+
if (!shouldObserve) {
|
|
1319
|
+
if (active) detector.reset();
|
|
1320
|
+
active = false;
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
if (!active) {
|
|
1324
|
+
detector.reset();
|
|
1325
|
+
active = true;
|
|
1326
|
+
}
|
|
1327
|
+
detector.update(snapshot);
|
|
1328
|
+
},
|
|
1329
|
+
reset() {
|
|
1330
|
+
active = false;
|
|
1331
|
+
detector.reset();
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
563
1334
|
}
|
|
564
1335
|
|
|
565
1336
|
// src/driad-discovery.ts
|
|
@@ -619,10 +1390,7 @@
|
|
|
619
1390
|
if (dohRelay) {
|
|
620
1391
|
return [dohRelay];
|
|
621
1392
|
}
|
|
622
|
-
return [
|
|
623
|
-
{ host: "amt-relay.m2icast.net", port: 2268, priority: 10, weight: 100, asn: 0 },
|
|
624
|
-
{ host: "amt.akamaistream.net", port: 2268, priority: 20, weight: 50, asn: 0 }
|
|
625
|
-
];
|
|
1393
|
+
return [];
|
|
626
1394
|
}
|
|
627
1395
|
/**
|
|
628
1396
|
* Query DNS-over-HTTPS for DRIAD relay based on source address (RFC 8777)
|
|
@@ -643,7 +1411,7 @@
|
|
|
643
1411
|
if (result.Answer && result.Answer.length > 0) {
|
|
644
1412
|
return {
|
|
645
1413
|
host: result.Answer[0].data,
|
|
646
|
-
port:
|
|
1414
|
+
port: DEFAULT_AMT_RELAY_PORT,
|
|
647
1415
|
priority: 10,
|
|
648
1416
|
weight: 100,
|
|
649
1417
|
asn: 0
|
|
@@ -713,16 +1481,14 @@
|
|
|
713
1481
|
capabilities = null;
|
|
714
1482
|
subscription = null;
|
|
715
1483
|
driad = new DRIADDiscovery();
|
|
716
|
-
|
|
1484
|
+
/** Built by `connect()` from the caller's ladder; there is no built-in one. */
|
|
1485
|
+
abr = null;
|
|
717
1486
|
multicastOrchestrator = null;
|
|
718
1487
|
onOrchestratorCommand;
|
|
719
1488
|
connectionGeneration = 0;
|
|
720
1489
|
onPacket;
|
|
721
1490
|
onError;
|
|
722
1491
|
onStateChange;
|
|
723
|
-
constructor() {
|
|
724
|
-
this.abr = new AdaptiveBitrateController();
|
|
725
|
-
}
|
|
726
1492
|
getWindow() {
|
|
727
1493
|
return typeof window === "undefined" ? void 0 : window;
|
|
728
1494
|
}
|
|
@@ -773,11 +1539,31 @@
|
|
|
773
1539
|
* 1. window.multicast gateway bytes
|
|
774
1540
|
* 2. AMT proxy (specified or discovered via DRIAD)
|
|
775
1541
|
* 3. MOQ WebTransport (final fallback)
|
|
1542
|
+
*
|
|
1543
|
+
* The ABR ladder (`abrLadder`) and initial rendition (`initialQuality`)
|
|
1544
|
+
* are checked first, before anything is torn down. A config that fails is
|
|
1545
|
+
* refused with the same shape as an invalid endpoint, the error to the
|
|
1546
|
+
* error callback and `false` resolved; unlike one, it is caught before the
|
|
1547
|
+
* live subscription closes, so a refused reconnect leaves that subscription,
|
|
1548
|
+
* its state and any connect already in flight untouched.
|
|
776
1549
|
*/
|
|
777
1550
|
async connect(config) {
|
|
1551
|
+
let abr;
|
|
1552
|
+
try {
|
|
1553
|
+
abr = new AdaptiveBitrateController({
|
|
1554
|
+
ladder: config.abrLadder,
|
|
1555
|
+
initialQuality: config.initialQuality?.name
|
|
1556
|
+
});
|
|
1557
|
+
} catch (error) {
|
|
1558
|
+
const refused = error instanceof Error ? error : new Error(String(error));
|
|
1559
|
+
if (this.onError) this.onError(refused);
|
|
1560
|
+
else console.error("[Transport] ABR config refused, not connecting:", refused);
|
|
1561
|
+
return false;
|
|
1562
|
+
}
|
|
778
1563
|
const generation = ++this.connectionGeneration;
|
|
779
1564
|
this.subscription?.close();
|
|
780
1565
|
this.subscription = null;
|
|
1566
|
+
this.abr = null;
|
|
781
1567
|
this.state = {
|
|
782
1568
|
mode: "disconnected",
|
|
783
1569
|
connected: false,
|
|
@@ -809,10 +1595,8 @@
|
|
|
809
1595
|
};
|
|
810
1596
|
const caps = await this.detectCapabilities();
|
|
811
1597
|
if (generation !== this.connectionGeneration) return false;
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}
|
|
815
|
-
this.abr.setCallback((quality) => {
|
|
1598
|
+
this.abr = abr;
|
|
1599
|
+
abr.setCallback((quality) => {
|
|
816
1600
|
this.state.quality = quality;
|
|
817
1601
|
this.subscription?.onqualitychange?.(quality);
|
|
818
1602
|
this.onStateChange?.(this.state);
|
|
@@ -877,7 +1661,7 @@
|
|
|
877
1661
|
connected: true,
|
|
878
1662
|
group: config.group,
|
|
879
1663
|
source: config.source,
|
|
880
|
-
quality: this.
|
|
1664
|
+
quality: this.requireAbr().getCurrentQuality(),
|
|
881
1665
|
packetsReceived: 0,
|
|
882
1666
|
bytesReceived: 0,
|
|
883
1667
|
lastPacketTime: 0
|
|
@@ -923,8 +1707,7 @@
|
|
|
923
1707
|
async connectAmtProxy(config, amtRelay, generation) {
|
|
924
1708
|
try {
|
|
925
1709
|
const relayHost = amtRelay.includes(":") && !amtRelay.startsWith("[") ? `[${amtRelay}]` : amtRelay;
|
|
926
|
-
const
|
|
927
|
-
const wsUrl = new URL(`wss://${relayAuthority}/amt-proxy`);
|
|
1710
|
+
const wsUrl = new URL(`wss://${relayHost}/amt-proxy`);
|
|
928
1711
|
wsUrl.searchParams.set("group", config.group);
|
|
929
1712
|
wsUrl.searchParams.set("source", config.source || "");
|
|
930
1713
|
wsUrl.searchParams.set("port", String(config.port));
|
|
@@ -954,7 +1737,7 @@
|
|
|
954
1737
|
group: config.group,
|
|
955
1738
|
source: config.source,
|
|
956
1739
|
amtRelay,
|
|
957
|
-
quality: this.
|
|
1740
|
+
quality: this.requireAbr().getCurrentQuality(),
|
|
958
1741
|
packetsReceived: 0,
|
|
959
1742
|
bytesReceived: 0,
|
|
960
1743
|
lastPacketTime: 0
|
|
@@ -999,7 +1782,7 @@
|
|
|
999
1782
|
this.state.packetsReceived++;
|
|
1000
1783
|
this.state.bytesReceived += data.byteLength;
|
|
1001
1784
|
this.state.lastPacketTime = Date.now();
|
|
1002
|
-
this.abr
|
|
1785
|
+
this.abr?.recordSample(data.byteLength, 0);
|
|
1003
1786
|
this.onPacket?.(data);
|
|
1004
1787
|
}
|
|
1005
1788
|
/**
|
|
@@ -1009,16 +1792,26 @@
|
|
|
1009
1792
|
return { ...this.state };
|
|
1010
1793
|
}
|
|
1011
1794
|
/**
|
|
1012
|
-
*
|
|
1795
|
+
* The rung of the live connection's `abrLadder` ABR is on; `null` when
|
|
1796
|
+
* there is no ladder (before `connect(config)` succeeds, after
|
|
1797
|
+
* `disconnect()`, or after a reconnect that was torn down and bailed).
|
|
1013
1798
|
*/
|
|
1014
1799
|
getQuality() {
|
|
1015
|
-
return this.abr
|
|
1800
|
+
return this.abr?.getCurrentQuality() ?? null;
|
|
1016
1801
|
}
|
|
1017
1802
|
/**
|
|
1018
|
-
* Force
|
|
1803
|
+
* Force the rung named `name` of the live connection's `abrLadder`.
|
|
1804
|
+
* Throws `Error` when there is no ladder (see `getQuality()`), and
|
|
1805
|
+
* `RangeError` when the ladder has no rung of that name; it never falls
|
|
1806
|
+
* back to another rung.
|
|
1019
1807
|
*/
|
|
1020
1808
|
setQuality(name) {
|
|
1021
|
-
this.
|
|
1809
|
+
this.requireAbr().setQuality(name);
|
|
1810
|
+
}
|
|
1811
|
+
/** The controller `connect()` built; before it there is no ladder to adapt over. */
|
|
1812
|
+
requireAbr() {
|
|
1813
|
+
if (!this.abr) throw new Error("[Transport] ABR has no ladder before connect(config)");
|
|
1814
|
+
return this.abr;
|
|
1022
1815
|
}
|
|
1023
1816
|
/** Configures multicast-only ABR while leaving the caller's MoQ ABR untouched. */
|
|
1024
1817
|
configureMulticastTiers(options, onCommand) {
|
|
@@ -1030,6 +1823,9 @@
|
|
|
1030
1823
|
recordMulticastGoodput(bitrate) {
|
|
1031
1824
|
return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onGoodput({ bitrate }));
|
|
1032
1825
|
}
|
|
1826
|
+
recordMulticastHeadroom(healthy) {
|
|
1827
|
+
return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onHeadroom({ healthy }));
|
|
1828
|
+
}
|
|
1033
1829
|
recordMulticastTierPacket(packet) {
|
|
1034
1830
|
return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onTierPacket(packet));
|
|
1035
1831
|
}
|
|
@@ -1061,6 +1857,7 @@
|
|
|
1061
1857
|
this.connectionGeneration++;
|
|
1062
1858
|
this.subscription?.close();
|
|
1063
1859
|
this.subscription = null;
|
|
1860
|
+
this.abr = null;
|
|
1064
1861
|
this.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());
|
|
1065
1862
|
this.multicastOrchestrator = null;
|
|
1066
1863
|
this.onOrchestratorCommand = void 0;
|
|
@@ -1127,8 +1924,21 @@
|
|
|
1127
1924
|
// src/no-modules.ts
|
|
1128
1925
|
var api = {
|
|
1129
1926
|
AdaptiveBitrateController,
|
|
1927
|
+
validateQualityLadder,
|
|
1928
|
+
catalogQualityLadder,
|
|
1929
|
+
classifyStall,
|
|
1930
|
+
createMulticastSignalingReconciler,
|
|
1931
|
+
endpointToMulticastConfig,
|
|
1932
|
+
hasMulticastCatalogBaseline,
|
|
1933
|
+
hasUnicastMediaRenditions,
|
|
1934
|
+
isRecoverableMulticastSignalReadError,
|
|
1935
|
+
isMulticastOnlyCatalog,
|
|
1130
1936
|
MulticastOrchestrator,
|
|
1131
1937
|
MulticastSubscriptionPool,
|
|
1938
|
+
MOQT_PUBLISHER_PRIORITY,
|
|
1939
|
+
MOQT_SUBSCRIBER_PRIORITY,
|
|
1940
|
+
moqtPublisherPriority,
|
|
1941
|
+
moqtSubscriberPriority,
|
|
1132
1942
|
TransportManager,
|
|
1133
1943
|
TransportStallDetector,
|
|
1134
1944
|
endpointKey,
|
|
@@ -1136,11 +1946,22 @@
|
|
|
1136
1946
|
isUsableMulticastEndpoint,
|
|
1137
1947
|
isValidMulticastGroupAddress,
|
|
1138
1948
|
makeSourceSwitchHandler,
|
|
1949
|
+
mergeUrlMulticastPolicy,
|
|
1139
1950
|
multicastEndpointKey,
|
|
1951
|
+
normalizeCatalogEndpoints,
|
|
1952
|
+
normalizeMulticastConfig,
|
|
1140
1953
|
normalizeMulticastEndpoint,
|
|
1954
|
+
readPlayerMulticastConfig,
|
|
1141
1955
|
readUrlMulticastConfig,
|
|
1956
|
+
toWindowMulticastSubscribeConfig,
|
|
1957
|
+
requiresMulticastCapability,
|
|
1142
1958
|
runUnicastFallback,
|
|
1959
|
+
sameMulticastConfig,
|
|
1960
|
+
selectEndpointPacketIds,
|
|
1961
|
+
selectMulticastCandidate,
|
|
1143
1962
|
selectMulticastTier,
|
|
1963
|
+
urlOverrideBlocksMulticastCandidate,
|
|
1964
|
+
withMulticastEndpoints,
|
|
1144
1965
|
wireStarvationDetector
|
|
1145
1966
|
};
|
|
1146
1967
|
Object.defineProperty(globalThis, "__MmtTransport", {
|