@blockcast/mmt-container 0.5.0 → 1.0.0-main.3cb7359ea316
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 +21 -16
- package/dist/browser-runtime.js +1024 -1078
- package/dist/browser-runtime.js.map +4 -4
- package/dist/carp-catalog.d.ts +33 -87
- package/dist/carp-catalog.d.ts.map +1 -1
- package/dist/carp-catalog.js +12 -434
- package/dist/carp-catalog.js.map +4 -4
- package/dist/catalog-fec-params.d.ts +48 -161
- package/dist/catalog-fec-params.d.ts.map +1 -1
- package/dist/catalog-fec-params.js +567 -592
- package/dist/catalog-fec-params.js.map +4 -4
- package/dist/catalog-network-source.js +6 -2
- package/dist/catalog-network-source.js.map +3 -3
- package/dist/delivery-catalog.js +12 -4
- package/dist/delivery-catalog.js.map +4 -4
- package/dist/delta-fallback-config.d.ts +21 -26
- package/dist/delta-fallback-config.d.ts.map +1 -1
- package/dist/delta-fallback-config.js +582 -24
- package/dist/delta-fallback-config.js.map +4 -4
- package/dist/fec-manager.js +6 -2
- package/dist/fec-manager.js.map +3 -3
- package/dist/fec-state.d.ts +5 -43
- package/dist/fec-state.d.ts.map +1 -1
- package/dist/fec-state.js +529 -34
- package/dist/fec-state.js.map +4 -4
- package/dist/fec-worker-setup.js +6 -2
- package/dist/fec-worker-setup.js.map +3 -3
- package/dist/held-layer-admission.js +6 -2
- package/dist/held-layer-admission.js.map +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -39
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.js +6 -2
- package/dist/iso-block-locator.js.map +3 -3
- package/dist/mfu-reassembler.js +6 -2
- package/dist/mfu-reassembler.js.map +3 -3
- package/dist/mmtp-container-parser.js +6 -2
- package/dist/mmtp-container-parser.js.map +3 -3
- package/dist/mmtp-container-parser.no-modules.js +551 -452
- package/dist/mmtp-container-parser.no-modules.js.map +4 -4
- package/dist/mmtp-decode-pipeline.js +6 -2
- package/dist/mmtp-decode-pipeline.js.map +3 -3
- package/dist/mmtp-fec-session.d.ts +7 -6
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +13 -9
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +7 -16
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +19 -29
- package/dist/mmtp-router.js.map +3 -3
- package/dist/multicast-auth-catalog.js +7 -3
- package/dist/multicast-auth-catalog.js.map +3 -3
- package/dist/multicast-input.js +7 -3
- package/dist/multicast-input.js.map +3 -3
- package/dist/multicast-manager.d.ts +29 -32
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +682 -253
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +23 -2
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +57 -16
- package/dist/multicast-transport.js.map +4 -4
- package/dist/multicast-unwrap.js +6 -2
- package/dist/multicast-unwrap.js.map +3 -3
- package/dist/no-modules.js +551 -452
- package/dist/no-modules.js.map +4 -4
- package/dist/phase14-catalog-override.d.ts +10 -5
- package/dist/phase14-catalog-override.d.ts.map +1 -1
- package/dist/phase14-catalog-override.js +35 -11
- package/dist/phase14-catalog-override.js.map +2 -2
- package/dist/repair-fetch.d.ts +3 -1
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +16 -3
- package/dist/repair-fetch.js.map +3 -3
- package/dist/repair-layers.d.ts +0 -123
- package/dist/repair-layers.d.ts.map +1 -1
- package/dist/repair-layers.js +1 -349
- package/dist/repair-layers.js.map +3 -3
- package/dist/signaling.js +6 -2
- package/dist/signaling.js.map +3 -3
- package/dist/timestamp-codec.js +6 -2
- package/dist/timestamp-codec.js.map +3 -3
- package/dist/wasm-mfu-reassembler.js +6 -2
- package/dist/wasm-mfu-reassembler.js.map +3 -3
- package/dist/wasm-mmtp-container-parser.js +6 -2
- package/dist/wasm-mmtp-container-parser.js.map +3 -3
- package/package.json +9 -5
- package/schemas/catalog.schema.json +53 -58
- package/src/carp-catalog.ts +39 -229
- package/src/catalog-fec-params.ts +130 -493
- package/src/delta-fallback-config.ts +63 -50
- package/src/fec-state.ts +7 -62
- package/src/index.ts +10 -30
- package/src/mmtp-fec-session.ts +11 -10
- package/src/mmtp-router.ts +14 -29
- package/src/multicast-manager.ts +89 -121
- package/src/multicast-transport.ts +65 -12
- package/src/phase14-catalog-override.ts +67 -16
- package/src/repair-fetch.ts +27 -3
- package/src/repair-layers.ts +7 -520
- package/test-vectors/catalog/README.md +39 -67
- package/test-vectors/catalog/fec-01-runtime/README.md +15 -0
- package/test-vectors/catalog/{negative/fec-block-profile-retired.json → fec-01-runtime/fec-iso-ssbg1.json} +1 -1
- package/test-vectors/catalog/fec-01-runtime/fec-layered-repair.json +106 -0
- package/test-vectors/catalog/{negative/fec-block-profile-unknown.json → fec-01-runtime/fec-multicast.json} +2 -1
- package/test-vectors/catalog/fec-01-runtime/multicast-native-ssm-no-network-source.json +44 -0
- package/test-vectors/catalog/negative/fec-block-profile-missing.json +24 -11
- package/test-vectors/catalog/negative/fec-iso-symbol-size-below-floor.json +13 -8
- package/test-vectors/catalog/negative/fec-reed-solomon-iso-symbol-size-13.json +13 -8
- package/test-vectors/catalog/negative/fec-repair-layer-geometry.json +20 -10
- package/test-vectors/catalog/negative/fec-repair-layer-missing-symbols.json +20 -10
- package/test-vectors/catalog/negative/fec-repair-overlay-source-symbols-zero.json +25 -17
- package/test-vectors/catalog/negative/fec-repair-symbols-zero.json +13 -8
- package/test-vectors/catalog/negative/fec-source-symbols-zero.json +13 -8
- package/test-vectors/catalog/negative/fec-symbol-size-zero.json +13 -8
- package/test-vectors/catalog/negative/raptorq-source-symbols-over-max.json +18 -9
- package/test-vectors/catalog/negative/raptorq-unaligned-symbol.json +13 -8
- package/test-vectors/catalog/positive/fec-algorithm-unimplemented.json +14 -9
- package/test-vectors/catalog/positive/fec-iso-ssbg1.json +25 -12
- package/test-vectors/catalog/positive/fec-layered-repair.json +79 -35
- package/test-vectors/catalog/positive/fec-multicast.json +26 -13
- package/test-vectors/catalog/positive/fec-reed-solomon-layered.json +29 -18
- package/test-vectors/catalog/positive/multicast-draft01-4-1-example.json +28 -20
- package/test-vectors/catalog/positive/multicast-draft01-7-2-6-example.json +14 -10
- package/test-vectors/catalog/positive/multicast-native-ssm-no-network-source.json +22 -12
- package/test-vectors/catalog/section-codes.json +13 -22
- package/test-vectors/catalog-timing.json +2 -2
- package/test-vectors/catalog-validation.json +30 -243
- package/test-vectors/catalog/negative/fec-enhancement-repair-removed.json +0 -28
- package/test-vectors/catalog/negative/fec-reed-solomon-k-plus-p-over-255.json +0 -39
- package/test-vectors/catalog/negative/fec-reed-solomon-overlay-over-255.json +0 -40
- package/test-vectors/catalog/negative/fec-repair-multiple-depends.json +0 -32
- package/test-vectors/catalog/negative/fec-repair-overlay-with-layer.json +0 -34
- package/test-vectors/catalog/negative/fec-repair-priority-out-of-band.json +0 -24
- package/test-vectors/catalog/negative/fec-repair-source-symbols-without-scope.json +0 -33
package/src/multicast-manager.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - MulticastTransport — SSM join/socket/watchdog
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { parseFecBlockProfile, type FecBlockProfile } from "@blockcast/mmt-base";
|
|
11
|
+
import { parseFecBlockProfile, resolveFecBindings, type FecBlockProfile, type FecSourceBinding } from "@blockcast/mmt-base";
|
|
12
12
|
import {
|
|
13
13
|
FECManager,
|
|
14
14
|
MIN_INTERLEAVE_DEPTH_MS,
|
|
@@ -79,12 +79,9 @@ import {
|
|
|
79
79
|
hasSwitchingSetRole,
|
|
80
80
|
parseTrackRole,
|
|
81
81
|
validateSwitchingSet,
|
|
82
|
-
isFecCatalogConfig,
|
|
83
|
-
isFecCatalogGeometry,
|
|
84
|
-
type FecCatalogConfig,
|
|
85
82
|
type TrackRole,
|
|
86
83
|
} from "./carp-catalog.js";
|
|
87
|
-
import {
|
|
84
|
+
import type { TrackFecUnavailable } from "./catalog-fec-params.js";
|
|
88
85
|
|
|
89
86
|
/**
|
|
90
87
|
* One dedicated repair group to join beside the media join: the shape
|
|
@@ -110,11 +107,13 @@ export interface MulticastRepairEndpoint {
|
|
|
110
107
|
/**
|
|
111
108
|
* Opens one repair-group subscription through the caller's own provider (a
|
|
112
109
|
* hang bridge, for one): delivers each packet to `onPacket` and returns (or
|
|
113
|
-
* resolves to) the call that releases it.
|
|
110
|
+
* resolves to) the call that releases it. Pass each datagram's sender as
|
|
111
|
+
* `sourceAddress` when known: the group then admits only its first sender,
|
|
112
|
+
* as MulticastTransport does (`packetsDroppedForeignSender`).
|
|
114
113
|
*/
|
|
115
114
|
export type MulticastRepairSubscribe = (
|
|
116
115
|
endpoint: MulticastRepairEndpoint,
|
|
117
|
-
onPacket: (packet: Uint8Array) => void,
|
|
116
|
+
onPacket: (packet: Uint8Array, sourceAddress?: string) => void,
|
|
118
117
|
) => (() => void) | Promise<() => void>;
|
|
119
118
|
|
|
120
119
|
/**
|
|
@@ -1092,10 +1091,12 @@ export interface MultiTrackCatalogTrack {
|
|
|
1092
1091
|
/** `"fec-repair"` marks a self-describing repair track (draft-ramadan-moq-fec
|
|
1093
1092
|
* §6.3). Absent on media tracks and on naming-era catalogs. */
|
|
1094
1093
|
packaging?: string;
|
|
1095
|
-
/** The
|
|
1096
|
-
*
|
|
1097
|
-
* is
|
|
1098
|
-
|
|
1094
|
+
/** The source track this repair track carries repair symbols for
|
|
1095
|
+
* (draft-ramadan-moq-fec-catalog-00 3.2), with `instanceId` naming the
|
|
1096
|
+
* instance (default 0). Layer 0 is also named by the instance's
|
|
1097
|
+
* `repairTrack`. */
|
|
1098
|
+
source?: string;
|
|
1099
|
+
instanceId?: number;
|
|
1099
1100
|
/** Base layer index. Absent on a keyframe overlay, which is a separate FEC
|
|
1100
1101
|
* instance rather than a member of the layer chain. */
|
|
1101
1102
|
repairLayer?: number;
|
|
@@ -1229,47 +1230,40 @@ function altGroupKey(track: MultiTrackCatalogTrack): string {
|
|
|
1229
1230
|
}
|
|
1230
1231
|
|
|
1231
1232
|
/**
|
|
1232
|
-
* Every repair track
|
|
1233
|
-
* `
|
|
1234
|
-
*
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1237
|
-
*
|
|
1238
|
-
* name suffix, so a publisher-chosen name like "parity-alpha" resolves.
|
|
1239
|
-
*
|
|
1240
|
-
* This deliberately does not call `resolveRepairLayers`: that validates the
|
|
1241
|
-
* full layer contract and requires `packaging: "mmtp"` on the source plus a
|
|
1242
|
-
* declared layer-0 repair track, neither of which a `MultiTrackCatalog`
|
|
1243
|
-
* carries. Running it here would throw on every naming-era catalog. Ownership
|
|
1244
|
-
* registration needs membership only; RS_ID offsets and layer ordering are the
|
|
1245
|
-
* decoder's concern, not the router's.
|
|
1233
|
+
* Every repair track each source names or is named by: each `fec` instance's
|
|
1234
|
+
* `repairTrack`, plus every `packaging: "fec-repair"` track whose `source`
|
|
1235
|
+
* names it (draft-ramadan-moq-fec-catalog-00 3.2). Membership only, so a
|
|
1236
|
+
* source whose FEC is not `ok` still keeps its legs roled `repair`; which legs
|
|
1237
|
+
* get an owner is the source's binding's (`resolveFecBindings`). Never from a
|
|
1238
|
+
* name suffix.
|
|
1246
1239
|
*/
|
|
1247
1240
|
function repairTrackNamesBySource(
|
|
1248
1241
|
tracks: readonly MultiTrackCatalogTrack[],
|
|
1249
1242
|
): Map<string, Set<string>> {
|
|
1250
1243
|
const bySource = new Map<string, Set<string>>();
|
|
1251
|
-
const add = (source:
|
|
1252
|
-
if (!source || !repair) return;
|
|
1244
|
+
const add = (source: unknown, repair: unknown): void => {
|
|
1245
|
+
if (typeof source !== "string" || typeof repair !== "string" || !source || !repair) return;
|
|
1253
1246
|
const names = bySource.get(source) ?? new Set<string>();
|
|
1254
1247
|
names.add(repair);
|
|
1255
1248
|
bySource.set(source, names);
|
|
1256
1249
|
};
|
|
1257
1250
|
for (const track of tracks) {
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
// not without its repair track): ownership is membership, not
|
|
1261
|
-
// decodability, so no isFecCatalogConfig gate here.
|
|
1262
|
-
const repairTrack = typeof track.fec === "object" && track.fec !== null
|
|
1263
|
-
? (track.fec as { readonly repairTrack?: unknown }).repairTrack
|
|
1264
|
-
: undefined;
|
|
1265
|
-
if (typeof repairTrack === "string") add(track.name, repairTrack);
|
|
1266
|
-
if (track.packaging === "fec-repair" && Array.isArray(track.depends) && track.depends.length === 1) {
|
|
1267
|
-
add(track.depends[0]!, track.name);
|
|
1251
|
+
if (Array.isArray(track.fec)) {
|
|
1252
|
+
for (const instance of track.fec) add(track.name, (instance as { repairTrack?: unknown } | null)?.repairTrack);
|
|
1268
1253
|
}
|
|
1254
|
+
if (track.packaging === "fec-repair") add(track.source, track.name);
|
|
1269
1255
|
}
|
|
1270
1256
|
return bySource;
|
|
1271
1257
|
}
|
|
1272
1258
|
|
|
1259
|
+
/** The repair tracks an `ok` binding joins: base layers, then the overlay. */
|
|
1260
|
+
function boundRepairNames(binding: FecSourceBinding | undefined): string[] {
|
|
1261
|
+
if (binding?.status !== "ok") return [];
|
|
1262
|
+
const names = binding.layers.map((layer) => layer.repairTrackName);
|
|
1263
|
+
if (binding.overlay) names.push(binding.overlay.repairTrackName);
|
|
1264
|
+
return names;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1273
1267
|
export class MulticastManager {
|
|
1274
1268
|
readonly #fec: FECManager;
|
|
1275
1269
|
readonly #router: MmtpRouter;
|
|
@@ -2032,10 +2026,6 @@ export class MulticastManager {
|
|
|
2032
2026
|
onTrackPacket(packetId: number, cb: TrackPacketCallback) {
|
|
2033
2027
|
this.#router.onTrackPacket(packetId, cb);
|
|
2034
2028
|
}
|
|
2035
|
-
/** @deprecated Use onTrackInit(packetId, cb); see MmtpRouter.onInitSegment. */
|
|
2036
|
-
onInitSegment(cb: (data: Uint8Array) => void) {
|
|
2037
|
-
this.#router.onInitSegment(cb);
|
|
2038
|
-
}
|
|
2039
2029
|
onTrackInit(packetId: number, cb: (data: Uint8Array) => void) {
|
|
2040
2030
|
this.#router.onTrackInit(packetId, cb);
|
|
2041
2031
|
}
|
|
@@ -2321,8 +2311,13 @@ export class MulticastManager {
|
|
|
2321
2311
|
tracks: Object.freeze(endpoint.tracks.map(({ name, packetId }) => Object.freeze({ name, packetId }))),
|
|
2322
2312
|
...(endpoint.flow === undefined ? {} : { flow: endpoint.flow }),
|
|
2323
2313
|
};
|
|
2324
|
-
|
|
2325
|
-
|
|
2314
|
+
let sender: string | undefined;
|
|
2315
|
+
const onPacket = (packet: Uint8Array, sourceAddress?: string) => {
|
|
2316
|
+
if (released) return;
|
|
2317
|
+
// The media join's sender owns the repair too; this group's own first
|
|
2318
|
+
// sender stands in only until the media join has one.
|
|
2319
|
+
if (sourceAddress !== undefined && sourceAddress !== (this.#transport.sender ?? (sender ??= sourceAddress))) return;
|
|
2320
|
+
this.#router.feedPacket(packet, "multicast", ingress);
|
|
2326
2321
|
};
|
|
2327
2322
|
const fail = (error: unknown) =>
|
|
2328
2323
|
console.warn(`[MulticastManager] repair group ${endpoint.group}:${endpoint.port} not joined`, error);
|
|
@@ -2332,7 +2327,10 @@ export class MulticastManager {
|
|
|
2332
2327
|
.then((release) => (released ? safely(release) : releases.push(release)), fail);
|
|
2333
2328
|
continue;
|
|
2334
2329
|
}
|
|
2335
|
-
const transport = new MulticastTransport((packet) => onPacket(packet), {
|
|
2330
|
+
const transport = new MulticastTransport((packet) => onPacket(packet), {
|
|
2331
|
+
gateway: provider,
|
|
2332
|
+
lockedTo: () => this.#transport.sender,
|
|
2333
|
+
});
|
|
2336
2334
|
releases.push(() => transport.dispose());
|
|
2337
2335
|
void transport.join({
|
|
2338
2336
|
sourceAddress: endpoint.source,
|
|
@@ -2663,8 +2661,8 @@ export class MulticastManager {
|
|
|
2663
2661
|
* transport attribution in FEC path counters.
|
|
2664
2662
|
* The `router` accessor above is already public.
|
|
2665
2663
|
*/
|
|
2666
|
-
feedPacket(data: ArrayBuffer | Uint8Array, ingress?: MmtpIngressContext) {
|
|
2667
|
-
this.#transport.feedPacket(data, ingress);
|
|
2664
|
+
feedPacket(data: ArrayBuffer | Uint8Array, ingress?: MmtpIngressContext, sourceAddress?: string) {
|
|
2665
|
+
this.#transport.feedPacket(data, ingress, sourceAddress);
|
|
2668
2666
|
}
|
|
2669
2667
|
|
|
2670
2668
|
/**
|
|
@@ -2718,20 +2716,18 @@ export class MulticastManager {
|
|
|
2718
2716
|
* D-17: Builds altGroup → track-name index for getSwitchingSet() (repair legs excluded).
|
|
2719
2717
|
* D-26: Registers packetId → TrackRole on MmtpRouter for block-complete fanout.
|
|
2720
2718
|
*
|
|
2721
|
-
* FEC
|
|
2722
|
-
*
|
|
2723
|
-
*
|
|
2724
|
-
*
|
|
2725
|
-
* its repair tracks
|
|
2726
|
-
*
|
|
2727
|
-
*
|
|
2728
|
-
*
|
|
2729
|
-
*
|
|
2730
|
-
*
|
|
2731
|
-
*
|
|
2732
|
-
*
|
|
2733
|
-
* Geometry a decoder could not configure (K, P, T, `repairTrack`,
|
|
2734
|
-
* algorithm) still throws.
|
|
2719
|
+
* FEC is per track and per instance (`resolveFecBindings`,
|
|
2720
|
+
* draft-ramadan-moq-fec-catalog-00 array, fec-core-00 10.3). A media track
|
|
2721
|
+
* whose base instance is not `ok` (an invalid instance, a profile other than
|
|
2722
|
+
* `iso-ssbg1-v1`, a draft-01 object `fec`) is consumed without FEC: it gets
|
|
2723
|
+
* no decoder, and none of its repair tracks is given an owner, so the router
|
|
2724
|
+
* drops their packets before any decoder. Those legs keep the `repair` role
|
|
2725
|
+
* and their owner's switching set. Neither the call nor any other track's
|
|
2726
|
+
* FEC fails on that account; `trackFecUnavailable(name)` reports the reason.
|
|
2727
|
+
* FEC a previous call configured on a packetId this call does not is
|
|
2728
|
+
* released (FECManager.releasePacket); frames its release gate held are
|
|
2729
|
+
* delivered in order, each gap stalled. FEC wired by hand (setFecDecoder) is
|
|
2730
|
+
* left alone.
|
|
2735
2731
|
*
|
|
2736
2732
|
* @param catalog - Multi-track catalog (tracks array with name, altGroup, fec, packetId).
|
|
2737
2733
|
* @param wasmDecoderSource - One WASM FEC decoder for a single FEC stream, or
|
|
@@ -2756,44 +2752,27 @@ export class MulticastManager {
|
|
|
2756
2752
|
// mis-roled or rejected on its name, and would land in its own altGroup
|
|
2757
2753
|
// rather than its owner's.
|
|
2758
2754
|
const repairNamesBySource = repairTrackNamesBySource(catalog.tracks);
|
|
2759
|
-
//
|
|
2760
|
-
//
|
|
2761
|
-
//
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
}
|
|
2755
|
+
// FEC per source, per instance (fec-core-00 10.3). A configureMultiTrack
|
|
2756
|
+
// catalog is MMTP by construction, so a media track that states no
|
|
2757
|
+
// `packaging` is read as `mmtp`.
|
|
2758
|
+
const bindings = resolveFecBindings({
|
|
2759
|
+
tracks: catalog.tracks.map((track) =>
|
|
2760
|
+
track.packaging === undefined ? { ...track, packaging: "mmtp" } : track,
|
|
2761
|
+
),
|
|
2762
|
+
});
|
|
2768
2763
|
const fecUnavailableByTrack = new Map<string, TrackFecUnavailable>();
|
|
2769
|
-
for (
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
const
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
const layerZero = (track.fec as { repairTrack?: unknown }).repairTrack;
|
|
2782
|
-
if (typeof layerZero === "string" && layerZero.length > 0) {
|
|
2783
|
-
const names = repairNamesBySource.get(track.name) ?? new Set<string>();
|
|
2784
|
-
names.add(layerZero);
|
|
2785
|
-
repairNamesBySource.set(track.name, names);
|
|
2786
|
-
}
|
|
2787
|
-
}
|
|
2788
|
-
// Every repair leg of a source without FEC goes unowned, including one a
|
|
2789
|
-
// source with FEC also names: the router could not tell the two sources'
|
|
2790
|
-
// symbols apart on it, and the unavailable source's must reach no decoder.
|
|
2791
|
-
const unownedRepairNames = new Set<string>();
|
|
2792
|
-
for (const sourceName of fecUnavailableByTrack.keys()) {
|
|
2793
|
-
for (const repairName of repairNamesBySource.get(sourceName) ?? []) {
|
|
2794
|
-
unownedRepairNames.add(repairName);
|
|
2795
|
-
}
|
|
2796
|
-
}
|
|
2764
|
+
for (const binding of bindings.values()) {
|
|
2765
|
+
if (binding.status === "unavailable") fecUnavailableByTrack.set(binding.track, binding.reason);
|
|
2766
|
+
}
|
|
2767
|
+
// Only the legs an `ok` binding joins get an owner. Every other named leg
|
|
2768
|
+
// (a source without FEC, a dropped overlay, a leg two sources name) keeps
|
|
2769
|
+
// its `repair` role and reaches no decoder.
|
|
2770
|
+
const ownedRepairNames = new Set<string>();
|
|
2771
|
+
const claimed = new Map<string, number>();
|
|
2772
|
+
for (const binding of bindings.values()) {
|
|
2773
|
+
for (const name of boundRepairNames(binding)) claimed.set(name, (claimed.get(name) ?? 0) + 1);
|
|
2774
|
+
}
|
|
2775
|
+
for (const [name, count] of claimed) if (count === 1) ownedRepairNames.add(name);
|
|
2797
2776
|
const repairOwnerGroup = new Map<string, string>();
|
|
2798
2777
|
for (const track of catalog.tracks) {
|
|
2799
2778
|
for (const repairName of repairNamesBySource.get(track.name) ?? []) {
|
|
@@ -2854,17 +2833,8 @@ export class MulticastManager {
|
|
|
2854
2833
|
const fecTrackIndexes: number[] = [];
|
|
2855
2834
|
for (let i = 0; i < catalog.tracks.length; i++) {
|
|
2856
2835
|
const track = catalog.tracks[i]!;
|
|
2857
|
-
if (resolvedRoles[i] === "repair" || track.
|
|
2858
|
-
|
|
2859
|
-
// unavailable profile or span only costs this track its FEC (§5.1).
|
|
2860
|
-
const unavailable = fecUnavailableByTrack.has(track.name);
|
|
2861
|
-
if (!(unavailable ? isFecCatalogGeometry(track.fec) : isFecCatalogConfig(track.fec))) {
|
|
2862
|
-
throw new Error(
|
|
2863
|
-
`[MulticastManager] invalid fec config for track '${track.name}'`,
|
|
2864
|
-
);
|
|
2865
|
-
}
|
|
2866
|
-
if (unavailable) continue;
|
|
2867
|
-
if (track.packetId !== undefined) fecTrackIndexes.push(i);
|
|
2836
|
+
if (resolvedRoles[i] === "repair" || track.packetId === undefined) continue;
|
|
2837
|
+
if (bindings.get(track.name)?.status === "ok") fecTrackIndexes.push(i);
|
|
2868
2838
|
}
|
|
2869
2839
|
|
|
2870
2840
|
// Stateful decoders cannot span physical streams: resetting one stream
|
|
@@ -2966,15 +2936,16 @@ export class MulticastManager {
|
|
|
2966
2936
|
|
|
2967
2937
|
// D-07: Apply per-track FEC config for non-repair tracks. A track whose
|
|
2968
2938
|
// FEC is unavailable (§5.1) is consumed like a track without `fec`.
|
|
2969
|
-
|
|
2970
|
-
|
|
2939
|
+
const binding = bindings.get(track.name);
|
|
2940
|
+
if (binding?.status === "ok") {
|
|
2941
|
+
const fecCfg = binding.base;
|
|
2971
2942
|
// Every repair layer and the keyframe overlay route to this source's
|
|
2972
2943
|
// decoder, not only layer 0. A layer whose packetId had no registered
|
|
2973
2944
|
// owner would have its repair symbols dropped as an unknown owner, and
|
|
2974
2945
|
// two sources whose RS_ID ranges overlap would be indistinguishable.
|
|
2975
2946
|
if (track.packetId !== undefined) {
|
|
2976
|
-
for (const repairName of
|
|
2977
|
-
if (
|
|
2947
|
+
for (const repairName of boundRepairNames(binding)) {
|
|
2948
|
+
if (!ownedRepairNames.has(repairName)) continue;
|
|
2978
2949
|
const repairTrack = catalogTracksByName.get(repairName);
|
|
2979
2950
|
if (repairTrack?.packetId !== undefined) {
|
|
2980
2951
|
this.#router.setRepairPacketIdOwner(repairTrack.packetId, track.packetId);
|
|
@@ -2988,7 +2959,7 @@ export class MulticastManager {
|
|
|
2988
2959
|
// have no FEC in Phase 14 (D-26 covers video base+delta only). Callers that need
|
|
2989
2960
|
// audio FEC can call setFecDecoder() directly after configureMultiTrack().
|
|
2990
2961
|
const blockProfile = parseFecBlockProfile(
|
|
2991
|
-
|
|
2962
|
+
fecCfg.blockProfile,
|
|
2992
2963
|
`tracks[${JSON.stringify(track.name)}].fec.blockProfile`,
|
|
2993
2964
|
);
|
|
2994
2965
|
this.#fec.setFecDecoder(
|
|
@@ -3021,14 +2992,11 @@ export class MulticastManager {
|
|
|
3021
2992
|
|
|
3022
2993
|
/**
|
|
3023
2994
|
* Why `configureMultiTrack` consumes the named catalog track without FEC
|
|
3024
|
-
* although its
|
|
3025
|
-
*
|
|
3026
|
-
*
|
|
3027
|
-
*
|
|
3028
|
-
*
|
|
3029
|
-
* resolver reports (`catalogFecUnavailable`). Undefined for every other
|
|
3030
|
-
* track, and for every track before the first call; each call replaces the
|
|
3031
|
-
* previous call's answers.
|
|
2995
|
+
* although it declares some: its base instance is not `ok`, with the
|
|
2996
|
+
* validator's code and path (`TrackFecUnavailable`), the same reason the
|
|
2997
|
+
* hang resolver reports (`catalogTrackFecUnavailable`). Undefined for every
|
|
2998
|
+
* other track, and for every track before the first call; each call
|
|
2999
|
+
* replaces the previous call's answers.
|
|
3032
3000
|
*/
|
|
3033
3001
|
trackFecUnavailable(trackName: string): TrackFecUnavailable | undefined {
|
|
3034
3002
|
return this.#fecUnavailableByTrack.get(trackName);
|
|
@@ -133,6 +133,15 @@ export interface MulticastTransportStats {
|
|
|
133
133
|
* `droppedPacketIds` slot.
|
|
134
134
|
*/
|
|
135
135
|
packetsDroppedSignaling: number
|
|
136
|
+
/**
|
|
137
|
+
* From a sender other than the one this join locked to (BLO-50098). An
|
|
138
|
+
* unprotected ASM subscription admits any source, so two senders can share
|
|
139
|
+
* one (G, port, packet_id); the first sender whose packet is accepted owns
|
|
140
|
+
* the sub-flow until the next join, and every other sender is dropped here,
|
|
141
|
+
* before any gap, duplicate or FEC ledger sees it. A join or a silence
|
|
142
|
+
* failback releases the lock.
|
|
143
|
+
*/
|
|
144
|
+
packetsDroppedForeignSender: number
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
/**
|
|
@@ -197,6 +206,12 @@ function hasMulticastGateway(): boolean {
|
|
|
197
206
|
return typeof getMulticastGlobals().multicast?.subscribe === 'function'
|
|
198
207
|
}
|
|
199
208
|
|
|
209
|
+
/** The `bytes` event's sender (multicast-contract 1.3.0); undefined when unknown. */
|
|
210
|
+
function senderOf(event: CustomEvent<unknown>): string | undefined {
|
|
211
|
+
const sourceAddress = (event as { sourceAddress?: unknown }).sourceAddress
|
|
212
|
+
return typeof sourceAddress === 'string' ? sourceAddress : undefined
|
|
213
|
+
}
|
|
214
|
+
|
|
200
215
|
/**
|
|
201
216
|
* The packet one `bytes` event (or one element of a `packets` batch) carries,
|
|
202
217
|
* as a view of the provider's own bytes. A Uint8Array is used as it is; the
|
|
@@ -293,6 +308,7 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
293
308
|
packetsDroppedUnregisteredPid: 0,
|
|
294
309
|
packetsDroppedUnparseable: 0,
|
|
295
310
|
packetsDroppedSignaling: 0,
|
|
311
|
+
packetsDroppedForeignSender: 0,
|
|
296
312
|
}
|
|
297
313
|
// join() calls by how they resolved; see joinStats.
|
|
298
314
|
#joinsSucceeded = 0
|
|
@@ -317,6 +333,11 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
317
333
|
// per packet once a count outgrows the small-integer range.
|
|
318
334
|
readonly #acceptedBytesByPacketId = new Map<number, { bytes: number }>()
|
|
319
335
|
#packetIds: ReadonlySet<number> | null = null
|
|
336
|
+
// The sender S this join's accepted packets came from, once known; see
|
|
337
|
+
// packetsDroppedForeignSender. Cleared by every join and by a silence
|
|
338
|
+
// failback, so a replacement publisher is not locked out.
|
|
339
|
+
#sender: string | undefined = undefined
|
|
340
|
+
readonly #lockedTo: (() => string | undefined) | undefined
|
|
320
341
|
// What the live join delivers with each accepted packet. A field, not a
|
|
321
342
|
// listener closure, so an in-place admission change (setContext) applies
|
|
322
343
|
// from the next packet on without rejoining.
|
|
@@ -340,12 +361,18 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
340
361
|
// The provider to subscribe through; window.multicast when unset.
|
|
341
362
|
readonly #gateway: WindowMulticastGateway | undefined
|
|
342
363
|
|
|
364
|
+
/**
|
|
365
|
+
* `lockedTo` reads the sender another transport is locked to (a repair
|
|
366
|
+
* group follows its media join, whose publisher sends both). While it reads
|
|
367
|
+
* a sender, this join admits only that one; until then its own lock applies.
|
|
368
|
+
*/
|
|
343
369
|
constructor(
|
|
344
370
|
onPacket: (packet: Uint8Array, context?: TPacketContext) => void,
|
|
345
|
-
options: { gateway?: WindowMulticastGateway } = {},
|
|
371
|
+
options: { gateway?: WindowMulticastGateway; lockedTo?: () => string | undefined } = {},
|
|
346
372
|
) {
|
|
347
373
|
this.#onPacket = onPacket
|
|
348
374
|
this.#gateway = options.gateway
|
|
375
|
+
this.#lockedTo = options.lockedTo
|
|
349
376
|
}
|
|
350
377
|
|
|
351
378
|
static isAvailable(): boolean {
|
|
@@ -356,6 +383,7 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
356
383
|
const generation = ++this.#joinGeneration
|
|
357
384
|
this.#releaseSubscription()
|
|
358
385
|
this.#dataPlaneLive = false
|
|
386
|
+
this.#sender = undefined
|
|
359
387
|
|
|
360
388
|
if (this.#disposed) return this.#declined(generation, 'disposed')
|
|
361
389
|
const gateway = this.#gateway ?? getMulticastGlobals().multicast
|
|
@@ -411,7 +439,13 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
411
439
|
// one DOM event and one liveness read per batch instead of per packet
|
|
412
440
|
// (BLO-36599). Every packet still goes through the same filter, counters
|
|
413
441
|
// and callback, in arrival order, as a view of the provider's bytes.
|
|
414
|
-
|
|
442
|
+
// A batch carries no per-packet sender, so an any-source join, the one
|
|
443
|
+
// join that can see two senders, listens to `bytes` (BLO-50098). That
|
|
444
|
+
// costs batching on every any-source join, including one on a pre-1.3.0
|
|
445
|
+
// provider that sends no sender and so gets no lock for it; keep it
|
|
446
|
+
// unless the provider can say it carries the sender.
|
|
447
|
+
const anySource = !config.sourceAddress && !config.senderAddress && !config.atsc3
|
|
448
|
+
const packetListener: PacketListener = !anySource && subscriptionDeliversPacketBatches(subscription)
|
|
415
449
|
? {
|
|
416
450
|
type: 'packets',
|
|
417
451
|
listener: (event) => {
|
|
@@ -423,7 +457,7 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
423
457
|
type: 'bytes',
|
|
424
458
|
listener: (event) => {
|
|
425
459
|
if (!live()) return
|
|
426
|
-
if (this.#deliver(event.detail)) this.#resetWatchdog()
|
|
460
|
+
if (this.#deliver(event.detail, senderOf(event))) this.#resetWatchdog()
|
|
427
461
|
},
|
|
428
462
|
}
|
|
429
463
|
this.#packetListener = packetListener
|
|
@@ -546,13 +580,13 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
546
580
|
* One packet from the subscription: filter, count, and hand it to the
|
|
547
581
|
* callback. Returns whether it was delivered; the caller notes liveness.
|
|
548
582
|
*/
|
|
549
|
-
#deliver(detail: unknown): boolean {
|
|
583
|
+
#deliver(detail: unknown, sourceAddress?: string): boolean {
|
|
550
584
|
const packet = normalizeBytesEventDetail(detail)
|
|
551
585
|
if (!packet) {
|
|
552
586
|
this.#stats.packetsDroppedUnparseable++
|
|
553
587
|
return false
|
|
554
588
|
}
|
|
555
|
-
if (!this.#acceptPacket(packet)) return false
|
|
589
|
+
if (!this.#acceptPacket(packet, sourceAddress)) return false
|
|
556
590
|
// Read per packet: a callback that changes the admission (an in-place
|
|
557
591
|
// rendition switch committing on this very packet) takes effect from the
|
|
558
592
|
// next packet, including the rest of a batch.
|
|
@@ -604,11 +638,15 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
604
638
|
}
|
|
605
639
|
}
|
|
606
640
|
|
|
607
|
-
/**
|
|
608
|
-
|
|
641
|
+
/**
|
|
642
|
+
* Feed a raw packet from an external source (e.g., AMT tunnel or ASM
|
|
643
|
+
* subscription). `sourceAddress` is the datagram's sender when the source
|
|
644
|
+
* knows it; see packetsDroppedForeignSender.
|
|
645
|
+
*/
|
|
646
|
+
feedPacket(data: ArrayBuffer | Uint8Array, context?: TPacketContext, sourceAddress?: string): void {
|
|
609
647
|
if (this.#disposed) return
|
|
610
648
|
const packet = data instanceof Uint8Array ? data : new Uint8Array(data)
|
|
611
|
-
if (!this.#acceptPacket(packet)) return
|
|
649
|
+
if (!this.#acceptPacket(packet, sourceAddress)) return
|
|
612
650
|
this.#onPacket(packet, context)
|
|
613
651
|
this.#resetWatchdog()
|
|
614
652
|
}
|
|
@@ -778,6 +816,11 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
778
816
|
return this.#withdrawnPacketIdDrops
|
|
779
817
|
}
|
|
780
818
|
|
|
819
|
+
/** The sender this join is locked to; undefined until one is known. */
|
|
820
|
+
get sender(): string | undefined {
|
|
821
|
+
return this.#sender
|
|
822
|
+
}
|
|
823
|
+
|
|
781
824
|
/** Whether a subscription is live: joined, and not yet released or replaced. */
|
|
782
825
|
get joined(): boolean {
|
|
783
826
|
return this.#subscription !== null
|
|
@@ -953,7 +996,13 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
953
996
|
}
|
|
954
997
|
|
|
955
998
|
/** The ingress filter; an accepted packet is counted here, once. */
|
|
956
|
-
#acceptPacket(packet: Uint8Array): boolean {
|
|
999
|
+
#acceptPacket(packet: Uint8Array, sourceAddress?: string): boolean {
|
|
1000
|
+
// Absent means unknown (a pre-1.3.0 provider, a tuner): never a new sender.
|
|
1001
|
+
const owner = this.#lockedTo?.() ?? this.#sender
|
|
1002
|
+
if (sourceAddress !== undefined && owner !== undefined && sourceAddress !== owner) {
|
|
1003
|
+
this.#stats.packetsDroppedForeignSender++
|
|
1004
|
+
return false
|
|
1005
|
+
}
|
|
957
1006
|
// This early read is intentional: rejected PIDs must not affect page
|
|
958
1007
|
// delivery, receive statistics, or the failback liveness watchdog. It
|
|
959
1008
|
// refuses exactly what parseMMTPHeader refuses but builds no header
|
|
@@ -963,7 +1012,7 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
963
1012
|
// Without an allow-list there is no filter to enforce, so an unparseable
|
|
964
1013
|
// packet keeps flowing to the router as before.
|
|
965
1014
|
if (!this.#packetIds) {
|
|
966
|
-
this.#countAccepted(packet, null)
|
|
1015
|
+
this.#countAccepted(packet, null, sourceAddress)
|
|
967
1016
|
return true
|
|
968
1017
|
}
|
|
969
1018
|
this.#stats.packetsDroppedUnparseable++
|
|
@@ -977,7 +1026,7 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
977
1026
|
return false
|
|
978
1027
|
}
|
|
979
1028
|
if (!this.#packetIds || this.#packetIds.has(packetId)) {
|
|
980
|
-
this.#countAccepted(packet, packetId)
|
|
1029
|
+
this.#countAccepted(packet, packetId, sourceAddress)
|
|
981
1030
|
return true
|
|
982
1031
|
}
|
|
983
1032
|
this.#recordUnregisteredPidDrop(packetId)
|
|
@@ -985,7 +1034,8 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
985
1034
|
}
|
|
986
1035
|
|
|
987
1036
|
/** Receive counters for one accepted packet; `packetId` is null when it has none. */
|
|
988
|
-
#countAccepted(packet: Uint8Array, packetId: number | null): void {
|
|
1037
|
+
#countAccepted(packet: Uint8Array, packetId: number | null, sourceAddress: string | undefined): void {
|
|
1038
|
+
this.#sender ??= sourceAddress
|
|
989
1039
|
this.#stats.packetsReceived++
|
|
990
1040
|
this.#stats.bytesReceived += packet.byteLength
|
|
991
1041
|
if (packetId === null) return
|
|
@@ -1067,6 +1117,9 @@ export class MulticastTransport<TPacketContext = never> {
|
|
|
1067
1117
|
console.warn(
|
|
1068
1118
|
`[MulticastTransport] No packets for ${this.#failbackWindowMs}ms; failing back to MoQ`,
|
|
1069
1119
|
)
|
|
1120
|
+
// The sub-flow's owner went silent: a sender that resumes after this,
|
|
1121
|
+
// the same one or a replacement publisher, owns the sub-flow anew.
|
|
1122
|
+
this.#sender = undefined
|
|
1070
1123
|
this.#onFailback({ kind: 'silence', windowMs: this.#failbackWindowMs })
|
|
1071
1124
|
}, delayMs)
|
|
1072
1125
|
}
|