@blockcast/mmt-container 2.2.0-main.6c178bf47550 → 2.2.0-main.75b39a2cdddf
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 +869 -180
- package/dist/browser-runtime.js.map +4 -4
- 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/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 +33 -1
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +222 -6
- package/dist/fec-manager.js.map +2 -2
- 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/index.d.ts +4 -3
- 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 +9 -0
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +17 -0
- 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/mmtp-container-parser.d.ts +58 -10
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +93 -15
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +96 -17
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +92 -15
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +18 -1
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +311 -45
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-manager.js +535 -53
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +28 -18
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.js +92 -15
- package/dist/no-modules.js.map +2 -2
- 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/track-session.d.ts +1 -1
- package/dist/track-session.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +92 -15
- 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 +277 -6
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +16 -2
- package/src/iso-block-locator.ts +19 -0
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +125 -19
- package/src/mmtp-router.ts +350 -13
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +1 -1
package/dist/browser-runtime.js
CHANGED
|
@@ -7988,8 +7988,8 @@ var MulticastSubscriptionPool = class {
|
|
|
7988
7988
|
record.claims--;
|
|
7989
7989
|
return this.acquire(gateway, config, consumer);
|
|
7990
7990
|
}
|
|
7991
|
-
const
|
|
7992
|
-
entry.consumers.set(
|
|
7991
|
+
const token2 = Symbol(key);
|
|
7992
|
+
entry.consumers.set(token2, consumer);
|
|
7993
7993
|
let released = false;
|
|
7994
7994
|
return {
|
|
7995
7995
|
key,
|
|
@@ -7997,7 +7997,7 @@ var MulticastSubscriptionPool = class {
|
|
|
7997
7997
|
if (released)
|
|
7998
7998
|
return;
|
|
7999
7999
|
released = true;
|
|
8000
|
-
entry.consumers.delete(
|
|
8000
|
+
entry.consumers.delete(token2);
|
|
8001
8001
|
record.claims--;
|
|
8002
8002
|
if (record.claims > 0)
|
|
8003
8003
|
return;
|
|
@@ -8625,9 +8625,9 @@ async function openSubscription(rx, openedPromise, options, packetIds) {
|
|
|
8625
8625
|
} catch (error) {
|
|
8626
8626
|
throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
|
|
8627
8627
|
}
|
|
8628
|
-
if (
|
|
8628
|
+
if (opened.integrityScheme !== INTEGRITY_SCHEME) {
|
|
8629
8629
|
await rx.close();
|
|
8630
|
-
throw unsupported(`integrity scheme ${String(
|
|
8630
|
+
throw unsupported(`integrity scheme ${String(opened.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
|
|
8631
8631
|
}
|
|
8632
8632
|
const events = new EventTarget();
|
|
8633
8633
|
let state = "connected";
|
|
@@ -8746,14 +8746,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
|
|
|
8746
8746
|
});
|
|
8747
8747
|
|
|
8748
8748
|
// node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
|
|
8749
|
+
var MAX_PENDING_PACKETS = 256;
|
|
8749
8750
|
var AMTGatewayManager = class {
|
|
8750
8751
|
subscriptions = /* @__PURE__ */ new Map();
|
|
8751
8752
|
messageHandler;
|
|
8753
|
+
messageListenerRegistered = false;
|
|
8752
8754
|
pendingPackets = /* @__PURE__ */ new Map();
|
|
8753
8755
|
constructor() {
|
|
8754
8756
|
this.messageHandler = (event) => {
|
|
8755
8757
|
const data = event.data;
|
|
8756
|
-
console.log(`[AMT] SW message received:`, data.type);
|
|
8757
8758
|
if (data.type === "UDP_PACKET") {
|
|
8758
8759
|
this.handlePacket(data);
|
|
8759
8760
|
} else if (data.type === "SOCKET_ERROR") {
|
|
@@ -8762,12 +8763,22 @@ var AMTGatewayManager = class {
|
|
|
8762
8763
|
console.log(`[AMT SW Debug] ${data.message}`);
|
|
8763
8764
|
}
|
|
8764
8765
|
};
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8766
|
+
}
|
|
8767
|
+
ensureMessageListener() {
|
|
8768
|
+
if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
|
|
8769
|
+
return;
|
|
8770
|
+
}
|
|
8771
|
+
navigator.serviceWorker.addEventListener("message", this.messageHandler);
|
|
8772
|
+
this.messageListenerRegistered = true;
|
|
8773
|
+
console.log("[AMT] Registered service worker message handler");
|
|
8774
|
+
}
|
|
8775
|
+
removeMessageListener() {
|
|
8776
|
+
if (!this.messageListenerRegistered)
|
|
8777
|
+
return;
|
|
8778
|
+
if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
8779
|
+
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
8770
8780
|
}
|
|
8781
|
+
this.messageListenerRegistered = false;
|
|
8771
8782
|
}
|
|
8772
8783
|
/**
|
|
8773
8784
|
* Subscribe via AMT tunnel
|
|
@@ -8777,6 +8788,7 @@ var AMTGatewayManager = class {
|
|
|
8777
8788
|
if (config.protected) {
|
|
8778
8789
|
throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
|
|
8779
8790
|
}
|
|
8791
|
+
this.ensureMessageListener();
|
|
8780
8792
|
const keyStr = this.getKeyString(key);
|
|
8781
8793
|
if (this.subscriptions.has(keyStr)) {
|
|
8782
8794
|
console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
|
|
@@ -8812,6 +8824,8 @@ var AMTGatewayManager = class {
|
|
|
8812
8824
|
this.subscriptions.delete(keyStr);
|
|
8813
8825
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
8814
8826
|
}
|
|
8827
|
+
if (this.subscriptions.size === 0)
|
|
8828
|
+
this.removeMessageListener();
|
|
8815
8829
|
throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
|
|
8816
8830
|
}
|
|
8817
8831
|
}
|
|
@@ -8833,6 +8847,8 @@ var AMTGatewayManager = class {
|
|
|
8833
8847
|
} finally {
|
|
8834
8848
|
this.subscriptions.delete(handle);
|
|
8835
8849
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
8850
|
+
if (this.subscriptions.size === 0)
|
|
8851
|
+
this.removeMessageListener();
|
|
8836
8852
|
}
|
|
8837
8853
|
}
|
|
8838
8854
|
/**
|
|
@@ -8849,14 +8865,12 @@ var AMTGatewayManager = class {
|
|
|
8849
8865
|
if (!subscriptionId)
|
|
8850
8866
|
return;
|
|
8851
8867
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
8852
|
-
if (!subscription)
|
|
8853
|
-
console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
|
|
8868
|
+
if (!subscription)
|
|
8854
8869
|
return;
|
|
8855
|
-
}
|
|
8856
8870
|
if (!subscription.handshakeComplete) {
|
|
8857
8871
|
const queue = this.pendingPackets.get(subscriptionId);
|
|
8858
8872
|
const packetData2 = data;
|
|
8859
|
-
if (queue) {
|
|
8873
|
+
if (queue && queue.length < MAX_PENDING_PACKETS) {
|
|
8860
8874
|
queue.push({
|
|
8861
8875
|
data: packetData2.data,
|
|
8862
8876
|
remoteAddress: packetData2.remoteAddress,
|
|
@@ -8882,19 +8896,15 @@ var AMTGatewayManager = class {
|
|
|
8882
8896
|
if (!subscriptionId)
|
|
8883
8897
|
return;
|
|
8884
8898
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
8885
|
-
if (!subscription)
|
|
8886
|
-
console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
|
|
8899
|
+
if (!subscription)
|
|
8887
8900
|
return;
|
|
8888
|
-
}
|
|
8889
8901
|
subscription.config.onError(new Error(error || "Unknown error"));
|
|
8890
8902
|
}
|
|
8891
8903
|
/**
|
|
8892
8904
|
* Cleanup on destroy
|
|
8893
8905
|
*/
|
|
8894
8906
|
destroy() {
|
|
8895
|
-
|
|
8896
|
-
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
8897
|
-
}
|
|
8907
|
+
this.removeMessageListener();
|
|
8898
8908
|
const handles = Array.from(this.subscriptions.keys());
|
|
8899
8909
|
for (const handle of handles) {
|
|
8900
8910
|
this.unsubscribe(handle).catch((err) => {
|
|
@@ -12939,6 +12949,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12939
12949
|
#retentionBlocks;
|
|
12940
12950
|
#heldEvicted;
|
|
12941
12951
|
#newest;
|
|
12952
|
+
#newestSource;
|
|
12942
12953
|
#holes;
|
|
12943
12954
|
static #checkedRetention(retentionBlocks) {
|
|
12944
12955
|
if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
|
|
@@ -12976,6 +12987,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12976
12987
|
observe(position) {
|
|
12977
12988
|
if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
|
|
12978
12989
|
}
|
|
12990
|
+
/**
|
|
12991
|
+
* The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
|
|
12992
|
+
* `newest`, a repair never moves it: a repair can run ahead of its sources,
|
|
12993
|
+
* a source cannot run ahead of the sources sent before it except by
|
|
12994
|
+
* reordering.
|
|
12995
|
+
*/
|
|
12996
|
+
get newestSource() {
|
|
12997
|
+
return this.#newestSource;
|
|
12998
|
+
}
|
|
12999
|
+
/** Note a received source's SS_ID; true when it is the newest one yet. */
|
|
13000
|
+
noteSource(ssId) {
|
|
13001
|
+
if (this.#newestSource !== void 0 && !serial32Precedes(this.#newestSource, ssId)) return false;
|
|
13002
|
+
this.#newestSource = ssId;
|
|
13003
|
+
return true;
|
|
13004
|
+
}
|
|
12979
13005
|
/** Held sources evicted by `heldCapacity` (never by the horizon itself). */
|
|
12980
13006
|
get heldEvicted() {
|
|
12981
13007
|
return this.#heldEvicted;
|
|
@@ -13208,6 +13234,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13208
13234
|
this.#held.clear();
|
|
13209
13235
|
this.#coverageEnd = void 0;
|
|
13210
13236
|
this.#newest = void 0;
|
|
13237
|
+
this.#newestSource = void 0;
|
|
13211
13238
|
}
|
|
13212
13239
|
#claim(block) {
|
|
13213
13240
|
const placed = [];
|
|
@@ -13827,6 +13854,7 @@ var WORKER_ERROR_LOG_CAP = 5;
|
|
|
13827
13854
|
var WORKER_ERROR_KIND_CAP = 32;
|
|
13828
13855
|
var WORKER_ERROR_OVERFLOW_KIND = "other";
|
|
13829
13856
|
var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
|
|
13857
|
+
var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
|
|
13830
13858
|
function isMulticastSymbolDelivery(value) {
|
|
13831
13859
|
return value === "datagram" || value === "ordered-relay";
|
|
13832
13860
|
}
|
|
@@ -13924,6 +13952,10 @@ var FECManager = class _FECManager {
|
|
|
13924
13952
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
13925
13953
|
this.#blockSourceCounts = /* @__PURE__ */ new Map();
|
|
13926
13954
|
this.#blockSourceEsis = /* @__PURE__ */ new Map();
|
|
13955
|
+
// Source ESIs per block that another FEC instance rebuilt and fed in
|
|
13956
|
+
// (feedRebuiltSourceSymbol). Kept apart from #blockSourceEsis: they are
|
|
13957
|
+
// not network arrivals and never make a block clean.
|
|
13958
|
+
this.#blockRebuiltEsis = /* @__PURE__ */ new Map();
|
|
13927
13959
|
// Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
|
|
13928
13960
|
// first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
|
|
13929
13961
|
// media units, usually before it holds K symbols; under iso-ssbg1-v1
|
|
@@ -14052,6 +14084,10 @@ var FECManager = class _FECManager {
|
|
|
14052
14084
|
// ── Keyframe-protected repair policy (block-policy.ts) ──
|
|
14053
14085
|
// One classifier per physical stream, built from that stream's geometry.
|
|
14054
14086
|
this.#classifiers = /* @__PURE__ */ new Map();
|
|
14087
|
+
// A stream without catalog timing has no classifier, so the last RAP it
|
|
14088
|
+
// released stands in for one: "an MPU boundary bounds its RAPs" holds until
|
|
14089
|
+
// a second RAP is released inside one MPU (BLO-52085).
|
|
14090
|
+
this.#noSpanRaps = /* @__PURE__ */ new Map();
|
|
14055
14091
|
// Geometry a stream-scoped configuration gave one stream (streamKey). ABR
|
|
14056
14092
|
// rungs can share a packetId with a different framerate or GOP, so the
|
|
14057
14093
|
// packet-level geometry is only the geometry of a packet whose streams
|
|
@@ -14209,6 +14245,7 @@ var FECManager = class _FECManager {
|
|
|
14209
14245
|
#pendingWorkerCleanupBounds;
|
|
14210
14246
|
#blockSourceCounts;
|
|
14211
14247
|
#blockSourceEsis;
|
|
14248
|
+
#blockRebuiltEsis;
|
|
14212
14249
|
#blockAuthoredLengths;
|
|
14213
14250
|
#ssbLengthConflictCount;
|
|
14214
14251
|
#blockRepairRsIds;
|
|
@@ -14251,6 +14288,7 @@ var FECManager = class _FECManager {
|
|
|
14251
14288
|
#orderedFrontiers;
|
|
14252
14289
|
#orderedPendingBlocks;
|
|
14253
14290
|
#classifiers;
|
|
14291
|
+
#noSpanRaps;
|
|
14254
14292
|
#streamGeometries;
|
|
14255
14293
|
#failedBlocks;
|
|
14256
14294
|
#resolvedBlockClasses;
|
|
@@ -14912,6 +14950,7 @@ var FECManager = class _FECManager {
|
|
|
14912
14950
|
};
|
|
14913
14951
|
const discontinuity = () => {
|
|
14914
14952
|
this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
|
|
14953
|
+
this.#noSpanRaps.delete(streamKey);
|
|
14915
14954
|
this.#highestSbn.set(streamKey, { sbn, atMs: now });
|
|
14916
14955
|
this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
|
|
14917
14956
|
if (psn === void 0) this.#sourceAnchors.delete(streamKey);
|
|
@@ -15324,6 +15363,9 @@ var FECManager = class _FECManager {
|
|
|
15324
15363
|
}
|
|
15325
15364
|
if (policyChanged) {
|
|
15326
15365
|
this.#bumpEpoch(packetId);
|
|
15366
|
+
for (const key of [...this.#noSpanRaps.keys()]) {
|
|
15367
|
+
if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
|
|
15368
|
+
}
|
|
15327
15369
|
for (const [key, classifier] of [...this.#classifiers]) {
|
|
15328
15370
|
if (!this.#belongsToPacket(key, packetId)) continue;
|
|
15329
15371
|
const stream = this.#parseStreamKey(key);
|
|
@@ -15526,6 +15568,7 @@ var FECManager = class _FECManager {
|
|
|
15526
15568
|
}
|
|
15527
15569
|
resetMap(this.#blockSourceCounts);
|
|
15528
15570
|
resetMap(this.#blockSourceEsis);
|
|
15571
|
+
resetMap(this.#blockRebuiltEsis);
|
|
15529
15572
|
resetMap(this.#blockAuthoredLengths);
|
|
15530
15573
|
resetMap(this.#blockRepairRsIds);
|
|
15531
15574
|
resetMap(this.#blockRsbLengths);
|
|
@@ -15559,6 +15602,7 @@ var FECManager = class _FECManager {
|
|
|
15559
15602
|
resetMap(this.#isoLocators);
|
|
15560
15603
|
resetMap(this.#sourceAnchors);
|
|
15561
15604
|
this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
|
|
15605
|
+
this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
|
|
15562
15606
|
this.#bumpEpoch(packetId);
|
|
15563
15607
|
for (const [sbn, keys] of this.#blockKeysBySbn) {
|
|
15564
15608
|
for (const key of keys.keys()) {
|
|
@@ -15649,6 +15693,7 @@ var FECManager = class _FECManager {
|
|
|
15649
15693
|
dropMap(this.#pendingWorkerCleanupClients);
|
|
15650
15694
|
dropMap(this.#blockSourceCounts);
|
|
15651
15695
|
dropMap(this.#blockSourceEsis);
|
|
15696
|
+
dropMap(this.#blockRebuiltEsis);
|
|
15652
15697
|
dropMap(this.#blockAuthoredLengths);
|
|
15653
15698
|
dropMap(this.#blockRepairRsIds);
|
|
15654
15699
|
dropMap(this.#blockRsbLengths);
|
|
@@ -15665,6 +15710,7 @@ var FECManager = class _FECManager {
|
|
|
15665
15710
|
dropMap(this.#isoLocators);
|
|
15666
15711
|
dropMap(this.#sourceAnchors);
|
|
15667
15712
|
dropMap(this.#classifiers);
|
|
15713
|
+
dropMap(this.#noSpanRaps);
|
|
15668
15714
|
dropMap(this.#streamGeometries);
|
|
15669
15715
|
dropSet(this.#pendingWorkerCleanups);
|
|
15670
15716
|
dropSet(this.#completedBlocks);
|
|
@@ -15810,6 +15856,9 @@ var FECManager = class _FECManager {
|
|
|
15810
15856
|
this.#unindexBlockKey(key);
|
|
15811
15857
|
}
|
|
15812
15858
|
}
|
|
15859
|
+
if (isSourceSymbol && source === "multicast") {
|
|
15860
|
+
this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
|
|
15861
|
+
}
|
|
15813
15862
|
return true;
|
|
15814
15863
|
}
|
|
15815
15864
|
#observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
|
|
@@ -16127,6 +16176,7 @@ var FECManager = class _FECManager {
|
|
|
16127
16176
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
16128
16177
|
const evictedBefore = locator.heldEvicted;
|
|
16129
16178
|
const held = locator.hold(ssId, source);
|
|
16179
|
+
if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
|
|
16130
16180
|
const evicted = locator.heldEvicted - evictedBefore;
|
|
16131
16181
|
if (evicted > 0) {
|
|
16132
16182
|
const before = this.#isoHeldEvictedCount;
|
|
@@ -16295,11 +16345,14 @@ var FECManager = class _FECManager {
|
|
|
16295
16345
|
/**
|
|
16296
16346
|
* Whether an MPU boundary still bounds this stream's RAPs (see
|
|
16297
16347
|
* `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
|
|
16298
|
-
* it does not. A stream with no classifier yet has shown nothing against it
|
|
16348
|
+
* it does not. A stream with no classifier yet has shown nothing against it,
|
|
16349
|
+
* and neither has a stream without catalog timing until it releases two
|
|
16350
|
+
* RAPs inside one MPU.
|
|
16299
16351
|
*/
|
|
16300
16352
|
mpuBoundsRapsFor(packetId, catalogTrackName) {
|
|
16301
|
-
|
|
16302
|
-
return this.#
|
|
16353
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
16354
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
|
|
16355
|
+
return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
|
|
16303
16356
|
}
|
|
16304
16357
|
/** A block's class now, or frozen at its resolution. */
|
|
16305
16358
|
blockClassFor(sbn, packetId, catalogTrackName) {
|
|
@@ -16325,7 +16378,14 @@ var FECManager = class _FECManager {
|
|
|
16325
16378
|
* invariant (consecutive MPUs exactly one keyframe interval apart).
|
|
16326
16379
|
*/
|
|
16327
16380
|
noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
|
|
16328
|
-
if (!this.#trackConfigs.has(packetId)
|
|
16381
|
+
if (!this.#trackConfigs.has(packetId)) return;
|
|
16382
|
+
if (!this.#hasCatalogTimingForPacket(packetId)) {
|
|
16383
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
16384
|
+
const last = this.#noSpanRaps.get(streamKey);
|
|
16385
|
+
const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
|
|
16386
|
+
this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
|
|
16387
|
+
return;
|
|
16388
|
+
}
|
|
16329
16389
|
this.#applyClassifierChanges(
|
|
16330
16390
|
packetId,
|
|
16331
16391
|
catalogTrackName,
|
|
@@ -16712,7 +16772,15 @@ var FECManager = class _FECManager {
|
|
|
16712
16772
|
this.#isoSourceOversizeCount++;
|
|
16713
16773
|
return null;
|
|
16714
16774
|
}
|
|
16715
|
-
|
|
16775
|
+
return this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, located.block.start);
|
|
16776
|
+
}
|
|
16777
|
+
/**
|
|
16778
|
+
* Hand one placed source symbol to the stream's decoder: its Worker, which
|
|
16779
|
+
* answers through onFrame (null here), or its synchronous decoder, whose
|
|
16780
|
+
* recovered block is returned. `sbn` is the SS_Start of the block a repair
|
|
16781
|
+
* stated for `ssId`.
|
|
16782
|
+
*/
|
|
16783
|
+
#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn) {
|
|
16716
16784
|
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
16717
16785
|
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
|
|
16718
16786
|
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
@@ -16741,6 +16809,74 @@ var FECManager = class _FECManager {
|
|
|
16741
16809
|
}
|
|
16742
16810
|
return recovered;
|
|
16743
16811
|
}
|
|
16812
|
+
/**
|
|
16813
|
+
* Feed a source symbol of this stream that another FEC instance over the
|
|
16814
|
+
* same packets rebuilt: a keyframe overlay recovery whose base SS_ID the
|
|
16815
|
+
* caller determined (draft-ramadan-moq-mmt-02 7.1, 7.4.4; joint decoding,
|
|
16816
|
+
* BLO-52085). It joins its block's decoder as a received source would, so
|
|
16817
|
+
* it reduces the erasures the block still has to solve, but it is not a
|
|
16818
|
+
* network arrival: no transport accounting, no classification evidence,
|
|
16819
|
+
* no deadline refresh, and it never makes a block clean.
|
|
16820
|
+
*
|
|
16821
|
+
* Only a block a repair stated takes one (a repair-less span has no
|
|
16822
|
+
* decoder block). A block that is terminal, or already holds that source,
|
|
16823
|
+
* does not. The symbol is the exact protected packet, unpadded, as
|
|
16824
|
+
* feedSourceSymbol takes it.
|
|
16825
|
+
*
|
|
16826
|
+
* `recovered` is the block the synchronous decoder completed with it; the
|
|
16827
|
+
* caller routes it and completes the block, as for any recovery. When
|
|
16828
|
+
* every source of the block is then present, received or rebuilt, and no
|
|
16829
|
+
* decoder returned it (a Worker answers asynchronously, and reports such a
|
|
16830
|
+
* block as clean on its own side), the block is completed here as
|
|
16831
|
+
* recovered, with nothing left to route.
|
|
16832
|
+
*/
|
|
16833
|
+
feedRebuiltSourceSymbol(packetId, ssId, symbolData, catalogTrackName) {
|
|
16834
|
+
if (!this.#hasBlockProfile(packetId)) return NOT_TAKEN;
|
|
16835
|
+
const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
|
|
16836
|
+
if (located === null || located.block.span) return NOT_TAKEN;
|
|
16837
|
+
if (!ssbg1FitsSymbol(symbolData.byteLength, this.#trackConfig(packetId).symbolSize)) return NOT_TAKEN;
|
|
16838
|
+
const sbn = located.block.start;
|
|
16839
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
16840
|
+
if (this.#completedBlocks.has(key) || this.#blockSourceEsis.get(key)?.has(located.esi)) return NOT_TAKEN;
|
|
16841
|
+
let rebuilt = this.#blockRebuiltEsis.get(key);
|
|
16842
|
+
if (rebuilt?.has(located.esi)) return NOT_TAKEN;
|
|
16843
|
+
if (rebuilt === void 0) {
|
|
16844
|
+
rebuilt = /* @__PURE__ */ new Set();
|
|
16845
|
+
this.#blockRebuiltEsis.set(key, rebuilt);
|
|
16846
|
+
}
|
|
16847
|
+
const config = this.#trackConfigs.get(packetId);
|
|
16848
|
+
const wasShort = config !== void 0 && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) !== void 0;
|
|
16849
|
+
rebuilt.add(located.esi);
|
|
16850
|
+
const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
|
|
16851
|
+
if (recovered === null && !this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName)) {
|
|
16852
|
+
if (wasShort && this.#activeBlocks.has(key) && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) === void 0) {
|
|
16853
|
+
this.#armBlockTimeout(sbn, packetId, catalogTrackName, this.#multicastWaitMs(config));
|
|
16854
|
+
}
|
|
16855
|
+
}
|
|
16856
|
+
return { taken: true, recovered };
|
|
16857
|
+
}
|
|
16858
|
+
/**
|
|
16859
|
+
* A block that holds every one of its `length` sources, received or
|
|
16860
|
+
* rebuilt by another instance, with at least one rebuilt, is recovered:
|
|
16861
|
+
* every packet of it was delivered, by its own arrival or by the instance
|
|
16862
|
+
* that rebuilt it, so nothing is routed. Returns whether it completed it.
|
|
16863
|
+
*/
|
|
16864
|
+
#completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
|
|
16865
|
+
const rebuilt = this.#blockRebuiltEsis.get(key);
|
|
16866
|
+
if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
|
|
16867
|
+
let present = rebuilt.size;
|
|
16868
|
+
for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
|
|
16869
|
+
if (present < length) return false;
|
|
16870
|
+
this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
|
|
16871
|
+
this.countRecovery(packetId, sbn, catalogTrackName);
|
|
16872
|
+
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
16873
|
+
if (catalogTrackName === void 0) {
|
|
16874
|
+
this.onBlockComplete?.(sbn, packetId);
|
|
16875
|
+
} else {
|
|
16876
|
+
this.onBlockComplete?.(sbn, packetId, catalogTrackName);
|
|
16877
|
+
}
|
|
16878
|
+
return true;
|
|
16879
|
+
}
|
|
16744
16880
|
/**
|
|
16745
16881
|
* fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
|
|
16746
16882
|
* exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
|
|
@@ -16943,8 +17079,75 @@ var FECManager = class _FECManager {
|
|
|
16943
17079
|
this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
|
|
16944
17080
|
return null;
|
|
16945
17081
|
}
|
|
17082
|
+
if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
|
|
16946
17083
|
return recovered;
|
|
16947
17084
|
}
|
|
17085
|
+
/**
|
|
17086
|
+
* A first-arrival multicast source, held or placed. A datagram flow keeps
|
|
17087
|
+
* its sources in send order, so one that moves the stream's newest source
|
|
17088
|
+
* position past a block leaves the sources that block still misses
|
|
17089
|
+
* reordered behind it at best: it shortens the wait of every block it
|
|
17090
|
+
* passed that only a late source can complete. MoQ is left out: its streams
|
|
17091
|
+
* can overtake one another, so a MoQ source shows nothing about the sources
|
|
17092
|
+
* sent before it.
|
|
17093
|
+
*/
|
|
17094
|
+
#noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
|
|
17095
|
+
const previous = locator.newestSource;
|
|
17096
|
+
if (!locator.noteSource(ssId) || previous === void 0) return;
|
|
17097
|
+
for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
|
|
17098
|
+
if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
|
|
17099
|
+
this.#shortenUnrecoverableWait(
|
|
17100
|
+
this.#blockKey(block.start, packetId, catalogTrackName),
|
|
17101
|
+
block.start,
|
|
17102
|
+
packetId,
|
|
17103
|
+
catalogTrackName
|
|
17104
|
+
);
|
|
17105
|
+
}
|
|
17106
|
+
}
|
|
17107
|
+
/**
|
|
17108
|
+
* The wait of a block that only a late source can complete (BLO-52085): a
|
|
17109
|
+
* repair stated it, its received sources plus every symbol its repair group
|
|
17110
|
+
* carries (RSB_length) are short of K' (the repair scheduler's
|
|
17111
|
+
* group-cannot-complete), and a later source of the stream has arrived, so
|
|
17112
|
+
* a source it misses is lost or reordered behind that one. The span, which
|
|
17113
|
+
* covers repair symbols paced after its sources, buys it nothing: it waits
|
|
17114
|
+
* the late-symbol allowance alone, the multicast wait less the span (the
|
|
17115
|
+
* catalog tolerance or the measured delivery gap). Undefined for any other
|
|
17116
|
+
* block.
|
|
17117
|
+
*/
|
|
17118
|
+
#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
|
|
17119
|
+
const authoredLength = this.#blockAuthoredLengths.get(key);
|
|
17120
|
+
const rsbLength = this.#blockRsbLengths.get(key);
|
|
17121
|
+
if (authoredLength === void 0 || rsbLength === void 0) return void 0;
|
|
17122
|
+
let present = this.#blockSourceCounts.get(key) ?? 0;
|
|
17123
|
+
const received = this.#blockSourceEsis.get(key);
|
|
17124
|
+
for (const esi of this.#blockRebuiltEsis.get(key) ?? []) if (!received?.has(esi)) present++;
|
|
17125
|
+
if (present + rsbLength >= authoredLength) return void 0;
|
|
17126
|
+
const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
|
|
17127
|
+
if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
|
|
17128
|
+
return void 0;
|
|
17129
|
+
}
|
|
17130
|
+
return this.#multicastWaitMs(config) - config.interleaveDepthMs;
|
|
17131
|
+
}
|
|
17132
|
+
/**
|
|
17133
|
+
* Bring an open block's wait in to its late-source wait, when it has one.
|
|
17134
|
+
* Never later than it is armed: only a source can complete such a block,
|
|
17135
|
+
* so a repair of its group (paced across the span) must not keep it open.
|
|
17136
|
+
*/
|
|
17137
|
+
#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
|
|
17138
|
+
if (!this.#activeBlocks.has(key)) return;
|
|
17139
|
+
const config = this.#trackConfigs.get(packetId);
|
|
17140
|
+
if (config === void 0) return;
|
|
17141
|
+
const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
|
|
17142
|
+
if (waitMs === void 0) return;
|
|
17143
|
+
const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
|
|
17144
|
+
this.#armBlockTimeout(
|
|
17145
|
+
sbn,
|
|
17146
|
+
packetId,
|
|
17147
|
+
catalogTrackName,
|
|
17148
|
+
armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
|
|
17149
|
+
);
|
|
17150
|
+
}
|
|
16948
17151
|
/**
|
|
16949
17152
|
* Record a block's authored SSB_length from a repair symbol. Returns false,
|
|
16950
17153
|
* leaving the block unchanged, when the symbol contradicts the block: a
|
|
@@ -17099,6 +17302,15 @@ var FECManager = class _FECManager {
|
|
|
17099
17302
|
this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
|
|
17100
17303
|
return;
|
|
17101
17304
|
}
|
|
17305
|
+
if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
|
|
17306
|
+
key,
|
|
17307
|
+
sbn,
|
|
17308
|
+
this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
|
|
17309
|
+
packetId,
|
|
17310
|
+
catalogTrackName
|
|
17311
|
+
)) {
|
|
17312
|
+
return;
|
|
17313
|
+
}
|
|
17102
17314
|
this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
|
|
17103
17315
|
}
|
|
17104
17316
|
#completeCleanBlock(key, sbn, packetId, catalogTrackName) {
|
|
@@ -17107,6 +17319,7 @@ var FECManager = class _FECManager {
|
|
|
17107
17319
|
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
17108
17320
|
this.#blockSourceCounts.delete(key);
|
|
17109
17321
|
this.#blockSourceEsis.delete(key);
|
|
17322
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17110
17323
|
this.#blockAuthoredLengths.delete(key);
|
|
17111
17324
|
this.#blockRepairRsIds.delete(key);
|
|
17112
17325
|
this.#blockRsbLengths.delete(key);
|
|
@@ -17173,7 +17386,16 @@ var FECManager = class _FECManager {
|
|
|
17173
17386
|
this.#repairDeadlineMs(trackConfig)
|
|
17174
17387
|
);
|
|
17175
17388
|
}
|
|
17176
|
-
this.#
|
|
17389
|
+
if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
|
|
17390
|
+
this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
|
|
17391
|
+
return;
|
|
17392
|
+
}
|
|
17393
|
+
this.#armBlockTimeout(
|
|
17394
|
+
sbn,
|
|
17395
|
+
packetId,
|
|
17396
|
+
catalogTrackName,
|
|
17397
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
|
|
17398
|
+
);
|
|
17177
17399
|
}
|
|
17178
17400
|
#failBlock(sbn, packetId, catalogTrackName) {
|
|
17179
17401
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
@@ -17186,6 +17408,7 @@ var FECManager = class _FECManager {
|
|
|
17186
17408
|
}
|
|
17187
17409
|
this.#blockSourceCounts.delete(key);
|
|
17188
17410
|
this.#blockSourceEsis.delete(key);
|
|
17411
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17189
17412
|
this.#blockAuthoredLengths.delete(key);
|
|
17190
17413
|
this.#blockRepairRsIds.delete(key);
|
|
17191
17414
|
this.#blockRsbLengths.delete(key);
|
|
@@ -17224,6 +17447,7 @@ var FECManager = class _FECManager {
|
|
|
17224
17447
|
this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
|
|
17225
17448
|
this.#blockSourceCounts.delete(key);
|
|
17226
17449
|
this.#blockSourceEsis.delete(key);
|
|
17450
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17227
17451
|
this.#blockAuthoredLengths.delete(key);
|
|
17228
17452
|
this.#blockRepairRsIds.delete(key);
|
|
17229
17453
|
this.#blockRsbLengths.delete(key);
|
|
@@ -17315,6 +17539,7 @@ var FECManager = class _FECManager {
|
|
|
17315
17539
|
this.#blockKeysBySbn.clear();
|
|
17316
17540
|
this.#blockSourceCounts.clear();
|
|
17317
17541
|
this.#blockSourceEsis.clear();
|
|
17542
|
+
this.#blockRebuiltEsis.clear();
|
|
17318
17543
|
this.#blockAuthoredLengths.clear();
|
|
17319
17544
|
this.#blockRepairRsIds.clear();
|
|
17320
17545
|
this.#blockRsbLengths.clear();
|
|
@@ -17340,6 +17565,7 @@ var FECManager = class _FECManager {
|
|
|
17340
17565
|
this.#blockAnchorsMs.clear();
|
|
17341
17566
|
this.#blockCatalogTrackNames.clear();
|
|
17342
17567
|
this.#classifiers.clear();
|
|
17568
|
+
this.#noSpanRaps.clear();
|
|
17343
17569
|
this.#streamGeometries.clear();
|
|
17344
17570
|
this.#failedBlocks.clear();
|
|
17345
17571
|
this.#resolvedBlockClasses.clear();
|
|
@@ -17870,9 +18096,21 @@ var IsoBmffTimingResolver = class {
|
|
|
17870
18096
|
}
|
|
17871
18097
|
#track = null;
|
|
17872
18098
|
#fragments = /* @__PURE__ */ new Map();
|
|
18099
|
+
/** First sample of each pending fragment, kept in `#delivered` with its sample count. */
|
|
18100
|
+
#firstSamples = /* @__PURE__ */ new Map();
|
|
18101
|
+
/**
|
|
18102
|
+
* First sample and sample count of the newest fragments whose every slot was
|
|
18103
|
+
* cleared. Abandonment counts: `releaseSample` clears a slot too, which
|
|
18104
|
+
* matches the combiner, since it retires an abandoned sample as well.
|
|
18105
|
+
*/
|
|
18106
|
+
#delivered = /* @__PURE__ */ new Map();
|
|
17873
18107
|
#pendingSamples = 0;
|
|
17874
18108
|
#evictedSamples = 0;
|
|
17875
18109
|
#lastMovieFragmentRejection = null;
|
|
18110
|
+
/** True once an FT=0 has bound the track that FT=1 timing is read against. */
|
|
18111
|
+
get initialized() {
|
|
18112
|
+
return this.#track !== null;
|
|
18113
|
+
}
|
|
17876
18114
|
get pendingSampleCount() {
|
|
17877
18115
|
return this.#pendingSamples;
|
|
17878
18116
|
}
|
|
@@ -17896,7 +18134,14 @@ var IsoBmffTimingResolver = class {
|
|
|
17896
18134
|
this.#track = timing;
|
|
17897
18135
|
return true;
|
|
17898
18136
|
}
|
|
17899
|
-
|
|
18137
|
+
/**
|
|
18138
|
+
* `recovered` marks an FEC-recovered copy. A recovered block hands back
|
|
18139
|
+
* every FT=1 it holds, the received ones included, so a recovered copy of a
|
|
18140
|
+
* fragment whose samples are all done is a duplicate: admitting it again
|
|
18141
|
+
* would charge stale timing and could displace the live MPU's. A source
|
|
18142
|
+
* copy is still admitted again (a rendition switch re-arms timing that way).
|
|
18143
|
+
*/
|
|
18144
|
+
addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
|
|
17900
18145
|
this.#lastMovieFragmentRejection = null;
|
|
17901
18146
|
if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
|
|
17902
18147
|
this.#lastMovieFragmentRejection = "invalid";
|
|
@@ -17922,6 +18167,8 @@ var IsoBmffTimingResolver = class {
|
|
|
17922
18167
|
(sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
|
|
17923
18168
|
);
|
|
17924
18169
|
}
|
|
18170
|
+
const delivered = recovered ? this.#delivered.get(key) : void 0;
|
|
18171
|
+
if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
|
|
17925
18172
|
let freed = 0;
|
|
17926
18173
|
while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
|
|
17927
18174
|
freed += this.#evictOldestFragment();
|
|
@@ -17935,6 +18182,7 @@ var IsoBmffTimingResolver = class {
|
|
|
17935
18182
|
return false;
|
|
17936
18183
|
}
|
|
17937
18184
|
this.#fragments.set(key, [...parsed.samples]);
|
|
18185
|
+
this.#firstSamples.set(key, parsed.samples[0]);
|
|
17938
18186
|
this.#pendingSamples += parsed.samples.length;
|
|
17939
18187
|
return true;
|
|
17940
18188
|
}
|
|
@@ -17992,6 +18240,7 @@ var IsoBmffTimingResolver = class {
|
|
|
17992
18240
|
let live = 0;
|
|
17993
18241
|
for (const sample of samples) if (sample !== null) live++;
|
|
17994
18242
|
this.#fragments.delete(oldest);
|
|
18243
|
+
this.#firstSamples.delete(oldest);
|
|
17995
18244
|
this.#pendingSamples -= live;
|
|
17996
18245
|
this.#evictedSamples += live;
|
|
17997
18246
|
return live;
|
|
@@ -18007,12 +18256,23 @@ var IsoBmffTimingResolver = class {
|
|
|
18007
18256
|
if (!samples || !samples[index]) return false;
|
|
18008
18257
|
samples[index] = null;
|
|
18009
18258
|
this.#pendingSamples--;
|
|
18010
|
-
if (samples.every((value) => value === null))
|
|
18259
|
+
if (samples.every((value) => value === null)) {
|
|
18260
|
+
this.#fragments.delete(key);
|
|
18261
|
+
const first = this.#firstSamples.get(key);
|
|
18262
|
+
this.#firstSamples.delete(key);
|
|
18263
|
+
this.#delivered.delete(key);
|
|
18264
|
+
this.#delivered.set(key, { first, sampleCount: samples.length });
|
|
18265
|
+
if (this.#delivered.size > this.maxPendingSamples) {
|
|
18266
|
+
this.#delivered.delete(this.#delivered.keys().next().value);
|
|
18267
|
+
}
|
|
18268
|
+
}
|
|
18011
18269
|
return true;
|
|
18012
18270
|
}
|
|
18013
18271
|
reset() {
|
|
18014
18272
|
this.#track = null;
|
|
18015
18273
|
this.#fragments.clear();
|
|
18274
|
+
this.#firstSamples.clear();
|
|
18275
|
+
this.#delivered.clear();
|
|
18016
18276
|
this.#pendingSamples = 0;
|
|
18017
18277
|
}
|
|
18018
18278
|
};
|
|
@@ -18494,6 +18754,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18494
18754
|
this.#reclaimedTimingSamples = 0;
|
|
18495
18755
|
this.#evictedTimingSamples = 0;
|
|
18496
18756
|
this.#rejectedMovieFragments = 0;
|
|
18757
|
+
this.#recoveredHeadsAwaitingTiming = {
|
|
18758
|
+
held: 0,
|
|
18759
|
+
expired: 0,
|
|
18760
|
+
discarded: 0,
|
|
18761
|
+
superseded: 0
|
|
18762
|
+
};
|
|
18497
18763
|
this.#sequencers = /* @__PURE__ */ new Map();
|
|
18498
18764
|
this.#fecGate = null;
|
|
18499
18765
|
this.#fecSequencer = null;
|
|
@@ -18602,6 +18868,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18602
18868
|
#reclaimedTimingSamples;
|
|
18603
18869
|
#evictedTimingSamples;
|
|
18604
18870
|
#rejectedMovieFragments;
|
|
18871
|
+
#recoveredHeadsAwaitingTiming;
|
|
18605
18872
|
#sequencers;
|
|
18606
18873
|
#fecGate;
|
|
18607
18874
|
#fecSequencer;
|
|
@@ -18707,11 +18974,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18707
18974
|
/**
|
|
18708
18975
|
* Typed view of the ISO-BMFF timing-sample budget.
|
|
18709
18976
|
*
|
|
18710
|
-
*
|
|
18711
|
-
*
|
|
18712
|
-
*
|
|
18713
|
-
*
|
|
18714
|
-
*
|
|
18977
|
+
* Each stream admits timing against its own window of `capacity` slots
|
|
18978
|
+
* (BLO-52085). A stream at capacity displaces its own oldest timing to
|
|
18979
|
+
* admit the moof in hand, and `evicted` counts the slots so displaced:
|
|
18980
|
+
* timing a stream declared and never delivered shows up there. `pending`
|
|
18981
|
+
* sums every stream, so it can exceed `capacity`. Per repo policy this is
|
|
18982
|
+
* a producer-owned counter -- never infer budget state by parsing logs.
|
|
18715
18983
|
*/
|
|
18716
18984
|
get timingBudgetStats() {
|
|
18717
18985
|
return {
|
|
@@ -18722,6 +18990,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18722
18990
|
rejectedMovieFragments: this.#rejectedMovieFragments
|
|
18723
18991
|
};
|
|
18724
18992
|
}
|
|
18993
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
18994
|
+
get recoveredHeadsAwaitingTiming() {
|
|
18995
|
+
let pending = 0;
|
|
18996
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
18997
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++;
|
|
18998
|
+
}
|
|
18999
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending };
|
|
19000
|
+
}
|
|
18725
19001
|
get reorderStats() {
|
|
18726
19002
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
|
|
18727
19003
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
|
|
@@ -18850,10 +19126,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18850
19126
|
get reassemblerDataUnitsReturned() {
|
|
18851
19127
|
return this.#reassemblerDataUnitsReturned;
|
|
18852
19128
|
}
|
|
18853
|
-
#countOutcome(
|
|
18854
|
-
const count = (this.#feedOutcomes.get(
|
|
18855
|
-
this.#feedOutcomes.set(
|
|
18856
|
-
this.#lastOutcome =
|
|
19129
|
+
#countOutcome(token2) {
|
|
19130
|
+
const count = (this.#feedOutcomes.get(token2) ?? 0) + 1;
|
|
19131
|
+
this.#feedOutcomes.set(token2, count);
|
|
19132
|
+
this.#lastOutcome = token2;
|
|
18857
19133
|
this.#lastOutcomeCount = count;
|
|
18858
19134
|
}
|
|
18859
19135
|
#countDeferredOutcome(reason) {
|
|
@@ -18895,7 +19171,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18895
19171
|
const now = Date.now();
|
|
18896
19172
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
18897
19173
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
|
|
18898
|
-
awaitingTiming.shift();
|
|
19174
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
18899
19175
|
}
|
|
18900
19176
|
}
|
|
18901
19177
|
}
|
|
@@ -19013,11 +19289,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19013
19289
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
19014
19290
|
return this.#drop("fragment-unexpected-fragmentation");
|
|
19015
19291
|
const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
|
|
19016
|
-
if (!resolver2) return this.#drop("fragment-no-resolver");
|
|
19292
|
+
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
19017
19293
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
19018
19294
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
19019
|
-
const remaining = this.#maxPendingTimingSamples -
|
|
19020
|
-
const admitted = resolver2.addMovieFragment(
|
|
19295
|
+
const remaining = this.#maxPendingTimingSamples - resolver2.pendingSampleCount;
|
|
19296
|
+
const admitted = resolver2.addMovieFragment(
|
|
19297
|
+
mpu.mpuSequenceNumber,
|
|
19298
|
+
data,
|
|
19299
|
+
remaining,
|
|
19300
|
+
options?.recovered === true
|
|
19301
|
+
);
|
|
19021
19302
|
this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
|
|
19022
19303
|
this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
|
|
19023
19304
|
if (!admitted) {
|
|
@@ -19037,7 +19318,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19037
19318
|
};
|
|
19038
19319
|
}
|
|
19039
19320
|
if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
|
|
19040
|
-
const resolver = this.#timingResolver(streamKey, info.packetId,
|
|
19321
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true);
|
|
19041
19322
|
if (!resolver) return this.#drop("mfu-no-resolver");
|
|
19042
19323
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
19043
19324
|
return this.#drop("mfu-empty-payload");
|
|
@@ -19058,6 +19339,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19058
19339
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
19059
19340
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
19060
19341
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
19342
|
+
sequenceNumber: info.sequenceNumber,
|
|
19061
19343
|
arrivedAt: Date.now()
|
|
19062
19344
|
});
|
|
19063
19345
|
return this.#drop("mfu-sample-timing-missing");
|
|
@@ -19381,6 +19663,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19381
19663
|
}
|
|
19382
19664
|
this.#emitFrame(data, meta);
|
|
19383
19665
|
}
|
|
19666
|
+
/** Whether FT=1 metadata already names this sample, without claiming it. */
|
|
19667
|
+
hasAuthoredTimingFor(packetId, mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
|
|
19668
|
+
return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber) != null;
|
|
19669
|
+
}
|
|
19384
19670
|
/**
|
|
19385
19671
|
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
19386
19672
|
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
@@ -19442,6 +19728,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19442
19728
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
|
|
19443
19729
|
this.#sequencers.clear();
|
|
19444
19730
|
this.#fecSequencer?.discard();
|
|
19731
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
19732
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19733
|
+
}
|
|
19445
19734
|
this.#timingStreams.clear();
|
|
19446
19735
|
this.#pendingTimingSamples = 0;
|
|
19447
19736
|
this.#reliableMoqStartupGate.clear();
|
|
@@ -19460,10 +19749,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19460
19749
|
return resolver;
|
|
19461
19750
|
}
|
|
19462
19751
|
#awaitTiming(streamKey, head) {
|
|
19463
|
-
const
|
|
19464
|
-
if (!
|
|
19752
|
+
const state = this.#timingStreams.get(streamKey);
|
|
19753
|
+
if (!state) return;
|
|
19754
|
+
if (!head.options?.recovered) {
|
|
19755
|
+
const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
|
|
19756
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
|
|
19757
|
+
state.awaitingTiming = kept;
|
|
19758
|
+
}
|
|
19759
|
+
const awaitingTiming = state.awaitingTiming;
|
|
19465
19760
|
awaitingTiming.push(head);
|
|
19466
|
-
if (
|
|
19761
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
|
|
19762
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
19763
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
19764
|
+
}
|
|
19765
|
+
}
|
|
19766
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
19767
|
+
#leaveAwaitingTiming(head, outcome) {
|
|
19768
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
|
|
19467
19769
|
}
|
|
19468
19770
|
/**
|
|
19469
19771
|
* Feed again the heads whose timing the FT=1 just admitted declares. That
|
|
@@ -19491,6 +19793,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19491
19793
|
const state = this.#timingStreams.get(streamKey);
|
|
19492
19794
|
if (!state) return;
|
|
19493
19795
|
this.#timingStreams.delete(streamKey);
|
|
19796
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19494
19797
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
|
|
19495
19798
|
if (this.#pendingTimingSamples < 0) {
|
|
19496
19799
|
throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
|
|
@@ -22697,6 +23000,16 @@ function keyframeOverlaySsId(header) {
|
|
|
22697
23000
|
if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
|
|
22698
23001
|
return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
|
|
22699
23002
|
}
|
|
23003
|
+
function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
|
|
23004
|
+
if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
|
|
23005
|
+
return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
|
|
23006
|
+
}
|
|
23007
|
+
function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
|
|
23008
|
+
if (offset === void 0 || offset !== unstampedOffset) return void 0;
|
|
23009
|
+
return headerPsn + offset >>> 0;
|
|
23010
|
+
}
|
|
23011
|
+
var OVERLAY_ROUTED_LIMIT = 1024;
|
|
23012
|
+
var OVERLAY_PENDING_LIMIT = 256;
|
|
22700
23013
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
22701
23014
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
22702
23015
|
function assertStreamIdentity(role, stream) {
|
|
@@ -22808,6 +23121,7 @@ var MmtpRouter = class {
|
|
|
22808
23121
|
#provenance = /* @__PURE__ */ new Map();
|
|
22809
23122
|
#onFlowProtected = null;
|
|
22810
23123
|
#unadmittedListedDrops = 0;
|
|
23124
|
+
#admittedSourceDatagrams = 0;
|
|
22811
23125
|
#unlistedRepairSetAside = 0;
|
|
22812
23126
|
#assistBlocks = /* @__PURE__ */ new Map();
|
|
22813
23127
|
#assistFailures = /* @__PURE__ */ new Map();
|
|
@@ -22864,6 +23178,15 @@ var MmtpRouter = class {
|
|
|
22864
23178
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
22865
23179
|
*/
|
|
22866
23180
|
#ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
23181
|
+
/**
|
|
23182
|
+
* Per stream, the SS_ID offset at which a base recovery last showed the
|
|
23183
|
+
* sender's own packet_sequence_number: a recovered packet's header psn
|
|
23184
|
+
* equal to its SS_ID minus that offset. While it equals the stream's
|
|
23185
|
+
* current offset, no hop re-stamps the stream, and a packet's protected psn
|
|
23186
|
+
* names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
|
|
23187
|
+
* an MPU no member of which arrived).
|
|
23188
|
+
*/
|
|
23189
|
+
#unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
22867
23190
|
/**
|
|
22868
23191
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
22869
23192
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -22933,7 +23256,9 @@ var MmtpRouter = class {
|
|
|
22933
23256
|
recovered: 0,
|
|
22934
23257
|
restarts: 0,
|
|
22935
23258
|
unsequencedDropped: 0,
|
|
22936
|
-
protectedDropped: 0
|
|
23259
|
+
protectedDropped: 0,
|
|
23260
|
+
baseFed: 0,
|
|
23261
|
+
baseUndetermined: 0
|
|
22937
23262
|
};
|
|
22938
23263
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
22939
23264
|
#onDeltaBlockCompleteCb = null;
|
|
@@ -23106,7 +23431,7 @@ var MmtpRouter = class {
|
|
|
23106
23431
|
const cb = this.#getFrameCallback(packetId);
|
|
23107
23432
|
if (isVideo && !assembler) {
|
|
23108
23433
|
if (frameMeta.recovered) {
|
|
23109
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
23434
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
23110
23435
|
} else {
|
|
23111
23436
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
23112
23437
|
}
|
|
@@ -23496,6 +23821,10 @@ var MmtpRouter = class {
|
|
|
23496
23821
|
get keyframeOverlayStats() {
|
|
23497
23822
|
return { ...this.#overlayStats };
|
|
23498
23823
|
}
|
|
23824
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
23825
|
+
get recoveredHeadsAwaitingTiming() {
|
|
23826
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
23827
|
+
}
|
|
23499
23828
|
#isOverlayRepair(repairPacketId, repairTrackName) {
|
|
23500
23829
|
return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
|
|
23501
23830
|
}
|
|
@@ -23546,6 +23875,11 @@ var MmtpRouter = class {
|
|
|
23546
23875
|
*/
|
|
23547
23876
|
resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
|
|
23548
23877
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
23878
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
23879
|
+
if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
|
|
23880
|
+
stream.overlayToBase.routed.clear();
|
|
23881
|
+
stream.overlayToBase.pending.clear();
|
|
23882
|
+
}
|
|
23549
23883
|
}
|
|
23550
23884
|
#resetOverlay(inst) {
|
|
23551
23885
|
inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
|
|
@@ -23602,9 +23936,10 @@ var MmtpRouter = class {
|
|
|
23602
23936
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
23603
23937
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
23604
23938
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
23605
|
-
* included (7.1, 7.4.3).
|
|
23939
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
23940
|
+
* FEC reads it.
|
|
23606
23941
|
*/
|
|
23607
|
-
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
|
|
23942
|
+
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
|
|
23608
23943
|
if (this.#overlays.size === 0) return;
|
|
23609
23944
|
const ssId = keyframeOverlaySsId(header);
|
|
23610
23945
|
if (ssId === void 0) return;
|
|
@@ -23613,7 +23948,11 @@ var MmtpRouter = class {
|
|
|
23613
23948
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
23614
23949
|
if (identity !== null) {
|
|
23615
23950
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
23616
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
23951
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
23952
|
+
psnOffset: ssId - sequenceNumber >>> 0,
|
|
23953
|
+
overlaySsId: ssId,
|
|
23954
|
+
...baseSsId === void 0 ? {} : { baseSsId }
|
|
23955
|
+
});
|
|
23617
23956
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
|
|
23618
23957
|
}
|
|
23619
23958
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23692,14 +24031,27 @@ var MmtpRouter = class {
|
|
|
23692
24031
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
23693
24032
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
23694
24033
|
const restored = /* @__PURE__ */ new Set();
|
|
24034
|
+
const baseSsIds = /* @__PURE__ */ new Map();
|
|
23695
24035
|
const symbols = result.symbols.flatMap((sym) => {
|
|
23696
|
-
const
|
|
23697
|
-
if (
|
|
23698
|
-
const
|
|
24036
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
24037
|
+
if (anchor !== void 0) {
|
|
24038
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
24039
|
+
const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
|
|
23699
24040
|
restored.add(packetSequenceNumber);
|
|
23700
|
-
|
|
24041
|
+
const out = { ...sym, packetSequenceNumber };
|
|
24042
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
24043
|
+
if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
|
|
24044
|
+
return [out];
|
|
24045
|
+
}
|
|
24046
|
+
if (!restamped) {
|
|
24047
|
+
const baseSsId = unstampedBaseSsId(
|
|
24048
|
+
this.#ssIdSeqOffsetByStream.get(streamKey),
|
|
24049
|
+
this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
|
|
24050
|
+
sym.packetSequenceNumber
|
|
24051
|
+
);
|
|
24052
|
+
if (baseSsId !== void 0) baseSsIds.set(sym, baseSsId);
|
|
24053
|
+
return [sym];
|
|
23701
24054
|
}
|
|
23702
|
-
if (!restamped) return [sym];
|
|
23703
24055
|
this.#overlayStats.unsequencedDropped++;
|
|
23704
24056
|
return [];
|
|
23705
24057
|
});
|
|
@@ -23708,6 +24060,85 @@ var MmtpRouter = class {
|
|
|
23708
24060
|
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
23709
24061
|
});
|
|
23710
24062
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
24063
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
|
|
24064
|
+
}
|
|
24065
|
+
/**
|
|
24066
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
24067
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
24068
|
+
* from a received member of its MPU, else unstampedBaseSsId),
|
|
24069
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
24070
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
24071
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
24072
|
+
* A base block no repair has stated yet takes it when one does
|
|
24073
|
+
* (#feedPendingRebuilt).
|
|
24074
|
+
*
|
|
24075
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
24076
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
24077
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
24078
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
24079
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
24080
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
24081
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
24082
|
+
* block's own arrivals cannot show.
|
|
24083
|
+
*/
|
|
24084
|
+
#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
|
|
24085
|
+
const fec = this.#fecManager;
|
|
24086
|
+
const owner = inst.leg.sourcePacketId;
|
|
24087
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
24088
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
24089
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
24090
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
24091
|
+
const feeds = [];
|
|
24092
|
+
for (const sym of symbols) {
|
|
24093
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
24094
|
+
const ssId = baseSsIds.get(sym);
|
|
24095
|
+
if (ssId === void 0) {
|
|
24096
|
+
this.#overlayStats.baseUndetermined++;
|
|
24097
|
+
continue;
|
|
24098
|
+
}
|
|
24099
|
+
routed.delete(ssId);
|
|
24100
|
+
routed.add(ssId);
|
|
24101
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
|
|
24102
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
24103
|
+
}
|
|
24104
|
+
for (const [ssId, symbolData] of feeds) {
|
|
24105
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24106
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24107
|
+
}
|
|
24108
|
+
}
|
|
24109
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
24110
|
+
#feedRebuiltToBase(owner, name, ssId, symbolData) {
|
|
24111
|
+
const fec = this.#fecManager;
|
|
24112
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
24113
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
24114
|
+
if (coords == null) {
|
|
24115
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
24116
|
+
pending.delete(ssId);
|
|
24117
|
+
pending.set(ssId, symbolData.slice());
|
|
24118
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
|
|
24119
|
+
return;
|
|
24120
|
+
}
|
|
24121
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
24122
|
+
if (!fed.taken) return;
|
|
24123
|
+
this.#overlayStats.baseFed++;
|
|
24124
|
+
if (fed.recovered === null) return;
|
|
24125
|
+
const sbn = coords.sbn;
|
|
24126
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
24127
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
24128
|
+
fec.countRecovery(owner, sbn, name);
|
|
24129
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
24130
|
+
});
|
|
24131
|
+
}
|
|
24132
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
24133
|
+
#feedPendingRebuilt(owner, name, block) {
|
|
24134
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
24135
|
+
if (pending === void 0 || pending.size === 0) return;
|
|
24136
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
24137
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24138
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
24139
|
+
pending.delete(ssId);
|
|
24140
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24141
|
+
}
|
|
23711
24142
|
}
|
|
23712
24143
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
23713
24144
|
* the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
|
|
@@ -23847,6 +24278,10 @@ var MmtpRouter = class {
|
|
|
23847
24278
|
get unadmittedListedDropCount() {
|
|
23848
24279
|
return this.#unadmittedListedDrops;
|
|
23849
24280
|
}
|
|
24281
|
+
/** 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. */
|
|
24282
|
+
get admittedSourceDatagramCount() {
|
|
24283
|
+
return this.#admittedSourceDatagrams;
|
|
24284
|
+
}
|
|
23850
24285
|
/** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
|
|
23851
24286
|
get unlistedRepairSetAsideCount() {
|
|
23852
24287
|
return this.#unlistedRepairSetAside;
|
|
@@ -24333,6 +24768,8 @@ var MmtpRouter = class {
|
|
|
24333
24768
|
)) return;
|
|
24334
24769
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
24335
24770
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
24771
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
24772
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
24336
24773
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
24337
24774
|
const recovered = fec.feedRepairSymbol(
|
|
24338
24775
|
ownerPacketId,
|
|
@@ -24470,6 +24907,7 @@ var MmtpRouter = class {
|
|
|
24470
24907
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
24471
24908
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
24472
24909
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
24910
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
24473
24911
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
24474
24912
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
24475
24913
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -25104,6 +25542,7 @@ var MmtpRouter = class {
|
|
|
25104
25542
|
const parsed = this.#parser.parsePacket(packet);
|
|
25105
25543
|
if (!parsed) return;
|
|
25106
25544
|
const info = parsed.info;
|
|
25545
|
+
if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
|
|
25107
25546
|
if (source === "multicast" && !manifestAdmitted) {
|
|
25108
25547
|
this.#fecManager?.noteMulticastArrival?.(info.packetId, info.sequenceNumber);
|
|
25109
25548
|
}
|
|
@@ -25385,7 +25824,8 @@ var MmtpRouter = class {
|
|
|
25385
25824
|
parsed.header,
|
|
25386
25825
|
info.sequenceNumber,
|
|
25387
25826
|
info.rapFlag,
|
|
25388
|
-
source
|
|
25827
|
+
source,
|
|
25828
|
+
fecPayloadId?.ssId
|
|
25389
25829
|
);
|
|
25390
25830
|
}
|
|
25391
25831
|
}
|
|
@@ -25535,7 +25975,8 @@ var MmtpRouter = class {
|
|
|
25535
25975
|
if (stream === void 0) {
|
|
25536
25976
|
stream = {
|
|
25537
25977
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
25538
|
-
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
|
|
25978
|
+
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
25979
|
+
overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
|
|
25539
25980
|
};
|
|
25540
25981
|
byName.set(catalogTrackName, stream);
|
|
25541
25982
|
}
|
|
@@ -25650,8 +26091,16 @@ var MmtpRouter = class {
|
|
|
25650
26091
|
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
25651
26092
|
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
25652
26093
|
}
|
|
25653
|
-
#routeDirectRecoveredVideo(packetId, data, meta, cb) {
|
|
26094
|
+
#routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
|
|
25654
26095
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
26096
|
+
if (this.#isFecTrack(packetId)) {
|
|
26097
|
+
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
26098
|
+
if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
|
|
26099
|
+
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
26100
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
26101
|
+
return;
|
|
26102
|
+
}
|
|
26103
|
+
}
|
|
25655
26104
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
25656
26105
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
25657
26106
|
this.#drainDirectRecoveredVideo(
|
|
@@ -25805,6 +26254,13 @@ var MmtpRouter = class {
|
|
|
25805
26254
|
* frames by epoch only if pre-protection input ever matters.
|
|
25806
26255
|
*/
|
|
25807
26256
|
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
26257
|
+
const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
26258
|
+
if (overlayRouted !== void 0 && overlayRouted.size > 0) {
|
|
26259
|
+
result = {
|
|
26260
|
+
...result,
|
|
26261
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
|
|
26262
|
+
};
|
|
26263
|
+
}
|
|
25808
26264
|
const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
25809
26265
|
const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
|
|
25810
26266
|
const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
@@ -25869,7 +26325,10 @@ var MmtpRouter = class {
|
|
|
25869
26325
|
...result,
|
|
25870
26326
|
symbols: result.symbols.map((sym) => {
|
|
25871
26327
|
const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
|
|
25872
|
-
if (wire === sym.packetSequenceNumber)
|
|
26328
|
+
if (wire === sym.packetSequenceNumber) {
|
|
26329
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
26330
|
+
return sym;
|
|
26331
|
+
}
|
|
25873
26332
|
this.#restampedRecoveredSeqs++;
|
|
25874
26333
|
this.#restampedStreams.add(streamKey);
|
|
25875
26334
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -25905,6 +26364,29 @@ var MmtpRouter = class {
|
|
|
25905
26364
|
}
|
|
25906
26365
|
);
|
|
25907
26366
|
}
|
|
26367
|
+
const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
|
|
26368
|
+
ownerPacketId,
|
|
26369
|
+
sym.fragment.mpuSequenceNumber,
|
|
26370
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
26371
|
+
sym.fragment.sampleNumber,
|
|
26372
|
+
catalogTrackName
|
|
26373
|
+
);
|
|
26374
|
+
for (const sym of result.symbols) {
|
|
26375
|
+
if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
26376
|
+
this.#feedViaParser(
|
|
26377
|
+
sym.rawSymbol,
|
|
26378
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
26379
|
+
void 0,
|
|
26380
|
+
{
|
|
26381
|
+
skipFecPayloadId: true,
|
|
26382
|
+
recovered: true,
|
|
26383
|
+
deliveryProvenance,
|
|
26384
|
+
...fecSbnOf(sym.index),
|
|
26385
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
26386
|
+
}
|
|
26387
|
+
);
|
|
26388
|
+
}
|
|
26389
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
25908
26390
|
const gated = this.#isFecTrack(ownerPacketId);
|
|
25909
26391
|
const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
|
|
25910
26392
|
if (assembler) {
|
|
@@ -29666,7 +30148,7 @@ async function setupFecWorkerForTrack(opts) {
|
|
|
29666
30148
|
blockProfile,
|
|
29667
30149
|
k: opts.fecK,
|
|
29668
30150
|
// P as this receiver joined it, not the catalog total: the worker snapshot
|
|
29669
|
-
// and CMCD `
|
|
30151
|
+
// and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
|
|
29670
30152
|
repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
|
|
29671
30153
|
symbolSize: opts.fecSymbolSize,
|
|
29672
30154
|
interleaveDepth: interleaveDepthBlocks,
|
|
@@ -30208,24 +30690,24 @@ var MediaRouter = class {
|
|
|
30208
30690
|
if (!this.#multicastVideoKeyed) return;
|
|
30209
30691
|
if (this.#requireAudio && !this.#multicastAudioReceived) return;
|
|
30210
30692
|
if (this.#requireAudio && !this.#moqAudioWarmed) return;
|
|
30211
|
-
const
|
|
30693
|
+
const token2 = ++this.#sourceSwitchToken;
|
|
30212
30694
|
const maybeReady = this.#onSourceSwitch?.("multicast");
|
|
30213
30695
|
if (!maybeReady || typeof maybeReady.then !== "function") {
|
|
30214
|
-
this.#commitMulticastActivation(
|
|
30696
|
+
this.#commitMulticastActivation(token2);
|
|
30215
30697
|
return;
|
|
30216
30698
|
}
|
|
30217
30699
|
const ready = Promise.resolve(maybeReady);
|
|
30218
|
-
this.#pendingMulticastToken =
|
|
30700
|
+
this.#pendingMulticastToken = token2;
|
|
30219
30701
|
this.#sourceSwitchReady = ready;
|
|
30220
30702
|
void ready.then(
|
|
30221
|
-
() => this.#commitMulticastActivation(
|
|
30222
|
-
(err) => this.#rejectMulticastActivation(
|
|
30703
|
+
() => this.#commitMulticastActivation(token2),
|
|
30704
|
+
(err) => this.#rejectMulticastActivation(token2, err)
|
|
30223
30705
|
).finally(() => {
|
|
30224
30706
|
if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
|
|
30225
30707
|
});
|
|
30226
30708
|
}
|
|
30227
|
-
#commitMulticastActivation(
|
|
30228
|
-
if (
|
|
30709
|
+
#commitMulticastActivation(token2) {
|
|
30710
|
+
if (token2 !== this.#sourceSwitchToken) return;
|
|
30229
30711
|
this.#active = "multicast";
|
|
30230
30712
|
this.#suppressed = true;
|
|
30231
30713
|
this.#pendingMulticastToken = null;
|
|
@@ -30236,9 +30718,9 @@ var MediaRouter = class {
|
|
|
30236
30718
|
if (video) this.#onVideoFrame?.(video.data, video.meta);
|
|
30237
30719
|
if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
|
|
30238
30720
|
}
|
|
30239
|
-
#rejectMulticastActivation(
|
|
30240
|
-
if (
|
|
30241
|
-
if (this.#pendingMulticastToken ===
|
|
30721
|
+
#rejectMulticastActivation(token2, err) {
|
|
30722
|
+
if (token2 !== this.#sourceSwitchToken) return;
|
|
30723
|
+
if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
|
|
30242
30724
|
this.#pendingMulticastVideo = null;
|
|
30243
30725
|
this.#pendingMulticastAudio = null;
|
|
30244
30726
|
console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
|
|
@@ -31207,63 +31689,65 @@ var FecStarvationDetector = class {
|
|
|
31207
31689
|
|
|
31208
31690
|
// src/cmcd-producer.ts
|
|
31209
31691
|
var PLAY_MODES = ["full", "base-only", "single-track"];
|
|
31210
|
-
|
|
31211
|
-
|
|
31212
|
-
|
|
31213
|
-
|
|
31214
|
-
|
|
31215
|
-
|
|
31216
|
-
|
|
31217
|
-
|
|
31218
|
-
|
|
31219
|
-
|
|
31220
|
-
|
|
31221
|
-
|
|
31222
|
-
|
|
31223
|
-
|
|
31224
|
-
|
|
31225
|
-
|
|
31226
|
-
|
|
31227
|
-
|
|
31228
|
-
}
|
|
31229
|
-
if (snap.multicastSource != null) {
|
|
31230
|
-
params["x-bc-multicast-source"] = snap.multicastSource;
|
|
31231
|
-
}
|
|
31232
|
-
if (snap.amtRelay != null) {
|
|
31233
|
-
params["x-bc-amt-relay"] = snap.amtRelay;
|
|
31234
|
-
}
|
|
31235
|
-
return params;
|
|
31236
|
-
}
|
|
31237
|
-
function formatQueryString(params) {
|
|
31238
|
-
return Object.keys(params).sort().map((key) => {
|
|
31239
|
-
const v = params[key];
|
|
31240
|
-
if (typeof v === "string") return `${key}=${sanitize(v)}`;
|
|
31241
|
-
return `${key}=${v}`;
|
|
31692
|
+
var DEFAULT_INTERVAL_MS = 3e4;
|
|
31693
|
+
var CUSTOM_PREFIX = "com.blockcast-";
|
|
31694
|
+
var MAX_CUSTOM_VALUE = 64;
|
|
31695
|
+
var TRANSPORT_TOKEN = {
|
|
31696
|
+
native: "ssm",
|
|
31697
|
+
amt: "amt",
|
|
31698
|
+
moq: "moq"
|
|
31699
|
+
};
|
|
31700
|
+
function sfString(value) {
|
|
31701
|
+
return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
|
|
31702
|
+
}
|
|
31703
|
+
function encodeCmcdRecord(record) {
|
|
31704
|
+
return Object.keys(record).sort().map((key) => {
|
|
31705
|
+
const value = record[key];
|
|
31706
|
+
if (typeof value === "number") return `${key}=${value}`;
|
|
31707
|
+
if (typeof value === "string") return `${key}=${sfString(value)}`;
|
|
31708
|
+
if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
|
|
31709
|
+
return `${key}=${value.token}`;
|
|
31242
31710
|
}).join(",");
|
|
31243
31711
|
}
|
|
31712
|
+
var token = (value) => ({ token: value });
|
|
31713
|
+
function putCustom(record, name, value) {
|
|
31714
|
+
if (value === void 0) return;
|
|
31715
|
+
if (typeof value === "number") value = String(value);
|
|
31716
|
+
if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
|
|
31717
|
+
record[CUSTOM_PREFIX + name] = value;
|
|
31718
|
+
}
|
|
31244
31719
|
var CmcdProducer = class {
|
|
31245
|
-
#onCmcdParams;
|
|
31246
31720
|
#onCmcdPost;
|
|
31721
|
+
#intervalMs;
|
|
31722
|
+
#sessionId;
|
|
31723
|
+
#now;
|
|
31724
|
+
#sn = 0;
|
|
31725
|
+
#lastHeartbeat = null;
|
|
31247
31726
|
#transport;
|
|
31248
31727
|
#lastStats;
|
|
31249
|
-
// Phase 14 state — Plan 03 Task 2 (D-12, D-15).
|
|
31250
31728
|
#playMode = null;
|
|
31251
31729
|
#deltaFailRate = null;
|
|
31252
31730
|
#switchMiscount = 0;
|
|
31253
31731
|
#manifestStats = null;
|
|
31254
31732
|
#fecFailClosed = null;
|
|
31733
|
+
/** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
|
|
31734
|
+
#lifecycle = null;
|
|
31255
31735
|
constructor(options) {
|
|
31256
|
-
this.#onCmcdParams = options.onCmcdParams;
|
|
31257
31736
|
this.#onCmcdPost = options.onCmcdPost;
|
|
31737
|
+
this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
31738
|
+
if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
|
|
31739
|
+
throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
|
|
31740
|
+
}
|
|
31741
|
+
this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
|
|
31742
|
+
this.#now = options.now ?? Date.now;
|
|
31258
31743
|
}
|
|
31259
31744
|
/** Update the stored transport state. Called by player wiring layer. */
|
|
31260
31745
|
updateTransport(snapshot) {
|
|
31261
31746
|
this.#transport = snapshot;
|
|
31262
31747
|
}
|
|
31263
31748
|
/**
|
|
31264
|
-
* Set the current play-mode
|
|
31265
|
-
*
|
|
31266
|
-
* (CLAUDE.md: error on bad init).
|
|
31749
|
+
* Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
|
|
31750
|
+
* value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
|
|
31267
31751
|
*/
|
|
31268
31752
|
updatePlayMode(mode) {
|
|
31269
31753
|
if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
|
|
@@ -31274,9 +31758,8 @@ var CmcdProducer = class {
|
|
|
31274
31758
|
this.#playMode = mode;
|
|
31275
31759
|
}
|
|
31276
31760
|
/**
|
|
31277
|
-
* Set the delta-track FEC block-failure rate (0..1 inclusive)
|
|
31278
|
-
* `
|
|
31279
|
-
* Omitted on single-track streams.
|
|
31761
|
+
* Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
|
|
31762
|
+
* `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
|
|
31280
31763
|
*/
|
|
31281
31764
|
updateDeltaFailRate(rate) {
|
|
31282
31765
|
if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
|
|
@@ -31286,112 +31769,139 @@ var CmcdProducer = class {
|
|
|
31286
31769
|
}
|
|
31287
31770
|
this.#deltaFailRate = rate;
|
|
31288
31771
|
}
|
|
31289
|
-
/**
|
|
31290
|
-
* Bump the cumulative switching-set misalignment counter. Emitted as
|
|
31291
|
-
* `x-bc-switch-miscount` on snapshots when the counter is > 0.
|
|
31292
|
-
*/
|
|
31772
|
+
/** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
|
|
31293
31773
|
incrementSwitchMiscount() {
|
|
31294
31774
|
this.#switchMiscount += 1;
|
|
31295
31775
|
}
|
|
31296
31776
|
/**
|
|
31297
|
-
* Record that FEC failed closed at runtime (fec-01 6.3.9).
|
|
31298
|
-
*
|
|
31299
|
-
* `
|
|
31300
|
-
* stream playing without FEC is told apart from one
|
|
31777
|
+
* Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
|
|
31778
|
+
* as `e=e` with the reason code in `ec`, and on every later counter report
|
|
31779
|
+
* as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
|
|
31780
|
+
* B_i was undercut), so a stream playing without FEC is told apart from one
|
|
31781
|
+
* with no loss.
|
|
31301
31782
|
*/
|
|
31302
31783
|
updateFecFailClosed(reason) {
|
|
31784
|
+
const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
|
|
31303
31785
|
this.#fecFailClosed = reason;
|
|
31786
|
+
if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
|
|
31304
31787
|
}
|
|
31305
31788
|
updateManifestVerification(stats) {
|
|
31306
31789
|
this.#manifestStats = stats;
|
|
31307
31790
|
}
|
|
31308
31791
|
/**
|
|
31309
|
-
*
|
|
31310
|
-
*
|
|
31311
|
-
*
|
|
31312
|
-
*
|
|
31792
|
+
* Count a video block's resolution from the repair lifecycle: recovered in
|
|
31793
|
+
* band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
|
|
31794
|
+
* as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
|
|
31795
|
+
* the video-track `-fbf`. Until the player feeds this, the three are absent.
|
|
31313
31796
|
*/
|
|
31797
|
+
onRepairLifecycle(event) {
|
|
31798
|
+
this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
|
|
31799
|
+
if (event.type === "recovered" && event.blockClass !== "audio") {
|
|
31800
|
+
if (event.via === "inband") this.#lifecycle.inband++;
|
|
31801
|
+
else this.#lifecycle.fetch++;
|
|
31802
|
+
} else if (event.type === "failed" && event.blockClass === "keyframe") {
|
|
31803
|
+
this.#lifecycle.failedKeyframe++;
|
|
31804
|
+
}
|
|
31805
|
+
}
|
|
31806
|
+
/** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
|
|
31314
31807
|
emitSwitchDrift(driftUs) {
|
|
31315
31808
|
if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
|
|
31316
31809
|
throw new Error(
|
|
31317
31810
|
`[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
|
|
31318
31811
|
);
|
|
31319
31812
|
}
|
|
31320
|
-
const
|
|
31321
|
-
|
|
31322
|
-
|
|
31323
|
-
if (this.#switchMiscount > 0) params["x-bc-switch-miscount"] = this.#switchMiscount;
|
|
31324
|
-
params["x-bc-switch-drift-us"] = driftUs;
|
|
31325
|
-
this.#emit(params);
|
|
31813
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
|
|
31814
|
+
putCustom(record, "sdu", Math.round(driftUs));
|
|
31815
|
+
this.#post([record]);
|
|
31326
31816
|
}
|
|
31327
|
-
/**
|
|
31328
|
-
* Process a FecTrackStats snapshot. Finds the video track, builds
|
|
31329
|
-
* x-bc-* vendor keys, and fires both delivery callbacks.
|
|
31330
|
-
*/
|
|
31817
|
+
/** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
|
|
31331
31818
|
onSnapshot(data) {
|
|
31332
31819
|
const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
|
|
31333
31820
|
if (!videoTrack) return;
|
|
31334
31821
|
this.#lastStats = videoTrack;
|
|
31335
|
-
|
|
31336
|
-
...buildFecParams(videoTrack),
|
|
31337
|
-
...buildTransportParams(this.#transport)
|
|
31338
|
-
};
|
|
31339
|
-
if (this.#playMode !== null) {
|
|
31340
|
-
params["x-bc-play-mode"] = this.#playMode;
|
|
31341
|
-
}
|
|
31342
|
-
if (this.#fecFailClosed) {
|
|
31343
|
-
params["x-bc-fec-fail-closed"] = this.#fecFailClosed.code;
|
|
31344
|
-
params["x-bc-fec-fail-layer"] = this.#fecFailClosed.layer;
|
|
31345
|
-
}
|
|
31346
|
-
if (this.#playMode !== null && this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
|
|
31347
|
-
params["x-bc-delta-fail-rate"] = parseFloat(this.#deltaFailRate.toFixed(4));
|
|
31348
|
-
}
|
|
31349
|
-
if (this.#switchMiscount > 0) {
|
|
31350
|
-
params["x-bc-switch-miscount"] = this.#switchMiscount;
|
|
31351
|
-
}
|
|
31352
|
-
if (this.#manifestStats) {
|
|
31353
|
-
params["x-bc-manifest-verified"] = this.#manifestStats.verified > 0;
|
|
31354
|
-
params["x-bc-manifest-failures"] = Object.values(this.#manifestStats.failures).reduce((total, count) => total + count, 0);
|
|
31355
|
-
}
|
|
31356
|
-
this.#emit(params);
|
|
31822
|
+
this.#heartbeat();
|
|
31357
31823
|
}
|
|
31358
31824
|
/**
|
|
31359
|
-
*
|
|
31360
|
-
*
|
|
31825
|
+
* Take transport state for receivers that do not own FEC stats, and send the
|
|
31826
|
+
* heartbeat when due.
|
|
31361
31827
|
*/
|
|
31362
31828
|
onTransportSnapshot(data) {
|
|
31363
31829
|
if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
|
|
31364
31830
|
throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
|
|
31365
31831
|
}
|
|
31366
31832
|
if (data.playMode !== void 0) this.updatePlayMode(data.playMode);
|
|
31367
|
-
if (data.switchMiscount !== void 0
|
|
31368
|
-
|
|
31833
|
+
if (data.switchMiscount !== void 0) {
|
|
31834
|
+
if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
|
|
31835
|
+
throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
|
|
31836
|
+
}
|
|
31837
|
+
this.#switchMiscount = data.switchMiscount;
|
|
31369
31838
|
}
|
|
31370
31839
|
this.#transport = data.transport;
|
|
31371
|
-
|
|
31372
|
-
if (this.#playMode !== null) params["x-bc-play-mode"] = this.#playMode;
|
|
31373
|
-
const switchMiscount = data.switchMiscount ?? this.#switchMiscount;
|
|
31374
|
-
if (switchMiscount > 0) params["x-bc-switch-miscount"] = switchMiscount;
|
|
31375
|
-
this.#emit(params);
|
|
31840
|
+
this.#heartbeat();
|
|
31376
31841
|
}
|
|
31377
|
-
/**
|
|
31378
|
-
* Signal a transport transition event. Fires an immediate out-of-band
|
|
31379
|
-
* emit with x-bc-ct set. The subsequent onSnapshot() will NOT include
|
|
31380
|
-
* x-bc-ct (one-shot).
|
|
31381
|
-
*/
|
|
31842
|
+
/** Report a transport transition at once, as custom event `com.blockcast-ct`. */
|
|
31382
31843
|
onTransition(event) {
|
|
31383
|
-
const
|
|
31384
|
-
|
|
31385
|
-
|
|
31844
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
|
|
31845
|
+
putCustom(record, "ct", token(event));
|
|
31846
|
+
this.#putTransport(record);
|
|
31847
|
+
this.#post([record]);
|
|
31848
|
+
}
|
|
31849
|
+
#base(event) {
|
|
31850
|
+
return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
|
|
31851
|
+
}
|
|
31852
|
+
#heartbeat() {
|
|
31853
|
+
const now = this.#now();
|
|
31854
|
+
if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
|
|
31855
|
+
this.#lastHeartbeat = now;
|
|
31856
|
+
this.#post([this.#base("t"), this.#counters()]);
|
|
31857
|
+
}
|
|
31858
|
+
#putTransport(record) {
|
|
31859
|
+
const snap = this.#transport;
|
|
31860
|
+
if (!snap) return;
|
|
31861
|
+
putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
|
|
31862
|
+
if (snap.multicastGroup != null && snap.multicastSource != null) {
|
|
31863
|
+
putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
|
|
31864
|
+
}
|
|
31865
|
+
putCustom(record, "ar", snap.amtRelay);
|
|
31866
|
+
}
|
|
31867
|
+
/** The `e=ce` counter report. */
|
|
31868
|
+
#counters() {
|
|
31869
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
|
|
31870
|
+
const stats = this.#lastStats;
|
|
31871
|
+
if (stats) {
|
|
31872
|
+
putCustom(record, "fa", stats.config.algorithm);
|
|
31873
|
+
putCustom(record, "fk", stats.config.k);
|
|
31874
|
+
putCustom(record, "fp", stats.config.repairCount);
|
|
31875
|
+
putCustom(record, "pr", stats.sourceSymbols);
|
|
31876
|
+
putCustom(record, "pl", stats.sourcePacketsMissing);
|
|
31877
|
+
putCustom(record, "pu", stats.sourcePacketsUnrecovered);
|
|
31878
|
+
putCustom(record, "fbf", stats.blocksFailed);
|
|
31879
|
+
}
|
|
31880
|
+
if (this.#lifecycle) {
|
|
31881
|
+
putCustom(record, "fbr", this.#lifecycle.inband);
|
|
31882
|
+
putCustom(record, "fbq", this.#lifecycle.fetch);
|
|
31883
|
+
putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
|
|
31884
|
+
}
|
|
31885
|
+
this.#putTransport(record);
|
|
31886
|
+
if (this.#playMode !== null) {
|
|
31887
|
+
putCustom(record, "pm", token(this.#playMode));
|
|
31888
|
+
if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
|
|
31889
|
+
putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
|
|
31890
|
+
}
|
|
31386
31891
|
}
|
|
31387
|
-
|
|
31388
|
-
|
|
31389
|
-
|
|
31892
|
+
if (this.#fecFailClosed) {
|
|
31893
|
+
putCustom(record, "ffc", token(this.#fecFailClosed.code));
|
|
31894
|
+
putCustom(record, "ffl", this.#fecFailClosed.layer);
|
|
31895
|
+
}
|
|
31896
|
+
if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
|
|
31897
|
+
if (this.#manifestStats) {
|
|
31898
|
+
putCustom(record, "mv", this.#manifestStats.verified);
|
|
31899
|
+
putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
|
|
31900
|
+
}
|
|
31901
|
+
return record;
|
|
31390
31902
|
}
|
|
31391
|
-
|
|
31392
|
-
|
|
31393
|
-
this.#onCmcdParams?.(params);
|
|
31394
|
-
this.#onCmcdPost?.(formatQueryString(params));
|
|
31903
|
+
#post(records) {
|
|
31904
|
+
this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
|
|
31395
31905
|
}
|
|
31396
31906
|
};
|
|
31397
31907
|
|
|
@@ -31487,6 +31997,19 @@ function resolveRepairLayers(catalog, source) {
|
|
|
31487
31997
|
totalBaseRepairSymbols: binding.totalBaseRepairSymbols
|
|
31488
31998
|
};
|
|
31489
31999
|
}
|
|
32000
|
+
function valueAt(catalog, pointer) {
|
|
32001
|
+
let node = catalog;
|
|
32002
|
+
for (const segment of pointer.split("/").slice(1)) {
|
|
32003
|
+
if (typeof node !== "object" || node === null) return void 0;
|
|
32004
|
+
node = node[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
|
|
32005
|
+
}
|
|
32006
|
+
return node;
|
|
32007
|
+
}
|
|
32008
|
+
function catalogTrackDeliveryMode(catalog, source) {
|
|
32009
|
+
const binding = bindingOf(catalog, source);
|
|
32010
|
+
const entry = binding ? valueAt(catalog, binding.pointer) : void 0;
|
|
32011
|
+
return entry?.deliveryMode === "stream" ? "stream" : "datagram";
|
|
32012
|
+
}
|
|
31490
32013
|
function moqtPriority2(value) {
|
|
31491
32014
|
return Number.isSafeInteger(value) && value >= 0 && value <= 255 ? value : null;
|
|
31492
32015
|
}
|
|
@@ -31530,6 +32053,7 @@ function fecParams(catalog, track, fps, sampleRate) {
|
|
|
31530
32053
|
repairTrackName: base.repairTrack,
|
|
31531
32054
|
repairLayers: repair.baseLayers,
|
|
31532
32055
|
repairOverlays: repair.overlays,
|
|
32056
|
+
deliveryMode: catalogTrackDeliveryMode(catalog, track),
|
|
31533
32057
|
...mediaRate
|
|
31534
32058
|
};
|
|
31535
32059
|
}
|
|
@@ -32019,6 +32543,168 @@ function requireNumber(value, path) {
|
|
|
32019
32543
|
return value;
|
|
32020
32544
|
}
|
|
32021
32545
|
|
|
32546
|
+
// src/repair-layer-subscriptions.ts
|
|
32547
|
+
var SERIAL32_HALF2 = 2147483648;
|
|
32548
|
+
var RepairLayerSubscriptions = class {
|
|
32549
|
+
#opts;
|
|
32550
|
+
#windowPackets;
|
|
32551
|
+
#margin;
|
|
32552
|
+
#dropAfterWindows;
|
|
32553
|
+
#source;
|
|
32554
|
+
#layerCount = 0;
|
|
32555
|
+
#subs = /* @__PURE__ */ new Map();
|
|
32556
|
+
#disposed = false;
|
|
32557
|
+
#lastSeq;
|
|
32558
|
+
#expected = 0;
|
|
32559
|
+
#lost = 0;
|
|
32560
|
+
#wantFewer = 0;
|
|
32561
|
+
#lossRate = null;
|
|
32562
|
+
#repairPackets = 0;
|
|
32563
|
+
#repairBytes = 0;
|
|
32564
|
+
#staleDropped = 0;
|
|
32565
|
+
constructor(opts) {
|
|
32566
|
+
this.#opts = opts;
|
|
32567
|
+
this.#source = opts.source;
|
|
32568
|
+
this.#windowPackets = opts.windowPackets ?? 500;
|
|
32569
|
+
this.#margin = opts.margin ?? 2;
|
|
32570
|
+
this.#dropAfterWindows = opts.dropAfterWindows ?? 3;
|
|
32571
|
+
}
|
|
32572
|
+
get layerCount() {
|
|
32573
|
+
return this.#layerCount;
|
|
32574
|
+
}
|
|
32575
|
+
/**
|
|
32576
|
+
* One source packet's MMTP `packet_sequence_number`, in arrival order.
|
|
32577
|
+
* The sequence is per `packet_id`: pass the packets of ONE packet_id only,
|
|
32578
|
+
* or interleaved sequences read as loss.
|
|
32579
|
+
*/
|
|
32580
|
+
noteSourceSequence(seq) {
|
|
32581
|
+
if (this.#disposed || this.#delivery() === "stream") return;
|
|
32582
|
+
seq >>>= 0;
|
|
32583
|
+
if (this.#lastSeq === void 0) {
|
|
32584
|
+
this.#lastSeq = seq;
|
|
32585
|
+
return;
|
|
32586
|
+
}
|
|
32587
|
+
const d = serial32Distance(this.#lastSeq, seq);
|
|
32588
|
+
if (d === 0) return;
|
|
32589
|
+
const back = d >= SERIAL32_HALF2 ? 2 ** 32 - d : 0;
|
|
32590
|
+
if (d > this.#windowPackets && (back === 0 || back > this.#windowPackets)) {
|
|
32591
|
+
this.#lastSeq = seq;
|
|
32592
|
+
this.#expected = 0;
|
|
32593
|
+
this.#lost = 0;
|
|
32594
|
+
return;
|
|
32595
|
+
}
|
|
32596
|
+
if (back === 0) {
|
|
32597
|
+
this.#expected += d;
|
|
32598
|
+
this.#lost += d - 1;
|
|
32599
|
+
this.#lastSeq = seq;
|
|
32600
|
+
} else if (this.#lost > 0) {
|
|
32601
|
+
this.#lost -= 1;
|
|
32602
|
+
}
|
|
32603
|
+
if (this.#expected >= this.#windowPackets) this.#closeWindow();
|
|
32604
|
+
}
|
|
32605
|
+
/** ABR switch R -> R': close R's tracks, carry the layer count to R'. */
|
|
32606
|
+
switchSource(next) {
|
|
32607
|
+
if (this.#disposed) return;
|
|
32608
|
+
this.#closeAll();
|
|
32609
|
+
this.#source = next;
|
|
32610
|
+
this.#lastSeq = void 0;
|
|
32611
|
+
this.#expected = 0;
|
|
32612
|
+
this.#lost = 0;
|
|
32613
|
+
this.#wantFewer = 0;
|
|
32614
|
+
this.#layerCount = this.#delivery() === "stream" ? 0 : Math.min(this.#layerCount, this.#layers().length);
|
|
32615
|
+
this.#reconcile();
|
|
32616
|
+
}
|
|
32617
|
+
snapshot() {
|
|
32618
|
+
return {
|
|
32619
|
+
delivery: this.#delivery(),
|
|
32620
|
+
layerCount: this.#layerCount,
|
|
32621
|
+
subscribedTracks: [...this.#subs.keys()],
|
|
32622
|
+
repairPackets: this.#repairPackets,
|
|
32623
|
+
repairBytes: this.#repairBytes,
|
|
32624
|
+
staleDropped: this.#staleDropped,
|
|
32625
|
+
lossRate: this.#lossRate
|
|
32626
|
+
};
|
|
32627
|
+
}
|
|
32628
|
+
dispose() {
|
|
32629
|
+
if (this.#disposed) return;
|
|
32630
|
+
this.#closeAll();
|
|
32631
|
+
this.#disposed = true;
|
|
32632
|
+
}
|
|
32633
|
+
#delivery() {
|
|
32634
|
+
return this.#source.deliveryMode === "stream" ? "stream" : "datagram";
|
|
32635
|
+
}
|
|
32636
|
+
#layers() {
|
|
32637
|
+
return this.#source.repairLayers ?? [];
|
|
32638
|
+
}
|
|
32639
|
+
#closeWindow() {
|
|
32640
|
+
const rate = this.#lost / this.#expected;
|
|
32641
|
+
this.#lossRate = rate;
|
|
32642
|
+
this.#expected = 0;
|
|
32643
|
+
this.#lost = 0;
|
|
32644
|
+
const target = this.#targetLayers(rate);
|
|
32645
|
+
if (target > this.#layerCount) {
|
|
32646
|
+
this.#wantFewer = 0;
|
|
32647
|
+
this.#layerCount = target;
|
|
32648
|
+
} else if (target < this.#layerCount) {
|
|
32649
|
+
this.#wantFewer += 1;
|
|
32650
|
+
if (this.#wantFewer >= this.#dropAfterWindows) {
|
|
32651
|
+
this.#wantFewer = 0;
|
|
32652
|
+
this.#layerCount -= 1;
|
|
32653
|
+
}
|
|
32654
|
+
} else {
|
|
32655
|
+
this.#wantFewer = 0;
|
|
32656
|
+
}
|
|
32657
|
+
this.#reconcile();
|
|
32658
|
+
}
|
|
32659
|
+
/** Fewest base layers whose summed P covers margin x the measured loss of K. */
|
|
32660
|
+
#targetLayers(rate) {
|
|
32661
|
+
if (rate <= 0) return 0;
|
|
32662
|
+
const needed = Math.ceil(rate * this.#source.fecK * this.#margin);
|
|
32663
|
+
const layers = this.#layers();
|
|
32664
|
+
let held = 0;
|
|
32665
|
+
for (let n = 0; n < layers.length; n++) {
|
|
32666
|
+
held += layers[n].repairSymbols;
|
|
32667
|
+
if (held >= needed) return n + 1;
|
|
32668
|
+
}
|
|
32669
|
+
return layers.length;
|
|
32670
|
+
}
|
|
32671
|
+
/** Stream delivery never leaves layer count 0, so it never subscribes. */
|
|
32672
|
+
#reconcile() {
|
|
32673
|
+
const want = /* @__PURE__ */ new Map();
|
|
32674
|
+
for (const layer of this.#layers().slice(0, this.#layerCount)) {
|
|
32675
|
+
want.set(layer.repairTrackName, layer.priority);
|
|
32676
|
+
}
|
|
32677
|
+
const overlay = this.#source.repairOverlays?.[0];
|
|
32678
|
+
if (this.#layerCount > 0 && overlay) want.set(overlay.repairTrackName, overlay.priority);
|
|
32679
|
+
for (const [name, entry] of this.#subs) {
|
|
32680
|
+
if (!want.has(name)) {
|
|
32681
|
+
entry.sub.close();
|
|
32682
|
+
this.#subs.delete(name);
|
|
32683
|
+
}
|
|
32684
|
+
}
|
|
32685
|
+
for (const [name, priority] of want) {
|
|
32686
|
+
if (this.#subs.has(name)) continue;
|
|
32687
|
+
const entry = {};
|
|
32688
|
+
entry.sub = this.#opts.subscribe(name, priority, (packet) => this.#onPacket(name, entry, packet));
|
|
32689
|
+
this.#subs.set(name, entry);
|
|
32690
|
+
}
|
|
32691
|
+
}
|
|
32692
|
+
/** Fenced on the entry itself, so a closed subscription is stale even if its track is re-added. */
|
|
32693
|
+
#onPacket(name, entry, packet) {
|
|
32694
|
+
if (this.#disposed || this.#subs.get(name) !== entry) {
|
|
32695
|
+
this.#staleDropped += 1;
|
|
32696
|
+
return;
|
|
32697
|
+
}
|
|
32698
|
+
this.#repairPackets += 1;
|
|
32699
|
+
this.#repairBytes += packet.byteLength;
|
|
32700
|
+
this.#opts.feed(packet, name);
|
|
32701
|
+
}
|
|
32702
|
+
#closeAll() {
|
|
32703
|
+
for (const entry of this.#subs.values()) entry.sub.close();
|
|
32704
|
+
this.#subs.clear();
|
|
32705
|
+
}
|
|
32706
|
+
};
|
|
32707
|
+
|
|
32022
32708
|
// src/repair-fetch.ts
|
|
32023
32709
|
var MOQT_PRIORITY_MAX = 255;
|
|
32024
32710
|
var SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1;
|
|
@@ -32664,6 +33350,7 @@ export {
|
|
|
32664
33350
|
REPAIR_LIFECYCLE_LOG_PREFIX,
|
|
32665
33351
|
REPAIR_PRIORITY_MAX,
|
|
32666
33352
|
REPAIR_PRIORITY_MIN,
|
|
33353
|
+
RepairLayerSubscriptions,
|
|
32667
33354
|
SOURCE_MEDIA_PRIORITY_MAX,
|
|
32668
33355
|
SampleCombineError,
|
|
32669
33356
|
SampleCombiner,
|
|
@@ -32688,6 +33375,7 @@ export {
|
|
|
32688
33375
|
catalogGroupDurationMs,
|
|
32689
33376
|
catalogKeyframeCadenceMs,
|
|
32690
33377
|
catalogRepairTarget,
|
|
33378
|
+
catalogTrackDeliveryMode,
|
|
32691
33379
|
catalogTrackFecUnavailable,
|
|
32692
33380
|
classifyH264Keyframe,
|
|
32693
33381
|
classifyHevcKeyframe,
|
|
@@ -32708,6 +33396,7 @@ export {
|
|
|
32708
33396
|
detectH264Keyframe,
|
|
32709
33397
|
detectHevcKeyframe,
|
|
32710
33398
|
driftCheck,
|
|
33399
|
+
encodeCmcdRecord,
|
|
32711
33400
|
encodeDataSegment,
|
|
32712
33401
|
encodeFecSourceInfo,
|
|
32713
33402
|
encodeLocTimestamp,
|