@blockcast/mmt-container 0.5.0 → 1.0.0-main.215b8665a0eb

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 CHANGED
@@ -270,7 +270,7 @@ per-commit `0.1.0-main.<sha>` prerelease from `stamp-prerelease.mjs`), and a
270
270
  > Superseded by BLO-40665. Every package is now bumped in lockstep by a `v*`
271
271
  > tag and the sibling ranges move with it, so the objection above no longer
272
272
  > applies -- it was an argument against bumping *one* package. The set is at
273
- > `0.5.0`; see `RELEASES.md`.
273
+ > `1.0.0`; see `RELEASES.md`.
274
274
 
275
275
  #### Track-role convention (Phase 14)
276
276
 
@@ -7090,6 +7090,299 @@ function validateSwitchingSetTiming(tracks) {
7090
7090
  return { ok: false, expectedTicks, mismatches };
7091
7091
  }
7092
7092
 
7093
+ // src/fec-catalog.ts
7094
+ var DEFAULT_SCHEMES = ["reed-solomon", "raptorq"];
7095
+ var DEFAULT_PROFILES = ["moq-object-v1", "iso-ssbg1-v1"];
7096
+ var RS_MAX_BLOCK = 255;
7097
+ var RAPTORQ_MAX_K = 56403;
7098
+ var MAX_24 = 16777215;
7099
+ var ISO_MIN_T = 14;
7100
+ var GEOMETRY_KEYS = [
7101
+ "scheme",
7102
+ "blockProfile",
7103
+ "mmtpMode",
7104
+ "timescale",
7105
+ "symbolSize",
7106
+ "interleaveDepthMs",
7107
+ "reorderToleranceMs",
7108
+ "sourceSymbols",
7109
+ "fec"
7110
+ ];
7111
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
7112
+ var has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
7113
+ var isInt = (v) => typeof v === "number" && Number.isInteger(v);
7114
+ var intIn = (v, lo, hi = Number.MAX_SAFE_INTEGER) => isInt(v) && v >= lo && v <= hi;
7115
+ var profileAdmits = (profile, packaging) => profile !== "iso-ssbg1-v1" || packaging === "mmtp";
7116
+ function validateFecCatalog(catalog, receiver = {}) {
7117
+ const schemes = receiver.schemes ?? DEFAULT_SCHEMES;
7118
+ const profiles = receiver.blockProfiles ?? DEFAULT_PROFILES;
7119
+ const overlays = receiver.overlays ?? true;
7120
+ const all = [];
7121
+ const report = (code, path, inst) => {
7122
+ const issue = { code, path };
7123
+ all.push(issue);
7124
+ inst?.issues.push(issue);
7125
+ };
7126
+ const tracks = isObj(catalog) && Array.isArray(catalog.tracks) ? catalog.tracks : [];
7127
+ if (!isObj(catalog) || !Array.isArray(catalog.tracks)) report("fec_member", "/tracks");
7128
+ let rejectAll = false;
7129
+ tracks.forEach((t, ti) => {
7130
+ if (!isObj(t)) return;
7131
+ if (has(t, "enhancementRepair")) {
7132
+ rejectAll = true;
7133
+ report("fec_enhancement_repair", `/tracks/${ti}/enhancementRepair`);
7134
+ }
7135
+ if (Array.isArray(t.fec))
7136
+ t.fec.forEach((o, ii) => {
7137
+ if (isObj(o) && has(o, "enhancementRepair")) {
7138
+ rejectAll = true;
7139
+ report("fec_enhancement_repair", `/tracks/${ti}/fec/${ii}/enhancementRepair`);
7140
+ }
7141
+ });
7142
+ });
7143
+ const key = (ns, name) => JSON.stringify([ns ?? null, name]);
7144
+ const byName = /* @__PURE__ */ new Map();
7145
+ tracks.forEach((t, ti) => {
7146
+ if (isObj(t) && typeof t.name === "string") {
7147
+ const k = key(t.namespace, t.name);
7148
+ if (!byName.has(k)) byName.set(k, ti);
7149
+ }
7150
+ });
7151
+ const trackAt = (ti) => tracks[ti];
7152
+ const insts = [];
7153
+ tracks.forEach((t, ti) => {
7154
+ if (!isObj(t) || !has(t, "fec") || t.packaging === "fec-repair" || t.packaging === "fec-source") return;
7155
+ if (!Array.isArray(t.fec)) {
7156
+ report("fec_member", `/tracks/${ti}/fec`);
7157
+ return;
7158
+ }
7159
+ t.fec.forEach((o, ii) => {
7160
+ const path = `/tracks/${ti}/fec/${ii}`;
7161
+ const inst = {
7162
+ path,
7163
+ ti,
7164
+ track: t,
7165
+ obj: isObj(o) ? o : null,
7166
+ id: null,
7167
+ unavailable: false,
7168
+ overlay: false,
7169
+ issues: [],
7170
+ layer0: null,
7171
+ symbolNamed: null,
7172
+ attached: []
7173
+ };
7174
+ insts.push(inst);
7175
+ if (!inst.obj) return report("fec_member", path, inst);
7176
+ const o2 = inst.obj;
7177
+ if (!has(o2, "instanceId")) inst.id = 0;
7178
+ else if (intIn(o2.instanceId, 0, 255)) inst.id = o2.instanceId;
7179
+ else report("fec_member", `${path}/instanceId`, inst);
7180
+ inst.overlay = has(o2, "scope");
7181
+ });
7182
+ });
7183
+ const declaresFec = insts.length > 0;
7184
+ for (const inst of insts) {
7185
+ const o = inst.obj;
7186
+ if (!o) continue;
7187
+ const avail = (field, impl) => {
7188
+ const p = `${inst.path}/${field}`;
7189
+ const absent = field === "scheme" ? "fec_scheme_absent" : "fec_block_profile_absent";
7190
+ const unimpl = field === "scheme" ? "fec_scheme_unimplemented" : "fec_block_profile_unimplemented";
7191
+ if (!has(o, field)) return report(absent, p, inst), false;
7192
+ if (typeof o[field] !== "string") return report("fec_member", p, inst), true;
7193
+ if (!impl.includes(o[field])) return report(unimpl, p, inst), false;
7194
+ return true;
7195
+ };
7196
+ if (!avail("scheme", schemes) || !avail("blockProfile", profiles)) inst.unavailable = true;
7197
+ else if (typeof o.blockProfile === "string" && !profileAdmits(o.blockProfile, inst.track.packaging)) {
7198
+ report("fec_block_profile_packaging", `${inst.path}/blockProfile`, inst);
7199
+ inst.unavailable = true;
7200
+ }
7201
+ }
7202
+ const live = (i) => !i.unavailable;
7203
+ for (const inst of insts.filter(live)) {
7204
+ const o = inst.obj;
7205
+ if (!o) continue;
7206
+ const p = inst.path;
7207
+ for (const m of ["sourceSymbols", "repairSymbols", "symbolSize"])
7208
+ if (!intIn(o[m], 1)) report("fec_member", `${p}/${m}`, inst);
7209
+ if (typeof o.repairTrack !== "string") report("fec_member", `${p}/repairTrack`, inst);
7210
+ if (has(o, "symbolTrack") && typeof o.symbolTrack !== "string") report("fec_member", `${p}/symbolTrack`, inst);
7211
+ if (has(o, "interleaveDepthMs")) {
7212
+ const d = o.interleaveDepthMs;
7213
+ if (!intIn(d, 0) || d === 0 && o.blockProfile !== "iso-ssbg1-v1")
7214
+ report("fec_member", `${p}/interleaveDepthMs`, inst);
7215
+ }
7216
+ if (has(o, "reorderToleranceMs")) {
7217
+ const r = o.reorderToleranceMs;
7218
+ if (typeof r !== "number" || !Number.isFinite(r) || r < 0) report("fec_member", `${p}/reorderToleranceMs`, inst);
7219
+ }
7220
+ if (has(o, "label") && typeof o.label !== "string") report("fec_member", `${p}/label`, inst);
7221
+ if (inst.overlay && o.scope !== "keyframe") report("fec_member", `${p}/scope`, inst);
7222
+ }
7223
+ const idGroups = /* @__PURE__ */ new Map();
7224
+ for (const inst of insts)
7225
+ if (inst.id !== null) {
7226
+ const k = `${inst.ti}:${inst.id}`;
7227
+ idGroups.set(k, [...idGroups.get(k) ?? [], inst]);
7228
+ }
7229
+ for (const group of idGroups.values())
7230
+ if (group.length > 1)
7231
+ for (const inst of group.filter(live))
7232
+ report("fec_instance_id", inst.obj && has(inst.obj, "instanceId") ? `${inst.path}/instanceId` : inst.path, inst);
7233
+ const nameUses = /* @__PURE__ */ new Map();
7234
+ for (const inst of insts)
7235
+ for (const field of ["repairTrack", "symbolTrack"])
7236
+ if (inst.obj && typeof inst.obj[field] === "string") {
7237
+ const k = key(inst.track.namespace, inst.obj[field]);
7238
+ nameUses.set(k, [...nameUses.get(k) ?? [], { inst, field }]);
7239
+ }
7240
+ const namedBy = /* @__PURE__ */ new Map();
7241
+ for (const [k, uses] of nameUses) {
7242
+ const tj = byName.get(k);
7243
+ for (const { inst, field } of uses) {
7244
+ const path = `${inst.path}/${field}`;
7245
+ if (tj === void 0 || trackAt(tj).packaging !== (field === "repairTrack" ? "fec-repair" : "fec-source")) {
7246
+ if (live(inst)) report("fec_naming", path, inst);
7247
+ } else if (uses.length > 1) {
7248
+ if (live(inst)) report("fec_naming", path, inst);
7249
+ if (!namedBy.has(tj)) namedBy.set(tj, inst);
7250
+ } else {
7251
+ namedBy.set(tj, inst);
7252
+ if (field === "repairTrack") inst.layer0 = tj;
7253
+ else inst.symbolNamed = tj;
7254
+ }
7255
+ }
7256
+ }
7257
+ const owner = /* @__PURE__ */ new Map();
7258
+ if (declaresFec)
7259
+ tracks.forEach((t, tj) => {
7260
+ if (!isObj(t) || t.packaging !== "fec-repair" && t.packaging !== "fec-source") return;
7261
+ const tp = `/tracks/${tj}`;
7262
+ const named = namedBy.get(tj);
7263
+ if (named) {
7264
+ owner.set(tj, named);
7265
+ if (!live(named)) return;
7266
+ if (has(t, "source") && t.source !== named.track.name) report("fec_resolution", `${tp}/source`, named);
7267
+ if (has(t, "instanceId") && t.instanceId !== named.id) report("fec_resolution", `${tp}/instanceId`, named);
7268
+ return;
7269
+ }
7270
+ if (!has(t, "source")) return report("fec_orphan", tp);
7271
+ const id = has(t, "instanceId") ? t.instanceId : 0;
7272
+ const src = typeof t.source === "string" ? byName.get(key(t.namespace, t.source)) : void 0;
7273
+ const target = src === void 0 ? void 0 : insts.find((i) => i.ti === src && i.id === id);
7274
+ if (!target) return report("fec_naming", has(t, "instanceId") && src !== void 0 ? `${tp}/instanceId` : `${tp}/source`);
7275
+ owner.set(tj, target);
7276
+ target.attached.push(tj);
7277
+ });
7278
+ for (const [tj, inst] of owner) {
7279
+ if (!live(inst)) continue;
7280
+ const t = trackAt(tj);
7281
+ const tp = `/tracks/${tj}`;
7282
+ const symbol = t.packaging === "fec-source";
7283
+ if (has(t, "source") && typeof t.source !== "string") report("fec_member", `${tp}/source`, inst);
7284
+ if (has(t, "instanceId") && !intIn(t.instanceId, 0, 255)) report("fec_member", `${tp}/instanceId`, inst);
7285
+ for (const m of ["repairLayer", "repairSymbols"]) {
7286
+ if (!has(t, m)) continue;
7287
+ if (symbol || !intIn(t[m], m === "repairLayer" ? 0 : 1)) report("fec_member", `${tp}/${m}`, inst);
7288
+ }
7289
+ if (has(t, "priority") && !intIn(t.priority, symbol ? 0 : 192, 255)) report("fec_member", `${tp}/priority`, inst);
7290
+ for (const k of Object.keys(t))
7291
+ if (GEOMETRY_KEYS.includes(k) || k.startsWith("groupDuration")) report("fec_track_geometry", `${tp}/${k}`, inst);
7292
+ }
7293
+ for (const inst of insts.filter(live)) {
7294
+ const o = inst.obj;
7295
+ if (!o) continue;
7296
+ const repairs = inst.attached.filter((tj) => trackAt(tj).packaging === "fec-repair");
7297
+ const symbols = inst.attached.filter((tj) => trackAt(tj).packaging === "fec-source");
7298
+ if (symbols.length + (inst.symbolNamed !== null ? 1 : 0) > 1)
7299
+ for (const tj of symbols) report("fec_symbol_tracks", `/tracks/${tj}`, inst);
7300
+ const l0 = inst.layer0 === null ? null : trackAt(inst.layer0);
7301
+ const l0p = `/tracks/${inst.layer0}`;
7302
+ if (inst.overlay) {
7303
+ if (l0 && has(l0, "repairLayer")) report("fec_overlay", `${l0p}/repairLayer`, inst);
7304
+ for (const tj of repairs)
7305
+ report("fec_overlay", has(trackAt(tj), "repairLayer") ? `/tracks/${tj}/repairLayer` : `/tracks/${tj}`, inst);
7306
+ } else {
7307
+ if (l0 && has(l0, "repairLayer") && l0.repairLayer !== 0) report("fec_layers", `${l0p}/repairLayer`, inst);
7308
+ if (l0 && has(l0, "repairSymbols") && intIn(o.repairSymbols, 1) && l0.repairSymbols !== o.repairSymbols)
7309
+ report("fec_layer0", `${l0p}/repairSymbols`, inst);
7310
+ const layers = [];
7311
+ for (const tj of repairs) {
7312
+ const t = trackAt(tj);
7313
+ if (!has(t, "repairLayer")) report("fec_member", `/tracks/${tj}/repairLayer`, inst);
7314
+ else if (t.repairLayer === 0) report("fec_layers", `/tracks/${tj}/repairLayer`, inst);
7315
+ else if (intIn(t.repairLayer, 1)) layers.push({ tj, layer: t.repairLayer });
7316
+ if (!has(t, "repairSymbols")) report("fec_member", `/tracks/${tj}/repairSymbols`, inst);
7317
+ }
7318
+ const counts = /* @__PURE__ */ new Map();
7319
+ for (const { layer } of layers) counts.set(layer, (counts.get(layer) ?? 0) + 1);
7320
+ let missing = 1;
7321
+ while (counts.has(missing)) missing++;
7322
+ for (const { tj, layer } of layers)
7323
+ if (counts.get(layer) > 1 || layer > missing) report("fec_layers", `/tracks/${tj}/repairLayer`, inst);
7324
+ const chain = [
7325
+ { layer: 0, tj: inst.layer0, pri: l0 && intIn(l0.priority, 192, 255) ? l0.priority : l0 && has(l0, "priority") ? null : 240 },
7326
+ ...layers.map(({ tj, layer }) => {
7327
+ const pr = trackAt(tj).priority;
7328
+ return { layer, tj, pri: intIn(pr, 192, 255) ? pr : null };
7329
+ })
7330
+ ].sort((a, b) => a.layer - b.layer);
7331
+ let prev = null;
7332
+ for (const c of chain) {
7333
+ if (c.pri === null) continue;
7334
+ if (prev !== null && c.pri < prev) report("fec_priority", `/tracks/${c.tj}/priority`, inst);
7335
+ prev = Math.max(prev ?? c.pri, c.pri);
7336
+ }
7337
+ }
7338
+ const K = o.sourceSymbols;
7339
+ const T = o.symbolSize;
7340
+ const P0 = o.repairSymbols;
7341
+ if (intIn(K, 1) && intIn(P0, 1)) {
7342
+ let P = P0;
7343
+ if (!inst.overlay)
7344
+ for (const tj of repairs) {
7345
+ const t = trackAt(tj);
7346
+ if (intIn(t.repairLayer, 1) && intIn(t.repairSymbols, 1)) P += t.repairSymbols;
7347
+ }
7348
+ if (P > MAX_24 || K + P - 1 > MAX_24) report("fec_bounds", inst.path, inst);
7349
+ else if (o.scheme === "reed-solomon" && K + P > RS_MAX_BLOCK) report("fec_bounds", inst.path, inst);
7350
+ }
7351
+ if (o.scheme === "raptorq" && intIn(K, 1) && K > RAPTORQ_MAX_K) report("fec_bounds", `${inst.path}/sourceSymbols`, inst);
7352
+ if (intIn(T, 1)) {
7353
+ if (o.scheme === "raptorq" && T % 8 !== 0) report("fec_bounds", `${inst.path}/symbolSize`, inst);
7354
+ else if (o.blockProfile === "iso-ssbg1-v1" && T < ISO_MIN_T) report("fec_bounds", `${inst.path}/symbolSize`, inst);
7355
+ }
7356
+ }
7357
+ const overlayTracks = new Set(insts.filter((i) => live(i) && i.overlay).map((i) => i.ti));
7358
+ for (const ti of overlayTracks) {
7359
+ const ofTrack = insts.filter((i) => i.ti === ti && live(i));
7360
+ const ovs = ofTrack.filter((i) => i.overlay);
7361
+ if (!overlays) {
7362
+ for (const ov of ovs) report("fec_overlay_unsupported", `${ov.path}/scope`, ov);
7363
+ for (const i of ofTrack) i.issues.length === 0 && i.issues.push({ code: "fec_overlay_unsupported", path: `${ovs[0].path}/scope` });
7364
+ continue;
7365
+ }
7366
+ if (ovs.length > 1) for (const ov of ovs) report("fec_overlay", `${ov.path}/scope`, ov);
7367
+ for (const ov of ovs)
7368
+ if (ov.obj?.blockProfile !== "iso-ssbg1-v1") report("fec_overlay", `${ov.path}/blockProfile`, ov);
7369
+ const cause = ovs.flatMap((ov) => ov.issues).find((x) => x.code === "fec_overlay");
7370
+ if (cause) {
7371
+ for (const ov of ovs) if (ov.issues.length === 0) ov.issues.push(cause);
7372
+ }
7373
+ }
7374
+ return {
7375
+ instances: insts.map((i) => ({
7376
+ path: i.path,
7377
+ track: typeof i.track.name === "string" ? i.track.name : null,
7378
+ instanceId: i.id,
7379
+ outcome: rejectAll || !i.unavailable && i.issues.length > 0 ? "violation" : i.unavailable ? "unavailable" : "ok",
7380
+ issues: i.issues
7381
+ })),
7382
+ issues: all
7383
+ };
7384
+ }
7385
+
7093
7386
  // node_modules/@blockcast/mmt-transport/dist/multicast-tier.js
