@blockcast/mmt-container 2.2.0-main.efb4fa4521af → 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/README.md +29 -17
- package/dist/browser-runtime.js +1336 -302
- package/dist/browser-runtime.js.map +3 -3
- package/dist/catalog-fec-params.d.ts +17 -1
- 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/cmcd-producer.d.ts +60 -47
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js +138 -109
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +26 -16
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +70 -12
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +403 -58
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-worker-setup.d.ts +1 -1
- package/dist/fec-worker-setup.js +1 -1
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.d.ts +48 -13
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +97 -25
- package/dist/iso-block-locator.js.map +2 -2
- package/dist/iso-bmff-timing.d.ts +10 -1
- package/dist/iso-bmff-timing.d.ts.map +1 -1
- package/dist/iso-bmff-timing.js +36 -2
- package/dist/iso-bmff-timing.js.map +2 -2
- package/dist/media-router.js.map +1 -1
- package/dist/mmtp-container-parser.d.ts +79 -10
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +207 -26
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +209 -29
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +206 -26
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +34 -8
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +555 -79
- 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 +980 -146
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +28 -18
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.js +206 -26
- 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/repair-layer-subscriptions.d.ts +6 -2
- package/dist/repair-layer-subscriptions.d.ts.map +1 -1
- package/dist/repair-layer-subscriptions.js +6 -3
- package/dist/repair-layer-subscriptions.js.map +2 -2
- package/dist/track-session.d.ts +1 -1
- 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 +206 -26
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +8 -5
- package/src/catalog-fec-params.ts +36 -1
- package/src/cmcd-producer.ts +189 -179
- package/src/fec-manager.ts +433 -32
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +9 -2
- package/src/iso-block-locator.ts +133 -31
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +306 -29
- package/src/mmtp-router.ts +548 -38
- package/src/multicast-manager.ts +44 -6
- package/src/repair-fetch.ts +363 -49
- package/src/repair-layer-subscriptions.ts +13 -5
- package/src/track-session.ts +4 -1
package/src/fec-worker-setup.ts
CHANGED
|
@@ -64,7 +64,7 @@ export interface FecWorkerSetupOptions {
|
|
|
64
64
|
/**
|
|
65
65
|
* Repair symbols per block this receiver actually joined: the sum of
|
|
66
66
|
* `repairSymbols` over the joined BASE layers (draft-ramadan-moq-fec 6.3).
|
|
67
|
-
* Reported as P by the worker snapshot and CMCD `
|
|
67
|
+
* Reported as P by the worker snapshot and CMCD `com.blockcast-fp`, because the
|
|
68
68
|
* catalog total is not what this receiver joined. Falls back to
|
|
69
69
|
* `fecRepairCount`, which is the same value on a single-layer track.
|
|
70
70
|
*/
|
|
@@ -252,7 +252,7 @@ export async function setupFecWorkerForTrack(
|
|
|
252
252
|
blockProfile,
|
|
253
253
|
k: opts.fecK,
|
|
254
254
|
// P as this receiver joined it, not the catalog total: the worker snapshot
|
|
255
|
-
// and CMCD `
|
|
255
|
+
// and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
|
|
256
256
|
repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
|
|
257
257
|
symbolSize: opts.fecSymbolSize,
|
|
258
258
|
interleaveDepth: interleaveDepthBlocks,
|
package/src/index.ts
CHANGED
|
@@ -260,6 +260,8 @@ export {
|
|
|
260
260
|
type FecStats,
|
|
261
261
|
type MulticastSymbolDelivery,
|
|
262
262
|
type UndelimitedIsoLoss,
|
|
263
|
+
type ExpiredIsoHole,
|
|
264
|
+
type IsoSpanFailure,
|
|
263
265
|
type FecBlockResolution,
|
|
264
266
|
type RepairVia,
|
|
265
267
|
type FecRepairLayer,
|
|
@@ -299,6 +301,7 @@ export {
|
|
|
299
301
|
type MmtpFeedOutcomeKey,
|
|
300
302
|
type TrackInfo,
|
|
301
303
|
type MmtpReorderStats,
|
|
304
|
+
type MmtpRecoveredHeadTimingStats,
|
|
302
305
|
type MmtpReorderFrameDiagnostics,
|
|
303
306
|
type MmtpReorderStreamDiagnostics,
|
|
304
307
|
type FrameMeta as ParserFrameMeta,
|
|
@@ -526,10 +529,13 @@ export {
|
|
|
526
529
|
type FecStarvationDetectorConfig,
|
|
527
530
|
} from './fec-starvation-detector.js'
|
|
528
531
|
|
|
529
|
-
// CMCD
|
|
532
|
+
// CMCD v2 Event-mode producer (CTA-5004-B, com.blockcast-* custom keys)
|
|
530
533
|
export {
|
|
531
534
|
CmcdProducer,
|
|
532
|
-
|
|
535
|
+
encodeCmcdRecord,
|
|
536
|
+
type CmcdRecord,
|
|
537
|
+
type CmcdToken,
|
|
538
|
+
type CmcdValue,
|
|
533
539
|
type CmcdProducerOptions,
|
|
534
540
|
type SnapshotData,
|
|
535
541
|
type TransportSnapshot,
|
|
@@ -546,6 +552,7 @@ export {
|
|
|
546
552
|
type TrackFecParams,
|
|
547
553
|
type TrackFecUnavailable,
|
|
548
554
|
catalogTrackFecUnavailable,
|
|
555
|
+
catalogTrackDeliveryMode,
|
|
549
556
|
catalogGroupDurationMs,
|
|
550
557
|
catalogKeyframeCadenceMs,
|
|
551
558
|
catalogFecMediaUnitsPerSecond,
|
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
|
|
@@ -85,8 +101,18 @@ export class IsoBlockLocator<T> {
|
|
|
85
101
|
#retentionBlocks: number
|
|
86
102
|
#heldEvicted = 0
|
|
87
103
|
#newest: number | undefined
|
|
104
|
+
#newestSource: number | undefined
|
|
88
105
|
/** Pending holes (see `deferIncompleteGaps`): unstated, sources still held. */
|
|
89
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[] = []
|
|
90
116
|
/**
|
|
91
117
|
* Keep a gap that is missing a source as a pending hole rather than a span,
|
|
92
118
|
* so a repair FETCHed for it (draft-ramadan-moq-fec §7.5) can still state
|
|
@@ -152,6 +178,23 @@ export class IsoBlockLocator<T> {
|
|
|
152
178
|
if (this.#newest === undefined || serial32Precedes(this.#newest, position)) this.#newest = position
|
|
153
179
|
}
|
|
154
180
|
|
|
181
|
+
/**
|
|
182
|
+
* The newest source SS_ID noted with `noteSource` (RFC 1982 order). Unlike
|
|
183
|
+
* `newest`, a repair never moves it: a repair can run ahead of its sources,
|
|
184
|
+
* a source cannot run ahead of the sources sent before it except by
|
|
185
|
+
* reordering.
|
|
186
|
+
*/
|
|
187
|
+
get newestSource(): number | undefined {
|
|
188
|
+
return this.#newestSource
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Note a received source's SS_ID; true when it is the newest one yet. */
|
|
192
|
+
noteSource(ssId: number): boolean {
|
|
193
|
+
if (this.#newestSource !== undefined && !serial32Precedes(this.#newestSource, ssId)) return false
|
|
194
|
+
this.#newestSource = ssId
|
|
195
|
+
return true
|
|
196
|
+
}
|
|
197
|
+
|
|
155
198
|
/** Held sources evicted by `heldCapacity` (never by the horizon itself). */
|
|
156
199
|
get heldEvicted(): number {
|
|
157
200
|
return this.#heldEvicted
|
|
@@ -174,18 +217,31 @@ export class IsoBlockLocator<T> {
|
|
|
174
217
|
|
|
175
218
|
/** The known block or span containing `ssId`, with its ESI. */
|
|
176
219
|
locate(ssId: number): { block: IsoBlockRange; esi: number } | null {
|
|
177
|
-
for (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
+
}
|
|
181
226
|
}
|
|
182
227
|
}
|
|
183
228
|
return null
|
|
184
229
|
}
|
|
185
230
|
|
|
186
|
-
/** Number of pending
|
|
231
|
+
/** Number of holes not closed yet, pending or past the horizon. */
|
|
187
232
|
get holeCount(): number {
|
|
188
|
-
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)
|
|
189
245
|
}
|
|
190
246
|
|
|
191
247
|
/** The stated block (never a span) ending exactly at `end`. */
|
|
@@ -250,9 +306,11 @@ export class IsoBlockLocator<T> {
|
|
|
250
306
|
/**
|
|
251
307
|
* Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
|
|
252
308
|
* block states its range and places every held source in it; later repair
|
|
253
|
-
* symbols of the same block change nothing. A range
|
|
254
|
-
*
|
|
255
|
-
*
|
|
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.
|
|
256
314
|
* A newly stated block retires behind its SS_Start (see the class note).
|
|
257
315
|
* A block so far past `newest` that stating it would retire the position
|
|
258
316
|
* `newest` is refused as `ahead`. When `newest` is a placed block's SS_Start
|
|
@@ -271,7 +329,25 @@ export class IsoBlockLocator<T> {
|
|
|
271
329
|
const known = this.blockAt(start)
|
|
272
330
|
if (known !== undefined) {
|
|
273
331
|
if (known.span) {
|
|
274
|
-
|
|
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}` }
|
|
275
351
|
}
|
|
276
352
|
if (known.length !== length) {
|
|
277
353
|
return {
|
|
@@ -349,18 +425,28 @@ export class IsoBlockLocator<T> {
|
|
|
349
425
|
}
|
|
350
426
|
|
|
351
427
|
/**
|
|
352
|
-
* Turn every
|
|
353
|
-
*
|
|
354
|
-
*
|
|
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.
|
|
355
434
|
*/
|
|
356
435
|
closeHolesWithin(start: number, length: number): { block: IsoBlockRange; placed: IsoPlacedSource<T>[] }[] {
|
|
357
436
|
const closed: { block: IsoBlockRange; placed: IsoPlacedSource<T>[] }[] = []
|
|
358
|
-
for (const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
+
}
|
|
364
450
|
}
|
|
365
451
|
return closed.sort((x, y) => serial32Distance(start, x.block.start) - serial32Distance(start, y.block.start))
|
|
366
452
|
}
|
|
@@ -377,15 +463,17 @@ export class IsoBlockLocator<T> {
|
|
|
377
463
|
}
|
|
378
464
|
|
|
379
465
|
/**
|
|
380
|
-
* Retire every block and span whose last SS_ID, and every held source
|
|
381
|
-
* lies more than `retentionBlocks` blocks behind
|
|
382
|
-
* lower-bound metric (`isoBlocksBehindLowerBound`)
|
|
383
|
-
* 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.
|
|
384
471
|
*/
|
|
385
472
|
retireBehind(position: number): number[] {
|
|
386
473
|
this.observe(position)
|
|
387
474
|
const horizon = this.#retentionBlocks
|
|
388
475
|
const retired: number[] = []
|
|
476
|
+
this.#closedBehind.length = 0
|
|
389
477
|
for (let i = this.#blocks.length - 1; i >= 0; i--) {
|
|
390
478
|
const block = this.#blocks[i]!
|
|
391
479
|
const last = serial32Add(block.start, block.length - 1)
|
|
@@ -397,10 +485,20 @@ export class IsoBlockLocator<T> {
|
|
|
397
485
|
for (let i = this.#holes.length - 1; i >= 0; i--) {
|
|
398
486
|
const hole = this.#holes[i]!
|
|
399
487
|
const last = serial32Add(hole.start, hole.length - 1)
|
|
400
|
-
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)
|
|
401
492
|
}
|
|
493
|
+
// A held source stays while a hole holding it does (class note).
|
|
402
494
|
for (const ssId of [...this.#held.keys()]) {
|
|
403
|
-
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
|
+
}
|
|
404
502
|
}
|
|
405
503
|
return retired
|
|
406
504
|
}
|
|
@@ -409,9 +507,13 @@ export class IsoBlockLocator<T> {
|
|
|
409
507
|
reset(): void {
|
|
410
508
|
this.#blocks.length = 0
|
|
411
509
|
this.#holes.length = 0
|
|
510
|
+
this.#expired.length = 0
|
|
511
|
+
this.#newlyExpired.length = 0
|
|
512
|
+
this.#closedBehind.length = 0
|
|
412
513
|
this.#held.clear()
|
|
413
514
|
this.#coverageEnd = undefined
|
|
414
515
|
this.#newest = undefined
|
|
516
|
+
this.#newestSource = undefined
|
|
415
517
|
}
|
|
416
518
|
|
|
417
519
|
#claim(block: IsoBlockRange): IsoPlacedSource<T>[] {
|
package/src/iso-bmff-timing.ts
CHANGED
|
@@ -581,6 +581,14 @@ type PendingMovieFragment = Array<IsoBmffSampleTiming | null>
|
|
|
581
581
|
export class IsoBmffTimingResolver {
|
|
582
582
|
#track: IsoBmffTrackTiming | null = null
|
|
583
583
|
#fragments = new Map<string, PendingMovieFragment>()
|
|
584
|
+
/** First sample of each pending fragment, kept in `#delivered` with its sample count. */
|
|
585
|
+
#firstSamples = new Map<string, IsoBmffSampleTiming>()
|
|
586
|
+
/**
|
|
587
|
+
* First sample and sample count of the newest fragments whose every slot was
|
|
588
|
+
* cleared. Abandonment counts: `releaseSample` clears a slot too, which
|
|
589
|
+
* matches the combiner, since it retires an abandoned sample as well.
|
|
590
|
+
*/
|
|
591
|
+
#delivered = new Map<string, { first: IsoBmffSampleTiming, sampleCount: number }>()
|
|
584
592
|
#pendingSamples = 0
|
|
585
593
|
#evictedSamples = 0
|
|
586
594
|
#lastMovieFragmentRejection: IsoBmffMovieFragmentRejection | null = null
|
|
@@ -591,6 +599,11 @@ export class IsoBmffTimingResolver {
|
|
|
591
599
|
}
|
|
592
600
|
}
|
|
593
601
|
|
|
602
|
+
/** True once an FT=0 has bound the track that FT=1 timing is read against. */
|
|
603
|
+
get initialized(): boolean {
|
|
604
|
+
return this.#track !== null
|
|
605
|
+
}
|
|
606
|
+
|
|
594
607
|
get pendingSampleCount(): number {
|
|
595
608
|
return this.#pendingSamples
|
|
596
609
|
}
|
|
@@ -618,10 +631,18 @@ export class IsoBmffTimingResolver {
|
|
|
618
631
|
return true
|
|
619
632
|
}
|
|
620
633
|
|
|
634
|
+
/**
|
|
635
|
+
* `recovered` marks an FEC-recovered copy. A recovered block hands back
|
|
636
|
+
* every FT=1 it holds, the received ones included, so a recovered copy of a
|
|
637
|
+
* fragment whose samples are all done is a duplicate: admitting it again
|
|
638
|
+
* would charge stale timing and could displace the live MPU's. A source
|
|
639
|
+
* copy is still admitted again (a rendition switch re-arms timing that way).
|
|
640
|
+
*/
|
|
621
641
|
addMovieFragment(
|
|
622
642
|
mpuSequenceNumber: number,
|
|
623
643
|
data: Uint8Array,
|
|
624
644
|
additionalSampleLimit = this.maxPendingSamples,
|
|
645
|
+
recovered = false,
|
|
625
646
|
): boolean {
|
|
626
647
|
this.#lastMovieFragmentRejection = null
|
|
627
648
|
if (!Number.isInteger(additionalSampleLimit) || additionalSampleLimit < 0) {
|
|
@@ -648,6 +669,9 @@ export class IsoBmffTimingResolver {
|
|
|
648
669
|
(sample, index) => sample !== null && sameSample(sample, parsed.samples[index]!),
|
|
649
670
|
)
|
|
650
671
|
}
|
|
672
|
+
const delivered = recovered ? this.#delivered.get(key) : undefined
|
|
673
|
+
if (delivered && delivered.sampleCount === parsed.samples.length &&
|
|
674
|
+
sameSample(delivered.first, parsed.samples[0]!)) return true
|
|
651
675
|
// Sliding window, not a one-way ratchet. `consumeSample` and
|
|
652
676
|
// `releaseSample` between them cover every sample some component observed
|
|
653
677
|
// -- but a sample whose data units never arrive at all is observed by
|
|
@@ -683,6 +707,7 @@ export class IsoBmffTimingResolver {
|
|
|
683
707
|
return false
|
|
684
708
|
}
|
|
685
709
|
this.#fragments.set(key, [...parsed.samples])
|
|
710
|
+
this.#firstSamples.set(key, parsed.samples[0]!)
|
|
686
711
|
this.#pendingSamples += parsed.samples.length
|
|
687
712
|
return true
|
|
688
713
|
}
|
|
@@ -758,6 +783,7 @@ export class IsoBmffTimingResolver {
|
|
|
758
783
|
let live = 0
|
|
759
784
|
for (const sample of samples) if (sample !== null) live++
|
|
760
785
|
this.#fragments.delete(oldest)
|
|
786
|
+
this.#firstSamples.delete(oldest)
|
|
761
787
|
this.#pendingSamples -= live
|
|
762
788
|
this.#evictedSamples += live
|
|
763
789
|
return live
|
|
@@ -779,13 +805,26 @@ export class IsoBmffTimingResolver {
|
|
|
779
805
|
if (!samples || !samples[index]) return false
|
|
780
806
|
samples[index] = null
|
|
781
807
|
this.#pendingSamples--
|
|
782
|
-
if (samples.every(value => value === null))
|
|
808
|
+
if (samples.every(value => value === null)) {
|
|
809
|
+
this.#fragments.delete(key)
|
|
810
|
+
const first = this.#firstSamples.get(key)!
|
|
811
|
+
this.#firstSamples.delete(key)
|
|
812
|
+
this.#delivered.delete(key)
|
|
813
|
+
this.#delivered.set(key, { first, sampleCount: samples.length })
|
|
814
|
+
// ponytail: the dedupe memory is as wide as the timing budget, in fragments;
|
|
815
|
+
// a recovered copy older than that is admitted again, as before.
|
|
816
|
+
if (this.#delivered.size > this.maxPendingSamples) {
|
|
817
|
+
this.#delivered.delete(this.#delivered.keys().next().value!)
|
|
818
|
+
}
|
|
819
|
+
}
|
|
783
820
|
return true
|
|
784
821
|
}
|
|
785
822
|
|
|
786
823
|
reset(): void {
|
|
787
824
|
this.#track = null
|
|
788
825
|
this.#fragments.clear()
|
|
826
|
+
this.#firstSamples.clear()
|
|
827
|
+
this.#delivered.clear()
|
|
789
828
|
this.#pendingSamples = 0
|
|
790
829
|
}
|
|
791
830
|
}
|