@blockcast/mmt-container 2.2.0-main.c4fa878f4ff4 → 2.2.0-main.e75bbd7576a6
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 +703 -175
- package/dist/browser-runtime.js.map +3 -3
- 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 +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -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 +18 -1
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +304 -39
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-manager.js +526 -45
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +26 -16
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.js +85 -9
- package/dist/no-modules.js.map +2 -2
- package/dist/repair-layer-subscriptions.d.ts +6 -2
- package/dist/repair-layer-subscriptions.d.ts.map +1 -1
- package/dist/repair-layer-subscriptions.js +6 -3
- package/dist/repair-layer-subscriptions.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/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 +7 -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 +350 -13
- package/src/repair-layer-subscriptions.ts +13 -5
- package/src/track-session.ts +1 -1
package/src/mmtp-router.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
type MoqRepairFrame,
|
|
25
25
|
} from "@blockcast/mmt-base";
|
|
26
26
|
import { USER_AGENT_VERIFIED } from "@blockcast/mmt-transport";
|
|
27
|
-
import type { FECManager, IsoHeldSource, WasmFecDecoder } from "./fec-manager.js";
|
|
27
|
+
import type { FECManager, IsoHeldSource, RebuiltSourceFeed, WasmFecDecoder } from "./fec-manager.js";
|
|
28
28
|
import type { IsoPlacedSource, IsoRepairPlacement } from "./iso-block-locator.js";
|
|
29
29
|
import { isMfuReassemblyError } from "./mfu-reassembler.js";
|
|
30
30
|
import type {
|
|
@@ -48,10 +48,12 @@ import {
|
|
|
48
48
|
} from "./cmaf-block-assembler.js";
|
|
49
49
|
import {
|
|
50
50
|
MmtpContainerParser,
|
|
51
|
+
authoredAdjacent,
|
|
51
52
|
isMfuKeyframeMetadata,
|
|
52
53
|
type FecReleaseGate,
|
|
53
54
|
type FrameMeta as ParserFrameMeta,
|
|
54
55
|
type ParsedMmtpPacket,
|
|
56
|
+
type MmtpRecoveredHeadTimingStats,
|
|
55
57
|
} from "./mmtp-container-parser.js";
|
|
56
58
|
import { type TrackRole } from "./carp-catalog.js";
|
|
57
59
|
import {
|
|
@@ -212,6 +214,90 @@ export interface KeyframeOverlayStats {
|
|
|
212
214
|
restarts: number;
|
|
213
215
|
unsequencedDropped: number;
|
|
214
216
|
protectedDropped: number;
|
|
217
|
+
/** Rebuilt packets fed to their base block's decoder (joint decoding). */
|
|
218
|
+
baseFed: number;
|
|
219
|
+
/** Rebuilt packets the base never took: no base SS_ID could be determined. */
|
|
220
|
+
baseUndetermined: number;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The newest received overlay member of one MPU. `psnOffset` (overlay SS_ID
|
|
225
|
+
* minus wire packet_sequence_number) restores a rebuilt packet's wire
|
|
226
|
+
* sequence; the member's own SS_IDs anchor a rebuilt packet's base SS_ID
|
|
227
|
+
* (overlayBaseSsId).
|
|
228
|
+
*/
|
|
229
|
+
interface OverlayMemberAnchor {
|
|
230
|
+
readonly psnOffset: number;
|
|
231
|
+
readonly overlaySsId: number;
|
|
232
|
+
/** The member's base SS_ID, from its Source FEC Payload ID. */
|
|
233
|
+
readonly baseSsId?: number;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The base SS_ID of a packet the overlay rebuilt at `overlaySsId`, which an
|
|
238
|
+
* overlay coordinate alone never yields (draft-ramadan-moq-mmt-02 7.1). Both
|
|
239
|
+
* SS_IDs advance by one per packet along a run of consecutive overlay
|
|
240
|
+
* members, so base minus overlay SS_ID is the anchor's along a run the wire
|
|
241
|
+
* proves to be all members: one over which the rebuilt packet's protected
|
|
242
|
+
* packet_sequence_number (`headerPsn`) advanced from the anchor's exactly as
|
|
243
|
+
* the overlay SS_ID did (`restoredPsn`), so no other packet lies inside it.
|
|
244
|
+
* A hop that re-stamped packet_sequence_number after protection fails the
|
|
245
|
+
* test. Undefined when it fails.
|
|
246
|
+
*/
|
|
247
|
+
function overlayBaseSsId(
|
|
248
|
+
anchor: OverlayMemberAnchor,
|
|
249
|
+
overlaySsId: number,
|
|
250
|
+
restoredPsn: number,
|
|
251
|
+
headerPsn: number,
|
|
252
|
+
): number | undefined {
|
|
253
|
+
if (anchor.baseSsId === undefined || headerPsn !== restoredPsn) return undefined;
|
|
254
|
+
return serial32Add(anchor.baseSsId, (overlaySsId - anchor.overlaySsId) | 0);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The base SS_ID of a rebuilt packet no received member of its MPU anchors,
|
|
259
|
+
* from its protected packet_sequence_number (`headerPsn`). SS_ID advances by
|
|
260
|
+
* one per FEC source packet (mmt-02 7.1) and psn by one per packet, so on a
|
|
261
|
+
* track whose every packet is an FEC source packet the sender keeps
|
|
262
|
+
* SS_ID - psn fixed. `offset` (SS_ID - psn of the stream's latest received
|
|
263
|
+
* source) is that sender value while a base recovery showed it
|
|
264
|
+
* (`unstampedOffset`): a recovered packet's header psn equal to its SS_ID
|
|
265
|
+
* minus the same offset. A hop that re-stamps psn moves the offset away from
|
|
266
|
+
* the one shown, which voids it, as does a packet with no SS_ID between the
|
|
267
|
+
* recovered packet that showed it and the latest received source (the shown
|
|
268
|
+
* span). Undefined without both.
|
|
269
|
+
* ponytail: exact only if every packet from the rebuilt one to the latest
|
|
270
|
+
* received source is an FEC source packet, as FFmpeg's muxer sends a track
|
|
271
|
+
* it protects. fec-core 8.3 lets a publisher leave one unprotected
|
|
272
|
+
* (FEC Type 0): outside the shown span it goes unseen, and each one puts the
|
|
273
|
+
* rebuilt packet a base slot off. If such a publisher appears, have mmt-02
|
|
274
|
+
* require FEC Type 1 across a protected track, or refuse a headerPsn outside
|
|
275
|
+
* the shown span.
|
|
276
|
+
*/
|
|
277
|
+
function unstampedBaseSsId(
|
|
278
|
+
offset: number | undefined,
|
|
279
|
+
unstampedOffset: number | undefined,
|
|
280
|
+
headerPsn: number,
|
|
281
|
+
): number | undefined {
|
|
282
|
+
if (offset === undefined || offset !== unstampedOffset) return undefined;
|
|
283
|
+
return (headerPsn + offset) >>> 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Base SS_IDs of overlay-routed packets kept per stream (see OverlayToBase). */
|
|
287
|
+
const OVERLAY_ROUTED_LIMIT = 1024;
|
|
288
|
+
/** Rebuilt sources kept per stream for a base block no repair stated yet. */
|
|
289
|
+
const OVERLAY_PENDING_LIMIT = 256;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* One stream's keyframe-overlay recoveries on their way into its base
|
|
293
|
+
* decoder (joint decoding, BLO-52085).
|
|
294
|
+
*/
|
|
295
|
+
interface OverlayToBase {
|
|
296
|
+
/** Base SS_IDs of the packets an overlay recovery routed (as media, or held
|
|
297
|
+
* for their FT=1): a base recovery does not route them a second time. */
|
|
298
|
+
readonly routed: Set<number>;
|
|
299
|
+
/** Rebuilt sources, by base SS_ID, whose base block no repair stated yet. */
|
|
300
|
+
readonly pending: Map<number, Uint8Array>;
|
|
215
301
|
}
|
|
216
302
|
|
|
217
303
|
interface OverlayInstance {
|
|
@@ -223,8 +309,8 @@ interface OverlayInstance {
|
|
|
223
309
|
readonly k: number;
|
|
224
310
|
/** The newest overlay symbol seen: its overlay SS_ID and wire sequence. */
|
|
225
311
|
last?: { ssId: number; psn: number };
|
|
226
|
-
/** MPU sequence number →
|
|
227
|
-
readonly offsets: Map<number,
|
|
312
|
+
/** MPU sequence number → its newest received member. */
|
|
313
|
+
readonly offsets: Map<number, OverlayMemberAnchor>;
|
|
228
314
|
/** Overlay blocks a MoQ symbol fed (multicast-02 7.2.7). */
|
|
229
315
|
readonly moqBlocks: Set<number>;
|
|
230
316
|
}
|
|
@@ -511,6 +597,9 @@ export type RoutedPacketMeta = {
|
|
|
511
597
|
mmtpTimestamp: bigint;
|
|
512
598
|
/**
|
|
513
599
|
* The MMTP header RAP flag: the packet starts a random access point. On a
|
|
600
|
+
* track under a keyframe overlay it is also set on the FT=0 and FT=1 data
|
|
601
|
+
* units that govern a covered keyframe (draft-ramadan-moq-mmt-02 7.4.2), so
|
|
602
|
+
* it can precede the keyframe's first MFU fragment. On a
|
|
514
603
|
* `recovered` packet it is read from the FEC-reconstructed header, which is
|
|
515
604
|
* byte-exact outside packet_sequence_number, so it is the flag the
|
|
516
605
|
* publisher sent. The router's own gap decisions deliberately ignore it for
|
|
@@ -574,6 +663,7 @@ interface StreamLedgers {
|
|
|
574
663
|
* the pair.
|
|
575
664
|
*/
|
|
576
665
|
readonly duplicates: StreamDuplicateLedgers;
|
|
666
|
+
readonly overlayToBase: OverlayToBase;
|
|
577
667
|
}
|
|
578
668
|
|
|
579
669
|
const NO_FEC_MANAGER_HORIZON: DuplicateHorizon = Object.freeze({
|
|
@@ -661,6 +751,17 @@ export interface FECManagerLike {
|
|
|
661
751
|
catalogTrackName?: string,
|
|
662
752
|
): Uint8Array | null;
|
|
663
753
|
|
|
754
|
+
/** Feed a source another FEC instance over the same packets rebuilt (a
|
|
755
|
+
* keyframe overlay) to its base block, which a repair must already have
|
|
756
|
+
* stated; not a network arrival (FECManager.feedRebuiltSourceSymbol).
|
|
757
|
+
* Optional: without it the overlay and the base decode apart. */
|
|
758
|
+
feedRebuiltSourceSymbol?(
|
|
759
|
+
packetId: number,
|
|
760
|
+
ssId: number,
|
|
761
|
+
symbolData: Uint8Array,
|
|
762
|
+
catalogTrackName?: string,
|
|
763
|
+
): RebuiltSourceFeed;
|
|
764
|
+
|
|
664
765
|
/** How long ago the stream's oldest open block with an FEC deadline took its first symbol (optional). */
|
|
665
766
|
oldestOpenBlockAgeMs?(packetId: number, catalogTrackName: string): number | undefined;
|
|
666
767
|
|
|
@@ -976,6 +1077,7 @@ export class MmtpRouter {
|
|
|
976
1077
|
readonly #provenance = new Map<string, ProvenanceEntry>();
|
|
977
1078
|
#onFlowProtected: ((flow: string) => void) | null = null;
|
|
978
1079
|
#unadmittedListedDrops = 0;
|
|
1080
|
+
#admittedSourceDatagrams = 0;
|
|
979
1081
|
#unlistedRepairSetAside = 0;
|
|
980
1082
|
readonly #assistBlocks = new Map<string, AssistBlock>();
|
|
981
1083
|
readonly #assistFailures = new Map<string, number>();
|
|
@@ -1044,6 +1146,15 @@ export class MmtpRouter {
|
|
|
1044
1146
|
* sequence is its SS_ID minus this; see #routeProcessedFecSymbols.
|
|
1045
1147
|
*/
|
|
1046
1148
|
#ssIdSeqOffsetByStream = new Map<string, number>();
|
|
1149
|
+
/**
|
|
1150
|
+
* Per stream, the SS_ID offset at which a base recovery last showed the
|
|
1151
|
+
* sender's own packet_sequence_number: a recovered packet's header psn
|
|
1152
|
+
* equal to its SS_ID minus that offset. While it equals the stream's
|
|
1153
|
+
* current offset, no hop re-stamps the stream, and a packet's protected psn
|
|
1154
|
+
* names its base SS_ID within unstampedBaseSsId's limits (joint decoding of
|
|
1155
|
+
* an MPU no member of which arrived).
|
|
1156
|
+
*/
|
|
1157
|
+
#unstampedSsIdSeqOffsetByStream = new Map<string, number>();
|
|
1047
1158
|
/**
|
|
1048
1159
|
* Streams whose multicast leg delivered a RAP-flagged source packet that was
|
|
1049
1160
|
* not parsed, typically because its overlap copy was parsed first
|
|
@@ -1121,7 +1232,7 @@ export class MmtpRouter {
|
|
|
1121
1232
|
#restampedStreams = new Set<string>();
|
|
1122
1233
|
#overlayStats: KeyframeOverlayStats = {
|
|
1123
1234
|
repairDropped: 0, ownerRefused: 0, recovered: 0, restarts: 0,
|
|
1124
|
-
unsequencedDropped: 0, protectedDropped: 0,
|
|
1235
|
+
unsequencedDropped: 0, protectedDropped: 0, baseFed: 0, baseUndetermined: 0,
|
|
1125
1236
|
};
|
|
1126
1237
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
1127
1238
|
#onDeltaBlockCompleteCb:
|
|
@@ -1400,7 +1511,7 @@ export class MmtpRouter {
|
|
|
1400
1511
|
const cb = this.#getFrameCallback(packetId);
|
|
1401
1512
|
if (isVideo && !assembler) {
|
|
1402
1513
|
if (frameMeta.recovered) {
|
|
1403
|
-
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
1514
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb, meta.fecSbn);
|
|
1404
1515
|
} else {
|
|
1405
1516
|
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
1406
1517
|
}
|
|
@@ -1915,6 +2026,11 @@ export class MmtpRouter {
|
|
|
1915
2026
|
return { ...this.#overlayStats };
|
|
1916
2027
|
}
|
|
1917
2028
|
|
|
2029
|
+
/** Recovered FT=2 heads held for their FT=1, and how each left the wait (BLO-53369). */
|
|
2030
|
+
get recoveredHeadsAwaitingTiming(): Readonly<MmtpRecoveredHeadTimingStats> {
|
|
2031
|
+
return this.#parser.recoveredHeadsAwaitingTiming;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
1918
2034
|
#isOverlayRepair(repairPacketId: number, repairTrackName?: string): boolean {
|
|
1919
2035
|
return this.#overlayLegs.has(repairPacketId) ||
|
|
1920
2036
|
(repairTrackName !== undefined && this.#overlayRepairNames.has(repairTrackName));
|
|
@@ -1966,6 +2082,13 @@ export class MmtpRouter {
|
|
|
1966
2082
|
*/
|
|
1967
2083
|
resetKeyframeOverlays(sourcePacketId: number, catalogTrackName?: string): void {
|
|
1968
2084
|
for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
|
|
2085
|
+
// The base restarted with it: base SS_IDs kept for joint decoding name
|
|
2086
|
+
// the old epoch's packets.
|
|
2087
|
+
for (const [name, stream] of this.#streams.get(sourcePacketId) ?? []) {
|
|
2088
|
+
if (catalogTrackName !== undefined && name !== catalogTrackName) continue;
|
|
2089
|
+
stream.overlayToBase.routed.clear();
|
|
2090
|
+
stream.overlayToBase.pending.clear();
|
|
2091
|
+
}
|
|
1969
2092
|
}
|
|
1970
2093
|
|
|
1971
2094
|
#resetOverlay(inst: OverlayInstance): void {
|
|
@@ -2043,7 +2166,8 @@ export class MmtpRouter {
|
|
|
2043
2166
|
* A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
|
|
2044
2167
|
* every overlay installed for its source, at that SS_ID, with the bytes the
|
|
2045
2168
|
* base protects: the packet minus its 4-byte SS_ID trailer, extension
|
|
2046
|
-
* included (7.1, 7.4.3).
|
|
2169
|
+
* included (7.1, 7.4.3). `baseSsId` is that trailer's SS_ID, when the base
|
|
2170
|
+
* FEC reads it.
|
|
2047
2171
|
*/
|
|
2048
2172
|
#feedOverlaySource(
|
|
2049
2173
|
packetId: number,
|
|
@@ -2053,6 +2177,7 @@ export class MmtpRouter {
|
|
|
2053
2177
|
sequenceNumber: number,
|
|
2054
2178
|
rapFlag: boolean,
|
|
2055
2179
|
source: MmtpSourcePath,
|
|
2180
|
+
baseSsId: number | undefined,
|
|
2056
2181
|
): void {
|
|
2057
2182
|
if (this.#overlays.size === 0) return;
|
|
2058
2183
|
const ssId = keyframeOverlaySsId(header);
|
|
@@ -2061,8 +2186,22 @@ export class MmtpRouter {
|
|
|
2061
2186
|
this.#noteOverlaySsId(inst, ssId, sequenceNumber);
|
|
2062
2187
|
const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
|
|
2063
2188
|
if (identity !== null) {
|
|
2189
|
+
// The newest member anchors its MPU; one with no base SS_ID leaves the
|
|
2190
|
+
// MPU no base anchor rather than keep an older member's. A member that
|
|
2191
|
+
// is no FEC source packet of the base takes an overlay SS_ID and a psn
|
|
2192
|
+
// but no base SS_ID, so past it an older anchor would put each rebuilt
|
|
2193
|
+
// packet one base slot high, and overlayBaseSsId's psn test would pass
|
|
2194
|
+
// it. That costs a conforming stream nothing: every member carries the
|
|
2195
|
+
// base trailer (7.4.3), unread only while the base track is
|
|
2196
|
+
// unconfigured (fecUnreadable), which joint decoding skips, and a base
|
|
2197
|
+
// release (MulticastManager) resets or removes the overlay, so no
|
|
2198
|
+
// readable member precedes an unread one in its MPU.
|
|
2064
2199
|
inst.offsets.delete(identity.mpuSequenceNumber);
|
|
2065
|
-
inst.offsets.set(identity.mpuSequenceNumber,
|
|
2200
|
+
inst.offsets.set(identity.mpuSequenceNumber, {
|
|
2201
|
+
psnOffset: (ssId - sequenceNumber) >>> 0,
|
|
2202
|
+
overlaySsId: ssId,
|
|
2203
|
+
...(baseSsId === undefined ? {} : { baseSsId }),
|
|
2204
|
+
});
|
|
2066
2205
|
if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value!);
|
|
2067
2206
|
}
|
|
2068
2207
|
const { fec, repairPacketId: pid, streamName: name } = inst;
|
|
@@ -2149,18 +2288,35 @@ export class MmtpRouter {
|
|
|
2149
2288
|
const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
|
|
2150
2289
|
const restamped = this.#restampedStreams.has(streamKey);
|
|
2151
2290
|
const restored = new Set<number>();
|
|
2291
|
+
const baseSsIds = new Map<ProcessFecBlockResult["symbols"][number], number>();
|
|
2152
2292
|
const symbols = result.symbols.flatMap((sym) => {
|
|
2153
|
-
const
|
|
2154
|
-
if (
|
|
2155
|
-
const
|
|
2293
|
+
const anchor = inst.offsets.get(sym.fragment.mpuSequenceNumber);
|
|
2294
|
+
if (anchor !== undefined) {
|
|
2295
|
+
const overlaySsId = serial32Add(blockStart, sym.index);
|
|
2296
|
+
const packetSequenceNumber = (overlaySsId - anchor.psnOffset) >>> 0;
|
|
2156
2297
|
restored.add(packetSequenceNumber);
|
|
2157
|
-
|
|
2298
|
+
const out = { ...sym, packetSequenceNumber };
|
|
2299
|
+
const baseSsId = overlayBaseSsId(anchor, overlaySsId, packetSequenceNumber, sym.packetSequenceNumber);
|
|
2300
|
+
if (baseSsId !== undefined) baseSsIds.set(out, baseSsId);
|
|
2301
|
+
return [out];
|
|
2158
2302
|
}
|
|
2159
2303
|
// A stream re-stamped by a bridge that no base recovery has shown yet
|
|
2160
2304
|
// keeps the header's sequence too: from received packets alone the
|
|
2161
2305
|
// router cannot tell (their psn is the wire one). Base recovery or a
|
|
2162
2306
|
// FETCH still covers such a keyframe.
|
|
2163
|
-
if (!restamped)
|
|
2307
|
+
if (!restamped) {
|
|
2308
|
+
// No member of this MPU arrived to anchor a base SS_ID. A base
|
|
2309
|
+
// recovery that showed the stream unstamped at its current offset
|
|
2310
|
+
// determines one from the packet's protected psn instead
|
|
2311
|
+
// (unstampedBaseSsId).
|
|
2312
|
+
const baseSsId = unstampedBaseSsId(
|
|
2313
|
+
this.#ssIdSeqOffsetByStream.get(streamKey),
|
|
2314
|
+
this.#unstampedSsIdSeqOffsetByStream.get(streamKey),
|
|
2315
|
+
sym.packetSequenceNumber,
|
|
2316
|
+
);
|
|
2317
|
+
if (baseSsId !== undefined) baseSsIds.set(sym, baseSsId);
|
|
2318
|
+
return [sym];
|
|
2319
|
+
}
|
|
2164
2320
|
this.#overlayStats.unsequencedDropped++;
|
|
2165
2321
|
return [];
|
|
2166
2322
|
});
|
|
@@ -2170,6 +2326,98 @@ export class MmtpRouter {
|
|
|
2170
2326
|
});
|
|
2171
2327
|
this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) =>
|
|
2172
2328
|
restored.has(sym.packetSequenceNumber)));
|
|
2329
|
+
this.#feedOverlayRecoveredToBase(inst, blockStart, symbols, baseSsIds);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
/**
|
|
2333
|
+
* Joint decoding (BLO-52085). A packet the overlay rebuilt is a source of
|
|
2334
|
+
* its base block too, once its base SS_ID is determined (overlayBaseSsId,
|
|
2335
|
+
* from a received member of its MPU, else unstampedBaseSsId),
|
|
2336
|
+
* so it is fed to the base decoder as a rebuilt source
|
|
2337
|
+
* (FECManager.feedRebuiltSourceSymbol): the base block has one erasure
|
|
2338
|
+
* fewer to solve, and its repair can recover the block's other packets.
|
|
2339
|
+
* A base block no repair has stated yet takes it when one does
|
|
2340
|
+
* (#feedPendingRebuilt).
|
|
2341
|
+
*
|
|
2342
|
+
* The overlay route already handled each packet fed (delivered it, or
|
|
2343
|
+
* held an untimed head for its FT=1), so a base recovery routes none of
|
|
2344
|
+
* them again (#routeVerifiedFecSymbols): a second recovered copy of a
|
|
2345
|
+
* held head would wait in the parser for timing its first copy consumed.
|
|
2346
|
+
* Only an overlay block decoded from multicast symbols alone is fed: with
|
|
2347
|
+
* a MoQ symbol in it, the base block's recovery would depend on bytes
|
|
2348
|
+
* that crossed no multicast socket, which the provenance counted from the
|
|
2349
|
+
* block's own arrivals cannot show.
|
|
2350
|
+
*/
|
|
2351
|
+
#feedOverlayRecoveredToBase(
|
|
2352
|
+
inst: OverlayInstance,
|
|
2353
|
+
blockStart: number,
|
|
2354
|
+
symbols: ProcessFecBlockResult["symbols"],
|
|
2355
|
+
baseSsIds: ReadonlyMap<ProcessFecBlockResult["symbols"][number], number>,
|
|
2356
|
+
): void {
|
|
2357
|
+
const fec = this.#fecManager;
|
|
2358
|
+
const owner = inst.leg.sourcePacketId;
|
|
2359
|
+
const name = inst.leg.sourceCatalogTrackName;
|
|
2360
|
+
if (!fec?.feedRebuiltSourceSymbol || inst.moqBlocks.has(blockStart)) return;
|
|
2361
|
+
if (this.#classifyPacketId(owner) === null || fec.isTrackConfigured?.(owner) === false) return;
|
|
2362
|
+
const { duplicates, overlayToBase: { routed } } = this.#stream(owner, name);
|
|
2363
|
+
const feeds: Array<readonly [number, Uint8Array]> = [];
|
|
2364
|
+
for (const sym of symbols) {
|
|
2365
|
+
// A packet the base received already sits in its block.
|
|
2366
|
+
if (duplicates.sourceSeqs.has(sym.packetSequenceNumber)) continue;
|
|
2367
|
+
const ssId = baseSsIds.get(sym);
|
|
2368
|
+
if (ssId === undefined) {
|
|
2369
|
+
this.#overlayStats.baseUndetermined++;
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
routed.delete(ssId);
|
|
2373
|
+
routed.add(ssId);
|
|
2374
|
+
if (routed.size > OVERLAY_ROUTED_LIMIT) routed.delete(routed.values().next().value!);
|
|
2375
|
+
feeds.push([ssId, sym.rawSymbol]);
|
|
2376
|
+
}
|
|
2377
|
+
// Any feed can recover the base block, whose routing must already skip
|
|
2378
|
+
// every packet this recovery routed: all are registered before the first.
|
|
2379
|
+
for (const [ssId, symbolData] of feeds) {
|
|
2380
|
+
// A recovery routed above can retire the owner (an in-place switch).
|
|
2381
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
2382
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
/** Feed one rebuilt source to its base block, or keep it until a repair states that block. */
|
|
2387
|
+
#feedRebuiltToBase(owner: number, name: string | undefined, ssId: number, symbolData: Uint8Array): void {
|
|
2388
|
+
const fec = this.#fecManager;
|
|
2389
|
+
if (!fec?.feedRebuiltSourceSymbol) return;
|
|
2390
|
+
const coords = fec.locateIsoSource?.(owner, name, ssId);
|
|
2391
|
+
if (coords == null) {
|
|
2392
|
+
const pending = this.#stream(owner, name).overlayToBase.pending;
|
|
2393
|
+
pending.delete(ssId);
|
|
2394
|
+
// Its own copy: the symbol is a view of the whole recovered block.
|
|
2395
|
+
pending.set(ssId, symbolData.slice());
|
|
2396
|
+
if (pending.size > OVERLAY_PENDING_LIMIT) pending.delete(pending.keys().next().value!);
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
const fed = fec.feedRebuiltSourceSymbol(owner, ssId, symbolData, name);
|
|
2400
|
+
if (!fed.taken) return;
|
|
2401
|
+
this.#overlayStats.baseFed++;
|
|
2402
|
+
if (fed.recovered === null) return;
|
|
2403
|
+
const sbn = coords.sbn;
|
|
2404
|
+
this.#routeRecoveredBlock(sbn, fed.recovered, owner, name, () => {
|
|
2405
|
+
fec.cancelBlockTimeout(sbn, owner, name);
|
|
2406
|
+
fec.countRecovery(owner, sbn, name);
|
|
2407
|
+
fec.markBlockCompleted(sbn, owner, name);
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
/** A repair stated `block`: the rebuilt sources that waited for it join it now. */
|
|
2412
|
+
#feedPendingRebuilt(owner: number, name: string | undefined, block: { start: number; length: number }): void {
|
|
2413
|
+
const pending = this.#streams.get(owner)?.get(name)?.overlayToBase.pending;
|
|
2414
|
+
if (pending === undefined || pending.size === 0) return;
|
|
2415
|
+
for (const [ssId, symbolData] of [...pending]) {
|
|
2416
|
+
if (this.#classifyPacketId(owner) === null) return;
|
|
2417
|
+
if (serial32Distance(block.start, ssId) >= block.length) continue;
|
|
2418
|
+
pending.delete(ssId);
|
|
2419
|
+
this.#feedRebuiltToBase(owner, name, ssId, symbolData);
|
|
2420
|
+
}
|
|
2173
2421
|
}
|
|
2174
2422
|
|
|
2175
2423
|
/** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
|
|
@@ -2205,6 +2453,9 @@ export class MmtpRouter {
|
|
|
2205
2453
|
const mpu = sym.fragment.mpuSequenceNumber;
|
|
2206
2454
|
if (sym.fragmentType !== 2 || !sym.fragment.rapFlag || !ft0.has(mpu) || !ft1.has(mpu)) continue;
|
|
2207
2455
|
if (!single.has(mpu) && !(first.has(mpu) && last.has(mpu))) continue;
|
|
2456
|
+
// Not overlayBaseSsId: a wrong sbn only keeps or wrongly stops a keyframe
|
|
2457
|
+
// FETCH (§6.6, a SHOULD; no symbol moves), and wire psn plus offset holds
|
|
2458
|
+
// under a constant re-stamp shift, which overlayBaseSsId's psn test refuses.
|
|
2208
2459
|
const ssId = (sym.packetSequenceNumber + ssIdSeqOffset) >>> 0;
|
|
2209
2460
|
// ponytail: a base block no repair has stated yet (an undelimited hole) is not reported.
|
|
2210
2461
|
const sbn = this.#fecManager?.locateIsoSource?.(packetId, catalogTrackName, ssId)?.sbn;
|
|
@@ -2355,6 +2606,11 @@ export class MmtpRouter {
|
|
|
2355
2606
|
return this.#unadmittedListedDrops;
|
|
2356
2607
|
}
|
|
2357
2608
|
|
|
2609
|
+
/** 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. */
|
|
2610
|
+
get admittedSourceDatagramCount(): number {
|
|
2611
|
+
return this.#admittedSourceDatagrams;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2358
2614
|
/** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
|
|
2359
2615
|
get unlistedRepairSetAsideCount(): number {
|
|
2360
2616
|
return this.#unlistedRepairSetAside;
|
|
@@ -2989,6 +3245,11 @@ export class MmtpRouter {
|
|
|
2989
3245
|
)) return;
|
|
2990
3246
|
fec.noteBlockActivity?.(blockStart, ownerPacketId, catalogTrackName);
|
|
2991
3247
|
fec.noteRepairGroupLength?.(blockStart, ownerPacketId, repair.rsbLength, catalogTrackName);
|
|
3248
|
+
// Rebuilt sources that waited for this block join it once the block took
|
|
3249
|
+
// a repair, so it has a deadline; still ahead of the repair, as the held
|
|
3250
|
+
// sources above are.
|
|
3251
|
+
this.#feedPendingRebuilt(ownerPacketId, catalogTrackName, placement.block);
|
|
3252
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return;
|
|
2992
3253
|
const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
2993
3254
|
const recovered = fec.feedRepairSymbol(
|
|
2994
3255
|
ownerPacketId,
|
|
@@ -3169,6 +3430,7 @@ export class MmtpRouter {
|
|
|
3169
3430
|
this.#droppedPreSyncVideo.delete(streamKey);
|
|
3170
3431
|
this.#latestVideoSeqByStream.delete(streamKey);
|
|
3171
3432
|
this.#ssIdSeqOffsetByStream.delete(streamKey);
|
|
3433
|
+
this.#unstampedSsIdSeqOffsetByStream.delete(streamKey);
|
|
3172
3434
|
this.#multicastRapTwinByStream.delete(streamKey);
|
|
3173
3435
|
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
3174
3436
|
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
@@ -3928,6 +4190,8 @@ export class MmtpRouter {
|
|
|
3928
4190
|
const parsed = this.#parser.parsePacket(packet);
|
|
3929
4191
|
if (!parsed) return;
|
|
3930
4192
|
const info = parsed.info;
|
|
4193
|
+
// manifestAdmitted is set only by a receiver release or the user agent's own verification.
|
|
4194
|
+
if (manifestAdmitted && !info.isRepair && ingress?.admittedBy !== USER_AGENT_VERIFIED) this.#admittedSourceDatagrams++;
|
|
3931
4195
|
// Every multicast packet is a delivery-gap sample, including one dropped
|
|
3932
4196
|
// below: the gap is a property of the link the FEC deadlines run on, not
|
|
3933
4197
|
// of which packets this router routes. The re-entry after a manifest
|
|
@@ -4391,6 +4655,7 @@ export class MmtpRouter {
|
|
|
4391
4655
|
if (!overlapCopy && this.#classifyPacketId(info.packetId) !== null) {
|
|
4392
4656
|
this.#feedOverlaySource(
|
|
4393
4657
|
info.packetId, catalogTrackName, packet, parsed.header, info.sequenceNumber, info.rapFlag, source,
|
|
4658
|
+
fecPayloadId?.ssId,
|
|
4394
4659
|
);
|
|
4395
4660
|
}
|
|
4396
4661
|
}
|
|
@@ -4619,6 +4884,7 @@ export class MmtpRouter {
|
|
|
4619
4884
|
stream = {
|
|
4620
4885
|
key: JSON.stringify([packetId, catalogTrackName ?? null]),
|
|
4621
4886
|
duplicates: new StreamDuplicateLedgers(() => this.duplicateHorizonFor(packetId)),
|
|
4887
|
+
overlayToBase: { routed: new Set(), pending: new Map() },
|
|
4622
4888
|
};
|
|
4623
4889
|
byName.set(catalogTrackName, stream);
|
|
4624
4890
|
}
|
|
@@ -4817,8 +5083,28 @@ export class MmtpRouter {
|
|
|
4817
5083
|
data: Uint8Array,
|
|
4818
5084
|
meta: RoutedFrameMeta,
|
|
4819
5085
|
cb: FrameCallback | null,
|
|
5086
|
+
/** The base SS_ID the FEC release gate ordered the frame by, if any. */
|
|
5087
|
+
fecSbn: number | undefined,
|
|
4820
5088
|
): void {
|
|
4821
5089
|
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
5090
|
+
// The FEC release gate let this frame out in order: only once every
|
|
5091
|
+
// block between it and the frame before it resolved, or because it
|
|
5092
|
+
// starts where that frame ended. Nothing still in flight can precede it
|
|
5093
|
+
// then, so it plays now, as a source frame does; holding it for the next
|
|
5094
|
+
// intact frame cost every repaired burst its length in decoder input
|
|
5095
|
+
// (BLO-52085). An overlay recovery names no base block for the gate to
|
|
5096
|
+
// wait on, so past a gap it may be ahead of frames still in flight (an
|
|
5097
|
+
// overlay that outran the wire, BLO-53342): it keeps waiting for the
|
|
5098
|
+
// source cursor below, unless it starts where the last emitted frame
|
|
5099
|
+
// ended (on this direct path, the delivered high water).
|
|
5100
|
+
if (this.#isFecTrack(packetId)) {
|
|
5101
|
+
const high = this.#deliveredVideoHighWaterByStream.get(streamKey);
|
|
5102
|
+
if (fecSbn !== undefined || (high !== undefined && authoredAdjacent(high, meta))) {
|
|
5103
|
+
this.#drainDirectRecoveredVideo(streamKey, packetId, frame => frame.timestamp <= meta.timestamp, cb);
|
|
5104
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
5105
|
+
return;
|
|
5106
|
+
}
|
|
5107
|
+
}
|
|
4822
5108
|
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
4823
5109
|
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
4824
5110
|
this.#drainDirectRecoveredVideo(
|
|
@@ -5056,6 +5342,17 @@ export class MmtpRouter {
|
|
|
5056
5342
|
complete?: () => void,
|
|
5057
5343
|
assisted = false,
|
|
5058
5344
|
): boolean {
|
|
5345
|
+
// A packet a keyframe overlay recovery routed before it fed the packet to
|
|
5346
|
+
// this block (#feedOverlayRecoveredToBase) is not routed again.
|
|
5347
|
+
const overlayRouted = ownerPacketId == null
|
|
5348
|
+
? undefined
|
|
5349
|
+
: this.#streams.get(ownerPacketId)?.get(catalogTrackName)?.overlayToBase.routed;
|
|
5350
|
+
if (overlayRouted !== undefined && overlayRouted.size > 0) {
|
|
5351
|
+
result = {
|
|
5352
|
+
...result,
|
|
5353
|
+
symbols: result.symbols.filter((sym) => !overlayRouted.has(this.#fecAnchorForRecovered(blockId, sym.index))),
|
|
5354
|
+
};
|
|
5355
|
+
}
|
|
5059
5356
|
const entry = ownerPacketId == null ? undefined : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
5060
5357
|
const receiver = entry === undefined || entry === "ambiguous" ? entry : entry.receiver;
|
|
5061
5358
|
const userAgent = !assisted && entry !== undefined && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
@@ -5149,7 +5446,10 @@ export class MmtpRouter {
|
|
|
5149
5446
|
...result,
|
|
5150
5447
|
symbols: result.symbols.map((sym) => {
|
|
5151
5448
|
const wire = (this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset) >>> 0;
|
|
5152
|
-
if (wire === sym.packetSequenceNumber)
|
|
5449
|
+
if (wire === sym.packetSequenceNumber) {
|
|
5450
|
+
this.#unstampedSsIdSeqOffsetByStream.set(streamKey, ssIdSeqOffset);
|
|
5451
|
+
return sym;
|
|
5452
|
+
}
|
|
5153
5453
|
this.#restampedRecoveredSeqs++;
|
|
5154
5454
|
this.#restampedStreams.add(streamKey);
|
|
5155
5455
|
return { ...sym, packetSequenceNumber: wire };
|
|
@@ -5206,6 +5506,43 @@ export class MmtpRouter {
|
|
|
5206
5506
|
);
|
|
5207
5507
|
}
|
|
5208
5508
|
|
|
5509
|
+
// A recovered head whose sample FT=1 has not named yet (an overlay that
|
|
5510
|
+
// outran the source path) would complete its data unit untimed: the unit
|
|
5511
|
+
// is dropped, marked completed, and the timed source copy is then refused
|
|
5512
|
+
// (BLO-53342). Hand it to the parser instead, which holds it in its
|
|
5513
|
+
// awaiting-timing queue and feeds it again when FT=1 lands; its recovered
|
|
5514
|
+
// continuations park in the reassembler meanwhile. If the source path
|
|
5515
|
+
// lost the head, this held copy is the keyframe (BLO-53369).
|
|
5516
|
+
const untimedHead = (sym: (typeof result.symbols)[number]): boolean =>
|
|
5517
|
+
sym.fragmentType === 2 &&
|
|
5518
|
+
sym.fragment.fragmentationIndicator === 1 &&
|
|
5519
|
+
sym.fragment.movieFragmentSequenceNumber !== undefined &&
|
|
5520
|
+
sym.fragment.sampleNumber !== undefined &&
|
|
5521
|
+
!this.#parser.hasAuthoredTimingFor(
|
|
5522
|
+
ownerPacketId,
|
|
5523
|
+
sym.fragment.mpuSequenceNumber,
|
|
5524
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
5525
|
+
sym.fragment.sampleNumber,
|
|
5526
|
+
catalogTrackName,
|
|
5527
|
+
);
|
|
5528
|
+
for (const sym of result.symbols) {
|
|
5529
|
+
if (!untimedHead(sym) ||
|
|
5530
|
+
this.#sourceSeqReceived(ownerPacketId, sym.packetSequenceNumber, catalogTrackName)) continue;
|
|
5531
|
+
this.#feedViaParser(
|
|
5532
|
+
sym.rawSymbol,
|
|
5533
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
5534
|
+
undefined,
|
|
5535
|
+
{
|
|
5536
|
+
skipFecPayloadId: true,
|
|
5537
|
+
recovered: true,
|
|
5538
|
+
deliveryProvenance,
|
|
5539
|
+
...fecSbnOf(sym.index),
|
|
5540
|
+
...(catalogTrackName === undefined ? {} : { catalogTrackName }),
|
|
5541
|
+
},
|
|
5542
|
+
);
|
|
5543
|
+
}
|
|
5544
|
+
result = { ...result, symbols: result.symbols.filter((sym) => !untimedHead(sym)) };
|
|
5545
|
+
|
|
5209
5546
|
// MSE assembler path: route recovered block as pre-assembled frames (skip FT=0)
|
|
5210
5547
|
// BLO-3035 layer 3: recovered symbols are RAW MFU fragments, not complete
|
|
5211
5548
|
// frames. A keyframe can span multiple packets (FI=1/2/3); pushing each
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
* base (`MmtpRouter.feedRepairPacket(..., repairTrackName)`).
|
|
16
16
|
*
|
|
17
17
|
* `stream` delivery means QUIC retransmits: no repair track is ever
|
|
18
|
-
* subscribed (FEC only for unreliable transports).
|
|
18
|
+
* subscribed (FEC only for unreliable transports). The mode is the source's
|
|
19
|
+
* `deliveryMode` (`catalogTrackDeliveryMode`); absent is `datagram`. It is
|
|
20
|
+
* read from the current source, so on a catalog refresh re-feed the updated
|
|
21
|
+
* source through `switchSource` for a changed `deliveryMode` to take effect.
|
|
19
22
|
*
|
|
20
23
|
* ABR switch R -> R' (`switchSource`): R's subscriptions are closed, the layer
|
|
21
24
|
* count carries to R', and any packet R's subscriptions deliver after the
|
|
@@ -35,6 +38,8 @@ export interface RepairLayerSource {
|
|
|
35
38
|
repairLayers?: readonly RepairLayerParams[]
|
|
36
39
|
/** Keyframe overlays (`TrackFecParams.repairOverlays`); at most one is used. */
|
|
37
40
|
repairOverlays?: readonly RepairOverlayParams[]
|
|
41
|
+
/** The publisher's MoQ delivery mode; absent is `datagram`. */
|
|
42
|
+
deliveryMode?: MoqRepairDelivery
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export interface RepairTrackSubscription {
|
|
@@ -43,7 +48,6 @@ export interface RepairTrackSubscription {
|
|
|
43
48
|
|
|
44
49
|
export interface RepairLayerSubscriptionsOptions {
|
|
45
50
|
source: RepairLayerSource
|
|
46
|
-
delivery: MoqRepairDelivery
|
|
47
51
|
/** Subscribe one repair track; every object payload goes to `onPacket`. */
|
|
48
52
|
subscribe(
|
|
49
53
|
repairTrackName: string,
|
|
@@ -110,7 +114,7 @@ export class RepairLayerSubscriptions {
|
|
|
110
114
|
* or interleaved sequences read as loss.
|
|
111
115
|
*/
|
|
112
116
|
noteSourceSequence(seq: number): void {
|
|
113
|
-
if (this.#disposed || this.#
|
|
117
|
+
if (this.#disposed || this.#delivery() === 'stream') return
|
|
114
118
|
seq >>>= 0
|
|
115
119
|
if (this.#lastSeq === undefined) {
|
|
116
120
|
this.#lastSeq = seq
|
|
@@ -152,13 +156,13 @@ export class RepairLayerSubscriptions {
|
|
|
152
156
|
this.#expected = 0
|
|
153
157
|
this.#lost = 0
|
|
154
158
|
this.#wantFewer = 0
|
|
155
|
-
this.#layerCount = Math.min(this.#layerCount, this.#layers().length)
|
|
159
|
+
this.#layerCount = this.#delivery() === 'stream' ? 0 : Math.min(this.#layerCount, this.#layers().length)
|
|
156
160
|
this.#reconcile()
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
snapshot(): RepairLayerSubscriptionsSnapshot {
|
|
160
164
|
return {
|
|
161
|
-
delivery: this.#
|
|
165
|
+
delivery: this.#delivery(),
|
|
162
166
|
layerCount: this.#layerCount,
|
|
163
167
|
subscribedTracks: [...this.#subs.keys()],
|
|
164
168
|
repairPackets: this.#repairPackets,
|
|
@@ -174,6 +178,10 @@ export class RepairLayerSubscriptions {
|
|
|
174
178
|
this.#disposed = true
|
|
175
179
|
}
|
|
176
180
|
|
|
181
|
+
#delivery(): MoqRepairDelivery {
|
|
182
|
+
return this.#source.deliveryMode === 'stream' ? 'stream' : 'datagram'
|
|
183
|
+
}
|
|
184
|
+
|
|
177
185
|
#layers(): readonly RepairLayerParams[] {
|
|
178
186
|
return this.#source.repairLayers ?? []
|
|
179
187
|
}
|
package/src/track-session.ts
CHANGED
|
@@ -58,7 +58,7 @@ export interface TrackSessionConfig {
|
|
|
58
58
|
* This is runtime join state, not catalog geometry: a receiver on a clean
|
|
59
59
|
* path joins layer 0 only while a lossy one adds layers, so the catalog's
|
|
60
60
|
* total P is not what this receiver is receiving. Diagnostics that report P
|
|
61
|
-
* (the worker snapshot, CMCD `
|
|
61
|
+
* (the worker snapshot, CMCD `com.blockcast-fp`) must read this.
|
|
62
62
|
*
|
|
63
63
|
* Absent when the caller joins no layers explicitly, in which case P_0 is
|
|
64
64
|
* what was joined. That is an identity on a single-layer track, not a
|