@blockcast/mmt-container 2.0.0 → 2.0.1-main.0f2de6af487b
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 +1 -1
- package/dist/browser-runtime.js +141 -22
- package/dist/browser-runtime.js.map +2 -2
- package/dist/delivery-catalog.js +3 -0
- package/dist/delivery-catalog.js.map +2 -2
- 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.no-modules.js +19 -10
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-router.d.ts +26 -1
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +66 -1
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-manager.d.ts +4 -1
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +141 -22
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.d.ts +8 -1
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +93 -2
- package/dist/multicast-transport.js.map +4 -4
- package/dist/no-modules.js +19 -10
- package/dist/no-modules.js.map +2 -2
- package/package.json +5 -5
- package/src/index.ts +1 -0
- package/src/mmtp-router.ts +80 -1
- package/src/multicast-manager.ts +52 -16
- package/src/multicast-transport.ts +43 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockcast/mmt-container",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1-main.0f2de6af487b",
|
|
4
4
|
"description": "MMTP/LOC container parsing for MMT over MoQ and SSM",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -169,10 +169,10 @@
|
|
|
169
169
|
"test:watch": "vitest"
|
|
170
170
|
},
|
|
171
171
|
"dependencies": {
|
|
172
|
-
"@blockcast/fec-worker": "
|
|
173
|
-
"@blockcast/mmt-base": "
|
|
174
|
-
"@blockcast/mmt-fec": "
|
|
175
|
-
"@blockcast/mmt-manifest-verify": "
|
|
172
|
+
"@blockcast/fec-worker": "2.0.1-main.0f2de6af487b",
|
|
173
|
+
"@blockcast/mmt-base": "2.0.1-main.0f2de6af487b",
|
|
174
|
+
"@blockcast/mmt-fec": "2.0.1-main.0f2de6af487b",
|
|
175
|
+
"@blockcast/mmt-manifest-verify": "2.0.1-main.0f2de6af487b",
|
|
176
176
|
"@noble/curves": "^1.9.7",
|
|
177
177
|
"@noble/hashes": "^1.8.0",
|
|
178
178
|
"ajv": "^8.17.1",
|
package/src/index.ts
CHANGED
package/src/mmtp-router.ts
CHANGED
|
@@ -175,6 +175,13 @@ export type RoutedFrameMeta = {
|
|
|
175
175
|
gapBefore?: FrameGapDecision;
|
|
176
176
|
};
|
|
177
177
|
|
|
178
|
+
/** A keyframe-overlay repair leg: the source its FEC instance protects. */
|
|
179
|
+
export interface KeyframeOverlayLeg {
|
|
180
|
+
sourcePacketId: number;
|
|
181
|
+
sourceCatalogTrackName?: string;
|
|
182
|
+
repairTrackName?: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
178
185
|
export interface MmtpIngressContext {
|
|
179
186
|
tracks: readonly Readonly<{ name: string; packetId: number }>[];
|
|
180
187
|
/**
|
|
@@ -1031,6 +1038,14 @@ export class MmtpRouter {
|
|
|
1031
1038
|
#packetIdRoles: Map<number, TrackRole> = new Map();
|
|
1032
1039
|
/** Explicit catalog ownership for dedicated repair-track packet IDs. */
|
|
1033
1040
|
#repairOwnersByPacketId = new Map<number, Set<number>>();
|
|
1041
|
+
/** Keyframe-overlay repair PID → the source its FEC instance protects
|
|
1042
|
+
* (draft-ramadan-mmt-fec-02 7.4.1). Never an owner in
|
|
1043
|
+
* #repairOwnersByPacketId: the overlay is decoded as its own instance. */
|
|
1044
|
+
#overlayLegs = new Map<number, KeyframeOverlayLeg>();
|
|
1045
|
+
/** Keyframe-overlay repair track names, which name a leg too: a catalog
|
|
1046
|
+
* may state no PID for it (learned at subscribe time). */
|
|
1047
|
+
#overlayRepairNames = new Map<string, KeyframeOverlayLeg>();
|
|
1048
|
+
#overlayStats = { repairDropped: 0, ownerRefused: 0 };
|
|
1034
1049
|
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
1035
1050
|
#onDeltaBlockCompleteCb:
|
|
1036
1051
|
| ((packetId: number, failed: boolean) => void)
|
|
@@ -1775,15 +1790,61 @@ export class MmtpRouter {
|
|
|
1775
1790
|
/** Replace catalog-derived repair ownership before a catalog reconfiguration. */
|
|
1776
1791
|
clearRepairPacketIdOwners(): void {
|
|
1777
1792
|
this.#repairOwnersByPacketId.clear();
|
|
1793
|
+
this.#overlayLegs.clear();
|
|
1794
|
+
this.#overlayRepairNames.clear();
|
|
1778
1795
|
}
|
|
1779
1796
|
|
|
1780
|
-
/** Bind one dedicated repair-track PID to its authoritative media PID.
|
|
1797
|
+
/** Bind one dedicated repair-track PID to its authoritative media PID. A
|
|
1798
|
+
* known keyframe-overlay PID is refused, and counted: its symbols belong to
|
|
1799
|
+
* the overlay instance, never to the source's base decoder (mmt-02 7.4.4). */
|
|
1781
1800
|
setRepairPacketIdOwner(repairPacketId: number, ownerPacketId: number): void {
|
|
1801
|
+
if (this.#overlayLegs.has(repairPacketId)) {
|
|
1802
|
+
this.#overlayStats.ownerRefused++;
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1782
1805
|
const owners = this.#repairOwnersByPacketId.get(repairPacketId) ?? new Set<number>();
|
|
1783
1806
|
owners.add(ownerPacketId);
|
|
1784
1807
|
this.#repairOwnersByPacketId.set(repairPacketId, owners);
|
|
1785
1808
|
}
|
|
1786
1809
|
|
|
1810
|
+
/**
|
|
1811
|
+
* Record a keyframe-overlay repair leg (draft-ramadan-mmt-fec-02 7.4): its
|
|
1812
|
+
* PID, the source it protects, and its repair track name. Either one names
|
|
1813
|
+
* the leg, so a catalog that states no PID (learned at subscribe time) is
|
|
1814
|
+
* covered by the name. Overlay repair is then never resolved to a base
|
|
1815
|
+
* owner, on multicast or MoQ. Cleared with the catalog owners
|
|
1816
|
+
* (`clearRepairPacketIdOwners`).
|
|
1817
|
+
*/
|
|
1818
|
+
setKeyframeOverlayLeg(repairPacketId: number | undefined, leg: KeyframeOverlayLeg): void {
|
|
1819
|
+
if (leg.repairTrackName !== undefined) this.#overlayRepairNames.set(leg.repairTrackName, { ...leg });
|
|
1820
|
+
if (repairPacketId === undefined) return;
|
|
1821
|
+
this.#repairOwnersByPacketId.delete(repairPacketId);
|
|
1822
|
+
this.#overlayLegs.set(repairPacketId, { ...leg });
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
/** The keyframe-overlay leg registered on `repairPacketId`, if any. */
|
|
1826
|
+
keyframeOverlayLeg(repairPacketId: number): Readonly<KeyframeOverlayLeg> | undefined {
|
|
1827
|
+
return this.#overlayLegs.get(repairPacketId);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
/** Overlay repair dropped for want of an overlay instance, and base-owner
|
|
1831
|
+
* registrations refused on an overlay PID. */
|
|
1832
|
+
get keyframeOverlayStats(): Readonly<{ repairDropped: number; ownerRefused: number }> {
|
|
1833
|
+
return { ...this.#overlayStats };
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
#isOverlayRepair(repairPacketId: number, repairTrackName?: string): boolean {
|
|
1837
|
+
return this.#overlayLegs.has(repairPacketId) ||
|
|
1838
|
+
(repairTrackName !== undefined && this.#overlayRepairNames.has(repairTrackName));
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
/** Overlay repair has no decoder until the overlay instance lands
|
|
1842
|
+
* (BLO-52077 PR 2): drop it before any base owner, locator or decoder. */
|
|
1843
|
+
#dropOverlayRepair(repairPacketId: number): void {
|
|
1844
|
+
this.#overlayStats.repairDropped++;
|
|
1845
|
+
debugRouter(`[MmtpRouter] Dropping keyframe-overlay repair pid=${repairPacketId}: no overlay instance`);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1787
1848
|
/**
|
|
1788
1849
|
* Register callback fired when a delta-track FEC block completes or fails.
|
|
1789
1850
|
*
|
|
@@ -2316,6 +2377,10 @@ export class MmtpRouter {
|
|
|
2316
2377
|
// A dedicated repair track can carry a packet ID that differs from the
|
|
2317
2378
|
// media packet ID. The subscribing track supplies the authoritative owner.
|
|
2318
2379
|
const repairPacketId = header.packetId;
|
|
2380
|
+
if (this.#isOverlayRepair(repairPacketId, repairTrackName)) {
|
|
2381
|
+
this.#dropOverlayRepair(repairPacketId);
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2319
2384
|
|
|
2320
2385
|
const hintedOwner =
|
|
2321
2386
|
ownerPacketIdHint != null && this.#classifyPacketId(ownerPacketIdHint) !== null
|
|
@@ -2803,6 +2868,13 @@ export class MmtpRouter {
|
|
|
2803
2868
|
this.#packetIdRoles.delete(packetId);
|
|
2804
2869
|
this.#catalogTrackNamesByPacketId.delete(packetId);
|
|
2805
2870
|
this.#repairOwnersByPacketId.delete(packetId);
|
|
2871
|
+
this.#overlayLegs.delete(packetId);
|
|
2872
|
+
for (const [repairPacketId, leg] of [...this.#overlayLegs]) {
|
|
2873
|
+
if (leg.sourcePacketId === packetId) this.#overlayLegs.delete(repairPacketId);
|
|
2874
|
+
}
|
|
2875
|
+
for (const [name, leg] of [...this.#overlayRepairNames]) {
|
|
2876
|
+
if (leg.sourcePacketId === packetId) this.#overlayRepairNames.delete(name);
|
|
2877
|
+
}
|
|
2806
2878
|
delete this.#pidCounts[packetId];
|
|
2807
2879
|
delete this.#repairPidCounts[packetId];
|
|
2808
2880
|
delete this.#moqRepairPidCounts[packetId];
|
|
@@ -3075,6 +3147,8 @@ export class MmtpRouter {
|
|
|
3075
3147
|
this.#parserDiagnostics.clear();
|
|
3076
3148
|
this.#catalogTrackNamesByPacketId.clear();
|
|
3077
3149
|
this.#repairOwnersByPacketId.clear();
|
|
3150
|
+
this.#overlayLegs.clear();
|
|
3151
|
+
this.#overlayRepairNames.clear();
|
|
3078
3152
|
for (const { receiver } of this.#provenance.values()) receiver.release();
|
|
3079
3153
|
this.#provenance.clear();
|
|
3080
3154
|
this.#provenanceStreamFlow.clear();
|
|
@@ -3550,6 +3624,11 @@ export class MmtpRouter {
|
|
|
3550
3624
|
// Repair packet — per ISO 23008-1 §C.5.3, carries (SS_Start, SSB_length, RS_ID).
|
|
3551
3625
|
// Read from the header parsed above rather than parsing it again
|
|
3552
3626
|
// (BLO-36599).
|
|
3627
|
+
// The track a repair packet arrives on names its instance (7.4.2).
|
|
3628
|
+
if (this.#isOverlayRepair(info.packetId, catalogTrackNameForPacket(ingress, info.packetId))) {
|
|
3629
|
+
this.#dropOverlayRepair(info.packetId);
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3553
3632
|
const ownerPacketId = this.#resolveRepairOwnerPacketId(info.packetId, ingress);
|
|
3554
3633
|
// An ambiguous name attributes the packet to no instance, so it is
|
|
3555
3634
|
// dropped before admission can count or fail closed against one.
|
package/src/multicast-manager.ts
CHANGED
|
@@ -97,7 +97,7 @@ export interface MulticastRepairEndpoint {
|
|
|
97
97
|
/** The repair packet IDs this group carries. */
|
|
98
98
|
packetIds: number[];
|
|
99
99
|
/** Each repair packet ID bound to the media packet ID it repairs. */
|
|
100
|
-
repairOwners: ReadonlyArray<{ packetId: number; ownerPacketId: number }>;
|
|
100
|
+
repairOwners: ReadonlyArray<{ packetId: number; ownerPacketId: number; instance?: "keyframe" }>;
|
|
101
101
|
/** Ingress tracks: the owners' source tracks and the repair tracks. */
|
|
102
102
|
tracks: MmtpIngressContext["tracks"];
|
|
103
103
|
/** The protected flow (`MmtpRouter.registerProvenanceFlow`) this group's datagrams belong to. */
|
|
@@ -1257,14 +1257,18 @@ function repairTrackNamesBySource(
|
|
|
1257
1257
|
return bySource;
|
|
1258
1258
|
}
|
|
1259
1259
|
|
|
1260
|
-
/** The repair
|
|
1260
|
+
/** The base repair layers an `ok` binding joins. The keyframe overlay is
|
|
1261
|
+
* not among them: it is its own FEC instance (draft-ramadan-mmt-fec-02 7.4). */
|
|
1261
1262
|
function boundRepairNames(binding: FecSourceBinding | undefined): string[] {
|
|
1262
1263
|
if (binding?.status !== "ok") return [];
|
|
1263
|
-
|
|
1264
|
-
if (binding.overlay) names.push(binding.overlay.repairTrackName);
|
|
1265
|
-
return names;
|
|
1264
|
+
return binding.layers.map((layer) => layer.repairTrackName);
|
|
1266
1265
|
}
|
|
1267
1266
|
|
|
1267
|
+
type RepairOwnerEntry = MulticastRepairEndpoint["repairOwners"][number] & {
|
|
1268
|
+
ownerTrackName?: string;
|
|
1269
|
+
repairTrackName?: string;
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1268
1272
|
export class MulticastManager {
|
|
1269
1273
|
readonly #fec: FECManager;
|
|
1270
1274
|
readonly #router: MmtpRouter;
|
|
@@ -1276,7 +1280,7 @@ export class MulticastManager {
|
|
|
1276
1280
|
#failbackHooked = false;
|
|
1277
1281
|
/** Live joinRepairEndpoints() releases → the repair owners they registered,
|
|
1278
1282
|
* re-applied after configureMultiTrack() clears catalog-derived owners. */
|
|
1279
|
-
readonly #repairReleases = new Map<() => void,
|
|
1283
|
+
readonly #repairReleases = new Map<() => void, RepairOwnerEntry[]>();
|
|
1280
1284
|
#reportedFecDiagnosticsFailure = false;
|
|
1281
1285
|
|
|
1282
1286
|
// ── Stream-global FEC timing reduction ──
|
|
@@ -2291,7 +2295,14 @@ export class MulticastManager {
|
|
|
2291
2295
|
console.warn("[MulticastManager] repair group release failed", error);
|
|
2292
2296
|
}
|
|
2293
2297
|
};
|
|
2294
|
-
|
|
2298
|
+
// An overlay leg's source and repair track names come from the
|
|
2299
|
+
// endpoint's ingress tracks.
|
|
2300
|
+
const repairOwners: RepairOwnerEntry[] = endpoints.flatMap((endpoint) =>
|
|
2301
|
+
endpoint.repairOwners.map((owner) => owner.instance !== "keyframe" ? owner : {
|
|
2302
|
+
...owner,
|
|
2303
|
+
ownerTrackName: endpoint.tracks.find((t) => t.packetId === owner.ownerPacketId)?.name,
|
|
2304
|
+
repairTrackName: endpoint.tracks.find((t) => t.packetId === owner.packetId)?.name,
|
|
2305
|
+
}));
|
|
2295
2306
|
this.#applyRepairOwners(repairOwners);
|
|
2296
2307
|
for (const endpoint of endpoints) {
|
|
2297
2308
|
const ingress: MmtpIngressContext = {
|
|
@@ -2344,10 +2355,18 @@ export class MulticastManager {
|
|
|
2344
2355
|
return release;
|
|
2345
2356
|
}
|
|
2346
2357
|
|
|
2347
|
-
#applyRepairOwners(owners:
|
|
2348
|
-
for (const { packetId, ownerPacketId } of owners) {
|
|
2358
|
+
#applyRepairOwners(owners: RepairOwnerEntry[]): void {
|
|
2359
|
+
for (const { packetId, ownerPacketId, instance, ownerTrackName, repairTrackName } of owners) {
|
|
2349
2360
|
this.#router.setPacketIdRole(packetId, "repair");
|
|
2350
|
-
|
|
2361
|
+
if (instance === "keyframe") {
|
|
2362
|
+
this.#router.setKeyframeOverlayLeg(packetId, {
|
|
2363
|
+
sourcePacketId: ownerPacketId,
|
|
2364
|
+
...(ownerTrackName === undefined ? {} : { sourceCatalogTrackName: ownerTrackName }),
|
|
2365
|
+
...(repairTrackName === undefined ? {} : { repairTrackName }),
|
|
2366
|
+
});
|
|
2367
|
+
} else {
|
|
2368
|
+
this.#router.setRepairPacketIdOwner(packetId, ownerPacketId);
|
|
2369
|
+
}
|
|
2351
2370
|
}
|
|
2352
2371
|
}
|
|
2353
2372
|
|
|
@@ -2668,8 +2687,10 @@ export class MulticastManager {
|
|
|
2668
2687
|
packet: Uint8Array,
|
|
2669
2688
|
ownerPacketId?: number,
|
|
2670
2689
|
catalogTrackName?: string,
|
|
2690
|
+
/** The repair track it arrived on: names its FEC instance (mmt-02 7.4.2). */
|
|
2691
|
+
repairTrackName?: string,
|
|
2671
2692
|
) {
|
|
2672
|
-
this.#router.feedRepairPacket(packet, ownerPacketId, catalogTrackName);
|
|
2693
|
+
this.#router.feedRepairPacket(packet, ownerPacketId, catalogTrackName, repairTrackName);
|
|
2673
2694
|
}
|
|
2674
2695
|
|
|
2675
2696
|
/** Route a Worker-recovered FEC block through the MMTP parser + dedup pipeline. */
|
|
@@ -2759,7 +2780,10 @@ export class MulticastManager {
|
|
|
2759
2780
|
const ownedRepairNames = new Set<string>();
|
|
2760
2781
|
const claimed = new Map<string, number>();
|
|
2761
2782
|
for (const binding of bindings.values()) {
|
|
2762
|
-
|
|
2783
|
+
const overlay = binding.status === "ok" ? binding.overlay?.repairTrackName : undefined;
|
|
2784
|
+
for (const name of [...boundRepairNames(binding), ...(overlay === undefined ? [] : [overlay])]) {
|
|
2785
|
+
claimed.set(name, (claimed.get(name) ?? 0) + 1);
|
|
2786
|
+
}
|
|
2763
2787
|
}
|
|
2764
2788
|
for (const [name, count] of claimed) if (count === 1) ownedRepairNames.add(name);
|
|
2765
2789
|
const repairOwnerGroup = new Map<string, string>();
|
|
@@ -2928,10 +2952,12 @@ export class MulticastManager {
|
|
|
2928
2952
|
const binding = bindings.get(track.name);
|
|
2929
2953
|
if (binding?.status === "ok") {
|
|
2930
2954
|
const fecCfg = binding.base;
|
|
2931
|
-
// Every repair layer
|
|
2932
|
-
//
|
|
2933
|
-
//
|
|
2934
|
-
//
|
|
2955
|
+
// Every base repair layer routes to this source's decoder, not only
|
|
2956
|
+
// layer 0. A layer whose packetId had no registered owner would have
|
|
2957
|
+
// its repair symbols dropped as an unknown owner, and two sources whose
|
|
2958
|
+
// RS_ID ranges overlap would be indistinguishable. The keyframe overlay
|
|
2959
|
+
// is another FEC instance: it is recorded as an overlay leg, never
|
|
2960
|
+
// owned by this source's base decoder (draft-ramadan-mmt-fec-02 7.4.4).
|
|
2935
2961
|
if (track.packetId !== undefined) {
|
|
2936
2962
|
for (const repairName of boundRepairNames(binding)) {
|
|
2937
2963
|
if (!ownedRepairNames.has(repairName)) continue;
|
|
@@ -2940,6 +2966,16 @@ export class MulticastManager {
|
|
|
2940
2966
|
this.#router.setRepairPacketIdOwner(repairTrack.packetId, track.packetId);
|
|
2941
2967
|
}
|
|
2942
2968
|
}
|
|
2969
|
+
// By its name too: a catalog that states no PID for it (learned at
|
|
2970
|
+
// subscribe time) must still keep its MoQ repair from the base.
|
|
2971
|
+
const overlayName = binding.overlay?.repairTrackName;
|
|
2972
|
+
if (overlayName !== undefined && ownedRepairNames.has(overlayName)) {
|
|
2973
|
+
this.#router.setKeyframeOverlayLeg(catalogTracksByName.get(overlayName)?.packetId, {
|
|
2974
|
+
sourcePacketId: track.packetId,
|
|
2975
|
+
sourceCatalogTrackName: track.name,
|
|
2976
|
+
repairTrackName: overlayName,
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2943
2979
|
}
|
|
2944
2980
|
const wasmDecoder = fecDecoders.get(i);
|
|
2945
2981
|
if (track.packetId !== undefined && wasmDecoder !== undefined) {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
MMTP_PACKET_ID_MAX,
|
|
17
17
|
MMTP_SIGNALING_PACKET_ID,
|
|
18
18
|
readMMTPPacketId,
|
|
19
|
+
sameIpAddress,
|
|
19
20
|
} from '@blockcast/mmt-base'
|
|
20
21
|
import { isBcpV2RecordCandidate } from '@blockcast/mmt-manifest-verify'
|
|
21
22
|
|
|
@@ -41,6 +42,12 @@ export interface MulticastTransportConfig {
|
|
|
41
42
|
* are "MHM2" passes ahead of the packet_id read and the allow-list, and is
|
|
42
43
|
* never counted toward a packet_id. On an unprotected join it is an
|
|
43
44
|
* unparseable MMTP packet, as before.
|
|
45
|
+
*
|
|
46
|
+
* A protected join admits only datagrams whose reported sender is the
|
|
47
|
+
* flow's S (`sourceAddress`, or `senderAddress` on ASM), compared as IP
|
|
48
|
+
* addresses, before anything hashes them (multicast-02 7.2.7). A datagram
|
|
49
|
+
* with no reported sender is dropped, and a join that cannot report one
|
|
50
|
+
* (no S, or an ATSC 3.0 tuner) is declined `invalid-request`.
|
|
44
51
|
*/
|
|
45
52
|
protected?: boolean
|
|
46
53
|
}
|
|
@@ -148,7 +155,8 @@ export interface MulticastTransportStats {
|
|
|
148
155
|
* one (G, port, packet_id); the first sender whose packet is accepted owns
|
|
149
156
|
* the sub-flow until the next join, and every other sender is dropped here,
|
|
150
157
|
* before any gap, duplicate or FEC ledger sees it. A join or a silence
|
|
151
|
-
* failback releases the lock.
|
|
158
|
+
* failback releases the lock. On a protected join it also counts every
|
|
159
|
+
* datagram whose sender is not the flow's S, or is not reported.
|
|
152
160
|
*/
|
|
153
161
|
packetsDroppedForeignSender: number
|
|
154
162
|
}
|
|
@@ -215,7 +223,14 @@ function hasMulticastGateway(): boolean {
|
|
|
215
223
|
return typeof getMulticastGlobals().multicast?.subscribe === 'function'
|
|
216
224
|
}
|
|
217
225
|
|
|
218
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* The `bytes` event's sender (multicast-contract 1.3.0); undefined when unknown.
|
|
228
|
+
*
|
|
229
|
+
* The provider filters (S, G, port) and owns the multicast-02 7.2.7
|
|
230
|
+
* destination MUST: the event carries no destination address, so libmmt
|
|
231
|
+
* cannot discard a datagram whose IP destination is not G (pim BLO-51627).
|
|
232
|
+
* On a protected flow libmmt checks the sender itself, against S.
|
|
233
|
+
*/
|
|
219
234
|
function senderOf(event: CustomEvent<unknown>): string | undefined {
|
|
220
235
|
const sourceAddress = (event as { sourceAddress?: unknown }).sourceAddress
|
|
221
236
|
return typeof sourceAddress === 'string' ? sourceAddress : undefined
|
|
@@ -343,6 +358,10 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
343
358
|
readonly #acceptedBytesByPacketId = new Map<number, { bytes: number }>()
|
|
344
359
|
#packetIds: ReadonlySet<number> | null = null
|
|
345
360
|
#protected = false
|
|
361
|
+
// A protected join's S, and the last sender spelling that matched it, so a
|
|
362
|
+
// matching sender parses once per spelling rather than per datagram.
|
|
363
|
+
#protectedSource: string | undefined = undefined
|
|
364
|
+
#matchedSource: string | undefined = undefined
|
|
346
365
|
// The sender S this join's accepted packets came from, once known; see
|
|
347
366
|
// packetsDroppedForeignSender. Cleared by every join and by a silence
|
|
348
367
|
// failback, so a replacement publisher is not locked out.
|
|
@@ -406,6 +425,11 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
406
425
|
// failure before it runs, so the catch classifies a decline by where the
|
|
407
426
|
// join stopped, never by what the error says.
|
|
408
427
|
let failure: MulticastJoinDeclineReason = 'invalid-request'
|
|
428
|
+
const protectedSource = config.protected === true ? config.sourceAddress ?? config.senderAddress : undefined
|
|
429
|
+
if (config.protected === true && (protectedSource === undefined || config.atsc3)) {
|
|
430
|
+
console.warn('[MulticastTransport] protected join refused: the path reports no per-datagram sender to check against S')
|
|
431
|
+
return this.#declined(generation, 'invalid-request')
|
|
432
|
+
}
|
|
409
433
|
try {
|
|
410
434
|
const packetIds = config.packetIds?.length ? new Set(config.packetIds) : null
|
|
411
435
|
console.log(
|
|
@@ -454,8 +478,9 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
454
478
|
// costs batching on every any-source join, including one on a pre-1.3.0
|
|
455
479
|
// provider that sends no sender and so gets no lock for it; keep it
|
|
456
480
|
// unless the provider can say it carries the sender.
|
|
481
|
+
// A protected join checks every datagram's sender, so it listens to `bytes` too.
|
|
457
482
|
const anySource = !config.sourceAddress && !config.senderAddress && !config.atsc3
|
|
458
|
-
const packetListener: PacketListener = !anySource && subscriptionDeliversPacketBatches(subscription)
|
|
483
|
+
const packetListener: PacketListener = !anySource && protectedSource === undefined && subscriptionDeliversPacketBatches(subscription)
|
|
459
484
|
? {
|
|
460
485
|
type: 'packets',
|
|
461
486
|
listener: (event) => {
|
|
@@ -473,6 +498,8 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
473
498
|
this.#packetListener = packetListener
|
|
474
499
|
this.#packetIds = packetIds
|
|
475
500
|
this.#protected = config.protected === true
|
|
501
|
+
this.#protectedSource = protectedSource
|
|
502
|
+
this.#matchedSource = undefined
|
|
476
503
|
this.#providerFilter = {
|
|
477
504
|
acknowledged: packetIds,
|
|
478
505
|
sent: 0,
|
|
@@ -1009,6 +1036,8 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
1009
1036
|
this.#delivery = undefined
|
|
1010
1037
|
this.#packetIds = null
|
|
1011
1038
|
this.#protected = false
|
|
1039
|
+
this.#protectedSource = undefined
|
|
1040
|
+
this.#matchedSource = undefined
|
|
1012
1041
|
this.#providerFilter = null
|
|
1013
1042
|
this.#context = undefined
|
|
1014
1043
|
this.#withdrawnPacketIds.clear()
|
|
@@ -1016,8 +1045,18 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
1016
1045
|
|
|
1017
1046
|
/** The ingress filter; an accepted packet is counted here, once. */
|
|
1018
1047
|
#acceptPacket(packet: Uint8Array, sourceAddress?: string): boolean {
|
|
1048
|
+
const source = this.#protectedSource
|
|
1049
|
+
if (source !== undefined && (sourceAddress === undefined || sourceAddress !== this.#matchedSource)) {
|
|
1050
|
+
if (sourceAddress === undefined || !sameIpAddress(sourceAddress, source)) {
|
|
1051
|
+
this.#stats.packetsDroppedForeignSender++
|
|
1052
|
+
return false
|
|
1053
|
+
}
|
|
1054
|
+
this.#matchedSource = sourceAddress
|
|
1055
|
+
}
|
|
1019
1056
|
// Absent means unknown (a pre-1.3.0 provider, a tuner): never a new sender.
|
|
1020
|
-
|
|
1057
|
+
// A protected flow's S check above replaces the first-seen lock, which
|
|
1058
|
+
// compares spellings and would refuse S written another way.
|
|
1059
|
+
const owner = source === undefined ? this.#lockedTo?.() ?? this.#sender : undefined
|
|
1021
1060
|
if (sourceAddress !== undefined && owner !== undefined && sourceAddress !== owner) {
|
|
1022
1061
|
this.#stats.packetsDroppedForeignSender++
|
|
1023
1062
|
return false
|