@blockcast/mmt-container 2.1.0 → 2.2.0-main.10bec7edb00b
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/dist/browser-runtime.js +647 -83
- package/dist/browser-runtime.js.map +4 -4
- package/dist/carp-catalog.js.map +1 -1
- package/dist/catalog-fec-params.d.ts +16 -0
- package/dist/catalog-fec-params.d.ts.map +1 -1
- package/dist/catalog-fec-params.js +15 -0
- package/dist/catalog-fec-params.js.map +2 -2
- package/dist/catalog-network-source.js.map +1 -1
- package/dist/cmcd-producer.d.ts +5 -0
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +11 -6
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-state.js.map +1 -1
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/held-layer-admission.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.js.map +1 -1
- package/dist/mfu-reassembler.js +12 -19
- package/dist/mfu-reassembler.js.map +2 -2
- package/dist/mmtp-container-parser.js +12 -19
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +133 -34
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +12 -19
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-fec-session.d.ts +41 -3
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +118 -13
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +9 -1
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +1473 -33
- package/dist/mmtp-router.js.map +4 -4
- package/dist/multicast-auth-catalog.js.map +1 -1
- package/dist/multicast-input.js.map +1 -1
- package/dist/multicast-manager.d.ts +15 -2
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +15216 -14934
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +11 -3
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +266 -25
- package/dist/multicast-transport.js.map +4 -4
- package/dist/multicast-unwrap.js +12 -19
- package/dist/multicast-unwrap.js.map +2 -2
- package/dist/no-modules.js +130 -33
- package/dist/no-modules.js.map +3 -3
- package/dist/repair-fetch.js.map +1 -1
- package/dist/repair-layer-subscriptions.d.ts +82 -0
- package/dist/repair-layer-subscriptions.d.ts.map +1 -0
- package/dist/repair-layer-subscriptions.js +192 -0
- package/dist/repair-layer-subscriptions.js.map +7 -0
- package/dist/signaling.js.map +2 -2
- package/dist/timestamp-codec.js +12 -19
- package/dist/timestamp-codec.js.map +2 -2
- package/dist/wasm-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +12 -19
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +5 -5
- package/schemas/catalog.schema.json +4 -0
- package/src/catalog-fec-params.ts +35 -0
- package/src/cmcd-producer.ts +5 -0
- package/src/index.ts +12 -0
- package/src/mmtp-fec-session.ts +115 -15
- package/src/mmtp-router.ts +81 -16
- package/src/multicast-manager.ts +26 -5
- package/src/multicast-transport.ts +44 -20
- package/src/repair-layer-subscriptions.ts +261 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockcast/mmt-container",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0-main.10bec7edb00b",
|
|
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.2.0-main.10bec7edb00b",
|
|
173
|
+
"@blockcast/mmt-base": "2.2.0-main.10bec7edb00b",
|
|
174
|
+
"@blockcast/mmt-fec": "2.2.0-main.10bec7edb00b",
|
|
175
|
+
"@blockcast/mmt-manifest-verify": "2.2.0-main.10bec7edb00b",
|
|
176
176
|
"@noble/curves": "^1.9.7",
|
|
177
177
|
"@noble/hashes": "^1.8.0",
|
|
178
178
|
"ajv": "^8.17.1",
|
|
@@ -138,6 +138,10 @@
|
|
|
138
138
|
"$ref": "#/$defs/positiveInteger",
|
|
139
139
|
"description": "Keyframe interval in the track's timescale ticks (moqcast-draft §4.4.3). Exact where keyframeIntervalMs rounds (e.g. 30000/1001 video); when both are present, keyframeIntervalTicks wins."
|
|
140
140
|
},
|
|
141
|
+
"deliveryMode": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"description": "How the publisher sends this track's objects (draft-ramadan-moq-fec-catalog-00 §3.3, proposed in moqcast-draft#26): \"datagram\" or \"stream\". A receiver treats a missing or unrecognized value as \"datagram\", so any string is admitted here rather than rejected. Allowed on source, repair and symbol tracks."
|
|
144
|
+
},
|
|
141
145
|
"fec": { "$ref": "#/$defs/fec" }
|
|
142
146
|
},
|
|
143
147
|
"allOf": [
|
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
RepairOverlayParams,
|
|
31
31
|
} from "./repair-layers.js";
|
|
32
32
|
import type { RepairTarget } from "./repair-fetch.js";
|
|
33
|
+
import type { MoqRepairDelivery } from "./repair-layer-subscriptions.js";
|
|
33
34
|
|
|
34
35
|
export { catalogFecInterleaveMs, type FecInterleaveCatalog };
|
|
35
36
|
export { FEC_BLOCK_PROFILES, isFecBlockProfile, parseFecBlockProfile, type FecBlockProfile };
|
|
@@ -273,6 +274,9 @@ export type TrackFecParams = {
|
|
|
273
274
|
/** Keyframe overlay instances declared for this track. Present (possibly
|
|
274
275
|
* empty) whenever repairLayers is. */
|
|
275
276
|
repairOverlays?: readonly RepairOverlayParams[];
|
|
277
|
+
/** The publisher's MoQ delivery mode (`catalogTrackDeliveryMode`).
|
|
278
|
+
* Present whenever repairLayers is; `RepairLayerSubscriptions` reads it. */
|
|
279
|
+
deliveryMode?: MoqRepairDelivery;
|
|
276
280
|
fps?: number;
|
|
277
281
|
sampleRate?: number;
|
|
278
282
|
};
|
|
@@ -368,6 +372,36 @@ export function resolveRepairLayers(
|
|
|
368
372
|
};
|
|
369
373
|
}
|
|
370
374
|
|
|
375
|
+
/** The catalog value a JSON Pointer (RFC 6901) names, or undefined. */
|
|
376
|
+
function valueAt(catalog: unknown, pointer: string): unknown {
|
|
377
|
+
let node = catalog;
|
|
378
|
+
for (const segment of pointer.split("/").slice(1)) {
|
|
379
|
+
if (typeof node !== "object" || node === null) return undefined;
|
|
380
|
+
node = (node as Record<string, unknown>)[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
|
|
381
|
+
}
|
|
382
|
+
return node;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* The publisher's MoQ delivery mode for one source track: `deliveryMode` on
|
|
387
|
+
* its base track entry (the hang rendition or MSF `tracks[]` entry its FEC
|
|
388
|
+
* binds to). `stream` means QUIC retransmits, so the receiver subscribes no
|
|
389
|
+
* repair track. Absent or any other value is `datagram`, FFmpeg's
|
|
390
|
+
* `moq_delivery_mode` default: a receiver never silently drops repair on a
|
|
391
|
+
* lossy path. When a track is declared both as a hang rendition and as an
|
|
392
|
+
* MSF `tracks[]` entry, the rendition is authoritative: a `deliveryMode` on
|
|
393
|
+
* the shadowed `tracks[]` entry is not read (fails safe to `datagram`).
|
|
394
|
+
* Never throws.
|
|
395
|
+
*/
|
|
396
|
+
export function catalogTrackDeliveryMode(
|
|
397
|
+
catalog: RepairLayerCatalog | undefined,
|
|
398
|
+
source: string | CatalogTrackRef,
|
|
399
|
+
): MoqRepairDelivery {
|
|
400
|
+
const binding = bindingOf(catalog, source);
|
|
401
|
+
const entry = binding ? valueAt(catalog, binding.pointer) : undefined;
|
|
402
|
+
return (entry as { deliveryMode?: unknown } | undefined)?.deliveryMode === "stream" ? "stream" : "datagram";
|
|
403
|
+
}
|
|
404
|
+
|
|
371
405
|
/** A MoQ Transport priority (0..255, lower first), or null. */
|
|
372
406
|
function moqtPriority(value: unknown): number | null {
|
|
373
407
|
return Number.isSafeInteger(value) && (value as number) >= 0 && (value as number) <= 255 ? (value as number) : null;
|
|
@@ -444,6 +478,7 @@ function fecParams(
|
|
|
444
478
|
repairTrackName: base.repairTrack,
|
|
445
479
|
repairLayers: repair.baseLayers,
|
|
446
480
|
repairOverlays: repair.overlays,
|
|
481
|
+
deliveryMode: catalogTrackDeliveryMode(catalog, track!),
|
|
447
482
|
...mediaRate,
|
|
448
483
|
};
|
|
449
484
|
}
|
package/src/cmcd-producer.ts
CHANGED
|
@@ -28,6 +28,11 @@ export interface ManifestVerificationStats {
|
|
|
28
28
|
|
|
29
29
|
/** Transport state snapshot, built by the player wiring layer from SocketPool. */
|
|
30
30
|
export interface TransportSnapshot {
|
|
31
|
+
/**
|
|
32
|
+
* CMCD has no user-agent value: report a `'user-agent'` join
|
|
33
|
+
* (`ResolvedMulticastTransport`, the browser's own MulticastReceiver) as
|
|
34
|
+
* `'native'`, since the browser does not say which leg it chose.
|
|
35
|
+
*/
|
|
31
36
|
transport: 'native' | 'amt' | 'moq'
|
|
32
37
|
/** IPv4/IPv6 multicast group address */
|
|
33
38
|
multicastGroup?: string
|
package/src/index.ts
CHANGED
|
@@ -546,6 +546,7 @@ export {
|
|
|
546
546
|
type TrackFecParams,
|
|
547
547
|
type TrackFecUnavailable,
|
|
548
548
|
catalogTrackFecUnavailable,
|
|
549
|
+
catalogTrackDeliveryMode,
|
|
549
550
|
catalogGroupDurationMs,
|
|
550
551
|
catalogKeyframeCadenceMs,
|
|
551
552
|
catalogFecMediaUnitsPerSecond,
|
|
@@ -602,11 +603,22 @@ export {
|
|
|
602
603
|
type MmtpFecSessionConfig,
|
|
603
604
|
type MmtpFecSessionDecoder,
|
|
604
605
|
type MmtpFecSessionDecoderConstructor,
|
|
606
|
+
type MmtpFecSessionMessageConfig,
|
|
607
|
+
type MmtpFecSessionMessageUpdate,
|
|
605
608
|
type MmtpFecSessionMixedPathStats,
|
|
606
609
|
type MmtpFecSessionPath,
|
|
607
610
|
type MmtpFecSessionPathCounts,
|
|
608
611
|
type MmtpFecSessionStats,
|
|
609
612
|
} from "./mmtp-fec-session.js";
|
|
613
|
+
// MoQ repair-layer subscriptions by measured loss (BLO-35456)
|
|
614
|
+
export {
|
|
615
|
+
RepairLayerSubscriptions,
|
|
616
|
+
type MoqRepairDelivery,
|
|
617
|
+
type RepairLayerSource,
|
|
618
|
+
type RepairLayerSubscriptionsOptions,
|
|
619
|
+
type RepairLayerSubscriptionsSnapshot,
|
|
620
|
+
type RepairTrackSubscription,
|
|
621
|
+
} from "./repair-layer-subscriptions.js";
|
|
610
622
|
|
|
611
623
|
export {
|
|
612
624
|
catalogAmtNetworkSource,
|
package/src/mmtp-fec-session.ts
CHANGED
|
@@ -23,6 +23,15 @@
|
|
|
23
23
|
* uses. A packet below B_0 at or after the anchor fails the session closed:
|
|
24
24
|
* `stats.failClosed` reports why, and every later repair is refused.
|
|
25
25
|
*
|
|
26
|
+
* Catalog-less (draft-ramadan-moq-mmt-02 8.3, 13.2): a receiver holding
|
|
27
|
+
* layer 0 under the AL-FEC message alone builds the session from the message's
|
|
28
|
+
* fec_flow entry (`alFecFlow`) instead. K, T, the code point and the repair
|
|
29
|
+
* packet_id come from that entry, and layer 0 is held at
|
|
30
|
+
* maximum_p_for_repair_flow, standing for O_0 + P_0, by the anchored clause of
|
|
31
|
+
* draft-ramadan-moq-fec-core-00 7.3.2 alone. A later version that raises the
|
|
32
|
+
* field raises the held total (updateAlFecFlow()); the message carries no
|
|
33
|
+
* millisecond budget, so the caller states the retention (fec-core-00 9.4).
|
|
34
|
+
*
|
|
26
35
|
* Every repair packet that cannot be used is counted under the reason it was
|
|
27
36
|
* refused, and every block ends under exactly one outcome, so a stream whose
|
|
28
37
|
* FEC is doing nothing can be told apart from one with nothing to repair.
|
|
@@ -30,6 +39,8 @@
|
|
|
30
39
|
|
|
31
40
|
import {
|
|
32
41
|
admitMmtpRepairObject,
|
|
42
|
+
alFecRaptorQFlowConfig,
|
|
43
|
+
FEC_CODE_RAPTORQ,
|
|
33
44
|
isoBlockContains,
|
|
34
45
|
isoBlocksBehindLowerBound,
|
|
35
46
|
parseFecBlockProfile,
|
|
@@ -39,6 +50,8 @@ import {
|
|
|
39
50
|
serial32Precedes,
|
|
40
51
|
ssbg1FitsSymbol,
|
|
41
52
|
trimSsbg1SourceSymbol,
|
|
53
|
+
type AlFecFlow,
|
|
54
|
+
type AlFecRaptorQFlowConfig,
|
|
42
55
|
type FecBlockProfile,
|
|
43
56
|
} from '@blockcast/mmt-base'
|
|
44
57
|
import { deriveFecRetentionBlocks, fecBlockBudgetMs } from '@blockcast/mmt-fec'
|
|
@@ -105,6 +118,25 @@ export interface MmtpFecSessionConfig {
|
|
|
105
118
|
heldLayers?: readonly number[]
|
|
106
119
|
}
|
|
107
120
|
|
|
121
|
+
/**
|
|
122
|
+
* A catalog-less session: one fec_flow entry of the AL-FEC message
|
|
123
|
+
* (`parseAlFecMessage(...).flows[i]`), draft-ramadan-moq-mmt-02 8.3.
|
|
124
|
+
*/
|
|
125
|
+
export interface MmtpFecSessionMessageConfig {
|
|
126
|
+
/** The source track's entry; it must pass `alFecRaptorQFlowConfig`. */
|
|
127
|
+
alFecFlow: AlFecFlow
|
|
128
|
+
/**
|
|
129
|
+
* The decoder retention in blocks. The message carries no elapsed-time
|
|
130
|
+
* budget (mmt-02 7.5), so open-block state is bounded by local policy
|
|
131
|
+
* (fec-core-00 9.4); required, never defaulted.
|
|
132
|
+
*/
|
|
133
|
+
retentionBlocks: number
|
|
134
|
+
Decoder: MmtpFecSessionDecoderConstructor
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** What updateAlFecFlow() did with a later version of the message. */
|
|
138
|
+
export type MmtpFecSessionMessageUpdate = 'raised' | 'unchanged' | 'refused'
|
|
139
|
+
|
|
108
140
|
/**
|
|
109
141
|
* The path a symbol arrived on: its MoQT subscription, or the multicast
|
|
110
142
|
* group (native SSM or AMT) carrying the same asset. The caller states it;
|
|
@@ -197,12 +229,17 @@ export class MmtpFecSession {
|
|
|
197
229
|
readonly algorithm: 'raptorq' | 'reed-solomon'
|
|
198
230
|
readonly sourceSymbols: number
|
|
199
231
|
/** P_0 = B_0, the held total every repair packet is checked against. */
|
|
200
|
-
|
|
232
|
+
get repairSymbols(): number {
|
|
233
|
+
return this.#repairSymbols
|
|
234
|
+
}
|
|
201
235
|
readonly symbolSize: number
|
|
202
236
|
/** The decoder's retention, in blocks, derived from the catalog budget. */
|
|
203
237
|
readonly retentionBlocks: number
|
|
204
238
|
|
|
205
239
|
#decoder: MmtpFecSessionDecoder | null
|
|
240
|
+
#repairSymbols: number
|
|
241
|
+
/** The message entry a catalog-less session was built from; undefined under a catalog. */
|
|
242
|
+
#message: AlFecRaptorQFlowConfig | undefined
|
|
206
243
|
/** Blocks a repair has defined and that are not settled yet, by SS_Start. */
|
|
207
244
|
#blocks = new Map<number, BlockState>()
|
|
208
245
|
/** SS_Starts of settled blocks, so a later repair for one is refused. */
|
|
@@ -240,7 +277,26 @@ export class MmtpFecSession {
|
|
|
240
277
|
* the scheme, the block profile, K, P_0 (`repairSymbols`), T,
|
|
241
278
|
* `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.
|
|
242
279
|
*/
|
|
243
|
-
constructor(config: MmtpFecSessionConfig) {
|
|
280
|
+
constructor(config: MmtpFecSessionConfig | MmtpFecSessionMessageConfig) {
|
|
281
|
+
if (config && 'alFecFlow' in config) {
|
|
282
|
+
const message = alFecRaptorQFlowConfig(config.alFecFlow)
|
|
283
|
+
if (!message) {
|
|
284
|
+
throw new Error('MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)')
|
|
285
|
+
}
|
|
286
|
+
this.#message = message
|
|
287
|
+
// Coding byte 0x14 is the iso-ssbg1-v1 profile (mmt-02 8.3).
|
|
288
|
+
this.blockProfile = parseFecBlockProfile('iso-ssbg1-v1', 'alFecFlow')
|
|
289
|
+
this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? 'raptorq' : 'reed-solomon'
|
|
290
|
+
this.sourceSymbols = message.maximumK
|
|
291
|
+
this.#repairSymbols = message.maximumP
|
|
292
|
+
this.symbolSize = message.repairSymbolSize
|
|
293
|
+
this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, 'retentionBlocks')
|
|
294
|
+
this.#repairPacketId = message.repairFlowId
|
|
295
|
+
this.#heldLayers = layerZero(message.maximumP)
|
|
296
|
+
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols)
|
|
297
|
+
this.#decoder = buildDecoder(config.Decoder, this)
|
|
298
|
+
return
|
|
299
|
+
}
|
|
244
300
|
const fec = config?.fec
|
|
245
301
|
if (!fec || typeof fec !== 'object') {
|
|
246
302
|
throw new Error('MmtpFecSession: the catalog fec instance is required')
|
|
@@ -251,7 +307,7 @@ export class MmtpFecSession {
|
|
|
251
307
|
}
|
|
252
308
|
this.algorithm = fec.scheme
|
|
253
309
|
this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, 'fec.sourceSymbols')
|
|
254
|
-
this
|
|
310
|
+
this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, 'fec.repairSymbols')
|
|
255
311
|
this.symbolSize = requirePositiveInteger(fec.symbolSize, 'fec.symbolSize')
|
|
256
312
|
const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, 'fec.interleaveDepthMs')
|
|
257
313
|
const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, 'fec.reorderToleranceMs')
|
|
@@ -271,17 +327,38 @@ export class MmtpFecSession {
|
|
|
271
327
|
config.heldLayers,
|
|
272
328
|
)
|
|
273
329
|
this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
330
|
+
this.#decoder = buildDecoder(config.Decoder, this)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* A later version of the AL-FEC message, catalog-less sessions only. A
|
|
335
|
+
* maximum_p_for_repair_flow above the held total raises it (`raised`): the
|
|
336
|
+
* anchored check restarts at the new total, so a block generated under the
|
|
337
|
+
* previous one stays valid before the first block carrying the new total
|
|
338
|
+
* (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing
|
|
339
|
+
* (`unchanged`): no publisher lowers it, and the check catches a wire that
|
|
340
|
+
* does. An entry with another asset, repair flow, code point, K or T, or
|
|
341
|
+
* one outside 8.3, is `refused`: that is a new source encoding, which needs
|
|
342
|
+
* a new session. A latched fail-closed stays latched either way.
|
|
343
|
+
*/
|
|
344
|
+
updateAlFecFlow(flow: AlFecFlow): MmtpFecSessionMessageUpdate {
|
|
345
|
+
const held = this.#message
|
|
346
|
+
if (!held) throw new Error('MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry')
|
|
347
|
+
const next = alFecRaptorQFlowConfig(flow)
|
|
348
|
+
if (
|
|
349
|
+
!next ||
|
|
350
|
+
next.sourcePacketIds[0] !== held.sourcePacketIds[0] ||
|
|
351
|
+
next.repairFlowId !== held.repairFlowId ||
|
|
352
|
+
next.fecCodeId !== held.fecCodeId ||
|
|
353
|
+
next.maximumK !== held.maximumK ||
|
|
354
|
+
next.repairSymbolSize !== held.repairSymbolSize
|
|
355
|
+
) {
|
|
356
|
+
return 'refused'
|
|
283
357
|
}
|
|
284
|
-
this.#
|
|
358
|
+
if (next.maximumP <= this.#repairSymbols) return 'unchanged'
|
|
359
|
+
this.#repairSymbols = next.maximumP
|
|
360
|
+
this.#heldLayers = layerZero(next.maximumP)
|
|
361
|
+
return 'raised'
|
|
285
362
|
}
|
|
286
363
|
|
|
287
364
|
/** True once release() ran. */
|
|
@@ -386,7 +463,7 @@ export class MmtpFecSession {
|
|
|
386
463
|
// An RSB_length below a held layer's total goes to the 7.3.2 stop, not
|
|
387
464
|
// to the discard; the block state is unchanged either way.
|
|
388
465
|
const verdict = this.#heldLayers.failsClosed(
|
|
389
|
-
repairTrackName, frame.ssStart, frame.rsbLength, this.#anchorHorizon, this.#newestSource,
|
|
466
|
+
this.#trackFor(repairTrackName), frame.ssStart, frame.rsbLength, this.#anchorHorizon, this.#newestSource,
|
|
390
467
|
)
|
|
391
468
|
if (verdict) {
|
|
392
469
|
this.#stats.failClosed = verdict
|
|
@@ -399,7 +476,7 @@ export class MmtpFecSession {
|
|
|
399
476
|
// fec-01 6.3.9, after the 4.5 discards and the size check, before a block
|
|
400
477
|
// is placed. Without a track name only the anchored rule (b) applies.
|
|
401
478
|
const verdict = this.#heldLayers.admit(
|
|
402
|
-
repairTrackName,
|
|
479
|
+
this.#trackFor(repairTrackName),
|
|
403
480
|
frame.ssStart,
|
|
404
481
|
frame.rsbLength,
|
|
405
482
|
this.#anchorHorizon,
|
|
@@ -464,6 +541,11 @@ export class MmtpFecSession {
|
|
|
464
541
|
return true
|
|
465
542
|
}
|
|
466
543
|
|
|
544
|
+
/** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */
|
|
545
|
+
#trackFor(repairTrackName: string | undefined): string | undefined {
|
|
546
|
+
return this.#message ? undefined : repairTrackName
|
|
547
|
+
}
|
|
548
|
+
|
|
467
549
|
#drop(reason: string): Uint8Array[] {
|
|
468
550
|
this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1
|
|
469
551
|
return []
|
|
@@ -600,6 +682,24 @@ export class MmtpFecSession {
|
|
|
600
682
|
}
|
|
601
683
|
}
|
|
602
684
|
|
|
685
|
+
function layerZero(total: number): HeldLayerAdmission {
|
|
686
|
+
return new HeldLayerAdmission([{ layer: 0, repairTrackName: '', rsIdOffset: 0, repairSymbols: total }])
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function buildDecoder(Decoder: MmtpFecSessionDecoderConstructor, session: MmtpFecSession): MmtpFecSessionDecoder {
|
|
690
|
+
if (typeof Decoder !== 'function') {
|
|
691
|
+
throw new Error('MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required')
|
|
692
|
+
}
|
|
693
|
+
const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm)
|
|
694
|
+
try {
|
|
695
|
+
decoder.configure_params(session.sourceSymbols, session.symbolSize)
|
|
696
|
+
} catch (error) {
|
|
697
|
+
decoder.free()
|
|
698
|
+
throw error
|
|
699
|
+
}
|
|
700
|
+
return decoder
|
|
701
|
+
}
|
|
702
|
+
|
|
603
703
|
function requirePositiveInteger(value: unknown, path: string): number {
|
|
604
704
|
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
|
|
605
705
|
throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`)
|
package/src/mmtp-router.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type MMTPHeader,
|
|
24
24
|
type MoqRepairFrame,
|
|
25
25
|
} from "@blockcast/mmt-base";
|
|
26
|
+
import { USER_AGENT_VERIFIED } from "@blockcast/mmt-transport";
|
|
26
27
|
import type { FECManager, IsoHeldSource, WasmFecDecoder } from "./fec-manager.js";
|
|
27
28
|
import type { IsoPlacedSource, IsoRepairPlacement } from "./iso-block-locator.js";
|
|
28
29
|
import { isMfuReassemblyError } from "./mfu-reassembler.js";
|
|
@@ -245,6 +246,14 @@ export interface MmtpIngressContext {
|
|
|
245
246
|
* the same ingress and `feedPacket` splits them off.
|
|
246
247
|
*/
|
|
247
248
|
flow?: string;
|
|
249
|
+
/**
|
|
250
|
+
* `USER_AGENT_VERIFIED` when the datagram came from the browser's own
|
|
251
|
+
* MulticastReceiver, which verified the flow's records itself and delivers
|
|
252
|
+
* only admitted data (`userAgentVerifiedIngress` in
|
|
253
|
+
* `@blockcast/mmt-transport`). A protected multicast datagram carrying it
|
|
254
|
+
* is admitted without a record. Any other value means nothing.
|
|
255
|
+
*/
|
|
256
|
+
admittedBy?: symbol;
|
|
248
257
|
}
|
|
249
258
|
|
|
250
259
|
/** One physical stream: an MMTP packet ID and the catalog track it carries. */
|
|
@@ -946,6 +955,15 @@ function sourceSymbolEvidence(
|
|
|
946
955
|
};
|
|
947
956
|
}
|
|
948
957
|
|
|
958
|
+
/** One protected flow: its 7.2.7 receiver, and the ingress its datagrams arrive with. */
|
|
959
|
+
interface ProvenanceEntry {
|
|
960
|
+
receiver: BcProvenanceV2Receiver;
|
|
961
|
+
ingress: MmtpIngressContext;
|
|
962
|
+
protected?: boolean;
|
|
963
|
+
/** A datagram the user agent verified admitted the flow (`admittedBy`). */
|
|
964
|
+
userAgent?: boolean;
|
|
965
|
+
}
|
|
966
|
+
|
|
949
967
|
export class MmtpRouter {
|
|
950
968
|
readonly #parser: MmtpContainerParser;
|
|
951
969
|
// Source AND FEC-recovered MFU fragments share ONE reassembler — the parser's
|
|
@@ -955,10 +973,7 @@ export class MmtpRouter {
|
|
|
955
973
|
#defaultFecMapper: FecBlockMapper | null;
|
|
956
974
|
#fecMappersByPacketId = new Map<number, FecBlockMapper>();
|
|
957
975
|
/** Protected flows: one 7.2.7 receiver each, and the ingress its datagrams arrive with. */
|
|
958
|
-
readonly #provenance = new Map<
|
|
959
|
-
string,
|
|
960
|
-
{ receiver: BcProvenanceV2Receiver; ingress: MmtpIngressContext; protected?: boolean }
|
|
961
|
-
>();
|
|
976
|
+
readonly #provenance = new Map<string, ProvenanceEntry>();
|
|
962
977
|
#onFlowProtected: ((flow: string) => void) | null = null;
|
|
963
978
|
#unadmittedListedDrops = 0;
|
|
964
979
|
#unlistedRepairSetAside = 0;
|
|
@@ -2329,12 +2344,13 @@ export class MmtpRouter {
|
|
|
2329
2344
|
if (!entry) return;
|
|
2330
2345
|
entry.receiver.release();
|
|
2331
2346
|
entry.protected = false;
|
|
2347
|
+
entry.userAgent = false;
|
|
2332
2348
|
// ponytail: drops every governed block's record, not only this flow's; per-flow if joins multiply.
|
|
2333
2349
|
this.#assistBlocks.clear();
|
|
2334
2350
|
this.#assistFailures.clear();
|
|
2335
2351
|
}
|
|
2336
2352
|
|
|
2337
|
-
/**
|
|
2353
|
+
/** Packets of a track a protected flow lists, dropped unread: a MoQ copy (duplicate, overflow, or listed by more than one flow) or a multicast source datagram that names no flow, of a track a user-agent-admitted flow lists. */
|
|
2338
2354
|
get unadmittedListedDropCount(): number {
|
|
2339
2355
|
return this.#unadmittedListedDrops;
|
|
2340
2356
|
}
|
|
@@ -2384,6 +2400,14 @@ export class MmtpRouter {
|
|
|
2384
2400
|
return false;
|
|
2385
2401
|
}
|
|
2386
2402
|
|
|
2403
|
+
/** Whether a protected flow the user agent admitted lists `packetId`. */
|
|
2404
|
+
#userAgentGoverns(packetId: number): boolean {
|
|
2405
|
+
for (const entry of this.#provenance.values()) {
|
|
2406
|
+
if (entry.protected && entry.userAgent && ingressHasPacketId(entry.ingress, packetId)) return true;
|
|
2407
|
+
}
|
|
2408
|
+
return false;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2387
2411
|
/**
|
|
2388
2412
|
* A MoQ repair symbol no protected flow lists, owned by a source track one
|
|
2389
2413
|
* does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
|
|
@@ -2503,7 +2527,7 @@ export class MmtpRouter {
|
|
|
2503
2527
|
fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
|
|
2504
2528
|
fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
|
|
2505
2529
|
fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
|
|
2506
|
-
})) {
|
|
2530
|
+
}, true)) {
|
|
2507
2531
|
this.#unlistedAssistedRecoveries++;
|
|
2508
2532
|
this.#assistBlocks.delete(key);
|
|
2509
2533
|
return;
|
|
@@ -3959,6 +3983,15 @@ export class MmtpRouter {
|
|
|
3959
3983
|
debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
|
|
3960
3984
|
return;
|
|
3961
3985
|
}
|
|
3986
|
+
// The user agent verified this datagram against the flow's records
|
|
3987
|
+
// itself, and the records never reach the page: admitted as is. The
|
|
3988
|
+
// flow stays registered, so its MoQ copies are still checked.
|
|
3989
|
+
if (ingress.admittedBy === USER_AGENT_VERIFIED) {
|
|
3990
|
+
this.#noteFlowAdmitted(ingress.flow);
|
|
3991
|
+
protectedFlow.userAgent = true;
|
|
3992
|
+
this.#processMMTP(packet, source, true, ingress, overlapCopy);
|
|
3993
|
+
return;
|
|
3994
|
+
}
|
|
3962
3995
|
// Held: released by a record (feedProvenanceRecord) or expired. Duplicate / overflow: dropped.
|
|
3963
3996
|
const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
|
|
3964
3997
|
if (verdict === "admitted") {
|
|
@@ -3983,6 +4016,18 @@ export class MmtpRouter {
|
|
|
3983
4016
|
}
|
|
3984
4017
|
}
|
|
3985
4018
|
|
|
4019
|
+
// A multicast datagram that names no flow is unverified. Of a track a
|
|
4020
|
+
// user-agent-admitted flow lists it would feed that flow's live decoder,
|
|
4021
|
+
// whose recoveries are kept without a record (#routeVerifiedFecSymbols):
|
|
4022
|
+
// its source is dropped here and its repair set aside below, as for
|
|
4023
|
+
// unlisted MoQ. Under a record-only flow it is read as before: those
|
|
4024
|
+
// recoveries still need a record.
|
|
4025
|
+
const flowless = source === "multicast" && !manifestAdmitted && ingress?.flow === undefined;
|
|
4026
|
+
if (flowless && !info.isRepair && this.#userAgentGoverns(info.packetId)) {
|
|
4027
|
+
this.#unadmittedListedDrops++;
|
|
4028
|
+
return;
|
|
4029
|
+
}
|
|
4030
|
+
|
|
3986
4031
|
if (info.isRepair) {
|
|
3987
4032
|
// Repair packet — per ISO 23008-1 §C.5.3, carries (SS_Start, SSB_length, RS_ID).
|
|
3988
4033
|
// Read from the header parsed above rather than parsing it again
|
|
@@ -4003,9 +4048,10 @@ export class MmtpRouter {
|
|
|
4003
4048
|
);
|
|
4004
4049
|
if (!repair) return;
|
|
4005
4050
|
if (ownerPacketId != null && this.#fecManager) {
|
|
4006
|
-
// An unadmitted MoQ symbol reaching here is unlisted (#admitUnicast)
|
|
4051
|
+
// An unadmitted MoQ symbol reaching here is unlisted (#admitUnicast);
|
|
4052
|
+
// a flow-less multicast one is unverified.
|
|
4007
4053
|
if (
|
|
4008
|
-
source === "moq" && !manifestAdmitted &&
|
|
4054
|
+
((flowless && this.#userAgentGoverns(ownerPacketId)) || (source === "moq" && !manifestAdmitted)) &&
|
|
4009
4055
|
this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)
|
|
4010
4056
|
) return;
|
|
4011
4057
|
this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
|
|
@@ -4950,6 +4996,7 @@ export class MmtpRouter {
|
|
|
4950
4996
|
ownerPacketId?: number,
|
|
4951
4997
|
catalogTrackName?: string,
|
|
4952
4998
|
complete?: () => void,
|
|
4999
|
+
assisted = false,
|
|
4953
5000
|
): boolean {
|
|
4954
5001
|
// MMTP path: use processFecBlock for MMTP-wrapped recovery.
|
|
4955
5002
|
// Recovered symbols are individual MMTP packets that may be fragments
|
|
@@ -4974,7 +5021,7 @@ export class MmtpRouter {
|
|
|
4974
5021
|
ownerPacketId,
|
|
4975
5022
|
blockProfile,
|
|
4976
5023
|
});
|
|
4977
|
-
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
|
|
5024
|
+
return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted);
|
|
4978
5025
|
}
|
|
4979
5026
|
|
|
4980
5027
|
/**
|
|
@@ -4987,6 +5034,19 @@ export class MmtpRouter {
|
|
|
4987
5034
|
* recovered packets is refused is unrecovered (7.2.7): its caller leaves it
|
|
4988
5035
|
* open, so it reaches its FEC deadline and fails by class. Always true
|
|
4989
5036
|
* outside a protected flow.
|
|
5037
|
+
*
|
|
5038
|
+
* On a flow the user agent admitted, the live decoder's recoveries are kept
|
|
5039
|
+
* without a record: while the flow is protected that decoder takes only
|
|
5040
|
+
* symbols the user agent or a record verified (a flow-less multicast
|
|
5041
|
+
* datagram of a listed track is dropped or set aside in #processMMTP), so
|
|
5042
|
+
* what it recovers is as verified as they are. An `assisted` (scratch-decoder) recovery mixes in
|
|
5043
|
+
* unlisted MoQ repair and still needs a record.
|
|
5044
|
+
* ponytail: per-flow flag; per-block provenance only if a protected flow
|
|
5045
|
+
* ever mixes verified and unverified live inputs.
|
|
5046
|
+
* ponytail: a worker frame computed before the protection reset can still
|
|
5047
|
+
* arrive after it (frames carry no epoch); its inputs are MoQ copies read
|
|
5048
|
+
* and played before protection, so it admits nothing new. Drop pre-reset
|
|
5049
|
+
* frames by epoch only if pre-protection input ever matters.
|
|
4990
5050
|
*/
|
|
4991
5051
|
#routeVerifiedFecSymbols(
|
|
4992
5052
|
blockId: number,
|
|
@@ -4994,8 +5054,11 @@ export class MmtpRouter {
|
|
|
4994
5054
|
ownerPacketId?: number,
|
|
4995
5055
|
catalogTrackName?: string,
|
|
4996
5056
|
complete?: () => void,
|
|
5057
|
+
assisted = false,
|
|
4997
5058
|
): boolean {
|
|
4998
|
-
const
|
|
5059
|
+
const entry = ownerPacketId == null ? undefined : this.#provenanceFor(ownerPacketId, catalogTrackName);
|
|
5060
|
+
const receiver = entry === undefined || entry === "ambiguous" ? entry : entry.receiver;
|
|
5061
|
+
const userAgent = !assisted && entry !== undefined && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
|
|
4999
5062
|
let recovered = true;
|
|
5000
5063
|
if (receiver) {
|
|
5001
5064
|
const parsed = this.#stream(ownerPacketId!, catalogTrackName).duplicates.parsedSymbols;
|
|
@@ -5008,6 +5071,8 @@ export class MmtpRouter {
|
|
|
5008
5071
|
let admitted = false;
|
|
5009
5072
|
if (receiver === "ambiguous") {
|
|
5010
5073
|
this.#ambiguousRecoveredDrops++;
|
|
5074
|
+
} else if (userAgent) {
|
|
5075
|
+
admitted = true;
|
|
5011
5076
|
} else {
|
|
5012
5077
|
const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
|
|
5013
5078
|
wire.set(sym.rawSymbol);
|
|
@@ -5033,7 +5098,7 @@ export class MmtpRouter {
|
|
|
5033
5098
|
#provenanceFor(
|
|
5034
5099
|
packetId: number,
|
|
5035
5100
|
catalogTrackName: string | undefined,
|
|
5036
|
-
):
|
|
5101
|
+
): ProvenanceEntry | "ambiguous" | undefined {
|
|
5037
5102
|
// A caller that names no track matches every name the packet id was admitted under.
|
|
5038
5103
|
const byName = this.#provenanceStreamFlow.get(packetId);
|
|
5039
5104
|
const flows = new Set(
|
|
@@ -5042,12 +5107,12 @@ export class MmtpRouter {
|
|
|
5042
5107
|
: byName.has(catalogTrackName) ? [byName.get(catalogTrackName)!] : [],
|
|
5043
5108
|
);
|
|
5044
5109
|
if (flows.size > 1) return "ambiguous";
|
|
5045
|
-
for (const bound of flows) return this.#provenance.get(bound)
|
|
5046
|
-
let match:
|
|
5047
|
-
for (const
|
|
5048
|
-
if (ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === undefined || t.name === catalogTrackName))) {
|
|
5110
|
+
for (const bound of flows) return this.#provenance.get(bound) ?? "ambiguous";
|
|
5111
|
+
let match: ProvenanceEntry | undefined;
|
|
5112
|
+
for (const entry of this.#provenance.values()) {
|
|
5113
|
+
if (entry.ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === undefined || t.name === catalogTrackName))) {
|
|
5049
5114
|
if (match) return "ambiguous";
|
|
5050
|
-
match =
|
|
5115
|
+
match = entry;
|
|
5051
5116
|
}
|
|
5052
5117
|
}
|
|
5053
5118
|
return match;
|
package/src/multicast-manager.ts
CHANGED
|
@@ -50,7 +50,11 @@ import {
|
|
|
50
50
|
type StreamSwitchCommit,
|
|
51
51
|
type TrackPacketCallback,
|
|
52
52
|
} from "./mmtp-router.js";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
isMulticastTrackPacketId,
|
|
55
|
+
userAgentVerifiedIngress,
|
|
56
|
+
type WindowMulticastGateway,
|
|
57
|
+
} from "@blockcast/mmt-transport";
|
|
54
58
|
import {
|
|
55
59
|
MulticastTransport,
|
|
56
60
|
type MulticastFailbackReason,
|
|
@@ -94,6 +98,8 @@ export interface MulticastRepairEndpoint {
|
|
|
94
98
|
mode?: string;
|
|
95
99
|
amtRelay?: string;
|
|
96
100
|
amtRelayPort?: number;
|
|
101
|
+
/** The media join's `'driad'` discovery (no relay pinned), carried so the repair group rides the same relay. */
|
|
102
|
+
amtDiscovery?: 'driad';
|
|
97
103
|
/** The repair packet IDs this group carries. */
|
|
98
104
|
packetIds: number[];
|
|
99
105
|
/** Each repair packet ID bound to the media packet ID it repairs. */
|
|
@@ -2353,10 +2359,20 @@ export class MulticastManager {
|
|
|
2353
2359
|
* repair roles and owners are registered here; in-band owners stay the
|
|
2354
2360
|
* caller's. Joining is best effort: a group that fails to join is logged
|
|
2355
2361
|
* and leaves the media join as it was.
|
|
2362
|
+
*
|
|
2363
|
+
* `options.gateway` names the gateway a subscribe callback reads through.
|
|
2364
|
+
* When it is the browser's own MulticastReceiver provider, the callback's
|
|
2365
|
+
* packets are fed as admitted by the user agent
|
|
2366
|
+
* (`userAgentVerifiedIngress`); nothing the callback passes can mark them.
|
|
2367
|
+
* The mark rests on this statement, not on the data path: every packet the
|
|
2368
|
+
* callback delivers must come from a subscription opened on
|
|
2369
|
+
* `options.gateway`. A callback that also merges another source (a MoQ or
|
|
2370
|
+
* WebSocket repair feed) must not name a native provider here.
|
|
2356
2371
|
*/
|
|
2357
2372
|
joinRepairEndpoints(
|
|
2358
2373
|
endpoints: readonly MulticastRepairEndpoint[],
|
|
2359
2374
|
provider?: WindowMulticastGateway | MulticastRepairSubscribe,
|
|
2375
|
+
options?: { gateway?: WindowMulticastGateway },
|
|
2360
2376
|
): () => void {
|
|
2361
2377
|
const releases: Array<() => void> = [];
|
|
2362
2378
|
let released = false;
|
|
@@ -2382,22 +2398,26 @@ export class MulticastManager {
|
|
|
2382
2398
|
...(endpoint.flow === undefined ? {} : { flow: endpoint.flow }),
|
|
2383
2399
|
};
|
|
2384
2400
|
let sender: string | undefined;
|
|
2385
|
-
const
|
|
2401
|
+
const feed = (packet: Uint8Array, sourceAddress: string | undefined, ingressToFeed: MmtpIngressContext) => {
|
|
2386
2402
|
if (released) return;
|
|
2387
2403
|
// The media join's sender owns the repair too; this group's own first
|
|
2388
2404
|
// sender stands in only until the media join has one.
|
|
2389
2405
|
if (sourceAddress !== undefined && !sameDatagramSource(sourceAddress, this.#transport.sender ?? (sender ??= sourceAddress))) return;
|
|
2390
|
-
this.#router.feedPacket(packet, "multicast",
|
|
2406
|
+
this.#router.feedPacket(packet, "multicast", ingressToFeed);
|
|
2391
2407
|
};
|
|
2392
2408
|
const fail = (error: unknown) =>
|
|
2393
2409
|
console.warn(`[MulticastManager] repair group ${endpoint.group}:${endpoint.port} not joined`, error);
|
|
2394
2410
|
if (typeof provider === "function") {
|
|
2411
|
+
const marked = userAgentVerifiedIngress(ingress, options?.gateway);
|
|
2412
|
+
const onPacket = (packet: Uint8Array, sourceAddress?: string) => feed(packet, sourceAddress, marked);
|
|
2395
2413
|
Promise.resolve()
|
|
2396
2414
|
.then(() => provider(endpoint, onPacket))
|
|
2397
2415
|
.then((release) => (released ? safely(release) : releases.push(release)), fail);
|
|
2398
2416
|
continue;
|
|
2399
2417
|
}
|
|
2400
|
-
|
|
2418
|
+
// The transport marks what it delivers when its gateway is the user
|
|
2419
|
+
// agent's MulticastReceiver provider; feedPacket would not.
|
|
2420
|
+
const transport = new MulticastTransport<MmtpIngressContext>((packet, delivered) => feed(packet, undefined, delivered ?? ingress), {
|
|
2401
2421
|
gateway: provider,
|
|
2402
2422
|
lockedTo: () => this.#transport.sender,
|
|
2403
2423
|
});
|
|
@@ -2409,11 +2429,12 @@ export class MulticastManager {
|
|
|
2409
2429
|
mode: endpoint.mode as MulticastEndpointConfig["mode"],
|
|
2410
2430
|
amtRelay: endpoint.amtRelay,
|
|
2411
2431
|
amtRelayPort: endpoint.amtRelayPort ?? (endpoint.amtRelay?.trim() ? DEFAULT_AMT_RELAY_PORT : undefined),
|
|
2432
|
+
amtDiscovery: endpoint.amtDiscovery,
|
|
2412
2433
|
packetIds: endpoint.packetIds,
|
|
2413
2434
|
tracks: endpoint.tracks.map(({ name, packetId }) => ({ name, packetId })),
|
|
2414
2435
|
// A protected repair group's records ride in band on its port (7.2.3).
|
|
2415
2436
|
...(endpoint.flow === undefined ? {} : { protected: true }),
|
|
2416
|
-
}).then((joined) => {
|
|
2437
|
+
}, ingress).then((joined) => {
|
|
2417
2438
|
if (!joined && !released) fail(transport.lastJoinOutcome);
|
|
2418
2439
|
});
|
|
2419
2440
|
}
|