@blockcast/mmt-container 2.2.0-main.73af0eedcbc8 → 2.2.0-main.a16efba1d85a
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 +630 -191
- 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 +60 -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 +3 -1
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +26 -4
- 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 +5 -4
- 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.js.map +1 -1
- 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 +35 -2
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +97 -28
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +222 -45
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +97 -28
- 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 +6 -0
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +150 -44
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-auth-catalog.js.map +1 -1
- package/dist/multicast-input.js.map +1 -1
- package/dist/multicast-manager.js +176 -48
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +26 -16
- package/dist/multicast-transport.js.map +2 -2
- package/dist/multicast-unwrap.js +12 -19
- package/dist/multicast-unwrap.js.map +2 -2
- package/dist/no-modules.js +215 -42
- package/dist/no-modules.js.map +3 -3
- package/dist/repair-fetch.js.map +1 -1
- 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 +1 -1
- 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.js.map +1 -1
- package/dist/wasm-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +97 -28
- 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 +189 -179
- package/src/fec-manager.ts +26 -4
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +18 -2
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +92 -7
- package/src/mmtp-fec-session.ts +115 -15
- package/src/mmtp-router.ts +46 -0
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +1 -1
|
@@ -171,30 +171,26 @@ function isEmptyOrExactLegacyTrailer(payload, start, end) {
|
|
|
171
171
|
const authLength = view2.getUint16(2, false);
|
|
172
172
|
return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end;
|
|
173
173
|
}
|
|
174
|
-
function resolveDeclaredTrailers(payload, declaredEnd, fecType, options
|
|
174
|
+
function resolveDeclaredTrailers(payload, declaredEnd, fecType, options) {
|
|
175
175
|
if (declaredEnd > payload.length)
|
|
176
176
|
return null;
|
|
177
177
|
if (options?.skipFecId) {
|
|
178
|
-
return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false
|
|
178
|
+
return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false } : null;
|
|
179
179
|
}
|
|
180
|
-
const overlay = options?.overlayDeclared === true && rapFlag;
|
|
181
180
|
if (fecType === 1) {
|
|
182
|
-
const fecStart = payload.length -
|
|
181
|
+
const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
183
182
|
if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {
|
|
184
183
|
return null;
|
|
185
184
|
}
|
|
186
|
-
return { hasFecId: true
|
|
185
|
+
return { hasFecId: true };
|
|
187
186
|
}
|
|
188
|
-
if (fecType !== 0
|
|
187
|
+
if (fecType !== 0)
|
|
189
188
|
return null;
|
|
190
189
|
if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {
|
|
191
|
-
return { hasFecId: false
|
|
190
|
+
return { hasFecId: false };
|
|
192
191
|
}
|
|
193
192
|
return null;
|
|
194
193
|
}
|
|
195
|
-
function sourceFecTrailerSize(overlay) {
|
|
196
|
-
return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
197
|
-
}
|
|
198
194
|
function planMfuExtraction(mmtpPacket, options, parsedHeader) {
|
|
199
195
|
if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {
|
|
200
196
|
return {
|
|
@@ -247,11 +243,11 @@ function planMfuExtraction(mmtpPacket, options, parsedHeader) {
|
|
|
247
243
|
if ("rejection" in layoutResult)
|
|
248
244
|
return layoutResult;
|
|
249
245
|
const { layout } = layoutResult;
|
|
250
|
-
const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options
|
|
246
|
+
const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options);
|
|
251
247
|
if (!trailerLayout) {
|
|
252
248
|
if (!options?.skipFecId && header.fecType === 1) {
|
|
253
249
|
const observed = payload.length - layout.dataEnd;
|
|
254
|
-
const expected =
|
|
250
|
+
const expected = SOURCE_FEC_PAYLOAD_ID_SIZE;
|
|
255
251
|
if (observed !== expected) {
|
|
256
252
|
return {
|
|
257
253
|
rejection: {
|
|
@@ -283,13 +279,10 @@ function extractPlannedMfu(mmtpPacket, result) {
|
|
|
283
279
|
const mfuDuHeader = mfuDuHeaderPresent && mpu.timedFlag === 1 ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE) : null;
|
|
284
280
|
const itemId = mfuDuHeaderPresent && mpu.timedFlag === 0 ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false) : null;
|
|
285
281
|
const data = payload.subarray(mfuOffset, dataEnd);
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null;
|
|
291
|
-
const overlayFecPayloadId = trailerLayout.hasOverlayFecId ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE) : null;
|
|
292
|
-
return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId };
|
|
282
|
+
const fecPayloadId = trailerLayout.hasFecId ? {
|
|
283
|
+
ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE, 4).getUint32(0, false)
|
|
284
|
+
} : null;
|
|
285
|
+
return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId };
|
|
293
286
|
}
|
|
294
287
|
function sourceFecPayloadIdOf(mmtpPacket, header) {
|
|
295
288
|
if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE)
|
|
@@ -2299,6 +2292,10 @@ var FECManager = class _FECManager {
|
|
|
2299
2292
|
// ── Keyframe-protected repair policy (block-policy.ts) ──
|
|
2300
2293
|
// One classifier per physical stream, built from that stream's geometry.
|
|
2301
2294
|
this.#classifiers = /* @__PURE__ */ new Map();
|
|
2295
|
+
// A stream without catalog timing has no classifier, so the last RAP it
|
|
2296
|
+
// released stands in for one: "an MPU boundary bounds its RAPs" holds until
|
|
2297
|
+
// a second RAP is released inside one MPU (BLO-52085).
|
|
2298
|
+
this.#noSpanRaps = /* @__PURE__ */ new Map();
|
|
2302
2299
|
// Geometry a stream-scoped configuration gave one stream (streamKey). ABR
|
|
2303
2300
|
// rungs can share a packetId with a different framerate or GOP, so the
|
|
2304
2301
|
// packet-level geometry is only the geometry of a packet whose streams
|
|
@@ -2498,6 +2495,7 @@ var FECManager = class _FECManager {
|
|
|
2498
2495
|
#orderedFrontiers;
|
|
2499
2496
|
#orderedPendingBlocks;
|
|
2500
2497
|
#classifiers;
|
|
2498
|
+
#noSpanRaps;
|
|
2501
2499
|
#streamGeometries;
|
|
2502
2500
|
#failedBlocks;
|
|
2503
2501
|
#resolvedBlockClasses;
|
|
@@ -3159,6 +3157,7 @@ var FECManager = class _FECManager {
|
|
|
3159
3157
|
};
|
|
3160
3158
|
const discontinuity = () => {
|
|
3161
3159
|
this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
|
|
3160
|
+
this.#noSpanRaps.delete(streamKey);
|
|
3162
3161
|
this.#highestSbn.set(streamKey, { sbn, atMs: now });
|
|
3163
3162
|
this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
|
|
3164
3163
|
if (psn === void 0) this.#sourceAnchors.delete(streamKey);
|
|
@@ -3571,6 +3570,9 @@ var FECManager = class _FECManager {
|
|
|
3571
3570
|
}
|
|
3572
3571
|
if (policyChanged) {
|
|
3573
3572
|
this.#bumpEpoch(packetId);
|
|
3573
|
+
for (const key of [...this.#noSpanRaps.keys()]) {
|
|
3574
|
+
if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
|
|
3575
|
+
}
|
|
3574
3576
|
for (const [key, classifier] of [...this.#classifiers]) {
|
|
3575
3577
|
if (!this.#belongsToPacket(key, packetId)) continue;
|
|
3576
3578
|
const stream = this.#parseStreamKey(key);
|
|
@@ -3806,6 +3808,7 @@ var FECManager = class _FECManager {
|
|
|
3806
3808
|
resetMap(this.#isoLocators);
|
|
3807
3809
|
resetMap(this.#sourceAnchors);
|
|
3808
3810
|
this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
|
|
3811
|
+
this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
|
|
3809
3812
|
this.#bumpEpoch(packetId);
|
|
3810
3813
|
for (const [sbn, keys] of this.#blockKeysBySbn) {
|
|
3811
3814
|
for (const key of keys.keys()) {
|
|
@@ -3912,6 +3915,7 @@ var FECManager = class _FECManager {
|
|
|
3912
3915
|
dropMap(this.#isoLocators);
|
|
3913
3916
|
dropMap(this.#sourceAnchors);
|
|
3914
3917
|
dropMap(this.#classifiers);
|
|
3918
|
+
dropMap(this.#noSpanRaps);
|
|
3915
3919
|
dropMap(this.#streamGeometries);
|
|
3916
3920
|
dropSet(this.#pendingWorkerCleanups);
|
|
3917
3921
|
dropSet(this.#completedBlocks);
|
|
@@ -4542,11 +4546,14 @@ var FECManager = class _FECManager {
|
|
|
4542
4546
|
/**
|
|
4543
4547
|
* Whether an MPU boundary still bounds this stream's RAPs (see
|
|
4544
4548
|
* `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
|
|
4545
|
-
* it does not. A stream with no classifier yet has shown nothing against it
|
|
4549
|
+
* it does not. A stream with no classifier yet has shown nothing against it,
|
|
4550
|
+
* and neither has a stream without catalog timing until it releases two
|
|
4551
|
+
* RAPs inside one MPU.
|
|
4546
4552
|
*/
|
|
4547
4553
|
mpuBoundsRapsFor(packetId, catalogTrackName) {
|
|
4548
|
-
|
|
4549
|
-
return this.#
|
|
4554
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
4555
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
|
|
4556
|
+
return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
|
|
4550
4557
|
}
|
|
4551
4558
|
/** A block's class now, or frozen at its resolution. */
|
|
4552
4559
|
blockClassFor(sbn, packetId, catalogTrackName) {
|
|
@@ -4572,7 +4579,14 @@ var FECManager = class _FECManager {
|
|
|
4572
4579
|
* invariant (consecutive MPUs exactly one keyframe interval apart).
|
|
4573
4580
|
*/
|
|
4574
4581
|
noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
|
|
4575
|
-
if (!this.#trackConfigs.has(packetId)
|
|
4582
|
+
if (!this.#trackConfigs.has(packetId)) return;
|
|
4583
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) {
|
|
4584
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
4585
|
+
const last = this.#noSpanRaps.get(streamKey);
|
|
4586
|
+
const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
|
|
4587
|
+
this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
|
|
4588
|
+
return;
|
|
4589
|
+
}
|
|
4576
4590
|
this.#applyClassifierChanges(
|
|
4577
4591
|
packetId,
|
|
4578
4592
|
catalogTrackName,
|
|
@@ -5587,6 +5601,7 @@ var FECManager = class _FECManager {
|
|
|
5587
5601
|
this.#blockAnchorsMs.clear();
|
|
5588
5602
|
this.#blockCatalogTrackNames.clear();
|
|
5589
5603
|
this.#classifiers.clear();
|
|
5604
|
+
this.#noSpanRaps.clear();
|
|
5590
5605
|
this.#streamGeometries.clear();
|
|
5591
5606
|
this.#failedBlocks.clear();
|
|
5592
5607
|
this.#resolvedBlockClasses.clear();
|
|
@@ -7061,14 +7076,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
|
|
|
7061
7076
|
});
|
|
7062
7077
|
|
|
7063
7078
|
// node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
|
|
7079
|
+
var MAX_PENDING_PACKETS = 256;
|
|
7064
7080
|
var AMTGatewayManager = class {
|
|
7065
7081
|
subscriptions = /* @__PURE__ */ new Map();
|
|
7066
7082
|
messageHandler;
|
|
7083
|
+
messageListenerRegistered = false;
|
|
7067
7084
|
pendingPackets = /* @__PURE__ */ new Map();
|
|
7068
7085
|
constructor() {
|
|
7069
7086
|
this.messageHandler = (event) => {
|
|
7070
7087
|
const data = event.data;
|
|
7071
|
-
console.log(`[AMT] SW message received:`, data.type);
|
|
7072
7088
|
if (data.type === "UDP_PACKET") {
|
|
7073
7089
|
this.handlePacket(data);
|
|
7074
7090
|
} else if (data.type === "SOCKET_ERROR") {
|
|
@@ -7077,12 +7093,22 @@ var AMTGatewayManager = class {
|
|
|
7077
7093
|
console.log(`[AMT SW Debug] ${data.message}`);
|
|
7078
7094
|
}
|
|
7079
7095
|
};
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7096
|
+
}
|
|
7097
|
+
ensureMessageListener() {
|
|
7098
|
+
if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
|
|
7099
|
+
return;
|
|
7100
|
+
}
|
|
7101
|
+
navigator.serviceWorker.addEventListener("message", this.messageHandler);
|
|
7102
|
+
this.messageListenerRegistered = true;
|
|
7103
|
+
console.log("[AMT] Registered service worker message handler");
|
|
7104
|
+
}
|
|
7105
|
+
removeMessageListener() {
|
|
7106
|
+
if (!this.messageListenerRegistered)
|
|
7107
|
+
return;
|
|
7108
|
+
if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
7109
|
+
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
7085
7110
|
}
|
|
7111
|
+
this.messageListenerRegistered = false;
|
|
7086
7112
|
}
|
|
7087
7113
|
/**
|
|
7088
7114
|
* Subscribe via AMT tunnel
|
|
@@ -7092,6 +7118,7 @@ var AMTGatewayManager = class {
|
|
|
7092
7118
|
if (config.protected) {
|
|
7093
7119
|
throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
|
|
7094
7120
|
}
|
|
7121
|
+
this.ensureMessageListener();
|
|
7095
7122
|
const keyStr = this.getKeyString(key);
|
|
7096
7123
|
if (this.subscriptions.has(keyStr)) {
|
|
7097
7124
|
console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
|
|
@@ -7127,6 +7154,8 @@ var AMTGatewayManager = class {
|
|
|
7127
7154
|
this.subscriptions.delete(keyStr);
|
|
7128
7155
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
7129
7156
|
}
|
|
7157
|
+
if (this.subscriptions.size === 0)
|
|
7158
|
+
this.removeMessageListener();
|
|
7130
7159
|
throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
|
|
7131
7160
|
}
|
|
7132
7161
|
}
|
|
@@ -7148,6 +7177,8 @@ var AMTGatewayManager = class {
|
|
|
7148
7177
|
} finally {
|
|
7149
7178
|
this.subscriptions.delete(handle);
|
|
7150
7179
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
7180
|
+
if (this.subscriptions.size === 0)
|
|
7181
|
+
this.removeMessageListener();
|
|
7151
7182
|
}
|
|
7152
7183
|
}
|
|
7153
7184
|
/**
|
|
@@ -7164,14 +7195,12 @@ var AMTGatewayManager = class {
|
|
|
7164
7195
|
if (!subscriptionId)
|
|
7165
7196
|
return;
|
|
7166
7197
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
7167
|
-
if (!subscription)
|
|
7168
|
-
console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
|
|
7198
|
+
if (!subscription)
|
|
7169
7199
|
return;
|
|
7170
|
-
}
|
|
7171
7200
|
if (!subscription.handshakeComplete) {
|
|
7172
7201
|
const queue = this.pendingPackets.get(subscriptionId);
|
|
7173
7202
|
const packetData2 = data;
|
|
7174
|
-
if (queue) {
|
|
7203
|
+
if (queue && queue.length < MAX_PENDING_PACKETS) {
|
|
7175
7204
|
queue.push({
|
|
7176
7205
|
data: packetData2.data,
|
|
7177
7206
|
remoteAddress: packetData2.remoteAddress,
|
|
@@ -7197,19 +7226,15 @@ var AMTGatewayManager = class {
|
|
|
7197
7226
|
if (!subscriptionId)
|
|
7198
7227
|
return;
|
|
7199
7228
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
7200
|
-
if (!subscription)
|
|
7201
|
-
console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
|
|
7229
|
+
if (!subscription)
|
|
7202
7230
|
return;
|
|
7203
|
-
}
|
|
7204
7231
|
subscription.config.onError(new Error(error || "Unknown error"));
|
|
7205
7232
|
}
|
|
7206
7233
|
/**
|
|
7207
7234
|
* Cleanup on destroy
|
|
7208
7235
|
*/
|
|
7209
7236
|
destroy() {
|
|
7210
|
-
|
|
7211
|
-
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
7212
|
-
}
|
|
7237
|
+
this.removeMessageListener();
|
|
7213
7238
|
const handles = Array.from(this.subscriptions.keys());
|
|
7214
7239
|
for (const handle of handles) {
|
|
7215
7240
|
this.unsubscribe(handle).catch((err) => {
|
|
@@ -13118,9 +13143,21 @@ var IsoBmffTimingResolver = class {
|
|
|
13118
13143
|
}
|
|
13119
13144
|
#track = null;
|
|
13120
13145
|
#fragments = /* @__PURE__ */ new Map();
|
|
13146
|
+
/** First sample of each pending fragment, kept in `#delivered` with its sample count. */
|
|
13147
|
+
#firstSamples = /* @__PURE__ */ new Map();
|
|
13148
|
+
/**
|
|
13149
|
+
* First sample and sample count of the newest fragments whose every slot was
|
|
13150
|
+
* cleared. Abandonment counts: `releaseSample` clears a slot too, which
|
|
13151
|
+
* matches the combiner, since it retires an abandoned sample as well.
|
|
13152
|
+
*/
|
|
13153
|
+
#delivered = /* @__PURE__ */ new Map();
|
|
13121
13154
|
#pendingSamples = 0;
|
|
13122
13155
|
#evictedSamples = 0;
|
|
13123
13156
|
#lastMovieFragmentRejection = null;
|
|
13157
|
+
/** True once an FT=0 has bound the track that FT=1 timing is read against. */
|
|
13158
|
+
get initialized() {
|
|
13159
|
+
return this.#track !== null;
|
|
13160
|
+
}
|
|
13124
13161
|
get pendingSampleCount() {
|
|
13125
13162
|
return this.#pendingSamples;
|
|
13126
13163
|
}
|
|
@@ -13144,7 +13181,14 @@ var IsoBmffTimingResolver = class {
|
|
|
13144
13181
|
this.#track = timing;
|
|
13145
13182
|
return true;
|
|
13146
13183
|
}
|
|
13147
|
-
|
|
13184
|
+
/**
|
|
13185
|
+
* `recovered` marks an FEC-recovered copy. A recovered block hands back
|
|
13186
|
+
* every FT=1 it holds, the received ones included, so a recovered copy of a
|
|
13187
|
+
* fragment whose samples are all done is a duplicate: admitting it again
|
|
13188
|
+
* would charge stale timing and could displace the live MPU's. A source
|
|
13189
|
+
* copy is still admitted again (a rendition switch re-arms timing that way).
|
|
13190
|
+
*/
|
|
13191
|
+
addMovieFragment(mpuSequenceNumber, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
|
|
13148
13192
|
this.#lastMovieFragmentRejection = null;
|
|
13149
13193
|
if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
|
|
13150
13194
|
this.#lastMovieFragmentRejection = "invalid";
|
|
@@ -13170,6 +13214,8 @@ var IsoBmffTimingResolver = class {
|
|
|
13170
13214
|
(sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
|
|
13171
13215
|
);
|
|
13172
13216
|
}
|
|
13217
|
+
const delivered = recovered ? this.#delivered.get(key) : void 0;
|
|
13218
|
+
if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
|
|
13173
13219
|
let freed = 0;
|
|
13174
13220
|
while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
|
|
13175
13221
|
freed += this.#evictOldestFragment();
|
|
@@ -13183,6 +13229,7 @@ var IsoBmffTimingResolver = class {
|
|
|
13183
13229
|
return false;
|
|
13184
13230
|
}
|
|
13185
13231
|
this.#fragments.set(key, [...parsed.samples]);
|
|
13232
|
+
this.#firstSamples.set(key, parsed.samples[0]);
|
|
13186
13233
|
this.#pendingSamples += parsed.samples.length;
|
|
13187
13234
|
return true;
|
|
13188
13235
|
}
|
|
@@ -13240,6 +13287,7 @@ var IsoBmffTimingResolver = class {
|
|
|
13240
13287
|
let live = 0;
|
|
13241
13288
|
for (const sample of samples) if (sample !== null) live++;
|
|
13242
13289
|
this.#fragments.delete(oldest);
|
|
13290
|
+
this.#firstSamples.delete(oldest);
|
|
13243
13291
|
this.#pendingSamples -= live;
|
|
13244
13292
|
this.#evictedSamples += live;
|
|
13245
13293
|
return live;
|
|
@@ -13255,12 +13303,23 @@ var IsoBmffTimingResolver = class {
|
|
|
13255
13303
|
if (!samples || !samples[index]) return false;
|
|
13256
13304
|
samples[index] = null;
|
|
13257
13305
|
this.#pendingSamples--;
|
|
13258
|
-
if (samples.every((value) => value === null))
|
|
13306
|
+
if (samples.every((value) => value === null)) {
|
|
13307
|
+
this.#fragments.delete(key);
|
|
13308
|
+
const first = this.#firstSamples.get(key);
|
|
13309
|
+
this.#firstSamples.delete(key);
|
|
13310
|
+
this.#delivered.delete(key);
|
|
13311
|
+
this.#delivered.set(key, { first, sampleCount: samples.length });
|
|
13312
|
+
if (this.#delivered.size > this.maxPendingSamples) {
|
|
13313
|
+
this.#delivered.delete(this.#delivered.keys().next().value);
|
|
13314
|
+
}
|
|
13315
|
+
}
|
|
13259
13316
|
return true;
|
|
13260
13317
|
}
|
|
13261
13318
|
reset() {
|
|
13262
13319
|
this.#track = null;
|
|
13263
13320
|
this.#fragments.clear();
|
|
13321
|
+
this.#firstSamples.clear();
|
|
13322
|
+
this.#delivered.clear();
|
|
13264
13323
|
this.#pendingSamples = 0;
|
|
13265
13324
|
}
|
|
13266
13325
|
};
|
|
@@ -13742,6 +13801,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
13742
13801
|
this.#reclaimedTimingSamples = 0;
|
|
13743
13802
|
this.#evictedTimingSamples = 0;
|
|
13744
13803
|
this.#rejectedMovieFragments = 0;
|
|
13804
|
+
this.#recoveredHeadsAwaitingTiming = {
|
|
13805
|
+
held: 0,
|
|
13806
|
+
expired: 0,
|
|
13807
|
+
discarded: 0,
|
|
13808
|
+
superseded: 0
|
|
13809
|
+
};
|
|
13745
13810
|
this.#sequencers = /* @__PURE__ */ new Map();
|
|
13746
13811
|
this.#fecGate = null;
|
|
13747
13812
|
this.#fecSequencer = null;
|
|
@@ -13850,6 +13915,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
13850
13915
|
#reclaimedTimingSamples;
|
|
13851
13916
|
#evictedTimingSamples;
|
|
13852
13917
|
#rejectedMovieFragments;
|
|
13918
|
+
#recoveredHeadsAwaitingTiming;
|
|
13853
13919
|
#sequencers;
|
|
13854
13920
|
#fecGate;
|
|
13855
13921
|
#fecSequencer;
|
|
@@ -13970,6 +14036,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
13970
14036
|
rejectedMovieFragments: this.#rejectedMovieFragments
|
|
13971
14037
|
};
|
|
13972
14038
|
}
|
|
14039
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
14040
|
+
get recoveredHeadsAwaitingTiming() {
|
|
14041
|
+
let pending = 0;
|
|
14042
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
14043
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++;
|
|
14044
|
+
}
|
|
14045
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending };
|
|
14046
|
+
}
|
|
13973
14047
|
get reorderStats() {
|
|
13974
14048
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
|
|
13975
14049
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
|
|
@@ -14143,7 +14217,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14143
14217
|
const now = Date.now();
|
|
14144
14218
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
14145
14219
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
|
|
14146
|
-
awaitingTiming.shift();
|
|
14220
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
14147
14221
|
}
|
|
14148
14222
|
}
|
|
14149
14223
|
}
|
|
@@ -14261,11 +14335,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14261
14335
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
14262
14336
|
return this.#drop("fragment-unexpected-fragmentation");
|
|
14263
14337
|
const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
|
|
14264
|
-
if (!resolver2) return this.#drop("fragment-no-resolver");
|
|
14338
|
+
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
14265
14339
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
14266
14340
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
14267
14341
|
const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
|
|
14268
|
-
const admitted = resolver2.addMovieFragment(
|
|
14342
|
+
const admitted = resolver2.addMovieFragment(
|
|
14343
|
+
mpu.mpuSequenceNumber,
|
|
14344
|
+
data,
|
|
14345
|
+
remaining,
|
|
14346
|
+
options?.recovered === true
|
|
14347
|
+
);
|
|
14269
14348
|
this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
|
|
14270
14349
|
this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
|
|
14271
14350
|
if (!admitted) {
|
|
@@ -14285,7 +14364,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14285
14364
|
};
|
|
14286
14365
|
}
|
|
14287
14366
|
if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
|
|
14288
|
-
const resolver = this.#timingResolver(streamKey, info.packetId,
|
|
14367
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true);
|
|
14289
14368
|
if (!resolver) return this.#drop("mfu-no-resolver");
|
|
14290
14369
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
14291
14370
|
return this.#drop("mfu-empty-payload");
|
|
@@ -14306,6 +14385,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14306
14385
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
14307
14386
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
14308
14387
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
14388
|
+
sequenceNumber: info.sequenceNumber,
|
|
14309
14389
|
arrivedAt: Date.now()
|
|
14310
14390
|
});
|
|
14311
14391
|
return this.#drop("mfu-sample-timing-missing");
|
|
@@ -14629,6 +14709,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14629
14709
|
}
|
|
14630
14710
|
this.#emitFrame(data, meta);
|
|
14631
14711
|
}
|
|
14712
|
+
/** Whether FT=1 metadata already names this sample, without claiming it. */
|
|
14713
|
+
hasAuthoredTimingFor(packetId, mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
|
|
14714
|
+
return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber, movieFragmentSequenceNumber, sampleNumber) != null;
|
|
14715
|
+
}
|
|
14632
14716
|
/**
|
|
14633
14717
|
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
14634
14718
|
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
@@ -14690,6 +14774,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14690
14774
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
|
|
14691
14775
|
this.#sequencers.clear();
|
|
14692
14776
|
this.#fecSequencer?.discard();
|
|
14777
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
14778
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
14779
|
+
}
|
|
14693
14780
|
this.#timingStreams.clear();
|
|
14694
14781
|
this.#pendingTimingSamples = 0;
|
|
14695
14782
|
this.#reliableMoqStartupGate.clear();
|
|
@@ -14708,10 +14795,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14708
14795
|
return resolver;
|
|
14709
14796
|
}
|
|
14710
14797
|
#awaitTiming(streamKey, head) {
|
|
14711
|
-
const
|
|
14712
|
-
if (!
|
|
14798
|
+
const state = this.#timingStreams.get(streamKey);
|
|
14799
|
+
if (!state) return;
|
|
14800
|
+
if (!head.options?.recovered) {
|
|
14801
|
+
const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
|
|
14802
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
|
|
14803
|
+
state.awaitingTiming = kept;
|
|
14804
|
+
}
|
|
14805
|
+
const awaitingTiming = state.awaitingTiming;
|
|
14713
14806
|
awaitingTiming.push(head);
|
|
14714
|
-
if (
|
|
14807
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
|
|
14808
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
14809
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
14810
|
+
}
|
|
14811
|
+
}
|
|
14812
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
14813
|
+
#leaveAwaitingTiming(head, outcome) {
|
|
14814
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
|
|
14715
14815
|
}
|
|
14716
14816
|
/**
|
|
14717
14817
|
* Feed again the heads whose timing the FT=1 just admitted declares. That
|
|
@@ -14739,6 +14839,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
14739
14839
|
const state = this.#timingStreams.get(streamKey);
|
|
14740
14840
|
if (!state) return;
|
|
14741
14841
|
this.#timingStreams.delete(streamKey);
|
|
14842
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
14742
14843
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
|
|
14743
14844
|
if (this.#pendingTimingSamples < 0) {
|
|
14744
14845
|
throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
|
|
@@ -18743,6 +18844,10 @@ var MmtpRouter = class {
|
|
|
18743
18844
|
get keyframeOverlayStats() {
|
|
18744
18845
|
return { ...this.#overlayStats };
|
|
18745
18846
|
}
|
|
18847
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
18848
|
+
get recoveredHeadsAwaitingTiming() {
|
|
18849
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
18850
|
+
}
|
|
18746
18851
|
#isOverlayRepair(repairPacketId, repairTrackName) {
|
|
18747
18852
|
return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
|
|
18748
18853
|
}
|
|
@@ -21152,6 +21257,29 @@ var MmtpRouter = class {
|
|
|
21152
21257
|
}
|
|
21153
21258
|
);
|
|
21154
21259
|
}
|
|
21260
|
+
const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
|
|
21261
|
+
ownerPacketId,
|
|
21262
|
+
sym.fragment.mpuSequenceNumber,
|
|
21263
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
21264
|
+
sym.fragment.sampleNumber,
|
|
21265
|
+
catalogTrackName
|
|
21266
|
+
);
|
|
21267
|
+
for (const sym of result.symbols) {
|
|
21268
|
+
if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
21269
|
+
this.#feedViaParser(
|
|
21270
|
+
sym.rawSymbol,
|
|
21271
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
21272
|
+
void 0,
|
|
21273
|
+
{
|
|
21274
|
+
skipFecPayloadId: true,
|
|
21275
|
+
recovered: true,
|
|
21276
|
+
deliveryProvenance,
|
|
21277
|
+
...fecSbnOf(sym.index),
|
|
21278
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
21279
|
+
}
|
|
21280
|
+
);
|
|
21281
|
+
}
|
|
21282
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
21155
21283
|
const gated = this.#isFecTrack(ownerPacketId);
|
|
21156
21284
|
const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
|
|
21157
21285
|
if (assembler) {
|