@torrent-tv/proxy 2.91.0 → 2.92.0
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/bin/cli.js +3 -2
- package/package.json +18 -2
- package/routes/api/playback-plan/post.js +21 -4
- package/routes/api/sources/files/get.js +20 -28
- package/routes/api/sources/post.js +4 -2
- package/routes/api/sources/stats/get.js +4 -11
- package/routes/api/sources/viewer/post.js +16 -0
- package/routes/api/sources/warm/post.js +6 -215
- package/routes/api/subtitles/get.js +22 -125
- package/routes/api/subtitles/post.js +26 -0
- package/routes/api/transcode-sessions/post.js +24 -2
- package/routes/media/get.js +80 -0
- package/routes/stream/get.js +26 -94
- package/routes/transcode/audio-warm/get.js +9 -21
- package/routes/transcode/variant-warm/get.js +8 -14
- package/server.js +410 -46
- package/services/encode/AdmittedInput.js +136 -0
- package/services/encode/CushionReport.js +1 -76
- package/services/encode/EncodeInputs.js +142 -0
- package/services/encode/EncodeOrchestrator.js +4 -1
- package/services/encode/EncodeRun.js +13 -4
- package/services/encode/EncodeRuns.js +132 -52
- package/services/encode/InputFailures.js +37 -0
- package/services/encode/MatroskaInput.js +158 -0
- package/services/encode/OutputOpening.js +16 -64
- package/services/encode/Renditions.js +1 -17
- package/services/encode/admitted-command.js +60 -0
- package/services/encode/annex-b-input.js +37 -0
- package/services/encode/bit-payload.js +29 -0
- package/services/encode/output/OutputCatalog.js +1 -1
- package/services/encode/output/Timeline.js +11 -0
- package/services/encode/pcm-normalization.js +30 -0
- package/services/encode/piece-completeness.js +54 -1
- package/services/encode/quality/HostLoad.js +19 -73
- package/services/encode/quality/QualityOffer.js +1 -1
- package/services/encode/run-command.js +1 -589
- package/services/encode/segment-formats/mp4-boxes.js +3 -1
- package/services/encode/segment-formats/presentation-segment.js +114 -0
- package/services/media/ContainerOrchestrator.js +118 -48
- package/services/media/KeyframeTables.js +16 -115
- package/services/media/MediaReadRequests.js +120 -0
- package/services/media/ProbeRequests.js +135 -0
- package/services/media/SegmentInputs.js +48 -0
- package/services/media/SourceFile.js +6 -0
- package/services/media/SubtitleOrchestrator.js +190 -4
- package/services/media/await-plan.js +47 -0
- package/services/media/container/AsfContainer.js +217 -0
- package/services/media/container/AviContainer.js +343 -34
- package/services/media/container/CadencedPictureOrder.js +45 -0
- package/services/media/container/Container.js +7 -3
- package/services/media/container/ContainerFactory.js +10 -3
- package/services/media/container/FfprobeContainer.js +222 -0
- package/services/media/container/MatroskaContainer.js +72 -46
- package/services/media/container/Mp4Container.js +472 -139
- package/services/media/container/MpegPsContainer.js +281 -0
- package/services/media/container/MpegTsContainer.js +277 -0
- package/services/media/container/PacketIndex.js +270 -0
- package/services/media/container/PacketRecords.js +237 -0
- package/services/media/container/RetainedBytes.js +39 -0
- package/services/media/container/RetainedReads.js +51 -0
- package/services/media/container/ac3-frame.js +46 -0
- package/services/media/container/asf-packets.js +153 -0
- package/services/media/container/avc-picture-order.js +50 -0
- package/services/media/container/avi-open-dml.js +47 -0
- package/services/media/container/dts-frame.js +51 -0
- package/services/media/container/ebml-stream.js +12 -1
- package/services/media/container/ffprobe-record.js +45 -0
- package/services/media/container/flv-packet-ranges.js +27 -0
- package/services/media/container/following-packet-time.js +37 -0
- package/services/media/container/h264-configuration.js +183 -0
- package/services/media/container/h264-elementary-index.js +245 -0
- package/services/media/container/hevc-configuration.js +165 -0
- package/services/media/container/hevc-elementary-index.js +132 -0
- package/services/media/container/hevc-picture-order.js +66 -0
- package/services/media/container/latm-frame.js +99 -0
- package/services/media/container/matroska-packets.js +207 -0
- package/services/media/container/memory-unavailable.js +8 -0
- package/services/media/container/mp3-packet-dependencies.js +41 -0
- package/services/media/container/mp4-fragments.js +148 -0
- package/services/media/container/mpeg-audio-frame.js +19 -0
- package/services/media/container/mpeg-audio-reservoir.js +27 -0
- package/services/media/container/mpeg-elementary-index.js +196 -0
- package/services/media/container/mpeg-sequence-reader.js +69 -0
- package/services/media/container/mpeg-ts-packets.js +93 -0
- package/services/media/container/mpeg-video-configuration.js +50 -0
- package/services/media/container/mpeg4-picture-timing.js +108 -0
- package/services/media/container/pes-header.js +34 -0
- package/services/media/playback-declarations.js +57 -0
- package/services/media/playback-planner.js +72 -230
- package/services/media/probe-packets.js +61 -0
- package/services/media/read-playback-declarations.js +18 -0
- package/services/media/tracks/AudioTrack.js +9 -0
- package/services/media/tracks/TextSubtitleTrack.js +4 -1
- package/services/server/OutputLifecycle.js +2 -0
- package/services/server/SegmentServing.js +40 -33
- package/services/server/ViewerRequests.js +22 -41
- package/services/server/controllers/PlaybackController.js +4 -0
- package/services/server/controllers/SubtitleController.js +8 -29
- package/services/server/transcode-session-files.js +58 -99
- package/services/server/wire-outputs.js +58 -15
- package/services/storage/IndexMemory.js +85 -0
- package/services/storage/MachineBudget.js +27 -3
- package/services/storage/allowance.js +8 -1
- package/services/storage/piece-store/piece-disk-store.js +18 -3
- package/services/storage/piece-store/piece-lru.js +8 -4
- package/services/storage/piece-store/shared-piece-store.js +26 -11
- package/services/storage/segment-store/SegmentStore.js +49 -18
- package/services/storage/wire.js +7 -0
- package/services/torrent/Contents.js +52 -20
- package/services/torrent/demand/DemandRegister.js +13 -4
- package/services/torrent/demand/Window.js +13 -2
- package/services/torrent/demand-progress.js +25 -0
- package/services/torrent/download/DeadlineScheduler.js +167 -0
- package/services/torrent/download/FutureDownload.js +166 -0
- package/services/torrent/download/SwarmSelection.js +30 -59
- package/services/torrent/download/registry.js +45 -5
- package/services/torrent/download/withdraw-claim.js +2 -0
- package/services/torrent/torrent-pool.js +39 -600
- package/services/torrent/worker/channel.js +8 -1
- package/services/torrent/worker/client.js +74 -72
- package/services/torrent/worker/files-in-use.js +9 -0
- package/services/torrent/worker/handle-state.js +12 -0
- package/services/torrent/worker/held-bytes.js +82 -99
- package/services/torrent/worker/held-file-bytes.js +17 -0
- package/services/torrent/worker/piece-reader.js +67 -683
- package/services/torrent/worker/pool-adapter.js +37 -123
- package/services/torrent/worker/protocol.js +6 -17
- package/services/torrent/worker/whole-file-stats.js +2 -0
- package/services/torrent/worker/worker.js +86 -156
- package/services/transport/data-channel-handler.js +56 -13
- package/services/viewer/DownloadMaps.js +168 -0
- package/services/viewer/NativeSourceMap.js +33 -0
- package/services/viewer/PriorityMap.js +49 -40
- package/services/viewer/PriorityOrchestrator.js +42 -13
- package/services/viewer/SourcePreparation.js +278 -0
- package/services/viewer/Viewer.js +11 -0
- package/services/viewer/Viewers.js +109 -2
- package/services/viewer/playback-readiness.js +50 -63
- package/services/viewer/segment-demands.js +37 -0
- package/services/viewer/urgent-output-ready.js +26 -0
- package/services/encode/run-inputs.js +0 -53
- package/services/media/keyframe-probe.js +0 -157
- package/services/media/media-info-probe.js +0 -93
- package/services/torrent/worker/background-fill.js +0 -213
- package/services/torrent/worker/resume-warm.js +0 -107
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/** Source preparation is derived from viewers before any output exists. */
|
|
2
|
+
export class SourcePreparation {
|
|
3
|
+
#viewers;
|
|
4
|
+
#candidates;
|
|
5
|
+
#related;
|
|
6
|
+
#priorityFor;
|
|
7
|
+
#inspect;
|
|
8
|
+
#withdraw;
|
|
9
|
+
#failed;
|
|
10
|
+
#reprice;
|
|
11
|
+
#inputReady;
|
|
12
|
+
#readyChanged;
|
|
13
|
+
#work = new Map();
|
|
14
|
+
#revision = 0;
|
|
15
|
+
#completedRevision = null;
|
|
16
|
+
#pending = null;
|
|
17
|
+
#candidateAbort = new AbortController();
|
|
18
|
+
|
|
19
|
+
constructor({ viewers, candidatesFor, relatedFilesFor = () => [], priorityFor = (_viewer, priority) => priority,
|
|
20
|
+
inputReady = async () => false, readyChanged = () => {}, inspect, withdraw, reprice = () => {}, failed }) {
|
|
21
|
+
this.#viewers = viewers;
|
|
22
|
+
this.#candidates = candidatesFor;
|
|
23
|
+
this.#related = relatedFilesFor;
|
|
24
|
+
this.#priorityFor = priorityFor;
|
|
25
|
+
this.#inspect = inspect;
|
|
26
|
+
this.#withdraw = withdraw;
|
|
27
|
+
this.#failed = failed;
|
|
28
|
+
this.#reprice = reprice;
|
|
29
|
+
this.#inputReady = inputReady;
|
|
30
|
+
this.#readyChanged = readyChanged;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
refresh() {
|
|
34
|
+
this.#revision++;
|
|
35
|
+
this.#candidateAbort.abort();
|
|
36
|
+
this.#candidateAbort = new AbortController();
|
|
37
|
+
for (const [key, work] of this.#work) {
|
|
38
|
+
const demand = this.demandFor(work);
|
|
39
|
+
if (demand) { this.#reprice(work, demand); continue; }
|
|
40
|
+
this.#work.delete(key);
|
|
41
|
+
this.#withdraw(work);
|
|
42
|
+
}
|
|
43
|
+
if (!this.#pending) {
|
|
44
|
+
let succeeded = false;
|
|
45
|
+
this.#pending = this.#run().then(() => { succeeded = true; }).catch(error => this.#failed?.(error)).finally(() => {
|
|
46
|
+
this.#pending = null;
|
|
47
|
+
if (succeeded && this.#completedRevision !== this.#revision) void this.refresh();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return this.#pending;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Current owners determine priority; old work never restores a withdrawn read. */
|
|
54
|
+
demandFor(work) {
|
|
55
|
+
if (work.selected && work.statement === "packets" &&
|
|
56
|
+
work.positions !== this.#positionsFor(work.sourceKey, work.fileIndex, work.ownerId)) return null;
|
|
57
|
+
if (work.role === "subtitle-embedded" && ["packets", "subtitle-cues"].includes(work.statement) &&
|
|
58
|
+
work.positions !== this.#positionsFor(work.sourceKey, work.fileIndex, work.ownerId, true)) return null;
|
|
59
|
+
const owners = this.#viewers.forSource(work.sourceKey).filter(viewer => {
|
|
60
|
+
if (work.ownerId !== undefined && viewer.id !== work.ownerId) return false;
|
|
61
|
+
if (work.role === "subtitle-embedded" && (viewer.subtitle?.fileIndex !== work.fileIndex ||
|
|
62
|
+
viewer.subtitle.trackIndex !== work.trackIndex)) return false;
|
|
63
|
+
const selected = viewer.source.selectedFileIndex;
|
|
64
|
+
if (work.ownerFileIndex !== undefined) return selected === work.ownerFileIndex &&
|
|
65
|
+
(work.role !== "source-rest" || viewer.outputs.size === 0);
|
|
66
|
+
if (work.statement !== "packets") return selected === null || selected === work.fileIndex;
|
|
67
|
+
if (selected !== null) return selected === work.fileIndex && viewer.outputs.size === 0 && work.selected;
|
|
68
|
+
return !work.selected && viewer.source.visibleFileIndices.includes(work.fileIndex);
|
|
69
|
+
});
|
|
70
|
+
if (!owners.length) return null;
|
|
71
|
+
const selected = work.ownerFileIndex === undefined && owners.some(viewer => viewer.source.selectedFileIndex !== null);
|
|
72
|
+
const base = work.role === "source-rest" ? 1 : work.role === "next-episode" ? 5 : work.ownerFileIndex !== undefined ? 10 : 100;
|
|
73
|
+
const demands = owners.map(viewer => {
|
|
74
|
+
const chosenSubtitle = ["subtitle", "subtitle-embedded"].includes(work.role) && viewer.subtitle?.fileIndex === work.fileIndex;
|
|
75
|
+
const ownBase = chosenSubtitle ? 100 : base;
|
|
76
|
+
const priority = this.#priorityFor(viewer, ownBase, work);
|
|
77
|
+
return { priority, urgent: (selected || chosenSubtitle) && priority === ownBase };
|
|
78
|
+
});
|
|
79
|
+
const priority = Math.max(...demands.map(demand => demand.priority));
|
|
80
|
+
const urgent = demands.some(demand => demand.urgent);
|
|
81
|
+
return { priority,
|
|
82
|
+
urgent, deadlineAt: urgent ? 0 : Infinity, order: work.order ?? 0 };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
accepts(work) { return this.#work.get(work.key) === work && this.demandFor(work) !== null; }
|
|
86
|
+
|
|
87
|
+
/** A retired torrent cannot retain completed preparation or accept late reads. */
|
|
88
|
+
forget(sourceKey) {
|
|
89
|
+
this.#revision++;
|
|
90
|
+
this.#candidateAbort.abort();
|
|
91
|
+
this.#candidateAbort = new AbortController();
|
|
92
|
+
for (const [key, work] of this.#work) {
|
|
93
|
+
if (work.sourceKey !== sourceKey) continue;
|
|
94
|
+
this.#work.delete(key);
|
|
95
|
+
this.#withdraw(work);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
urgentReadyFor(viewer) {
|
|
100
|
+
if (!this.subtitleReadyFor(viewer)) return false;
|
|
101
|
+
return [...this.#work.values()].some(work => work.selected && work.ownerId === viewer.id &&
|
|
102
|
+
work.urgentReady === true && viewer.source?.sourceKey === work.sourceKey &&
|
|
103
|
+
viewer.source.selectedFileIndex === work.fileIndex && viewer.outputs.size === 0 &&
|
|
104
|
+
work.positions === this.#positionsFor(work.sourceKey, work.fileIndex, viewer.id));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
subtitleReadyFor(viewer) {
|
|
108
|
+
const selection = viewer.subtitle;
|
|
109
|
+
if (!selection) return true;
|
|
110
|
+
return [...this.#work.values()].some(work => work.sourceKey === selection.sourceKey &&
|
|
111
|
+
work.fileIndex === selection.fileIndex && ["result", "terminal"].includes(work.result) &&
|
|
112
|
+
(selection.trackIndex === null ? work.statement === "subtitle-file" && work.ownerFileIndex === viewer.source.selectedFileIndex
|
|
113
|
+
: work.statement === "subtitle-cues" && work.ownerId === viewer.id && work.trackIndex === selection.trackIndex &&
|
|
114
|
+
work.positions === this.#positionsFor(work.sourceKey, work.fileIndex, viewer.id, true)));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async bytesChanged(sourceKey, fileIndex) {
|
|
118
|
+
let changed = false;
|
|
119
|
+
for (const work of this.#work.values()) {
|
|
120
|
+
if (!work.selected || work.sourceKey !== sourceKey || work.fileIndex !== fileIndex || !work.inputRanges) continue;
|
|
121
|
+
const revision = work.readinessRevision = (work.readinessRevision ?? 0) + 1;
|
|
122
|
+
const ready = await this.#inputReady(work);
|
|
123
|
+
if (revision !== work.readinessRevision || !this.accepts(work)) continue;
|
|
124
|
+
if (work.urgentReady !== ready) {
|
|
125
|
+
work.urgentReady = ready;
|
|
126
|
+
changed = true;
|
|
127
|
+
this.#readyChanged(work, ready);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (changed) for (const work of this.#work.values()) {
|
|
131
|
+
const demand = this.demandFor(work);
|
|
132
|
+
if (demand) this.#reprice(work, demand);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
ownsFile(sourceKey, fileIndex) {
|
|
137
|
+
return [...this.#work.values()].some(work => work.sourceKey === sourceKey &&
|
|
138
|
+
work.fileIndex === fileIndex && this.demandFor(work));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#positionsFor(sourceKey, fileIndex, ownerId, includeOutputs = false) {
|
|
142
|
+
return JSON.stringify(this.#viewers.forSource(sourceKey)
|
|
143
|
+
.filter(viewer => viewer.source.selectedFileIndex === fileIndex && (includeOutputs || viewer.outputs.size === 0) &&
|
|
144
|
+
(ownerId === undefined || viewer.id === ownerId))
|
|
145
|
+
.map(viewer => [viewer.id, viewer.position?.seconds ?? 0]).sort((left, right) => left[0].localeCompare(right[0])));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
result(work, result) {
|
|
149
|
+
const current = this.#work.get(work.key);
|
|
150
|
+
if (current !== work || !this.demandFor(work)) return;
|
|
151
|
+
current.result = result.kind;
|
|
152
|
+
if (!["needs-ranges", "needs-memory"].includes(result.kind)) void this.refresh();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async #run() {
|
|
156
|
+
let revision;
|
|
157
|
+
do {
|
|
158
|
+
revision = this.#revision;
|
|
159
|
+
for (const sourceKey of this.#viewers.sourceKeys()) {
|
|
160
|
+
let candidates;
|
|
161
|
+
try { candidates = await this.#availableCandidates(sourceKey, this.#candidateAbort.signal); }
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error?.name === "AbortError") break;
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
if (revision !== this.#revision) break;
|
|
167
|
+
const people = this.#viewers.forSource(sourceKey);
|
|
168
|
+
const selected = people.map(viewer => viewer.source.selectedFileIndex).filter(index => index !== null);
|
|
169
|
+
const files = [...new Set([...candidates, ...selected])];
|
|
170
|
+
const related = [...new Set(selected)].flatMap(ownerFileIndex =>
|
|
171
|
+
this.#related(sourceKey, ownerFileIndex).map(file => ({ ...file, ownerFileIndex })));
|
|
172
|
+
for (const viewer of people) {
|
|
173
|
+
if (viewer.subtitle?.trackIndex !== null && viewer.subtitle?.trackIndex !== undefined &&
|
|
174
|
+
viewer.subtitle.fileIndex === viewer.source.selectedFileIndex) {
|
|
175
|
+
related.push({ fileIndex: viewer.subtitle.fileIndex, ownerFileIndex: viewer.source.selectedFileIndex,
|
|
176
|
+
ownerId: viewer.id, trackIndex: viewer.subtitle.trackIndex, role: "subtitle-embedded" });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const [key, work] of this.#work) {
|
|
180
|
+
if (work.sourceKey === sourceKey && !files.includes(work.fileIndex) &&
|
|
181
|
+
!related.some(file => file.fileIndex === work.fileIndex && file.ownerFileIndex === work.ownerFileIndex)) {
|
|
182
|
+
this.#work.delete(key);
|
|
183
|
+
this.#withdraw(work);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const metadata = [];
|
|
187
|
+
for (const [order, fileIndex] of files.entries()) {
|
|
188
|
+
for (const statement of ["tracks", "media-info"]) {
|
|
189
|
+
if (revision !== this.#revision) break;
|
|
190
|
+
const work = this.#request({ sourceKey, fileIndex, statement, order });
|
|
191
|
+
if (!this.demandFor(work)) continue;
|
|
192
|
+
metadata.push(work);
|
|
193
|
+
await this.#read(work);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (revision !== this.#revision || metadata.some(work => !["result", "terminal"].includes(work.result))) continue;
|
|
197
|
+
for (const [order, fileIndex] of files.entries()) {
|
|
198
|
+
if (revision !== this.#revision) break;
|
|
199
|
+
const selections = [{ selected: false }, ...people
|
|
200
|
+
.filter(viewer => viewer.source.selectedFileIndex === fileIndex && viewer.outputs.size === 0)
|
|
201
|
+
.map(viewer => ({ selected: true, ownerId: viewer.id }))];
|
|
202
|
+
for (const selection of selections) {
|
|
203
|
+
const work = this.#request({ sourceKey, fileIndex, statement: "packets", ...selection, order });
|
|
204
|
+
if (this.demandFor(work)) await this.#read(work);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const [order, file] of related.entries()) {
|
|
208
|
+
if (revision !== this.#revision) break;
|
|
209
|
+
if (file.role === "subtitle") {
|
|
210
|
+
await this.#read(this.#request({ sourceKey, ...file, statement: "subtitle-file", order: files.length + order }));
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const declarations = [];
|
|
214
|
+
for (const statement of ["tracks", "media-info"]) {
|
|
215
|
+
const work = this.#request({ sourceKey, ...file, statement, order: files.length + order });
|
|
216
|
+
declarations.push(work);
|
|
217
|
+
await this.#read(work);
|
|
218
|
+
}
|
|
219
|
+
if (declarations.some(work => work.result !== "result")) continue;
|
|
220
|
+
const packets = this.#request({ sourceKey, ...file, statement: "packets", order: files.length + order });
|
|
221
|
+
await this.#read(packets);
|
|
222
|
+
if (file.role === "subtitle-embedded" && packets.result === "result") {
|
|
223
|
+
await this.#read(this.#request({ sourceKey, ...file, statement: "subtitle-cues", order: files.length + order }));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} while (revision !== this.#revision);
|
|
228
|
+
this.#completedRevision = revision;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#request(address) {
|
|
232
|
+
if (address.selected && address.statement === "packets") {
|
|
233
|
+
address = { ...address, positions: this.#positionsFor(address.sourceKey, address.fileIndex, address.ownerId) };
|
|
234
|
+
}
|
|
235
|
+
if (address.role === "subtitle-embedded" && ["packets", "subtitle-cues"].includes(address.statement)) {
|
|
236
|
+
address = { ...address, positions: this.#positionsFor(address.sourceKey, address.fileIndex, address.ownerId, true) };
|
|
237
|
+
}
|
|
238
|
+
const key = JSON.stringify([address.sourceKey, address.fileIndex, address.statement, address.selected ?? null,
|
|
239
|
+
address.ownerFileIndex ?? null, address.role ?? null, address.positions ?? null, address.ownerId ?? null, address.trackIndex ?? null]);
|
|
240
|
+
let work = this.#work.get(key);
|
|
241
|
+
if (work && work.order !== address.order) {
|
|
242
|
+
work.order = address.order;
|
|
243
|
+
const demand = this.demandFor(work);
|
|
244
|
+
if (demand) this.#reprice(work, demand);
|
|
245
|
+
}
|
|
246
|
+
if (!work) {
|
|
247
|
+
work = { ...address, key, requestId: `prepare:${key}`, result: null };
|
|
248
|
+
if (this.demandFor(work)) this.#work.set(key, work);
|
|
249
|
+
}
|
|
250
|
+
return work;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#availableCandidates(sourceKey, signal) {
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
let done = false;
|
|
256
|
+
const finish = (error, value) => {
|
|
257
|
+
if (done) return;
|
|
258
|
+
done = true;
|
|
259
|
+
signal.removeEventListener("abort", aborted);
|
|
260
|
+
if (error) reject(error); else resolve(value);
|
|
261
|
+
};
|
|
262
|
+
const aborted = () => finish(new DOMException("Source preparation was cancelled.", "AbortError"));
|
|
263
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
264
|
+
if (signal.aborted) { aborted(); return; }
|
|
265
|
+
Promise.resolve().then(() => {
|
|
266
|
+
if (signal.aborted) throw new DOMException("Source preparation was cancelled.", "AbortError");
|
|
267
|
+
return this.#candidates(sourceKey);
|
|
268
|
+
}).then(value => finish(null, value), error => finish(error));
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async #read(work) {
|
|
273
|
+
if (work.result !== null || !this.demandFor(work)) return;
|
|
274
|
+
const result = await this.#inspect(work);
|
|
275
|
+
if (this.#work.get(work.key) === work && this.demandFor(work)) work.result = result.kind;
|
|
276
|
+
if (work.selected && work.inputRanges) await this.bytesChanged(work.sourceKey, work.fileIndex);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -192,6 +192,8 @@ export class Viewer {
|
|
|
192
192
|
* @type {Set<string>}
|
|
193
193
|
*/
|
|
194
194
|
this.outputs = new Set();
|
|
195
|
+
/** Source selection exists before any prepared output. */
|
|
196
|
+
this.source = null;
|
|
195
197
|
/**
|
|
196
198
|
* Which output answers THIS viewer, and for how long that stands.
|
|
197
199
|
*
|
|
@@ -225,6 +227,7 @@ export class Viewer {
|
|
|
225
227
|
* @type {Set<string>}
|
|
226
228
|
*/
|
|
227
229
|
this.wantsCuesFor = new Set();
|
|
230
|
+
this.subtitle = null;
|
|
228
231
|
// WHETHER THE PICTURE IS MOVING, and separately WHETHER THIS VIEWER IS
|
|
229
232
|
// BLOCKED ON US. Two booleans, because there are three states and one
|
|
230
233
|
// boolean cannot hold them:
|
|
@@ -254,6 +257,8 @@ export class Viewer {
|
|
|
254
257
|
// the instant they arrive, without the position ever being invented.
|
|
255
258
|
this.playing = false;
|
|
256
259
|
this.waiting = true;
|
|
260
|
+
/** When explicit paused state began; waiting for media is not a pause. */
|
|
261
|
+
this.pausedAt = null;
|
|
257
262
|
// Whether the page carrying this viewer is ON SCREEN, and whether the
|
|
258
263
|
// picture has been pulled out of it.
|
|
259
264
|
//
|
|
@@ -312,6 +317,7 @@ export class Viewer {
|
|
|
312
317
|
return;
|
|
313
318
|
}
|
|
314
319
|
this.#place(seconds, now);
|
|
320
|
+
if (this.pausedAt !== null) this.pausedAt = now;
|
|
315
321
|
// The buffer's history belongs to the place they left.
|
|
316
322
|
this.bufferReadings = [];
|
|
317
323
|
// WHAT THEY HELD AT THE PLACE THEY LEFT SAYS NOTHING ABOUT THE PLACE THEY
|
|
@@ -475,6 +481,11 @@ export class Viewer {
|
|
|
475
481
|
// material, which is the direction that costs an encoder rather than a
|
|
476
482
|
// viewer.
|
|
477
483
|
this.waiting = waiting === undefined ? this.playing === false && held === 0 : waiting === true;
|
|
484
|
+
if (!this.playing && !this.waiting) {
|
|
485
|
+
if (this.pausedAt === null) this.pausedAt = now;
|
|
486
|
+
} else {
|
|
487
|
+
this.pausedAt = null;
|
|
488
|
+
}
|
|
478
489
|
this.inPictureInPicture = inPictureInPicture === undefined ? false : Boolean(inPictureInPicture);
|
|
479
490
|
this.onScreen = onScreen === undefined ? true : Boolean(onScreen);
|
|
480
491
|
this.seen(now);
|
|
@@ -50,6 +50,79 @@ export class Viewers {
|
|
|
50
50
|
this.#onChange = typeof onChange === "function" ? onChange : () => {};
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/** Register presence before source metadata or an output exists. */
|
|
54
|
+
present(consumerId, now = Date.now()) {
|
|
55
|
+
if (typeof consumerId !== "string" || !consumerId) throw new TypeError("A viewer needs a name.");
|
|
56
|
+
const known = this.#byId.get(consumerId);
|
|
57
|
+
const viewer = known ?? new Viewer(consumerId, now);
|
|
58
|
+
viewer.seen(now);
|
|
59
|
+
viewer.gone = false;
|
|
60
|
+
this.#byId.set(consumerId, viewer);
|
|
61
|
+
if (!known) this.#onChange();
|
|
62
|
+
return viewer;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Bind the same viewer to their source before metadata preparation. */
|
|
66
|
+
selectsSource(consumerId, sourceKey, now = Date.now()) {
|
|
67
|
+
if (typeof sourceKey !== "string" || !sourceKey) throw new TypeError("A selected source needs a key.");
|
|
68
|
+
const viewer = this.present(consumerId, now);
|
|
69
|
+
if (viewer.source?.sourceKey !== sourceKey) {
|
|
70
|
+
viewer.source = { sourceKey, visibleFileIndices: [], selectedFileIndex: null };
|
|
71
|
+
viewer.subtitle = null;
|
|
72
|
+
this.#onChange();
|
|
73
|
+
}
|
|
74
|
+
return viewer;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
selectsFile(consumerId, sourceKey, fileIndex, now = Date.now(), selection = {}) {
|
|
78
|
+
if (!Number.isSafeInteger(fileIndex) || fileIndex < 0) throw new TypeError("A selected file needs its source index.");
|
|
79
|
+
const viewer = this.selectsSource(consumerId, sourceKey, now);
|
|
80
|
+
const changed = viewer.source.selectedFileIndex !== fileIndex;
|
|
81
|
+
if (changed) {
|
|
82
|
+
viewer.source.selectedFileIndex = fileIndex;
|
|
83
|
+
viewer.source.generation = 0;
|
|
84
|
+
viewer.subtitle = null;
|
|
85
|
+
}
|
|
86
|
+
if (changed || Number.isFinite(selection.positionSeconds)) viewer.moveTo(selection.positionSeconds ?? 0, now);
|
|
87
|
+
if (typeof selection.wantsToPlay === "boolean") {
|
|
88
|
+
viewer.playing = false;
|
|
89
|
+
viewer.waiting = selection.wantsToPlay;
|
|
90
|
+
viewer.pausedAt = selection.wantsToPlay ? null : viewer.pausedAt ?? now;
|
|
91
|
+
}
|
|
92
|
+
if (changed || Object.keys(selection).length) this.#onChange();
|
|
93
|
+
return viewer;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
visibleFiles(consumerId, sourceKey, indices, now = Date.now()) {
|
|
97
|
+
if (!Array.isArray(indices) || indices.some(index => !Number.isSafeInteger(index) || index < 0)) {
|
|
98
|
+
throw new TypeError("Visible files need source indices.");
|
|
99
|
+
}
|
|
100
|
+
const viewer = this.selectsSource(consumerId, sourceKey, now);
|
|
101
|
+
const visible = [...new Set(indices)];
|
|
102
|
+
if (JSON.stringify(viewer.source.visibleFileIndices) !== JSON.stringify(visible)) {
|
|
103
|
+
viewer.source.visibleFileIndices = visible;
|
|
104
|
+
this.#onChange();
|
|
105
|
+
}
|
|
106
|
+
return viewer;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Accept a source report only while its original selection is still present. */
|
|
110
|
+
reportSource(consumerId, sourceKey, fileIndex, report, now = Date.now()) {
|
|
111
|
+
const viewer = this.get(consumerId);
|
|
112
|
+
if (!viewer || viewer.gone || viewer.outputs.size !== 0 ||
|
|
113
|
+
viewer.source?.sourceKey !== sourceKey || viewer.source.selectedFileIndex !== fileIndex) return false;
|
|
114
|
+
if (report.generation !== undefined) {
|
|
115
|
+
if (!Number.isSafeInteger(report.generation) || report.generation < (viewer.source.generation ?? 0)) return false;
|
|
116
|
+
viewer.source.generation = report.generation;
|
|
117
|
+
}
|
|
118
|
+
if (report.seek === true && Number.isFinite(report.positionSeconds) && report.positionSeconds >= 0) {
|
|
119
|
+
viewer.moveTo(report.positionSeconds, now);
|
|
120
|
+
}
|
|
121
|
+
viewer.report(report, now);
|
|
122
|
+
this.#onChange();
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
53
126
|
/**
|
|
54
127
|
* This viewer, watching this output.
|
|
55
128
|
*
|
|
@@ -106,6 +179,15 @@ export class Viewers {
|
|
|
106
179
|
return this.#byId.get(consumerId) ?? null;
|
|
107
180
|
}
|
|
108
181
|
|
|
182
|
+
sourceKeys() {
|
|
183
|
+
return new Set([...this.#byId.values()].filter(viewer => viewer.isPresent() && viewer.source)
|
|
184
|
+
.map(viewer => viewer.source.sourceKey));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
forSource(sourceKey) {
|
|
188
|
+
return [...this.#byId.values()].filter(viewer => viewer.isPresent() && viewer.source?.sourceKey === sourceKey);
|
|
189
|
+
}
|
|
190
|
+
|
|
109
191
|
/**
|
|
110
192
|
* A viewer of one output, without creating one.
|
|
111
193
|
*
|
|
@@ -244,7 +326,7 @@ export class Viewers {
|
|
|
244
326
|
return false;
|
|
245
327
|
}
|
|
246
328
|
viewer.outputs.delete(output.id);
|
|
247
|
-
if (viewer.outputs.size === 0) {
|
|
329
|
+
if (viewer.outputs.size === 0 && !viewer.source) {
|
|
248
330
|
// Watching nothing at all: this is a statement that they are gone, and
|
|
249
331
|
// not merely that this one output is no longer theirs.
|
|
250
332
|
viewer.markGone();
|
|
@@ -287,7 +369,7 @@ export class Viewers {
|
|
|
287
369
|
for (const [consumerId, viewer] of this.#byId) {
|
|
288
370
|
if (!viewer.outputs.delete(outputId)) continue;
|
|
289
371
|
if (viewer.activeVariantId === outputId) viewer.activeVariantId = null;
|
|
290
|
-
if (viewer.outputs.size === 0) {
|
|
372
|
+
if (viewer.outputs.size === 0 && !viewer.source) {
|
|
291
373
|
viewer.markGone();
|
|
292
374
|
this.#byId.delete(consumerId);
|
|
293
375
|
}
|
|
@@ -334,6 +416,31 @@ export class Viewers {
|
|
|
334
416
|
return true;
|
|
335
417
|
}
|
|
336
418
|
|
|
419
|
+
/** Record the chosen subtitle without creating or restoring a viewer. */
|
|
420
|
+
selectsSubtitle(consumerId, sourceKey, fileIndex, trackIndex = null) {
|
|
421
|
+
const viewer = this.get(consumerId);
|
|
422
|
+
if (!viewer || viewer.gone || viewer.source?.sourceKey !== sourceKey ||
|
|
423
|
+
!Number.isSafeInteger(fileIndex) || fileIndex < 0 ||
|
|
424
|
+
(trackIndex !== null && (!Number.isSafeInteger(trackIndex) || trackIndex < 0))) return false;
|
|
425
|
+
if (trackIndex !== null && viewer.source.selectedFileIndex !== fileIndex) return false;
|
|
426
|
+
const selection = { sourceKey, fileIndex, trackIndex };
|
|
427
|
+
if (JSON.stringify(viewer.subtitle) !== JSON.stringify(selection)) {
|
|
428
|
+
viewer.subtitle = selection;
|
|
429
|
+
this.#onChange();
|
|
430
|
+
}
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
clearsSubtitle(consumerId, sourceKey) {
|
|
435
|
+
const viewer = this.get(consumerId);
|
|
436
|
+
if (!viewer || viewer.gone || viewer.source?.sourceKey !== sourceKey) return false;
|
|
437
|
+
if (viewer.subtitle !== null) {
|
|
438
|
+
viewer.subtitle = null;
|
|
439
|
+
this.#onChange();
|
|
440
|
+
}
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
|
|
337
444
|
/**
|
|
338
445
|
* Who has subtitles switched on for this file.
|
|
339
446
|
*
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* exhausting the continuous media available for the required tracks.
|
|
4
4
|
*
|
|
5
5
|
* This module contains the playback decision's only model. Its inputs are
|
|
6
|
-
* measurements and physical limits:
|
|
6
|
+
* measurements and physical limits: mapped whole-piece arrivals, encoded
|
|
7
7
|
* media and encoder rate, segment sizes and client-link rate, existing
|
|
8
8
|
* continuous media, and measured supply interruptions. Each service integral
|
|
9
9
|
* extends its latest measured rate until the next observation. The forecast
|
|
@@ -99,8 +99,8 @@ export class RateTrend {
|
|
|
99
99
|
* @param {number} input.lookaheadSeconds
|
|
100
100
|
* @param {number} input.now
|
|
101
101
|
* @param {boolean} [input.requiredAudio]
|
|
102
|
-
* @param {Array<{ id: string,
|
|
103
|
-
*
|
|
102
|
+
* @param {Array<{ id: string, complete: boolean,
|
|
103
|
+
* downloadForecast?: { ranges: Array<{ start: number, end: number, availableAt: number | null }> } }>} input.sources
|
|
104
104
|
* @param {Array<{ id: string, sourceIds: string[], processedSeconds: number,
|
|
105
105
|
* bitsPerMediaSecond: number, readings: Array<{ at: number, value: number }>,
|
|
106
106
|
* clockOffsetSeconds?: number, clientRanges?: Array<{ start: number, end: number }>,
|
|
@@ -137,10 +137,10 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
137
137
|
]))
|
|
138
138
|
]);
|
|
139
139
|
const zero = mediaTime(0n, unit);
|
|
140
|
-
const segmentsOf = tracks.map((track, trackIndex) =>
|
|
140
|
+
const segmentsOf = tracks.map((track, trackIndex) => predictedMediaSegments(
|
|
141
141
|
(Array.isArray(track.segments) ? track.segments : []).map((segment) => ({
|
|
142
142
|
...segment, ranges: playerRanges(segment.mediaRanges, rescale(clocks[trackIndex], unit), unit)
|
|
143
|
-
})));
|
|
143
|
+
})), unit));
|
|
144
144
|
const origin = latest(zero, ...segmentsOf.map((segments) =>
|
|
145
145
|
segments.find(({ index }) => index === 0)?.ranges?.[0]?.start ?? zero));
|
|
146
146
|
const at = latest(origin, rescale(fromSeconds(finiteNonNegative(input.positionSeconds)), unit));
|
|
@@ -182,7 +182,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
182
182
|
curve: rateCurve(track.readings, now),
|
|
183
183
|
bitsPerMediaSecond: averageBitsPerMediaSecond(track, track.segments,
|
|
184
184
|
new Set(Array.isArray(track.readySegmentIndices) ? track.readySegmentIndices : [])),
|
|
185
|
-
segments:
|
|
185
|
+
segments: segmentsOf[trackIndex]
|
|
186
186
|
.filter((segment) => Number.isInteger(segment?.index) &&
|
|
187
187
|
Number.isFinite(segment?.startSeconds) && Number.isFinite(segment?.endSeconds) &&
|
|
188
188
|
segment.endSeconds > segment.startSeconds && segment.endSeconds > position)
|
|
@@ -192,19 +192,8 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
192
192
|
return result(false, null, buffered, reserve, null, "timeline-unavailable", 0);
|
|
193
193
|
}
|
|
194
194
|
const sourceState = new Map();
|
|
195
|
-
const downloadServices = new Map();
|
|
196
195
|
for (const [id, source] of sources) {
|
|
197
|
-
|
|
198
|
-
const service = downloadServices.get(serviceId);
|
|
199
|
-
if (!service || (!service.curve && source.complete !== true)) {
|
|
200
|
-
downloadServices.set(serviceId, {
|
|
201
|
-
curve: source.complete === true ? null : rateCurve(source.readings, now)
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
sourceState.set(id, {
|
|
205
|
-
source,
|
|
206
|
-
serviceId
|
|
207
|
-
});
|
|
196
|
+
sourceState.set(id, { source });
|
|
208
197
|
}
|
|
209
198
|
const link = rateCurve(input.linkReadings, now);
|
|
210
199
|
let unknownReason = null;
|
|
@@ -246,19 +235,28 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
246
235
|
Number.isFinite(range?.start) && Number.isFinite(range?.end) && range.end > range.start));
|
|
247
236
|
const encodeWork = produced ? 0 : remainingProcessing.reduce((sum, part) => sum + part.end - part.start, 0);
|
|
248
237
|
|
|
249
|
-
const
|
|
238
|
+
const sourceByteIntervals = new Map();
|
|
250
239
|
if (!produced && encodeWork > 0) {
|
|
251
|
-
|
|
240
|
+
const exactInputs = Array.isArray(segment.sourceInputs) ? segment.sourceInputs : null;
|
|
241
|
+
const requiredSources = new Set([
|
|
242
|
+
...(Array.isArray(track.sourceIds) ? track.sourceIds : []),
|
|
243
|
+
...(exactInputs ?? []).map(input => input?.sourceId)
|
|
244
|
+
]);
|
|
245
|
+
for (const sourceId of requiredSources) {
|
|
252
246
|
const source = sourceState.get(sourceId);
|
|
253
247
|
if (!source) {
|
|
254
248
|
unknownReason ??= "source-measurement-unavailable";
|
|
255
249
|
} else if (source.source.complete !== true) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
250
|
+
if (exactInputs) {
|
|
251
|
+
const inputs = exactInputs.filter(input => input?.sourceId === sourceId);
|
|
252
|
+
const ranges = inputs.flatMap(input => Array.isArray(input.ranges) ? input.ranges : []);
|
|
253
|
+
if (!ranges.length || inputs.some(input => !Array.isArray(input.ranges) || !input.ranges.length) ||
|
|
254
|
+
ranges.some(range => !range || !Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) || range.start < 0 || range.end <= range.start)) {
|
|
255
|
+
unknownReason ??= "source-input-ranges-unavailable";
|
|
256
|
+
} else sourceByteIntervals.set(sourceId, ranges);
|
|
257
|
+
continue;
|
|
261
258
|
}
|
|
259
|
+
unknownReason ??= "source-input-ranges-unavailable";
|
|
262
260
|
}
|
|
263
261
|
}
|
|
264
262
|
}
|
|
@@ -272,8 +270,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
272
270
|
ahead: Math.max(0, segment.startSeconds - position),
|
|
273
271
|
sizeBits,
|
|
274
272
|
encodeWork,
|
|
275
|
-
|
|
276
|
-
sourceIntervals,
|
|
273
|
+
sourceByteIntervals,
|
|
277
274
|
produced
|
|
278
275
|
});
|
|
279
276
|
}
|
|
@@ -295,17 +292,20 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
295
292
|
// scheduled in media order, independently of browser storage capacity.
|
|
296
293
|
// The earliest legal start is the supremum of completion lateness against
|
|
297
294
|
// presentation deadlines, not a search over guessed delay candidates.
|
|
298
|
-
const
|
|
299
|
-
const sourceMediaService = new Map();
|
|
295
|
+
const sourceByteService = new Map();
|
|
300
296
|
for (const [id, { source }] of sourceState) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
.
|
|
297
|
+
sourceByteService.set(id, Array.isArray(source.residence) ? source.residence
|
|
298
|
+
.filter(({ location, start, end }) => location !== "missing" && Number.isFinite(start) && Number.isFinite(end) && end > start)
|
|
299
|
+
.map(({ start, end }) => ({ start, end, begin: 0, finish: 0 })) : []);
|
|
300
|
+
if (source.downloadForecast) {
|
|
301
|
+
for (const range of source.downloadForecast.ranges ?? []) {
|
|
302
|
+
if (!Number.isFinite(range?.availableAt) || !Number.isSafeInteger(range.start) ||
|
|
303
|
+
!Number.isSafeInteger(range.end) || range.start < 0 || range.end <= range.start) continue;
|
|
304
|
+
const arrival = Math.max(0, (range.availableAt - now) / 1000);
|
|
305
|
+
// The entire protocol piece becomes readable after verification;
|
|
306
|
+
// bytes inside it never arrive proportionally to their file address.
|
|
307
|
+
sourceByteService.get(id).push({ start: range.start, end: range.end, begin: arrival, finish: arrival });
|
|
308
|
+
}
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
311
|
const trackFinish = trackState.map(({ track }) => finiteNonNegative(track.startupRemainingSeconds));
|
|
@@ -313,36 +313,23 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
313
313
|
const completions = [];
|
|
314
314
|
const productions = [];
|
|
315
315
|
for (const job of jobs) {
|
|
316
|
-
const { track, trackIndex, segment, sizeBits, encodeWork,
|
|
316
|
+
const { track, trackIndex, segment, sizeBits, encodeWork, sourceByteIntervals } = job;
|
|
317
317
|
const sourceArrivals = [];
|
|
318
|
-
for (const [sourceId,
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const begin = sourceFinish.get(source.serviceId) ?? 0;
|
|
326
|
-
const end = supply.finish(
|
|
327
|
-
(part.end - part.start) * Number(source.source.bytesPerMediaSecond), begin);
|
|
328
|
-
sourceFinish.set(source.serviceId, end);
|
|
329
|
-
served.push({ ...part, begin, finish: end });
|
|
318
|
+
for (const [sourceId, ranges] of sourceByteIntervals) {
|
|
319
|
+
const served = sourceByteService.get(sourceId) ?? [];
|
|
320
|
+
for (const interval of ranges) {
|
|
321
|
+
if (uncoveredIntervals(interval, served).length) return result(false, null, buffered, reserve, null, "download-schedule-unavailable", preparedSegments);
|
|
322
|
+
for (const part of served.filter(part => part.end > interval.start && part.start < interval.end)) {
|
|
323
|
+
sourceArrivals.push({ at: part.finish });
|
|
324
|
+
}
|
|
330
325
|
}
|
|
331
|
-
|
|
332
|
-
sourceArrivals.push(...served.filter((part) => part.end > interval.start && part.start < interval.end)
|
|
333
|
-
.flatMap((part) => [Math.max(part.start, interval.start), Math.min(part.end, interval.end)]
|
|
334
|
-
.map((media) => ({ media, at: media === part.start ? part.begin : part.begin +
|
|
335
|
-
(part.finish - part.begin) * (media - part.start) / (part.end - part.start) }))));
|
|
326
|
+
sourceByteService.set(sourceId, served);
|
|
336
327
|
}
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
// Reused input keeps its original arrival times; it is neither downloaded
|
|
340
|
-
// twice nor charged the queue start of a later request.
|
|
328
|
+
// Production accepts a complete held segment input. It cannot overlap its
|
|
329
|
+
// own source download; reused input retains its original arrival time.
|
|
341
330
|
const processing = trackState[trackIndex].curve;
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
...sourceArrivals.map(({ media, at }) => processing.finish(
|
|
345
|
-
remainingProcessing.reduce((sum, part) => sum + Math.max(0, part.end - Math.max(part.start, media)), 0), at)));
|
|
331
|
+
const admittedAt = Math.max(trackFinish[trackIndex], ...sourceArrivals.map(({ at }) => at));
|
|
332
|
+
const producedAt = job.produced || encodeWork === 0 ? 0 : processing.finish(encodeWork, admittedAt);
|
|
346
333
|
trackFinish[trackIndex] = Math.max(trackFinish[trackIndex], producedAt);
|
|
347
334
|
productions.push({ at: producedAt, trackIndex, segment, requiresSource: sourceArrivals.some(({ at }) => at > 0) });
|
|
348
335
|
const deliveryAt = link.finish(sizeBits, Math.max(transferFinish, producedAt));
|