7094
7387
  function endpointKey(endpoint) {
7095
7388
  return `${endpoint.sourceAddress ?? ""}|${endpoint.groupAddress}|${endpoint.port}`;
@@ -19622,8 +19915,6 @@ var MmtpRouter = class {
19622
19915
  // Callbacks (legacy — used when per-track callbacks are not registered)
19623
19916
  #onVideoFrame = null;
19624
19917
  #onAudioFrame = null;
19625
- // Init segment (ftyp+moov) from MMTP MPU metadata — needed for MSE SourceBuffer
19626
- #onInitSegment = null;
19627
19918
  // Per-track init segment callbacks keyed by packetId
19628
19919
  #trackInitCallbacks = /* @__PURE__ */ new Map();
19629
19920
  // Track whether init has been delivered per physical stream — prevent duplicates
@@ -19886,11 +20177,10 @@ var MmtpRouter = class {
19886
20177
  };
19887
20178
  }
19888
20179
  /**
19889
- * Init segments delivered for a packetId with no `onTrackInit` handler
19890
- * since construction, whether the deprecated aggregate `onInitSegment`
19891
- * callback then received them or nothing did. A supported deployment reads
19892
- * 0 here; a non-zero value is the evidence that blocks removing the
19893
- * aggregate callback (MOQT-01, BLO-21181).
20180
+ * Init segments rejected since construction because their packetId had no
20181
+ * `onTrackInit` handler. A rejected init is delivered nowhere; there is no
20182
+ * aggregate fallback (removed in 1.0.0, MOQT-01 / BLO-21181). A supported
20183
+ * deployment reads 0 here.
19894
20184
  */
19895
20185
  get unmappedInitDeliveryCount() {
19896
20186
  return this.#unmappedInitDeliveries;
@@ -20067,20 +20357,10 @@ var MmtpRouter = class {
20067
20357
  onTrackPacket(packetId, cb) {
20068
20358
  this.#trackPacketCallbacks.set(packetId, cb);
20069
20359
  }
20070
- /**
20071
- * Register an init segment callback (ftyp+moov from MMTP MPU metadata).
20072
- * Receives an init only for a packetId with no `onTrackInit` handler.
20073
- *
20074
- * @deprecated Use onTrackInit(packetId, cb). Still delivered as a fallback;
20075
- * removed in the 1.0.0 major, no earlier than one minor release after this
20076
- * deprecation reaches the `latest` dist-tag (BLO-21181). Count fallback
20077
- * deliveries with `unmappedInitDeliveryCount`.
20078
- */
20079
- onInitSegment(cb) {
20080
- this.#onInitSegment = cb;
20081
- }
20082
20360
  /** Register an init segment callback for a specific MMTP packetId.
20083
- * Called when FT=0 init arrives via direct source or FEC recovery. */
20361
+ * Called when FT=0 init arrives via direct source or FEC recovery. An init
20362
+ * for a packetId with no handler is rejected and counted by
20363
+ * `unmappedInitDeliveryCount`. */
20084
20364
  onTrackInit(packetId, cb) {
20085
20365
  this.#trackInitCallbacks.set(packetId, cb);
20086
20366
  }
@@ -21480,16 +21760,15 @@ var MmtpRouter = class {
21480
21760
  if (this.#trackInitDelivered.has(streamKey)) return;
21481
21761
  if (data.byteLength <= 8) return;
21482
21762
  this.#trackInitDelivered.add(streamKey);
21763
+ const track = `packetId=${packetId}${catalogTrackName === void 0 ? "" : ` track=${catalogTrackName}`}`;
21483
21764
  const cb = this.#trackInitCallbacks.get(packetId);
21484
- if (cb) {
21485
- cb(data);
21486
- } else {
21765
+ if (!cb) {
21487
21766
  this.#unmappedInitDeliveries++;
21488
- this.#onInitSegment?.(data);
21767
+ console.log(`[MmtpRouter] Init rejected for ${track}: no onTrackInit handler`);
21768
+ return;
21489
21769
  }
21490
- console.log(
21491
- `[MmtpRouter] Init delivered for packetId=${packetId}${catalogTrackName === void 0 ? "" : ` track=${catalogTrackName}`} (${data.byteLength}B)`
21492
- );
21770
+ cb(data);
21771
+ console.log(`[MmtpRouter] Init delivered for ${track} (${data.byteLength}B)`);
21493
21772
  }
21494
21773
  /**
21495
21774
  * Core MMTP packet processing — source packets → play + FEC, repair → FEC only.
@@ -22989,6 +23268,10 @@ function getMulticastGlobals() {
22989
23268
  function hasMulticastGateway() {
22990
23269
  return typeof getMulticastGlobals().multicast?.subscribe === "function";
22991
23270
  }
23271
+ function senderOf(event) {
23272
+ const sourceAddress = event.sourceAddress;
23273
+ return typeof sourceAddress === "string" ? sourceAddress : void 0;
23274
+ }
22992
23275
  function normalizeBytesEventDetail(detail) {
22993
23276
  const value = detail && typeof detail === "object" && "data" in detail ? detail.data : detail;
22994
23277
  if (value instanceof Uint8Array) return value;
@@ -23035,7 +23318,8 @@ var MulticastTransport = class {
23035
23318
  bytesReceived: 0,
23036
23319
  packetsDroppedUnregisteredPid: 0,
23037
23320
  packetsDroppedUnparseable: 0,
23038
- packetsDroppedSignaling: 0
23321
+ packetsDroppedSignaling: 0,
23322
+ packetsDroppedForeignSender: 0
23039
23323
  };
23040
23324
  // join() calls by how they resolved; see joinStats.
23041
23325
  #joinsSucceeded = 0;
@@ -23060,6 +23344,11 @@ var MulticastTransport = class {
23060
23344
  // per packet once a count outgrows the small-integer range.
23061
23345
  #acceptedBytesByPacketId = /* @__PURE__ */ new Map();
23062
23346
  #packetIds = null;
23347
+ // The sender S this join's accepted packets came from, once known; see
23348
+ // packetsDroppedForeignSender. Cleared by every join and by a silence
23349
+ // failback, so a replacement publisher is not locked out.
23350
+ #sender = void 0;
23351
+ #lockedTo;
23063
23352
  // What the live join delivers with each accepted packet. A field, not a
23064
23353
  // listener closure, so an in-place admission change (setContext) applies
23065
23354
  // from the next packet on without rejoining.
@@ -23081,9 +23370,15 @@ var MulticastTransport = class {
23081
23370
  #joinGeneration = 0;
23082
23371
  // The provider to subscribe through; window.multicast when unset.
23083
23372
  #gateway;
23373
+ /**
23374
+ * `lockedTo` reads the sender another transport is locked to (a repair
23375
+ * group follows its media join, whose publisher sends both). While it reads
23376
+ * a sender, this join admits only that one; until then its own lock applies.
23377
+ */
23084
23378
  constructor(onPacket, options = {}) {
23085
23379
  this.#onPacket = onPacket;
23086
23380
  this.#gateway = options.gateway;
23381
+ this.#lockedTo = options.lockedTo;
23087
23382
  }
23088
23383
  static isAvailable() {
23089
23384
  return hasMulticastGateway();
@@ -23092,6 +23387,7 @@ var MulticastTransport = class {
23092
23387
  const generation = ++this.#joinGeneration;
23093
23388
  this.#releaseSubscription();
23094
23389
  this.#dataPlaneLive = false;
23390
+ this.#sender = void 0;
23095
23391
  if (this.#disposed) return this.#declined(generation, "disposed");
23096
23392
  const gateway = this.#gateway ?? getMulticastGlobals().multicast;
23097
23393
  if (typeof gateway?.subscribe !== "function") {
@@ -23133,7 +23429,8 @@ var MulticastTransport = class {
23133
23429
  }
23134
23430
  this.#subscription = subscription;
23135
23431
  const live = () => !this.#disposed && generation === this.#joinGeneration;
23136
- const packetListener = subscriptionDeliversPacketBatches(subscription) ? {
23432
+ const anySource = !config.sourceAddress && !config.senderAddress && !config.atsc3;
23433
+ const packetListener = !anySource && subscriptionDeliversPacketBatches(subscription) ? {
23137
23434
  type: "packets",
23138
23435
  listener: (event) => {
23139
23436
  if (!live()) return;
@@ -23143,7 +23440,7 @@ var MulticastTransport = class {
23143
23440
  type: "bytes",
23144
23441
  listener: (event) => {
23145
23442
  if (!live()) return;
23146
- if (this.#deliver(event.detail)) this.#resetWatchdog();
23443
+ if (this.#deliver(event.detail, senderOf(event))) this.#resetWatchdog();
23147
23444
  }
23148
23445
  };
23149
23446
  this.#packetListener = packetListener;
@@ -23234,13 +23531,13 @@ var MulticastTransport = class {
23234
23531
  * One packet from the subscription: filter, count, and hand it to the
23235
23532
  * callback. Returns whether it was delivered; the caller notes liveness.
23236
23533
  */
23237
- #deliver(detail) {
23534
+ #deliver(detail, sourceAddress) {
23238
23535
  const packet = normalizeBytesEventDetail(detail);
23239
23536
  if (!packet) {
23240
23537
  this.#stats.packetsDroppedUnparseable++;
23241
23538
  return false;
23242
23539
  }
23243
- if (!this.#acceptPacket(packet)) return false;
23540
+ if (!this.#acceptPacket(packet, sourceAddress)) return false;
23244
23541
  this.#onPacket(packet, this.#context);
23245
23542
  return true;
23246
23543
  }
@@ -23281,11 +23578,15 @@ var MulticastTransport = class {
23281
23578
  throw failures[0];
23282
23579
  }
23283
23580
  }
23284
- /** Feed a raw packet from an external source (e.g., AMT tunnel or ASM subscription). */
23285
- feedPacket(data, context) {
23581
+ /**
23582
+ * Feed a raw packet from an external source (e.g., AMT tunnel or ASM
23583
+ * subscription). `sourceAddress` is the datagram's sender when the source
23584
+ * knows it; see packetsDroppedForeignSender.
23585
+ */
23586
+ feedPacket(data, context, sourceAddress) {
23286
23587
  if (this.#disposed) return;
23287
23588
  const packet = data instanceof Uint8Array ? data : new Uint8Array(data);
23288
- if (!this.#acceptPacket(packet)) return;
23589
+ if (!this.#acceptPacket(packet, sourceAddress)) return;
23289
23590
  this.#onPacket(packet, context);
23290
23591
  this.#resetWatchdog();
23291
23592
  }
@@ -23435,6 +23736,10 @@ var MulticastTransport = class {
23435
23736
  get withdrawnPacketIdDrops() {
23436
23737
  return this.#withdrawnPacketIdDrops;
23437
23738
  }
23739
+ /** The sender this join is locked to; undefined until one is known. */
23740
+ get sender() {
23741
+ return this.#sender;
23742
+ }
23438
23743
  /** Whether a subscription is live: joined, and not yet released or replaced. */
23439
23744
  get joined() {
23440
23745
  return this.#subscription !== null;
@@ -23597,11 +23902,16 @@ var MulticastTransport = class {
23597
23902
  this.#withdrawnPacketIds.clear();
23598
23903
  }
23599
23904
  /** The ingress filter; an accepted packet is counted here, once. */
23600
- #acceptPacket(packet) {
23905
+ #acceptPacket(packet, sourceAddress) {
23906
+ const owner = this.#lockedTo?.() ?? this.#sender;
23907
+ if (sourceAddress !== void 0 && owner !== void 0 && sourceAddress !== owner) {
23908
+ this.#stats.packetsDroppedForeignSender++;
23909
+ return false;
23910
+ }
23601
23911
  const packetId = readMMTPPacketId(packet);
23602
23912
  if (packetId === null) {
23603
23913
  if (!this.#packetIds) {
23604
- this.#countAccepted(packet, null);
23914
+ this.#countAccepted(packet, null, sourceAddress);
23605
23915
  return true;
23606
23916
  }
23607
23917
  this.#stats.packetsDroppedUnparseable++;
@@ -23612,14 +23922,15 @@ var MulticastTransport = class {
23612
23922
  return false;
23613
23923
  }
23614
23924
  if (!this.#packetIds || this.#packetIds.has(packetId)) {
23615
- this.#countAccepted(packet, packetId);
23925
+ this.#countAccepted(packet, packetId, sourceAddress);
23616
23926
  return true;
23617
23927
  }
23618
23928
  this.#recordUnregisteredPidDrop(packetId);
23619
23929
  return false;
23620
23930
  }
23621
23931
  /** Receive counters for one accepted packet; `packetId` is null when it has none. */
23622
- #countAccepted(packet, packetId) {
23932
+ #countAccepted(packet, packetId, sourceAddress) {
23933
+ this.#sender ??= sourceAddress;
23623
23934
  this.#stats.packetsReceived++;
23624
23935
  this.#stats.bytesReceived += packet.byteLength;
23625
23936
  if (packetId === null) return;
@@ -23690,6 +24001,7 @@ var MulticastTransport = class {
23690
24001
  console.warn(
23691
24002
  `[MulticastTransport] No packets for ${this.#failbackWindowMs}ms; failing back to MoQ`
23692
24003
  );
24004
+ this.#sender = void 0;
23693
24005
  this.#onFailback({ kind: "silence", windowMs: this.#failbackWindowMs });
23694
24006
  }, delayMs);
23695
24007
  }
@@ -26440,7 +26752,7 @@ function catalogAmtNetworkSource(networkSource) {
26440
26752
  }
26441
26753
  return void 0;
26442
26754
  }
26443
- function intIn(value, min, max) {
26755
+ function intIn2(value, min, max) {
26444
26756
  return Number.isInteger(value) && value >= min && value <= max;
26445
26757
  }
26446
26758
  function catalogAtsc3NetworkSource(networkSource) {
@@ -26448,12 +26760,12 @@ function catalogAtsc3NetworkSource(networkSource) {
26448
26760
  for (const raw of entries) {
26449
26761
  if (!raw || typeof raw !== "object") continue;
26450
26762
  const ns = raw;
26451
- if (ns.type !== "atsc3" || !intIn(ns.frequency, 1, Number.MAX_SAFE_INTEGER)) continue;
26763
+ if (ns.type !== "atsc3" || !intIn2(ns.frequency, 1, Number.MAX_SAFE_INTEGER)) continue;
26452
26764
  const slsUri = typeof ns.slsUri === "string" ? ns.slsUri.trim() : "";
26453
26765
  return {
26454
26766
  frequency: ns.frequency,
26455
- ...intIn(ns.plpId, 0, 63) ? { plpId: ns.plpId } : {},
26456
- ...intIn(ns.serviceId, 0, 65535) ? { serviceId: ns.serviceId } : {},
26767
+ ...intIn2(ns.plpId, 0, 63) ? { plpId: ns.plpId } : {},
26768
+ ...intIn2(ns.serviceId, 0, 65535) ? { serviceId: ns.serviceId } : {},
26457
26769
  ...slsUri ? { slsUri } : {}
26458
26770
  };
26459
26771
  }
@@ -27704,10 +28016,6 @@ var MulticastManager = class {
27704
28016
  onTrackPacket(packetId, cb) {
27705
28017
  this.#router.onTrackPacket(packetId, cb);
27706
28018
  }
27707
- /** @deprecated Use onTrackInit(packetId, cb); see MmtpRouter.onInitSegment. */
27708
- onInitSegment(cb) {
27709
- this.#router.onInitSegment(cb);
27710
- }
27711
28019
  onTrackInit(packetId, cb) {
27712
28020
  this.#router.onTrackInit(packetId, cb);
27713
28021
  }
@@ -27931,15 +28239,21 @@ var MulticastManager = class {
27931
28239
  tracks: Object.freeze(endpoint.tracks.map(({ name, packetId }) => Object.freeze({ name, packetId }))),
27932
28240
  ...endpoint.flow === void 0 ? {} : { flow: endpoint.flow }
27933
28241
  };
27934
- const onPacket = (packet) => {
27935
- if (!released) this.#router.feedPacket(packet, "multicast", ingress);
28242
+ let sender;
28243
+ const onPacket = (packet, sourceAddress) => {
28244
+ if (released) return;
28245
+ if (sourceAddress !== void 0 && sourceAddress !== (this.#transport.sender ?? (sender ??= sourceAddress))) return;
28246
+ this.#router.feedPacket(packet, "multicast", ingress);
27936
28247
  };
27937
28248
  const fail = (error) => console.warn(`[MulticastManager] repair group ${endpoint.group}:${endpoint.port} not joined`, error);
27938
28249
  if (typeof provider === "function") {
27939
28250
  Promise.resolve().then(() => provider(endpoint, onPacket)).then((release2) => released ? safely(release2) : releases.push(release2), fail);
27940
28251
  continue;
27941
28252
  }
27942
- const transport = new MulticastTransport((packet) => onPacket(packet), { gateway: provider });
28253
+ const transport = new MulticastTransport((packet) => onPacket(packet), {
28254
+ gateway: provider,
28255
+ lockedTo: () => this.#transport.sender
28256
+ });
27943
28257
  releases.push(() => transport.dispose());
27944
28258
  void transport.join({
27945
28259
  sourceAddress: endpoint.source,
@@ -28218,8 +28532,8 @@ var MulticastManager = class {
28218
28532
  * transport attribution in FEC path counters.
28219
28533
  * The `router` accessor above is already public.
28220
28534
  */
28221
- feedPacket(data, ingress) {
28222
- this.#transport.feedPacket(data, ingress);
28535
+ feedPacket(data, ingress, sourceAddress) {
28536
+ this.#transport.feedPacket(data, ingress, sourceAddress);
28223
28537
  }
28224
28538
  /**
28225
28539
  * Declare how packets given to feedPacket() reach this manager. A consumer
@@ -31716,6 +32030,7 @@ export {
31716
32030
  splitH264SampleNalUnits,
31717
32031
  unwrapMulticastPacket,
31718
32032
  validateCmsfMmtpCatalog,
32033
+ validateFecCatalog,
31719
32034
  validateMulticastContainerContract,
31720
32035
  validateRepairTrackFields,
31721
32036
  validateSwitchingSetTiming,