@blockcast/mmt-container 2.2.0-main.f480a7ede8b1 → 2.2.0-main.fb5c7dfbe2de
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 +891 -141
- package/dist/browser-runtime.js.map +3 -3
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +67 -11
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +283 -54
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.d.ts +39 -13
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +80 -25
- package/dist/iso-block-locator.js.map +2 -2
- package/dist/media-router.js.map +1 -1
- package/dist/mmtp-container-parser.d.ts +42 -8
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +121 -17
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +120 -18
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +121 -17
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +28 -8
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +407 -52
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-manager.d.ts +13 -3
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +712 -115
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +2 -2
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.js +121 -17
- package/dist/no-modules.js.map +2 -2
- package/dist/repair-fetch.d.ts +36 -8
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +285 -26
- package/dist/repair-fetch.js.map +3 -3
- package/dist/track-session.d.ts.map +1 -1
- package/dist/track-session.js +1 -0
- package/dist/track-session.js.map +2 -2
- package/dist/wasm-mmtp-container-parser.js +121 -17
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +5 -5
- package/src/fec-manager.ts +304 -28
- package/src/index.ts +2 -0
- package/src/iso-block-locator.ts +114 -31
- package/src/mmtp-container-parser.ts +210 -21
- package/src/mmtp-router.ts +488 -45
- package/src/multicast-manager.ts +44 -6
- package/src/repair-fetch.ts +363 -49
- package/src/track-session.ts +3 -0
package/src/iso-block-locator.ts
CHANGED
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
* between are one or more blocks whose every repair symbol was lost: they are
|
|
15
15
|
* kept as one repair-less span, which completes clean once all of its SS_IDs
|
|
16
16
|
* arrived (C.5.2 makes a gap in consecutive SS_IDs directly visible) and
|
|
17
|
-
* otherwise resolves through its catalog-derived deadline.
|
|
18
|
-
*
|
|
19
|
-
* a
|
|
17
|
+
* otherwise resolves through its catalog-derived deadline. A block without
|
|
18
|
+
* repair completes that way, on the next block's SS_Start, never on a count of
|
|
19
|
+
* K source symbols, unless a repair that arrives later (a FETCH of the span's
|
|
20
|
+
* first group) states exactly the span's range: the span was that one block.
|
|
20
21
|
*/
|
|
21
22
|
import {
|
|
22
23
|
isoBlockContains,
|
|
@@ -58,14 +59,29 @@ export type IsoRepairPlacement<T> =
|
|
|
58
59
|
*/
|
|
59
60
|
readonly hole?: { readonly block: IsoBlockRange; readonly lostSsId: number }
|
|
60
61
|
}
|
|
61
|
-
/**
|
|
62
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Not a block this stream can use; the repair symbol is dropped. `ofSpan` is
|
|
64
|
+
* set when the repair stated only the first block of that repair-less span:
|
|
65
|
+
* the span holds more than one block, so no single repair group completes it.
|
|
66
|
+
*/
|
|
67
|
+
| {
|
|
68
|
+
readonly status: 'conflict' | 'overlap' | 'stale' | 'ahead'
|
|
69
|
+
readonly reason: string
|
|
70
|
+
readonly ofSpan?: IsoBlockRange
|
|
71
|
+
}
|
|
63
72
|
|
|
64
73
|
/**
|
|
65
74
|
* Retention: the locator is never without one. Every call that can grow it
|
|
66
75
|
* (`hold`, `placeRepair`) retires, behind the position it just saw, every
|
|
67
76
|
* block, span and held source more than `retentionBlocks` blocks back, by the
|
|
68
|
-
* one ISO horizon metric (`isoBlocksBehindLowerBound`). A
|
|
77
|
+
* one ISO horizon metric (`isoBlocksBehindLowerBound`). A held source stays
|
|
78
|
+
* while a pending hole holding it does: the hole's repair FETCH may still
|
|
79
|
+
* state its block, and closing the hole places it, so the span it leaves
|
|
80
|
+
* counts every source it received (BLO-52085). A pending hole the horizon
|
|
81
|
+
* passes stops being pending (its blocks can no longer be stated, and the
|
|
82
|
+
* release gate no longer waits for it), but keeps its sources until its
|
|
83
|
+
* owner closes it (`takeExpiredHoles`), so it still fails as the span it
|
|
84
|
+
* is. A block is measured
|
|
69
85
|
* from its last SS_ID, so it stays while any SS_ID of it is inside the
|
|
70
86
|
* horizon: a repair-less span has no length bound, and one measured from its
|
|
71
87
|
* start could be retired the moment it is created, before the sources it
|
|
@@ -88,6 +104,15 @@ export class IsoBlockLocator<T> {
|
|
|
88
104
|
#newestSource: number | undefined
|
|
89
105
|
/** Pending holes (see `deferIncompleteGaps`): unstated, sources still held. */
|
|
90
106
|
readonly #holes: IsoBlockRange[] = []
|
|
107
|
+
/** Holes the horizon passed while pending: sources held until closed. */
|
|
108
|
+
readonly #expired: IsoBlockRange[] = []
|
|
109
|
+
/** Of `#expired`, those `takeExpiredHoles` has not handed out yet. */
|
|
110
|
+
readonly #newlyExpired: IsoBlockRange[] = []
|
|
111
|
+
/**
|
|
112
|
+
* Spans closed from expired holes: behind the horizon, so kept only for
|
|
113
|
+
* `locate` while their sources are fed, until the next retirement.
|
|
114
|
+
*/
|
|
115
|
+
readonly #closedBehind: IsoBlockRange[] = []
|
|
91
116
|
/**
|
|
92
117
|
* Keep a gap that is missing a source as a pending hole rather than a span,
|
|
93
118
|
* so a repair FETCHed for it (draft-ramadan-moq-fec §7.5) can still state
|
|
@@ -192,18 +217,31 @@ export class IsoBlockLocator<T> {
|
|
|
192
217
|
|
|
193
218
|
/** The known block or span containing `ssId`, with its ESI. */
|
|
194
219
|
locate(ssId: number): { block: IsoBlockRange; esi: number } | null {
|
|
195
|
-
for (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
220
|
+
for (const blocks of [this.#blocks, this.#closedBehind]) {
|
|
221
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
222
|
+
const block = blocks[i]!
|
|
223
|
+
if (isoBlockContains(block.start, block.length, ssId)) {
|
|
224
|
+
return { block, esi: serial32Distance(block.start, ssId) }
|
|
225
|
+
}
|
|
199
226
|
}
|
|
200
227
|
}
|
|
201
228
|
return null
|
|
202
229
|
}
|
|
203
230
|
|
|
204
|
-
/** Number of pending
|
|
231
|
+
/** Number of holes not closed yet, pending or past the horizon. */
|
|
205
232
|
get holeCount(): number {
|
|
206
|
-
return this.#holes.length
|
|
233
|
+
return this.#holes.length + this.#expired.length
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The pending holes the horizon has passed since the last call, in the
|
|
238
|
+
* order it passed them. Each is no longer pending: no repair states a block
|
|
239
|
+
* in it (that repair is `stale`, as for a span the horizon retired) and the
|
|
240
|
+
* release gate does not wait for it. Its owner must close it
|
|
241
|
+
* (`closeHolesWithin`), which turns it into a span that is not kept here.
|
|
242
|
+
*/
|
|
243
|
+
takeExpiredHoles(): IsoBlockRange[] {
|
|
244
|
+
return this.#newlyExpired.splice(0)
|
|
207
245
|
}
|
|
208
246
|
|
|
209
247
|
/** The stated block (never a span) ending exactly at `end`. */
|
|
@@ -268,9 +306,11 @@ export class IsoBlockLocator<T> {
|
|
|
268
306
|
/**
|
|
269
307
|
* Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
|
|
270
308
|
* block states its range and places every held source in it; later repair
|
|
271
|
-
* symbols of the same block change nothing. A range
|
|
272
|
-
*
|
|
273
|
-
*
|
|
309
|
+
* symbols of the same block change nothing. A repair whose range is exactly
|
|
310
|
+
* a repair-less span states that span's one block. A range that contradicts
|
|
311
|
+
* a known block (a different length, or an overlap with another block or
|
|
312
|
+
* with part of a span) fails closed: the repair symbol is dropped and no
|
|
313
|
+
* block changes.
|
|
274
314
|
* A newly stated block retires behind its SS_Start (see the class note).
|
|
275
315
|
* A block so far past `newest` that stating it would retire the position
|
|
276
316
|
* `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
|
|
@@ -289,7 +329,25 @@ export class IsoBlockLocator<T> {
|
|
|
289
329
|
const known = this.blockAt(start)
|
|
290
330
|
if (known !== undefined) {
|
|
291
331
|
if (known.span) {
|
|
292
|
-
|
|
332
|
+
// A span is no accepted block: it is the whole blocks between two
|
|
333
|
+
// stated ones whose repair never came (blocks are consecutive), so a
|
|
334
|
+
// repair whose range is exactly the span states its one block and
|
|
335
|
+
// contradicts nothing (draft-ramadan-moq-fec-core-00 5.4). Its sources
|
|
336
|
+
// were placed at these same ESIs when the span closed. A FETCH of the
|
|
337
|
+
// span's Group ext(SS_Start) returns just this when one block lost
|
|
338
|
+
// every multicast repair (a join).
|
|
339
|
+
if (known.length === length) {
|
|
340
|
+
const block: IsoBlockRange = { start, length, span: false }
|
|
341
|
+
this.#blocks[this.#blocks.indexOf(known)] = block
|
|
342
|
+
return { status: 'placed', block, placed: [] }
|
|
343
|
+
}
|
|
344
|
+
return length < known.length
|
|
345
|
+
? {
|
|
346
|
+
status: 'overlap',
|
|
347
|
+
reason: `repair block ${start}+${length} is only the first block of repair-less span ${known.start}+${known.length}`,
|
|
348
|
+
ofSpan: known,
|
|
349
|
+
}
|
|
350
|
+
: { status: 'overlap', reason: `repair block ${start}+${length} runs past repair-less span ${known.start}+${known.length}` }
|
|
293
351
|
}
|
|
294
352
|
if (known.length !== length) {
|
|
295
353
|
return {
|
|
@@ -367,18 +425,28 @@ export class IsoBlockLocator<T> {
|
|
|
367
425
|
}
|
|
368
426
|
|
|
369
427
|
/**
|
|
370
|
-
* Turn every
|
|
371
|
-
*
|
|
372
|
-
*
|
|
428
|
+
* Turn every hole inside `[start, start + length)` into a repair-less span,
|
|
429
|
+
* claiming its held sources: its range FETCH ended, so nothing more will
|
|
430
|
+
* state its blocks. A hole the horizon passed (`takeExpiredHoles`) leaves a
|
|
431
|
+
* span that is not kept among the blocks, as the horizon would retire it at
|
|
432
|
+
* once; only `locate` finds it, until the next retirement, so its sources
|
|
433
|
+
* can be fed. Returns the spans, in SS_ID order.
|
|
373
434
|
*/
|
|
374
435
|
closeHolesWithin(start: number, length: number): { block: IsoBlockRange; placed: IsoPlacedSource<T>[] }[] {
|
|
375
436
|
const closed: { block: IsoBlockRange; placed: IsoPlacedSource<T>[] }[] = []
|
|
376
|
-
for (const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
437
|
+
for (const holes of [this.#holes, this.#expired]) {
|
|
438
|
+
for (const hole of [...holes]) {
|
|
439
|
+
if (!isoBlockContains(start, length, hole.start)) continue
|
|
440
|
+
holes.splice(holes.indexOf(hole), 1)
|
|
441
|
+
const spanBlock: IsoBlockRange = { start: hole.start, length: hole.length, span: true }
|
|
442
|
+
closed.push({ block: spanBlock, placed: this.#claim(spanBlock) })
|
|
443
|
+
if (holes === this.#holes) {
|
|
444
|
+
this.#blocks.push(spanBlock)
|
|
445
|
+
} else {
|
|
446
|
+
this.#closedBehind.push(spanBlock)
|
|
447
|
+
if (this.#newlyExpired.includes(hole)) this.#newlyExpired.splice(this.#newlyExpired.indexOf(hole), 1)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
382
450
|
}
|
|
383
451
|
return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start))
|
|
384
452
|
}
|
|
@@ -395,15 +463,17 @@ export class IsoBlockLocator<T> {
|
|
|
395
463
|
}
|
|
396
464
|
|
|
397
465
|
/**
|
|
398
|
-
* Retire every block and span whose last SS_ID, and every held source
|
|
399
|
-
* lies more than `retentionBlocks` blocks behind
|
|
400
|
-
* lower-bound metric (`isoBlocksBehindLowerBound`)
|
|
401
|
-
* horizon uses
|
|
466
|
+
* Retire every block and span whose last SS_ID, and every held source
|
|
467
|
+
* outside a hole, lies more than `retentionBlocks` blocks behind
|
|
468
|
+
* `position`, with the shared lower-bound metric (`isoBlocksBehindLowerBound`)
|
|
469
|
+
* every ISO interleave horizon uses; a pending hole so far behind expires
|
|
470
|
+
* (`takeExpiredHoles`). Returns the retired block starts.
|
|
402
471
|
*/
|
|
403
472
|
retireBehind(position: number): number[] {
|
|
404
473
|
this.observe(position)
|
|
405
474
|
const horizon = this.#retentionBlocks
|
|
406
475
|
const retired: number[] = []
|
|
476
|
+
this.#closedBehind.length = 0
|
|
407
477
|
for (let i = this.#blocks.length - 1; i >= 0; i--) {
|
|
408
478
|
const block = this.#blocks[i]!
|
|
409
479
|
const last = serial32Add(block.start, block.length - 1)
|
|
@@ -415,10 +485,20 @@ export class IsoBlockLocator<T> {
|
|
|
415
485
|
for (let i = this.#holes.length - 1; i >= 0; i--) {
|
|
416
486
|
const hole = this.#holes[i]!
|
|
417
487
|
const last = serial32Add(hole.start, hole.length - 1)
|
|
418
|
-
if (isoBlocksBehindLowerBound(last, position, this.#kMax)
|
|
488
|
+
if (isoBlocksBehindLowerBound(last, position, this.#kMax) <= horizon) continue
|
|
489
|
+
this.#holes.splice(i, 1)
|
|
490
|
+
this.#expired.push(hole)
|
|
491
|
+
this.#newlyExpired.push(hole)
|
|
419
492
|
}
|
|
493
|
+
// A held source stays while a hole holding it does (class note).
|
|
420
494
|
for (const ssId of [...this.#held.keys()]) {
|
|
421
|
-
if (
|
|
495
|
+
if (
|
|
496
|
+
isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon &&
|
|
497
|
+
!this.#holes.some((hole) => isoBlockContains(hole.start, hole.length, ssId)) &&
|
|
498
|
+
!this.#expired.some((hole) => isoBlockContains(hole.start, hole.length, ssId))
|
|
499
|
+
) {
|
|
500
|
+
this.#held.delete(ssId)
|
|
501
|
+
}
|
|
422
502
|
}
|
|
423
503
|
return retired
|
|
424
504
|
}
|
|
@@ -427,6 +507,9 @@ export class IsoBlockLocator<T> {
|
|
|
427
507
|
reset(): void {
|
|
428
508
|
this.#blocks.length = 0
|
|
429
509
|
this.#holes.length = 0
|
|
510
|
+
this.#expired.length = 0
|
|
511
|
+
this.#newlyExpired.length = 0
|
|
512
|
+
this.#closedBehind.length = 0
|
|
430
513
|
this.#held.clear()
|
|
431
514
|
this.#coverageEnd = undefined
|
|
432
515
|
this.#newest = undefined
|
|
@@ -173,6 +173,15 @@ export interface FrameMeta {
|
|
|
173
173
|
* on FEC-gated streams.
|
|
174
174
|
*/
|
|
175
175
|
fecSbn?: number
|
|
176
|
+
/**
|
|
177
|
+
* Set on a frame the keyframe overlay rebuilt (draft-ramadan-moq-mmt-02
|
|
178
|
+
* 7.4) whose base SS_IDs are determined: the lowest base SS_ID of its
|
|
179
|
+
* MPU's packets in that decode, its FT=0 when the decode holds it. The
|
|
180
|
+
* release gate waits only for the blocks before it, and stops waiting once
|
|
181
|
+
* a base frame after this one arrived. `fecSbn` stays the SS_ID the frame
|
|
182
|
+
* completed in.
|
|
183
|
+
*/
|
|
184
|
+
fecSbnFirst?: number
|
|
176
185
|
/**
|
|
177
186
|
* Set on the first frame released after a gap: the blocks between it and
|
|
178
187
|
* the previous release resolved with lost media, or authored decode time
|
|
@@ -242,13 +251,22 @@ export interface FecReleaseGate {
|
|
|
242
251
|
isRap?(data: Uint8Array, meta: FrameMeta): boolean
|
|
243
252
|
/** A complete RAP entered the gate at SS_ID `ssId` (see `isRap`). */
|
|
244
253
|
onRap?(packetId: number, catalogTrackName: string | undefined, ssId: number): void
|
|
254
|
+
/**
|
|
255
|
+
* Whether what reads this stream decides on when its frames arrive (a
|
|
256
|
+
* staged stream switch): the gate then releases a held overlay-rebuilt
|
|
257
|
+
* frame when it did unanchored (#cursorPassed).
|
|
258
|
+
*/
|
|
259
|
+
releaseUnanchored?(packetId: number, catalogTrackName: string | undefined): boolean
|
|
245
260
|
}
|
|
246
261
|
|
|
247
262
|
/** Typed ISO-BMFF timing-sample budget counters. See `timingBudgetStats`. */
|
|
248
263
|
export interface MmtpTimingBudgetStats {
|
|
249
264
|
/** Slots currently charged to declared-but-undelivered samples. */
|
|
250
265
|
pending: number
|
|
251
|
-
/**
|
|
266
|
+
/**
|
|
267
|
+
* Slots each stream's window holds (`maxBufferedMfus`). A stream at
|
|
268
|
+
* capacity displaces its own oldest timing; `pending` sums every stream.
|
|
269
|
+
*/
|
|
252
270
|
capacity: number
|
|
253
271
|
/** Slots returned by samples the combiner abandoned (evicted or timed out). */
|
|
254
272
|
reclaimed: number
|
|
@@ -259,7 +277,13 @@ export interface MmtpTimingBudgetStats {
|
|
|
259
277
|
* up, `evicted` is loss that was never observable at all.
|
|
260
278
|
*/
|
|
261
279
|
evicted: number
|
|
262
|
-
/**
|
|
280
|
+
/**
|
|
281
|
+
* Movie fragments refused because the budget had no room for their samples.
|
|
282
|
+
* Stays 0 with a window per stream (BLO-52085): a stream at capacity
|
|
283
|
+
* displaces its own oldest timing to make room, and a moof declaring more
|
|
284
|
+
* samples than a whole window holds is refused as invalid before any budget
|
|
285
|
+
* is consulted. Kept as a guard, and because it is public.
|
|
286
|
+
*/
|
|
263
287
|
rejectedMovieFragments: number
|
|
264
288
|
}
|
|
265
289
|
|
|
@@ -367,7 +391,10 @@ export type MmtpFeedDropReason =
|
|
|
367
391
|
| 'fragment-unexpected-fragmentation'
|
|
368
392
|
/** FT=1 before an FT=0 has bound the stream's timing track. */
|
|
369
393
|
| 'fragment-no-resolver'
|
|
370
|
-
/** addMovieFragment refused
|
|
394
|
+
/** addMovieFragment refused: the moof does not parse against the stream's
|
|
395
|
+
* track, conflicts with a pending copy of itself, or repeats a pending
|
|
396
|
+
* copy whose samples are partly consumed. Never the timing budget: a full
|
|
397
|
+
* window displaces the stream's own oldest timing. */
|
|
371
398
|
| 'fragment-timing-refused'
|
|
372
399
|
| 'unknown-fragment-type'
|
|
373
400
|
/** Stable code, no longer produced: an FT=2 with no timing (before the
|
|
@@ -508,6 +535,8 @@ export interface ParseHeaderOptions {
|
|
|
508
535
|
deliveryProvenance?: MmtpDeliveryProvenance
|
|
509
536
|
/** FEC source block this packet belongs to (FEC-protected packets only). */
|
|
510
537
|
fecSbn?: number
|
|
538
|
+
/** See FrameMeta.fecSbnFirst. */
|
|
539
|
+
fecSbnFirst?: number
|
|
511
540
|
}
|
|
512
541
|
|
|
513
542
|
// ── FrameSequencer ──────────────────────────────────────────────────
|
|
@@ -721,9 +750,19 @@ function ssIdAfter(a: number, b: number): boolean {
|
|
|
721
750
|
return ((a - b) | 0) > 0
|
|
722
751
|
}
|
|
723
752
|
|
|
753
|
+
/** The stream holds a frame the keyframe overlay rebuilt (FrameMeta.fecSbnFirst). */
|
|
754
|
+
function holdsRebuilt(stream: GatedStream): boolean {
|
|
755
|
+
return stream.buf.some(({ meta }) => meta.fecSbnFirst !== undefined)
|
|
756
|
+
}
|
|
757
|
+
|
|
724
758
|
interface GatedFrame extends PendingFrame {
|
|
725
759
|
/** performance.now() at push; measures the hold against the gate's limit. */
|
|
726
760
|
arrivedAtMs: number
|
|
761
|
+
/**
|
|
762
|
+
* The unanchored path's gate is done with this frame (see #cursorPassed):
|
|
763
|
+
* it released it, or dropped it as stale.
|
|
764
|
+
*/
|
|
765
|
+
passed?: boolean
|
|
727
766
|
}
|
|
728
767
|
|
|
729
768
|
interface GatedStream {
|
|
@@ -735,6 +774,10 @@ interface GatedStream {
|
|
|
735
774
|
discontinuity: boolean
|
|
736
775
|
/** Latest (serial order) SS_ID of a complete RAP that entered the gate since the last reset. */
|
|
737
776
|
rapSsId: number | undefined
|
|
777
|
+
/** A frame that was not FEC-recovered was released since the last reset. */
|
|
778
|
+
sourced: boolean
|
|
779
|
+
/** Drains of this stream in progress: a release calls out, and back in (a stream switch's onCommit). */
|
|
780
|
+
draining: number
|
|
738
781
|
}
|
|
739
782
|
|
|
740
783
|
/**
|
|
@@ -743,9 +786,11 @@ interface GatedStream {
|
|
|
743
786
|
* to the last released frame is released immediately. A frame after a gap
|
|
744
787
|
* waits until every block between the last released frame's block and its
|
|
745
788
|
* own has resolved; a D-28 repair or a late in-band recovery can therefore
|
|
746
|
-
* still fill the gap.
|
|
747
|
-
*
|
|
748
|
-
*
|
|
789
|
+
* still fill the gap. A frame the keyframe overlay rebuilt waits only for the
|
|
790
|
+
* blocks before its MPU (FrameMeta.fecSbnFirst), and no longer than the
|
|
791
|
+
* source cursor would hold it unanchored (#cursorPassed). When the resolved
|
|
792
|
+
* blocks lost media, or authored decode time shows a missing sample, the
|
|
793
|
+
* release carries the gate's stall/conceal decision as `gapBefore`.
|
|
749
794
|
*
|
|
750
795
|
* The wait is bounded here, not by the blocks: a frame held longer than the
|
|
751
796
|
* gate's `holdLimitMs` since it arrived is released with the gate's
|
|
@@ -829,6 +874,7 @@ class FecReleaseSequencer {
|
|
|
829
874
|
stream.lastSbn = undefined
|
|
830
875
|
stream.discontinuity = false
|
|
831
876
|
stream.rapSsId = undefined
|
|
877
|
+
stream.sourced = false
|
|
832
878
|
}
|
|
833
879
|
if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
|
|
834
880
|
this.#recordStat('droppedStale')
|
|
@@ -857,6 +903,8 @@ class FecReleaseSequencer {
|
|
|
857
903
|
if (compareFrameOrder(stream.buf[mid]!, frame) < 0) lo = mid + 1
|
|
858
904
|
else hi = mid
|
|
859
905
|
}
|
|
906
|
+
// In front of a frame the unanchored path released, it was stale there.
|
|
907
|
+
if (stream.buf[lo]?.passed === true) frame.passed = true
|
|
860
908
|
stream.buf.splice(lo, 0, frame)
|
|
861
909
|
this.#drainUnlessDeferred(stream)
|
|
862
910
|
}
|
|
@@ -895,6 +943,54 @@ class FecReleaseSequencer {
|
|
|
895
943
|
return false
|
|
896
944
|
}
|
|
897
945
|
|
|
946
|
+
/**
|
|
947
|
+
* These streams' sink is being replaced by one that has seen no source
|
|
948
|
+
* frame yet. First, from a stream that holds a frame the overlay rebuilt,
|
|
949
|
+
* release to the sink being replaced the frames the unanchored path had
|
|
950
|
+
* already released to it (#cursorPassed): there they waited in its queue,
|
|
951
|
+
* which its replacement flushes. A stream that holds none is not drained:
|
|
952
|
+
* it holds nothing the anchor kept back, and a drain would still force out
|
|
953
|
+
* a frame held past the hold limit, which before the anchor waited for the
|
|
954
|
+
* next push. Then treat the streams as unsourced until a source frame is
|
|
955
|
+
* released.
|
|
956
|
+
*/
|
|
957
|
+
unsource(packetId: number, catalogTrackName: string | undefined): void {
|
|
958
|
+
for (const stream of this.#streams.values()) {
|
|
959
|
+
if (stream.packetId === packetId && (catalogTrackName === undefined || stream.catalogTrackName === catalogTrackName)) {
|
|
960
|
+
// From inside this stream's own drain (a sink replaced from a frame
|
|
961
|
+
// callback, as a stream switch's onCommit can), the drain under way
|
|
962
|
+
// releases the rest, in order, once the frame it is delivering is
|
|
963
|
+
// done (a commit delivers its held GOP and RAP after onCommit):
|
|
964
|
+
// released here, they would go ahead of them. Inside a deferral
|
|
965
|
+
// scope (a protected flow's record admits frames of several streams
|
|
966
|
+
// in one), the scope's drain does, for the same reason, into the
|
|
967
|
+
// replacement. With the stream unsourced, that drain lets a rebuilt
|
|
968
|
+
// frame go as unanchored did until it releases a source frame; a
|
|
969
|
+
// rebuilt frame behind that one waits for the blocks before it, as an
|
|
970
|
+
// anchored frame does (the record may hold the base frames it needs).
|
|
971
|
+
if (stream.draining === 0 && holdsRebuilt(stream)) {
|
|
972
|
+
if (this.#deferDepth > 0) this.#deferredStreams.add(stream)
|
|
973
|
+
else this.#drain(stream, true)
|
|
974
|
+
}
|
|
975
|
+
stream.sourced = false
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Whether a stream of `packetId` holds a frame the keyframe overlay rebuilt
|
|
982
|
+
* (FrameMeta.fecSbnFirst). Without `catalogTrackName`, every stream of the
|
|
983
|
+
* packetId.
|
|
984
|
+
*/
|
|
985
|
+
holdsRebuilt(packetId: number, catalogTrackName: string | undefined): boolean {
|
|
986
|
+
for (const stream of this.#streams.values()) {
|
|
987
|
+
if (stream.packetId !== packetId) continue
|
|
988
|
+
if (catalogTrackName !== undefined && stream.catalogTrackName !== catalogTrackName) continue
|
|
989
|
+
if (holdsRebuilt(stream)) return true
|
|
990
|
+
}
|
|
991
|
+
return false
|
|
992
|
+
}
|
|
993
|
+
|
|
898
994
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
899
995
|
poke(streamKey?: string): void {
|
|
900
996
|
if (streamKey !== undefined) {
|
|
@@ -1001,16 +1097,34 @@ class FecReleaseSequencer {
|
|
|
1001
1097
|
lastSbn: undefined,
|
|
1002
1098
|
discontinuity: false,
|
|
1003
1099
|
rapSsId: undefined,
|
|
1100
|
+
sourced: false,
|
|
1101
|
+
draining: 0,
|
|
1004
1102
|
}
|
|
1005
1103
|
this.#streams.set(streamKey, stream)
|
|
1006
1104
|
}
|
|
1007
1105
|
return stream
|
|
1008
1106
|
}
|
|
1009
1107
|
|
|
1010
|
-
#
|
|
1108
|
+
/** `asUnanchored`: release overlay-rebuilt frames when unanchored did (#cursorPassed). */
|
|
1109
|
+
#drain(stream: GatedStream, asUnanchored = false): void {
|
|
1110
|
+
stream.draining++
|
|
1111
|
+
try {
|
|
1112
|
+
this.#drainLoop(stream, asUnanchored)
|
|
1113
|
+
} finally {
|
|
1114
|
+
stream.draining--
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
#drainLoop(stream: GatedStream, asUnanchored: boolean): void {
|
|
1011
1119
|
while (stream.buf.length > 0 && stream.last !== null) {
|
|
1012
1120
|
const head = stream.buf[0]!
|
|
1013
1121
|
const last = stream.last
|
|
1122
|
+
// A second held copy of the frame just released waits on nothing.
|
|
1123
|
+
if (compareFrameOrder(head, last) <= 0) {
|
|
1124
|
+
stream.buf.shift()
|
|
1125
|
+
this.#release(stream, head, undefined)
|
|
1126
|
+
continue
|
|
1127
|
+
}
|
|
1014
1128
|
if (stream.discontinuity) {
|
|
1015
1129
|
stream.discontinuity = false
|
|
1016
1130
|
stream.buf.shift()
|
|
@@ -1024,13 +1138,19 @@ class FecReleaseSequencer {
|
|
|
1024
1138
|
}
|
|
1025
1139
|
const headSbn = head.meta.fecSbn
|
|
1026
1140
|
const from = stream.lastSbn ?? headSbn
|
|
1027
|
-
|
|
1141
|
+
// An overlay-rebuilt frame waits only for the blocks before its MPU's
|
|
1142
|
+
// first packet in that decode (fecSbnFirst): the overlay delivered the
|
|
1143
|
+
// packets from there on.
|
|
1144
|
+
const to = head.meta.fecSbnFirst ?? headSbn ?? stream.lastSbn
|
|
1028
1145
|
let lost: readonly number[] = []
|
|
1029
1146
|
// Neither side carries a Source FEC Payload ID: on a catalog-declared
|
|
1030
1147
|
// FEC track that is the trailer-stripped profile violation, and there
|
|
1031
1148
|
// are no blocks to wait on. The gap is still a gap: never release across
|
|
1032
1149
|
// it undecided (fail closed), let the gate decide with no known blocks.
|
|
1033
1150
|
const unanchored = from === undefined && to === undefined
|
|
1151
|
+
// The source cursor released it (#cursorPassed) across blocks that have
|
|
1152
|
+
// not resolved: nothing sized that gap either, so it is decided too.
|
|
1153
|
+
let unresolved = false
|
|
1034
1154
|
if (from !== undefined && to !== undefined) {
|
|
1035
1155
|
const resolved = this.#gate.resolvedLoss(
|
|
1036
1156
|
stream.packetId,
|
|
@@ -1039,7 +1159,8 @@ class FecReleaseSequencer {
|
|
|
1039
1159
|
Math.max(from, to),
|
|
1040
1160
|
from,
|
|
1041
1161
|
)
|
|
1042
|
-
|
|
1162
|
+
unresolved = resolved === null
|
|
1163
|
+
if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
|
|
1043
1164
|
const heldMs = performance.now() - head.arrivedAtMs
|
|
1044
1165
|
if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return
|
|
1045
1166
|
// Past the point where these blocks must have resolved: nothing
|
|
@@ -1049,7 +1170,7 @@ class FecReleaseSequencer {
|
|
|
1049
1170
|
this.#release(stream, head, this.#gate.decide(last, head, [], true))
|
|
1050
1171
|
continue
|
|
1051
1172
|
}
|
|
1052
|
-
lost = resolved
|
|
1173
|
+
lost = resolved ?? []
|
|
1053
1174
|
}
|
|
1054
1175
|
stream.buf.shift()
|
|
1055
1176
|
// Every gap in the frame sequence gets a decision, not only one whose
|
|
@@ -1060,19 +1181,67 @@ class FecReleaseSequencer {
|
|
|
1060
1181
|
this.#release(
|
|
1061
1182
|
stream,
|
|
1062
1183
|
head,
|
|
1063
|
-
lost.length > 0 || unanchored || authoredGap(last.meta, head.meta)
|
|
1184
|
+
lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta)
|
|
1064
1185
|
? this.#gate.decide(last, head, lost, false)
|
|
1065
1186
|
: undefined,
|
|
1066
1187
|
)
|
|
1067
1188
|
}
|
|
1068
1189
|
}
|
|
1069
1190
|
|
|
1191
|
+
/**
|
|
1192
|
+
* An overlay-rebuilt frame (fecSbnFirst) waits no longer than the BLO-53342
|
|
1193
|
+
* source cursor holds it unanchored, and neither does a later frame behind
|
|
1194
|
+
* it. Unanchored, a frame that waits on no block (fecSbnFirst set, or no
|
|
1195
|
+
* fecSbn) left the gate once every frame ahead of it had: mark it `passed`,
|
|
1196
|
+
* like each frame that arrived in front of it since (stale there). The head
|
|
1197
|
+
* then goes, decided as it would be unanchored, once a frame the base path
|
|
1198
|
+
* delivered (a source frame, or one base FEC recovered) is buffered after a
|
|
1199
|
+
* marked one: the base path has passed that one, so a frame still missing
|
|
1200
|
+
* before it can only come from a repair. It also goes while no source frame
|
|
1201
|
+
* has been released, when the direct path shows a recovered frame at once.
|
|
1202
|
+
* And it goes at once while a staged switch decides on when frames arrive
|
|
1203
|
+
* (FecReleaseGate.releaseUnanchored), and when unsource releases into a
|
|
1204
|
+
* sink being replaced: unanchored, it had already reached the sink.
|
|
1205
|
+
* The scan also writes: the `passed` marks it sets stay on the frames,
|
|
1206
|
+
* and push marks a frame it inserts in front of a marked one from them.
|
|
1207
|
+
*/
|
|
1208
|
+
#cursorPassed(stream: GatedStream, asUnanchored: boolean): boolean {
|
|
1209
|
+
let at = -1
|
|
1210
|
+
for (let i = 0; i < stream.buf.length; i++) {
|
|
1211
|
+
const frame = stream.buf[i]!
|
|
1212
|
+
if (frame.meta.fecSbnFirst !== undefined || frame.meta.fecSbn === undefined) {
|
|
1213
|
+
frame.passed = true
|
|
1214
|
+
if (at < 0) at = i
|
|
1215
|
+
} else if (frame.passed !== true) break
|
|
1216
|
+
}
|
|
1217
|
+
if (at < 0) return false
|
|
1218
|
+
return asUnanchored || !stream.sourced ||
|
|
1219
|
+
this.#gate.releaseUnanchored?.(stream.packetId, stream.catalogTrackName) === true ||
|
|
1220
|
+
stream.buf.some(({ meta }, i) =>
|
|
1221
|
+
i > at && meta.fecSbnFirst === undefined && (meta.recovered !== true || meta.fecSbn !== undefined))
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1070
1224
|
#release(stream: GatedStream, frame: PendingFrame, decision: FrameGapDecision | undefined): void {
|
|
1225
|
+
// Two copies of one frame were held (its base copy and the overlay's
|
|
1226
|
+
// rebuild) and the other went out: this one is stale, as a copy pushed
|
|
1227
|
+
// after its frame was released is. It still moves the state as a release
|
|
1228
|
+
// does: only the second emission is dropped.
|
|
1229
|
+
const stale = stream.last !== null && compareFrameOrder(frame, stream.last) <= 0
|
|
1230
|
+
if (stale) this.#recordStat('droppedStale')
|
|
1071
1231
|
if (decision) frame.meta.gapBefore = decision
|
|
1072
1232
|
stream.last = frame
|
|
1073
|
-
|
|
1233
|
+
// An overlay-rebuilt frame (fecSbnFirst) released before any base frame
|
|
1234
|
+
// since the last reset leaves lastSbn unset, as it did unanchored: the
|
|
1235
|
+
// base frame after it then waits for nothing, not for the frames between.
|
|
1236
|
+
if (frame.meta.fecSbn !== undefined && (frame.meta.fecSbnFirst === undefined || stream.lastSbn !== undefined)) {
|
|
1237
|
+
stream.lastSbn = frame.meta.fecSbn
|
|
1238
|
+
}
|
|
1239
|
+
if (frame.meta.recovered !== true) stream.sourced = true
|
|
1240
|
+
// A stale copy of the frame just released (equal order) repeats an
|
|
1241
|
+
// onReleased call already made: noteReleasedRap ignores a second RAP of
|
|
1242
|
+
// an MPU it has anchored (the same MPU and presentation time).
|
|
1074
1243
|
this.#gate.onReleased?.(frame.data, frame.meta)
|
|
1075
|
-
this.#emit(frame.data, frame.meta)
|
|
1244
|
+
if (!stale) this.#emit(frame.data, frame.meta)
|
|
1076
1245
|
}
|
|
1077
1246
|
}
|
|
1078
1247
|
|
|
@@ -1366,11 +1535,12 @@ export class MmtpContainerParser {
|
|
|
1366
1535
|
/**
|
|
1367
1536
|
* Typed view of the ISO-BMFF timing-sample budget.
|
|
1368
1537
|
*
|
|
1369
|
-
*
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1538
|
+
* Each stream admits timing against its own window of `capacity` slots
|
|
1539
|
+
* (BLO-52085). A stream at capacity displaces its own oldest timing to
|
|
1540
|
+
* admit the moof in hand, and `evicted` counts the slots so displaced:
|
|
1541
|
+
* timing a stream declared and never delivered shows up there. `pending`
|
|
1542
|
+
* sums every stream, so it can exceed `capacity`. Per repo policy this is
|
|
1543
|
+
* a producer-owned counter -- never infer budget state by parsing logs.
|
|
1374
1544
|
*/
|
|
1375
1545
|
get timingBudgetStats(): Readonly<MmtpTimingBudgetStats> {
|
|
1376
1546
|
return {
|
|
@@ -1439,6 +1609,16 @@ export class MmtpContainerParser {
|
|
|
1439
1609
|
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false
|
|
1440
1610
|
}
|
|
1441
1611
|
|
|
1612
|
+
/** See FecReleaseSequencer.unsource. */
|
|
1613
|
+
unsourceFecRelease(packetId: number, catalogTrackName?: string): void {
|
|
1614
|
+
this.#fecSequencer?.unsource(packetId, catalogTrackName)
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/** See FecReleaseSequencer.holdsRebuilt. */
|
|
1618
|
+
fecReleaseHoldsRebuilt(packetId: number, catalogTrackName?: string): boolean {
|
|
1619
|
+
return this.#fecSequencer?.holdsRebuilt(packetId, catalogTrackName) ?? false
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1442
1622
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
1443
1623
|
pokeFecRelease(packetId?: number, catalogTrackName?: string): void {
|
|
1444
1624
|
if (!this.#fecSequencer) return
|
|
@@ -1759,7 +1939,11 @@ export class MmtpContainerParser {
|
|
|
1759
1939
|
if (!resolver?.initialized) return this.#drop('fragment-no-resolver')
|
|
1760
1940
|
const pendingBefore = resolver.pendingSampleCount
|
|
1761
1941
|
const evictedBefore = resolver.evictedSampleCount
|
|
1762
|
-
|
|
1942
|
+
// Each stream's window is the whole budget. A resolver can displace only
|
|
1943
|
+
// its own timing, so a pool shared across streams let one stream's
|
|
1944
|
+
// unconsumed slots refuse another stream's moofs with nothing to
|
|
1945
|
+
// displace: BLO-52085 G7b, overlay-rebuilt video keyframes refusing audio.
|
|
1946
|
+
const remaining = this.#maxPendingTimingSamples - resolver.pendingSampleCount
|
|
1763
1947
|
const admitted = resolver.addMovieFragment(
|
|
1764
1948
|
mpu.mpuSequenceNumber,
|
|
1765
1949
|
data,
|
|
@@ -1767,11 +1951,12 @@ export class MmtpContainerParser {
|
|
|
1767
1951
|
options?.recovered === true,
|
|
1768
1952
|
)
|
|
1769
1953
|
// Sync on BOTH paths: the resolver can displace stale timing and still
|
|
1770
|
-
// refuse the fragment in hand, and
|
|
1771
|
-
// freed slots would keep refusing on a budget that is no longer full.
|
|
1954
|
+
// refuse the fragment in hand, and the total must count those freed slots.
|
|
1772
1955
|
this.#pendingTimingSamples += resolver.pendingSampleCount - pendingBefore
|
|
1773
1956
|
this.#evictedTimingSamples += resolver.evictedSampleCount - evictedBefore
|
|
1774
1957
|
if (!admitted) {
|
|
1958
|
+
// Never 'budget' with a window per stream (see
|
|
1959
|
+
// `rejectedMovieFragments`); kept as a guard.
|
|
1775
1960
|
if (resolver.lastMovieFragmentRejection === 'budget') {
|
|
1776
1961
|
this.#rejectedMovieFragments++
|
|
1777
1962
|
}
|
|
@@ -1858,6 +2043,7 @@ export class MmtpContainerParser {
|
|
|
1858
2043
|
resolver,
|
|
1859
2044
|
options?.recovered === true,
|
|
1860
2045
|
options?.fecSbn,
|
|
2046
|
+
options?.fecSbnFirst,
|
|
1861
2047
|
)
|
|
1862
2048
|
if (deferred.record === undefined) this.#countDeferredOutcome(deferred.reason)
|
|
1863
2049
|
}
|
|
@@ -1969,6 +2155,7 @@ export class MmtpContainerParser {
|
|
|
1969
2155
|
resolver,
|
|
1970
2156
|
options?.recovered === true,
|
|
1971
2157
|
options?.fecSbn,
|
|
2158
|
+
options?.fecSbnFirst,
|
|
1972
2159
|
)
|
|
1973
2160
|
if (outcome.record !== undefined) return outcome.record
|
|
1974
2161
|
return this.#drop(outcome.reason)
|
|
@@ -1995,6 +2182,7 @@ export class MmtpContainerParser {
|
|
|
1995
2182
|
resolver: IsoBmffTimingResolver,
|
|
1996
2183
|
recovered: boolean,
|
|
1997
2184
|
fecSbn: number | undefined,
|
|
2185
|
+
fecSbnFirst: number | undefined,
|
|
1998
2186
|
): CombinedSampleOutcome {
|
|
1999
2187
|
const timing = resolver.sampleTiming(
|
|
2000
2188
|
combined.mpuSequenceNumber,
|
|
@@ -2016,6 +2204,7 @@ export class MmtpContainerParser {
|
|
|
2016
2204
|
)
|
|
2017
2205
|
if (!meta) return { reason: 'combined-meta-build-failed' }
|
|
2018
2206
|
if (fecSbn !== undefined) meta.fecSbn = fecSbn
|
|
2207
|
+
if (fecSbnFirst !== undefined) meta.fecSbnFirst = fecSbnFirst
|
|
2019
2208
|
if (!resolver.consumeSample(
|
|
2020
2209
|
combined.mpuSequenceNumber,
|
|
2021
2210
|
combined.movieFragmentSequenceNumber,
|