@blockcast/mmt-container 2.2.0-main.da2e7cd1dd5d → 2.2.0-main.e75bbd7576a6
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 +472 -36
- package/dist/browser-runtime.js.map +2 -2
- package/dist/delivery-catalog.js +2 -1
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/fec-manager.d.ts +30 -0
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +196 -2
- package/dist/fec-manager.js.map +2 -2
- 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 +9 -0
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +17 -0
- 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/mmtp-container-parser.d.ts +35 -2
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +82 -9
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +85 -11
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/mmtp-decode-pipeline.js +81 -9
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-router.d.ts +18 -1
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +276 -34
- package/dist/mmtp-router.js.map +2 -2
- package/dist/multicast-manager.js +472 -36
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.js +2 -1
- package/dist/multicast-transport.js.map +2 -2
- package/dist/no-modules.js +81 -9
- package/dist/no-modules.js.map +2 -2
- package/dist/wasm-mmtp-container-parser.js +81 -9
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +5 -5
- package/src/fec-manager.ts +251 -2
- package/src/index.ts +1 -0
- package/src/iso-block-locator.ts +19 -0
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +84 -8
- package/src/mmtp-router.ts +346 -30
|
@@ -365,10 +365,19 @@ export type MmtpFeedDropReason =
|
|
|
365
365
|
/** No timing resolver, or it refused the initialization segment. */
|
|
366
366
|
| 'init-timing-refused'
|
|
367
367
|
| 'fragment-unexpected-fragmentation'
|
|
368
|
+
/** FT=1 before an FT=0 has bound the stream's timing track. */
|
|
368
369
|
| 'fragment-no-resolver'
|
|
369
370
|
/** addMovieFragment refused — includes the pending-timing-sample budget. */
|
|
370
371
|
| 'fragment-timing-refused'
|
|
371
372
|
| 'unknown-fragment-type'
|
|
373
|
+
/** Stable code, no longer produced: an FT=2 with no timing (before the
|
|
374
|
+
* stream's first FT=0, or after setTrack dropped it) now waits for its
|
|
375
|
+
* timing like one whose FT=1 has not arrived. A head counts as
|
|
376
|
+
* `drop:mfu-sample-timing-missing` (parked) and a FI=2/FI=3 continuation
|
|
377
|
+
* as the `pending` record. A continuation that completes a head the
|
|
378
|
+
* reassembler buffered before the timing was dropped counts as
|
|
379
|
+
* `drop:assembled-timing-missing`. A panel keyed on this code reads zero:
|
|
380
|
+
* a re-attribution, not lost coverage. Re-key on both drop codes. */
|
|
372
381
|
| 'mfu-no-resolver'
|
|
373
382
|
| 'mfu-empty-payload'
|
|
374
383
|
| 'mfu-du-header-missing'
|
|
@@ -679,7 +688,10 @@ class FrameSequencer {
|
|
|
679
688
|
// ── FecReleaseSequencer ─────────────────────────────────────────────
|
|
680
689
|
|
|
681
690
|
/** Authored decode-time adjacency: the next frame starts where this one ends. */
|
|
682
|
-
function authoredAdjacent(
|
|
691
|
+
export function authoredAdjacent(
|
|
692
|
+
before: Pick<FrameMeta, 'authoredTiming'>,
|
|
693
|
+
after: Pick<FrameMeta, 'authoredTiming'>,
|
|
694
|
+
): boolean {
|
|
683
695
|
const a = before.authoredTiming
|
|
684
696
|
const b = after.authoredTiming
|
|
685
697
|
if (!a || !b || a.mediaTimescale !== b.mediaTimescale) return false
|
|
@@ -1089,9 +1101,30 @@ interface AwaitingTimingHead {
|
|
|
1089
1101
|
mpuSequenceNumber: number
|
|
1090
1102
|
movieFragmentSequenceNumber: number
|
|
1091
1103
|
sampleNumber: number
|
|
1104
|
+
/** MMTP packet sequence number; a recovered copy carries its source's. */
|
|
1105
|
+
sequenceNumber: number
|
|
1092
1106
|
arrivedAt: number
|
|
1093
1107
|
}
|
|
1094
1108
|
|
|
1109
|
+
/** FEC-recovered heads that went through the awaiting-timing queue. Every
|
|
1110
|
+
* count but `pending` is cumulative. A held head is pending or has left the
|
|
1111
|
+
* queue exactly once, so `held - expired - discarded - superseded - pending`
|
|
1112
|
+
* is the number fed again once their FT=1 landed. That feed can still refuse
|
|
1113
|
+
* one (size, offset, dispose from a frame callback); the refusal is counted in
|
|
1114
|
+
* `feedStats.deferredOutcomes`, not here. */
|
|
1115
|
+
export interface MmtpRecoveredHeadTimingStats {
|
|
1116
|
+
/** Held because their sample's FT=1 had not arrived yet. */
|
|
1117
|
+
held: number
|
|
1118
|
+
/** Left without timing: the reassembler-timeout deadline, or pushed out by the queue's capacity. */
|
|
1119
|
+
expired: number
|
|
1120
|
+
/** Dropped with their stream (track reconfigure, reset or retire) or by dispose(). */
|
|
1121
|
+
discarded: number
|
|
1122
|
+
/** Gave way to their arriving source copy, which takes their place in the queue. */
|
|
1123
|
+
superseded: number
|
|
1124
|
+
/** In the queue now; current occupancy, not cumulative. */
|
|
1125
|
+
pending: number
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1095
1128
|
const MAX_AWAITING_TIMING_HEADS = 8
|
|
1096
1129
|
|
|
1097
1130
|
// ── MmtpContainerParser ─────────────────────────────────────────────
|
|
@@ -1129,6 +1162,9 @@ export class MmtpContainerParser {
|
|
|
1129
1162
|
#reclaimedTimingSamples = 0
|
|
1130
1163
|
#evictedTimingSamples = 0
|
|
1131
1164
|
#rejectedMovieFragments = 0
|
|
1165
|
+
#recoveredHeadsAwaitingTiming: Omit<MmtpRecoveredHeadTimingStats, 'pending'> = {
|
|
1166
|
+
held: 0, expired: 0, discarded: 0, superseded: 0,
|
|
1167
|
+
}
|
|
1132
1168
|
#sequencers = new Map<string, { packetId: number; sequencer: FrameSequencer }>()
|
|
1133
1169
|
#fecGate: FecReleaseGate | null = null
|
|
1134
1170
|
#fecSequencer: FecReleaseSequencer | null = null
|
|
@@ -1346,6 +1382,15 @@ export class MmtpContainerParser {
|
|
|
1346
1382
|
}
|
|
1347
1383
|
}
|
|
1348
1384
|
|
|
1385
|
+
/** Recovered heads parked for FT=1 timing (BLO-53369). Survives dispose(). */
|
|
1386
|
+
get recoveredHeadsAwaitingTiming(): Readonly<MmtpRecoveredHeadTimingStats> {
|
|
1387
|
+
let pending = 0
|
|
1388
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
1389
|
+
for (const head of awaitingTiming) if (head.options?.recovered) pending++
|
|
1390
|
+
}
|
|
1391
|
+
return { ...this.#recoveredHeadsAwaitingTiming, pending }
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1349
1394
|
get reorderStats(): Readonly<MmtpReorderStats> {
|
|
1350
1395
|
let bufferedFrames = this.#fecSequencer?.buffered ?? 0
|
|
1351
1396
|
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered
|
|
@@ -1541,7 +1586,7 @@ export class MmtpContainerParser {
|
|
|
1541
1586
|
const now = Date.now()
|
|
1542
1587
|
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
1543
1588
|
while (awaitingTiming.length > 0 && now - awaitingTiming[0]!.arrivedAt > timeoutMs) {
|
|
1544
|
-
awaitingTiming.shift()
|
|
1589
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift()!, 'expired')
|
|
1545
1590
|
}
|
|
1546
1591
|
}
|
|
1547
1592
|
}
|
|
@@ -1711,11 +1756,16 @@ export class MmtpContainerParser {
|
|
|
1711
1756
|
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
1712
1757
|
return this.#drop('fragment-unexpected-fragmentation')
|
|
1713
1758
|
const resolver = this.#timingResolver(streamKey, info.packetId, false)
|
|
1714
|
-
if (!resolver) return this.#drop('fragment-no-resolver')
|
|
1759
|
+
if (!resolver?.initialized) return this.#drop('fragment-no-resolver')
|
|
1715
1760
|
const pendingBefore = resolver.pendingSampleCount
|
|
1716
1761
|
const evictedBefore = resolver.evictedSampleCount
|
|
1717
1762
|
const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples
|
|
1718
|
-
const admitted = resolver.addMovieFragment(
|
|
1763
|
+
const admitted = resolver.addMovieFragment(
|
|
1764
|
+
mpu.mpuSequenceNumber,
|
|
1765
|
+
data,
|
|
1766
|
+
remaining,
|
|
1767
|
+
options?.recovered === true,
|
|
1768
|
+
)
|
|
1719
1769
|
// Sync on BOTH paths: the resolver can displace stale timing and still
|
|
1720
1770
|
// refuse the fragment in hand, and a global count that missed those
|
|
1721
1771
|
// freed slots would keep refusing on a budget that is no longer full.
|
|
@@ -1739,7 +1789,11 @@ export class MmtpContainerParser {
|
|
|
1739
1789
|
}
|
|
1740
1790
|
if (fragmentType !== 2) return this.#drop('unknown-fragment-type')
|
|
1741
1791
|
|
|
1742
|
-
|
|
1792
|
+
// An FT=2 before the stream's first FT=0 (a join, a track reset) is taken
|
|
1793
|
+
// like one whose FT=1 has not arrived: a head waits for its timing, a
|
|
1794
|
+
// continuation parks in the reassembler, and a later or FEC-recovered
|
|
1795
|
+
// FT=0/FT=1 can still time them.
|
|
1796
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true)
|
|
1743
1797
|
if (!resolver) return this.#drop('mfu-no-resolver')
|
|
1744
1798
|
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
1745
1799
|
return this.#drop('mfu-empty-payload')
|
|
@@ -1761,6 +1815,7 @@ export class MmtpContainerParser {
|
|
|
1761
1815
|
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
1762
1816
|
movieFragmentSequenceNumber: mfuDuHeader.movieFragmentSequenceNumber,
|
|
1763
1817
|
sampleNumber: mfuDuHeader.sampleNumber,
|
|
1818
|
+
sequenceNumber: info.sequenceNumber,
|
|
1764
1819
|
arrivedAt: Date.now(),
|
|
1765
1820
|
})
|
|
1766
1821
|
return this.#drop('mfu-sample-timing-missing')
|
|
@@ -2228,6 +2283,9 @@ export class MmtpContainerParser {
|
|
|
2228
2283
|
for (const { sequencer } of this.#sequencers.values()) sequencer.discard()
|
|
2229
2284
|
this.#sequencers.clear()
|
|
2230
2285
|
this.#fecSequencer?.discard()
|
|
2286
|
+
for (const { awaitingTiming } of this.#timingStreams.values()) {
|
|
2287
|
+
for (const head of awaitingTiming) this.#leaveAwaitingTiming(head, 'discarded')
|
|
2288
|
+
}
|
|
2231
2289
|
this.#timingStreams.clear()
|
|
2232
2290
|
this.#pendingTimingSamples = 0
|
|
2233
2291
|
this.#reliableMoqStartupGate.clear()
|
|
@@ -2253,10 +2311,27 @@ export class MmtpContainerParser {
|
|
|
2253
2311
|
}
|
|
2254
2312
|
|
|
2255
2313
|
#awaitTiming(streamKey: string, head: AwaitingTimingHead): void {
|
|
2256
|
-
const
|
|
2257
|
-
if (!
|
|
2314
|
+
const state = this.#timingStreams.get(streamKey)
|
|
2315
|
+
if (!state) return
|
|
2316
|
+
// The source copy of a held recovered head takes its place, so the frame
|
|
2317
|
+
// is delivered with source provenance once FT=1 lands.
|
|
2318
|
+
if (!head.options?.recovered) {
|
|
2319
|
+
const kept = state.awaitingTiming.filter(queued =>
|
|
2320
|
+
!queued.options?.recovered || queued.sequenceNumber !== head.sequenceNumber)
|
|
2321
|
+
this.#recoveredHeadsAwaitingTiming.superseded += state.awaitingTiming.length - kept.length
|
|
2322
|
+
state.awaitingTiming = kept
|
|
2323
|
+
}
|
|
2324
|
+
const awaitingTiming = state.awaitingTiming
|
|
2258
2325
|
awaitingTiming.push(head)
|
|
2259
|
-
if (
|
|
2326
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming.held++
|
|
2327
|
+
if (awaitingTiming.length > MAX_AWAITING_TIMING_HEADS) {
|
|
2328
|
+
this.#leaveAwaitingTiming(awaitingTiming.shift()!, 'expired')
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
/** Count a recovered head leaving the queue other than by replay or its source copy. */
|
|
2333
|
+
#leaveAwaitingTiming(head: AwaitingTimingHead, outcome: 'expired' | 'discarded'): void {
|
|
2334
|
+
if (head.options?.recovered) this.#recoveredHeadsAwaitingTiming[outcome]++
|
|
2260
2335
|
}
|
|
2261
2336
|
|
|
2262
2337
|
/**
|
|
@@ -2286,6 +2361,7 @@ export class MmtpContainerParser {
|
|
|
2286
2361
|
const state = this.#timingStreams.get(streamKey)
|
|
2287
2362
|
if (!state) return
|
|
2288
2363
|
this.#timingStreams.delete(streamKey)
|
|
2364
|
+
for (const head of state.awaitingTiming) this.#leaveAwaitingTiming(head, 'discarded')
|
|
2289
2365
|
this.#pendingTimingSamples -= state.resolver.pendingSampleCount
|
|
2290
2366
|
if (this.#pendingTimingSamples < 0) {
|
|
2291
2367
|
throw new Error('[MmtpContainerParser] pending timing sample accounting underflow')
|