@blockcast/mmt-container 2.2.0-main.226dae8b7a27 → 2.2.0-main.2c4d96d3ffc2
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 +658 -168
- package/dist/browser-runtime.js.map +3 -3
- package/dist/catalog-fec-params.d.ts +1 -1
- package/dist/catalog-fec-params.js.map +1 -1
- package/dist/cmcd-producer.d.ts +60 -47
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js +138 -109
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +17 -12
- 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 +213 -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 +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -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 +37 -2
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +86 -9
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +89 -11
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +85 -9
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +16 -1
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +289 -35
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-manager.js +502 -41
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +17 -12
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.js +85 -9
- package/dist/no-modules.js.map +2 -2
- package/dist/track-session.d.ts +1 -1
- package/dist/track-session.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +85 -9
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +8 -5
- package/src/catalog-fec-params.ts +1 -1
- package/src/cmcd-producer.ts +189 -179
- package/src/fec-manager.ts +256 -6
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +6 -2
- package/src/iso-block-locator.ts +19 -0
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +96 -8
- package/src/mmtp-router.ts +342 -13
- 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;
|
|
@@ -8746,6 +8746,7 @@ 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;
|
|
@@ -8754,7 +8755,6 @@ var AMTGatewayManager = class {
|
|
|
8754
8755
|
constructor() {
|
|
8755
8756
|
this.messageHandler = (event) => {
|
|
8756
8757
|
const data = event.data;
|
|
8757
|
-
console.log(`[AMT] SW message received:`, data.type);
|
|
8758
8758
|
if (data.type === "UDP_PACKET") {
|
|
8759
8759
|
this.handlePacket(data);
|
|
8760
8760
|
} else if (data.type === "SOCKET_ERROR") {
|
|
@@ -8772,6 +8772,14 @@ var AMTGatewayManager = class {
|
|
|
8772
8772
|
this.messageListenerRegistered = true;
|
|
8773
8773
|
console.log("[AMT] Registered service worker message handler");
|
|
8774
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);
|
|
8780
|
+
}
|
|
8781
|
+
this.messageListenerRegistered = false;
|
|
8782
|
+
}
|
|
8775
8783
|
/**
|
|
8776
8784
|
* Subscribe via AMT tunnel
|
|
8777
8785
|
*/
|
|
@@ -8816,6 +8824,8 @@ var AMTGatewayManager = class {
|
|
|
8816
8824
|
this.subscriptions.delete(keyStr);
|
|
8817
8825
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
8818
8826
|
}
|
|
8827
|
+
if (this.subscriptions.size === 0)
|
|
8828
|
+
this.removeMessageListener();
|
|
8819
8829
|
throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
|
|
8820
8830
|
}
|
|
8821
8831
|
}
|
|
@@ -8837,6 +8847,8 @@ var AMTGatewayManager = class {
|
|
|
8837
8847
|
} finally {
|
|
8838
8848
|
this.subscriptions.delete(handle);
|
|
8839
8849
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
8850
|
+
if (this.subscriptions.size === 0)
|
|
8851
|
+
this.removeMessageListener();
|
|
8840
8852
|
}
|
|
8841
8853
|
}
|
|
8842
8854
|
/**
|
|
@@ -8853,14 +8865,12 @@ var AMTGatewayManager = class {
|
|
|
8853
8865
|
if (!subscriptionId)
|
|
8854
8866
|
return;
|
|
8855
8867
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
8856
|
-
if (!subscription)
|
|
8857
|
-
console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
|
|
8868
|
+
if (!subscription)
|
|
8858
8869
|
return;
|
|
8859
|
-
}
|
|
8860
8870
|
if (!subscription.handshakeComplete) {
|
|
8861
8871
|
const queue = this.pendingPackets.get(subscriptionId);
|
|
8862
8872
|
const packetData2 = data;
|
|
8863
|
-
if (queue) {
|
|
8873
|
+
if (queue && queue.length < MAX_PENDING_PACKETS) {
|
|
8864
8874
|
queue.push({
|
|
8865
8875
|
data: packetData2.data,
|
|
8866
8876
|
remoteAddress: packetData2.remoteAddress,
|
|
@@ -8886,20 +8896,15 @@ var AMTGatewayManager = class {
|
|
|
8886
8896
|
if (!subscriptionId)
|
|
8887
8897
|
return;
|
|
8888
8898
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
8889
|
-
if (!subscription)
|
|
8890
|
-
console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
|
|
8899
|
+
if (!subscription)
|
|
8891
8900
|
return;
|
|
8892
|
-
}
|
|
8893
8901
|
subscription.config.onError(new Error(error || "Unknown error"));
|
|
8894
8902
|
}
|
|
8895
8903
|
/**
|
|
8896
8904
|
* Cleanup on destroy
|
|
8897
8905
|
*/
|
|
8898
8906
|
destroy() {
|
|
8899
|
-
|
|
8900
|
-
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
8901
|
-
this.messageListenerRegistered = false;
|
|
8902
|
-
}
|
|
8907
|
+
this.removeMessageListener();
|
|
8903
8908
|
const handles = Array.from(this.subscriptions.keys());
|
|
8904
8909
|
for (const handle of handles) {
|
|
8905
8910
|
this.unsubscribe(handle).catch((err) => {
|
|
@@ -12944,6 +12949,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12944
12949
|
#retentionBlocks;
|
|
12945
12950
|
#heldEvicted;
|
|
12946
12951
|
#newest;
|
|
12952
|
+
#newestSource;
|
|
12947
12953
|
#holes;
|
|
12948
12954
|
static #checkedRetention(retentionBlocks) {
|
|
12949
12955
|
if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
|
|
@@ -12981,6 +12987,21 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
12981
12987
|
observe(position) {
|
|
12982
12988
|
if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
|
|
12983
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
|
+
}
|
|
12984
13005
|
/** Held sources evicted by `heldCapacity` (never by the horizon itself). */
|
|
12985
13006
|
get heldEvicted() {
|
|
12986
13007
|
return this.#heldEvicted;
|
|
@@ -13213,6 +13234,7 @@ var IsoBlockLocator = class _IsoBlockLocator {
|
|
|
13213
13234
|
this.#held.clear();
|
|
13214
13235
|
this.#coverageEnd = void 0;
|
|
13215
13236
|
this.#newest = void 0;
|
|
13237
|
+
this.#newestSource = void 0;
|
|
13216
13238
|
}
|
|
13217
13239
|
#claim(block) {
|
|
13218
13240
|
const placed = [];
|
|
@@ -13832,6 +13854,7 @@ var WORKER_ERROR_LOG_CAP = 5;
|
|
|
13832
13854
|
var WORKER_ERROR_KIND_CAP = 32;
|
|
13833
13855
|
var WORKER_ERROR_OVERFLOW_KIND = "other";
|
|
13834
13856
|
var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
|
|
13857
|
+
var NOT_TAKEN = Object.freeze({ taken: false, recovered: null });
|
|
13835
13858
|
function isMulticastSymbolDelivery(value) {
|
|
13836
13859
|
return value === "datagram" || value === "ordered-relay";
|
|
13837
13860
|
}
|
|
@@ -13929,6 +13952,10 @@ var FECManager = class _FECManager {
|
|
|
13929
13952
|
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
13930
13953
|
this.#blockSourceCounts = /* @__PURE__ */ new Map();
|
|
13931
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();
|
|
13932
13959
|
// Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
|
|
13933
13960
|
// first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
|
|
13934
13961
|
// media units, usually before it holds K symbols; under iso-ssbg1-v1
|
|
@@ -14057,6 +14084,10 @@ var FECManager = class _FECManager {
|
|
|
14057
14084
|
// ── Keyframe-protected repair policy (block-policy.ts) ──
|
|
14058
14085
|
// One classifier per physical stream, built from that stream's geometry.
|
|
14059
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();
|
|
14060
14091
|
// Geometry a stream-scoped configuration gave one stream (streamKey). ABR
|
|
14061
14092
|
// rungs can share a packetId with a different framerate or GOP, so the
|
|
14062
14093
|
// packet-level geometry is only the geometry of a packet whose streams
|
|
@@ -14214,6 +14245,7 @@ var FECManager = class _FECManager {
|
|
|
14214
14245
|
#pendingWorkerCleanupBounds;
|
|
14215
14246
|
#blockSourceCounts;
|
|
14216
14247
|
#blockSourceEsis;
|
|
14248
|
+
#blockRebuiltEsis;
|
|
14217
14249
|
#blockAuthoredLengths;
|
|
14218
14250
|
#ssbLengthConflictCount;
|
|
14219
14251
|
#blockRepairRsIds;
|
|
@@ -14256,6 +14288,7 @@ var FECManager = class _FECManager {
|
|
|
14256
14288
|
#orderedFrontiers;
|
|
14257
14289
|
#orderedPendingBlocks;
|
|
14258
14290
|
#classifiers;
|
|
14291
|
+
#noSpanRaps;
|
|
14259
14292
|
#streamGeometries;
|
|
14260
14293
|
#failedBlocks;
|
|
14261
14294
|
#resolvedBlockClasses;
|
|
@@ -14917,6 +14950,7 @@ var FECManager = class _FECManager {
|
|
|
14917
14950
|
};
|
|
14918
14951
|
const discontinuity = () => {
|
|
14919
14952
|
this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
|
|
14953
|
+
this.#noSpanRaps.delete(streamKey);
|
|
14920
14954
|
this.#highestSbn.set(streamKey, { sbn, atMs: now });
|
|
14921
14955
|
this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
|
|
14922
14956
|
if (psn === void 0) this.#sourceAnchors.delete(streamKey);
|
|
@@ -15329,6 +15363,9 @@ var FECManager = class _FECManager {
|
|
|
15329
15363
|
}
|
|
15330
15364
|
if (policyChanged) {
|
|
15331
15365
|
this.#bumpEpoch(packetId);
|
|
15366
|
+
for (const key of [...this.#noSpanRaps.keys()]) {
|
|
15367
|
+
if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
|
|
15368
|
+
}
|
|
15332
15369
|
for (const [key, classifier] of [...this.#classifiers]) {
|
|
15333
15370
|
if (!this.#belongsToPacket(key, packetId)) continue;
|
|
15334
15371
|
const stream = this.#parseStreamKey(key);
|
|
@@ -15531,6 +15568,7 @@ var FECManager = class _FECManager {
|
|
|
15531
15568
|
}
|
|
15532
15569
|
resetMap(this.#blockSourceCounts);
|
|
15533
15570
|
resetMap(this.#blockSourceEsis);
|
|
15571
|
+
resetMap(this.#blockRebuiltEsis);
|
|
15534
15572
|
resetMap(this.#blockAuthoredLengths);
|
|
15535
15573
|
resetMap(this.#blockRepairRsIds);
|
|
15536
15574
|
resetMap(this.#blockRsbLengths);
|
|
@@ -15564,6 +15602,7 @@ var FECManager = class _FECManager {
|
|
|
15564
15602
|
resetMap(this.#isoLocators);
|
|
15565
15603
|
resetMap(this.#sourceAnchors);
|
|
15566
15604
|
this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
|
|
15605
|
+
this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
|
|
15567
15606
|
this.#bumpEpoch(packetId);
|
|
15568
15607
|
for (const [sbn, keys] of this.#blockKeysBySbn) {
|
|
15569
15608
|
for (const key of keys.keys()) {
|
|
@@ -15654,6 +15693,7 @@ var FECManager = class _FECManager {
|
|
|
15654
15693
|
dropMap(this.#pendingWorkerCleanupClients);
|
|
15655
15694
|
dropMap(this.#blockSourceCounts);
|
|
15656
15695
|
dropMap(this.#blockSourceEsis);
|
|
15696
|
+
dropMap(this.#blockRebuiltEsis);
|
|
15657
15697
|
dropMap(this.#blockAuthoredLengths);
|
|
15658
15698
|
dropMap(this.#blockRepairRsIds);
|
|
15659
15699
|
dropMap(this.#blockRsbLengths);
|
|
@@ -15670,6 +15710,7 @@ var FECManager = class _FECManager {
|
|
|
15670
15710
|
dropMap(this.#isoLocators);
|
|
15671
15711
|
dropMap(this.#sourceAnchors);
|
|
15672
15712
|
dropMap(this.#classifiers);
|
|
15713
|
+
dropMap(this.#noSpanRaps);
|
|
15673
15714
|
dropMap(this.#streamGeometries);
|
|
15674
15715
|
dropSet(this.#pendingWorkerCleanups);
|
|
15675
15716
|
dropSet(this.#completedBlocks);
|
|
@@ -15815,6 +15856,9 @@ var FECManager = class _FECManager {
|
|
|
15815
15856
|
this.#unindexBlockKey(key);
|
|
15816
15857
|
}
|
|
15817
15858
|
}
|
|
15859
|
+
if (isSourceSymbol && source === "multicast") {
|
|
15860
|
+
this.#noteMulticastSource(this.#isoLocatorFor(packetId, catalogTrackName), sbn + esi >>> 0, packetId, catalogTrackName);
|
|
15861
|
+
}
|
|
15818
15862
|
return true;
|
|
15819
15863
|
}
|
|
15820
15864
|
#observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
|
|
@@ -16132,6 +16176,7 @@ var FECManager = class _FECManager {
|
|
|
16132
16176
|
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
16133
16177
|
const evictedBefore = locator.heldEvicted;
|
|
16134
16178
|
const held = locator.hold(ssId, source);
|
|
16179
|
+
if (held && source.source === "multicast") this.#noteMulticastSource(locator, ssId, packetId, catalogTrackName);
|
|
16135
16180
|
const evicted = locator.heldEvicted - evictedBefore;
|
|
16136
16181
|
if (evicted > 0) {
|
|
16137
16182
|
const before = this.#isoHeldEvictedCount;
|
|
@@ -16300,11 +16345,14 @@ var FECManager = class _FECManager {
|
|
|
16300
16345
|
/**
|
|
16301
16346
|
* Whether an MPU boundary still bounds this stream's RAPs (see
|
|
16302
16347
|
* `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
|
|
16303
|
-
* 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.
|
|
16304
16351
|
*/
|
|
16305
16352
|
mpuBoundsRapsFor(packetId, catalogTrackName) {
|
|
16306
|
-
|
|
16307
|
-
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;
|
|
16308
16356
|
}
|
|
16309
16357
|
/** A block's class now, or frozen at its resolution. */
|
|
16310
16358
|
blockClassFor(sbn, packetId, catalogTrackName) {
|
|
@@ -16330,7 +16378,14 @@ var FECManager = class _FECManager {
|
|
|
16330
16378
|
* invariant (consecutive MPUs exactly one keyframe interval apart).
|
|
16331
16379
|
*/
|
|
16332
16380
|
noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
|
|
16333
|
-
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
|
+
}
|
|
16334
16389
|
this.#applyClassifierChanges(
|
|
16335
16390
|
packetId,
|
|
16336
16391
|
catalogTrackName,
|
|
@@ -16717,7 +16772,15 @@ var FECManager = class _FECManager {
|
|
|
16717
16772
|
this.#isoSourceOversizeCount++;
|
|
16718
16773
|
return null;
|
|
16719
16774
|
}
|
|
16720
|
-
|
|
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) {
|
|
16721
16784
|
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
16722
16785
|
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
|
|
16723
16786
|
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
@@ -16746,6 +16809,68 @@ var FECManager = class _FECManager {
|
|
|
16746
16809
|
}
|
|
16747
16810
|
return recovered;
|
|
16748
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
|
+
rebuilt.add(located.esi);
|
|
16848
|
+
const recovered = this.#feedDecoderSource(packetId, ssId, symbolData, catalogTrackName, sbn);
|
|
16849
|
+
if (recovered === null) this.#completeWhenSourcesPresent(key, sbn, located.block.length, packetId, catalogTrackName);
|
|
16850
|
+
return { taken: true, recovered };
|
|
16851
|
+
}
|
|
16852
|
+
/**
|
|
16853
|
+
* A block that holds every one of its `length` sources, received or
|
|
16854
|
+
* rebuilt by another instance, with at least one rebuilt, is recovered:
|
|
16855
|
+
* every packet of it was delivered, by its own arrival or by the instance
|
|
16856
|
+
* that rebuilt it, so nothing is routed. Returns whether it completed it.
|
|
16857
|
+
*/
|
|
16858
|
+
#completeWhenSourcesPresent(key, sbn, length, packetId, catalogTrackName) {
|
|
16859
|
+
const rebuilt = this.#blockRebuiltEsis.get(key);
|
|
16860
|
+
if (rebuilt === void 0 || this.#completedBlocks.has(key)) return false;
|
|
16861
|
+
let present = rebuilt.size;
|
|
16862
|
+
for (const esi of this.#blockSourceEsis.get(key) ?? []) if (!rebuilt.has(esi)) present++;
|
|
16863
|
+
if (present < length) return false;
|
|
16864
|
+
this.#traceLifecycle("recovered", sbn, packetId, catalogTrackName);
|
|
16865
|
+
this.countRecovery(packetId, sbn, catalogTrackName);
|
|
16866
|
+
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
16867
|
+
if (catalogTrackName === void 0) {
|
|
16868
|
+
this.onBlockComplete?.(sbn, packetId);
|
|
16869
|
+
} else {
|
|
16870
|
+
this.onBlockComplete?.(sbn, packetId, catalogTrackName);
|
|
16871
|
+
}
|
|
16872
|
+
return true;
|
|
16873
|
+
}
|
|
16749
16874
|
/**
|
|
16750
16875
|
* fec-01 4.5: a reed-solomon repair packet whose SSB_length + RSB_length
|
|
16751
16876
|
* exceeds 255, or whose RS_ID is not below RSB_length, is discarded with the
|
|
@@ -16948,8 +17073,72 @@ var FECManager = class _FECManager {
|
|
|
16948
17073
|
this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
|
|
16949
17074
|
return null;
|
|
16950
17075
|
}
|
|
17076
|
+
if (recovered === null) this.#shortenUnrecoverableWait(blockKey, sbn, packetId, catalogTrackName);
|
|
16951
17077
|
return recovered;
|
|
16952
17078
|
}
|
|
17079
|
+
/**
|
|
17080
|
+
* A first-arrival multicast source, held or placed. A datagram flow keeps
|
|
17081
|
+
* its sources in send order, so one that moves the stream's newest source
|
|
17082
|
+
* position past a block leaves the sources that block still misses
|
|
17083
|
+
* reordered behind it at best: it shortens the wait of every block it
|
|
17084
|
+
* passed that only a late source can complete. MoQ is left out: its streams
|
|
17085
|
+
* can overtake one another, so a MoQ source shows nothing about the sources
|
|
17086
|
+
* sent before it.
|
|
17087
|
+
*/
|
|
17088
|
+
#noteMulticastSource(locator, ssId, packetId, catalogTrackName) {
|
|
17089
|
+
const previous = locator.newestSource;
|
|
17090
|
+
if (!locator.noteSource(ssId) || previous === void 0) return;
|
|
17091
|
+
for (const block of locator.blocksBetween(previous - 1 >>> 0, ssId)) {
|
|
17092
|
+
if (block.span || !serial32Precedes(serial32Add(block.start, block.length - 1), ssId)) continue;
|
|
17093
|
+
this.#shortenUnrecoverableWait(
|
|
17094
|
+
this.#blockKey(block.start, packetId, catalogTrackName),
|
|
17095
|
+
block.start,
|
|
17096
|
+
packetId,
|
|
17097
|
+
catalogTrackName
|
|
17098
|
+
);
|
|
17099
|
+
}
|
|
17100
|
+
}
|
|
17101
|
+
/**
|
|
17102
|
+
* The wait of a block that only a late source can complete (BLO-52085): a
|
|
17103
|
+
* repair stated it, its received sources plus every symbol its repair group
|
|
17104
|
+
* carries (RSB_length) are short of K' (the repair scheduler's
|
|
17105
|
+
* group-cannot-complete), and a later source of the stream has arrived, so
|
|
17106
|
+
* a source it misses is lost or reordered behind that one. The span, which
|
|
17107
|
+
* covers repair symbols paced after its sources, buys it nothing: it waits
|
|
17108
|
+
* the late-symbol allowance alone, the multicast wait less the span (the
|
|
17109
|
+
* catalog tolerance or the measured delivery gap). Undefined for any other
|
|
17110
|
+
* block.
|
|
17111
|
+
*/
|
|
17112
|
+
#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config) {
|
|
17113
|
+
const authoredLength = this.#blockAuthoredLengths.get(key);
|
|
17114
|
+
const rsbLength = this.#blockRsbLengths.get(key);
|
|
17115
|
+
if (authoredLength === void 0 || rsbLength === void 0) return void 0;
|
|
17116
|
+
if ((this.#blockSourceCounts.get(key) ?? 0) + rsbLength >= authoredLength) return void 0;
|
|
17117
|
+
const newestSource = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newestSource;
|
|
17118
|
+
if (newestSource === void 0 || !serial32Precedes(serial32Add(sbn, authoredLength - 1), newestSource)) {
|
|
17119
|
+
return void 0;
|
|
17120
|
+
}
|
|
17121
|
+
return this.#multicastWaitMs(config) - config.interleaveDepthMs;
|
|
17122
|
+
}
|
|
17123
|
+
/**
|
|
17124
|
+
* Bring an open block's wait in to its late-source wait, when it has one.
|
|
17125
|
+
* Never later than it is armed: only a source can complete such a block,
|
|
17126
|
+
* so a repair of its group (paced across the span) must not keep it open.
|
|
17127
|
+
*/
|
|
17128
|
+
#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName) {
|
|
17129
|
+
if (!this.#activeBlocks.has(key)) return;
|
|
17130
|
+
const config = this.#trackConfigs.get(packetId);
|
|
17131
|
+
if (config === void 0) return;
|
|
17132
|
+
const waitMs = this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, config);
|
|
17133
|
+
if (waitMs === void 0) return;
|
|
17134
|
+
const armedAtMs = this.#blockWaitDeadlinesMs.get(key);
|
|
17135
|
+
this.#armBlockTimeout(
|
|
17136
|
+
sbn,
|
|
17137
|
+
packetId,
|
|
17138
|
+
catalogTrackName,
|
|
17139
|
+
armedAtMs === void 0 ? waitMs : Math.min(waitMs, armedAtMs - this.#receiptNow())
|
|
17140
|
+
);
|
|
17141
|
+
}
|
|
16953
17142
|
/**
|
|
16954
17143
|
* Record a block's authored SSB_length from a repair symbol. Returns false,
|
|
16955
17144
|
* leaving the block unchanged, when the symbol contradicts the block: a
|
|
@@ -17104,6 +17293,15 @@ var FECManager = class _FECManager {
|
|
|
17104
17293
|
this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
|
|
17105
17294
|
return;
|
|
17106
17295
|
}
|
|
17296
|
+
if (this.#blockRebuiltEsis.has(key) && this.#completeWhenSourcesPresent(
|
|
17297
|
+
key,
|
|
17298
|
+
sbn,
|
|
17299
|
+
this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? authoredLength,
|
|
17300
|
+
packetId,
|
|
17301
|
+
catalogTrackName
|
|
17302
|
+
)) {
|
|
17303
|
+
return;
|
|
17304
|
+
}
|
|
17107
17305
|
this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
|
|
17108
17306
|
}
|
|
17109
17307
|
#completeCleanBlock(key, sbn, packetId, catalogTrackName) {
|
|
@@ -17112,6 +17310,7 @@ var FECManager = class _FECManager {
|
|
|
17112
17310
|
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
17113
17311
|
this.#blockSourceCounts.delete(key);
|
|
17114
17312
|
this.#blockSourceEsis.delete(key);
|
|
17313
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17115
17314
|
this.#blockAuthoredLengths.delete(key);
|
|
17116
17315
|
this.#blockRepairRsIds.delete(key);
|
|
17117
17316
|
this.#blockRsbLengths.delete(key);
|
|
@@ -17178,7 +17377,16 @@ var FECManager = class _FECManager {
|
|
|
17178
17377
|
this.#repairDeadlineMs(trackConfig)
|
|
17179
17378
|
);
|
|
17180
17379
|
}
|
|
17181
|
-
this.#
|
|
17380
|
+
if (!sourceProgress && this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) !== void 0) {
|
|
17381
|
+
this.#shortenUnrecoverableWait(key, sbn, packetId, catalogTrackName);
|
|
17382
|
+
return;
|
|
17383
|
+
}
|
|
17384
|
+
this.#armBlockTimeout(
|
|
17385
|
+
sbn,
|
|
17386
|
+
packetId,
|
|
17387
|
+
catalogTrackName,
|
|
17388
|
+
this.#lateSourceWaitMs(key, sbn, packetId, catalogTrackName, trackConfig) ?? timeoutMs
|
|
17389
|
+
);
|
|
17182
17390
|
}
|
|
17183
17391
|
#failBlock(sbn, packetId, catalogTrackName) {
|
|
17184
17392
|
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
@@ -17191,6 +17399,7 @@ var FECManager = class _FECManager {
|
|
|
17191
17399
|
}
|
|
17192
17400
|
this.#blockSourceCounts.delete(key);
|
|
17193
17401
|
this.#blockSourceEsis.delete(key);
|
|
17402
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17194
17403
|
this.#blockAuthoredLengths.delete(key);
|
|
17195
17404
|
this.#blockRepairRsIds.delete(key);
|
|
17196
17405
|
this.#blockRsbLengths.delete(key);
|
|
@@ -17229,6 +17438,7 @@ var FECManager = class _FECManager {
|
|
|
17229
17438
|
this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
|
|
17230
17439
|
this.#blockSourceCounts.delete(key);
|
|
17231
17440
|
this.#blockSourceEsis.delete(key);
|
|
17441
|
+
this.#blockRebuiltEsis.delete(key);
|
|
17232
17442
|
this.#blockAuthoredLengths.delete(key);
|
|
17233
17443
|
this.#blockRepairRsIds.delete(key);
|
|
17234
17444
|
this.#blockRsbLengths.delete(key);
|
|
@@ -17320,6 +17530,7 @@ var FECManager = class _FECManager {
|
|
|
17320
17530
|
this.#blockKeysBySbn.clear();
|
|
17321
17531
|
this.#blockSourceCounts.clear();
|
|
17322
17532
|
this.#blockSourceEsis.clear();
|
|
17533
|
+
this.#blockRebuiltEsis.clear();
|
|
17323
17534
|
this.#blockAuthoredLengths.clear();
|
|
17324
17535
|
this.#blockRepairRsIds.clear();
|
|
17325
17536
|
this.#blockRsbLengths.clear();
|
|
@@ -17345,6 +17556,7 @@ var FECManager = class _FECManager {
|
|
|
17345
17556
|
this.#blockAnchorsMs.clear();
|
|
17346
17557
|
this.#blockCatalogTrackNames.clear();
|
|
17347
17558
|
this.#classifiers.clear();
|
|
17559
|
+
this.#noSpanRaps.clear();
|
|
17348
17560
|
this.#streamGeometries.clear();
|
|
17349
17561
|
this.#failedBlocks.clear();
|
|
17350
17562
|
this.#resolvedBlockClasses.clear();
|
|
@@ -17875,9 +18087,21 @@ var IsoBmffTimingResolver = class {
|
|
|
17875
18087
|
}
|
|
17876
18088
|
#track = null;
|
|
17877
18089
|
#fragments = /* @__PURE__ */ new Map();
|
|
18090
|
+
/** First sample of each pending fragment, kept in `#delivered` with its sample count. */
|
|
18091
|
+
#firstSamples = /* @__PURE__ */ new Map();
|
|
18092
|
+
/**
|
|
18093
|
+
* First sample and sample count of the newest fragments whose every slot was
|
|
18094
|
+
* cleared. Abandonment counts: `releaseSample` clears a slot too, which
|
|
18095
|
+
* matches the combiner, since it retires an abandoned sample as well.
|
|
18096
|
+
*/
|
|
18097
|
+
#delivered = /* @__PURE__ */ new Map();
|
|
17878
18098
|
#pendingSamples = 0;
|
|
17879
18099
|
#evictedSamples = 0;
|
|
17880
18100
|
#lastMovieFragmentRejection = null;
|
|
18101
|
+
/** True once an FT=0 has bound the track that FT=1 timing is read against. */
|
|
18102
|
+
get initialized() {
|
|
18103
|
+
return this.#track !== null;
|
|
18104
|
+
}
|
|
17881
18105
|
get pendingSampleCount() {
|
|
17882
18106
|
return this.#pendingSamples;
|
|
17883
18107
|
}
|
|
@@ -17901,7 +18125,14 @@ var IsoBmffTimingResolver = class {
|
|
|
17901
18125
|
this.#track = timing;
|
|
17902
18126
|
return true;
|
|
17903
18127
|
}
|
|
17904
|
-
|
|
18128
|
+
/**
|
|
18129
|
+
* `recovered` marks an FEC-recovered copy. A recovered block hands back
|
|
18130
|
+
* every FT=1 it holds, the received ones included, so a recovered copy of a
|
|
18131
|
+
* fragment whose samples are all done is a duplicate: admitting it again
|
|
18132
|
+
* would charge stale timing and could displace the live MPU's. A source
|
|
18133
|
+
* copy is still admitted again (a rendition switch re-arms timing that way).
|
|
18134
|
+
*/
|
|
18135
|
+
addMovieFragment(mpuSequenceNumber2, data, additionalSampleLimit = this.maxPendingSamples, recovered = false) {
|
|
17905
18136
|
this.#lastMovieFragmentRejection = null;
|
|
17906
18137
|
if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
|
|
17907
18138
|
this.#lastMovieFragmentRejection = "invalid";
|
|
@@ -17927,6 +18158,8 @@ var IsoBmffTimingResolver = class {
|
|
|
17927
18158
|
(sample, index) => sample !== null && sameSample(sample, parsed.samples[index])
|
|
17928
18159
|
);
|
|
17929
18160
|
}
|
|
18161
|
+
const delivered = recovered ? this.#delivered.get(key) : void 0;
|
|
18162
|
+
if (delivered && delivered.sampleCount === parsed.samples.length && sameSample(delivered.first, parsed.samples[0])) return true;
|
|
17930
18163
|
let freed = 0;
|
|
17931
18164
|
while (this.#fragments.size > 0 && (this.#pendingSamples + parsed.samples.length > this.maxPendingSamples || parsed.samples.length - freed > additionalSampleLimit)) {
|
|
17932
18165
|
freed += this.#evictOldestFragment();
|
|
@@ -17940,6 +18173,7 @@ var IsoBmffTimingResolver = class {
|
|
|
17940
18173
|
return false;
|
|
17941
18174
|
}
|
|
17942
18175
|
this.#fragments.set(key, [...parsed.samples]);
|
|
18176
|
+
this.#firstSamples.set(key, parsed.samples[0]);
|
|
17943
18177
|
this.#pendingSamples += parsed.samples.length;
|
|
17944
18178
|
return true;
|
|
17945
18179
|
}
|
|
@@ -17997,6 +18231,7 @@ var IsoBmffTimingResolver = class {
|
|
|
17997
18231
|
let live = 0;
|
|
17998
18232
|
for (const sample of samples) if (sample !== null) live++;
|
|
17999
18233
|
this.#fragments.delete(oldest);
|
|
18234
|
+
this.#firstSamples.delete(oldest);
|
|
18000
18235
|
this.#pendingSamples -= live;
|
|
18001
18236
|
this.#evictedSamples += live;
|
|
18002
18237
|
return live;
|
|
@@ -18012,12 +18247,23 @@ var IsoBmffTimingResolver = class {
|
|
|
18012
18247
|
if (!samples || !samples[index]) return false;
|
|
18013
18248
|
samples[index] = null;
|
|
18014
18249
|
this.#pendingSamples--;
|
|
18015
|
-
if (samples.every((value) => value === null))
|
|
18250
|
+
if (samples.every((value) => value === null)) {
|
|
18251
|
+
this.#fragments.delete(key);
|
|
18252
|
+
const first = this.#firstSamples.get(key);
|
|
18253
|
+
this.#firstSamples.delete(key);
|
|
18254
|
+
this.#delivered.delete(key);
|
|
18255
|
+
this.#delivered.set(key, { first, sampleCount: samples.length });
|
|
18256
|
+
if (this.#delivered.size > this.maxPendingSamples) {
|
|
18257
|
+
this.#delivered.delete(this.#delivered.keys().next().value);
|
|
18258
|
+
}
|
|
18259
|
+
}
|
|
18016
18260
|
return true;
|
|
18017
18261
|
}
|
|
18018
18262
|
reset() {
|
|
18019
18263
|
this.#track = null;
|
|
18020
18264
|
this.#fragments.clear();
|
|
18265
|
+
this.#firstSamples.clear();
|
|
18266
|
+
this.#delivered.clear();
|
|
18021
18267
|
this.#pendingSamples = 0;
|
|
18022
18268
|
}
|
|
18023
18269
|
};
|
|
@@ -18499,6 +18745,12 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18499
18745
|
this.#reclaimedTimingSamples = 0;
|
|
18500
18746
|
this.#evictedTimingSamples = 0;
|
|
18501
18747
|
this.#rejectedMovieFragments = 0;
|
|
18748
|
+
this.#recoveredHeadsAwaitingTiming = {
|
|
18749
|
+
held: 0,
|
|
18750
|
+
expired: 0,
|
|
18751
|
+
discarded: 0,
|
|
18752
|
+
superseded: 0
|
|
18753
|
+
};
|
|
18502
18754
|
this.#sequencers = /* @__PURE__ */ new Map();
|
|
18503
18755
|
this.#fecGate = null;
|
|
18504
18756
|
this.#fecSequencer = null;
|
|
@@ -18607,6 +18859,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18607
18859
|
#reclaimedTimingSamples;
|
|
18608
18860
|
#evictedTimingSamples;
|
|
18609
18861
|
#rejectedMovieFragments;
|
|
18862
|
+
#recoveredHeadsAwaitingTiming;
|
|
18610
18863
|
#sequencers;
|
|
18611
18864
|
#fecGate;
|
|
18612
18865
|
#fecSequencer;
|
|
@@ -18727,6 +18980,14 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18727
18980
|
rejectedMovieFragments: this.#rejectedMovieFragments
|
|
18728
18981
|
};
|
|
18729
18982
|
}
|
|
18983
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
18984
|
+
get recoveredHeadsAwaitingTiming() {
|
|
18985
|
+
let pending = 0;
|
|
18986
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
18987
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++;
|
|
18988
|
+
}
|
|
18989
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending };
|
|
18990
|
+
}
|
|
18730
18991
|
get reorderStats() {
|
|
18731
18992
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0;
|
|
18732
18993
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered;
|
|
@@ -18855,10 +19116,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18855
19116
|
get reassemblerDataUnitsReturned() {
|
|
18856
19117
|
return this.#reassemblerDataUnitsReturned;
|
|
18857
19118
|
}
|
|
18858
|
-
#countOutcome(
|
|
18859
|
-
const count = (this.#feedOutcomes.get(
|
|
18860
|
-
this.#feedOutcomes.set(
|
|
18861
|
-
this.#lastOutcome =
|
|
19119
|
+
#countOutcome(token2) {
|
|
19120
|
+
const count = (this.#feedOutcomes.get(token2) ?? 0) + 1;
|
|
19121
|
+
this.#feedOutcomes.set(token2, count);
|
|
19122
|
+
this.#lastOutcome = token2;
|
|
18862
19123
|
this.#lastOutcomeCount = count;
|
|
18863
19124
|
}
|
|
18864
19125
|
#countDeferredOutcome(reason) {
|
|
@@ -18900,7 +19161,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
18900
19161
|
const now = Date.now();
|
|
18901
19162
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
18902
19163
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0].arrivedAt > timeoutMs) {
|
|
18903
|
-
awaitingTiming.shift();
|
|
19164
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
18904
19165
|
}
|
|
18905
19166
|
}
|
|
18906
19167
|
}
|
|
@@ -19018,11 +19279,16 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19018
19279
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
19019
19280
|
return this.#drop("fragment-unexpected-fragmentation");
|
|
19020
19281
|
const resolver2 = this.#timingResolver(streamKey, info.packetId, false);
|
|
19021
|
-
if (!resolver2) return this.#drop("fragment-no-resolver");
|
|
19282
|
+
if (!resolver2?.initialized) return this.#drop("fragment-no-resolver");
|
|
19022
19283
|
const pendingBefore = resolver2.pendingSampleCount;
|
|
19023
19284
|
const evictedBefore = resolver2.evictedSampleCount;
|
|
19024
19285
|
const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples;
|
|
19025
|
-
const admitted = resolver2.addMovieFragment(
|
|
19286
|
+
const admitted = resolver2.addMovieFragment(
|
|
19287
|
+
mpu.mpuSequenceNumber,
|
|
19288
|
+
data,
|
|
19289
|
+
remaining,
|
|
19290
|
+
options?.recovered === true
|
|
19291
|
+
);
|
|
19026
19292
|
this.#pendingTimingSamples += resolver2.pendingSampleCount - pendingBefore;
|
|
19027
19293
|
this.#evictedTimingSamples += resolver2.evictedSampleCount - evictedBefore;
|
|
19028
19294
|
if (!admitted) {
|
|
@@ -19042,7 +19308,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19042
19308
|
};
|
|
19043
19309
|
}
|
|
19044
19310
|
if (fragmentType !== 2) return this.#drop("unknown-fragment-type");
|
|
19045
|
-
const resolver = this.#timingResolver(streamKey, info.packetId,
|
|
19311
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true);
|
|
19046
19312
|
if (!resolver) return this.#drop("mfu-no-resolver");
|
|
19047
19313
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
19048
19314
|
return this.#drop("mfu-empty-payload");
|
|
@@ -19063,6 +19329,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19063
19329
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
19064
19330
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
19065
19331
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
19332
|
+
sequenceNumber: info.sequenceNumber,
|
|
19066
19333
|
arrivedAt: Date.now()
|
|
19067
19334
|
});
|
|
19068
19335
|
return this.#drop("mfu-sample-timing-missing");
|
|
@@ -19386,6 +19653,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19386
19653
|
}
|
|
19387
19654
|
this.#emitFrame(data, meta);
|
|
19388
19655
|
}
|
|
19656
|
+
/** Whether FT=1 metadata already names this sample, without claiming it. */
|
|
19657
|
+
hasAuthoredTimingFor(packetId, mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
|
|
19658
|
+
return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber) != null;
|
|
19659
|
+
}
|
|
19389
19660
|
/**
|
|
19390
19661
|
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
19391
19662
|
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
@@ -19447,6 +19718,9 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19447
19718
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard();
|
|
19448
19719
|
this.#sequencers.clear();
|
|
19449
19720
|
this.#fecSequencer?.discard();
|
|
19721
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
19722
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19723
|
+
}
|
|
19450
19724
|
this.#timingStreams.clear();
|
|
19451
19725
|
this.#pendingTimingSamples = 0;
|
|
19452
19726
|
this.#reliableMoqStartupGate.clear();
|
|
@@ -19465,10 +19739,23 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19465
19739
|
return resolver;
|
|
19466
19740
|
}
|
|
19467
19741
|
#awaitTiming(streamKey, head) {
|
|
19468
|
-
const
|
|
19469
|
-
if (!
|
|
19742
|
+
const state = this.#timingStreams.get(streamKey);
|
|
19743
|
+
if (!state) return;
|
|
19744
|
+
if (!head.options?.recovered) {
|
|
19745
|
+
const kept = state.awaitingTiming.filter((queued) => !queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber);
|
|
19746
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length;
|
|
19747
|
+
state.awaitingTiming = kept;
|
|
19748
|
+
}
|
|
19749
|
+
const awaitingTiming = state.awaitingTiming;
|
|
19470
19750
|
awaitingTiming.push(head);
|
|
19471
|
-
if (
|
|
19751
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++;
|
|
19752
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
19753
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift(), "expired");
|
|
19754
|
+
}
|
|
19755
|
+
}
|
|
19756
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
19757
|
+
#leaveAwaitingTiming(head, outcome) {
|
|
19758
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++;
|
|
19472
19759
|
}
|
|
19473
19760
|
/**
|
|
19474
19761
|
* Feed again the heads whose timing the FT=1 just admitted declares. That
|
|
@@ -19496,6 +19783,7 @@ var MmtpContainerParser = class _MmtpContainerParser {
|
|
|
19496
19783
|
const state = this.#timingStreams.get(streamKey);
|
|
19497
19784
|
if (!state) return;
|
|
19498
19785
|
this.#timingStreams.delete(streamKey);
|
|
19786
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, "discarded");
|
|
19499
19787
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount;
|
|
19500
19788
|
if (this.#pendingTimingSamples < 0) {
|
|
19501
19789
|
throw new Error("[MmtpContainerParser] pending timing sample accounting underflow");
|
|
@@ -22702,6 +22990,16 @@ function keyframeOverlaySsId(header) {
|
|
|
22702
22990
|
if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
|
|
22703
22991
|
return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
|
|
22704
22992
|
}
|
|
22993
|
+
function overlayBaseSsId(anchor, overlaySsId, restoredPsn, headerPsn) {
|
|
22994
|
+
if (anchor.baseSsId === void 0 || headerPsn !== restoredPsn) return void 0;
|
|
22995
|
+
return serial32Add(anchor.baseSsId, overlaySsId - anchor.overlaySsId | 0);
|
|
22996
|
+
}
|
|
22997
|
+
function unstampedBaseSsId(offset, unstampedOffset, headerPsn) {
|
|
22998
|
+
if (offset === void 0 || offset !== unstampedOffset) return void 0;
|
|
22999
|
+
return headerPsn + offset >>> 0;
|
|
23000
|
+
}
|
|
23001
|
+
var OVERLAY_ROUTED_LIMIT = 1024;
|
|
23002
|
+
var OVERLAY_PENDING_LIMIT = 256;
|
|
22705
23003
|
var OVERLAY_OFFSET_MPUS = 16;
|
|
22706
23004
|
var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
|
|
22707
23005
|
function assertStreamIdentity(role, stream) {
|
|
@@ -22869,6 +23167,15 @@ var MmtpRouter = class {
|
|
|
22869
23167
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
22870
23168
|
*/
|
|
22871
23169
|
#ssIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
23170
|
+
/**
|
|
23171
|
+
* Per stream, the SS_ID offset at which a base recovery last showed the
|
|
23172
|
+
* sender's own packet_sequence_number: a recovered packet's header psn
|
|
23173
|
+
* equal to its SS_ID minus that offset. While it equals the stream's
|
|
23174
|
+
* current offset, no hop re-stamps the stream, and a packet's protected psn
|
|
23175
|
+
* names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
|
|
23176
|
+
* an MPU no member of which arrived).
|
|
23177
|
+
*/
|
|
23178
|
+
#unstampedSsIdSeqOffsetByStream = /* @__PURE__ */ new Map();
|
|
22872
23179
|
/**
|
|
22873
23180
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
22874
23181
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -22938,7 +23245,9 @@ var MmtpRouter = class {
|
|
|
22938
23245
|
recovered: 0,
|
|
22939
23246
|
restarts: 0,
|
|
22940
23247
|
unsequencedDropped: 0,
|
|
22941
|
-
protectedDropped: 0
|
|
23248
|
+
protectedDropped: 0,
|
|
23249
|
+
baseFed: 0,
|
|
23250
|
+
baseUndetermined: 0
|
|
22942
23251
|
};
|
|
22943
23252
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
22944
23253
|
#onDeltaBlockCompleteCb = null;
|
|
@@ -23111,7 +23420,7 @@ var MmtpRouter = class {
|
|
|
23111
23420
|
const cb = this.#getFrameCallback(packetId);
|
|
23112
23421
|
if (isVideo && !assembler) {
|
|
23113
23422
|
if (frameMeta.recovered) {
|
|
23114
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
23423
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
23115
23424
|
} else {
|
|
23116
23425
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
23117
23426
|
}
|
|
@@ -23501,6 +23810,10 @@ var MmtpRouter = class {
|
|
|
23501
23810
|
get keyframeOverlayStats() {
|
|
23502
23811
|
return { ...this.#overlayStats };
|
|
23503
23812
|
}
|
|
23813
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
23814
|
+
get recoveredHeadsAwaitingTiming() {
|
|
23815
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
23816
|
+
}
|
|
23504
23817
|
#isOverlayRepair(repairPacketId, repairTrackName) {
|
|
23505
23818
|
return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
|
|
23506
23819
|
}
|
|
@@ -23551,6 +23864,11 @@ var MmtpRouter = class {
|
|
|
23551
23864
|
*/
|
|
23552
23865
|
resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
|
|
23553
23866
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
23867
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
23868
|
+
if (catalogTrackName !== void 0 && name !== catalogTrackName) continue;
|
|
23869
|
+
stream.overlayToBase.routed.clear();
|
|
23870
|
+
stream.overlayToBase.pending.clear();
|
|
23871
|
+
}
|
|
23554
23872
|
}
|
|
23555
23873
|
#resetOverlay(inst) {
|
|
23556
23874
|
inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
|
|
@@ -23607,9 +23925,10 @@ var MmtpRouter = class {
|
|
|
23607
23925
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
23608
23926
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
23609
23927
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
23610
|
-
* included (7.1, 7.4.3).
|
|
23928
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
23929
|
+
* FEC reads it.
|
|
23611
23930
|
*/
|
|
23612
|
-
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
|
|
23931
|
+
#feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source, baseSsId) {
|
|
23613
23932
|
if (this.#overlays.size === 0) return;
|
|
23614
23933
|
const ssId = keyframeOverlaySsId(header);
|
|
23615
23934
|
if (ssId === void 0) return;
|
|
@@ -23618,7 +23937,11 @@ var MmtpRouter = class {
|
|
|
23618
23937
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
23619
23938
|
if (identity !== null) {
|
|
23620
23939
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
23621
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
23940
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
23941
|
+
psnOffset: ssId - sequenceNumber >>> 0,
|
|
23942
|
+
overlaySsId: ssId,
|
|
23943
|
+
...baseSsId === void 0 ? {} : { baseSsId }
|
|
23944
|
+
});
|
|
23622
23945
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
|
|
23623
23946
|
}
|
|
23624
23947
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -23697,14 +24020,27 @@ var MmtpRouter = class {
|
|
|
23697
24020
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
23698
24021
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
23699
24022
|
const restored = /* @__PURE__ */ new Set();
|
|
24023
|
+
const baseSsIds = /* @__PURE__ */ new Map();
|
|
23700
24024
|
const symbols = result.symbols.flatMap((sym) => {
|
|
23701
|
-
const
|
|
23702
|
-
if (
|
|
23703
|
-
const
|
|
24025
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
24026
|
+
if (anchor !== void 0) {
|
|
24027
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
24028
|
+
const packetSequenceNumber = overlaySsId - anchor.psnOffset >>> 0;
|
|
23704
24029
|
restored.add(packetSequenceNumber);
|
|
23705
|
-
|
|
24030
|
+
const out = { ...sym, packetSequenceNumber };
|
|
24031
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
24032
|
+
if (baseSsId !== void 0) baseSsIds.set(out, baseSsId);
|
|
24033
|
+
return [out];
|
|
24034
|
+
}
|
|
24035
|
+
if (!restamped) {
|
|
24036
|
+
const baseSsId = unstampedBaseSsId(
|
|
24037
|
+
this.#ssIdSeqOffsetByStream.get(streamKey),
|
|
24038
|
+
this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
|
|
24039
|
+
sym.packetSequenceNumber
|
|
24040
|
+
);
|
|
24041
|
+
if (baseSsId !== void 0) baseSsIds.set(sym, baseSsId);
|
|
24042
|
+
return [sym];
|
|
23706
24043
|
}
|
|
23707
|
-
if (!restamped) return [sym];
|
|
23708
24044
|
this.#overlayStats.unsequencedDropped++;
|
|
23709
24045
|
return [];
|
|
23710
24046
|
});
|
|
@@ -23713,6 +24049,85 @@ var MmtpRouter = class {
|
|
|
23713
24049
|
deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
|
|
23714
24050
|
});
|
|
23715
24051
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
|
|
24052
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
|
|
24053
|
+
}
|
|
24054
|
+
/**
|
|
24055
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
24056
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
24057
|
+
* from a received member of its MPU, else unstampedBaseSsId),
|
|
24058
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
24059
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
24060
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
24061
|
+
* A base block no repair has stated yet takes it when one does
|
|
24062
|
+
* (#feedPendingRebuilt).
|
|
24063
|
+
*
|
|
24064
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
24065
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
24066
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
24067
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
24068
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
24069
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
24070
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
24071
|
+
* block's own arrivals cannot show.
|
|
24072
|
+
*/
|
|
24073
|
+
#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds) {
|
|
24074
|
+
const fec = this.#fecManager;
|
|
24075
|
+
const owner = inst.leg.sourcePacketId;
|
|
24076
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
24077
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
24078
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
24079
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
24080
|
+
const feeds = [];
|
|
24081
|
+
for (const sym of symbols) {
|
|
24082
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
24083
|
+
const ssId = baseSsIds.get(sym);
|
|
24084
|
+
if (ssId === void 0) {
|
|
24085
|
+
this.#overlayStats.baseUndetermined++;
|
|
24086
|
+
continue;
|
|
24087
|
+
}
|
|
24088
|
+
routed.delete(ssId);
|
|
24089
|
+
routed.add(ssId);
|
|
24090
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value);
|
|
24091
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
24092
|
+
}
|
|
24093
|
+
for (const [ssId, symbolData] of feeds) {
|
|
24094
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24095
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24096
|
+
}
|
|
24097
|
+
}
|
|
24098
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
24099
|
+
#feedRebuiltToBase(owner, name, ssId, symbolData) {
|
|
24100
|
+
const fec = this.#fecManager;
|
|
24101
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
24102
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
24103
|
+
if (coords == null) {
|
|
24104
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
24105
|
+
pending.delete(ssId);
|
|
24106
|
+
pending.set(ssId, symbolData.slice());
|
|
24107
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value);
|
|
24108
|
+
return;
|
|
24109
|
+
}
|
|
24110
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
24111
|
+
if (!fed.taken) return;
|
|
24112
|
+
this.#overlayStats.baseFed++;
|
|
24113
|
+
if (fed.recovered === null) return;
|
|
24114
|
+
const sbn = coords.sbn;
|
|
24115
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
24116
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
24117
|
+
fec.countRecovery(owner, sbn, name);
|
|
24118
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
24119
|
+
});
|
|
24120
|
+
}
|
|
24121
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
24122
|
+
#feedPendingRebuilt(owner, name, block) {
|
|
24123
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
24124
|
+
if (pending === void 0 || pending.size === 0) return;
|
|
24125
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
24126
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
24127
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
24128
|
+
pending.delete(ssId);
|
|
24129
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
24130
|
+
}
|
|
23716
24131
|
}
|
|
23717
24132
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
23718
24133
|
* the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
|
|
@@ -24338,6 +24753,8 @@ var MmtpRouter = class {
|
|
|
24338
24753
|
)) return;
|
|
24339
24754
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
24340
24755
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
24756
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
24757
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
24341
24758
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
24342
24759
|
const recovered = fec.feedRepairSymbol(
|
|
24343
24760
|
ownerPacketId,
|
|
@@ -24475,6 +24892,7 @@ var MmtpRouter = class {
|
|
|
24475
24892
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
24476
24893
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
24477
24894
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
24895
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
24478
24896
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
24479
24897
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
24480
24898
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -25390,7 +25808,8 @@ var MmtpRouter = class {
|
|
|
25390
25808
|
parsed.header,
|
|
25391
25809
|
info.sequenceNumber,
|
|
25392
25810
|
info.rapFlag,
|
|
25393
|
-
source
|
|
25811
|
+
source,
|
|
25812
|
+
fecPayloadId?.ssId
|
|
25394
25813
|
);
|
|
25395
25814
|
}
|
|
25396
25815
|
}
|
|
@@ -25540,7 +25959,8 @@ var MmtpRouter = class {
|
|
|
25540
25959
|
if (stream === void 0) {
|
|
25541
25960
|
stream = {
|
|
25542
25961
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
25543
|
-
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId))
|
|
25962
|
+
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
25963
|
+
overlayToBase: { routed: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Map() }
|
|
25544
25964
|
};
|
|
25545
25965
|
byName.set(catalogTrackName, stream);
|
|
25546
25966
|
}
|
|
@@ -25655,8 +26075,16 @@ var MmtpRouter = class {
|
|
|
25655
26075
|
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
25656
26076
|
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
25657
26077
|
}
|
|
25658
|
-
#routeDirectRecoveredVideo(packetId, data, meta, cb) {
|
|
26078
|
+
#routeDirectRecoveredVideo(packetId, data, meta, cb, fecSbn) {
|
|
25659
26079
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
26080
|
+
if (this.#isFecTrack(packetId)) {
|
|
26081
|
+
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
26082
|
+
if (fecSbn !== void 0 || high !== void 0 && authoredAdjacent(high, meta)) {
|
|
26083
|
+
this.#drainDirectRecoveredVideo(streamKey, packetId, (frame) => frame.timestamp <= meta.timestamp, cb);
|
|
26084
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
26085
|
+
return;
|
|
26086
|
+
}
|
|
26087
|
+
}
|
|
25660
26088
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
25661
26089
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
25662
26090
|
this.#drainDirectRecoveredVideo(
|
|
@@ -25810,6 +26238,13 @@ var MmtpRouter = class {
|
|
|
25810
26238
|
* frames by epoch only if pre-protection input ever matters.
|
|
25811
26239
|
*/
|
|
25812
26240
|
#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
|
|
26241
|
+
const overlayRouted = ownerPacketId == null ? void 0 : this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
26242
|
+
if (overlayRouted !== void 0 && overlayRouted.size > 0) {
|
|
26243
|
+
result = {
|
|
26244
|
+
...result,
|
|
26245
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index)))
|
|
26246
|
+
};
|
|
26247
|
+
}
|
|
25813
26248
|
const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
25814
26249
|
const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
|
|
25815
26250
|
const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
@@ -25874,7 +26309,10 @@ var MmtpRouter = class {
|
|
|
25874
26309
|
...result,
|
|
25875
26310
|
symbols: result.symbols.map((sym) => {
|
|
25876
26311
|
const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
|
|
25877
|
-
if (wire === sym.packetSequenceNumber)
|
|
26312
|
+
if (wire === sym.packetSequenceNumber) {
|
|
26313
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
26314
|
+
return sym;
|
|
26315
|
+
}
|
|
25878
26316
|
this.#restampedRecoveredSeqs++;
|
|
25879
26317
|
this.#restampedStreams.add(streamKey);
|
|
25880
26318
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -25910,6 +26348,29 @@ var MmtpRouter = class {
|
|
|
25910
26348
|
}
|
|
25911
26349
|
);
|
|
25912
26350
|
}
|
|
26351
|
+
const untimedHead = (sym) => sym.fragmentType === 2 && sym.fragment.fragmentationIndicator === 1 && sym.fragment.movieFragmentSequenceNumber !== void 0 && sym.fragment.sampleNumber !== void 0 && !this.#parser.hasAuthoredTimingFor(
|
|
26352
|
+
ownerPacketId,
|
|
26353
|
+
sym.fragment.mpuSequenceNumber,
|
|
26354
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
26355
|
+
sym.fragment.sampleNumber,
|
|
26356
|
+
catalogTrackName
|
|
26357
|
+
);
|
|
26358
|
+
for (const sym of result.symbols) {
|
|
26359
|
+
if (!untimedHead(sym) || this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
26360
|
+
this.#feedViaParser(
|
|
26361
|
+
sym.rawSymbol,
|
|
26362
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
26363
|
+
void 0,
|
|
26364
|
+
{
|
|
26365
|
+
skipFecPayloadId: true,
|
|
26366
|
+
recovered: true,
|
|
26367
|
+
deliveryProvenance,
|
|
26368
|
+
...fecSbnOf(sym.index),
|
|
26369
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
26370
|
+
}
|
|
26371
|
+
);
|
|
26372
|
+
}
|
|
26373
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
25913
26374
|
const gated = this.#isFecTrack(ownerPacketId);
|
|
25914
26375
|
const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
|
|
25915
26376
|
if (assembler) {
|
|
@@ -29671,7 +30132,7 @@ async function setupFecWorkerForTrack(opts) {
|
|
|
29671
30132
|
blockProfile,
|
|
29672
30133
|
k: opts.fecK,
|
|
29673
30134
|
// P as this receiver joined it, not the catalog total: the worker snapshot
|
|
29674
|
-
// and CMCD `
|
|
30135
|
+
// and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
|
|
29675
30136
|
repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
|
|
29676
30137
|
symbolSize: opts.fecSymbolSize,
|
|
29677
30138
|
interleaveDepth: interleaveDepthBlocks,
|
|
@@ -30213,24 +30674,24 @@ var MediaRouter = class {
|
|
|
30213
30674
|
if (!this.#multicastVideoKeyed) return;
|
|
30214
30675
|
if (this.#requireAudio && !this.#multicastAudioReceived) return;
|
|
30215
30676
|
if (this.#requireAudio && !this.#moqAudioWarmed) return;
|
|
30216
|
-
const
|
|
30677
|
+
const token2 = ++this.#sourceSwitchToken;
|
|
30217
30678
|
const maybeReady = this.#onSourceSwitch?.("multicast");
|
|
30218
30679
|
if (!maybeReady || typeof maybeReady.then !== "function") {
|
|
30219
|
-
this.#commitMulticastActivation(
|
|
30680
|
+
this.#commitMulticastActivation(token2);
|
|
30220
30681
|
return;
|
|
30221
30682
|
}
|
|
30222
30683
|
const ready = Promise.resolve(maybeReady);
|
|
30223
|
-
this.#pendingMulticastToken =
|
|
30684
|
+
this.#pendingMulticastToken = token2;
|
|
30224
30685
|
this.#sourceSwitchReady = ready;
|
|
30225
30686
|
void ready.then(
|
|
30226
|
-
() => this.#commitMulticastActivation(
|
|
30227
|
-
(err) => this.#rejectMulticastActivation(
|
|
30687
|
+
() => this.#commitMulticastActivation(token2),
|
|
30688
|
+
(err) => this.#rejectMulticastActivation(token2, err)
|
|
30228
30689
|
).finally(() => {
|
|
30229
30690
|
if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
|
|
30230
30691
|
});
|
|
30231
30692
|
}
|
|
30232
|
-
#commitMulticastActivation(
|
|
30233
|
-
if (
|
|
30693
|
+
#commitMulticastActivation(token2) {
|
|
30694
|
+
if (token2 !== this.#sourceSwitchToken) return;
|
|
30234
30695
|
this.#active = "multicast";
|
|
30235
30696
|
this.#suppressed = true;
|
|
30236
30697
|
this.#pendingMulticastToken = null;
|
|
@@ -30241,9 +30702,9 @@ var MediaRouter = class {
|
|
|
30241
30702
|
if (video) this.#onVideoFrame?.(video.data, video.meta);
|
|
30242
30703
|
if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
|
|
30243
30704
|
}
|
|
30244
|
-
#rejectMulticastActivation(
|
|
30245
|
-
if (
|
|
30246
|
-
if (this.#pendingMulticastToken ===
|
|
30705
|
+
#rejectMulticastActivation(token2, err) {
|
|
30706
|
+
if (token2 !== this.#sourceSwitchToken) return;
|
|
30707
|
+
if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
|
|
30247
30708
|
this.#pendingMulticastVideo = null;
|
|
30248
30709
|
this.#pendingMulticastAudio = null;
|
|
30249
30710
|
console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
|
|
@@ -31212,63 +31673,65 @@ var FecStarvationDetector = class {
|
|
|
31212
31673
|
|
|
31213
31674
|
// src/cmcd-producer.ts
|
|
31214
31675
|
var PLAY_MODES = ["full", "base-only", "single-track"];
|
|
31215
|
-
|
|
31216
|
-
|
|
31217
|
-
|
|
31218
|
-
|
|
31219
|
-
|
|
31220
|
-
|
|
31221
|
-
|
|
31222
|
-
|
|
31223
|
-
|
|
31224
|
-
|
|
31225
|
-
|
|
31226
|
-
|
|
31227
|
-
|
|
31228
|
-
|
|
31229
|
-
|
|
31230
|
-
|
|
31231
|
-
|
|
31232
|
-
|
|
31233
|
-
}
|
|
31234
|
-
if (snap.multicastSource != null) {
|
|
31235
|
-
params["x-bc-multicast-source"] = snap.multicastSource;
|
|
31236
|
-
}
|
|
31237
|
-
if (snap.amtRelay != null) {
|
|
31238
|
-
params["x-bc-amt-relay"] = snap.amtRelay;
|
|
31239
|
-
}
|
|
31240
|
-
return params;
|
|
31241
|
-
}
|
|
31242
|
-
function formatQueryString(params) {
|
|
31243
|
-
return Object.keys(params).sort().map((key) => {
|
|
31244
|
-
const v = params[key];
|
|
31245
|
-
if (typeof v === "string") return `${key}=${sanitize(v)}`;
|
|
31246
|
-
return `${key}=${v}`;
|
|
31676
|
+
var DEFAULT_INTERVAL_MS = 3e4;
|
|
31677
|
+
var CUSTOM_PREFIX = "com.blockcast-";
|
|
31678
|
+
var MAX_CUSTOM_VALUE = 64;
|
|
31679
|
+
var TRANSPORT_TOKEN = {
|
|
31680
|
+
native: "ssm",
|
|
31681
|
+
amt: "amt",
|
|
31682
|
+
moq: "moq"
|
|
31683
|
+
};
|
|
31684
|
+
function sfString(value) {
|
|
31685
|
+
return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
|
|
31686
|
+
}
|
|
31687
|
+
function encodeCmcdRecord(record) {
|
|
31688
|
+
return Object.keys(record).sort().map((key) => {
|
|
31689
|
+
const value = record[key];
|
|
31690
|
+
if (typeof value === "number") return `${key}=${value}`;
|
|
31691
|
+
if (typeof value === "string") return `${key}=${sfString(value)}`;
|
|
31692
|
+
if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
|
|
31693
|
+
return `${key}=${value.token}`;
|
|
31247
31694
|
}).join(",");
|
|
31248
31695
|
}
|
|
31696
|
+
var token = (value) => ({ token: value });
|
|
31697
|
+
function putCustom(record, name, value) {
|
|
31698
|
+
if (value === void 0) return;
|
|
31699
|
+
if (typeof value === "number") value = String(value);
|
|
31700
|
+
if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
|
|
31701
|
+
record[CUSTOM_PREFIX + name] = value;
|
|
31702
|
+
}
|
|
31249
31703
|
var CmcdProducer = class {
|
|
31250
|
-
#onCmcdParams;
|
|
31251
31704
|
#onCmcdPost;
|
|
31705
|
+
#intervalMs;
|
|
31706
|
+
#sessionId;
|
|
31707
|
+
#now;
|
|
31708
|
+
#sn = 0;
|
|
31709
|
+
#lastHeartbeat = null;
|
|
31252
31710
|
#transport;
|
|
31253
31711
|
#lastStats;
|
|
31254
|
-
// Phase 14 state — Plan 03 Task 2 (D-12, D-15).
|
|
31255
31712
|
#playMode = null;
|
|
31256
31713
|
#deltaFailRate = null;
|
|
31257
31714
|
#switchMiscount = 0;
|
|
31258
31715
|
#manifestStats = null;
|
|
31259
31716
|
#fecFailClosed = null;
|
|
31717
|
+
/** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
|
|
31718
|
+
#lifecycle = null;
|
|
31260
31719
|
constructor(options) {
|
|
31261
|
-
this.#onCmcdParams = options.onCmcdParams;
|
|
31262
31720
|
this.#onCmcdPost = options.onCmcdPost;
|
|
31721
|
+
this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
31722
|
+
if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
|
|
31723
|
+
throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
|
|
31724
|
+
}
|
|
31725
|
+
this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
|
|
31726
|
+
this.#now = options.now ?? Date.now;
|
|
31263
31727
|
}
|
|
31264
31728
|
/** Update the stored transport state. Called by player wiring layer. */
|
|
31265
31729
|
updateTransport(snapshot) {
|
|
31266
31730
|
this.#transport = snapshot;
|
|
31267
31731
|
}
|
|
31268
31732
|
/**
|
|
31269
|
-
* Set the current play-mode
|
|
31270
|
-
*
|
|
31271
|
-
* (CLAUDE.md: error on bad init).
|
|
31733
|
+
* Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
|
|
31734
|
+
* value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
|
|
31272
31735
|
*/
|
|
31273
31736
|
updatePlayMode(mode) {
|
|
31274
31737
|
if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
|
|
@@ -31279,9 +31742,8 @@ var CmcdProducer = class {
|
|
|
31279
31742
|
this.#playMode = mode;
|
|
31280
31743
|
}
|
|
31281
31744
|
/**
|
|
31282
|
-
* Set the delta-track FEC block-failure rate (0..1 inclusive)
|
|
31283
|
-
* `
|
|
31284
|
-
* Omitted on single-track streams.
|
|
31745
|
+
* Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
|
|
31746
|
+
* `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
|
|
31285
31747
|
*/
|
|
31286
31748
|
updateDeltaFailRate(rate) {
|
|
31287
31749
|
if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
|
|
@@ -31291,112 +31753,139 @@ var CmcdProducer = class {
|
|
|
31291
31753
|
}
|
|
31292
31754
|
this.#deltaFailRate = rate;
|
|
31293
31755
|
}
|
|
31294
|
-
/**
|
|
31295
|
-
* Bump the cumulative switching-set misalignment counter. Emitted as
|
|
31296
|
-
* `x-bc-switch-miscount` on snapshots when the counter is > 0.
|
|
31297
|
-
*/
|
|
31756
|
+
/** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
|
|
31298
31757
|
incrementSwitchMiscount() {
|
|
31299
31758
|
this.#switchMiscount += 1;
|
|
31300
31759
|
}
|
|
31301
31760
|
/**
|
|
31302
|
-
* Record that FEC failed closed at runtime (fec-01 6.3.9).
|
|
31303
|
-
*
|
|
31304
|
-
* `
|
|
31305
|
-
* stream playing without FEC is told apart from one
|
|
31761
|
+
* Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
|
|
31762
|
+
* as `e=e` with the reason code in `ec`, and on every later counter report
|
|
31763
|
+
* as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
|
|
31764
|
+
* B_i was undercut), so a stream playing without FEC is told apart from one
|
|
31765
|
+
* with no loss.
|
|
31306
31766
|
*/
|
|
31307
31767
|
updateFecFailClosed(reason) {
|
|
31768
|
+
const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
|
|
31308
31769
|
this.#fecFailClosed = reason;
|
|
31770
|
+
if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
|
|
31309
31771
|
}
|
|
31310
31772
|
updateManifestVerification(stats) {
|
|
31311
31773
|
this.#manifestStats = stats;
|
|
31312
31774
|
}
|
|
31313
31775
|
/**
|
|
31314
|
-
*
|
|
31315
|
-
*
|
|
31316
|
-
*
|
|
31317
|
-
*
|
|
31776
|
+
* Count a video block's resolution from the repair lifecycle: recovered in
|
|
31777
|
+
* band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
|
|
31778
|
+
* as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
|
|
31779
|
+
* the video-track `-fbf`. Until the player feeds this, the three are absent.
|
|
31318
31780
|
*/
|
|
31781
|
+
onRepairLifecycle(event) {
|
|
31782
|
+
this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
|
|
31783
|
+
if (event.type === "recovered" && event.blockClass !== "audio") {
|
|
31784
|
+
if (event.via === "inband") this.#lifecycle.inband++;
|
|
31785
|
+
else this.#lifecycle.fetch++;
|
|
31786
|
+
} else if (event.type === "failed" && event.blockClass === "keyframe") {
|
|
31787
|
+
this.#lifecycle.failedKeyframe++;
|
|
31788
|
+
}
|
|
31789
|
+
}
|
|
31790
|
+
/** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
|
|
31319
31791
|
emitSwitchDrift(driftUs) {
|
|
31320
31792
|
if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
|
|
31321
31793
|
throw new Error(
|
|
31322
31794
|
`[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
|
|
31323
31795
|
);
|
|
31324
31796
|
}
|
|
31325
|
-
const
|
|
31326
|
-
|
|
31327
|
-
|
|
31328
|
-
if (this.#switchMiscount > 0) params["x-bc-switch-miscount"] = this.#switchMiscount;
|
|
31329
|
-
params["x-bc-switch-drift-us"] = driftUs;
|
|
31330
|
-
this.#emit(params);
|
|
31797
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
|
|
31798
|
+
putCustom(record, "sdu", Math.round(driftUs));
|
|
31799
|
+
this.#post([record]);
|
|
31331
31800
|
}
|
|
31332
|
-
/**
|
|
31333
|
-
* Process a FecTrackStats snapshot. Finds the video track, builds
|
|
31334
|
-
* x-bc-* vendor keys, and fires both delivery callbacks.
|
|
31335
|
-
*/
|
|
31801
|
+
/** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
|
|
31336
31802
|
onSnapshot(data) {
|
|
31337
31803
|
const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
|
|
31338
31804
|
if (!videoTrack) return;
|
|
31339
31805
|
this.#lastStats = videoTrack;
|
|
31340
|
-
|
|
31341
|
-
...buildFecParams(videoTrack),
|
|
31342
|
-
...buildTransportParams(this.#transport)
|
|
31343
|
-
};
|
|
31344
|
-
if (this.#playMode !== null) {
|
|
31345
|
-
params["x-bc-play-mode"] = this.#playMode;
|
|
31346
|
-
}
|
|
31347
|
-
if (this.#fecFailClosed) {
|
|
31348
|
-
params["x-bc-fec-fail-closed"] = this.#fecFailClosed.code;
|
|
31349
|
-
params["x-bc-fec-fail-layer"] = this.#fecFailClosed.layer;
|
|
31350
|
-
}
|
|
31351
|
-
if (this.#playMode !== null && this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
|
|
31352
|
-
params["x-bc-delta-fail-rate"] = parseFloat(this.#deltaFailRate.toFixed(4));
|
|
31353
|
-
}
|
|
31354
|
-
if (this.#switchMiscount > 0) {
|
|
31355
|
-
params["x-bc-switch-miscount"] = this.#switchMiscount;
|
|
31356
|
-
}
|
|
31357
|
-
if (this.#manifestStats) {
|
|
31358
|
-
params["x-bc-manifest-verified"] = this.#manifestStats.verified > 0;
|
|
31359
|
-
params["x-bc-manifest-failures"] = Object.values(this.#manifestStats.failures).reduce((total, count) => total + count, 0);
|
|
31360
|
-
}
|
|
31361
|
-
this.#emit(params);
|
|
31806
|
+
this.#heartbeat();
|
|
31362
31807
|
}
|
|
31363
31808
|
/**
|
|
31364
|
-
*
|
|
31365
|
-
*
|
|
31809
|
+
* Take transport state for receivers that do not own FEC stats, and send the
|
|
31810
|
+
* heartbeat when due.
|
|
31366
31811
|
*/
|
|
31367
31812
|
onTransportSnapshot(data) {
|
|
31368
31813
|
if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
|
|
31369
31814
|
throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
|
|
31370
31815
|
}
|
|
31371
31816
|
if (data.playMode !== void 0) this.updatePlayMode(data.playMode);
|
|
31372
|
-
if (data.switchMiscount !== void 0
|
|
31373
|
-
|
|
31817
|
+
if (data.switchMiscount !== void 0) {
|
|
31818
|
+
if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
|
|
31819
|
+
throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
|
|
31820
|
+
}
|
|
31821
|
+
this.#switchMiscount = data.switchMiscount;
|
|
31374
31822
|
}
|
|
31375
31823
|
this.#transport = data.transport;
|
|
31376
|
-
|
|
31377
|
-
if (this.#playMode !== null) params["x-bc-play-mode"] = this.#playMode;
|
|
31378
|
-
const switchMiscount = data.switchMiscount ?? this.#switchMiscount;
|
|
31379
|
-
if (switchMiscount > 0) params["x-bc-switch-miscount"] = switchMiscount;
|
|
31380
|
-
this.#emit(params);
|
|
31824
|
+
this.#heartbeat();
|
|
31381
31825
|
}
|
|
31382
|
-
/**
|
|
31383
|
-
* Signal a transport transition event. Fires an immediate out-of-band
|
|
31384
|
-
* emit with x-bc-ct set. The subsequent onSnapshot() will NOT include
|
|
31385
|
-
* x-bc-ct (one-shot).
|
|
31386
|
-
*/
|
|
31826
|
+
/** Report a transport transition at once, as custom event `com.blockcast-ct`. */
|
|
31387
31827
|
onTransition(event) {
|
|
31388
|
-
const
|
|
31389
|
-
|
|
31390
|
-
|
|
31828
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
|
|
31829
|
+
putCustom(record, "ct", token(event));
|
|
31830
|
+
this.#putTransport(record);
|
|
31831
|
+
this.#post([record]);
|
|
31832
|
+
}
|
|
31833
|
+
#base(event) {
|
|
31834
|
+
return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
|
|
31835
|
+
}
|
|
31836
|
+
#heartbeat() {
|
|
31837
|
+
const now = this.#now();
|
|
31838
|
+
if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
|
|
31839
|
+
this.#lastHeartbeat = now;
|
|
31840
|
+
this.#post([this.#base("t"), this.#counters()]);
|
|
31841
|
+
}
|
|
31842
|
+
#putTransport(record) {
|
|
31843
|
+
const snap = this.#transport;
|
|
31844
|
+
if (!snap) return;
|
|
31845
|
+
putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
|
|
31846
|
+
if (snap.multicastGroup != null && snap.multicastSource != null) {
|
|
31847
|
+
putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
|
|
31848
|
+
}
|
|
31849
|
+
putCustom(record, "ar", snap.amtRelay);
|
|
31850
|
+
}
|
|
31851
|
+
/** The `e=ce` counter report. */
|
|
31852
|
+
#counters() {
|
|
31853
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
|
|
31854
|
+
const stats = this.#lastStats;
|
|
31855
|
+
if (stats) {
|
|
31856
|
+
putCustom(record, "fa", stats.config.algorithm);
|
|
31857
|
+
putCustom(record, "fk", stats.config.k);
|
|
31858
|
+
putCustom(record, "fp", stats.config.repairCount);
|
|
31859
|
+
putCustom(record, "pr", stats.sourceSymbols);
|
|
31860
|
+
putCustom(record, "pl", stats.sourcePacketsMissing);
|
|
31861
|
+
putCustom(record, "pu", stats.sourcePacketsUnrecovered);
|
|
31862
|
+
putCustom(record, "fbf", stats.blocksFailed);
|
|
31863
|
+
}
|
|
31864
|
+
if (this.#lifecycle) {
|
|
31865
|
+
putCustom(record, "fbr", this.#lifecycle.inband);
|
|
31866
|
+
putCustom(record, "fbq", this.#lifecycle.fetch);
|
|
31867
|
+
putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
|
|
31868
|
+
}
|
|
31869
|
+
this.#putTransport(record);
|
|
31870
|
+
if (this.#playMode !== null) {
|
|
31871
|
+
putCustom(record, "pm", token(this.#playMode));
|
|
31872
|
+
if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
|
|
31873
|
+
putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
|
|
31874
|
+
}
|
|
31391
31875
|
}
|
|
31392
|
-
|
|
31393
|
-
|
|
31394
|
-
|
|
31876
|
+
if (this.#fecFailClosed) {
|
|
31877
|
+
putCustom(record, "ffc", token(this.#fecFailClosed.code));
|
|
31878
|
+
putCustom(record, "ffl", this.#fecFailClosed.layer);
|
|
31879
|
+
}
|
|
31880
|
+
if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
|
|
31881
|
+
if (this.#manifestStats) {
|
|
31882
|
+
putCustom(record, "mv", this.#manifestStats.verified);
|
|
31883
|
+
putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
|
|
31884
|
+
}
|
|
31885
|
+
return record;
|
|
31395
31886
|
}
|
|
31396
|
-
|
|
31397
|
-
|
|
31398
|
-
this.#onCmcdParams?.(params);
|
|
31399
|
-
this.#onCmcdPost?.(formatQueryString(params));
|
|
31887
|
+
#post(records) {
|
|
31888
|
+
this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
|
|
31400
31889
|
}
|
|
31401
31890
|
};
|
|
31402
31891
|
|
|
@@ -32891,6 +33380,7 @@ export {
|
|
|
32891
33380
|
detectH264Keyframe,
|
|
32892
33381
|
detectHevcKeyframe,
|
|
32893
33382
|
driftCheck,
|
|
33383
|
+
encodeCmcdRecord,
|
|
32894
33384
|
encodeDataSegment,
|
|
32895
33385
|
encodeFecSourceInfo,
|
|
32896
33386
|
encodeLocTimestamp,
|