@blockcast/mmt-container 2.2.0-main.75b39a2cdddf → 2.2.0-main.7e8a9d13c01c
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 +634 -129
- package/dist/browser-runtime.js.map +3 -3
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +37 -11
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +181 -52
- 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 +21 -0
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +114 -11
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +113 -12
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +114 -11
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +18 -9
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +254 -44
- 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 +455 -103
- package/dist/multicast-manager.js.map +2 -2
- package/dist/no-modules.js +114 -11
- 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 +114 -11
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +5 -5
- package/src/fec-manager.ts +156 -26
- package/src/index.ts +2 -0
- package/src/iso-block-locator.ts +114 -31
- package/src/mmtp-container-parser.ts +181 -10
- package/src/mmtp-router.ts +231 -58
- 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,6 +251,12 @@ 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`. */
|
|
@@ -520,6 +535,8 @@ export interface ParseHeaderOptions {
|
|
|
520
535
|
deliveryProvenance?: MmtpDeliveryProvenance
|
|
521
536
|
/** FEC source block this packet belongs to (FEC-protected packets only). */
|
|
522
537
|
fecSbn?: number
|
|
538
|
+
/** See FrameMeta.fecSbnFirst. */
|
|
539
|
+
fecSbnFirst?: number
|
|
523
540
|
}
|
|
524
541
|
|
|
525
542
|
// ── FrameSequencer ──────────────────────────────────────────────────
|
|
@@ -733,9 +750,19 @@ function ssIdAfter(a: number, b: number): boolean {
|
|
|
733
750
|
return ((a - b) | 0) > 0
|
|
734
751
|
}
|
|
735
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
|
+
|
|
736
758
|
interface GatedFrame extends PendingFrame {
|
|
737
759
|
/** performance.now() at push; measures the hold against the gate's limit. */
|
|
738
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
|
|
739
766
|
}
|
|
740
767
|
|
|
741
768
|
interface GatedStream {
|
|
@@ -747,6 +774,10 @@ interface GatedStream {
|
|
|
747
774
|
discontinuity: boolean
|
|
748
775
|
/** Latest (serial order) SS_ID of a complete RAP that entered the gate since the last reset. */
|
|
749
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
|
|
750
781
|
}
|
|
751
782
|
|
|
752
783
|
/**
|
|
@@ -755,9 +786,11 @@ interface GatedStream {
|
|
|
755
786
|
* to the last released frame is released immediately. A frame after a gap
|
|
756
787
|
* waits until every block between the last released frame's block and its
|
|
757
788
|
* own has resolved; a D-28 repair or a late in-band recovery can therefore
|
|
758
|
-
* still fill the gap.
|
|
759
|
-
*
|
|
760
|
-
*
|
|
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`.
|
|
761
794
|
*
|
|
762
795
|
* The wait is bounded here, not by the blocks: a frame held longer than the
|
|
763
796
|
* gate's `holdLimitMs` since it arrived is released with the gate's
|
|
@@ -841,6 +874,7 @@ class FecReleaseSequencer {
|
|
|
841
874
|
stream.lastSbn = undefined
|
|
842
875
|
stream.discontinuity = false
|
|
843
876
|
stream.rapSsId = undefined
|
|
877
|
+
stream.sourced = false
|
|
844
878
|
}
|
|
845
879
|
if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
|
|
846
880
|
this.#recordStat('droppedStale')
|
|
@@ -869,6 +903,8 @@ class FecReleaseSequencer {
|
|
|
869
903
|
if (compareFrameOrder(stream.buf[mid]!, frame) < 0) lo = mid + 1
|
|
870
904
|
else hi = mid
|
|
871
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
|
|
872
908
|
stream.buf.splice(lo, 0, frame)
|
|
873
909
|
this.#drainUnlessDeferred(stream)
|
|
874
910
|
}
|
|
@@ -907,6 +943,54 @@ class FecReleaseSequencer {
|
|
|
907
943
|
return false
|
|
908
944
|
}
|
|
909
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
|
+
|
|
910
994
|
/** Re-evaluate held gaps after FEC state changed. */
|
|
911
995
|
poke(streamKey?: string): void {
|
|
912
996
|
if (streamKey !== undefined) {
|
|
@@ -1013,16 +1097,34 @@ class FecReleaseSequencer {
|
|
|
1013
1097
|
lastSbn: undefined,
|
|
1014
1098
|
discontinuity: false,
|
|
1015
1099
|
rapSsId: undefined,
|
|
1100
|
+
sourced: false,
|
|
1101
|
+
draining: 0,
|
|
1016
1102
|
}
|
|
1017
1103
|
this.#streams.set(streamKey, stream)
|
|
1018
1104
|
}
|
|
1019
1105
|
return stream
|
|
1020
1106
|
}
|
|
1021
1107
|
|
|
1022
|
-
#
|
|
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 {
|
|
1023
1119
|
while (stream.buf.length > 0 && stream.last !== null) {
|
|
1024
1120
|
const head = stream.buf[0]!
|
|
1025
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
|
+
}
|
|
1026
1128
|
if (stream.discontinuity) {
|
|
1027
1129
|
stream.discontinuity = false
|
|
1028
1130
|
stream.buf.shift()
|
|
@@ -1036,13 +1138,19 @@ class FecReleaseSequencer {
|
|
|
1036
1138
|
}
|
|
1037
1139
|
const headSbn = head.meta.fecSbn
|
|
1038
1140
|
const from = stream.lastSbn ?? headSbn
|
|
1039
|
-
|
|
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
|
|
1040
1145
|
let lost: readonly number[] = []
|
|
1041
1146
|
// Neither side carries a Source FEC Payload ID: on a catalog-declared
|
|
1042
1147
|
// FEC track that is the trailer-stripped profile violation, and there
|
|
1043
1148
|
// are no blocks to wait on. The gap is still a gap: never release across
|
|
1044
1149
|
// it undecided (fail closed), let the gate decide with no known blocks.
|
|
1045
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
|
|
1046
1154
|
if (from !== undefined && to !== undefined) {
|
|
1047
1155
|
const resolved = this.#gate.resolvedLoss(
|
|
1048
1156
|
stream.packetId,
|
|
@@ -1051,7 +1159,8 @@ class FecReleaseSequencer {
|
|
|
1051
1159
|
Math.max(from, to),
|
|
1052
1160
|
from,
|
|
1053
1161
|
)
|
|
1054
|
-
|
|
1162
|
+
unresolved = resolved === null
|
|
1163
|
+
if (unresolved && !this.#cursorPassed(stream, asUnanchored)) {
|
|
1055
1164
|
const heldMs = performance.now() - head.arrivedAtMs
|
|
1056
1165
|
if (heldMs <= this.#gate.holdLimitMs(stream.packetId, stream.catalogTrackName)) return
|
|
1057
1166
|
// Past the point where these blocks must have resolved: nothing
|
|
@@ -1061,7 +1170,7 @@ class FecReleaseSequencer {
|
|
|
1061
1170
|
this.#release(stream, head, this.#gate.decide(last, head, [], true))
|
|
1062
1171
|
continue
|
|
1063
1172
|
}
|
|
1064
|
-
lost = resolved
|
|
1173
|
+
lost = resolved ?? []
|
|
1065
1174
|
}
|
|
1066
1175
|
stream.buf.shift()
|
|
1067
1176
|
// Every gap in the frame sequence gets a decision, not only one whose
|
|
@@ -1072,19 +1181,67 @@ class FecReleaseSequencer {
|
|
|
1072
1181
|
this.#release(
|
|
1073
1182
|
stream,
|
|
1074
1183
|
head,
|
|
1075
|
-
lost.length > 0 || unanchored || authoredGap(last.meta, head.meta)
|
|
1184
|
+
lost.length > 0 || unanchored || unresolved || authoredGap(last.meta, head.meta)
|
|
1076
1185
|
? this.#gate.decide(last, head, lost, false)
|
|
1077
1186
|
: undefined,
|
|
1078
1187
|
)
|
|
1079
1188
|
}
|
|
1080
1189
|
}
|
|
1081
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
|
+
|
|
1082
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')
|
|
1083
1231
|
if (decision) frame.meta.gapBefore = decision
|
|
1084
1232
|
stream.last = frame
|
|
1085
|
-
|
|
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).
|
|
1086
1243
|
this.#gate.onReleased?.(frame.data, frame.meta)
|
|
1087
|
-
this.#emit(frame.data, frame.meta)
|
|
1244
|
+
if (!stale) this.#emit(frame.data, frame.meta)
|
|
1088
1245
|
}
|
|
1089
1246
|
}
|
|
1090
1247
|
|
|
@@ -1452,6 +1609,16 @@ export class MmtpContainerParser {
|
|
|
1452
1609
|
return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false
|
|
1453
1610
|
}
|
|
1454
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
|
+
|
|
1455
1622
|
/** Re-evaluate held gaps after FEC block state changed. */
|
|
1456
1623
|
pokeFecRelease(packetId?: number, catalogTrackName?: string): void {
|
|
1457
1624
|
if (!this.#fecSequencer) return
|
|
@@ -1876,6 +2043,7 @@ export class MmtpContainerParser {
|
|
|
1876
2043
|
resolver,
|
|
1877
2044
|
options?.recovered === true,
|
|
1878
2045
|
options?.fecSbn,
|
|
2046
|
+
options?.fecSbnFirst,
|
|
1879
2047
|
)
|
|
1880
2048
|
if (deferred.record === undefined) this.#countDeferredOutcome(deferred.reason)
|
|
1881
2049
|
}
|
|
@@ -1987,6 +2155,7 @@ export class MmtpContainerParser {
|
|
|
1987
2155
|
resolver,
|
|
1988
2156
|
options?.recovered === true,
|
|
1989
2157
|
options?.fecSbn,
|
|
2158
|
+
options?.fecSbnFirst,
|
|
1990
2159
|
)
|
|
1991
2160
|
if (outcome.record !== undefined) return outcome.record
|
|
1992
2161
|
return this.#drop(outcome.reason)
|
|
@@ -2013,6 +2182,7 @@ export class MmtpContainerParser {
|
|
|
2013
2182
|
resolver: IsoBmffTimingResolver,
|
|
2014
2183
|
recovered: boolean,
|
|
2015
2184
|
fecSbn: number | undefined,
|
|
2185
|
+
fecSbnFirst: number | undefined,
|
|
2016
2186
|
): CombinedSampleOutcome {
|
|
2017
2187
|
const timing = resolver.sampleTiming(
|
|
2018
2188
|
combined.mpuSequenceNumber,
|
|
@@ -2034,6 +2204,7 @@ export class MmtpContainerParser {
|
|
|
2034
2204
|
)
|
|
2035
2205
|
if (!meta) return { reason: 'combined-meta-build-failed' }
|
|
2036
2206
|
if (fecSbn !== undefined) meta.fecSbn = fecSbn
|
|
2207
|
+
if (fecSbnFirst !== undefined) meta.fecSbnFirst = fecSbnFirst
|
|
2037
2208
|
if (!resolver.consumeSample(
|
|
2038
2209
|
combined.mpuSequenceNumber,
|
|
2039
2210
|
combined.movieFragmentSequenceNumber,
|