@blockcast/mmt-container 0.4.1 → 0.5.0-main.34c7b9c2cfa0
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 +2 -2
- package/dist/browser-runtime.js +3769 -364
- package/dist/browser-runtime.js.map +4 -4
- package/dist/cmcd-producer.d.ts +5 -1
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +18 -2
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/fec-manager.d.ts +21 -16
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +197 -80
- package/dist/fec-manager.js.map +3 -3
- package/dist/held-layer-admission.d.ts +58 -0
- package/dist/held-layer-admission.d.ts.map +1 -0
- package/dist/held-layer-admission.js +105 -0
- package/dist/held-layer-admission.js.map +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/mmtp-container-parser.d.ts +10 -0
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +31 -1
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +2713 -1024
- package/dist/mmtp-container-parser.no-modules.js.map +4 -4
- package/dist/mmtp-decode-pipeline.js +31 -1
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-fec-session.d.ts +36 -3
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +114 -5
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +96 -29
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +522 -205
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-auth-catalog.d.ts +10 -2
- package/dist/multicast-auth-catalog.d.ts.map +1 -1
- package/dist/multicast-auth-catalog.js +422 -22
- package/dist/multicast-auth-catalog.js.map +4 -4
- package/dist/multicast-manager.d.ts +78 -11
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +3663 -333
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +4 -1
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +20 -3
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.d.ts +1 -2
- package/dist/no-modules.d.ts.map +1 -1
- package/dist/no-modules.js +2699 -1008
- package/dist/no-modules.js.map +4 -4
- package/dist/repair-fetch.d.ts +50 -10
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +62 -20
- package/dist/repair-fetch.js.map +2 -2
- package/dist/wasm-mmtp-container-parser.js +31 -1
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +7 -15
- package/src/cmcd-producer.ts +6 -1
- package/src/fec-manager.ts +147 -131
- package/src/held-layer-admission.ts +161 -0
- package/src/index.ts +6 -9
- package/src/mmtp-container-parser.ts +48 -0
- package/src/mmtp-fec-session.ts +76 -3
- package/src/mmtp-router.ts +637 -274
- package/src/multicast-auth-catalog.ts +81 -8
- package/src/multicast-manager.ts +274 -49
- package/src/multicast-transport.ts +6 -2
- package/src/no-modules.ts +1 -6
- package/src/repair-fetch.ts +154 -34
- package/test-vectors/catalog/negative/multicast-auth-padded-key.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-reused-track-format.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-root-dotted-key.json +0 -1
- package/test-vectors/catalog/negative/multicast-auth-trailing-pad-bits.json +0 -1
- package/test-vectors/repair-policy-v1.json +113 -0
- package/dist/manifest-auth-plan.d.ts +0 -31
- package/dist/manifest-auth-plan.d.ts.map +0 -1
- package/dist/manifest-auth-plan.js +0 -426
- package/dist/manifest-auth-plan.js.map +0 -7
- package/dist/source-auth.d.ts +0 -2
- package/dist/source-auth.d.ts.map +0 -1
- package/dist/source-auth.js +0 -3912
- package/dist/source-auth.js.map +0 -7
- package/src/manifest-auth-plan.ts +0 -55
- package/src/source-auth.ts +0 -1
- package/test-vectors/generate-source-auth-fixture.mjs +0 -114
- package/test-vectors/source-auth-v1.json +0 -61
package/src/multicast-manager.ts
CHANGED
|
@@ -38,12 +38,12 @@ import type {
|
|
|
38
38
|
RepairPriorityReason,
|
|
39
39
|
RepairSendMeasurement,
|
|
40
40
|
RepairSkipReason,
|
|
41
|
+
RepairStopReason,
|
|
41
42
|
RepairSubscribeCallback,
|
|
42
43
|
} from "./repair-fetch.js";
|
|
43
44
|
import {
|
|
44
45
|
MmtpRouter,
|
|
45
46
|
type FrameCallback,
|
|
46
|
-
type ManifestIdentityResolver,
|
|
47
47
|
type MmtpIngressContext,
|
|
48
48
|
type MmtpStreamIdentity,
|
|
49
49
|
type StagedStreamSwitch,
|
|
@@ -56,8 +56,11 @@ import {
|
|
|
56
56
|
type MulticastFailbackReason,
|
|
57
57
|
type MulticastJoinOutcome,
|
|
58
58
|
type MulticastJoinStats,
|
|
59
|
+
type MulticastTransportConfig,
|
|
59
60
|
} from "./multicast-transport.js";
|
|
60
61
|
import { computeFecDeliveryWindowMs } from "./track-session.js";
|
|
62
|
+
import { BcProvenanceV2Receiver, type BcpV2ReceiverConfig } from "@blockcast/mmt-manifest-verify";
|
|
63
|
+
import type { ResolvedMulticastAuthConfig, ResolvedMulticastAuthFlow } from "./multicast-auth-catalog.js";
|
|
61
64
|
import { DEFAULT_AMT_RELAY_PORT } from "./catalog-network-source.js";
|
|
62
65
|
import { deriveFecInterleaveDepth, FecBlockMapper } from "@blockcast/mmt-fec";
|
|
63
66
|
import type {
|
|
@@ -68,10 +71,6 @@ import type {
|
|
|
68
71
|
} from "@blockcast/fec-worker";
|
|
69
72
|
import type { MfuReassemblerBackend } from "./mfu-reassembler.js";
|
|
70
73
|
import type { MfuReassemblyPolicy } from "./mfu-reassembly-policy.js";
|
|
71
|
-
import type {
|
|
72
|
-
ManifestObjectIdentity,
|
|
73
|
-
ManifestVerifier,
|
|
74
|
-
} from "@blockcast/mmt-manifest-verify";
|
|
75
74
|
import type {
|
|
76
75
|
BlockAssemblerConfig,
|
|
77
76
|
CmafBlockSample,
|
|
@@ -104,6 +103,8 @@ export interface MulticastRepairEndpoint {
|
|
|
104
103
|
repairOwners: ReadonlyArray<{ packetId: number; ownerPacketId: number }>;
|
|
105
104
|
/** Ingress tracks: the owners' source tracks and the repair tracks. */
|
|
106
105
|
tracks: MmtpIngressContext["tracks"];
|
|
106
|
+
/** The protected flow (`MmtpRouter.registerProvenanceFlow`) this group's datagrams belong to. */
|
|
107
|
+
flow?: string;
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
/**
|
|
@@ -116,8 +117,32 @@ export type MulticastRepairSubscribe = (
|
|
|
116
117
|
onPacket: (packet: Uint8Array) => void,
|
|
117
118
|
) => (() => void) | Promise<() => void>;
|
|
118
119
|
|
|
120
|
+
/**
|
|
121
|
+
* A protected endpoint's join (draft -01 7.2.7): the keys and flow
|
|
122
|
+
* `resolveMulticastAuthCatalog` resolved for it. The join registers the flow
|
|
123
|
+
* with the router and subscribes `manifestPort` on the same `(S, G)` beside
|
|
124
|
+
* the media, so every media datagram is admitted by a record before it is read.
|
|
125
|
+
*/
|
|
126
|
+
export interface MulticastProvenanceJoin {
|
|
127
|
+
auth: Pick<ResolvedMulticastAuthConfig, "keys" | "minEpoch">;
|
|
128
|
+
flow: ResolvedMulticastAuthFlow;
|
|
129
|
+
/** Receiver options beyond the catalog's (ctx, clock, freshness, W, lead). */
|
|
130
|
+
receiver?: Omit<Partial<BcpV2ReceiverConfig>, "endpoint" | "keys" | "minEpoch" | "bandwidth" | "maxAuthDelayMs" | "manifestTimerMs">;
|
|
131
|
+
}
|
|
132
|
+
|
|
119
133
|
export interface MulticastEndpointConfig {
|
|
120
|
-
|
|
134
|
+
/** SSM source; omitted on ASM. */
|
|
135
|
+
sourceAddress?: string;
|
|
136
|
+
/** ASM sender filter: `auth.senderAddress` of a protected ASM endpoint. Never beside `sourceAddress`. */
|
|
137
|
+
senderAddress?: string;
|
|
138
|
+
/**
|
|
139
|
+
* Present on a protected endpoint: the join admits through bc-provenance v2.
|
|
140
|
+
* From its first admission (`onProtected`) MoQ copies of the listed tracks
|
|
141
|
+
* are dropped until the media transport fails back (silence or a
|
|
142
|
+
* subscription error); the manager un-protects then whether or not a player
|
|
143
|
+
* registered `onFailback`.
|
|
144
|
+
*/
|
|
145
|
+
provenance?: MulticastProvenanceJoin;
|
|
121
146
|
groupAddress: string;
|
|
122
147
|
port: number;
|
|
123
148
|
/** Explicit gateway transport policy. Omitted means gateway-selected auto mode. */
|
|
@@ -190,15 +215,6 @@ export interface MulticastManagerOptions {
|
|
|
190
215
|
reassemblyPolicy?: MfuReassemblyPolicy;
|
|
191
216
|
reassembler?: MfuReassemblerBackend;
|
|
192
217
|
maxBufferedMfus?: number;
|
|
193
|
-
manifestVerifier?: ManifestVerifier;
|
|
194
|
-
manifestVerifierOwnership?: "caller" | "router";
|
|
195
|
-
resolveManifestIdentity?: (
|
|
196
|
-
packetId: number,
|
|
197
|
-
sbn: number,
|
|
198
|
-
esi: number,
|
|
199
|
-
catalogTrackName?: string,
|
|
200
|
-
) => ManifestObjectIdentity | null;
|
|
201
|
-
resolveManifestIdentityForRoute?: ManifestIdentityResolver;
|
|
202
218
|
/**
|
|
203
219
|
* Also write every repair lifecycle event to the console as one
|
|
204
220
|
* `REPAIR_LIFECYCLE_LOG_PREFIX` JSON line. Off unless asked for: the lines
|
|
@@ -296,11 +312,13 @@ interface ActiveRenditionSwitch {
|
|
|
296
312
|
state: RenditionSwitchState;
|
|
297
313
|
}
|
|
298
314
|
|
|
299
|
-
|
|
315
|
+
/** `flow` is the live join's protected flow: every ingress a switch installs keeps it, or the router reads media unauthenticated. */
|
|
316
|
+
function freezeIngress(tracks: Iterable<Readonly<{ name: string; packetId: number }>>, flow: string | undefined): MmtpIngressContext {
|
|
300
317
|
return Object.freeze({
|
|
301
318
|
tracks: Object.freeze(
|
|
302
319
|
[...tracks].map(({ name, packetId }) => Object.freeze({ name, packetId })),
|
|
303
320
|
),
|
|
321
|
+
...(flow === undefined ? {} : { flow }),
|
|
304
322
|
});
|
|
305
323
|
}
|
|
306
324
|
|
|
@@ -341,6 +359,9 @@ interface RepairSymbolCounts {
|
|
|
341
359
|
* the deadline) so a harness reads the budget instead of inferring it.
|
|
342
360
|
* - `repair-preempted` / `repair-deferred`: the scheduler aborted a delta
|
|
343
361
|
* repair for keyframe work, or never ran it before its block resolved.
|
|
362
|
+
* - `repair-stopped`: the scheduler ended a running repair FETCH (its
|
|
363
|
+
* transport cancels it on the wire) and why; `moot` is a RAP above the
|
|
364
|
+
* block that arrived complete first (draft-ramadan-moq-fec 11.4.2).
|
|
344
365
|
* - `repair-priority`: the catalog could not supply the SUBSCRIBER_PRIORITY a
|
|
345
366
|
* repair FETCH must carry (draft-ramadan-moq-fec §11.4.5), so the FETCH was
|
|
346
367
|
* sent without the parameter (`omitted`, keyframe/unknown blocks) or not
|
|
@@ -418,6 +439,12 @@ export type RepairLifecycleEvent =
|
|
|
418
439
|
readonly rttMs: number | null;
|
|
419
440
|
readonly budgetMs: number;
|
|
420
441
|
}
|
|
442
|
+
| {
|
|
443
|
+
readonly version: 2;
|
|
444
|
+
readonly type: "repair-stopped";
|
|
445
|
+
readonly block: RepairBlockIdentity;
|
|
446
|
+
readonly reason: RepairStopReason;
|
|
447
|
+
}
|
|
421
448
|
| {
|
|
422
449
|
readonly version: 2;
|
|
423
450
|
readonly type: "repair-priority";
|
|
@@ -488,6 +515,18 @@ export interface BlockPolicyStats {
|
|
|
488
515
|
* §7.5 anchor (no epoch reference, or none it could be extended from).
|
|
489
516
|
*/
|
|
490
517
|
repairsSkippedRangeNoAnchor: number;
|
|
518
|
+
/**
|
|
519
|
+
* `repairsSkipped` by reason: the block's repair group could not complete
|
|
520
|
+
* it (sources + RSB_length < K'), so no FETCH was sent.
|
|
521
|
+
*/
|
|
522
|
+
repairsSkippedGroupCannotComplete: number;
|
|
523
|
+
/**
|
|
524
|
+
* `repairsSkipped` by reason: a keyframe-bearing block lay below a RAP
|
|
525
|
+
* that had already arrived complete (draft-ramadan-moq-fec 11.4.2).
|
|
526
|
+
*/
|
|
527
|
+
repairsSkippedMootRap: number;
|
|
528
|
+
/** Running repair FETCHes the scheduler stopped, by reason (`repair-stopped`). */
|
|
529
|
+
repairStops: Record<RepairStopReason, number>;
|
|
491
530
|
/**
|
|
492
531
|
* The most recent skip: its reason, the measured RTT (null when
|
|
493
532
|
* unmeasured), the time that was left to the deadline, and the request's
|
|
@@ -549,6 +588,9 @@ function newBlockPolicyStats(
|
|
|
549
588
|
repairsSkippedRttExceedsBudget: 0,
|
|
550
589
|
repairsSkippedPresentationDelayUnavailable: 0,
|
|
551
590
|
repairsSkippedRangeNoAnchor: 0,
|
|
591
|
+
repairsSkippedGroupCannotComplete: 0,
|
|
592
|
+
repairsSkippedMootRap: 0,
|
|
593
|
+
repairStops: { preempted: 0, reprioritized: 0, expired: 0, resolved: 0, reset: 0, disposed: 0, moot: 0 },
|
|
552
594
|
lastRepairSkip: null,
|
|
553
595
|
keyframeRepairFetchesSent: 0,
|
|
554
596
|
rangeRepairFetchesSent: 0,
|
|
@@ -701,6 +743,18 @@ const REPAIR_SKIP_REASONS: ReadonlySet<unknown> = new Set([
|
|
|
701
743
|
"rtt-unmeasured",
|
|
702
744
|
"rtt-exceeds-budget",
|
|
703
745
|
"presentation-delay-unavailable",
|
|
746
|
+
"range-no-anchor",
|
|
747
|
+
"group-cannot-complete",
|
|
748
|
+
"moot-rap",
|
|
749
|
+
]);
|
|
750
|
+
const REPAIR_STOP_REASONS: ReadonlySet<unknown> = new Set([
|
|
751
|
+
"preempted",
|
|
752
|
+
"reprioritized",
|
|
753
|
+
"expired",
|
|
754
|
+
"resolved",
|
|
755
|
+
"reset",
|
|
756
|
+
"disposed",
|
|
757
|
+
"moot",
|
|
704
758
|
]);
|
|
705
759
|
const REPAIR_DEADLINE_BOUNDS: ReadonlySet<unknown> = new Set(["wait", "presentation", "keyframe", "retention"]);
|
|
706
760
|
const GAP_REASONS: Readonly<Record<string, "stall" | "conceal">> = {
|
|
@@ -815,6 +869,12 @@ export function isRepairLifecycleEvent(
|
|
|
815
869
|
isNullableNonNegative(value["rttMs"]) &&
|
|
816
870
|
isFiniteNumber(value["budgetMs"])
|
|
817
871
|
);
|
|
872
|
+
case "repair-stopped":
|
|
873
|
+
return (
|
|
874
|
+
hasExactKeys(value, ["version", "type", "block", "reason"]) &&
|
|
875
|
+
isRepairBlockIdentity(block) &&
|
|
876
|
+
REPAIR_STOP_REASONS.has(value["reason"])
|
|
877
|
+
);
|
|
818
878
|
case "repair-priority": {
|
|
819
879
|
const blockClass = value["blockClass"];
|
|
820
880
|
const action = value["action"];
|
|
@@ -1048,7 +1108,11 @@ export interface MultiTrackCatalog {
|
|
|
1048
1108
|
tracks: MultiTrackCatalogTrack[];
|
|
1049
1109
|
}
|
|
1050
1110
|
|
|
1051
|
-
/**
|
|
1111
|
+
/**
|
|
1112
|
+
* Creates one stateful synchronous FEC decoder per physical catalog stream.
|
|
1113
|
+
* Every call must return a new decoder: a governed instance also calls it for
|
|
1114
|
+
* one-shot scratch decoders, which the router frees after use.
|
|
1115
|
+
*/
|
|
1052
1116
|
export type WasmFecDecoderFactory = (
|
|
1053
1117
|
track: Readonly<MultiTrackCatalogTrack>,
|
|
1054
1118
|
) => WasmFecDecoder;
|
|
@@ -1211,6 +1275,10 @@ export class MulticastManager {
|
|
|
1211
1275
|
readonly #router: MmtpRouter;
|
|
1212
1276
|
#hostMfuTimeoutMs: number | null;
|
|
1213
1277
|
readonly #transport: MulticastTransport<MmtpIngressContext>;
|
|
1278
|
+
/** The protected flow of the current join and its manifestPort subscription. */
|
|
1279
|
+
#provenanceJoin: { flow: string; manifest: MulticastTransport } | null = null;
|
|
1280
|
+
#onFailback: ((reason: MulticastFailbackReason) => void) | null = null;
|
|
1281
|
+
#failbackHooked = false;
|
|
1214
1282
|
/** Live joinRepairEndpoints() releases → the repair owners they registered,
|
|
1215
1283
|
* re-applied after configureMultiTrack() clears catalog-derived owners. */
|
|
1216
1284
|
readonly #repairReleases = new Map<() => void, MulticastRepairEndpoint["repairOwners"]>();
|
|
@@ -1276,10 +1344,6 @@ export class MulticastManager {
|
|
|
1276
1344
|
reassemblyPolicy: options?.reassemblyPolicy,
|
|
1277
1345
|
reassembler: options?.reassembler,
|
|
1278
1346
|
maxBufferedMfus: options?.maxBufferedMfus,
|
|
1279
|
-
manifestVerifier: options?.manifestVerifier,
|
|
1280
|
-
manifestVerifierOwnership: options?.manifestVerifierOwnership,
|
|
1281
|
-
resolveManifestIdentity: options?.resolveManifestIdentity,
|
|
1282
|
-
resolveManifestIdentityForRoute: options?.resolveManifestIdentityForRoute,
|
|
1283
1347
|
// The router has reset its own state for the stream; reset the rest.
|
|
1284
1348
|
onPublisherRestart: (packetId, catalogTrackName) => {
|
|
1285
1349
|
this.#resetStreamOutsideRouter(packetId, catalogTrackName);
|
|
@@ -1326,7 +1390,7 @@ export class MulticastManager {
|
|
|
1326
1390
|
});
|
|
1327
1391
|
this.#hostMfuTimeoutMs = this.#router.reassembler.timeoutMs;
|
|
1328
1392
|
this.#transport = new MulticastTransport<MmtpIngressContext>((packet, ingress) => {
|
|
1329
|
-
this.#router.feedPacket(packet, "multicast",
|
|
1393
|
+
this.#router.feedPacket(packet, "multicast", ingress);
|
|
1330
1394
|
});
|
|
1331
1395
|
// Each joined subscription declares how it delivers packets. An ordered
|
|
1332
1396
|
// relay decides FEC block fate by stream order instead of the datagram
|
|
@@ -1343,17 +1407,9 @@ export class MulticastManager {
|
|
|
1343
1407
|
const recovered = new Uint8Array(data);
|
|
1344
1408
|
const catalogTrackName = recoveredTrackName
|
|
1345
1409
|
?? this.#fec.catalogTrackNameForBlock(sbn, packetId);
|
|
1346
|
-
|
|
1347
|
-
this.#router.routeRecoveredSymbol(sbn, recovered, packetId, meta.esi)
|
|
1348
|
-
|
|
1349
|
-
this.#router.routeRecoveredSymbol(
|
|
1350
|
-
sbn,
|
|
1351
|
-
recovered,
|
|
1352
|
-
packetId,
|
|
1353
|
-
meta.esi,
|
|
1354
|
-
catalogTrackName,
|
|
1355
|
-
);
|
|
1356
|
-
}
|
|
1410
|
+
return catalogTrackName === undefined
|
|
1411
|
+
? this.#router.routeRecoveredSymbol(sbn, recovered, packetId, meta.esi)
|
|
1412
|
+
: this.#router.routeRecoveredSymbol(sbn, recovered, packetId, meta.esi, catalogTrackName);
|
|
1357
1413
|
};
|
|
1358
1414
|
|
|
1359
1415
|
// D-28: release the in-flight repair slot when a repair-requested block
|
|
@@ -1438,6 +1494,11 @@ export class MulticastManager {
|
|
|
1438
1494
|
get router(): MmtpRouter {
|
|
1439
1495
|
return this.#router;
|
|
1440
1496
|
}
|
|
1497
|
+
/** Repair FETCH wiring (wireRepairFetch): this manager can request unicast repair. */
|
|
1498
|
+
setUnicastRepair(enabled: boolean): void {
|
|
1499
|
+
this.#router.setUnicastRepair(enabled);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1441
1502
|
get transport(): MulticastTransport<MmtpIngressContext> {
|
|
1442
1503
|
return this.#transport;
|
|
1443
1504
|
}
|
|
@@ -2028,8 +2089,36 @@ export class MulticastManager {
|
|
|
2028
2089
|
onTakeover(cb: () => void) {
|
|
2029
2090
|
this.#transport.onTakeover(cb);
|
|
2030
2091
|
}
|
|
2092
|
+
/** Failback un-protects the join's flow (7.2.7 failover), so the fallback's MoQ packets are read. */
|
|
2031
2093
|
onFailback(cb: (reason: MulticastFailbackReason) => void) {
|
|
2032
|
-
this.#
|
|
2094
|
+
this.#onFailback = cb;
|
|
2095
|
+
this.#hookFailback();
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
/**
|
|
2099
|
+
* Install the manager's own failback listener on the media transport, which
|
|
2100
|
+
* arms its silence watchdog: a protected join un-protects on failover even
|
|
2101
|
+
* when no player listens, else its tracks' MoQ copies stay dropped.
|
|
2102
|
+
*/
|
|
2103
|
+
#hookFailback(): void {
|
|
2104
|
+
if (this.#failbackHooked) return;
|
|
2105
|
+
this.#failbackHooked = true;
|
|
2106
|
+
this.#transport.onFailback((reason) => {
|
|
2107
|
+
const joined = this.#provenanceJoin;
|
|
2108
|
+
if (joined !== null) this.#router.unprotectProvenanceFlow(joined.flow);
|
|
2109
|
+
this.#onFailback?.(reason);
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* A protected join's handoff (draft -01 7.2.7), before `onTakeover`: its
|
|
2114
|
+
* first admitted datagram. MoQ copies of the flow's tracks the router still
|
|
2115
|
+
* held unadmitted are discarded, and later ones are dropped. Fires again
|
|
2116
|
+
* after a failback once multicast resumes.
|
|
2117
|
+
*/
|
|
2118
|
+
onProtected(cb: (() => void) | null) {
|
|
2119
|
+
this.#router.onProvenanceFlowProtected(cb && ((flow) => {
|
|
2120
|
+
if (this.#provenanceJoin?.flow === flow) cb();
|
|
2121
|
+
}));
|
|
2033
2122
|
}
|
|
2034
2123
|
/** See MulticastTransport.onDataPlaneLive: the join's first accepted packet. */
|
|
2035
2124
|
onDataPlaneLive(cb: () => void) {
|
|
@@ -2047,6 +2136,17 @@ export class MulticastManager {
|
|
|
2047
2136
|
}
|
|
2048
2137
|
|
|
2049
2138
|
async join(config: MulticastEndpointConfig): Promise<boolean> {
|
|
2139
|
+
const provenanceFlow = config.provenance?.flow;
|
|
2140
|
+
if (
|
|
2141
|
+
provenanceFlow !== undefined &&
|
|
2142
|
+
((config.sourceAddress ?? config.senderAddress) !== provenanceFlow.source ||
|
|
2143
|
+
config.groupAddress !== provenanceFlow.groupAddress ||
|
|
2144
|
+
config.port !== provenanceFlow.port)
|
|
2145
|
+
) {
|
|
2146
|
+
throw new RangeError(
|
|
2147
|
+
"[MulticastManager] join: provenance flow (S, G, port) must match sourceAddress ?? senderAddress, groupAddress and port",
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2050
2150
|
// A join replaces the admission a staged switch was built on. The new
|
|
2051
2151
|
// join's config decides what is admitted, so nothing is restored or
|
|
2052
2152
|
// retired here: the switch just ends.
|
|
@@ -2071,22 +2171,27 @@ export class MulticastManager {
|
|
|
2071
2171
|
: selectedPacketIds === null
|
|
2072
2172
|
? undefined
|
|
2073
2173
|
: [...selectedPacketIds].map((packetId) => ({ name: "", packetId }));
|
|
2074
|
-
|
|
2174
|
+
this.#releaseProvenanceJoin();
|
|
2175
|
+
const provenance = config.provenance;
|
|
2176
|
+
const protectedTracks = ingressTracks ?? provenance?.flow.tracks;
|
|
2177
|
+
const ingress: MmtpIngressContext | undefined = protectedTracks === undefined
|
|
2075
2178
|
? undefined
|
|
2076
2179
|
: {
|
|
2077
2180
|
tracks: Object.freeze(
|
|
2078
|
-
|
|
2181
|
+
protectedTracks.map(({ name, packetId }) =>
|
|
2079
2182
|
Object.freeze({ name, packetId }),
|
|
2080
2183
|
),
|
|
2081
2184
|
),
|
|
2185
|
+
...(provenance === undefined ? {} : { flow: multicastProvenanceFlowKey(provenance.flow) }),
|
|
2082
2186
|
};
|
|
2083
2187
|
// The single relay inlet: the player/UI override, or the catalog's
|
|
2084
2188
|
// `multicast.networkSource[{type:"amt"}]` relay, which
|
|
2085
2189
|
// `endpointToMulticastConfig` (packages/transport
|
|
2086
2190
|
// multicast-catalog-policy.ts) resolves into `amtRelay` via `findAmtRelay`.
|
|
2087
2191
|
const amtRelay = config.amtRelay;
|
|
2088
|
-
const transportConfig = {
|
|
2192
|
+
const transportConfig: MulticastTransportConfig = {
|
|
2089
2193
|
sourceAddress: config.sourceAddress,
|
|
2194
|
+
senderAddress: config.senderAddress,
|
|
2090
2195
|
groupAddress: config.groupAddress,
|
|
2091
2196
|
port: config.port,
|
|
2092
2197
|
mode: config.mode,
|
|
@@ -2116,9 +2221,70 @@ export class MulticastManager {
|
|
|
2116
2221
|
packetIds: config.packetIds,
|
|
2117
2222
|
tracks: catalogTracks?.map(({ name, packetId }) => ({ name, packetId })),
|
|
2118
2223
|
};
|
|
2119
|
-
|
|
2224
|
+
if (provenance !== undefined) this.#joinProvenance(provenance, ingress as MmtpIngressContext & { flow: string }, transportConfig);
|
|
2225
|
+
const protectedJoin = this.#provenanceJoin;
|
|
2226
|
+
const joined = await (ingress === undefined
|
|
2120
2227
|
? this.#transport.join(transportConfig)
|
|
2121
|
-
: this.#transport.join(transportConfig, ingress);
|
|
2228
|
+
: this.#transport.join(transportConfig, ingress));
|
|
2229
|
+
// No media: leave the manifest subscription and release the flow now, not at the next join.
|
|
2230
|
+
if (!joined && protectedJoin !== null && this.#provenanceJoin === protectedJoin) this.#releaseProvenanceJoin();
|
|
2231
|
+
return joined;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
/**
|
|
2235
|
+
* Register the protected flow and subscribe its `manifestPort` on the same
|
|
2236
|
+
* `(S, G)` with its own listener (draft -01 7.2.7), so records reach the
|
|
2237
|
+
* flow's receiver and media datagrams are held until one admits them. A
|
|
2238
|
+
* manifest subscription that fails to join admits nothing: the media is
|
|
2239
|
+
* held and expires, never read unauthenticated.
|
|
2240
|
+
*/
|
|
2241
|
+
#joinProvenance(
|
|
2242
|
+
provenance: MulticastProvenanceJoin,
|
|
2243
|
+
ingress: MmtpIngressContext & { flow: string },
|
|
2244
|
+
media: MulticastTransportConfig,
|
|
2245
|
+
): void {
|
|
2246
|
+
const { flow, auth } = provenance;
|
|
2247
|
+
const receiver = new BcProvenanceV2Receiver({
|
|
2248
|
+
...provenance.receiver,
|
|
2249
|
+
// The caller's ctx when it passes one, else the catalog's: an explicit
|
|
2250
|
+
// `catalogCtx: undefined` must not switch off the 7.2.4 ctx check.
|
|
2251
|
+
catalogCtx: provenance.receiver?.catalogCtx ?? flow.ctx,
|
|
2252
|
+
endpoint: {
|
|
2253
|
+
sourceAddress: flow.source,
|
|
2254
|
+
groupAddress: flow.groupAddress,
|
|
2255
|
+
port: flow.port,
|
|
2256
|
+
manifestPort: flow.manifestPort,
|
|
2257
|
+
streamId: flow.streamId,
|
|
2258
|
+
},
|
|
2259
|
+
keys: auth.keys,
|
|
2260
|
+
minEpoch: auth.minEpoch,
|
|
2261
|
+
bandwidth: flow.bandwidth,
|
|
2262
|
+
maxAuthDelayMs: flow.maxAuthDelayMs,
|
|
2263
|
+
manifestTimerMs: flow.manifestTimerMs,
|
|
2264
|
+
});
|
|
2265
|
+
this.#router.registerProvenanceFlow(ingress, receiver);
|
|
2266
|
+
this.#hookFailback();
|
|
2267
|
+
const manifest = new MulticastTransport((record) => {
|
|
2268
|
+
if (this.#provenanceJoin?.manifest === manifest) {
|
|
2269
|
+
this.#router.feedProvenanceRecord(ingress.flow, record, "multicast");
|
|
2270
|
+
}
|
|
2271
|
+
});
|
|
2272
|
+
this.#provenanceJoin = { flow: ingress.flow, manifest };
|
|
2273
|
+
const { packetIds: _packetIds, tracks: _tracks, ...path } = media;
|
|
2274
|
+
void manifest.join({ ...path, port: flow.manifestPort }).then((joined) => {
|
|
2275
|
+
if (!joined && this.#provenanceJoin?.manifest === manifest) {
|
|
2276
|
+
console.warn(`[MulticastManager] manifestPort ${flow.groupAddress}:${flow.manifestPort} not joined`, manifest.lastJoinOutcome);
|
|
2277
|
+
}
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
/** Un-protect: drop the flow's hold queues (no expiry counted) and leave its manifestPort. */
|
|
2282
|
+
#releaseProvenanceJoin(): void {
|
|
2283
|
+
const joined = this.#provenanceJoin;
|
|
2284
|
+
if (joined === null) return;
|
|
2285
|
+
this.#provenanceJoin = null;
|
|
2286
|
+
joined.manifest.dispose();
|
|
2287
|
+
this.#router.releaseProvenanceFlow(joined.flow);
|
|
2122
2288
|
}
|
|
2123
2289
|
|
|
2124
2290
|
/**
|
|
@@ -2153,9 +2319,10 @@ export class MulticastManager {
|
|
|
2153
2319
|
for (const endpoint of endpoints) {
|
|
2154
2320
|
const ingress: MmtpIngressContext = {
|
|
2155
2321
|
tracks: Object.freeze(endpoint.tracks.map(({ name, packetId }) => Object.freeze({ name, packetId }))),
|
|
2322
|
+
...(endpoint.flow === undefined ? {} : { flow: endpoint.flow }),
|
|
2156
2323
|
};
|
|
2157
2324
|
const onPacket = (packet: Uint8Array) => {
|
|
2158
|
-
if (!released) this.#router.feedPacket(packet, "multicast",
|
|
2325
|
+
if (!released) this.#router.feedPacket(packet, "multicast", ingress);
|
|
2159
2326
|
};
|
|
2160
2327
|
const fail = (error: unknown) =>
|
|
2161
2328
|
console.warn(`[MulticastManager] repair group ${endpoint.group}:${endpoint.port} not joined`, error);
|
|
@@ -2323,7 +2490,7 @@ export class MulticastManager {
|
|
|
2323
2490
|
for (const track of [...live.tracks, ...request.tracks]) {
|
|
2324
2491
|
stagedTracks.set(`${track.packetId}\0${track.name}`, track);
|
|
2325
2492
|
}
|
|
2326
|
-
const stagedIngress = freezeIngress(stagedTracks.values());
|
|
2493
|
+
const stagedIngress = freezeIngress(stagedTracks.values(), live.flow);
|
|
2327
2494
|
const stagedPacketIds =
|
|
2328
2495
|
committedPacketIds === undefined || livePacketIds === null
|
|
2329
2496
|
? null
|
|
@@ -2338,7 +2505,7 @@ export class MulticastManager {
|
|
|
2338
2505
|
liveIngress: live,
|
|
2339
2506
|
livePacketIds,
|
|
2340
2507
|
widened,
|
|
2341
|
-
committedIngress: freezeIngress(request.tracks),
|
|
2508
|
+
committedIngress: freezeIngress(request.tracks, live.flow),
|
|
2342
2509
|
committedPacketIds,
|
|
2343
2510
|
router: null,
|
|
2344
2511
|
state: "staged",
|
|
@@ -2352,7 +2519,7 @@ export class MulticastManager {
|
|
|
2352
2519
|
let widening: Promise<void> | undefined;
|
|
2353
2520
|
try {
|
|
2354
2521
|
if (widened) widening = this.#transport.setPacketIds([...stagedPacketIds!]);
|
|
2355
|
-
this.#
|
|
2522
|
+
this.#setSwitchIngress(stagedIngress);
|
|
2356
2523
|
} catch (error) {
|
|
2357
2524
|
active.router.cancel();
|
|
2358
2525
|
throw error;
|
|
@@ -2374,6 +2541,12 @@ export class MulticastManager {
|
|
|
2374
2541
|
};
|
|
2375
2542
|
}
|
|
2376
2543
|
|
|
2544
|
+
/** Route a switch's ingress and keep a protected flow's registration in step (7.2.7). */
|
|
2545
|
+
#setSwitchIngress(ingress: MmtpIngressContext): void {
|
|
2546
|
+
this.#transport.setContext(ingress);
|
|
2547
|
+
if (ingress.flow !== undefined) this.#router.updateProvenanceFlowIngress({ ...ingress, flow: ingress.flow });
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2377
2550
|
/** The router committed the staged switch (it already retired `from`). */
|
|
2378
2551
|
#commitRenditionSwitch(active: ActiveRenditionSwitch, commit: StreamSwitchCommit): void {
|
|
2379
2552
|
if (this.#renditionSwitch !== active) return;
|
|
@@ -2381,7 +2554,7 @@ export class MulticastManager {
|
|
|
2381
2554
|
active.state = "committed";
|
|
2382
2555
|
active.router = null;
|
|
2383
2556
|
if (this.#transport.joined) {
|
|
2384
|
-
this.#
|
|
2557
|
+
this.#setSwitchIngress(active.committedIngress);
|
|
2385
2558
|
if (active.committedPacketIds !== undefined) {
|
|
2386
2559
|
// The page-side filter narrows now; the provider may keep delivering
|
|
2387
2560
|
// the retired IDs until it applies the narrower list, and the page
|
|
@@ -2429,7 +2602,7 @@ export class MulticastManager {
|
|
|
2429
2602
|
active.router = null;
|
|
2430
2603
|
if (restore) {
|
|
2431
2604
|
if (this.#transport.joined) {
|
|
2432
|
-
this.#
|
|
2605
|
+
this.#setSwitchIngress(active.liveIngress);
|
|
2433
2606
|
if (active.widened && active.livePacketIds !== null) {
|
|
2434
2607
|
this.#transport.setPacketIds([...active.livePacketIds]).catch((error: unknown) => {
|
|
2435
2608
|
console.warn(
|
|
@@ -2518,8 +2691,8 @@ export class MulticastManager {
|
|
|
2518
2691
|
data: Uint8Array,
|
|
2519
2692
|
packetId: number,
|
|
2520
2693
|
catalogTrackName?: string,
|
|
2521
|
-
) {
|
|
2522
|
-
this.#router.routeRecoveredBlock(blockId, data, packetId, catalogTrackName);
|
|
2694
|
+
): boolean {
|
|
2695
|
+
return this.#router.routeRecoveredBlock(blockId, data, packetId, catalogTrackName);
|
|
2523
2696
|
}
|
|
2524
2697
|
|
|
2525
2698
|
/** Route one Worker-recovered source symbol through the MMTP parser + dedup pipeline. */
|
|
@@ -2529,8 +2702,8 @@ export class MulticastManager {
|
|
|
2529
2702
|
packetId: number,
|
|
2530
2703
|
esi = 0,
|
|
2531
2704
|
catalogTrackName?: string,
|
|
2532
|
-
) {
|
|
2533
|
-
this.#router.routeRecoveredSymbol(blockId, data, packetId, esi, catalogTrackName);
|
|
2705
|
+
): boolean {
|
|
2706
|
+
return this.#router.routeRecoveredSymbol(blockId, data, packetId, esi, catalogTrackName);
|
|
2534
2707
|
}
|
|
2535
2708
|
|
|
2536
2709
|
// ---------------------------------------------------------------------------
|
|
@@ -2719,6 +2892,18 @@ export class MulticastManager {
|
|
|
2719
2892
|
}
|
|
2720
2893
|
}
|
|
2721
2894
|
}
|
|
2895
|
+
// A factory also supplies the one-shot scratch decoder an unlisted-assisted
|
|
2896
|
+
// decode on a governed instance runs in (7.2.7 step 2); without one the
|
|
2897
|
+
// router drops unlisted repair there.
|
|
2898
|
+
const factory = typeof wasmDecoderSource === "function" ? wasmDecoderSource : null;
|
|
2899
|
+
const fecTracks = fecTrackIndexes.map((i) => catalog.tracks[i]!);
|
|
2900
|
+
this.#router.setScratchFecDecoderFactory(factory && ((ownerPacketId, catalogTrackName) => {
|
|
2901
|
+
const track = fecTracks.find((t) => t.packetId === ownerPacketId && t.name === catalogTrackName);
|
|
2902
|
+
if (!track) return null;
|
|
2903
|
+
const decoder = factory(track);
|
|
2904
|
+
// A memoizing factory would hand back a live decoder the router then frees.
|
|
2905
|
+
return [...fecDecoders.values()].includes(decoder) ? null : decoder;
|
|
2906
|
+
}));
|
|
2722
2907
|
|
|
2723
2908
|
// Reset switching-set index, repair packetId set and FEC availability.
|
|
2724
2909
|
this.#switchingSets.clear();
|
|
@@ -2946,6 +3131,8 @@ export class MulticastManager {
|
|
|
2946
3131
|
if (reason === "rtt-unmeasured") stats.repairsSkippedRttUnmeasured++;
|
|
2947
3132
|
else if (reason === "rtt-exceeds-budget") stats.repairsSkippedRttExceedsBudget++;
|
|
2948
3133
|
else if (reason === "range-no-anchor") stats.repairsSkippedRangeNoAnchor++;
|
|
3134
|
+
else if (reason === "group-cannot-complete") stats.repairsSkippedGroupCannotComplete++;
|
|
3135
|
+
else if (reason === "moot-rap") stats.repairsSkippedMootRap++;
|
|
2949
3136
|
else stats.repairsSkippedPresentationDelayUnavailable++;
|
|
2950
3137
|
stats.lastRepairSkip = {
|
|
2951
3138
|
reason,
|
|
@@ -3027,6 +3214,37 @@ export class MulticastManager {
|
|
|
3027
3214
|
});
|
|
3028
3215
|
}
|
|
3029
3216
|
|
|
3217
|
+
/** Record a running repair FETCH the scheduler stopped, and why. */
|
|
3218
|
+
noteRepairStopped(
|
|
3219
|
+
reason: RepairStopReason,
|
|
3220
|
+
sbn: number,
|
|
3221
|
+
packetId: number,
|
|
3222
|
+
catalogTrackName?: string,
|
|
3223
|
+
): void {
|
|
3224
|
+
this.#statsFor(packetId, catalogTrackName).repairStops[reason]++;
|
|
3225
|
+
this.#emitRepairLifecycle({
|
|
3226
|
+
version: 2,
|
|
3227
|
+
type: "repair-stopped",
|
|
3228
|
+
block: repairBlockIdentity(packetId, sbn, catalogTrackName),
|
|
3229
|
+
reason,
|
|
3230
|
+
});
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
/**
|
|
3234
|
+
* Whether a complete RAP above SS_ID `ssId` reached the stream's in-order
|
|
3235
|
+
* release gate (draft-ramadan-moq-fec 11.4.2: repair below it is moot).
|
|
3236
|
+
*/
|
|
3237
|
+
rapCompleteAbove(packetId: number, catalogTrackName: string | undefined, ssId: number): boolean {
|
|
3238
|
+
return this.#router.fecReleaseRapAbove(packetId, catalogTrackName, ssId);
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
/** Observe each complete RAP reaching a release gate (synchronous: defer FEC work). */
|
|
3242
|
+
addRapCompleteObserver(
|
|
3243
|
+
observer: (packetId: number, catalogTrackName: string | undefined, ssId: number) => void,
|
|
3244
|
+
): () => void {
|
|
3245
|
+
return this.#router.addFecRapObserver(observer);
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3030
3248
|
/** Catalog/reset epoch of a packet's streams; gap decisions carry it. */
|
|
3031
3249
|
epochFor(packetId: number): number {
|
|
3032
3250
|
return this.#fec.epochFor(packetId);
|
|
@@ -3043,6 +3261,7 @@ export class MulticastManager {
|
|
|
3043
3261
|
if (stats.epoch !== this.#fec.epochFor(stats.packetId)) continue;
|
|
3044
3262
|
out[key] = {
|
|
3045
3263
|
...stats,
|
|
3264
|
+
repairStops: { ...stats.repairStops },
|
|
3046
3265
|
lastRepairSkip: stats.lastRepairSkip === null ? null : { ...stats.lastRepairSkip },
|
|
3047
3266
|
};
|
|
3048
3267
|
}
|
|
@@ -3196,6 +3415,7 @@ export class MulticastManager {
|
|
|
3196
3415
|
this.#trackResetObservers.clear();
|
|
3197
3416
|
this.#repairLifecycleObservers.clear();
|
|
3198
3417
|
this.#transport.dispose();
|
|
3418
|
+
this.#releaseProvenanceJoin();
|
|
3199
3419
|
for (const release of [...this.#repairReleases.keys()]) release();
|
|
3200
3420
|
this.#router.dispose();
|
|
3201
3421
|
this.#fec.dispose();
|
|
@@ -3206,3 +3426,8 @@ export class MulticastManager {
|
|
|
3206
3426
|
// (wireRepairFetch) drives.
|
|
3207
3427
|
const repairFetchHost = (manager: MulticastManager): RepairFetchHost => manager;
|
|
3208
3428
|
void repairFetchHost;
|
|
3429
|
+
|
|
3430
|
+
/** The router's key of a protected flow: its `(S, G, port)`. */
|
|
3431
|
+
export function multicastProvenanceFlowKey(flow: Pick<ResolvedMulticastAuthFlow, "source" | "groupAddress" | "port">): string {
|
|
3432
|
+
return JSON.stringify([flow.source, flow.groupAddress, flow.port]);
|
|
3433
|
+
}
|
|
@@ -19,7 +19,10 @@ import {
|
|
|
19
19
|
} from '@blockcast/mmt-base'
|
|
20
20
|
|
|
21
21
|
export interface MulticastTransportConfig {
|
|
22
|
-
|
|
22
|
+
/** SSM source; omitted on ASM. */
|
|
23
|
+
sourceAddress?: string
|
|
24
|
+
/** ASM sender filter (multicast-contract 1.1.0 `senderAddress`); never beside `sourceAddress`. */
|
|
25
|
+
senderAddress?: string
|
|
23
26
|
groupAddress: string
|
|
24
27
|
port: number
|
|
25
28
|
mode?: 'auto' | 'native' | 'tunnel'
|
|
@@ -368,10 +371,11 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
368
371
|
try {
|
|
369
372
|
const packetIds = config.packetIds?.length ? new Set(config.packetIds) : null
|
|
370
373
|
console.log(
|
|
371
|
-
`[MulticastTransport] window.multicast.subscribe(${config.sourceAddress}, ${config.groupAddress}, ${config.port})`,
|
|
374
|
+
`[MulticastTransport] window.multicast.subscribe(${config.sourceAddress ?? config.senderAddress ?? '*'}, ${config.groupAddress}, ${config.port})`,
|
|
372
375
|
)
|
|
373
376
|
const subscribeConfig = toWindowMulticastSubscribeConfig({
|
|
374
377
|
source: config.sourceAddress,
|
|
378
|
+
senderAddress: config.senderAddress,
|
|
375
379
|
group: config.groupAddress,
|
|
376
380
|
port: config.port,
|
|
377
381
|
mode: config.mode,
|
package/src/no-modules.ts
CHANGED
|
@@ -19,9 +19,4 @@ export { MmtpToCmaf } from './mmtp-to-cmaf.js'
|
|
|
19
19
|
// reach only this classic-script surface, so it has to be exported here as
|
|
20
20
|
// well as from the barrel (BLO-49943).
|
|
21
21
|
export { isFmp4InitSegment } from './codec-utils.js'
|
|
22
|
-
export {
|
|
23
|
-
export {
|
|
24
|
-
ManifestVerifier,
|
|
25
|
-
createWebCryptoEd25519Verifier,
|
|
26
|
-
manifestBindingId,
|
|
27
|
-
} from '@blockcast/mmt-manifest-verify'
|
|
22
|
+
export { BcProvenanceV2Receiver } from '@blockcast/mmt-manifest-verify'
|