@blockcast/mmt-container 2.1.0 → 2.2.0-main.050b52fb9b00
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 +29 -17
- package/dist/browser-runtime.js +1949 -374
- package/dist/browser-runtime.js.map +4 -4
- package/dist/carp-catalog.js.map +1 -1
- package/dist/catalog-fec-params.d.ts +17 -1
- package/dist/catalog-fec-params.d.ts.map +1 -1
- package/dist/catalog-fec-params.js +15 -0
- package/dist/catalog-fec-params.js.map +2 -2
- package/dist/catalog-network-source.js.map +1 -1
- package/dist/cmcd-producer.d.ts +65 -47
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js +138 -109
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +26 -16
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +70 -12
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +403 -58
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-state.js.map +1 -1
- package/dist/fec-worker-setup.d.ts +1 -1
- package/dist/fec-worker-setup.js +1 -1
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/held-layer-admission.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.d.ts +48 -13
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +97 -25
- package/dist/iso-block-locator.js.map +2 -2
- package/dist/iso-bmff-timing.d.ts +10 -1
- package/dist/iso-bmff-timing.d.ts.map +1 -1
- package/dist/iso-bmff-timing.js +36 -2
- package/dist/iso-bmff-timing.js.map +2 -2
- package/dist/media-router.js.map +1 -1
- package/dist/mfu-reassembler.js +12 -19
- package/dist/mfu-reassembler.js.map +2 -2
- package/dist/mmtp-container-parser.d.ts +79 -10
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +219 -45
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +342 -63
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +218 -45
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-fec-session.d.ts +41 -3
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +118 -13
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +43 -9
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +2006 -95
- package/dist/mmtp-router.js.map +4 -4
- package/dist/multicast-auth-catalog.js.map +1 -1
- package/dist/multicast-input.js.map +1 -1
- package/dist/multicast-manager.d.ts +28 -5
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +16116 -15005
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +11 -3
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +281 -35
- package/dist/multicast-transport.js.map +4 -4
- package/dist/multicast-unwrap.js +12 -19
- package/dist/multicast-unwrap.js.map +2 -2
- package/dist/no-modules.js +336 -59
- package/dist/no-modules.js.map +3 -3
- package/dist/repair-fetch.d.ts +36 -8
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +285 -26
- package/dist/repair-fetch.js.map +3 -3
- package/dist/repair-layer-subscriptions.d.ts +82 -0
- package/dist/repair-layer-subscriptions.d.ts.map +1 -0
- package/dist/repair-layer-subscriptions.js +192 -0
- package/dist/repair-layer-subscriptions.js.map +7 -0
- package/dist/signaling.js.map +2 -2
- package/dist/timestamp-codec.js +12 -19
- package/dist/timestamp-codec.js.map +2 -2
- package/dist/track-session.d.ts +1 -1
- package/dist/track-session.d.ts.map +1 -1
- package/dist/track-session.js +1 -0
- package/dist/track-session.js.map +2 -2
- package/dist/wasm-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +218 -45
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +8 -5
- package/schemas/catalog.schema.json +4 -0
- package/src/catalog-fec-params.ts +36 -1
- package/src/cmcd-producer.ts +194 -179
- package/src/fec-manager.ts +433 -32
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +20 -2
- package/src/iso-block-locator.ts +133 -31
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +306 -29
- package/src/mmtp-fec-session.ts +115 -15
- package/src/mmtp-router.ts +629 -54
- package/src/multicast-manager.ts +70 -11
- package/src/multicast-transport.ts +44 -20
- package/src/repair-fetch.ts +363 -49
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +4 -1
package/dist/mmtp-router.js
CHANGED
|
@@ -149,30 +149,26 @@ function isEmptyOrExactLegacyTrailer(payload, start, end) {
|
|
|
149
149
|
const authLength = view2.getUint16(2, false);
|
|
150
150
|
return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end;
|
|
151
151
|
}
|
|
152
|
-
function resolveDeclaredTrailers(payload, declaredEnd, fecType, options
|
|
152
|
+
function resolveDeclaredTrailers(payload, declaredEnd, fecType, options) {
|
|
153
153
|
if (declaredEnd > payload.length)
|
|
154
154
|
return null;
|
|
155
155
|
if (options?.skipFecId) {
|
|
156
|
-
return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false
|
|
156
|
+
return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false } : null;
|
|
157
157
|
}
|
|
158
|
-
const overlay = options?.overlayDeclared === true && rapFlag;
|
|
159
158
|
if (fecType === 1) {
|
|
160
|
-
const fecStart = payload.length -
|
|
159
|
+
const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
161
160
|
if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {
|
|
162
161
|
return null;
|
|
163
162
|
}
|
|
164
|
-
return { hasFecId: true
|
|
163
|
+
return { hasFecId: true };
|
|
165
164
|
}
|
|
166
|
-
if (fecType !== 0
|
|
165
|
+
if (fecType !== 0)
|
|
167
166
|
return null;
|
|
168
167
|
if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {
|
|
169
|
-
return { hasFecId: false
|
|
168
|
+
return { hasFecId: false };
|
|
170
169
|
}
|
|
171
170
|
return null;
|
|
172
171
|
}
|
|
173
|
-
function sourceFecTrailerSize(overlay) {
|
|
174
|
-
return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
175
|
-
}
|
|
176
172
|
function planMfuExtraction(mmtpPacket, options, parsedHeader) {
|
|
177
173
|
if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {
|
|
178
174
|
return {
|
|
@@ -225,11 +221,11 @@ function planMfuExtraction(mmtpPacket, options, parsedHeader) {
|
|
|
225
221
|
if ("rejection" in layoutResult)
|
|
226
222
|
return layoutResult;
|
|
227
223
|
const { layout } = layoutResult;
|
|
228
|
-
const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options
|
|
224
|
+
const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options);
|
|
229
225
|
if (!trailerLayout) {
|
|
230
226
|
if (!options?.skipFecId && header.fecType === 1) {
|
|
231
227
|
const observed = payload.length - layout.dataEnd;
|
|
232
|
-
const expected =
|
|
228
|
+
const expected = SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
233
229
|
if (observed !== expected) {
|
|
234
230
|
return {
|
|
235
231
|
rejection: {
|
|
@@ -261,13 +257,10 @@ function extractPlannedMfu(mmtpPacket, result) {
|
|
|
261
257
|
const mfuDuHeader = mfuDuHeaderPresent && mpu.timedFlag === 1 ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE) : null;
|
|
262
258
|
const itemId = mfuDuHeaderPresent && mpu.timedFlag === 0 ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false) : null;
|
|
263
259
|
const data = payload.subarray(mfuOffset, dataEnd);
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null;
|
|
269
|
-
const overlayFecPayloadId = trailerLayout.hasOverlayFecId ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE) : null;
|
|
270
|
-
return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId };
|
|
260
|
+
const fecPayloadId = trailerLayout.hasFecId ? {
|
|
261
|
+
ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE, 4).getUint32(0, false)
|
|
262
|
+
} : null;
|
|
263
|
+
return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId };
|
|
271
264
|
}
|
|
272
265
|
function sourceFecPayloadIdOf(mmtpPacket, header) {
|
|
273
266
|
if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE)
|
|
@@ -377,6 +370,1422 @@ var RUNTIME_FEC_RECEIVER = Object.freeze({
|
|
|
377
370
|
overlays: true
|
|
378
371
|
});
|
|
379
372
|
|
|
373
|
+
// node_modules/@blockcast/mmt-transport/dist/multicast-tier.js
|
|
374
|
+
function endpointKey(endpoint) {
|
|
375
|
+
return `${endpoint.sourceAddress ?? ""}|${endpoint.groupAddress}|${endpoint.port}`;
|
|
376
|
+
}
|
|
377
|
+
function carriesMedia(endpoint) {
|
|
378
|
+
return endpoint.carriesMedia !== false;
|
|
379
|
+
}
|
|
380
|
+
function selectMulticastTier(endpoints, estimate) {
|
|
381
|
+
const media = endpoints.filter(carriesMedia);
|
|
382
|
+
if (media.length <= 1)
|
|
383
|
+
return media[0];
|
|
384
|
+
const ranked = media.filter((endpoint) => typeof endpoint.bandwidth === "number" && endpoint.bandwidth > 0).sort((a, b) => a.bandwidth - b.bandwidth);
|
|
385
|
+
if (ranked.length < 2)
|
|
386
|
+
return media[0];
|
|
387
|
+
if (estimate === void 0)
|
|
388
|
+
return ranked[0];
|
|
389
|
+
const budget = estimate * 0.8;
|
|
390
|
+
let selected;
|
|
391
|
+
for (const endpoint of ranked) {
|
|
392
|
+
if (endpoint.bandwidth <= budget)
|
|
393
|
+
selected = endpoint;
|
|
394
|
+
}
|
|
395
|
+
return selected ?? ranked[0];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// node_modules/@blockcast/mmt-transport/dist/abr-controller.js
|
|
399
|
+
var QUALITY_LEVELS = Object.freeze([
|
|
400
|
+
{ name: "240p", bitrate: 3e5, width: 426, height: 240, fps: 15 },
|
|
401
|
+
{ name: "360p", bitrate: 5e5, width: 640, height: 360, fps: 24 },
|
|
402
|
+
{ name: "480p", bitrate: 1e6, width: 854, height: 480, fps: 30 },
|
|
403
|
+
{ name: "720p", bitrate: 25e5, width: 1280, height: 720, fps: 30 },
|
|
404
|
+
{ name: "1080p", bitrate: 5e6, width: 1920, height: 1080, fps: 30 },
|
|
405
|
+
{ name: "1440p", bitrate: 8e6, width: 2560, height: 1440, fps: 30 },
|
|
406
|
+
{ name: "4K", bitrate: 15e6, width: 3840, height: 2160, fps: 30 }
|
|
407
|
+
].map((level) => Object.freeze(level)));
|
|
408
|
+
function positiveFinite(value) {
|
|
409
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
410
|
+
}
|
|
411
|
+
function validateQualityLadder(ladder) {
|
|
412
|
+
if (!Array.isArray(ladder) || ladder.length === 0) {
|
|
413
|
+
throw new TypeError("[ABR] ladder must list at least one rendition (e.g. the catalog's video tracks)");
|
|
414
|
+
}
|
|
415
|
+
const names = /* @__PURE__ */ new Set();
|
|
416
|
+
for (const level of ladder) {
|
|
417
|
+
if (typeof level?.name !== "string" || level.name.length === 0) {
|
|
418
|
+
throw new TypeError("[ABR] every ladder rendition needs a name");
|
|
419
|
+
}
|
|
420
|
+
if (!positiveFinite(level.bitrate)) {
|
|
421
|
+
throw new RangeError(`[ABR] rendition ${level.name}: bitrate must be a positive finite number, got ${String(level.bitrate)}`);
|
|
422
|
+
}
|
|
423
|
+
for (const field of ["width", "height", "fps"]) {
|
|
424
|
+
if (level[field] !== void 0 && !positiveFinite(level[field])) {
|
|
425
|
+
throw new RangeError(`[ABR] rendition ${level.name}: ${field} must be a positive finite number when stated, got ${String(level[field])}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (names.has(level.name))
|
|
429
|
+
throw new RangeError(`[ABR] rendition ${level.name} appears twice in the ladder`);
|
|
430
|
+
names.add(level.name);
|
|
431
|
+
}
|
|
432
|
+
return Object.freeze([...ladder].sort((a, b) => a.bitrate - b.bitrate).map((level) => Object.freeze({ ...level })));
|
|
433
|
+
}
|
|
434
|
+
var AdaptiveBitrateController = class {
|
|
435
|
+
/** The caller's renditions, ascending by bitrate (`validateQualityLadder`). */
|
|
436
|
+
ladder;
|
|
437
|
+
currentQualityIndex;
|
|
438
|
+
history = [];
|
|
439
|
+
historyWindow = 1e4;
|
|
440
|
+
// 10 second window
|
|
441
|
+
upgradeThreshold = 0.8;
|
|
442
|
+
// 80% of target bitrate sustained
|
|
443
|
+
downgradeThreshold = 0.5;
|
|
444
|
+
// 50% of target bitrate triggers downgrade
|
|
445
|
+
stabilityPeriod = 5e3;
|
|
446
|
+
// Wait 5s before upgrading
|
|
447
|
+
lastQualityChange = 0;
|
|
448
|
+
onQualityChange;
|
|
449
|
+
// FEC stats for effective loss rate calculation
|
|
450
|
+
fecStats = {
|
|
451
|
+
rawLossRate: 0,
|
|
452
|
+
effectiveLossRate: 0,
|
|
453
|
+
recoveryRate: 1,
|
|
454
|
+
fecEnabled: false
|
|
455
|
+
};
|
|
456
|
+
constructor(options) {
|
|
457
|
+
if (!options || typeof options !== "object") {
|
|
458
|
+
throw new TypeError("[ABR] AdaptiveBitrateController needs { ladder, initialQuality }");
|
|
459
|
+
}
|
|
460
|
+
this.ladder = validateQualityLadder(options.ladder);
|
|
461
|
+
this.currentQualityIndex = options.initialQuality === void 0 ? 0 : this.ladder.findIndex((q) => q.name === options.initialQuality);
|
|
462
|
+
if (this.currentQualityIndex < 0) {
|
|
463
|
+
throw new RangeError(`[ABR] initial rendition ${String(options.initialQuality)} is not in the ladder`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
setCallback(callback) {
|
|
467
|
+
this.onQualityChange = callback;
|
|
468
|
+
}
|
|
469
|
+
getCurrentQuality() {
|
|
470
|
+
return this.ladder[this.currentQualityIndex];
|
|
471
|
+
}
|
|
472
|
+
/** Selects the multicast (S,G) tier without replacing a player's unicast ABR manager. */
|
|
473
|
+
selectMulticastTier(endpoints, estimate = this.getMetrics().measuredBitrate || void 0) {
|
|
474
|
+
return selectMulticastTier(endpoints, estimate);
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Record network sample for bitrate adaptation
|
|
478
|
+
*
|
|
479
|
+
* @param bytesReceived - Bytes received in this sample
|
|
480
|
+
* @param packetsLost - Packets lost (raw, before FEC recovery)
|
|
481
|
+
*/
|
|
482
|
+
recordSample(bytesReceived, packetsLost) {
|
|
483
|
+
const now = Date.now();
|
|
484
|
+
this.history.push({ timestamp: now, bytesReceived, packetsLost });
|
|
485
|
+
this.history = this.history.filter((s) => now - s.timestamp < this.historyWindow);
|
|
486
|
+
const { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();
|
|
487
|
+
const currentQuality = this.getCurrentQuality();
|
|
488
|
+
const effectiveLossRate = this.fecStats.fecEnabled ? this.fecStats.effectiveLossRate : rawLossRate;
|
|
489
|
+
if (now - this.lastQualityChange < this.stabilityPeriod) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (effectiveLossRate > 0.05 || measuredBitrate < currentQuality.bitrate * this.downgradeThreshold) {
|
|
493
|
+
this.downgrade();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (measuredBitrate > currentQuality.bitrate * this.upgradeThreshold && effectiveLossRate < 0.01 && this.currentQualityIndex < this.ladder.length - 1) {
|
|
497
|
+
this.upgrade();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Update FEC statistics for ABR decisions
|
|
502
|
+
*
|
|
503
|
+
* ABR uses the effective loss rate (after FEC recovery) rather than the
|
|
504
|
+
* raw transport loss rate.
|
|
505
|
+
*
|
|
506
|
+
* @param stats - FEC statistics from the FEC decoder
|
|
507
|
+
*/
|
|
508
|
+
setFecStats(stats) {
|
|
509
|
+
this.fecStats = stats;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Get current FEC statistics
|
|
513
|
+
*/
|
|
514
|
+
getFecStats() {
|
|
515
|
+
return { ...this.fecStats };
|
|
516
|
+
}
|
|
517
|
+
calculateMetrics() {
|
|
518
|
+
if (this.history.length < 2) {
|
|
519
|
+
return { measuredBitrate: 0, lossRate: 0 };
|
|
520
|
+
}
|
|
521
|
+
const totalBytes = this.history.reduce((sum, s) => sum + s.bytesReceived, 0);
|
|
522
|
+
const totalLost = this.history.reduce((sum, s) => sum + s.packetsLost, 0);
|
|
523
|
+
const duration = (this.history[this.history.length - 1].timestamp - this.history[0].timestamp) / 1e3;
|
|
524
|
+
const measuredBitrate = duration > 0 ? totalBytes * 8 / duration : 0;
|
|
525
|
+
const lossRate = totalLost / (this.history.length + totalLost);
|
|
526
|
+
return { measuredBitrate, lossRate };
|
|
527
|
+
}
|
|
528
|
+
upgrade() {
|
|
529
|
+
if (this.currentQualityIndex >= this.ladder.length - 1)
|
|
530
|
+
return;
|
|
531
|
+
this.currentQualityIndex++;
|
|
532
|
+
this.lastQualityChange = Date.now();
|
|
533
|
+
const newQuality = this.getCurrentQuality();
|
|
534
|
+
console.log(`[ABR] Upgrading to ${newQuality.name} (${newQuality.bitrate / 1e6}Mbps)`);
|
|
535
|
+
this.onQualityChange?.(newQuality);
|
|
536
|
+
}
|
|
537
|
+
downgrade() {
|
|
538
|
+
if (this.currentQualityIndex <= 0)
|
|
539
|
+
return;
|
|
540
|
+
this.currentQualityIndex--;
|
|
541
|
+
this.lastQualityChange = Date.now();
|
|
542
|
+
const newQuality = this.getCurrentQuality();
|
|
543
|
+
console.log(`[ABR] Downgrading to ${newQuality.name} (${newQuality.bitrate / 1e6}Mbps)`);
|
|
544
|
+
this.onQualityChange?.(newQuality);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Select a ladder rendition: the caller switched to it (a manual override,
|
|
548
|
+
* or a switch the caller made). Throws for a name the ladder does not carry.
|
|
549
|
+
*/
|
|
550
|
+
setQuality(name) {
|
|
551
|
+
const index = this.ladder.findIndex((q) => q.name === name);
|
|
552
|
+
if (index < 0)
|
|
553
|
+
throw new RangeError(`[ABR] rendition ${name} is not in the ladder`);
|
|
554
|
+
this.currentQualityIndex = index;
|
|
555
|
+
this.lastQualityChange = Date.now();
|
|
556
|
+
this.onQualityChange?.(this.getCurrentQuality());
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Get current metrics including both raw and effective loss rates
|
|
560
|
+
*/
|
|
561
|
+
getMetrics() {
|
|
562
|
+
const { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();
|
|
563
|
+
return {
|
|
564
|
+
measuredBitrate,
|
|
565
|
+
rawLossRate,
|
|
566
|
+
effectiveLossRate: this.fecStats.fecEnabled ? this.fecStats.effectiveLossRate : rawLossRate,
|
|
567
|
+
fecEnabled: this.fecStats.fecEnabled,
|
|
568
|
+
sampleCount: this.history.length
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Reset history and start fresh
|
|
573
|
+
*/
|
|
574
|
+
reset() {
|
|
575
|
+
this.history = [];
|
|
576
|
+
this.lastQualityChange = 0;
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// node_modules/@blockcast/mmt-base/dist/mmtp.js
|
|
581
|
+
var MPU_HEADER_SIZE2 = 8;
|
|
582
|
+
var MPU_LENGTH_FIELD_SIZE2 = 2;
|
|
583
|
+
var MPU_HEADER_REMAINDER_SIZE2 = MPU_HEADER_SIZE2 - MPU_LENGTH_FIELD_SIZE2;
|
|
584
|
+
var MMTP_SIGNALING_PACKET_ID2 = 0;
|
|
585
|
+
var MMTP_PACKET_ID_MAX = 65535;
|
|
586
|
+
|
|
587
|
+
// node_modules/@blockcast/mmt-base/dist/net-constants.js
|
|
588
|
+
var DEFAULT_AMT_RELAY_PORT = 2268;
|
|
589
|
+
|
|
590
|
+
// node_modules/@blockcast/mmt-base/dist/ntp-short.js
|
|
591
|
+
var NTP_SHORT_BITS2 = 32;
|
|
592
|
+
var NTP_SHORT_MODULUS2 = 2 ** NTP_SHORT_BITS2;
|
|
593
|
+
var NTP_SHORT_HALF_RANGE2 = 2 ** (NTP_SHORT_BITS2 - 1);
|
|
594
|
+
function isNtpShort(value) {
|
|
595
|
+
return Number.isInteger(value) && value >= 0 && value < NTP_SHORT_MODULUS2;
|
|
596
|
+
}
|
|
597
|
+
function compareNtpShort(a, b) {
|
|
598
|
+
if (!isNtpShort(a) || !isNtpShort(b)) {
|
|
599
|
+
throw new RangeError(`NTP-short values must be integers in [0, 2^${NTP_SHORT_BITS2}), got ${String(a)} and ${String(b)}`);
|
|
600
|
+
}
|
|
601
|
+
if (a === b)
|
|
602
|
+
return "equal";
|
|
603
|
+
const forward = (a - b + NTP_SHORT_MODULUS2) % NTP_SHORT_MODULUS2;
|
|
604
|
+
if (forward < NTP_SHORT_HALF_RANGE2)
|
|
605
|
+
return "after";
|
|
606
|
+
if (forward > NTP_SHORT_HALF_RANGE2)
|
|
607
|
+
return "before";
|
|
608
|
+
return "unordered";
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// node_modules/@blockcast/mmt-base/dist/fec-block-profile.js
|
|
612
|
+
var FEC_BLOCK_PROFILES2 = Object.freeze([
|
|
613
|
+
"iso-ssbg1-v1"
|
|
614
|
+
]);
|
|
615
|
+
|
|
616
|
+
// node_modules/@blockcast/mmt-base/dist/fec-bindings.js
|
|
617
|
+
var RUNTIME_FEC_RECEIVER2 = Object.freeze({
|
|
618
|
+
schemes: Object.freeze(["raptorq", "reed-solomon"]),
|
|
619
|
+
blockProfiles: Object.freeze(["iso-ssbg1-v1"]),
|
|
620
|
+
overlays: true
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// node_modules/@blockcast/mmt-transport/dist/multicast-orchestrator.js
|
|
624
|
+
var DEFAULT_DWELL_MS = 3e3;
|
|
625
|
+
var DEFAULT_JOIN_DEADLINE_MS = 5e3;
|
|
626
|
+
var MulticastOrchestrator = class {
|
|
627
|
+
options;
|
|
628
|
+
#state = "stable";
|
|
629
|
+
#current;
|
|
630
|
+
#target;
|
|
631
|
+
#estimate;
|
|
632
|
+
#favored;
|
|
633
|
+
#joinStartedAt = 0;
|
|
634
|
+
/**
|
|
635
|
+
* Latest active-tier send time (NTP short), or undefined before the first.
|
|
636
|
+
* "Latest" is serial order, not the numerically largest value: after the
|
|
637
|
+
* field wraps, the largest value is the stale one.
|
|
638
|
+
*/
|
|
639
|
+
#highWater;
|
|
640
|
+
#dwellMs;
|
|
641
|
+
#joinDeadlineMs;
|
|
642
|
+
constructor(options) {
|
|
643
|
+
this.options = options;
|
|
644
|
+
this.#dwellMs = options.dwellMs ?? DEFAULT_DWELL_MS;
|
|
645
|
+
this.#joinDeadlineMs = options.joinDeadlineMs ?? DEFAULT_JOIN_DEADLINE_MS;
|
|
646
|
+
if (!Number.isFinite(this.#dwellMs) || !Number.isFinite(this.#joinDeadlineMs) || this.#dwellMs <= 0 || this.#joinDeadlineMs <= 0) {
|
|
647
|
+
throw new Error("Multicast dwell and join deadline must be positive");
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
get state() {
|
|
651
|
+
return this.#state;
|
|
652
|
+
}
|
|
653
|
+
get current() {
|
|
654
|
+
return this.#current;
|
|
655
|
+
}
|
|
656
|
+
get target() {
|
|
657
|
+
return this.#target;
|
|
658
|
+
}
|
|
659
|
+
get estimate() {
|
|
660
|
+
return this.#estimate;
|
|
661
|
+
}
|
|
662
|
+
start() {
|
|
663
|
+
this.#current ??= this.options.initialEndpoint ?? selectMulticastTier(this.options.endpoints, void 0);
|
|
664
|
+
return this.#current;
|
|
665
|
+
}
|
|
666
|
+
onGoodput(sample) {
|
|
667
|
+
const bitrate = sample.bitrate;
|
|
668
|
+
if (!this.#current)
|
|
669
|
+
return void 0;
|
|
670
|
+
if (this.#state === "joining")
|
|
671
|
+
return this.poll();
|
|
672
|
+
if (this.options.adaptive === false)
|
|
673
|
+
return void 0;
|
|
674
|
+
if (bitrate === void 0 || !Number.isFinite(bitrate))
|
|
675
|
+
return void 0;
|
|
676
|
+
this.#estimate = bitrate;
|
|
677
|
+
const desired = selectMulticastTier(this.options.endpoints, bitrate);
|
|
678
|
+
if (!desired)
|
|
679
|
+
return void 0;
|
|
680
|
+
const desiredKey = endpointKey(desired);
|
|
681
|
+
if (desiredKey === endpointKey(this.#current)) {
|
|
682
|
+
if (this.#favored?.source === "goodput")
|
|
683
|
+
this.#favored = void 0;
|
|
684
|
+
return void 0;
|
|
685
|
+
}
|
|
686
|
+
if (typeof desired.bandwidth !== "number" || typeof this.#current.bandwidth !== "number" || desired.bandwidth >= this.#current.bandwidth) {
|
|
687
|
+
if (this.#favored?.source === "goodput")
|
|
688
|
+
this.#favored = void 0;
|
|
689
|
+
return void 0;
|
|
690
|
+
}
|
|
691
|
+
const now = this.#now();
|
|
692
|
+
if (!this.#favored || this.#favored.key !== desiredKey || this.#favored.source !== "goodput") {
|
|
693
|
+
this.#favored = { key: desiredKey, since: now, source: "goodput" };
|
|
694
|
+
return void 0;
|
|
695
|
+
}
|
|
696
|
+
if (now - this.#favored.since < this.#dwellMs)
|
|
697
|
+
return void 0;
|
|
698
|
+
this.#state = "joining";
|
|
699
|
+
this.#target = desired;
|
|
700
|
+
this.#joinStartedAt = now;
|
|
701
|
+
this.#favored = void 0;
|
|
702
|
+
return { kind: "join-target", current: this.#current, target: desired };
|
|
703
|
+
}
|
|
704
|
+
onHeadroom(sample) {
|
|
705
|
+
if (!this.#current || this.options.adaptive === false || this.#state === "joining")
|
|
706
|
+
return void 0;
|
|
707
|
+
if (!sample.healthy) {
|
|
708
|
+
if (this.#favored?.source === "headroom")
|
|
709
|
+
this.#favored = void 0;
|
|
710
|
+
return void 0;
|
|
711
|
+
}
|
|
712
|
+
if (this.#favored?.source === "goodput")
|
|
713
|
+
return void 0;
|
|
714
|
+
const currentBandwidth = this.#current.bandwidth;
|
|
715
|
+
if (typeof currentBandwidth !== "number")
|
|
716
|
+
return void 0;
|
|
717
|
+
const desired = this.options.endpoints.filter((endpoint) => carriesMedia(endpoint) && typeof endpoint.bandwidth === "number" && endpoint.bandwidth > currentBandwidth).sort((a, b) => a.bandwidth - b.bandwidth)[0];
|
|
718
|
+
if (!desired) {
|
|
719
|
+
this.#favored = void 0;
|
|
720
|
+
return void 0;
|
|
721
|
+
}
|
|
722
|
+
const desiredKey = endpointKey(desired);
|
|
723
|
+
if (desiredKey === endpointKey(this.#current)) {
|
|
724
|
+
this.#favored = void 0;
|
|
725
|
+
return void 0;
|
|
726
|
+
}
|
|
727
|
+
const now = this.#now();
|
|
728
|
+
if (!this.#favored || this.#favored.key !== desiredKey) {
|
|
729
|
+
this.#favored = { key: desiredKey, since: now, source: "headroom" };
|
|
730
|
+
return void 0;
|
|
731
|
+
}
|
|
732
|
+
if (now - this.#favored.since < this.#dwellMs)
|
|
733
|
+
return void 0;
|
|
734
|
+
this.#state = "joining";
|
|
735
|
+
this.#target = desired;
|
|
736
|
+
this.#joinStartedAt = now;
|
|
737
|
+
this.#favored = void 0;
|
|
738
|
+
return { kind: "join-target", current: this.#current, target: desired };
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Join `target` because the viewer selected it, not because goodput or
|
|
742
|
+
* headroom ranked it. It runs whether or not the orchestrator adapts, and
|
|
743
|
+
* takes the path an adaptive join takes: commit on the target's RAP at or
|
|
744
|
+
* beyond the high-water mark, abandon at the join deadline. While it is
|
|
745
|
+
* pending nothing re-ranks the ladder; goodput only advances the deadline.
|
|
746
|
+
*
|
|
747
|
+
* Returns undefined, changing nothing, while another join is pending (cancel
|
|
748
|
+
* it first), when `target` is the current tier, or when it carries no media.
|
|
749
|
+
*/
|
|
750
|
+
select(target) {
|
|
751
|
+
if (!this.#current || this.#state === "joining" || !carriesMedia(target))
|
|
752
|
+
return void 0;
|
|
753
|
+
if (endpointKey(target) === endpointKey(this.#current))
|
|
754
|
+
return void 0;
|
|
755
|
+
this.#state = "joining";
|
|
756
|
+
this.#target = target;
|
|
757
|
+
this.#joinStartedAt = this.#now();
|
|
758
|
+
this.#favored = void 0;
|
|
759
|
+
return { kind: "join-target", current: this.#current, target };
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Feed a packet from the active tier (to advance the high-water mark) or
|
|
763
|
+
* from the in-flight target (to find a committable RAP).
|
|
764
|
+
*
|
|
765
|
+
* Serial order is only the true order while the compared instants are less
|
|
766
|
+
* than `NTP_SHORT_HALF_RANGE` (2^15 s, ~9.1 h) apart. Feeding every
|
|
767
|
+
* active-tier packet keeps the mark that close to the live stream. A mark
|
|
768
|
+
* refreshed only by commits can fall further behind than that, and a
|
|
769
|
+
* target RAP may then compare as older, so the join is abandoned.
|
|
770
|
+
*/
|
|
771
|
+
onTierPacket(packet) {
|
|
772
|
+
if (!this.#current)
|
|
773
|
+
return void 0;
|
|
774
|
+
const sentAt = tierPacketSendTime(packet);
|
|
775
|
+
if (packet.endpointKey === endpointKey(this.#current) && sentAt !== void 0 && (this.#highWater === void 0 || compareNtpShort(sentAt, this.#highWater) === "after")) {
|
|
776
|
+
this.#highWater = sentAt;
|
|
777
|
+
}
|
|
778
|
+
if (this.#state !== "joining" || !this.#target)
|
|
779
|
+
return void 0;
|
|
780
|
+
const expired = this.poll();
|
|
781
|
+
if (expired)
|
|
782
|
+
return expired;
|
|
783
|
+
if (packet.endpointKey !== endpointKey(this.#target) || !packet.keyframe || !packet.hasValidCodecConfig || sentAt === void 0 || !this.#reachesHighWater(sentAt))
|
|
784
|
+
return void 0;
|
|
785
|
+
const command = {
|
|
786
|
+
kind: "commit-swap",
|
|
787
|
+
target: this.#target,
|
|
788
|
+
old: this.#current
|
|
789
|
+
};
|
|
790
|
+
this.#highWater = sentAt;
|
|
791
|
+
this.#current = this.#target;
|
|
792
|
+
this.#target = void 0;
|
|
793
|
+
this.#state = "stable";
|
|
794
|
+
return command;
|
|
795
|
+
}
|
|
796
|
+
poll() {
|
|
797
|
+
if (this.#state !== "joining" || !this.#target || !this.#current)
|
|
798
|
+
return void 0;
|
|
799
|
+
if (this.#now() - this.#joinStartedAt < this.#joinDeadlineMs)
|
|
800
|
+
return void 0;
|
|
801
|
+
return this.cancel();
|
|
802
|
+
}
|
|
803
|
+
cancel() {
|
|
804
|
+
if (this.#state !== "joining" || !this.#target || !this.#current)
|
|
805
|
+
return void 0;
|
|
806
|
+
const command = {
|
|
807
|
+
kind: "abandon-join",
|
|
808
|
+
current: this.#current,
|
|
809
|
+
target: this.#target
|
|
810
|
+
};
|
|
811
|
+
this.#target = void 0;
|
|
812
|
+
this.#state = "stable";
|
|
813
|
+
return command;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Whether a target RAP sent at `sentAt` is at or beyond the active tier's
|
|
817
|
+
* high-water mark. A pair exactly half the NTP-short space apart has no
|
|
818
|
+
* serial order, so it cannot show that and does not commit.
|
|
819
|
+
*/
|
|
820
|
+
#reachesHighWater(sentAt) {
|
|
821
|
+
if (this.#highWater === void 0)
|
|
822
|
+
return true;
|
|
823
|
+
const order = compareNtpShort(sentAt, this.#highWater);
|
|
824
|
+
return order === "after" || order === "equal";
|
|
825
|
+
}
|
|
826
|
+
#now() {
|
|
827
|
+
const now = this.options.now();
|
|
828
|
+
if (!Number.isFinite(now))
|
|
829
|
+
throw new Error("Multicast orchestrator clock must be finite");
|
|
830
|
+
return now;
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
function tierPacketSendTime(packet) {
|
|
834
|
+
if (!Number.isFinite(packet.timestamp))
|
|
835
|
+
return void 0;
|
|
836
|
+
if (!isNtpShort(packet.timestamp)) {
|
|
837
|
+
throw new RangeError(`Multicast tier packet timestamp must be an MMTP NTP-short send time, got ${packet.timestamp}`);
|
|
838
|
+
}
|
|
839
|
+
return packet.timestamp;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// node_modules/@blockcast/mmt-transport/dist/endpoint-config.js
|
|
843
|
+
function toWindowMulticastSubscribeConfig(input) {
|
|
844
|
+
if (input.mode !== void 0 && !["auto", "native", "tunnel"].includes(input.mode)) {
|
|
845
|
+
throw new TypeError(`Unsupported multicast mode: ${String(input.mode)}`);
|
|
846
|
+
}
|
|
847
|
+
if (input.packetIds !== void 0 && !Array.isArray(input.packetIds)) {
|
|
848
|
+
throw new TypeError(`multicast packetIds must be an array, got ${input.packetIds === null ? "null" : typeof input.packetIds}`);
|
|
849
|
+
}
|
|
850
|
+
if (input.packetIds?.some((id) => !isMulticastTrackPacketId(id)) || input.tracks?.some((track) => !isMulticastTrackPacketId(track.packetId))) {
|
|
851
|
+
throw new RangeError(`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`);
|
|
852
|
+
}
|
|
853
|
+
const source = input.source?.trim() || void 0;
|
|
854
|
+
const senderAddress = input.senderAddress?.trim() || void 0;
|
|
855
|
+
if (source && senderAddress) {
|
|
856
|
+
throw new TypeError("multicast senderAddress is an ASM filter and never appears beside source");
|
|
857
|
+
}
|
|
858
|
+
const base = {
|
|
859
|
+
group: input.group,
|
|
860
|
+
port: input.port,
|
|
861
|
+
...source ? { source } : {},
|
|
862
|
+
consume: "bytes",
|
|
863
|
+
...input.packetIds?.length ? { packetIds: [...input.packetIds] } : {}
|
|
864
|
+
};
|
|
865
|
+
const filter = senderAddress ? { senderAddress } : {};
|
|
866
|
+
if (input.atsc3) {
|
|
867
|
+
if (senderAddress)
|
|
868
|
+
throw new TypeError("an ATSC3 tuner request carries no senderAddress");
|
|
869
|
+
const { frequency, plpId, serviceId } = input.atsc3;
|
|
870
|
+
if (!Number.isInteger(frequency) || frequency < 1) {
|
|
871
|
+
throw new TypeError(`ATSC3 tuning requires a positive integer frequency in kHz, got ${String(frequency)}`);
|
|
872
|
+
}
|
|
873
|
+
return {
|
|
874
|
+
...base,
|
|
875
|
+
origin: "tuner",
|
|
876
|
+
standard: "atsc3",
|
|
877
|
+
frequency,
|
|
878
|
+
...plpId !== void 0 ? { plpId } : {},
|
|
879
|
+
...serviceId !== void 0 ? { serviceId } : {}
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
const relay = input.amtRelay?.trim();
|
|
883
|
+
const driad = !relay && input.amtDiscovery === "driad";
|
|
884
|
+
const wantsAmt = input.mode === "tunnel" || input.mode !== "native" && (relay !== void 0 && relay !== "" || driad);
|
|
885
|
+
if (!wantsAmt) {
|
|
886
|
+
const native = {
|
|
887
|
+
...base,
|
|
888
|
+
...filter,
|
|
889
|
+
origin: "ip-multicast",
|
|
890
|
+
...input.tracks?.length ? { tracks: input.tracks.map(({ name, packetId }) => ({ name, packetId })) } : {}
|
|
891
|
+
};
|
|
892
|
+
return native;
|
|
893
|
+
}
|
|
894
|
+
const relayPort = input.amtRelayPort;
|
|
895
|
+
if (driad) {
|
|
896
|
+
if (!base.source)
|
|
897
|
+
throw new TypeError("AMT DRIAD discovery requires a source address");
|
|
898
|
+
if (relayPort !== void 0 && !isPositivePort(relayPort)) {
|
|
899
|
+
throw new TypeError("AMT DRIAD discovery requires a valid relay port when one is given");
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
...base,
|
|
903
|
+
source: base.source,
|
|
904
|
+
origin: "amt",
|
|
905
|
+
discovery: "driad",
|
|
906
|
+
...relayPort !== void 0 ? { relayPort } : {}
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
if (!relay || relayPort === void 0 || !isPositivePort(relayPort)) {
|
|
910
|
+
throw new TypeError("AMT requires a resolved relay and a valid relay port");
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
...base,
|
|
914
|
+
...filter,
|
|
915
|
+
origin: "amt",
|
|
916
|
+
relay,
|
|
917
|
+
relayPort
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
function isValidMulticastGroupAddress(address) {
|
|
921
|
+
const value = address?.trim().toLowerCase();
|
|
922
|
+
if (!value)
|
|
923
|
+
return false;
|
|
924
|
+
if (value.startsWith("ff"))
|
|
925
|
+
return isValidIpv6MulticastAddress(value);
|
|
926
|
+
const parts = value.split(".");
|
|
927
|
+
if (parts.length !== 4)
|
|
928
|
+
return false;
|
|
929
|
+
const octets = parts.map((part) => {
|
|
930
|
+
if (!/^\d+$/.test(part))
|
|
931
|
+
return Number.NaN;
|
|
932
|
+
const octet = Number(part);
|
|
933
|
+
return Number.isInteger(octet) && octet >= 0 && octet <= 255 ? octet : Number.NaN;
|
|
934
|
+
});
|
|
935
|
+
return octets.every(Number.isInteger) && octets[0] >= 224 && octets[0] <= 239;
|
|
936
|
+
}
|
|
937
|
+
function normalizeMulticastEndpoint(endpoint) {
|
|
938
|
+
const groupAddress = endpoint.groupAddress.trim().toLowerCase();
|
|
939
|
+
const sourceAddress = endpoint.sourceAddress?.trim().toLowerCase() || void 0;
|
|
940
|
+
const senderAddress = endpoint.senderAddress?.trim().toLowerCase() || void 0;
|
|
941
|
+
if (sourceAddress && senderAddress)
|
|
942
|
+
return void 0;
|
|
943
|
+
const amtRelay = endpoint.amtRelay?.trim().toLowerCase() || void 0;
|
|
944
|
+
if (!isValidMulticastGroupAddress(groupAddress) || !isPositivePort(endpoint.port))
|
|
945
|
+
return void 0;
|
|
946
|
+
if (endpoint.mode && !["auto", "native", "tunnel"].includes(endpoint.mode))
|
|
947
|
+
return void 0;
|
|
948
|
+
if (endpoint.amtRelayPort !== void 0 && !isPositivePort(endpoint.amtRelayPort))
|
|
949
|
+
return void 0;
|
|
950
|
+
return {
|
|
951
|
+
groupAddress,
|
|
952
|
+
...sourceAddress ? { sourceAddress } : {},
|
|
953
|
+
...senderAddress ? { senderAddress } : {},
|
|
954
|
+
port: endpoint.port,
|
|
955
|
+
...endpoint.mode ? { mode: endpoint.mode } : {},
|
|
956
|
+
...amtRelay ? { amtRelay } : {},
|
|
957
|
+
...endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {},
|
|
958
|
+
...endpoint.amtDiscovery === "driad" ? { amtDiscovery: "driad" } : {}
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
function multicastEndpointKey(endpoint) {
|
|
962
|
+
const normalized = normalizeMulticastEndpoint(endpoint);
|
|
963
|
+
if (!normalized)
|
|
964
|
+
throw new Error("Invalid multicast endpoint");
|
|
965
|
+
return JSON.stringify([
|
|
966
|
+
normalized.sourceAddress ?? "",
|
|
967
|
+
normalized.groupAddress,
|
|
968
|
+
normalized.port,
|
|
969
|
+
normalized.mode ?? "auto",
|
|
970
|
+
normalized.amtRelay ?? "",
|
|
971
|
+
normalized.amtRelayPort ?? "",
|
|
972
|
+
// A DRIAD join is a different gateway subscription from a native one.
|
|
973
|
+
...normalized.amtDiscovery ? [normalized.amtDiscovery] : [],
|
|
974
|
+
// An ASM sender filter is a different subscription from the unfiltered join.
|
|
975
|
+
...normalized.senderAddress ? [{ senderAddress: normalized.senderAddress }] : []
|
|
976
|
+
]);
|
|
977
|
+
}
|
|
978
|
+
function isMulticastTrackPacketId(value) {
|
|
979
|
+
return Number.isInteger(value) && value > MMTP_SIGNALING_PACKET_ID2 && value <= MMTP_PACKET_ID_MAX;
|
|
980
|
+
}
|
|
981
|
+
function isValidIpv6MulticastAddress(value) {
|
|
982
|
+
if (!/^[0-9a-f:]+$/.test(value))
|
|
983
|
+
return false;
|
|
984
|
+
const compressed = value.includes("::");
|
|
985
|
+
if (value.indexOf("::") !== value.lastIndexOf("::"))
|
|
986
|
+
return false;
|
|
987
|
+
const groups = value.split(":").filter(Boolean);
|
|
988
|
+
if (groups.length === 0 || groups.length > 8)
|
|
989
|
+
return false;
|
|
990
|
+
if (!compressed && groups.length !== 8)
|
|
991
|
+
return false;
|
|
992
|
+
return groups.every((group) => /^[0-9a-f]{1,4}$/.test(group));
|
|
993
|
+
}
|
|
994
|
+
function isPositivePort(port) {
|
|
995
|
+
return Number.isInteger(port) && port > 0 && port <= 65535;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// node_modules/@blockcast/mmt-transport/dist/packet-batch.js
|
|
999
|
+
function subscriptionDeliversPacketBatches(subscription) {
|
|
1000
|
+
return subscription.eventTypes?.includes("packets") === true;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// node_modules/@blockcast/mmt-transport/dist/multicast-subscription-pool.js
|
|
1004
|
+
var MulticastSubscriptionPool = class {
|
|
1005
|
+
#entries = /* @__PURE__ */ new Map();
|
|
1006
|
+
#gatewayIds = /* @__PURE__ */ new WeakMap();
|
|
1007
|
+
#nextGatewayId = 1;
|
|
1008
|
+
async acquire(gateway, config, consumer) {
|
|
1009
|
+
const endpoint = normalizeMulticastEndpoint({
|
|
1010
|
+
groupAddress: config.group,
|
|
1011
|
+
sourceAddress: config.source,
|
|
1012
|
+
port: config.port,
|
|
1013
|
+
mode: config.mode,
|
|
1014
|
+
amtRelay: config.amtRelay,
|
|
1015
|
+
amtRelayPort: config.amtRelayPort,
|
|
1016
|
+
amtDiscovery: config.amtDiscovery
|
|
1017
|
+
});
|
|
1018
|
+
if (!endpoint)
|
|
1019
|
+
throw new Error("Invalid multicast endpoint");
|
|
1020
|
+
if (typeof gateway.subscribe !== "function")
|
|
1021
|
+
throw new Error("window.multicast.subscribe unavailable");
|
|
1022
|
+
const key = multicastEndpointKey(endpoint);
|
|
1023
|
+
const internalKey = `${this.#gatewayId(gateway)}|${key}`;
|
|
1024
|
+
let record = this.#entries.get(internalKey);
|
|
1025
|
+
if (!record) {
|
|
1026
|
+
record = { claims: 0, entry: Promise.resolve(null) };
|
|
1027
|
+
const createdRecord = record;
|
|
1028
|
+
record.entry = this.#createEntry(gateway, endpoint, (entry2) => {
|
|
1029
|
+
if (this.#entries.get(internalKey) === createdRecord)
|
|
1030
|
+
this.#entries.delete(internalKey);
|
|
1031
|
+
this.#detach(entry2);
|
|
1032
|
+
}).catch((error) => {
|
|
1033
|
+
if (this.#entries.get(internalKey) === createdRecord)
|
|
1034
|
+
this.#entries.delete(internalKey);
|
|
1035
|
+
throw error;
|
|
1036
|
+
});
|
|
1037
|
+
this.#entries.set(internalKey, record);
|
|
1038
|
+
}
|
|
1039
|
+
record.claims++;
|
|
1040
|
+
let entry;
|
|
1041
|
+
try {
|
|
1042
|
+
entry = await record.entry;
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
record.claims--;
|
|
1045
|
+
throw error;
|
|
1046
|
+
}
|
|
1047
|
+
if (entry.closed) {
|
|
1048
|
+
record.claims--;
|
|
1049
|
+
return this.acquire(gateway, config, consumer);
|
|
1050
|
+
}
|
|
1051
|
+
const token = Symbol(key);
|
|
1052
|
+
entry.consumers.set(token, consumer);
|
|
1053
|
+
let released = false;
|
|
1054
|
+
return {
|
|
1055
|
+
key,
|
|
1056
|
+
release: async () => {
|
|
1057
|
+
if (released)
|
|
1058
|
+
return;
|
|
1059
|
+
released = true;
|
|
1060
|
+
entry.consumers.delete(token);
|
|
1061
|
+
record.claims--;
|
|
1062
|
+
if (record.claims > 0)
|
|
1063
|
+
return;
|
|
1064
|
+
if (this.#entries.get(internalKey) === record)
|
|
1065
|
+
this.#entries.delete(internalKey);
|
|
1066
|
+
this.#detach(entry);
|
|
1067
|
+
if (!entry.closed)
|
|
1068
|
+
await entry.subscription.unsubscribe();
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
get size() {
|
|
1073
|
+
return this.#entries.size;
|
|
1074
|
+
}
|
|
1075
|
+
async #createEntry(gateway, endpoint, onClosed) {
|
|
1076
|
+
const subscription = await gateway.subscribe(toWindowMulticastSubscribeConfig({
|
|
1077
|
+
group: endpoint.groupAddress,
|
|
1078
|
+
...endpoint.sourceAddress ? { source: endpoint.sourceAddress } : {},
|
|
1079
|
+
port: endpoint.port,
|
|
1080
|
+
...endpoint.mode ? { mode: endpoint.mode } : {},
|
|
1081
|
+
...endpoint.amtRelay ? { amtRelay: endpoint.amtRelay } : {},
|
|
1082
|
+
...endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {},
|
|
1083
|
+
...endpoint.amtDiscovery ? { amtDiscovery: endpoint.amtDiscovery } : {}
|
|
1084
|
+
}));
|
|
1085
|
+
const consumers = /* @__PURE__ */ new Map();
|
|
1086
|
+
let entry;
|
|
1087
|
+
const packetListener = subscriptionDeliversPacketBatches(subscription) ? {
|
|
1088
|
+
type: "packets",
|
|
1089
|
+
listener: (event) => {
|
|
1090
|
+
for (const data of event.detail) {
|
|
1091
|
+
this.#dispatch(consumers, (consumer) => consumer.onBytes(data));
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
} : {
|
|
1095
|
+
type: "bytes",
|
|
1096
|
+
listener: (event) => {
|
|
1097
|
+
this.#dispatch(consumers, (consumer) => consumer.onBytes(event.detail));
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
const stateListener = (event) => {
|
|
1101
|
+
this.#dispatch(consumers, (consumer) => consumer.onState?.(event.detail));
|
|
1102
|
+
if (event.detail === "closed" && !entry.closed) {
|
|
1103
|
+
entry.closed = true;
|
|
1104
|
+
onClosed(entry);
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
const errorListener = (event) => {
|
|
1108
|
+
const error = event.detail instanceof Error ? event.detail : new Error(String(event.detail));
|
|
1109
|
+
this.#dispatch(consumers, (consumer) => consumer.onError?.(error));
|
|
1110
|
+
};
|
|
1111
|
+
entry = { subscription, consumers, packetListener, stateListener, errorListener, closed: false };
|
|
1112
|
+
if (packetListener.type === "packets") {
|
|
1113
|
+
subscription.addEventListener("packets", packetListener.listener);
|
|
1114
|
+
} else {
|
|
1115
|
+
subscription.addEventListener("bytes", packetListener.listener);
|
|
1116
|
+
}
|
|
1117
|
+
subscription.addEventListener("state", stateListener);
|
|
1118
|
+
subscription.addEventListener("error", errorListener);
|
|
1119
|
+
return entry;
|
|
1120
|
+
}
|
|
1121
|
+
#gatewayId(gateway) {
|
|
1122
|
+
const object = gateway;
|
|
1123
|
+
let id = this.#gatewayIds.get(object);
|
|
1124
|
+
if (!id) {
|
|
1125
|
+
id = this.#nextGatewayId++;
|
|
1126
|
+
this.#gatewayIds.set(object, id);
|
|
1127
|
+
}
|
|
1128
|
+
return id;
|
|
1129
|
+
}
|
|
1130
|
+
#detach(entry) {
|
|
1131
|
+
const { packetListener } = entry;
|
|
1132
|
+
if (packetListener.type === "packets") {
|
|
1133
|
+
entry.subscription.removeEventListener("packets", packetListener.listener);
|
|
1134
|
+
} else {
|
|
1135
|
+
entry.subscription.removeEventListener("bytes", packetListener.listener);
|
|
1136
|
+
}
|
|
1137
|
+
entry.subscription.removeEventListener("state", entry.stateListener);
|
|
1138
|
+
entry.subscription.removeEventListener("error", entry.errorListener);
|
|
1139
|
+
}
|
|
1140
|
+
#dispatch(consumers, callback) {
|
|
1141
|
+
for (const consumer of consumers.values()) {
|
|
1142
|
+
try {
|
|
1143
|
+
callback(consumer);
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
console.error("[Transport] multicast consumer callback failed", error);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
var multicastSubscriptionPool = new MulticastSubscriptionPool();
|
|
1151
|
+
|
|
1152
|
+
// node_modules/@blockcast/mmt-transport/dist/transport-manager.js
|
|
1153
|
+
var TransportManager = class {
|
|
1154
|
+
state = {
|
|
1155
|
+
mode: "disconnected",
|
|
1156
|
+
connected: false,
|
|
1157
|
+
packetsReceived: 0,
|
|
1158
|
+
bytesReceived: 0,
|
|
1159
|
+
lastPacketTime: 0
|
|
1160
|
+
};
|
|
1161
|
+
capabilities = null;
|
|
1162
|
+
subscription = null;
|
|
1163
|
+
/** Built by `connect()` from the caller's ladder; there is no built-in one. */
|
|
1164
|
+
abr = null;
|
|
1165
|
+
multicastOrchestrator = null;
|
|
1166
|
+
onOrchestratorCommand;
|
|
1167
|
+
connectionGeneration = 0;
|
|
1168
|
+
onPacket;
|
|
1169
|
+
onError;
|
|
1170
|
+
onStateChange;
|
|
1171
|
+
getWindow() {
|
|
1172
|
+
return typeof window === "undefined" ? void 0 : window;
|
|
1173
|
+
}
|
|
1174
|
+
getWindowMulticastGateway() {
|
|
1175
|
+
return this.getWindow()?.multicast;
|
|
1176
|
+
}
|
|
1177
|
+
async getGatewayCapabilities(gateway) {
|
|
1178
|
+
if (!gateway?.capabilities)
|
|
1179
|
+
return void 0;
|
|
1180
|
+
try {
|
|
1181
|
+
return typeof gateway.capabilities === "function" ? await gateway.capabilities() : gateway.capabilities;
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
console.warn("[Transport] window.multicast capabilities failed:", error);
|
|
1184
|
+
return void 0;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async gatewaySupportsBytes(gateway) {
|
|
1188
|
+
if (typeof gateway?.subscribe !== "function")
|
|
1189
|
+
return false;
|
|
1190
|
+
if (!gateway.capabilities)
|
|
1191
|
+
return true;
|
|
1192
|
+
const capabilities = await this.getGatewayCapabilities(gateway);
|
|
1193
|
+
if (!capabilities)
|
|
1194
|
+
return false;
|
|
1195
|
+
const consumeModes = capabilities.consumeModes;
|
|
1196
|
+
return !consumeModes || consumeModes.includes("bytes");
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Detect available transport capabilities
|
|
1200
|
+
*/
|
|
1201
|
+
async detectCapabilities() {
|
|
1202
|
+
if (this.capabilities)
|
|
1203
|
+
return this.capabilities;
|
|
1204
|
+
const gateway = this.getWindowMulticastGateway();
|
|
1205
|
+
const hasGatewayBytes = await this.gatewaySupportsBytes(gateway);
|
|
1206
|
+
const win = this.getWindow();
|
|
1207
|
+
this.capabilities = {
|
|
1208
|
+
nativeSSM: hasGatewayBytes,
|
|
1209
|
+
nativeIGMP: hasGatewayBytes,
|
|
1210
|
+
directSockets: typeof win?.UDPSocket !== "undefined",
|
|
1211
|
+
amt: false,
|
|
1212
|
+
driad: hasGatewayBytes,
|
|
1213
|
+
// the gateway runs RFC 8777 discovery
|
|
1214
|
+
fecDecode: false
|
|
1215
|
+
};
|
|
1216
|
+
console.log("[Transport] Capabilities:", this.capabilities);
|
|
1217
|
+
return this.capabilities;
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Connect using best available transport
|
|
1221
|
+
*
|
|
1222
|
+
* Promotion hierarchy:
|
|
1223
|
+
* 1. window.multicast gateway bytes
|
|
1224
|
+
* 2. AMT: a configured relay via the AMT proxy, else gateway DRIAD discovery
|
|
1225
|
+
* 3. MOQ WebTransport (final fallback)
|
|
1226
|
+
*
|
|
1227
|
+
* The ABR ladder (`abrLadder`) and initial rendition (`initialQuality`)
|
|
1228
|
+
* are checked first, before anything is torn down. A config that fails is
|
|
1229
|
+
* refused with the same shape as an invalid endpoint, the error to the
|
|
1230
|
+
* error callback and `false` resolved; unlike one, it is caught before the
|
|
1231
|
+
* live subscription closes, so a refused reconnect leaves that subscription,
|
|
1232
|
+
* its state and any connect already in flight untouched.
|
|
1233
|
+
*/
|
|
1234
|
+
async connect(config) {
|
|
1235
|
+
let abr;
|
|
1236
|
+
try {
|
|
1237
|
+
abr = new AdaptiveBitrateController({
|
|
1238
|
+
ladder: config.abrLadder,
|
|
1239
|
+
initialQuality: config.initialQuality?.name
|
|
1240
|
+
});
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
const refused = error instanceof Error ? error : new Error(String(error));
|
|
1243
|
+
if (this.onError)
|
|
1244
|
+
this.onError(refused);
|
|
1245
|
+
else
|
|
1246
|
+
console.error("[Transport] ABR config refused, not connecting:", refused);
|
|
1247
|
+
return false;
|
|
1248
|
+
}
|
|
1249
|
+
const generation = ++this.connectionGeneration;
|
|
1250
|
+
this.subscription?.close();
|
|
1251
|
+
this.subscription = null;
|
|
1252
|
+
this.abr = null;
|
|
1253
|
+
this.state = {
|
|
1254
|
+
mode: "disconnected",
|
|
1255
|
+
connected: false,
|
|
1256
|
+
packetsReceived: 0,
|
|
1257
|
+
bytesReceived: 0,
|
|
1258
|
+
lastPacketTime: 0
|
|
1259
|
+
};
|
|
1260
|
+
this.onStateChange?.(this.state);
|
|
1261
|
+
const endpoint = normalizeMulticastEndpoint({
|
|
1262
|
+
groupAddress: config.group,
|
|
1263
|
+
sourceAddress: config.source,
|
|
1264
|
+
port: config.port,
|
|
1265
|
+
mode: config.mode,
|
|
1266
|
+
amtRelay: config.amtRelay,
|
|
1267
|
+
amtRelayPort: config.amtRelayPort
|
|
1268
|
+
});
|
|
1269
|
+
if (!endpoint) {
|
|
1270
|
+
const error = new Error("Invalid multicast endpoint configuration");
|
|
1271
|
+
this.onError?.(error);
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
config = {
|
|
1275
|
+
...config,
|
|
1276
|
+
group: endpoint.groupAddress,
|
|
1277
|
+
source: endpoint.sourceAddress ?? "",
|
|
1278
|
+
mode: endpoint.mode,
|
|
1279
|
+
amtRelay: endpoint.amtRelay,
|
|
1280
|
+
amtRelayPort: endpoint.amtRelayPort
|
|
1281
|
+
};
|
|
1282
|
+
const caps = await this.detectCapabilities();
|
|
1283
|
+
if (generation !== this.connectionGeneration)
|
|
1284
|
+
return false;
|
|
1285
|
+
this.abr = abr;
|
|
1286
|
+
abr.setCallback((quality) => {
|
|
1287
|
+
this.state.quality = quality;
|
|
1288
|
+
this.subscription?.onqualitychange?.(quality);
|
|
1289
|
+
this.onStateChange?.(this.state);
|
|
1290
|
+
});
|
|
1291
|
+
let connected = false;
|
|
1292
|
+
if (caps.nativeSSM || caps.nativeIGMP) {
|
|
1293
|
+
console.log("[Transport] Attempting window.multicast...");
|
|
1294
|
+
connected = await this.connectWindowMulticast(config, generation);
|
|
1295
|
+
if (connected) {
|
|
1296
|
+
if (generation !== this.connectionGeneration)
|
|
1297
|
+
return false;
|
|
1298
|
+
this.state.mode = "multicast";
|
|
1299
|
+
return true;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
if (!connected) {
|
|
1303
|
+
console.log("[Transport] Attempting AMT tunnel...");
|
|
1304
|
+
connected = await this.connectAmt(config, generation);
|
|
1305
|
+
if (connected) {
|
|
1306
|
+
if (generation !== this.connectionGeneration)
|
|
1307
|
+
return false;
|
|
1308
|
+
this.state.mode = "amt";
|
|
1309
|
+
return true;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
console.log("[Transport] All multicast transports failed, falling back to MOQ");
|
|
1313
|
+
return false;
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Connect via the lowercase window.multicast bytes gateway.
|
|
1317
|
+
*/
|
|
1318
|
+
async connectWindowMulticast(config, generation, mode = "multicast") {
|
|
1319
|
+
try {
|
|
1320
|
+
const gateway = this.getWindowMulticastGateway();
|
|
1321
|
+
if (typeof gateway?.subscribe !== "function")
|
|
1322
|
+
return false;
|
|
1323
|
+
const lease = await multicastSubscriptionPool.acquire(gateway, config, {
|
|
1324
|
+
onBytes: (data) => {
|
|
1325
|
+
if (generation === this.connectionGeneration)
|
|
1326
|
+
this.handlePacket(data);
|
|
1327
|
+
},
|
|
1328
|
+
onState: (state) => {
|
|
1329
|
+
if (generation !== this.connectionGeneration)
|
|
1330
|
+
return;
|
|
1331
|
+
this.state.connected = state !== "closed";
|
|
1332
|
+
this.onStateChange?.(this.state);
|
|
1333
|
+
},
|
|
1334
|
+
onError: (error) => {
|
|
1335
|
+
if (generation !== this.connectionGeneration)
|
|
1336
|
+
return;
|
|
1337
|
+
console.error("[Transport] window.multicast error:", error);
|
|
1338
|
+
this.onError?.(error);
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
if (generation !== this.connectionGeneration) {
|
|
1342
|
+
await lease.release();
|
|
1343
|
+
return false;
|
|
1344
|
+
}
|
|
1345
|
+
this.subscription = {
|
|
1346
|
+
id: lease.key,
|
|
1347
|
+
config,
|
|
1348
|
+
close: () => {
|
|
1349
|
+
void lease.release();
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
this.state = {
|
|
1353
|
+
mode,
|
|
1354
|
+
connected: true,
|
|
1355
|
+
group: config.group,
|
|
1356
|
+
source: config.source,
|
|
1357
|
+
quality: this.requireAbr().getCurrentQuality(),
|
|
1358
|
+
packetsReceived: 0,
|
|
1359
|
+
bytesReceived: 0,
|
|
1360
|
+
lastPacketTime: 0
|
|
1361
|
+
};
|
|
1362
|
+
console.log(`[Transport] Connected via window.multicast: (${config.source}, ${config.group})`);
|
|
1363
|
+
this.onStateChange?.(this.state);
|
|
1364
|
+
return true;
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
console.warn("[Transport] window.multicast failed:", error);
|
|
1367
|
+
return false;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Connect via AMT: a configured relay goes through the AMT proxy; with no
|
|
1372
|
+
* relay, the window.multicast gateway discovers one per RFC 8777 (DRIAD).
|
|
1373
|
+
*/
|
|
1374
|
+
async connectAmt(config, generation) {
|
|
1375
|
+
try {
|
|
1376
|
+
if (config.amtRelay)
|
|
1377
|
+
return await this.connectAmtProxy(config, config.amtRelay, generation);
|
|
1378
|
+
if (config.driadEnabled === false || !config.source || config.mode === "native") {
|
|
1379
|
+
console.warn("[Transport] No AMT relay available (DRIAD requires source address for SSM)");
|
|
1380
|
+
return false;
|
|
1381
|
+
}
|
|
1382
|
+
console.log(`[Transport] Asking the gateway for a DRIAD relay for source ${config.source}...`);
|
|
1383
|
+
return await this.connectWindowMulticast({ ...config, amtDiscovery: "driad" }, generation, "amt");
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
console.warn("[Transport] AMT connection failed:", error);
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Connect via WebSocket-based AMT proxy (browser fallback)
|
|
1391
|
+
*/
|
|
1392
|
+
async connectAmtProxy(config, amtRelay, generation) {
|
|
1393
|
+
try {
|
|
1394
|
+
const relayHost = amtRelay.includes(":") && !amtRelay.startsWith("[") ? `[${amtRelay}]` : amtRelay;
|
|
1395
|
+
const wsUrl = new URL(`wss://${relayHost}/amt-proxy`);
|
|
1396
|
+
wsUrl.searchParams.set("group", config.group);
|
|
1397
|
+
wsUrl.searchParams.set("source", config.source || "");
|
|
1398
|
+
wsUrl.searchParams.set("port", String(config.port));
|
|
1399
|
+
const ws = new WebSocket(wsUrl);
|
|
1400
|
+
return new Promise((resolve, reject) => {
|
|
1401
|
+
ws.binaryType = "arraybuffer";
|
|
1402
|
+
const timeout = setTimeout(() => {
|
|
1403
|
+
ws.close();
|
|
1404
|
+
reject(new Error("AMT proxy connection timeout"));
|
|
1405
|
+
}, 1e4);
|
|
1406
|
+
ws.onopen = () => {
|
|
1407
|
+
clearTimeout(timeout);
|
|
1408
|
+
if (generation !== this.connectionGeneration) {
|
|
1409
|
+
ws.close();
|
|
1410
|
+
resolve(false);
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
console.log(`[Transport] Connected to AMT proxy: ${amtRelay}`);
|
|
1414
|
+
this.subscription = {
|
|
1415
|
+
id: `amt-proxy-${Date.now()}`,
|
|
1416
|
+
config,
|
|
1417
|
+
close: () => ws.close()
|
|
1418
|
+
};
|
|
1419
|
+
this.state = {
|
|
1420
|
+
mode: "amt",
|
|
1421
|
+
connected: true,
|
|
1422
|
+
group: config.group,
|
|
1423
|
+
source: config.source,
|
|
1424
|
+
amtRelay,
|
|
1425
|
+
quality: this.requireAbr().getCurrentQuality(),
|
|
1426
|
+
packetsReceived: 0,
|
|
1427
|
+
bytesReceived: 0,
|
|
1428
|
+
lastPacketTime: 0
|
|
1429
|
+
};
|
|
1430
|
+
this.onStateChange?.(this.state);
|
|
1431
|
+
resolve(true);
|
|
1432
|
+
};
|
|
1433
|
+
ws.onmessage = (event) => {
|
|
1434
|
+
if (generation === this.connectionGeneration && event.data instanceof ArrayBuffer) {
|
|
1435
|
+
this.handlePacket(event.data);
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
ws.onerror = (error) => {
|
|
1439
|
+
clearTimeout(timeout);
|
|
1440
|
+
if (generation !== this.connectionGeneration) {
|
|
1441
|
+
resolve(false);
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
console.error("[Transport] AMT proxy error:", error);
|
|
1445
|
+
reject(error);
|
|
1446
|
+
};
|
|
1447
|
+
ws.onclose = () => {
|
|
1448
|
+
clearTimeout(timeout);
|
|
1449
|
+
if (generation !== this.connectionGeneration) {
|
|
1450
|
+
resolve(false);
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
this.state.connected = false;
|
|
1454
|
+
this.onStateChange?.(this.state);
|
|
1455
|
+
resolve(false);
|
|
1456
|
+
};
|
|
1457
|
+
});
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
console.warn("[Transport] AMT proxy failed:", error);
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Handle received packet
|
|
1465
|
+
*/
|
|
1466
|
+
handlePacket(data) {
|
|
1467
|
+
this.state.packetsReceived++;
|
|
1468
|
+
this.state.bytesReceived += data.byteLength;
|
|
1469
|
+
this.state.lastPacketTime = Date.now();
|
|
1470
|
+
this.abr?.recordSample(data.byteLength, 0);
|
|
1471
|
+
this.onPacket?.(data);
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Get current transport state
|
|
1475
|
+
*/
|
|
1476
|
+
getState() {
|
|
1477
|
+
return { ...this.state };
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* The rung of the live connection's `abrLadder` ABR is on; `null` when
|
|
1481
|
+
* there is no ladder (before `connect(config)` succeeds, after
|
|
1482
|
+
* `disconnect()`, or after a reconnect that was torn down and bailed).
|
|
1483
|
+
*/
|
|
1484
|
+
getQuality() {
|
|
1485
|
+
return this.abr?.getCurrentQuality() ?? null;
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Force the rung named `name` of the live connection's `abrLadder`.
|
|
1489
|
+
* Throws `Error` when there is no ladder (see `getQuality()`), and
|
|
1490
|
+
* `RangeError` when the ladder has no rung of that name; it never falls
|
|
1491
|
+
* back to another rung.
|
|
1492
|
+
*/
|
|
1493
|
+
setQuality(name) {
|
|
1494
|
+
this.requireAbr().setQuality(name);
|
|
1495
|
+
}
|
|
1496
|
+
/** The controller `connect()` built; before it there is no ladder to adapt over. */
|
|
1497
|
+
requireAbr() {
|
|
1498
|
+
if (!this.abr)
|
|
1499
|
+
throw new Error("[Transport] ABR has no ladder before connect(config)");
|
|
1500
|
+
return this.abr;
|
|
1501
|
+
}
|
|
1502
|
+
/** Configures multicast-only ABR while leaving the caller's MoQ ABR untouched. */
|
|
1503
|
+
configureMulticastTiers(options, onCommand) {
|
|
1504
|
+
this.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());
|
|
1505
|
+
this.multicastOrchestrator = new MulticastOrchestrator(options);
|
|
1506
|
+
this.onOrchestratorCommand = onCommand;
|
|
1507
|
+
return this.multicastOrchestrator.start();
|
|
1508
|
+
}
|
|
1509
|
+
recordMulticastGoodput(bitrate) {
|
|
1510
|
+
return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onGoodput({ bitrate }));
|
|
1511
|
+
}
|
|
1512
|
+
recordMulticastHeadroom(healthy) {
|
|
1513
|
+
return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onHeadroom({ healthy }));
|
|
1514
|
+
}
|
|
1515
|
+
recordMulticastTierPacket(packet) {
|
|
1516
|
+
return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onTierPacket(packet));
|
|
1517
|
+
}
|
|
1518
|
+
pollMulticastTierSwitch() {
|
|
1519
|
+
return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.poll());
|
|
1520
|
+
}
|
|
1521
|
+
/** Abandon the in-flight tier join, e.g. because the host could not join the target. */
|
|
1522
|
+
cancelMulticastTierJoin() {
|
|
1523
|
+
return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Set packet callback
|
|
1527
|
+
*/
|
|
1528
|
+
setPacketCallback(callback) {
|
|
1529
|
+
this.onPacket = callback;
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Set error callback
|
|
1533
|
+
*/
|
|
1534
|
+
setErrorCallback(callback) {
|
|
1535
|
+
this.onError = callback;
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Set state change callback
|
|
1539
|
+
*/
|
|
1540
|
+
setStateCallback(callback) {
|
|
1541
|
+
this.onStateChange = callback;
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Disconnect
|
|
1545
|
+
*/
|
|
1546
|
+
disconnect() {
|
|
1547
|
+
this.connectionGeneration++;
|
|
1548
|
+
this.subscription?.close();
|
|
1549
|
+
this.subscription = null;
|
|
1550
|
+
this.abr = null;
|
|
1551
|
+
this.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());
|
|
1552
|
+
this.multicastOrchestrator = null;
|
|
1553
|
+
this.onOrchestratorCommand = void 0;
|
|
1554
|
+
this.state = {
|
|
1555
|
+
mode: "disconnected",
|
|
1556
|
+
connected: false,
|
|
1557
|
+
packetsReceived: 0,
|
|
1558
|
+
bytesReceived: 0,
|
|
1559
|
+
lastPacketTime: 0
|
|
1560
|
+
};
|
|
1561
|
+
this.onStateChange?.(this.state);
|
|
1562
|
+
}
|
|
1563
|
+
dispatchOrchestratorCommand(command) {
|
|
1564
|
+
if (command)
|
|
1565
|
+
this.onOrchestratorCommand?.(command);
|
|
1566
|
+
return command;
|
|
1567
|
+
}
|
|
1568
|
+
};
|
|
1569
|
+
var transportManager = new TransportManager();
|
|
1570
|
+
|
|
1571
|
+
// node_modules/@blockcast/mmt-transport/dist/multicast-receiver-gateway.js
|
|
1572
|
+
var USER_AGENT_VERIFIED = /* @__PURE__ */ Symbol.for("@blockcast/mmt-transport/user-agent-verified/v1");
|
|
1573
|
+
|
|
1574
|
+
// node_modules/@blockcast/mmt-transport/dist/moqt-priority.js
|
|
1575
|
+
var MAX_MOQT_PRIORITY = 255;
|
|
1576
|
+
function moqtPriority(value, field) {
|
|
1577
|
+
if (!Number.isInteger(value) || value < 0 || value > MAX_MOQT_PRIORITY) {
|
|
1578
|
+
throw new RangeError(`${field} must be an integer in [0, ${MAX_MOQT_PRIORITY}], got ${value}`);
|
|
1579
|
+
}
|
|
1580
|
+
return value;
|
|
1581
|
+
}
|
|
1582
|
+
function moqtSubscriberPriority(value) {
|
|
1583
|
+
return moqtPriority(value, "MOQT subscriber priority");
|
|
1584
|
+
}
|
|
1585
|
+
function moqtPublisherPriority(value) {
|
|
1586
|
+
return moqtPriority(value, "MOQT publisher priority");
|
|
1587
|
+
}
|
|
1588
|
+
var MOQT_SUBSCRIBER_PRIORITY = Object.freeze({
|
|
1589
|
+
catalog: moqtSubscriberPriority(0),
|
|
1590
|
+
/**
|
|
1591
|
+
* @deprecated A repair FETCH's SUBSCRIBER_PRIORITY is derived per request
|
|
1592
|
+
* from the catalog source and repair priorities (draft-ramadan-moq-fec
|
|
1593
|
+
* §11.4.5), not a player constant. Use the `subscriberPriority` that
|
|
1594
|
+
* `@blockcast/mmt-container`'s `wireRepairFetch` puts on each
|
|
1595
|
+
* `RepairFetchRequest` (`repairFetchSubscriberPriority`).
|
|
1596
|
+
*/
|
|
1597
|
+
deadlineRepair: moqtSubscriberPriority(0),
|
|
1598
|
+
audio: moqtSubscriberPriority(1),
|
|
1599
|
+
video: moqtSubscriberPriority(2),
|
|
1600
|
+
/**
|
|
1601
|
+
* @deprecated See `deadlineRepair`: use the request's catalog-derived
|
|
1602
|
+
* `RepairFetchRequest.subscriberPriority` (draft-ramadan-moq-fec §11.4.5).
|
|
1603
|
+
*/
|
|
1604
|
+
backgroundRepair: moqtSubscriberPriority(3)
|
|
1605
|
+
});
|
|
1606
|
+
var MOQT_PUBLISHER_PRIORITY = Object.freeze({
|
|
1607
|
+
catalog: moqtPublisherPriority(32),
|
|
1608
|
+
source: moqtPublisherPriority(128),
|
|
1609
|
+
repair: moqtPublisherPriority(240)
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
// node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
|
|
1613
|
+
var MAX_PENDING_PACKETS = 256;
|
|
1614
|
+
var AMTGatewayManager = class {
|
|
1615
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
1616
|
+
messageHandler;
|
|
1617
|
+
messageListenerRegistered = false;
|
|
1618
|
+
pendingPackets = /* @__PURE__ */ new Map();
|
|
1619
|
+
constructor() {
|
|
1620
|
+
this.messageHandler = (event) => {
|
|
1621
|
+
const data = event.data;
|
|
1622
|
+
if (data.type === "UDP_PACKET") {
|
|
1623
|
+
this.handlePacket(data);
|
|
1624
|
+
} else if (data.type === "SOCKET_ERROR") {
|
|
1625
|
+
this.handleSocketError(data);
|
|
1626
|
+
} else if (data.type === "DEBUG_LOG") {
|
|
1627
|
+
console.log(`[AMT SW Debug] ${data.message}`);
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
ensureMessageListener() {
|
|
1632
|
+
if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
navigator.serviceWorker.addEventListener("message", this.messageHandler);
|
|
1636
|
+
this.messageListenerRegistered = true;
|
|
1637
|
+
console.log("[AMT] Registered service worker message handler");
|
|
1638
|
+
}
|
|
1639
|
+
removeMessageListener() {
|
|
1640
|
+
if (!this.messageListenerRegistered)
|
|
1641
|
+
return;
|
|
1642
|
+
if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
1643
|
+
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
1644
|
+
}
|
|
1645
|
+
this.messageListenerRegistered = false;
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Subscribe via AMT tunnel
|
|
1649
|
+
*/
|
|
1650
|
+
async subscribe(config) {
|
|
1651
|
+
const { key } = config;
|
|
1652
|
+
if (config.protected) {
|
|
1653
|
+
throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
|
|
1654
|
+
}
|
|
1655
|
+
this.ensureMessageListener();
|
|
1656
|
+
const keyStr = this.getKeyString(key);
|
|
1657
|
+
if (this.subscriptions.has(keyStr)) {
|
|
1658
|
+
console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
|
|
1659
|
+
return keyStr;
|
|
1660
|
+
}
|
|
1661
|
+
try {
|
|
1662
|
+
const relayAddress = config.relayAddress;
|
|
1663
|
+
const relayPort = config.relayPort || DEFAULT_AMT_RELAY_PORT;
|
|
1664
|
+
if (!relayAddress) {
|
|
1665
|
+
throw new Error(`AMT relay address required: this manager does no relay discovery. For RFC 8777 DRIAD discovery, join through window.multicast with amtDiscovery: "driad".`);
|
|
1666
|
+
}
|
|
1667
|
+
console.log(`[AMT] Using configured relay ${relayAddress}:${relayPort}`);
|
|
1668
|
+
const subscriptionId = keyStr;
|
|
1669
|
+
this.pendingPackets.set(subscriptionId, []);
|
|
1670
|
+
const subscription = {
|
|
1671
|
+
key,
|
|
1672
|
+
subscriptionId,
|
|
1673
|
+
config,
|
|
1674
|
+
handshakeComplete: false,
|
|
1675
|
+
relay: {
|
|
1676
|
+
address: relayAddress,
|
|
1677
|
+
port: relayPort,
|
|
1678
|
+
discoveryMethod: "manual",
|
|
1679
|
+
discoveredAt: Date.now()
|
|
1680
|
+
}
|
|
1681
|
+
};
|
|
1682
|
+
this.subscriptions.set(keyStr, subscription);
|
|
1683
|
+
console.log(`[AMT] Subscription created for ${keyStr} via ${relayAddress}:${relayPort}`);
|
|
1684
|
+
return keyStr;
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
const subscription = this.subscriptions.get(keyStr);
|
|
1687
|
+
if (subscription) {
|
|
1688
|
+
this.subscriptions.delete(keyStr);
|
|
1689
|
+
this.pendingPackets.delete(subscription.subscriptionId);
|
|
1690
|
+
}
|
|
1691
|
+
if (this.subscriptions.size === 0)
|
|
1692
|
+
this.removeMessageListener();
|
|
1693
|
+
throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Unsubscribe from AMT tunnel
|
|
1698
|
+
*/
|
|
1699
|
+
async unsubscribe(handle) {
|
|
1700
|
+
const subscription = this.subscriptions.get(handle);
|
|
1701
|
+
if (!subscription) {
|
|
1702
|
+
throw new Error(`Subscription ${handle} not found`);
|
|
1703
|
+
}
|
|
1704
|
+
try {
|
|
1705
|
+
if (subscription.keepAliveTimer) {
|
|
1706
|
+
clearInterval(subscription.keepAliveTimer);
|
|
1707
|
+
}
|
|
1708
|
+
console.log(`[AMT] Unsubscribed from ${handle}`);
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
console.error("[AMT] Error during unsubscribe:", error);
|
|
1711
|
+
} finally {
|
|
1712
|
+
this.subscriptions.delete(handle);
|
|
1713
|
+
this.pendingPackets.delete(subscription.subscriptionId);
|
|
1714
|
+
if (this.subscriptions.size === 0)
|
|
1715
|
+
this.removeMessageListener();
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Get list of active subscriptions
|
|
1720
|
+
*/
|
|
1721
|
+
getSubscriptions() {
|
|
1722
|
+
return Array.from(this.subscriptions.keys());
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Handle packet from service worker
|
|
1726
|
+
*/
|
|
1727
|
+
handlePacket(message) {
|
|
1728
|
+
const { subscriptionId, data } = message;
|
|
1729
|
+
if (!subscriptionId)
|
|
1730
|
+
return;
|
|
1731
|
+
const subscription = this.subscriptions.get(subscriptionId);
|
|
1732
|
+
if (!subscription)
|
|
1733
|
+
return;
|
|
1734
|
+
if (!subscription.handshakeComplete) {
|
|
1735
|
+
const queue = this.pendingPackets.get(subscriptionId);
|
|
1736
|
+
const packetData2 = data;
|
|
1737
|
+
if (queue && queue.length < MAX_PENDING_PACKETS) {
|
|
1738
|
+
queue.push({
|
|
1739
|
+
data: packetData2.data,
|
|
1740
|
+
remoteAddress: packetData2.remoteAddress,
|
|
1741
|
+
remotePort: packetData2.remotePort
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
const packetData = data;
|
|
1747
|
+
const metadata = {
|
|
1748
|
+
sourceAddress: subscription.relay?.address || "unknown",
|
|
1749
|
+
sourcePort: subscription.relay?.port || 0,
|
|
1750
|
+
transport: "tunnel",
|
|
1751
|
+
timestamp: performance.now()
|
|
1752
|
+
};
|
|
1753
|
+
subscription.config.onPacket(packetData.data, metadata);
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Handle socket error from service worker
|
|
1757
|
+
*/
|
|
1758
|
+
handleSocketError(message) {
|
|
1759
|
+
const { subscriptionId, error } = message;
|
|
1760
|
+
if (!subscriptionId)
|
|
1761
|
+
return;
|
|
1762
|
+
const subscription = this.subscriptions.get(subscriptionId);
|
|
1763
|
+
if (!subscription)
|
|
1764
|
+
return;
|
|
1765
|
+
subscription.config.onError(new Error(error || "Unknown error"));
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Cleanup on destroy
|
|
1769
|
+
*/
|
|
1770
|
+
destroy() {
|
|
1771
|
+
this.removeMessageListener();
|
|
1772
|
+
const handles = Array.from(this.subscriptions.keys());
|
|
1773
|
+
for (const handle of handles) {
|
|
1774
|
+
this.unsubscribe(handle).catch((err) => {
|
|
1775
|
+
console.error(`[AMT] Error unsubscribing ${handle}:`, err);
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Get key string for HashMap
|
|
1781
|
+
*/
|
|
1782
|
+
getKeyString(key) {
|
|
1783
|
+
const source = key.source ? `@${key.source}` : "";
|
|
1784
|
+
return `${key.group}:${key.port}${source}`;
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
var amtGatewayManager = new AMTGatewayManager();
|
|
1788
|
+
|
|
380
1789
|
// src/mfu-reassembler.ts
|
|
381
1790
|
function fragmentProvenance(fragment) {
|
|
382
1791
|
if (fragment.deliveryProvenance !== void 0) return fragment.deliveryProvenance;
|
|
@@ -6268,9 +7677,21 @@ var IsoBmffTimingResolver = class {
|
|
|
6268
7677
|
}
|
|
6269
7678
|
#track = null;
|
|
6270
7679
|
#fragments = /* @__PURE__ */ new Map();
|
|
7680
|
+
/** First sample of each pending fragment, kept in `#delivered` with its sample count. */
|
|
7681
|
+
#firstSamples = /* @__PURE__ */ new Map();
|
|
7682
|
+
/**
|
|
7683
|
+
* First sample and sample count of the newest fragments whose every slot was
|
|
7684
|
+
* cleared. Abandonment counts: `releaseSample` clears a slot too, which
|
|
7685
|
+
* matches the combiner, since it retires an abandoned sample as well.
|
|
7686
|
+
*/
|
|
7687
|
+
#delivered = /* @__PURE__ */ new Map();
|
|
6271
7688
|
#pendingSamples = 0;
|
|
6272
7689
|
#evictedSamples = 0;
|
|
6273
7690
|
#lastMovieFragmentRejection = null;
|
|
7691
|
+
/** True once an FT=0 has bound the track that FT=1 timing is read against. */
|
|
7692
|
+
get initialized() {
|
|
7693
|
+
return this.#track !== null;
|
|
7694
|
+
}
|
|
6274
7695
|
get pendingSampleCount() {
|
|
6275
7696
|
return this.#pendingSamples;
|
|
6276
7697
|
}
|
|
@@ -6294,7 +7715,14 @@ var IsoBmffTimingResolver = class {
|
|
|
6294
7715
|
this.#track = timing;
|
|
6295
7716
|
return true;
|
|
6296
7717
|
}
|
|
6297
|
-
|
|
7718
|
+
/**
|
|
7719
|
+
* `recovered` marks an FEC-recovered copy. A recovered block hands back
|
|
7720
|
+
* every FT=1 it holds, the received ones included, so a recovered copy of a
|
|
7721
|
+
* fragment whose samples are all done is a duplicate: admitting it again
|
|
7722
|
+
* would charge stale timing and could displace the live MPU's. A source
|
|
7723
|
+
* copy is still admitted again (a rendition switch re-arms timing that way).
|
|
7724
|
+
*/
|
|
7725
|
+
addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
|
|
6298
7726
|
this.#lastMovieFragmentRejection = null;
|
|
6299
7727
|
if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
|
|
6300
7728
|
this.#lastMovieFragmentRejection = "invalid";
|
|
@@ -6320,6 +7748,8 @@ var IsoBmffTimingResolver = class {
|
|
|
6320
7748
|
(sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
|
|
6321
7749
|
);
|
|
6322
7750
|
}
|
|
7751
|
+
const delivered = recovered ? this.#delivered.get(key) : void 0;
|
|
7752
|
+
if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
|
|
6323
7753
|
let freed = 0;
|
|
6324
7754
|
while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
|
|
6325
7755
|
freed += this.#evictOldestFragment();
|
|
@@ -6333,6 +7763,7 @@ var IsoBmffTimingResolver = class {
|
|
|
6333
7763
|
return false;
|
|
6334
7764
|
}
|
|
6335
7765
|
this.#fragments.set(key, [...parsed.samples]);
|
|
7766
|
+
this.#firstSamples.set(key, parsed.samples[0]);
|
|
6336
7767
|
this.#pendingSamples += parsed.samples.length;
|
|
6337
7768
|
return true;
|
|
6338
7769
|
}
|
|
@@ -6390,6 +7821,7 @@ var IsoBmffTimingResolver = class {
|
|
|
6390
7821
|
let live = 0;
|
|
6391
7822
|
for (const sample of samples) if (sample !== null) live++;
|
|
6392
7823
|
this.#fragments.delete(oldest);
|
|
7824
|
+
this.#firstSamples.delete(oldest);
|
|
6393
7825
|
this.#pendingSamples -= live;
|
|
6394
7826
|
this.#evictedSamples += live;
|
|
6395
7827
|
return live;
|
|
@@ -6405,12 +7837,23 @@ var IsoBmffTimingResolver = class {
|
|
|
6405
7837
|
if (!samples || !samples[index]) return false;
|
|
6406
7838
|
samples[index] = null;
|
|
6407
7839
|
this.#pendingSamples--;
|
|
6408
|
-
if (samples.every((value) => value === null))
|
|
7840
|
+
if (samples.every((value) => value === null)) {
|
|
7841
|
+
this.#fragments.delete(key);
|
|
7842
|
+
const first = this.#firstSamples.get(key);
|
|
7843
|
+
this.#firstSamples.delete(key);
|
|
7844
|
+
this.#delivered.delete(key);
|
|
7845
|
+
this.#delivered.set(key, { first, sampleCount: samples.length });
|
|
7846
|
+
if (this.#delivered.size > this.maxPendingSamples) {
|
|
7847
|
+
this.#delivered.delete(this.#delivered.keys().next().value);
|
|
7848
|
+
}
|
|
7849
|
+
}
|
|
6409
7850
|
return true;
|
|
6410
7851
|
}
|
|
6411
7852
|
reset() {
|
|
6412
7853
|
this.#track = null;
|
|
6413
7854
|
this.#fragments.clear();
|
|
7855
|
+
this.#firstSamples.clear();
|
|
7856
|
+
this.#delivered.clear();
|
|
6414
7857
|
this.#pendingSamples = 0;
|
|
6415
7858
|
}
|
|
6416
7859
|
};
|
|
@@ -6580,6 +8023,9 @@ function authoredGap(before, after) {
|
|
|
6580
8023
|
function ssIdAfter(a, b) {
|
|
6581
8024
|
return (a - b | 0) > 0;
|
|
6582
8025
|
}
|
|
8026
|
+
function holdsRebuilt(stream) {
|
|
8027
|
+
return stream.buf.some(({ meta }) => meta.fecSbnFirst !== void 0);
|
|
8028
|
+
}
|
|
6583
8029
|
var FecReleaseSequencer = class {
|
|
6584
8030
|
#streams = /* @__PURE__ */ new Map();
|
|
6585
8031
|
#gate;
|
|
@@ -6642,6 +8088,7 @@ var FecReleaseSequencer = class {
|
|
|
6642
8088
|
stream.lastSbn = void 0;
|
|
6643
8089
|
stream.discontinuity = false;
|
|
6644
8090
|
stream.rapSsId = void 0;
|
|
8091
|
+
stream.sourced = false;
|
|
6645
8092
|
}
|
|
6646
8093
|
if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
|
|
6647
8094
|
this.#recordStat("droppedStale");
|
|
@@ -6668,6 +8115,7 @@ var FecReleaseSequencer = class {
|
|
|
6668
8115
|
if (compareFrameOrder(stream.buf[mid], frame) < 0) lo = mid + 1;
|
|
6669
8116
|
else hi = mid;
|
|
6670
8117
|
}
|
|
8118
|
+
if (stream.buf[lo]?.passed === true) frame.passed = true;
|
|
6671
8119
|
stream.buf.splice(lo, 0, frame);
|
|
6672
8120
|
this.#drainUnlessDeferred(stream);
|
|
6673
8121
|
}
|
|
@@ -6703,6 +8151,41 @@ var FecReleaseSequencer = class {
|
|
|
6703
8151
|
}
|
|
6704
8152
|
return false;
|
|
6705
8153
|
}
|
|
8154
|
+
/**
|
|
8155
|
+
* These streams' sink is being replaced by one that has seen no source
|
|
8156
|
+
* frame yet. First, from a stream that holds a frame the overlay rebuilt,
|
|
8157
|
+
* release to the sink being replaced the frames the unanchored path had
|
|
8158
|
+
* already released to it (#cursorPassed): there they waited in its queue,
|
|
8159
|
+
* which its replacement flushes. A stream that holds none is not drained:
|
|
8160
|
+
* it holds nothing the anchor kept back, and a drain would still force out
|
|
8161
|
+
* a frame held past the hold limit, which before the anchor waited for the
|
|
8162
|
+
* next push. Then treat the streams as unsourced until a source frame is
|
|
8163
|
+
* released.
|
|
8164
|
+
*/
|
|
8165
|
+
unsource(packetId, catalogTrackName) {
|
|
8166
|
+
for (const stream of this.#streams.values()) {
|
|
8167
|
+
if (stream.packetId === packetId && (catalogTrackName === void 0 || stream.catalogTrackName === catalogTrackName)) {
|
|
8168
|
+
if (stream.draining === 0 && holdsRebuilt(stream)) {
|
|
8169
|
+
if (this.#deferDepth > 0) this.#deferredStreams.add(stream);
|
|
8170
|
+
else this.#drain(stream, true);
|
|
8171
|
+
}
|
|
8172
|
+
stream.sourced = false;
|
|
8173
|
+
}
|
|
8174
|
+
}
|
|
8175
|
+
}
|
|
8176
|
+
/**
|
|
8177
|
+
* Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
|
|
8178
|
+
* (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
|
|
8179
|
+
* packetId.
|
|
8180
|
+
*/
|
|
8181
|
+
holdsRebuilt(packetId, catalogTrackName) {
|
|
8182
|
+
for (const stream of this.#streams.values()) {
|
|
8183
|
+
if (stream.packetId !== packetId) continue;
|
|
8184
|
+
if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
|
|
8185
|
+
if (holdsRebuilt(stream)) return true;
|
|
8186
|
+
}
|
|
8187
|
+
return false;
|
|
8188
|
+
}
|
|
6706
8189
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
6707
8190
|
poke(streamKey) {
|
|
6708
8191
|
if (streamKey !== void 0) {
|
|
@@ -6794,16 +8277,32 @@ var FecReleaseSequencer = class {
|
|
|
6794
8277
|
last: null,
|
|
6795
8278
|
lastSbn: void 0,
|
|
6796
8279
|
discontinuity: false,
|
|
6797
|
-
rapSsId: void 0
|
|
8280
|
+
rapSsId: void 0,
|
|
8281
|
+
sourced: false,
|
|
8282
|
+
draining: 0
|
|
6798
8283
|
};
|
|
6799
8284
|
this.#streams.set(streamKey, stream);
|
|
6800
8285
|
}
|
|
6801
8286
|
return stream;
|
|
6802
8287
|
}
|
|
6803
|
-
#
|
|
8288
|
+
/** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
|
|
8289
|
+
#drain(stream, asUnanchored = false) {
|
|
8290
|
+
stream.draining++;
|
|
8291
|
+
try {
|
|
8292
|
+
this.#drainLoop(stream, asUnanchored);
|
|
8293
|
+
} finally {
|
|
8294
|
+
stream.draining--;
|
|
8295
|
+
}
|
|
8296
|
+
}
|
|
8297
|
+
#drainLoop(stream, asUnanchored) {
|
|
6804
8298
|
while (stream.buf.length > 0 && stream.last !== null) {
|
|
6805
8299
|
const head = stream.buf[0];
|
|
6806
8300
|
const last = stream.last;
|
|
8301
|
+
if (compareFrameOrder(head, last) <= 0) {
|
|
8302
|
+
stream.buf.shift();
|
|
8303
|
+
this.#release(stream, head, void 0);
|
|
8304
|
+
continue;
|
|
8305
|
+
}
|
|
6807
8306
|
if (stream.discontinuity) {
|
|
6808
8307
|
stream.discontinuity = false;
|
|
6809
8308
|
stream.buf.shift();
|
|
@@ -6817,9 +8316,10 @@ var FecReleaseSequencer = class {
|
|
|
6817
8316
|
}
|
|
6818
8317
|
const headSbn = head.meta.fecSbn;
|
|
6819
8318
|
const from = stream.lastSbn ?? headSbn;
|
|
6820
|
-
const to = headSbn ?? stream.lastSbn;
|
|
8319
|
+
const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn;
|
|
6821
8320
|
let lost = [];
|
|
6822
8321
|
const unanchored = from === void 0 && to === void 0;
|
|
8322
|
+
let unresolved = false;
|
|
6823
8323
|
if (from !== void 0 && to !== void 0) {
|
|
6824
8324
|
const resolved = this.#gate.resolvedLoss(
|
|
6825
8325
|
stream.packetId,
|
|
@@ -6828,7 +8328,8 @@ var FecReleaseSequencer = class {
|
|
|
6828
8328
|
Math.max(from, to),
|
|
6829
8329
|
from
|
|
6830
8330
|
);
|
|
6831
|
-
|
|
8331
|
+
unresolved = resolved === null;
|
|
8332
|
+
if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
|
|
6832
8333
|
const heldMs = performance.now() - head.arrivedAtMs;
|
|
6833
8334
|
if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return;
|
|
6834
8335
|
this.#recordStat("forcedWindowFlushes");
|
|
@@ -6836,22 +8337,56 @@ var FecReleaseSequencer = class {
|
|
|
6836
8337
|
this.#release(stream, head, this.#gate.decide(last, head, [], true));
|
|
6837
8338
|
continue;
|
|
6838
8339
|
}
|
|
6839
|
-
lost = resolved;
|
|
8340
|
+
lost = resolved ?? [];
|
|
6840
8341
|
}
|
|
6841
8342
|
stream.buf.shift();
|
|
6842
8343
|
this.#release(
|
|
6843
8344
|
stream,
|
|
6844
8345
|
head,
|
|
6845
|
-
lost.length > 0 || unanchored || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
8346
|
+
lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta) ? this.#gate.decide(last, head, lost, false) : void 0
|
|
6846
8347
|
);
|
|
6847
8348
|
}
|
|
6848
8349
|
}
|
|
8350
|
+
/**
|
|
8351
|
+
* An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
|
|
8352
|
+
* source cursor holds it unanchored, and neither does a later frame behind
|
|
8353
|
+
* it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
|
|
8354
|
+
* fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
|
|
8355
|
+
* like each frame that arrived in front of it since (stale there). The head
|
|
8356
|
+
* then goes, decided as it would be unanchored, once a frame the base path
|
|
8357
|
+
* delivered (a source frame, or one base FEC recovered) is buffered after a
|
|
8358
|
+
* marked one: the base path has passed that one, so a frame still missing
|
|
8359
|
+
* before it can only come from a repair. It also goes while no source frame
|
|
8360
|
+
* has been released, when the direct path shows a recovered frame at once.
|
|
8361
|
+
* And it goes at once while a staged switch decides on when frames arrive
|
|
8362
|
+
* (FecReleaseGate.releaseUnanchored), and when unsource releases into a
|
|
8363
|
+
* sink being replaced: unanchored, it had already reached the sink.
|
|
8364
|
+
* The scan also writes: the `passed` marks it sets stay on the frames,
|
|
8365
|
+
* and push marks a frame it inserts in front of a marked one from them.
|
|
8366
|
+
*/
|
|
8367
|
+
#cursorPassed(stream, asUnanchored) {
|
|
8368
|
+
let at = -1;
|
|
8369
|
+
for (let i = 0; i < stream.buf.length; i++) {
|
|
8370
|
+
const frame = stream.buf[i];
|
|
8371
|
+
if (frame.meta.fecSbnFirst !== void 0 || frame.meta.fecSbn === void 0) {
|
|
8372
|
+
frame.passed = true;
|
|
8373
|
+
if (at < 0) at = i;
|
|
8374
|
+
} else if (frame.passed !== true) break;
|
|
8375
|
+
}
|
|
8376
|
+
if (at < 0) return false;
|
|
8377
|
+
return asUnanchored || !stream.sourced || this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true || stream.buf.some(({ meta }, i) => i > at && meta.fecSbnFirst === void 0 && (meta.recovered !== true || meta.fecSbn !== void 0));
|
|
8378
|
+
}
|
|
6849
8379
|
#release(stream, frame, decision) {
|
|
8380
|
+
const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0;
|
|
8381
|
+
if (stale) this.#recordStat("droppedStale");
|
|
6850
8382
|
if (decision) frame.meta.gapBefore = decision;
|
|
6851
8383
|
stream.last = frame;
|
|
6852
|
-
if (frame.meta.fecSbn !== void 0
|
|
8384
|
+
if (frame.meta.fecSbn !== void 0 && (frame.meta.fecSbnFirst === void 0 || stream.lastSbn !== void 0)) {
|
|
8385
|
+
stream.lastSbn = frame.meta.fecSbn;
|
|
8386
|
+
}
|
|
8387
|
+
if (frame.meta.recovered !== true) stream.sourced = true;
|
|
6853
8388
|
this.#gate.onReleased?.(frame.data, frame.meta);
|
|
6854
|
-
this.#emit(frame.data, frame.meta);
|
|
8389
|
+
if (!stale) this.#emit(frame.data, frame.meta);
|
|
6855
8390
|
}
|
|
6856
8391
|
};
|
|
6857
8392
|
var FMPU_BOX_TYPES = /* @__PURE__ */ new Set(["ftyp", "moov", "moof"]);
|
|
@@ -6892,6 +8427,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
6892
8427
|
this.#reclaimedTimingSamples = 0;
|
|
6893
8428
|
this.#evictedTimingSamples = 0;
|
|
6894
8429
|
this.#rejectedMovieFragments = 0;
|
|
8430
|
+
this.#recoveredHeadsAwaitingTiming = {
|
|
8431
|
+
held: 0,
|
|
8432
|
+
expired: 0,
|
|
8433
|
+
discarded: 0,
|
|
8434
|
+
superseded: 0
|
|
8435
|
+
};
|
|
6895
8436
|
this.#sequencers = /* @__PURE__ */ new Map();
|
|
6896
8437
|
this.#fecGate = null;
|
|
6897
8438
|
this.#fecSequencer = null;
|
|
@@ -7000,6 +8541,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7000
8541
|
#reclaimedTimingSamples;
|
|
7001
8542
|
#evictedTimingSamples;
|
|
7002
8543
|
#rejectedMovieFragments;
|
|
8544
|
+
#recoveredHeadsAwaitingTiming;
|
|
7003
8545
|
#sequencers;
|
|
7004
8546
|
#fecGate;
|
|
7005
8547
|
#fecSequencer;
|
|
@@ -7105,11 +8647,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7105
8647
|
/**
|
|
7106
8648
|
* Typed view of the ISO-BMFF timing-sample budget.
|
|
7107
8649
|
*
|
|
7108
|
-
*
|
|
7109
|
-
*
|
|
7110
|
-
*
|
|
7111
|
-
*
|
|
7112
|
-
*
|
|
8650
|
+
* Each stream admits timing against its own window of `capacity` slots
|
|
8651
|
+
* (BLO-52085). A stream at capacity displaces its own oldest timing to
|
|
8652
|
+
* admit the moof in hand, and `evicted` counts the slots so displaced:
|
|
8653
|
+
* timing a stream declared and never delivered shows up there. `pending`
|
|
8654
|
+
* sums every stream, so it can exceed `capacity`. Per repo policy this is
|
|
8655
|
+
* a producer-owned counter -- never infer budget state by parsing logs.
|
|
7113
8656
|
*/
|
|
7114
8657
|
get timingBudgetStats() {
|
|
7115
8658
|
return {
|
|
@@ -7120,6 +8663,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7120
8663
|
rejectedMovieFragments: this.#rejectedMovieFragments
|
|
7121
8664
|
};
|
|
7122
8665
|
}
|
|
8666
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
8667
|
+
get recoveredHeadsAwaitingTiming() {
|
|
8668
|
+
let pending = 0;
|
|
8669
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
8670
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++;
|
|
8671
|
+
}
|
|
8672
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending };
|
|
8673
|
+
}
|
|
7123
8674
|
get reorderStats() {
|
|
7124
8675
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
|
|
7125
8676
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
|
|
@@ -7161,6 +8712,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7161
8712
|
fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
|
|
7162
8713
|
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
|
|
7163
8714
|
}
|
|
8715
|
+
/** See FecReleaseSequencer.unsource. */
|
|
8716
|
+
unsourceFecRelease(packetId, catalogTrackName) {
|
|
8717
|
+
this.#fecSequencer?.unsource(packetId, catalogTrackName);
|
|
8718
|
+
}
|
|
8719
|
+
/** See FecReleaseSequencer.holdsRebuilt. */
|
|
8720
|
+
fecReleaseHoldsRebuilt(packetId, catalogTrackName) {
|
|
8721
|
+
return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false;
|
|
8722
|
+
}
|
|
7164
8723
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
7165
8724
|
pokeFecRelease(packetId, catalogTrackName) {
|
|
7166
8725
|
if (!this.#fecSequencer) return;
|
|
@@ -7293,7 +8852,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7293
8852
|
const now = Date.now();
|
|
7294
8853
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
7295
8854
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
|
|
7296
|
-
awaitingTiming.shift();
|
|
8855
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
7297
8856
|
}
|
|
7298
8857
|
}
|
|
7299
8858
|
}
|
|
@@ -7411,11 +8970,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7411
8970
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
7412
8971
|
return this.#drop("fragment-unexpected-fragmentation");
|
|
7413
8972
|
const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
|
|
7414
|
-
if (!resolver2) return this.#drop("fragment-no-resolver");
|
|
8973
|
+
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
7415
8974
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
7416
8975
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
7417
|
-
const remaining = this.#maxPendingTimingSamples -
|
|
7418
|
-
const admitted = resolver2.addMovieFragment(
|
|
8976
|
+
const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
|
|
8977
|
+
const admitted = resolver2.addMovieFragment(
|
|
8978
|
+
mpu.mpuSequenceNumber,
|
|
8979
|
+
data,
|
|
8980
|
+
remaining,
|
|
8981
|
+
options?.recovered === true
|
|
8982
|
+
);
|
|
7419
8983
|
this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
|
|
7420
8984
|
this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
|
|
7421
8985
|
if (!admitted) {
|
|
@@ -7435,7 +8999,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7435
8999
|
};
|
|
7436
9000
|
}
|
|
7437
9001
|
if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
|
|
7438
|
-
const resolver = this.#timingResolver(streamKey, info.packetId,
|
|
9002
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true);
|
|
7439
9003
|
if (!resolver) return this.#drop("mfu-no-resolver");
|
|
7440
9004
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
7441
9005
|
return this.#drop("mfu-empty-payload");
|
|
@@ -7456,6 +9020,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7456
9020
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
7457
9021
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
7458
9022
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
9023
|
+
sequenceNumber: info.sequenceNumber,
|
|
7459
9024
|
arrivedAt: Date.now()
|
|
7460
9025
|
});
|
|
7461
9026
|
return this.#drop("mfu-sample-timing-missing");
|
|
@@ -7481,7 +9046,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7481
9046
|
info,
|
|
7482
9047
|
resolver,
|
|
7483
9048
|
options?.recovered === true,
|
|
7484
|
-
options?.fecSbn
|
|
9049
|
+
options?.fecSbn,
|
|
9050
|
+
options?.fecSbnFirst
|
|
7485
9051
|
);
|
|
7486
9052
|
if (deferred.record === void 0) this.#countDeferredOutcome(deferred.reason);
|
|
7487
9053
|
}
|
|
@@ -7574,7 +9140,8 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7574
9140
|
info,
|
|
7575
9141
|
resolver,
|
|
7576
9142
|
options?.recovered === true,
|
|
7577
|
-
options?.fecSbn
|
|
9143
|
+
options?.fecSbn,
|
|
9144
|
+
options?.fecSbnFirst
|
|
7578
9145
|
);
|
|
7579
9146
|
if (outcome.record !== void 0) return outcome.record;
|
|
7580
9147
|
return this.#drop(outcome.reason);
|
|
@@ -7594,7 +9161,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7594
9161
|
* attribution exists to keep readable. Leaving the choice at the call site
|
|
7595
9162
|
* is what keeps both true.
|
|
7596
9163
|
*/
|
|
7597
|
-
#emitCombinedSample(combined, info, resolver, recovered, fecSbn) {
|
|
9164
|
+
#emitCombinedSample(combined, info, resolver, recovered, fecSbn, fecSbnFirst) {
|
|
7598
9165
|
const timing = resolver.sampleTiming(
|
|
7599
9166
|
combined.mpuSequenceNumber,
|
|
7600
9167
|
combined.movieFragmentSequenceNumber,
|
|
@@ -7611,6 +9178,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7611
9178
|
);
|
|
7612
9179
|
if (!meta) return { reason: "combined-meta-build-failed" };
|
|
7613
9180
|
if (fecSbn !== void 0) meta.fecSbn = fecSbn;
|
|
9181
|
+
if (fecSbnFirst !== void 0) meta.fecSbnFirst = fecSbnFirst;
|
|
7614
9182
|
if (!resolver.consumeSample(
|
|
7615
9183
|
combined.mpuSequenceNumber,
|
|
7616
9184
|
combined.movieFragmentSequenceNumber,
|
|
@@ -7779,6 +9347,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7779
9347
|
}
|
|
7780
9348
|
this.#emitFrame(data, meta);
|
|
7781
9349
|
}
|
|
9350
|
+
/** Whether FT=1 metadata already names this sample, without claiming it. */
|
|
9351
|
+
hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
|
|
9352
|
+
return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
|
|
9353
|
+
}
|
|
7782
9354
|
/**
|
|
7783
9355
|
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
7784
9356
|
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
@@ -7840,6 +9412,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7840
9412
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
|
|
7841
9413
|
this.#sequencers.clear();
|
|
7842
9414
|
this.#fecSequencer?.discard();
|
|
9415
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
9416
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
9417
|
+
}
|
|
7843
9418
|
this.#timingStreams.clear();
|
|
7844
9419
|
this.#pendingTimingSamples = 0;
|
|
7845
9420
|
this.#reliableMoqStartupGate.clear();
|
|
@@ -7858,10 +9433,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7858
9433
|
return resolver;
|
|
7859
9434
|
}
|
|
7860
9435
|
#awaitTiming(streamKey, head) {
|
|
7861
|
-
const
|
|
7862
|
-
if (!
|
|
9436
|
+
const state = this.#timingStreams.get(streamKey);
|
|
9437
|
+
if (!state) return;
|
|
9438
|
+
if (!head.options?.recovered) {
|
|
9439
|
+
const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
|
|
9440
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
|
|
9441
|
+
state.awaitingTiming = kept;
|
|
9442
|
+
}
|
|
9443
|
+
const awaitingTiming = state.awaitingTiming;
|
|
7863
9444
|
awaitingTiming.push(head);
|
|
7864
|
-
if (
|
|
9445
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
|
|
9446
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
9447
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
9448
|
+
}
|
|
9449
|
+
}
|
|
9450
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
9451
|
+
#leaveAwaitingTiming(head, outcome) {
|
|
9452
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
|
|
7865
9453
|
}
|
|
7866
9454
|
/**
|
|
7867
9455
|
* Feed again the heads whose timing the FT=1 just admitted declares. That
|
|
@@ -7889,6 +9477,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
7889
9477
|
const state = this.#timingStreams.get(streamKey);
|
|
7890
9478
|
if (!state) return;
|
|
7891
9479
|
this.#timingStreams.delete(streamKey);
|
|
9480
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
7892
9481
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
|
|
7893
9482
|
if (this.#pendingTimingSamples < 0) {
|
|
7894
9483
|
throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
|
|
@@ -8183,7 +9772,7 @@ function decideGap(input) {
|
|
|
8183
9772
|
|
|
8184
9773
|
// src/duplicate-ledger.ts
|
|
8185
9774
|
var COUNT_BOUNDED_GENERATION = 2e3;
|
|
8186
|
-
function
|
|
9775
|
+
function serial32Precedes3(a, b) {
|
|
8187
9776
|
const distance = b - a >>> 0;
|
|
8188
9777
|
return distance !== 0 && distance < 2147483648;
|
|
8189
9778
|
}
|
|
@@ -8359,12 +9948,12 @@ var StreamDuplicateLedgers = class {
|
|
|
8359
9948
|
predatesHorizon(packetSequenceNumber, ssId) {
|
|
8360
9949
|
if (this.#retentionMs === null) return false;
|
|
8361
9950
|
const seqFloor = this.sourceSeqs.horizonFloor;
|
|
8362
|
-
if (seqFloor !== void 0 &&
|
|
9951
|
+
if (seqFloor !== void 0 && serial32Precedes3(packetSequenceNumber, seqFloor) && !this.sourceSeqs.has(packetSequenceNumber)) {
|
|
8363
9952
|
return true;
|
|
8364
9953
|
}
|
|
8365
9954
|
if (ssId === void 0) return false;
|
|
8366
9955
|
const ssIdFloor = this.parsedSymbols.horizonFloor;
|
|
8367
|
-
return ssIdFloor !== void 0 &&
|
|
9956
|
+
return ssIdFloor !== void 0 && serial32Precedes3(ssId, ssIdFloor) && !this.parsedSymbols.has(ssId);
|
|
8368
9957
|
}
|
|
8369
9958
|
/** Whether the stream keeps ledgers, for its FEC horizon or by count. */
|
|
8370
9959
|
get retaining() {
|
|
@@ -8429,7 +10018,18 @@ function keyframeOverlaySsId(header) {
|
|
|
8429
10018
|
if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
|
|
8430
10019
|
return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
|
|
8431
10020
|
}
|
|
10021
|
+
function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
|
|
10022
|
+
if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
|
|
10023
|
+
return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
|
|
10024
|
+
}
|
|
10025
|
+
function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
|
|
10026
|
+
if (offset === void 0 || offset !== unstampedOffset) return void 0;
|
|
10027
|
+
return headerPsn + offset >>> 0;
|
|
10028
|
+
}
|
|
10029
|
+
var OVERLAY_ROUTED_LIMIT = 1024;
|
|
10030
|
+
var OVERLAY_PENDING_LIMIT = 256;
|
|
8432
10031
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
10032
|
+
var OVERLAY_REBUILT_LIMIT = 256;
|
|
8433
10033
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
8434
10034
|
function assertStreamIdentity(role, stream) {
|
|
8435
10035
|
if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
|
|
@@ -8540,6 +10140,7 @@ var MmtpRouter = class {
|
|
|
8540
10140
|
#provenance = /* @__PURE__ */ new Map();
|
|
8541
10141
|
#onFlowProtected = null;
|
|
8542
10142
|
#unadmittedListedDrops = 0;
|
|
10143
|
+
#admittedSourceDatagrams = 0;
|
|
8543
10144
|
#unlistedRepairSetAside = 0;
|
|
8544
10145
|
#assistBlocks = /* @__PURE__ */ new Map();
|
|
8545
10146
|
#assistFailures = /* @__PURE__ */ new Map();
|
|
@@ -8596,6 +10197,26 @@ var MmtpRouter = class {
|
|
|
8596
10197
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
8597
10198
|
*/
|
|
8598
10199
|
#ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
10200
|
+
/**
|
|
10201
|
+
* Per stream, the SS_ID offset at which a base recovery last showed the
|
|
10202
|
+
* sender's own packet_sequence_number: a recovered packet's header psn
|
|
10203
|
+
* equal to its SS_ID minus that offset. While it equals the stream's
|
|
10204
|
+
* current offset, no hop re-stamps the stream, and a packet's protected psn
|
|
10205
|
+
* names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
|
|
10206
|
+
* an MPU no member of which arrived).
|
|
10207
|
+
*/
|
|
10208
|
+
#unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
10209
|
+
/**
|
|
10210
|
+
* The same, shown by the overlay alone, before any base recovery did: a
|
|
10211
|
+
* packet it rebuilt whose base copy carries its protected psn
|
|
10212
|
+
* (#feedOverlaySource), or whose protected psn passes overlayBaseSsId's
|
|
10213
|
+
* test at that offset (#routeOverlayRecovered). A base SS_ID placed by it
|
|
10214
|
+
* anchors the release gate only (BLO-52085 D3): fed to the base, it would
|
|
10215
|
+
* make the base drop a late base copy of a keyframe the overlay never
|
|
10216
|
+
* finished, and lift a block's late-source cut, which nothing before
|
|
10217
|
+
* this offset did.
|
|
10218
|
+
*/
|
|
10219
|
+
#overlayUnstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
8599
10220
|
/**
|
|
8600
10221
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
8601
10222
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -8665,7 +10286,9 @@ var MmtpRouter = class {
|
|
|
8665
10286
|
recovered: 0,
|
|
8666
10287
|
restarts: 0,
|
|
8667
10288
|
unsequencedDropped: 0,
|
|
8668
|
-
protectedDropped: 0
|
|
10289
|
+
protectedDropped: 0,
|
|
10290
|
+
baseFed: 0,
|
|
10291
|
+
baseUndetermined: 0
|
|
8669
10292
|
};
|
|
8670
10293
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
8671
10294
|
#onDeltaBlockCompleteCb = null;
|
|
@@ -8838,7 +10461,7 @@ var MmtpRouter = class {
|
|
|
8838
10461
|
const cb = this.#getFrameCallback(packetId);
|
|
8839
10462
|
if (isVideo && !assembler) {
|
|
8840
10463
|
if (frameMeta.recovered) {
|
|
8841
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
10464
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
8842
10465
|
} else {
|
|
8843
10466
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
8844
10467
|
}
|
|
@@ -8871,13 +10494,18 @@ var MmtpRouter = class {
|
|
|
8871
10494
|
this.#onDeltaBlockCompleteCb?.(packetId, false);
|
|
8872
10495
|
}
|
|
8873
10496
|
};
|
|
8874
|
-
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass) => {
|
|
10497
|
+
this.#fecManager.onBlockFailed = (sbn, packetId, catalogTrackName, blockClass, span) => {
|
|
8875
10498
|
this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
|
|
8876
10499
|
if (this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) && this.#packetIdRoles.get(packetId) === "delta") {
|
|
8877
10500
|
this.#onDeltaBlockCompleteCb?.(packetId, true);
|
|
8878
10501
|
}
|
|
8879
|
-
|
|
8880
|
-
|
|
10502
|
+
if (span === void 0) {
|
|
10503
|
+
onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
10504
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
10505
|
+
} else {
|
|
10506
|
+
onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
10507
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass, span);
|
|
10508
|
+
}
|
|
8881
10509
|
};
|
|
8882
10510
|
}
|
|
8883
10511
|
}
|
|
@@ -8922,8 +10550,9 @@ var MmtpRouter = class {
|
|
|
8922
10550
|
}
|
|
8923
10551
|
/**
|
|
8924
10552
|
* FEC-recovered source symbols whose reconstructed header named a
|
|
8925
|
-
* packet_sequence_number other than the wire sequence their SS_ID maps to
|
|
8926
|
-
*
|
|
10553
|
+
* packet_sequence_number other than the wire sequence their SS_ID maps to,
|
|
10554
|
+
* or, rebuilt by the overlay, than their base copy carries: a hop
|
|
10555
|
+
* re-stamped the sequence after the sender protected the packet.
|
|
8927
10556
|
*/
|
|
8928
10557
|
get restampedRecoveredSequenceCount() {
|
|
8929
10558
|
return this.#restampedRecoveredSeqs;
|
|
@@ -9102,6 +10731,7 @@ var MmtpRouter = class {
|
|
|
9102
10731
|
enableMseAssembler(packetId, config, catalogTrackName) {
|
|
9103
10732
|
const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
|
|
9104
10733
|
const previous = this.#mseAssemblers.get(assemblerKey);
|
|
10734
|
+
this.#parser.unsourceFecRelease(packetId, catalogTrackName);
|
|
9105
10735
|
if (previous) {
|
|
9106
10736
|
previous.flush();
|
|
9107
10737
|
previous.dispose();
|
|
@@ -9228,6 +10858,10 @@ var MmtpRouter = class {
|
|
|
9228
10858
|
get keyframeOverlayStats() {
|
|
9229
10859
|
return { ...this.#overlayStats };
|
|
9230
10860
|
}
|
|
10861
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
10862
|
+
get recoveredHeadsAwaitingTiming() {
|
|
10863
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
10864
|
+
}
|
|
9231
10865
|
#isOverlayRepair(repairPacketId, repairTrackName) {
|
|
9232
10866
|
return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
|
|
9233
10867
|
}
|
|
@@ -9248,7 +10882,8 @@ var MmtpRouter = class {
|
|
|
9248
10882
|
fec,
|
|
9249
10883
|
k,
|
|
9250
10884
|
offsets: /* @__PURE__ */ new Map(),
|
|
9251
|
-
moqBlocks: /* @__PURE__ */ new Set()
|
|
10885
|
+
moqBlocks: /* @__PURE__ */ new Set(),
|
|
10886
|
+
rebuilt: /* @__PURE__ */ new Map()
|
|
9252
10887
|
};
|
|
9253
10888
|
this.#overlays.set(repairPacketId, inst);
|
|
9254
10889
|
return () => {
|
|
@@ -9278,12 +10913,18 @@ var MmtpRouter = class {
|
|
|
9278
10913
|
*/
|
|
9279
10914
|
resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
|
|
9280
10915
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
10916
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
10917
|
+
if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
|
|
10918
|
+
stream.overlayToBase.routed.clear();
|
|
10919
|
+
stream.overlayToBase.pending.clear();
|
|
10920
|
+
}
|
|
9281
10921
|
}
|
|
9282
10922
|
#resetOverlay(inst) {
|
|
9283
10923
|
inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
|
|
9284
10924
|
inst.last = void 0;
|
|
9285
10925
|
inst.offsets.clear();
|
|
9286
10926
|
inst.moqBlocks.clear();
|
|
10927
|
+
inst.rebuilt.clear();
|
|
9287
10928
|
}
|
|
9288
10929
|
#overlaysForSource(sourcePacketId, catalogTrackName) {
|
|
9289
10930
|
return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
|
|
@@ -9334,18 +10975,33 @@ var MmtpRouter = class {
|
|
|
9334
10975
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
9335
10976
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
9336
10977
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
9337
|
-
* included (7.1, 7.4.3).
|
|
10978
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
10979
|
+
* FEC reads it.
|
|
10980
|
+
*
|
|
10981
|
+
* The base copy of a packet an overlay rebuilt with no base SS_ID shows
|
|
10982
|
+
* what a base recovery of it would (#routeProcessedFecSymbols, BLO-52085):
|
|
10983
|
+
* its wire packet_sequence_number equal to the protected one rebuilt shows
|
|
10984
|
+
* the stream unstamped at the SS_ID offset it states, for the release gate
|
|
10985
|
+
* (#overlayUnstampedSsIdSeqOffsetByStream); any other shows a hop that
|
|
10986
|
+
* re-stamps it, which is counted (restampedRecoveredSequenceCount).
|
|
9338
10987
|
*/
|
|
9339
|
-
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
|
|
10988
|
+
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
|
|
9340
10989
|
if (this.#overlays.size === 0) return;
|
|
9341
10990
|
const ssId = keyframeOverlaySsId(header);
|
|
9342
10991
|
if (ssId === void 0) return;
|
|
10992
|
+
let rebuiltPsn;
|
|
9343
10993
|
for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
|
|
9344
10994
|
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
10995
|
+
rebuiltPsn ??= inst.rebuilt.get(ssId);
|
|
10996
|
+
inst.rebuilt.delete(ssId);
|
|
9345
10997
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
9346
10998
|
if (identity !== null) {
|
|
9347
10999
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
9348
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
11000
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
11001
|
+
psnOffset: ssId - sequenceNumber >>> 0,
|
|
11002
|
+
overlaySsId: ssId,
|
|
11003
|
+
...baseSsId === void 0 ? {} : { baseSsId }
|
|
11004
|
+
});
|
|
9349
11005
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
|
|
9350
11006
|
}
|
|
9351
11007
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -9367,6 +11023,14 @@ var MmtpRouter = class {
|
|
|
9367
11023
|
const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
|
|
9368
11024
|
if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
|
|
9369
11025
|
}
|
|
11026
|
+
if (rebuiltPsn === void 0 || this.#classifyPacketId(packetId) === null) return;
|
|
11027
|
+
const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
|
|
11028
|
+
if (rebuiltPsn !== sequenceNumber) {
|
|
11029
|
+
this.#restampedRecoveredSeqs++;
|
|
11030
|
+
} else if (baseSsId !== void 0) {
|
|
11031
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
11032
|
+
if (offset === baseSsId - sequenceNumber >>> 0) this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
11033
|
+
}
|
|
9370
11034
|
}
|
|
9371
11035
|
#noteOverlayMoqBlock(inst, blockStart) {
|
|
9372
11036
|
inst.moqBlocks.add(blockStart);
|
|
@@ -9400,11 +11064,16 @@ var MmtpRouter = class {
|
|
|
9400
11064
|
}
|
|
9401
11065
|
/**
|
|
9402
11066
|
* Route an overlay-recovered block's packets as source media of the
|
|
9403
|
-
* overlay's source (7.2: an overlay coordinate yields no base SS_ID).
|
|
9404
|
-
*
|
|
9405
|
-
*
|
|
9406
|
-
*
|
|
9407
|
-
*
|
|
11067
|
+
* overlay's source (7.2: an overlay coordinate yields no base SS_ID). A
|
|
11068
|
+
* packet whose base SS_ID is determined independently (overlayBaseSsId,
|
|
11069
|
+
* unstampedBaseSsId) enters the release gate at that SS_ID, as a packet
|
|
11070
|
+
* base FEC recovered does, so it waits behind base frames still in flight,
|
|
11071
|
+
* though not for its own MPU's packets (FrameMeta.fecSbnFirst) and no
|
|
11072
|
+
* longer than unanchored; one whose base SS_ID is undetermined has no
|
|
11073
|
+
* release-gate anchor. No parse ledger, no base re-stamp, no block
|
|
11074
|
+
* advance. Each packet's wire sequence is restored from its keyframe's own
|
|
11075
|
+
* offset; without one, a stream the base never re-stamped keeps the
|
|
11076
|
+
* header's sequence and any other drops the packet.
|
|
9408
11077
|
*/
|
|
9409
11078
|
#routeOverlayRecovered(inst, blockStart, recovered) {
|
|
9410
11079
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -9424,22 +11093,139 @@ var MmtpRouter = class {
|
|
|
9424
11093
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
9425
11094
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
9426
11095
|
const restored = /* @__PURE__ */ new Set();
|
|
11096
|
+
const baseSsIds = /* @__PURE__ */ new Map();
|
|
11097
|
+
const gateOnly = /* @__PURE__ */ new Set();
|
|
9427
11098
|
const symbols = result.symbols.flatMap((sym) => {
|
|
9428
|
-
const
|
|
9429
|
-
|
|
9430
|
-
|
|
11099
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
11100
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
11101
|
+
if (anchor !== void 0) {
|
|
11102
|
+
const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
|
|
9431
11103
|
restored.add(packetSequenceNumber);
|
|
9432
|
-
|
|
11104
|
+
const out = { ...sym, packetSequenceNumber };
|
|
11105
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
11106
|
+
if (baseSsId !== void 0) {
|
|
11107
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
11108
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
11109
|
+
if (!this.#sourceSeqReceived(sourcePacketId, packetSequenceNumber, sourceName) && offset === baseSsId - packetSequenceNumber >>> 0) {
|
|
11110
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.set(streamKey, offset);
|
|
11111
|
+
}
|
|
11112
|
+
}
|
|
11113
|
+
return [out];
|
|
11114
|
+
}
|
|
11115
|
+
if (!restamped) {
|
|
11116
|
+
const offset = this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
11117
|
+
const fedSsId = unstampedBaseSsId(offset, this.#unstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
11118
|
+
const baseSsId = fedSsId ?? unstampedBaseSsId(offset, this.#overlayUnstampedSsIdSeqOffsetByStream.get(streamKey), sym.packetSequenceNumber);
|
|
11119
|
+
if (baseSsId !== void 0) {
|
|
11120
|
+
baseSsIds.set(sym.index, baseSsId);
|
|
11121
|
+
if (fedSsId === void 0) gateOnly.add(sym.index);
|
|
11122
|
+
} else {
|
|
11123
|
+
inst.rebuilt.set(overlaySsId, sym.packetSequenceNumber);
|
|
11124
|
+
if (inst.rebuilt.size > OVERLAY_REBUILT_LIMIT) inst.rebuilt.delete(inst.rebuilt.keys().next().value);
|
|
11125
|
+
}
|
|
11126
|
+
return [sym];
|
|
9433
11127
|
}
|
|
9434
|
-
if (!restamped) return [sym];
|
|
9435
11128
|
this.#overlayStats.unsequencedDropped++;
|
|
9436
11129
|
return [];
|
|
9437
11130
|
});
|
|
9438
11131
|
this.#overlayStats.recovered++;
|
|
11132
|
+
const mpuFirst = /* @__PURE__ */ new Map();
|
|
11133
|
+
for (const sym of symbols) {
|
|
11134
|
+
const ssId = baseSsIds.get(sym.index);
|
|
11135
|
+
const mpu = sym.fragment.mpuSequenceNumber;
|
|
11136
|
+
const first = mpuFirst.get(mpu);
|
|
11137
|
+
if (ssId !== void 0 && (first === void 0 || serial32Precedes(ssId, first))) mpuFirst.set(mpu, ssId);
|
|
11138
|
+
}
|
|
11139
|
+
const firstSsIds = /* @__PURE__ */ new Map();
|
|
11140
|
+
for (const sym of symbols) {
|
|
11141
|
+
if (baseSsIds.has(sym.index)) firstSsIds.set(sym.index, mpuFirst.get(sym.fragment.mpuSequenceNumber));
|
|
11142
|
+
}
|
|
9439
11143
|
this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
|
|
9440
|
-
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
11144
|
+
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast",
|
|
11145
|
+
baseSsIds,
|
|
11146
|
+
firstSsIds
|
|
9441
11147
|
});
|
|
9442
11148
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
11149
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, gateOnly.size === 0 ? baseSsIds : new Map([...baseSsIds].filter(([index]) => !gateOnly.has(index))));
|
|
11150
|
+
}
|
|
11151
|
+
/**
|
|
11152
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
11153
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
11154
|
+
* from a received member of its MPU, else unstampedBaseSsId at an offset a
|
|
11155
|
+
* base recovery showed; `baseSsIds` holds no other),
|
|
11156
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
11157
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
11158
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
11159
|
+
* A base block no repair has stated yet takes it when one does
|
|
11160
|
+
* (#feedPendingRebuilt).
|
|
11161
|
+
*
|
|
11162
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
11163
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
11164
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
11165
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
11166
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
11167
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
11168
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
11169
|
+
* block's own arrivals cannot show.
|
|
11170
|
+
*/
|
|
11171
|
+
#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
|
|
11172
|
+
const fec = this.#fecManager;
|
|
11173
|
+
const owner = inst.leg.sourcePacketId;
|
|
11174
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
11175
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
11176
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
11177
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
11178
|
+
const feeds = [];
|
|
11179
|
+
for (const sym of symbols) {
|
|
11180
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
11181
|
+
const ssId = baseSsIds.get(sym.index);
|
|
11182
|
+
if (ssId === void 0) {
|
|
11183
|
+
this.#overlayStats.baseUndetermined++;
|
|
11184
|
+
continue;
|
|
11185
|
+
}
|
|
11186
|
+
routed.delete(ssId);
|
|
11187
|
+
routed.add(ssId);
|
|
11188
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
|
|
11189
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
11190
|
+
}
|
|
11191
|
+
for (const [ssId, symbolData] of feeds) {
|
|
11192
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
11193
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
11194
|
+
}
|
|
11195
|
+
}
|
|
11196
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
11197
|
+
#feedRebuiltToBase(owner, name, ssId, symbolData) {
|
|
11198
|
+
const fec = this.#fecManager;
|
|
11199
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
11200
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
11201
|
+
if (coords == null) {
|
|
11202
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
11203
|
+
pending.delete(ssId);
|
|
11204
|
+
pending.set(ssId, symbolData.slice());
|
|
11205
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
|
|
11206
|
+
return;
|
|
11207
|
+
}
|
|
11208
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
11209
|
+
if (!fed.taken) return;
|
|
11210
|
+
this.#overlayStats.baseFed++;
|
|
11211
|
+
if (fed.recovered === null) return;
|
|
11212
|
+
const sbn = coords.sbn;
|
|
11213
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
11214
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
11215
|
+
fec.countRecovery(owner, sbn, name);
|
|
11216
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
11217
|
+
});
|
|
11218
|
+
}
|
|
11219
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
11220
|
+
#feedPendingRebuilt(owner, name, block) {
|
|
11221
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
11222
|
+
if (pending === void 0 || pending.size === 0) return;
|
|
11223
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
11224
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
11225
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
11226
|
+
pending.delete(ssId);
|
|
11227
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
11228
|
+
}
|
|
9443
11229
|
}
|
|
9444
11230
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
9445
11231
|
* the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
|
|
@@ -9571,13 +11357,18 @@ var MmtpRouter = class {
|
|
|
9571
11357
|
if (!entry) return;
|
|
9572
11358
|
entry.receiver.release();
|
|
9573
11359
|
entry.protected = false;
|
|
11360
|
+
entry.userAgent = false;
|
|
9574
11361
|
this.#assistBlocks.clear();
|
|
9575
11362
|
this.#assistFailures.clear();
|
|
9576
11363
|
}
|
|
9577
|
-
/**
|
|
11364
|
+
/** Packets of a track a protected flow lists, dropped unread: a MoQ copy (duplicate, overflow, or listed by more than one flow) or a multicast source datagram that names no flow, of a track a user-agent-admitted flow lists. */
|
|
9578
11365
|
get unadmittedListedDropCount() {
|
|
9579
11366
|
return this.#unadmittedListedDrops;
|
|
9580
11367
|
}
|
|
11368
|
+
/** Source datagrams (multicast or MoQ) a protected flow's bc-provenance v2 receiver released: admitted at once or held, then admitted by a record. Repair symbols and user-agent-admitted datagrams are not counted. Counted at release, before this router's own track checks: a released datagram those drop, which only a track change between hold and release can cause, still counts, so this is not a count of routed media. */
|
|
11369
|
+
get admittedSourceDatagramCount() {
|
|
11370
|
+
return this.#admittedSourceDatagrams;
|
|
11371
|
+
}
|
|
9581
11372
|
/** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
|
|
9582
11373
|
get unlistedRepairSetAsideCount() {
|
|
9583
11374
|
return this.#unlistedRepairSetAside;
|
|
@@ -9615,6 +11406,13 @@ var MmtpRouter = class {
|
|
|
9615
11406
|
}
|
|
9616
11407
|
return false;
|
|
9617
11408
|
}
|
|
11409
|
+
/** Whether a protected flow the user agent admitted lists `packetId`. */
|
|
11410
|
+
#userAgentGoverns(packetId) {
|
|
11411
|
+
for (const entry of this.#provenance.values()) {
|
|
11412
|
+
if (entry.protected && entry.userAgent && ingressHasPacketId(entry.ingress, packetId)) return true;
|
|
11413
|
+
}
|
|
11414
|
+
return false;
|
|
11415
|
+
}
|
|
9618
11416
|
/**
|
|
9619
11417
|
* A MoQ repair symbol no protected flow lists, owned by a source track one
|
|
9620
11418
|
* does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
|
|
@@ -9705,7 +11503,7 @@ var MmtpRouter = class {
|
|
|
9705
11503
|
fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
|
|
9706
11504
|
fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
|
|
9707
11505
|
fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
9708
|
-
})) {
|
|
11506
|
+
}, true)) {
|
|
9709
11507
|
this.#unlistedAssistedRecoveries++;
|
|
9710
11508
|
this.#assistBlocks.delete(key);
|
|
9711
11509
|
return;
|
|
@@ -10057,6 +11855,8 @@ var MmtpRouter = class {
|
|
|
10057
11855
|
)) return;
|
|
10058
11856
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
10059
11857
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
11858
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
11859
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
10060
11860
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
10061
11861
|
const recovered = fec.feedRepairSymbol(
|
|
10062
11862
|
ownerPacketId,
|
|
@@ -10107,7 +11907,7 @@ var MmtpRouter = class {
|
|
|
10107
11907
|
catalogTrackName
|
|
10108
11908
|
)) continue;
|
|
10109
11909
|
if (fec.isTrackConfigured?.(ownerPacketId) !== false) {
|
|
10110
|
-
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence);
|
|
11910
|
+
fec.trackBlock(blockStart, ownerPacketId, catalogTrackName, esi, payload.evidence, true);
|
|
10111
11911
|
this.#requestFecPoke(ownerPacketId, catalogTrackName);
|
|
10112
11912
|
}
|
|
10113
11913
|
if (payload.oversized) continue;
|
|
@@ -10194,6 +11994,8 @@ var MmtpRouter = class {
|
|
|
10194
11994
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
10195
11995
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
10196
11996
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
11997
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
11998
|
+
this.#overlayUnstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
10197
11999
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
10198
12000
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
10199
12001
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -10320,6 +12122,12 @@ var MmtpRouter = class {
|
|
|
10320
12122
|
* the outgoing stream stops. The hold is bounded by the incoming stream's
|
|
10321
12123
|
* own RAP cadence, so no timer is involved.
|
|
10322
12124
|
*
|
|
12125
|
+
* While the switch is staged, the FEC release gate lets a keyframe the
|
|
12126
|
+
* overlay rebuilt reach it, on either side, when the keyframe did before
|
|
12127
|
+
* the gate anchored it (FecReleaseGate.releaseUnanchored): the switch
|
|
12128
|
+
* decides on what each side delivered, and the commit discards the gate's
|
|
12129
|
+
* frames of the outgoing stream.
|
|
12130
|
+
*
|
|
10323
12131
|
* The commit is one synchronous transition inside the frame path: the
|
|
10324
12132
|
* outgoing stream's already-admitted frames are released (its queued FEC
|
|
10325
12133
|
* gap fills only up to the RAP, since recovery can queue a frame ahead of
|
|
@@ -10370,6 +12178,9 @@ var MmtpRouter = class {
|
|
|
10370
12178
|
droppedIncomingFrames: 0
|
|
10371
12179
|
};
|
|
10372
12180
|
this.#staged = state;
|
|
12181
|
+
if (this.#parser.fecReleaseHoldsRebuilt(from.packetId, from.catalogTrackName)) {
|
|
12182
|
+
this.#requestFecPoke(from.packetId, from.catalogTrackName);
|
|
12183
|
+
}
|
|
10373
12184
|
return {
|
|
10374
12185
|
cancel: () => {
|
|
10375
12186
|
if (this.#staged !== state) return false;
|
|
@@ -10452,7 +12263,17 @@ var MmtpRouter = class {
|
|
|
10452
12263
|
*/
|
|
10453
12264
|
#routeParsedFrame(data, meta) {
|
|
10454
12265
|
if (this.#classifyPacketId(meta.packetId) === null) return;
|
|
10455
|
-
if (this.#staged !== null
|
|
12266
|
+
if (this.#staged !== null) {
|
|
12267
|
+
let consumed = false;
|
|
12268
|
+
this.#fecPokeDepth++;
|
|
12269
|
+
try {
|
|
12270
|
+
consumed = this.#interceptSwitchFrame(data, meta);
|
|
12271
|
+
} finally {
|
|
12272
|
+
this.#fecPokeDepth--;
|
|
12273
|
+
this.#flushPendingFecPokes();
|
|
12274
|
+
}
|
|
12275
|
+
if (consumed) return;
|
|
12276
|
+
}
|
|
10456
12277
|
this.#deliverParsedFrame(data, meta);
|
|
10457
12278
|
}
|
|
10458
12279
|
#commitStreamSwitch(state, rap, reason) {
|
|
@@ -10604,12 +12425,16 @@ var MmtpRouter = class {
|
|
|
10604
12425
|
this.#parser.deferFecRelease(work);
|
|
10605
12426
|
} finally {
|
|
10606
12427
|
this.#fecPokeDepth--;
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
12428
|
+
this.#flushPendingFecPokes();
|
|
12429
|
+
}
|
|
12430
|
+
}
|
|
12431
|
+
/** Run the pokes requested while #fecPokeDepth was raised, once it is 0. */
|
|
12432
|
+
#flushPendingFecPokes() {
|
|
12433
|
+
if (this.#fecPokeDepth === 0 && this.#pendingFecPokes.size > 0 && !this.#disposed) {
|
|
12434
|
+
const pokes = [...this.#pendingFecPokes.values()];
|
|
12435
|
+
this.#pendingFecPokes.clear();
|
|
12436
|
+
for (const poke of pokes) {
|
|
12437
|
+
this.#parser.pokeFecRelease(poke.packetId, poke.catalogTrackName);
|
|
10613
12438
|
}
|
|
10614
12439
|
}
|
|
10615
12440
|
}
|
|
@@ -10687,6 +12512,14 @@ var MmtpRouter = class {
|
|
|
10687
12512
|
onRap: (packetId, catalogTrackName, ssId) => {
|
|
10688
12513
|
for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
|
|
10689
12514
|
},
|
|
12515
|
+
// A staged switch commits on when each side's frames reach it
|
|
12516
|
+
// (#interceptSwitchFrame): both sides' overlay keyframes reach it when
|
|
12517
|
+
// they did unanchored.
|
|
12518
|
+
releaseUnanchored: (packetId, catalogTrackName) => {
|
|
12519
|
+
const staged = this.#staged;
|
|
12520
|
+
const stream = { packetId, ...catalogTrackName === void 0 ? {} : { catalogTrackName } };
|
|
12521
|
+
return staged !== null && (isStream(stream, staged.from) || isStream(stream, staged.to));
|
|
12522
|
+
},
|
|
10690
12523
|
onReleased: (data, meta) => {
|
|
10691
12524
|
if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
|
|
10692
12525
|
const timing = meta.authoredTiming;
|
|
@@ -10828,6 +12661,7 @@ var MmtpRouter = class {
|
|
|
10828
12661
|
const parsed = this.#parser.parsePacket(packet);
|
|
10829
12662
|
if (!parsed) return;
|
|
10830
12663
|
const info = parsed.info;
|
|
12664
|
+
if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
|
|
10831
12665
|
if (source === "multicast" && !manifestAdmitted) {
|
|
10832
12666
|
this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
|
|
10833
12667
|
}
|
|
@@ -10861,6 +12695,12 @@ var MmtpRouter = class {
|
|
|
10861
12695
|
debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
|
|
10862
12696
|
return;
|
|
10863
12697
|
}
|
|
12698
|
+
if (ingress.admittedBy === USER_AGENT_VERIFIED) {
|
|
12699
|
+
this.#noteFlowAdmitted(ingress.flow);
|
|
12700
|
+
protectedFlow.userAgent = true;
|
|
12701
|
+
this.#processMMTP(packet, source, true, ingress, overlapCopy);
|
|
12702
|
+
return;
|
|
12703
|
+
}
|
|
10864
12704
|
const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
|
|
10865
12705
|
if (verdict === "admitted") {
|
|
10866
12706
|
this.#noteFlowAdmitted(ingress.flow);
|
|
@@ -10878,6 +12718,11 @@ var MmtpRouter = class {
|
|
|
10878
12718
|
return;
|
|
10879
12719
|
}
|
|
10880
12720
|
}
|
|
12721
|
+
const flowless = source === "multicast" && !manifestAdmitted && ingress?.flow === void 0;
|
|
12722
|
+
if (flowless && !info.isRepair && this.#userAgentGoverns(info.packetId)) {
|
|
12723
|
+
this.#unadmittedListedDrops++;
|
|
12724
|
+
return;
|
|
12725
|
+
}
|
|
10881
12726
|
if (info.isRepair) {
|
|
10882
12727
|
const overlay = this.#overlayRepairTarget(info.packetId, catalogTrackNameForPacket(ingress, info.packetId));
|
|
10883
12728
|
if (overlay !== void 0) {
|
|
@@ -10895,7 +12740,7 @@ var MmtpRouter = class {
|
|
|
10895
12740
|
);
|
|
10896
12741
|
if (!repair) return;
|
|
10897
12742
|
if (ownerPacketId != null && this.#fecManager) {
|
|
10898
|
-
if (source === "moq" && !manifestAdmitted && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
|
|
12743
|
+
if ((flowless && this.#userAgentGoverns(ownerPacketId) || source === "moq" && !manifestAdmitted) && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
|
|
10899
12744
|
this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
|
|
10900
12745
|
const count = this.#repairPidCounts[info.packetId];
|
|
10901
12746
|
if (count <= 2 || count % 500 === 0) {
|
|
@@ -11098,7 +12943,8 @@ var MmtpRouter = class {
|
|
|
11098
12943
|
parsed.header,
|
|
11099
12944
|
info.sequenceNumber,
|
|
11100
12945
|
info.rapFlag,
|
|
11101
|
-
source
|
|
12946
|
+
source,
|
|
12947
|
+
fecPayloadId?.ssId
|
|
11102
12948
|
);
|
|
11103
12949
|
}
|
|
11104
12950
|
}
|
|
@@ -11124,7 +12970,8 @@ var MmtpRouter = class {
|
|
|
11124
12970
|
source: parserOptions?.source,
|
|
11125
12971
|
deliveryProvenance: parserOptions?.deliveryProvenance,
|
|
11126
12972
|
catalogTrackName: parserOptions?.catalogTrackName,
|
|
11127
|
-
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn }
|
|
12973
|
+
...parserOptions?.fecSbn === void 0 ? {} : { fecSbn: parserOptions.fecSbn },
|
|
12974
|
+
...parserOptions?.fecSbnFirst === void 0 ? {} : { fecSbnFirst: parserOptions.fecSbnFirst }
|
|
11128
12975
|
};
|
|
11129
12976
|
if (parsed) this.#parser.feedParsed(packet, parsed, feedOptions);
|
|
11130
12977
|
else this.#parser.feed(packet, feedOptions);
|
|
@@ -11248,7 +13095,8 @@ var MmtpRouter = class {
|
|
|
11248
13095
|
if (stream === void 0) {
|
|
11249
13096
|
stream = {
|
|
11250
13097
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
11251
|
-
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
|
|
13098
|
+
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
13099
|
+
overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
|
|
11252
13100
|
};
|
|
11253
13101
|
byName.set(catalogTrackName, stream);
|
|
11254
13102
|
}
|
|
@@ -11363,8 +13211,16 @@ var MmtpRouter = class {
|
|
|
11363
13211
|
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
11364
13212
|
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
11365
13213
|
}
|
|
11366
|
-
#routeDirectRecoveredVideo(packetId, data, meta, cb) {
|
|
13214
|
+
#routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
|
|
11367
13215
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
13216
|
+
if (this.#isFecTrack(packetId)) {
|
|
13217
|
+
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
13218
|
+
if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
|
|
13219
|
+
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
13220
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
13221
|
+
return;
|
|
13222
|
+
}
|
|
13223
|
+
}
|
|
11368
13224
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
11369
13225
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
11370
13226
|
this.#drainDirectRecoveredVideo(
|
|
@@ -11474,7 +13330,7 @@ var MmtpRouter = class {
|
|
|
11474
13330
|
}
|
|
11475
13331
|
// ── Private: FEC recovery routing ───────────────────────────────
|
|
11476
13332
|
/** Route WASM-recovered block through appropriate path based on container kind. */
|
|
11477
|
-
#routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete) {
|
|
13333
|
+
#routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
11478
13334
|
const packetSymbolSize = this.#symbolSizeForPacket(ownerPacketId);
|
|
11479
13335
|
if (ownerPacketId === void 0) {
|
|
11480
13336
|
throw new Error("[MmtpRouter] a recovered FEC block needs its owner packetId to read its fec.blockProfile");
|
|
@@ -11491,7 +13347,7 @@ var MmtpRouter = class {
|
|
|
11491
13347
|
ownerPacketId,
|
|
11492
13348
|
blockProfile
|
|
11493
13349
|
});
|
|
11494
|
-
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
|
|
13350
|
+
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted);
|
|
11495
13351
|
}
|
|
11496
13352
|
/**
|
|
11497
13353
|
* A recovered source packet of a protected flow is admitted on its own
|
|
@@ -11503,9 +13359,31 @@ var MmtpRouter = class {
|
|
|
11503
13359
|
* recovered packets is refused is unrecovered (7.2.7): its caller leaves it
|
|
11504
13360
|
* open, so it reaches its FEC deadline and fails by class. Always true
|
|
11505
13361
|
* outside a protected flow.
|
|
13362
|
+
*
|
|
13363
|
+
* On a flow the user agent admitted, the live decoder's recoveries are kept
|
|
13364
|
+
* without a record: while the flow is protected that decoder takes only
|
|
13365
|
+
* symbols the user agent or a record verified (a flow-less multicast
|
|
13366
|
+
* datagram of a listed track is dropped or set aside in #processMMTP), so
|
|
13367
|
+
* what it recovers is as verified as they are. An `assisted` (scratch-decoder) recovery mixes in
|
|
13368
|
+
* unlisted MoQ repair and still needs a record.
|
|
13369
|
+
* ponytail: per-flow flag; per-block provenance only if a protected flow
|
|
13370
|
+
* ever mixes verified and unverified live inputs.
|
|
13371
|
+
* ponytail: a worker frame computed before the protection reset can still
|
|
13372
|
+
* arrive after it (frames carry no epoch); its inputs are MoQ copies read
|
|
13373
|
+
* and played before protection, so it admits nothing new. Drop pre-reset
|
|
13374
|
+
* frames by epoch only if pre-protection input ever matters.
|
|
11506
13375
|
*/
|
|
11507
|
-
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete) {
|
|
11508
|
-
const
|
|
13376
|
+
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
13377
|
+
const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
13378
|
+
if (overlayRouted !== void 0 && overlayRouted.size > 0) {
|
|
13379
|
+
result = {
|
|
13380
|
+
...result,
|
|
13381
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
|
|
13382
|
+
};
|
|
13383
|
+
}
|
|
13384
|
+
const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
13385
|
+
const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
|
|
13386
|
+
const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
11509
13387
|
let recovered = true;
|
|
11510
13388
|
if (receiver) {
|
|
11511
13389
|
const parsed = this.#stream(ownerPacketId, catalogTrackName).duplicates.parsedSymbols;
|
|
@@ -11517,6 +13395,8 @@ var MmtpRouter = class {
|
|
|
11517
13395
|
let admitted = false;
|
|
11518
13396
|
if (receiver === "ambiguous") {
|
|
11519
13397
|
this.#ambiguousRecoveredDrops++;
|
|
13398
|
+
} else if (userAgent) {
|
|
13399
|
+
admitted = true;
|
|
11520
13400
|
} else {
|
|
11521
13401
|
const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
|
|
11522
13402
|
wire.set(sym.rawSymbol);
|
|
@@ -11544,12 +13424,12 @@ var MmtpRouter = class {
|
|
|
11544
13424
|
catalogTrackName === void 0 || !byName ? byName?.values() : byName.has(catalogTrackName) ? [byName.get(catalogTrackName)] : []
|
|
11545
13425
|
);
|
|
11546
13426
|
if (flows.size > 1) return "ambiguous";
|
|
11547
|
-
for (const bound of flows) return this.#provenance.get(bound)
|
|
13427
|
+
for (const bound of flows) return this.#provenance.get(bound) ?? "ambiguous";
|
|
11548
13428
|
let match;
|
|
11549
|
-
for (const
|
|
11550
|
-
if (ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
|
|
13429
|
+
for (const entry of this.#provenance.values()) {
|
|
13430
|
+
if (entry.ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
|
|
11551
13431
|
if (match) return "ambiguous";
|
|
11552
|
-
match =
|
|
13432
|
+
match = entry;
|
|
11553
13433
|
}
|
|
11554
13434
|
}
|
|
11555
13435
|
return match;
|
|
@@ -11557,7 +13437,12 @@ var MmtpRouter = class {
|
|
|
11557
13437
|
#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
|
|
11558
13438
|
if (ownerPacketId == null) return;
|
|
11559
13439
|
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
11560
|
-
const fecSbnOf = (index) =>
|
|
13440
|
+
const fecSbnOf = (index) => {
|
|
13441
|
+
if (!overlay) return { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
|
|
13442
|
+
const fecSbn = overlay.baseSsIds.get(index);
|
|
13443
|
+
const fecSbnFirst = overlay.firstSsIds.get(index);
|
|
13444
|
+
return fecSbn === void 0 || fecSbnFirst === void 0 ? {} : { fecSbn, fecSbnFirst };
|
|
13445
|
+
};
|
|
11561
13446
|
const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
|
|
11562
13447
|
const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
|
|
11563
13448
|
if (ssIdSeqOffset !== void 0) {
|
|
@@ -11565,7 +13450,10 @@ var MmtpRouter = class {
|
|
|
11565
13450
|
...result,
|
|
11566
13451
|
symbols: result.symbols.map((sym) => {
|
|
11567
13452
|
const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
|
|
11568
|
-
if (wire === sym.packetSequenceNumber)
|
|
13453
|
+
if (wire === sym.packetSequenceNumber) {
|
|
13454
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
13455
|
+
return sym;
|
|
13456
|
+
}
|
|
11569
13457
|
this.#restampedRecoveredSeqs++;
|
|
11570
13458
|
this.#restampedStreams.add(streamKey);
|
|
11571
13459
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -11601,6 +13489,29 @@ var MmtpRouter = class {
|
|
|
11601
13489
|
}
|
|
11602
13490
|
);
|
|
11603
13491
|
}
|
|
13492
|
+
const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
|
|
13493
|
+
ownerPacketId,
|
|
13494
|
+
sym.fragment.mpuSequenceNumber,
|
|
13495
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
13496
|
+
sym.fragment.sampleNumber,
|
|
13497
|
+
catalogTrackName
|
|
13498
|
+
);
|
|
13499
|
+
for (const sym of result.symbols) {
|
|
13500
|
+
if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
13501
|
+
this.#feedViaParser(
|
|
13502
|
+
sym.rawSymbol,
|
|
13503
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
13504
|
+
void 0,
|
|
13505
|
+
{
|
|
13506
|
+
skipFecPayloadId: true,
|
|
13507
|
+
recovered: true,
|
|
13508
|
+
deliveryProvenance,
|
|
13509
|
+
...fecSbnOf(sym.index),
|
|
13510
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
13511
|
+
}
|
|
13512
|
+
);
|
|
13513
|
+
}
|
|
13514
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
11604
13515
|
const gated = this.#isFecTrack(ownerPacketId);
|
|
11605
13516
|
const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
|
|
11606
13517
|
if (assembler) {
|