@blockcast/mmt-container 2.1.0 → 2.2.0-main.050b52fb9b00
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 +1949 -374
- package/dist/browser-runtime.js.map +4 -4
- package/dist/carp-catalog.js.map +1 -1
- 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/catalog-network-source.js.map +1 -1
- package/dist/cmcd-producer.d.ts +65 -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-state.js.map +1 -1
- 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/held-layer-admission.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -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/mfu-reassembler.js +12 -19
- package/dist/mfu-reassembler.js.map +2 -2
- 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 +219 -45
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +342 -63
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +218 -45
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-fec-session.d.ts +41 -3
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +118 -13
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +43 -9
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +2006 -95
- package/dist/mmtp-router.js.map +4 -4
- package/dist/multicast-auth-catalog.js.map +1 -1
- package/dist/multicast-input.js.map +1 -1
- package/dist/multicast-manager.d.ts +28 -5
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +16116 -15005
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +11 -3
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +281 -35
- package/dist/multicast-transport.js.map +4 -4
- package/dist/multicast-unwrap.js +12 -19
- package/dist/multicast-unwrap.js.map +2 -2
- package/dist/no-modules.js +336 -59
- package/dist/no-modules.js.map +3 -3
- 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 +82 -0
- package/dist/repair-layer-subscriptions.d.ts.map +1 -0
- package/dist/repair-layer-subscriptions.js +192 -0
- package/dist/repair-layer-subscriptions.js.map +7 -0
- package/dist/signaling.js.map +2 -2
- package/dist/timestamp-codec.js +12 -19
- package/dist/timestamp-codec.js.map +2 -2
- package/dist/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-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +218 -45
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +8 -5
- package/schemas/catalog.schema.json +4 -0
- package/src/catalog-fec-params.ts +36 -1
- package/src/cmcd-producer.ts +194 -179
- package/src/fec-manager.ts +433 -32
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +20 -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-fec-session.ts +115 -15
- package/src/mmtp-router.ts +629 -54
- package/src/multicast-manager.ts +70 -11
- package/src/multicast-transport.ts +44 -20
- package/src/repair-fetch.ts +363 -49
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +4 -1
package/src/repair-fetch.ts
CHANGED
|
@@ -41,6 +41,18 @@
|
|
|
41
41
|
* or otherwise never sent cancels nothing (§11.4.5 item 3), and an audio
|
|
42
42
|
* request never preempts. A P2 that never ran before its block resolved
|
|
43
43
|
* is reported as `repair-deferred`.
|
|
44
|
+
* - A hole walked over the one-Group transport (no `rangeTransport`) is
|
|
45
|
+
* repair the run gets before its FEC deadline (`walkHole`). On a video
|
|
46
|
+
* track it is keyframe repair (class `unknown`): P0, and no audio or
|
|
47
|
+
* delta FETCH is issued while one of its Group FETCHes is outstanding
|
|
48
|
+
* (fec-multicast-00 rule 3). It reaches no FEC deadline, so it preempts
|
|
49
|
+
* nothing and holds back no audio or delta request: one that comes due
|
|
50
|
+
* cancels the walk's Group FETCH and goes out at once, and the walk asks
|
|
51
|
+
* for that Group again after it (`yieldVideoWalks`). On an audio track
|
|
52
|
+
* it is audio repair: P1 at the audio wire priority, one Group per FETCH,
|
|
53
|
+
* and like any audio request it waits while keyframe repair is
|
|
54
|
+
* outstanding, stopping before its next Group, and is preempted by a
|
|
55
|
+
* keyframe FETCH sent.
|
|
44
56
|
* - A block reclassified to a higher class while its P2 fetch is in flight
|
|
45
57
|
* is re-issued at once under the new class's wire priority: the running
|
|
46
58
|
* FETCH is aborted (FETCH_CANCEL) and a new one sent.
|
|
@@ -100,21 +112,35 @@
|
|
|
100
112
|
* gate behind it) past that deadline.
|
|
101
113
|
*/
|
|
102
114
|
|
|
103
|
-
import {
|
|
115
|
+
import {
|
|
116
|
+
extractRepairFecPayloadId,
|
|
117
|
+
parseFecBlockProfile,
|
|
118
|
+
serial32Add,
|
|
119
|
+
serial32Distance,
|
|
120
|
+
type FecBlockProfile,
|
|
121
|
+
} from '@blockcast/mmt-base'
|
|
104
122
|
import { extendSsId, repairFetchRange, type RepairFetchRange } from '@blockcast/mmt-fec'
|
|
105
123
|
import {
|
|
106
124
|
repairPriorityForClass,
|
|
107
125
|
type BlockClass,
|
|
108
126
|
type ClassifierMode,
|
|
109
127
|
type RepairPriority,
|
|
128
|
+
type TrackKind,
|
|
110
129
|
} from './block-policy.js'
|
|
111
|
-
import type { UndelimitedIsoLoss } from './fec-manager.js'
|
|
130
|
+
import type { ExpiredIsoHole, UndelimitedIsoLoss } from './fec-manager.js'
|
|
112
131
|
import type { RepairLifecycleObserver } from './multicast-manager.js'
|
|
113
132
|
import { REPAIR_PRIORITY_MIN } from './repair-layers.js'
|
|
114
133
|
|
|
115
134
|
/** The MoQ Transport priority scale: 0..255, a lower value is delivered first. */
|
|
116
135
|
const MOQT_PRIORITY_MAX = 255
|
|
117
136
|
|
|
137
|
+
/**
|
|
138
|
+
* How long before the span a walked hole leaves asks for its repair (D-28)
|
|
139
|
+
* the walk ends: past the deadline timer's 1 ms slack and one timer tick, so
|
|
140
|
+
* the span's request timer is armed before it is due (walkDeadlineAt).
|
|
141
|
+
*/
|
|
142
|
+
const WALK_LEAD_MS = 3
|
|
143
|
+
|
|
118
144
|
/**
|
|
119
145
|
* The highest value in the Source Media priority range of
|
|
120
146
|
* draft-ramadan-moq-fec §10 (64..191): one below the repair range, which
|
|
@@ -370,10 +396,14 @@ export interface WireRepairFetchOptions {
|
|
|
370
396
|
keyframeRepairBudgetMs?: (packetId: number, catalogTrackName?: string) => number
|
|
371
397
|
/**
|
|
372
398
|
* Range repair for a lost SS_ID whose block no repair delimited
|
|
373
|
-
* (draft-ramadan-moq-fec §7.5), optional.
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
399
|
+
* (draft-ramadan-moq-fec §7.5), optional. A host that can keep such a gap as
|
|
400
|
+
* a pending hole gets range repair either way, under the same admission,
|
|
401
|
+
* priority and abort rules as a block request (class `unknown`, P0). Given,
|
|
402
|
+
* the scheduler FETCHes `repairFetchRange(x, K, anchor)` over it, and
|
|
403
|
+
* requires `repairEpochReference`. Absent, it walks the hole over
|
|
404
|
+
* `transport` one Group at a time from the hole's start, whose Group is a
|
|
405
|
+
* block's, each repair stating where the next block starts (BLO-52085),
|
|
406
|
+
* under the class of the hole's track (see the module note).
|
|
377
407
|
*/
|
|
378
408
|
rangeTransport?: RepairRangeFetchTransport
|
|
379
409
|
/**
|
|
@@ -475,6 +505,12 @@ export interface RepairFetchHost {
|
|
|
475
505
|
): boolean
|
|
476
506
|
/** The admitted FETCH ended without delivering: back to the multicast wait. */
|
|
477
507
|
releaseRepairHold(sbn: number, packetId: number, catalogTrackName?: string): void
|
|
508
|
+
/** Run `fn` with `receivedAtMs` as the receipt time of what it feeds (optional). */
|
|
509
|
+
withReceiptTime?(receivedAtMs: number, fn: () => void): void
|
|
510
|
+
/** The configured media kind of a packetId (optional; absent reads as video). */
|
|
511
|
+
trackKindFor?(packetId: number): TrackKind | undefined
|
|
512
|
+
/** The stream's D-28 repair deadline: how long after a block's sources it asks for repair (optional). */
|
|
513
|
+
repairDeadlineMsForPacket?(packetId: number): number | undefined
|
|
478
514
|
}
|
|
479
515
|
onRepairSubscribe(cb: RepairSubscribeCallback): void
|
|
480
516
|
/**
|
|
@@ -493,8 +529,17 @@ export interface RepairFetchHost {
|
|
|
493
529
|
): void
|
|
494
530
|
addRepairLifecycleObserver(observer: RepairLifecycleObserver): () => void
|
|
495
531
|
addTrackResetObserver(observer: (packetId: number, catalogTrackName: string) => void): () => void
|
|
496
|
-
/**
|
|
497
|
-
|
|
532
|
+
/**
|
|
533
|
+
* Register for lost SS_IDs in undelimited spans (required with
|
|
534
|
+
* `rangeTransport`). A host that offers it and `closeIsoHoles` keeps such a
|
|
535
|
+
* gap as a pending hole for the scheduler to repair. `onExpired` is told of
|
|
536
|
+
* a pending hole the stream ran past the locator's horizon; returning true,
|
|
537
|
+
* the scheduler closes it (`closeIsoHoles`), otherwise the host discards it.
|
|
538
|
+
*/
|
|
539
|
+
onUndelimitedLoss?(
|
|
540
|
+
cb: (loss: UndelimitedIsoLoss) => void,
|
|
541
|
+
onExpired?: (hole: ExpiredIsoHole) => boolean,
|
|
542
|
+
): void
|
|
498
543
|
/** A hole's range repair ended, or none is sent: feed what was not stated. */
|
|
499
544
|
closeIsoHoles?(packetId: number, catalogTrackName: string | undefined, start: number, length: number): void
|
|
500
545
|
clearRepairForBlock(sbn: number, packetId: number, catalogTrackName?: string): void
|
|
@@ -624,9 +669,26 @@ interface RepairEntry {
|
|
|
624
669
|
attempts: number
|
|
625
670
|
running: FetchRun | null
|
|
626
671
|
/** A range request for a pending hole `[sbn, sbn + holeLength)`. */
|
|
627
|
-
readonly range?:
|
|
672
|
+
readonly range?: {
|
|
673
|
+
readonly lostSsId: number
|
|
674
|
+
readonly holeLength: number
|
|
675
|
+
/** The §7.7 Group range for `rangeTransport`; null walks the hole over `transport`. */
|
|
676
|
+
readonly groups: RepairFetchRange | null
|
|
677
|
+
/** When the hole was reported: what it states keeps the deadline a span opened then would have. */
|
|
678
|
+
readonly reportedAt: number
|
|
679
|
+
}
|
|
628
680
|
/** A range request's deadline timer (see the module note). */
|
|
629
681
|
expiry?: ReturnType<typeof setTimeout>
|
|
682
|
+
/**
|
|
683
|
+
* A hole walked over `transport` (walkHole): the Group its next FETCH asks
|
|
684
|
+
* for, and the Group FETCH it has outstanding, if any.
|
|
685
|
+
*/
|
|
686
|
+
walk?: {
|
|
687
|
+
next: number
|
|
688
|
+
group: { readonly sbn: number; readonly controller: AbortController } | null
|
|
689
|
+
/** Its Group FETCH was cancelled for best-effort repair: ask for that Group again. */
|
|
690
|
+
yielded: boolean
|
|
691
|
+
}
|
|
630
692
|
}
|
|
631
693
|
|
|
632
694
|
interface ResolvedDeadline {
|
|
@@ -686,8 +748,26 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
|
|
|
686
748
|
|
|
687
749
|
/** Whatever ends an entry: a block's repair slot, or a hole's pending state. */
|
|
688
750
|
const clearEntry = (entry: RepairEntry): void => {
|
|
689
|
-
|
|
690
|
-
|
|
751
|
+
const { range } = entry
|
|
752
|
+
if (range) {
|
|
753
|
+
atReport(range, () => mc.closeIsoHoles!(entry.packetId, entry.catalogTrackName, entry.sbn, range.holeLength))
|
|
754
|
+
} else {
|
|
755
|
+
clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName)
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Run what a hole's walk states, or the rest of the hole it leaves as
|
|
761
|
+
* spans, at the time the hole was reported: the blocks start their
|
|
762
|
+
* deadlines from there, as the one repair-less span a host without holes
|
|
763
|
+
* opened then would have, so the walk never holds a frame later than that
|
|
764
|
+
* span did. A §7.7 range FETCH feeds at its own receipt time, as a host
|
|
765
|
+
* with `rangeTransport` always has.
|
|
766
|
+
*/
|
|
767
|
+
const atReport = (range: NonNullable<RepairEntry['range']>, work: () => void): void => {
|
|
768
|
+
const fec = mc.fecManager
|
|
769
|
+
if (range.groups !== null || fec.withReceiptTime === undefined) work()
|
|
770
|
+
else fec.withReceiptTime(range.reportedAt, work)
|
|
691
771
|
}
|
|
692
772
|
|
|
693
773
|
const releaseHold = (entry: RepairEntry): void => {
|
|
@@ -781,32 +861,78 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
|
|
|
781
861
|
const admit = (entry: RepairEntry, now: number): number | null => {
|
|
782
862
|
const budgetMs = entry.deadlineAt - now
|
|
783
863
|
const rttMs = readRtt()
|
|
784
|
-
const reason
|
|
785
|
-
!entry.range && !groupCanComplete(entry.sbn, entry.packetId, entry.catalogTrackName)
|
|
786
|
-
? 'group-cannot-complete'
|
|
787
|
-
: rttMs === null
|
|
788
|
-
? 'rtt-unmeasured'
|
|
789
|
-
: rttMs < budgetMs
|
|
790
|
-
? null
|
|
791
|
-
: 'rtt-exceeds-budget'
|
|
864
|
+
const reason = refusal(entry, now, rttMs)
|
|
792
865
|
if (reason === null) return rttMs
|
|
793
866
|
skip(entry, reason, rttMs, budgetMs)
|
|
794
867
|
return null
|
|
795
868
|
}
|
|
796
869
|
|
|
870
|
+
/** Why admission refuses `entry` at `now` with round trip `rttMs`, or null when it admits it. */
|
|
871
|
+
const refusal = (entry: RepairEntry, now: number, rttMs: number | null): RepairSkipReason | null =>
|
|
872
|
+
!entry.range && !groupCanComplete(entry.sbn, entry.packetId, entry.catalogTrackName)
|
|
873
|
+
? 'group-cannot-complete'
|
|
874
|
+
: rttMs === null
|
|
875
|
+
? 'rtt-unmeasured'
|
|
876
|
+
: rttMs < entry.deadlineAt - now
|
|
877
|
+
? null
|
|
878
|
+
: 'rtt-exceeds-budget'
|
|
879
|
+
|
|
797
880
|
const retire = (entry: RepairEntry): void => {
|
|
798
881
|
if (entries.get(entry.key) === entry) entries.delete(entry.key)
|
|
799
882
|
if (entry.expiry !== undefined) clearTimeout(entry.expiry)
|
|
800
883
|
}
|
|
801
884
|
|
|
802
|
-
/**
|
|
803
|
-
|
|
885
|
+
/**
|
|
886
|
+
* Whether `entry` holds back a request of `priority`: no audio or delta
|
|
887
|
+
* repair FETCH is issued while a keyframe repair FETCH is outstanding
|
|
888
|
+
* (fec-multicast-00 rule 3), and no delta one while audio repair is. A
|
|
889
|
+
* walk (walkHole) is repair a run gets before it reaches its FEC deadline:
|
|
890
|
+
* on a video track it is keyframe repair while one of its Group FETCHes is
|
|
891
|
+
* outstanding and between two Groups holds nothing back; on an audio track
|
|
892
|
+
* it holds nothing back (BLO-52085). `videoWalks` false leaves video walks
|
|
893
|
+
* out: they yield to a request that is no walk (`yieldVideoWalks`).
|
|
894
|
+
*/
|
|
895
|
+
const holdsBack = (entry: RepairEntry, priority: RepairPriority, videoWalks = true): boolean => {
|
|
896
|
+
if (entry.priority >= priority) return false
|
|
897
|
+
const { walk } = entry
|
|
898
|
+
if (walk === undefined) return true
|
|
899
|
+
if (entry.priority !== 0 || !videoWalks) return false
|
|
900
|
+
return entry.running === null || (walk.group !== null && !walk.group.controller.signal.aborted)
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** Whether any entry holds back a request of `priority` (see `holdsBack`). */
|
|
904
|
+
const outstandingBelow = (priority: RepairPriority, videoWalks = true): boolean => {
|
|
804
905
|
for (const entry of entries.values()) {
|
|
805
|
-
if (entry
|
|
906
|
+
if (holdsBack(entry, priority, videoWalks)) return true
|
|
806
907
|
}
|
|
807
908
|
return false
|
|
808
909
|
}
|
|
809
910
|
|
|
911
|
+
/**
|
|
912
|
+
* A video walk reaches no FEC deadline, so it holds back no audio or delta
|
|
913
|
+
* request that came due (one of `queued` that is no walk), that nothing
|
|
914
|
+
* else holds back and that admission takes (`admit`): it cancels its outstanding Group FETCH first, so no
|
|
915
|
+
* keyframe repair FETCH is outstanding when that request is issued
|
|
916
|
+
* (fec-multicast-00 rule 3), and asks for that Group again after it
|
|
917
|
+
* (walkHole). Audio and delta repair so goes out when it would with no
|
|
918
|
+
* walk at all (BLO-52085).
|
|
919
|
+
*/
|
|
920
|
+
const yieldVideoWalks = (queued: readonly RepairEntry[]): void => {
|
|
921
|
+
const now = performance.now()
|
|
922
|
+
const rttMs = readRtt()
|
|
923
|
+
const due = queued.some((entry) =>
|
|
924
|
+
entry.priority !== 0 && entry.walk === undefined && !outstandingBelow(entry.priority, false) &&
|
|
925
|
+
refusal(entry, now, rttMs) === null)
|
|
926
|
+
if (!due) return
|
|
927
|
+
for (const entry of entries.values()) {
|
|
928
|
+
const walk = entry.walk
|
|
929
|
+
if (entry.priority !== 0 || walk === undefined || walk.group === null) continue
|
|
930
|
+
if (walk.group.controller.signal.aborted) continue
|
|
931
|
+
walk.yielded = true
|
|
932
|
+
walk.group.controller.abort()
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
810
936
|
/**
|
|
811
937
|
* A keyframe FETCH is being sent: cancel every in-flight audio and delta
|
|
812
938
|
* FETCH (§11.4.5 item 3). Called only for a FETCH actually sent.
|
|
@@ -849,8 +975,12 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
|
|
|
849
975
|
}
|
|
850
976
|
const rttMs = admit(entry, now)
|
|
851
977
|
if (rttMs === null) return
|
|
852
|
-
// Before the transport sees this request, so the cancels go out first.
|
|
853
|
-
|
|
978
|
+
// Before the transport sees this request, so the cancels go out first. A
|
|
979
|
+
// walk reaches no FEC deadline, so it cancels nothing (rule 3).
|
|
980
|
+
if (entry.priority === 0 && entry.walk === undefined) preemptBestEffort()
|
|
981
|
+
// fec-multicast-00 rule 1: one FETCH per block and repair layer. A walk
|
|
982
|
+
// reading this block's Group leaves it to this request.
|
|
983
|
+
if (entry.range === undefined) leaveWalkedGroup(entry)
|
|
854
984
|
if (counted) entry.attempts++
|
|
855
985
|
mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
|
|
856
986
|
rttMs,
|
|
@@ -875,6 +1005,20 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
|
|
|
875
1005
|
void execute(entry, run)
|
|
876
1006
|
}
|
|
877
1007
|
|
|
1008
|
+
/** End any walk's FETCH of `block`'s Group, cancelling it, before the block's own goes out. */
|
|
1009
|
+
const leaveWalkedGroup = (block: RepairEntry): void => {
|
|
1010
|
+
for (const entry of entries.values()) {
|
|
1011
|
+
const group = entry.walk?.group
|
|
1012
|
+
if (
|
|
1013
|
+
group?.sbn === block.sbn &&
|
|
1014
|
+
entry.packetId === block.packetId &&
|
|
1015
|
+
entry.catalogTrackName === block.catalogTrackName
|
|
1016
|
+
) {
|
|
1017
|
+
group.controller.abort()
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
878
1022
|
const dispatch = (): void => {
|
|
879
1023
|
if (disposed) return
|
|
880
1024
|
const now = performance.now()
|
|
@@ -905,6 +1049,7 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
|
|
|
905
1049
|
for (const entry of queued) {
|
|
906
1050
|
if (entry.priority === 0) start(entry)
|
|
907
1051
|
}
|
|
1052
|
+
yieldVideoWalks(queued)
|
|
908
1053
|
if (outstandingBelow(1)) return
|
|
909
1054
|
for (const entry of queued) {
|
|
910
1055
|
if (entry.priority === 1) start(entry)
|
|
@@ -954,6 +1099,25 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
|
|
|
954
1099
|
}
|
|
955
1100
|
return counts.sources + counts.repairGroupLength >= counts.authoredLength
|
|
956
1101
|
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Whether the block a walked Group states can use none of the Group's other
|
|
1104
|
+
* repair symbols: the host holds as many of its symbols as its K' (complete,
|
|
1105
|
+
* or decoding), its group cannot complete it, or it no longer tracks it
|
|
1106
|
+
* (resolved). A relay may hold only part of a Group, or keep its FETCH open,
|
|
1107
|
+
* so the walk does not wait for the rest. False when the host reports no
|
|
1108
|
+
* counts: the walk then takes the whole Group.
|
|
1109
|
+
*/
|
|
1110
|
+
const groupServed = (sbn: number, packetId: number, catalogTrackName: string | undefined): boolean => {
|
|
1111
|
+
if (mc.fecManager.blockSymbolCounts === undefined) return false
|
|
1112
|
+
const counts = symbolCounts(sbn, packetId, catalogTrackName)
|
|
1113
|
+
if (counts === undefined || counts.authoredLength === undefined) return true
|
|
1114
|
+
return counts.sources + counts.repairs >= counts.authoredLength || !groupCanComplete(sbn, packetId, catalogTrackName)
|
|
1115
|
+
}
|
|
1116
|
+
/** Where the next block starts after the open block `sbn`, when a repair stated it. */
|
|
1117
|
+
const statedEnd = (sbn: number, packetId: number, catalogTrackName: string | undefined): number | undefined => {
|
|
1118
|
+
const length = symbolCounts(sbn, packetId, catalogTrackName)?.authoredLength
|
|
1119
|
+
return length === undefined ? undefined : serial32Add(sbn, length)
|
|
1120
|
+
}
|
|
957
1121
|
|
|
958
1122
|
const finish = (entry: RepairEntry, run: FetchRun, fed: number, error: unknown): void => {
|
|
959
1123
|
if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {
|
|
@@ -1019,23 +1183,30 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
|
|
|
1019
1183
|
signal: run.controller.signal,
|
|
1020
1184
|
subscriberPriority: entry.subscriberPriority,
|
|
1021
1185
|
}
|
|
1186
|
+
const { range } = entry
|
|
1187
|
+
const feed = (packet: Uint8Array): void => {
|
|
1188
|
+
if (entry.catalogTrackName === undefined) {
|
|
1189
|
+
mc.processRepairFromMoQ(packet, entry.packetId)
|
|
1190
|
+
} else {
|
|
1191
|
+
mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName)
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1022
1194
|
try {
|
|
1023
|
-
const packets =
|
|
1024
|
-
? opts.
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1195
|
+
const packets = range === undefined
|
|
1196
|
+
? opts.transport({ ...common, sbn: entry.sbn })
|
|
1197
|
+
: entry.walk !== undefined
|
|
1198
|
+
? walkHole(entry, entry.walk, range, common)
|
|
1199
|
+
: opts.rangeTransport!({
|
|
1200
|
+
...common,
|
|
1201
|
+
startGroup: range.groups!.startGroup,
|
|
1202
|
+
endGroup: range.groups!.endGroup,
|
|
1203
|
+
lostSsId: range.lostSsId,
|
|
1204
|
+
})
|
|
1031
1205
|
for await (const packet of packets) {
|
|
1032
1206
|
if (run.stopped !== null || entry.running !== run) break
|
|
1033
1207
|
fed++
|
|
1034
|
-
if (
|
|
1035
|
-
|
|
1036
|
-
} else {
|
|
1037
|
-
mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName)
|
|
1038
|
-
}
|
|
1208
|
+
if (range === undefined) feed(packet)
|
|
1209
|
+
else atReport(range, () => feed(packet))
|
|
1039
1210
|
}
|
|
1040
1211
|
finish(entry, run, fed, undefined)
|
|
1041
1212
|
} catch (error) {
|
|
@@ -1043,6 +1214,86 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
|
|
|
1043
1214
|
}
|
|
1044
1215
|
}
|
|
1045
1216
|
|
|
1217
|
+
/**
|
|
1218
|
+
* The §7.7 range FETCH of a hole over a transport that FETCHes one Group.
|
|
1219
|
+
* A hole starts where a stated block ends, so ext(hole start) is a block's
|
|
1220
|
+
* Group; the repair of each Group states its block, and that block's end is
|
|
1221
|
+
* the next block's Group. Walks to the block holding the hole's highest
|
|
1222
|
+
* lost SS_ID (everything after it arrived) and stops early when a Group
|
|
1223
|
+
* names no block and none is stated there. A Group is done when it ends,
|
|
1224
|
+
* when it held its RSB_length repair symbols, or once the block it states
|
|
1225
|
+
* can use none of the rest (`groupServed`): a relay may keep the FETCH open
|
|
1226
|
+
* past its last object (BLO-52085 D2 80601), or hold only part of the
|
|
1227
|
+
* Group. Each Group is its own FETCH with its own signal, aborted once the
|
|
1228
|
+
* walk leaves it, so the transport cancels it on the wire, and never one
|
|
1229
|
+
* the block's own FETCH reads (fec-multicast-00 rule 1: `leaveWalkedGroup`
|
|
1230
|
+
* takes it over). Between two Groups the walk honours rule 3 (see
|
|
1231
|
+
* `holdsBack`): an audio walk that finds keyframe repair outstanding goes
|
|
1232
|
+
* back to the queue and resumes at `walk.next`; a video walk first
|
|
1233
|
+
* dispatches what came due, and asks again for a Group whose FETCH it
|
|
1234
|
+
* cancelled for audio or delta repair (`yieldVideoWalks`). Every packet is
|
|
1235
|
+
* fed before the walk reads the host's counts: the consumer feeds what it
|
|
1236
|
+
* yields before asking for more.
|
|
1237
|
+
*/
|
|
1238
|
+
async function* walkHole(
|
|
1239
|
+
entry: RepairEntry,
|
|
1240
|
+
walk: NonNullable<RepairEntry['walk']>,
|
|
1241
|
+
hole: NonNullable<RepairEntry['range']>,
|
|
1242
|
+
common: Omit<RepairFetchRequest, 'sbn'>,
|
|
1243
|
+
): AsyncGenerator<Uint8Array> {
|
|
1244
|
+
const lostAt = serial32Distance(entry.sbn, hole.lostSsId)
|
|
1245
|
+
for (let first = true; !common.signal.aborted; first = false) {
|
|
1246
|
+
if (!first && entry.priority !== 0 && outstandingBelow(1)) {
|
|
1247
|
+
stop(entry, 'preempted')
|
|
1248
|
+
note('repair-preempted', entry)
|
|
1249
|
+
return
|
|
1250
|
+
}
|
|
1251
|
+
if (!first && entry.priority === 0) {
|
|
1252
|
+
dispatch()
|
|
1253
|
+
if (common.signal.aborted) return
|
|
1254
|
+
}
|
|
1255
|
+
const sbn = walk.next
|
|
1256
|
+
let next: number | undefined
|
|
1257
|
+
if (entries.get(requestKey(sbn, entry.packetId, entry.catalogTrackName))?.running == null) {
|
|
1258
|
+
const rsIds = new Set<number>()
|
|
1259
|
+
const group = new AbortController()
|
|
1260
|
+
const cancel = (): void => group.abort()
|
|
1261
|
+
common.signal.addEventListener('abort', cancel, { once: true })
|
|
1262
|
+
walk.group = { sbn, controller: group }
|
|
1263
|
+
try {
|
|
1264
|
+
for await (const packet of opts.transport({ ...common, sbn, signal: group.signal })) {
|
|
1265
|
+
yield packet
|
|
1266
|
+
const id = extractRepairFecPayloadId(packet)
|
|
1267
|
+
if (id === null || id.ssStart !== sbn || id.ssbLength === 0) continue
|
|
1268
|
+
next ??= serial32Add(sbn, id.ssbLength)
|
|
1269
|
+
rsIds.add(id.rsId)
|
|
1270
|
+
// Aborted before the loop lets go of the transport, whose own
|
|
1271
|
+
// cleanup may stop listening for the abort.
|
|
1272
|
+
if (rsIds.size >= id.rsbLength || groupServed(sbn, entry.packetId, entry.catalogTrackName)) {
|
|
1273
|
+
group.abort()
|
|
1274
|
+
break
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
// The Group was taken over or yielded: the walk goes on.
|
|
1279
|
+
if (!group.signal.aborted || common.signal.aborted) throw error
|
|
1280
|
+
} finally {
|
|
1281
|
+
common.signal.removeEventListener('abort', cancel)
|
|
1282
|
+
walk.group = null
|
|
1283
|
+
}
|
|
1284
|
+
if (walk.yielded) {
|
|
1285
|
+
walk.yielded = false
|
|
1286
|
+
continue
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
next ??= statedEnd(sbn, entry.packetId, entry.catalogTrackName)
|
|
1290
|
+
if (next === undefined) return
|
|
1291
|
+
// Past the highest lost SS_ID, which lies in the hole: done.
|
|
1292
|
+
if (serial32Distance(entry.sbn, next) > lostAt) return
|
|
1293
|
+
walk.next = next
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1046
1297
|
const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {
|
|
1047
1298
|
for (const entry of [...entries.values()]) {
|
|
1048
1299
|
if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue
|
|
@@ -1300,17 +1551,75 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
|
|
|
1300
1551
|
}
|
|
1301
1552
|
}
|
|
1302
1553
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1554
|
+
const trackKind = (packetId: number): TrackKind | undefined => {
|
|
1555
|
+
try {
|
|
1556
|
+
return mc.fecManager.trackKindFor?.(packetId)
|
|
1557
|
+
} catch {
|
|
1558
|
+
return undefined
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/**
|
|
1563
|
+
* Where a walk of a hole reported `now` ends: WALK_LEAD_MS before the span
|
|
1564
|
+
* it leaves asks for its repair (D-28 from the report, as the span a host
|
|
1565
|
+
* without holes opened then did), so that span's request is armed before
|
|
1566
|
+
* it is due and goes out at the same time; never later than `deadlineAt`.
|
|
1567
|
+
*/
|
|
1568
|
+
const walkDeadlineAt = (packetId: number, now: number, deadlineAt: number): number => {
|
|
1569
|
+
let repairDeadlineMs: number | undefined
|
|
1570
|
+
try {
|
|
1571
|
+
repairDeadlineMs = mc.fecManager.repairDeadlineMsForPacket?.(packetId)
|
|
1572
|
+
} catch {
|
|
1573
|
+
repairDeadlineMs = undefined
|
|
1574
|
+
}
|
|
1575
|
+
if (repairDeadlineMs === undefined || !Number.isFinite(repairDeadlineMs)) return deadlineAt
|
|
1576
|
+
return Math.min(deadlineAt, now + repairDeadlineMs - WALK_LEAD_MS)
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/**
|
|
1580
|
+
* The stream ran past the locator's horizon behind a hole: no repair can
|
|
1581
|
+
* state its blocks any more. A walking hole's walk ends and the hole closes
|
|
1582
|
+
* as at its deadline, so the span it leaves fails, and is reported, as the
|
|
1583
|
+
* span opened at its report would have (BLO-52085); the host discards any
|
|
1584
|
+
* other. Called from inside the stream's packet handling, so the walk ends
|
|
1585
|
+
* once that returns, as for a RAP (below).
|
|
1586
|
+
*/
|
|
1587
|
+
const onHoleExpired = (hole: ExpiredIsoHole): boolean => {
|
|
1588
|
+
if (disposed) return false
|
|
1589
|
+
const entry = [...entries.values()].find((candidate) =>
|
|
1590
|
+
candidate.walk !== undefined &&
|
|
1591
|
+
candidate.packetId === hole.packetId &&
|
|
1592
|
+
candidate.catalogTrackName === hole.catalogTrackName &&
|
|
1593
|
+
serial32Distance(candidate.sbn, hole.holeStart) < candidate.range!.holeLength)
|
|
1594
|
+
if (entry === undefined) return false
|
|
1595
|
+
queueMicrotask(() => {
|
|
1596
|
+
if (entries.get(entry.key) !== entry) return
|
|
1597
|
+
retire(entry)
|
|
1598
|
+
stop(entry, 'expired')
|
|
1599
|
+
clearEntry(entry)
|
|
1600
|
+
dispatch()
|
|
1601
|
+
})
|
|
1602
|
+
return true
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// A host that can keep a hole gets range repair: the §7.7 range over
|
|
1606
|
+
// `rangeTransport`, or else a walk of the hole's Groups over `transport`.
|
|
1607
|
+
if (mc.onUndelimitedLoss !== undefined && mc.closeIsoHoles !== undefined) {
|
|
1608
|
+
mc.onUndelimitedLoss((loss) => {
|
|
1305
1609
|
const { packetId, catalogTrackName, holeStart, holeLength } = loss
|
|
1306
1610
|
const close = (): void => mc.closeIsoHoles!(packetId, catalogTrackName, holeStart, holeLength)
|
|
1307
1611
|
if (disposed) return close()
|
|
1308
1612
|
const blockProfile = blockProfileOf(packetId)
|
|
1309
1613
|
const target = opts.resolveTarget(packetId, catalogTrackName)
|
|
1310
1614
|
if (!target) return close()
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1615
|
+
// The run's class (fec-multicast-00 Block Classification, Audio
|
|
1616
|
+
// Blocks): walked, a hole on an audio track is audio repair, one Group
|
|
1617
|
+
// per FETCH; any other is of unknown class, so keyframe repair.
|
|
1618
|
+
const walked = opts.rangeTransport === undefined
|
|
1619
|
+
const holeClass: BlockClass = walked && trackKind(packetId) === 'audio' ? 'audio' : 'unknown'
|
|
1620
|
+
const priority = repairPriorityForClass(holeClass)
|
|
1621
|
+
const groups = walked ? null : rangeFor(loss, target.repairTrack)
|
|
1622
|
+
if (!walked && groups === null) {
|
|
1314
1623
|
close()
|
|
1315
1624
|
mc.noteRepairSkipped('range-no-anchor', { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName)
|
|
1316
1625
|
return
|
|
@@ -1321,12 +1630,15 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
|
|
|
1321
1630
|
packetId,
|
|
1322
1631
|
...(catalogTrackName === undefined ? {} : { catalogTrackName }),
|
|
1323
1632
|
}
|
|
1324
|
-
// `unknown` is P0, whose request is never declined (omitted at worst)
|
|
1325
|
-
|
|
1633
|
+
// `unknown` is P0, whose request is never declined (omitted at worst);
|
|
1634
|
+
// an audio FETCH the catalog gives no priority for is not sent.
|
|
1635
|
+
const wire = wirePriority(identity, holeClass)
|
|
1636
|
+
if (wire === null) return close()
|
|
1326
1637
|
// The hole is no tracked block: no anchor, so this is the multicast
|
|
1327
1638
|
// wait (`bound: 'wait'`), by intent. A hole holds the release gate, so it
|
|
1328
1639
|
// must not wait out a presentation delay on top.
|
|
1329
|
-
const
|
|
1640
|
+
const now = performance.now()
|
|
1641
|
+
const deadline = presentationDeadlineAt(holeStart, packetId, catalogTrackName, now, holeClass)
|
|
1330
1642
|
if (deadline === null) {
|
|
1331
1643
|
close()
|
|
1332
1644
|
mc.noteRepairSkipped(
|
|
@@ -1338,6 +1650,7 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
|
|
|
1338
1650
|
)
|
|
1339
1651
|
return
|
|
1340
1652
|
}
|
|
1653
|
+
const deadlineAt = walked ? walkDeadlineAt(packetId, now, deadline.deadlineAt) : deadline.deadlineAt
|
|
1341
1654
|
const entry: RepairEntry = {
|
|
1342
1655
|
key: JSON.stringify(['range', packetId, catalogTrackName ?? null, holeStart]),
|
|
1343
1656
|
...identity,
|
|
@@ -1345,15 +1658,16 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
|
|
|
1345
1658
|
blockProfile,
|
|
1346
1659
|
priority,
|
|
1347
1660
|
subscriberPriority: wire.subscriberPriority,
|
|
1348
|
-
deadlineAt
|
|
1349
|
-
playoutDeadlineAt:
|
|
1661
|
+
deadlineAt,
|
|
1662
|
+
playoutDeadlineAt: deadlineAt - deadline.extensionMs,
|
|
1350
1663
|
anchorAt: deadline.anchorAt,
|
|
1351
1664
|
delayMs: deadline.delayMs,
|
|
1352
1665
|
bound: deadline.bound,
|
|
1353
1666
|
order: enqueueOrder++,
|
|
1354
1667
|
attempts: 0,
|
|
1355
1668
|
running: null,
|
|
1356
|
-
range: {
|
|
1669
|
+
range: { lostSsId: loss.lostSsId, holeLength, groups, reportedAt: now },
|
|
1670
|
+
...(walked ? { walk: { next: holeStart, group: null, yielded: false } } : {}),
|
|
1357
1671
|
}
|
|
1358
1672
|
const previous = entries.get(entry.key)
|
|
1359
1673
|
if (previous) {
|
|
@@ -1363,7 +1677,7 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
|
|
|
1363
1677
|
entries.set(entry.key, entry)
|
|
1364
1678
|
entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1)
|
|
1365
1679
|
dispatch()
|
|
1366
|
-
})
|
|
1680
|
+
}, onHoleExpired)
|
|
1367
1681
|
}
|
|
1368
1682
|
|
|
1369
1683
|
// A RAP arrived complete: a keyframe FETCH below it is moot (§11.4.2). The
|