@torrent-tv/proxy 2.107.18 → 2.107.20
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/package.json
CHANGED
|
@@ -985,6 +985,9 @@ export class ViewerRequests {
|
|
|
985
985
|
residence: stats?.residence,
|
|
986
986
|
fileOffset: stats?.fileOffset,
|
|
987
987
|
fileLength: stats?.fileLength,
|
|
988
|
+
bytesPerMediaSecond: Number.isFinite(stats?.fileLength) && stats.fileLength > 0 &&
|
|
989
|
+
Number.isFinite(session.file.durationSeconds) && session.file.durationSeconds > 0
|
|
990
|
+
? stats.fileLength / session.file.durationSeconds : null,
|
|
988
991
|
pieceLength: stats?.pieceLength,
|
|
989
992
|
downloadRateReadings: this.#sourceRateReadings(measurement, output.file.sourceKey, stats, now),
|
|
990
993
|
downloadRateSummaries: this.#sourceRateSummaries(measurement, output.file.sourceKey),
|
|
@@ -1022,6 +1025,8 @@ export class ViewerRequests {
|
|
|
1022
1025
|
})),
|
|
1023
1026
|
bitsPerMediaSecond: Number.isFinite(observedMbps) && observedMbps > 0 ? observedMbps * 1_000_000 :
|
|
1024
1027
|
this.#host.initialOutputBitsPerMediaSecondOf?.(output) ?? 0,
|
|
1028
|
+
durationSeconds: output.timeline?.publishedStartOf(output.timeline.segmentCount) ?? session.file.durationSeconds,
|
|
1029
|
+
segmentCount: output.timeline?.segmentCount ?? segments.length,
|
|
1025
1030
|
readings: trackRates,
|
|
1026
1031
|
segments,
|
|
1027
1032
|
geometry: this.#forecastPieces.get(output)?.geometry,
|
|
@@ -1247,7 +1252,27 @@ export class ViewerRequests {
|
|
|
1247
1252
|
}
|
|
1248
1253
|
const kept = forecast.pieces;
|
|
1249
1254
|
const segments = [];
|
|
1250
|
-
|
|
1255
|
+
const lookahead = Number(this.#host.lookaheadSeconds);
|
|
1256
|
+
const start = Number.isFinite(positionSeconds) ? Math.max(0, positionSeconds) : 0;
|
|
1257
|
+
const end = Number.isFinite(lookahead) && lookahead > 0 ? start + lookahead : Number.POSITIVE_INFINITY;
|
|
1258
|
+
let low = 0, high = count;
|
|
1259
|
+
while (low < high) {
|
|
1260
|
+
const middle = (low + high) >>> 1;
|
|
1261
|
+
if (timeline.publishedStartOf(middle + 1) <= start) low = middle + 1;
|
|
1262
|
+
else high = middle;
|
|
1263
|
+
}
|
|
1264
|
+
const first = Math.max(0, low - 1);
|
|
1265
|
+
low = first;
|
|
1266
|
+
high = count;
|
|
1267
|
+
while (low < high) {
|
|
1268
|
+
const middle = (low + high) >>> 1;
|
|
1269
|
+
if (timeline.publishedStartOf(middle) < end) low = middle + 1;
|
|
1270
|
+
else high = middle;
|
|
1271
|
+
}
|
|
1272
|
+
const last = low;
|
|
1273
|
+
const indexes = new Set([0]);
|
|
1274
|
+
for (let index = first; index < last; index++) indexes.add(index);
|
|
1275
|
+
for (const index of indexes) {
|
|
1251
1276
|
const endSeconds = timeline.publishedStartOf(index + 1);
|
|
1252
1277
|
// Only a made piece has media intervals to read. Asked of every piece,
|
|
1253
1278
|
// each one not made cost a `stat` of its file in the store's safety net
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Predict the
|
|
3
|
-
* exhausting the continuous media available for the required tracks.
|
|
2
|
+
* Predict whether the viewer can start from the current position without
|
|
3
|
+
* exhausting the continuous media already available for the required tracks.
|
|
4
4
|
*
|
|
5
5
|
* This module contains the playback decision's only model. Its inputs are
|
|
6
6
|
* measurements and physical limits: mapped whole-piece arrivals, encoded
|
|
7
7
|
* media and encoder rate, segment sizes and client-link rate, existing
|
|
8
|
-
* continuous media, and measured supply interruptions.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* continuous media, and measured supply interruptions. Exact scheduling is
|
|
9
|
+
* limited to the configured near window; the remaining duration is priced by
|
|
10
|
+
* aggregate work per source and track. Each service integral extends its latest
|
|
11
|
+
* measured rate until the next observation. The forecast is conditional on
|
|
12
|
+
* these measured services continuing. Browser storage
|
|
11
13
|
* capacity is not an admission condition. There are no fitted weights or
|
|
12
14
|
* rate multipliers, and no search over candidate start times.
|
|
13
15
|
*
|
|
@@ -157,7 +159,7 @@ export class ProgressRate {
|
|
|
157
159
|
* @param {number} input.now
|
|
158
160
|
* @param {boolean} [input.requiredAudio]
|
|
159
161
|
* @param {Array<{ id: string, serviceKey?: string, complete: boolean, fileOffset?: number,
|
|
160
|
-
* fileLength?: number, pieceLength?: number, downloadRateReadings?: object,
|
|
162
|
+
* fileLength?: number, bytesPerMediaSecond?: number, pieceLength?: number, downloadRateReadings?: object,
|
|
161
163
|
* downloadForecast?: { ranges: Array<{ start: number, end: number, availableAt: number | null }> } }>} input.sources
|
|
162
164
|
* @param {Array<{ id: string, sourceIds: string[], processedSeconds: number,
|
|
163
165
|
* bitsPerMediaSecond: number, readings: Array<{ at: number, value: number }>,
|
|
@@ -182,18 +184,15 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
182
184
|
// starts), the read piece nearest before them (what an unread piece is placed
|
|
183
185
|
// by) and read pieces inside what the page reports holding (their frames).
|
|
184
186
|
//
|
|
185
|
-
// THE NEAR WINDOW AND THE
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
// arrival exists for them. Placing every remaining piece of a two-hour film
|
|
191
|
-
// in exact time on every poll cost 70-1128 ms a call on Home Assistant (r73,
|
|
192
|
-
// r74).
|
|
187
|
+
// THE NEAR WINDOW AND THE SUFFIX. The look-ahead is scheduled with exact
|
|
188
|
+
// segment times and source ranges when known. Missing range metadata is priced
|
|
189
|
+
// from aggregate file density and measured download service without claiming
|
|
190
|
+
// an address or byte. Beyond the look-ahead, {@link scheduleTail} estimates
|
|
191
|
+
// remaining source, encoder and delivery work without visiting every segment.
|
|
193
192
|
const lookahead = finiteNonNegative(input.lookaheadSeconds);
|
|
194
193
|
const horizonEnd = lookahead > 0 ? finiteNonNegative(input.positionSeconds) + lookahead : Number.POSITIVE_INFINITY;
|
|
195
194
|
const tracks = (Array.isArray(input.tracks) ? input.tracks : []).map((track) =>
|
|
196
|
-
Array.isArray(track.segments) ? { ...track,
|
|
195
|
+
Array.isArray(track.segments) ? { ...track, segmentCount: track.segmentCount ?? track.segments.length,
|
|
197
196
|
segments: piecesThatMatter(track, finiteNonNegative(input.positionSeconds), horizonEnd) } : track);
|
|
198
197
|
const duration = finiteNonNegative(input.durationSeconds);
|
|
199
198
|
// Each track's served ticks are placed on the clock the page reports by the
|
|
@@ -266,15 +265,23 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
266
265
|
.sort((left, right) => left.startSeconds - right.startSeconds || left.index - right.index),
|
|
267
266
|
// The pieces after the near window, as the playlist cuts them, and the
|
|
268
267
|
// offset the page's player placed them by.
|
|
269
|
-
tail:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
268
|
+
tail: [],
|
|
269
|
+
tailStartSeconds: horizonEnd,
|
|
270
|
+
durationSeconds: Number.isFinite(track.durationSeconds) ? track.durationSeconds : duration,
|
|
271
|
+
segmentCount: Number.isSafeInteger(track.segmentCount) ? track.segmentCount
|
|
272
|
+
: Math.max(0, ...track.segments.map(segment => segment.index + 1)),
|
|
273
273
|
clockSeconds: Number.isFinite(track.clockOffsetSeconds) ? track.clockOffsetSeconds : 0
|
|
274
274
|
}));
|
|
275
275
|
for (const state of trackState) {
|
|
276
|
-
state.tailEndSeconds =
|
|
277
|
-
? Math.min(duration, state.
|
|
276
|
+
state.tailEndSeconds = Number.isFinite(horizonEnd) && horizonEnd < duration
|
|
277
|
+
? Math.min(duration, state.durationSeconds + state.clockSeconds) : null;
|
|
278
|
+
const endIndex = state.segments.reduce((last, segment) => segment.startSeconds < horizonEnd
|
|
279
|
+
? Math.max(last, segment.index) : last, -1);
|
|
280
|
+
state.tailSegmentCount = Math.max(0, state.segmentCount - endIndex - 1);
|
|
281
|
+
const readyTail = [...state.ready].reduce((count, index) => count + Number(index > endIndex), 0);
|
|
282
|
+
state.tailWorkFraction = state.tailSegmentCount > 0
|
|
283
|
+
? Math.max(0, 1 - Math.min(state.tailSegmentCount, readyTail) / state.tailSegmentCount)
|
|
284
|
+
: 0;
|
|
278
285
|
}
|
|
279
286
|
if (trackState.some(({ segments }) => segments.length === 0)) {
|
|
280
287
|
return result(false, null, buffered, reserve, null, "timeline-unavailable", 0);
|
|
@@ -294,6 +301,13 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
294
301
|
}
|
|
295
302
|
sourceState.set(id, { source, service });
|
|
296
303
|
}
|
|
304
|
+
const sourceUseCounts = new Map();
|
|
305
|
+
for (const { track } of trackState) {
|
|
306
|
+
for (const sourceId of new Set([...(track.sourceIds ?? []),
|
|
307
|
+
...(track.segments ?? []).flatMap(segment => (segment.sourceInputs ?? []).map(item => item.sourceId))])) {
|
|
308
|
+
sourceUseCounts.set(sourceId, (sourceUseCounts.get(sourceId) ?? 0) + 1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
297
311
|
const link = rateCurve(input.linkReadings, now);
|
|
298
312
|
let unknownReason = null;
|
|
299
313
|
let unavailableSource = null;
|
|
@@ -301,9 +315,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
301
315
|
return result(false, null, buffered, reserve, null, "link-rate-unavailable", 0);
|
|
302
316
|
}
|
|
303
317
|
|
|
304
|
-
|
|
305
|
-
sum + segments.filter((segment) => ready.has(segment.index)).length +
|
|
306
|
-
tail.filter((segment) => ready.has(segment.index)).length, 0);
|
|
318
|
+
let preparedSegments = trackState.reduce((sum, { ready }) => sum + ready.size, 0);
|
|
307
319
|
const jobs = [];
|
|
308
320
|
|
|
309
321
|
for (let trackIndex = 0; trackIndex < trackState.length; trackIndex += 1) {
|
|
@@ -344,6 +356,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
344
356
|
const encodeWork = produced ? 0 : remainingProcessing.reduce((sum, part) => sum + part.end - part.start, 0);
|
|
345
357
|
|
|
346
358
|
let sourceByteIntervals = NO_SOURCE_BYTES;
|
|
359
|
+
const estimatedSourceBytes = new Map();
|
|
347
360
|
if (!produced && encodeWork > 0) {
|
|
348
361
|
// Each source the piece reads, in order and once: the track's own, then
|
|
349
362
|
// those its inputs name. Gathered with loops and no copies, because
|
|
@@ -361,11 +374,18 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
361
374
|
unknownReason ??= "source-measurement-unavailable";
|
|
362
375
|
} else if (source.source.complete !== true) {
|
|
363
376
|
const ranges = exactInputs ? inputRangesOf(exactInputs, sourceId) : null;
|
|
364
|
-
if (ranges === null) {
|
|
365
|
-
|
|
377
|
+
if (ranges === null && exactInputs === null) {
|
|
378
|
+
const estimate = estimateUnmappedSourceBytes(source.source, segmentSeconds, input.durationSeconds);
|
|
379
|
+
if (estimate !== null) {
|
|
380
|
+
const bytes = estimate / Math.max(1, sourceUseCounts.get(sourceId) ?? 1);
|
|
381
|
+
estimatedSourceBytes.set(sourceId, bytes);
|
|
382
|
+
} else if (unknownReason === null) {
|
|
366
383
|
unknownReason = "source-input-ranges-unavailable";
|
|
367
384
|
unavailableSource = { sourceId, trackId: track.id, segmentIndex: segment.index, phase: "near" };
|
|
368
385
|
}
|
|
386
|
+
} else if (ranges === null) {
|
|
387
|
+
unknownReason ??= "source-input-ranges-unavailable";
|
|
388
|
+
unavailableSource ??= { sourceId, trackId: track.id, segmentIndex: segment.index, phase: "near" };
|
|
369
389
|
} else {
|
|
370
390
|
if (sourceByteIntervals === NO_SOURCE_BYTES) sourceByteIntervals = new Map();
|
|
371
391
|
sourceByteIntervals.set(sourceId, ranges);
|
|
@@ -384,11 +404,14 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
384
404
|
sizeBits,
|
|
385
405
|
encodeWork,
|
|
386
406
|
sourceByteIntervals,
|
|
407
|
+
estimatedSourceBytes,
|
|
387
408
|
produced
|
|
388
409
|
});
|
|
389
410
|
}
|
|
390
411
|
}
|
|
391
412
|
|
|
413
|
+
preparedSegments = trackState.reduce((sum, { ready }) => sum + ready.size, 0);
|
|
414
|
+
|
|
392
415
|
if (unknownReason) {
|
|
393
416
|
return { ...result(false, null, buffered, reserve, null, unknownReason, preparedSegments),
|
|
394
417
|
...(unavailableSource ? { unavailableSource } : {}) };
|
|
@@ -450,7 +473,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
450
473
|
const completions = [];
|
|
451
474
|
const productions = [];
|
|
452
475
|
for (const job of jobs) {
|
|
453
|
-
const { track, trackIndex, segment, sizeBits, encodeWork, sourceByteIntervals } = job;
|
|
476
|
+
const { track, trackIndex, segment, sizeBits, encodeWork, sourceByteIntervals, estimatedSourceBytes } = job;
|
|
454
477
|
let sourceArrivalAt = 0;
|
|
455
478
|
const plan = { finish: 0 };
|
|
456
479
|
for (const [sourceId, ranges] of sourceByteIntervals) {
|
|
@@ -498,6 +521,21 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
498
521
|
sourceService.finish = finish;
|
|
499
522
|
}
|
|
500
523
|
}
|
|
524
|
+
for (const [sourceId, bytes] of estimatedSourceBytes) {
|
|
525
|
+
if (!(bytes > 0)) continue;
|
|
526
|
+
const source = sourceState.get(sourceId);
|
|
527
|
+
if (!source) {
|
|
528
|
+
return result(false, null, buffered, reserve, null, "source-measurement-unavailable", preparedSegments);
|
|
529
|
+
}
|
|
530
|
+
const { service } = source;
|
|
531
|
+
if (!service.rate || !(service.rate.rateAt(0) > 0)) {
|
|
532
|
+
return { ...result(false, null, buffered, reserve, null,
|
|
533
|
+
service.rate ? "service-not-advancing" : "download-rate-unavailable", preparedSegments),
|
|
534
|
+
unavailableSource: { sourceId, segmentIndex: segment.index, estimatedBytes: bytes } };
|
|
535
|
+
}
|
|
536
|
+
service.finish = service.rate.finish(bytes, Math.max(service.finish, sourceArrivalAt));
|
|
537
|
+
sourceArrivalAt = Math.max(sourceArrivalAt, service.finish);
|
|
538
|
+
}
|
|
501
539
|
// Production accepts a complete held segment input. It cannot overlap its
|
|
502
540
|
// own source download; reused input retains its original arrival time.
|
|
503
541
|
const processing = trackState[trackIndex].curve;
|
|
@@ -592,9 +630,9 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
592
630
|
production.requiresSource, production.sourceArrivalAt, production.sourcePlanned);
|
|
593
631
|
}
|
|
594
632
|
}
|
|
595
|
-
if (trackState.some(({
|
|
596
|
-
const stop = scheduleTail({ trackState, sourceState, sourceByteCoverage,
|
|
597
|
-
link, position, reserve, transferFinish, constrain });
|
|
633
|
+
if (trackState.some(({ tailEndSeconds }) => Number.isFinite(tailEndSeconds))) {
|
|
634
|
+
const stop = scheduleTail({ trackState, sourceState, sourceByteCoverage, runStarts, trackFinish,
|
|
635
|
+
link, position, buffered, reserve, transferFinish, constrain, duration });
|
|
598
636
|
if (stop) {
|
|
599
637
|
return { ...result(false, null, buffered, reserve, null, stop.reason, preparedSegments),
|
|
600
638
|
...(stop.unavailableSource ? { unavailableSource: stop.unavailableSource } : {}) };
|
|
@@ -665,185 +703,90 @@ function isPiece(segment) {
|
|
|
665
703
|
}
|
|
666
704
|
|
|
667
705
|
/**
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
* Where a byte stands in that order is the bytes still missing before it,
|
|
676
|
-
* read off what is held, planned and brought by the near window
|
|
677
|
-
* ({@link missingBefore}); a piece is placed by the last range of it that
|
|
678
|
-
* misses bytes, so a range it reads that is already held — an AVI's index at
|
|
679
|
-
* the end of the file — does not place it at the end of the file. Planned and
|
|
680
|
-
* brought bytes come when they were to come. No exact time, list of arrivals
|
|
681
|
-
* or per-piece object is built: this runs over the rest of the film on every
|
|
682
|
-
* poll (torrent-tv/meta#185).
|
|
706
|
+
* A duration-level estimate of the suffix after the exact near window. Source
|
|
707
|
+
* work uses each file's aggregate byte density and measured download service;
|
|
708
|
+
* processing uses each track's measured rate; delivery uses output bitrates and
|
|
709
|
+
* measured link service. Held source bytes and made segments reduce the
|
|
710
|
+
* estimate. This conditional forecast does not assert exact source ranges or
|
|
711
|
+
* individual frame timing. Its work is bounded by sources and tracks, not by
|
|
712
|
+
* the film's segment count.
|
|
683
713
|
*
|
|
684
714
|
* @returns {{ reason: string, unavailableSource?: object } | null} Why there is no answer, or null.
|
|
685
715
|
*/
|
|
686
|
-
function scheduleTail({ trackState, sourceState, sourceByteCoverage,
|
|
687
|
-
position, reserve, transferFinish, constrain }) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
state = { coverage, heldBefore, asked: null, orders: [], finishes: [] };
|
|
697
|
-
supply.set(sourceId, state);
|
|
698
|
-
}
|
|
699
|
-
return state;
|
|
700
|
-
};
|
|
701
|
-
const next = trackState.map(() => 0);
|
|
702
|
-
const plan = { finish: 0 };
|
|
703
|
-
let delivered = transferFinish;
|
|
704
|
-
for (;;) {
|
|
705
|
-
// The next piece in media order across the tracks, as the near window orders them.
|
|
706
|
-
let trackIndex = -1;
|
|
707
|
-
for (let index = 0; index < trackState.length; index += 1) {
|
|
708
|
-
const candidate = trackState[index].tail[next[index]];
|
|
709
|
-
if (!candidate) continue;
|
|
710
|
-
const best = trackIndex < 0 ? null : trackState[trackIndex].tail[next[trackIndex]];
|
|
711
|
-
if (!best || candidate.startSeconds < best.startSeconds ||
|
|
712
|
-
(candidate.startSeconds === best.startSeconds && candidate.endSeconds < best.endSeconds)) trackIndex = index;
|
|
713
|
-
}
|
|
714
|
-
if (trackIndex < 0) return null;
|
|
715
|
-
const state = trackState[trackIndex];
|
|
716
|
-
const { track } = state;
|
|
717
|
-
const segment = state.tail[next[trackIndex]++];
|
|
718
|
-
const startsAt = segment.startSeconds + state.clockSeconds;
|
|
719
|
-
const endsAt = segment.endSeconds + state.clockSeconds;
|
|
720
|
-
// Media the page already holds is not transferred again.
|
|
721
|
-
if (Array.isArray(track.clientRanges) && track.clientRanges.some((range) =>
|
|
722
|
-
range?.start <= Math.max(startsAt, position) && range?.end >= endsAt)) continue;
|
|
723
|
-
const exactBytes = sizeOf(track.segmentSizesBytes, segment.index);
|
|
724
|
-
const sizeBits = exactBytes > 0 ? exactBytes * 8 : state.bitsPerMediaSecond * (segment.endSeconds - segment.startSeconds);
|
|
725
|
-
if (!(sizeBits > 0)) return { reason: "segment-size-unavailable" };
|
|
726
|
-
const produced = state.ready.has(segment.index);
|
|
727
|
-
let work = 0;
|
|
728
|
-
if (!produced) {
|
|
729
|
-
work = segment.endSeconds - segment.startSeconds;
|
|
730
|
-
for (const range of Array.isArray(track.processingRanges) ? track.processingRanges : []) {
|
|
731
|
-
if (Number.isFinite(range?.start) && Number.isFinite(range?.end)) {
|
|
732
|
-
work -= Math.max(0, Math.min(range.end, segment.endSeconds) - Math.max(range.start, segment.startSeconds));
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
work = Math.max(0, work);
|
|
736
|
-
}
|
|
737
|
-
let sourceArrivalAt = 0;
|
|
738
|
-
plan.finish = 0;
|
|
739
|
-
if (work > 0) {
|
|
740
|
-
if (!state.curve) return { reason: "encode-rate-unavailable" };
|
|
741
|
-
const trackSources = Array.isArray(track.sourceIds) ? track.sourceIds : [];
|
|
742
|
-
const exactInputs = Array.isArray(segment.sourceInputs) ? segment.sourceInputs : null;
|
|
743
|
-
const count = trackSources.length + (exactInputs?.length ?? 0);
|
|
744
|
-
for (let place = 0; place < count; place += 1) {
|
|
745
|
-
const sourceId = place < trackSources.length ? trackSources[place]
|
|
746
|
-
: exactInputs[place - trackSources.length]?.sourceId;
|
|
747
|
-
if (namedBefore(sourceId, place, trackSources, exactInputs)) continue;
|
|
748
|
-
const source = sourceState.get(sourceId);
|
|
749
|
-
if (!source) return { reason: "source-measurement-unavailable" };
|
|
750
|
-
if (source.source.complete === true) continue;
|
|
751
|
-
const ranges = exactInputs ? inputRangesOf(exactInputs, sourceId) : null;
|
|
752
|
-
if (ranges === null) {
|
|
753
|
-
// The tail extends beyond the ranges the playback map has asked to
|
|
754
|
-
// resolve. Stop this projection at that boundary: the near window
|
|
755
|
-
// remains exact, and later segments enter the map as the viewer
|
|
756
|
-
// advances. Waiting for the whole tail here prevents a cold start.
|
|
757
|
-
return null;
|
|
758
|
-
}
|
|
759
|
-
const bytes = supplyOf(sourceId);
|
|
760
|
-
let order = 0;
|
|
761
|
-
for (const range of ranges) {
|
|
762
|
-
// Bytes the torrent thread planned, or the near window brought, come when they were to come.
|
|
763
|
-
sourceArrivalAt = Math.max(sourceArrivalAt, latestArrival(range, sourceArrivals.get(sourceId), plan));
|
|
764
|
-
const after = missingBefore(bytes, range.end);
|
|
765
|
-
if (after > missingBefore(bytes, range.start)) order = Math.max(order, after);
|
|
766
|
-
}
|
|
767
|
-
if (order === 0) continue;
|
|
768
|
-
const { service } = source;
|
|
769
|
-
if (!service.rate || !(service.rate.rateAt(0) > 0)) {
|
|
770
|
-
return { reason: service.rate ? "service-not-advancing" : "download-rate-unavailable",
|
|
771
|
-
unavailableSource: { sourceId, segmentIndex: segment.index, missingBytes: order - (bytes.asked ?? 0) } };
|
|
772
|
-
}
|
|
773
|
-
// The tail's first asked byte is where this source's tail begins:
|
|
774
|
-
// bytes missing before it are not this film's to wait for.
|
|
775
|
-
bytes.asked ??= Math.max(0, order - missingWithin(bytes, ranges));
|
|
776
|
-
let arrival;
|
|
777
|
-
if (order > bytes.asked) {
|
|
778
|
-
service.finish = service.rate.finish(order - bytes.asked, service.finish);
|
|
779
|
-
bytes.asked = order;
|
|
780
|
-
bytes.orders.push(order);
|
|
781
|
-
bytes.finishes.push(service.finish);
|
|
782
|
-
arrival = service.finish;
|
|
783
|
-
} else {
|
|
784
|
-
// Brought by an earlier piece's ask: when that ask reached it.
|
|
785
|
-
let low = 0, high = bytes.orders.length - 1;
|
|
786
|
-
while (low < high) {
|
|
787
|
-
const middle = (low + high) >>> 1;
|
|
788
|
-
if (bytes.orders[middle] < order) low = middle + 1;
|
|
789
|
-
else high = middle;
|
|
790
|
-
}
|
|
791
|
-
arrival = bytes.finishes[low] ?? service.finish;
|
|
792
|
-
}
|
|
793
|
-
sourceArrivalAt = Math.max(sourceArrivalAt, arrival);
|
|
794
|
-
}
|
|
716
|
+
function scheduleTail({ trackState, sourceState, sourceByteCoverage, runStarts, trackFinish, link,
|
|
717
|
+
position, buffered, reserve, transferFinish, constrain, duration }) {
|
|
718
|
+
const tailStart = Math.max(position, ...trackState.map(state => state.tailStartSeconds));
|
|
719
|
+
if (!Number.isFinite(tailStart)) return null;
|
|
720
|
+
const sourceDurations = new Map();
|
|
721
|
+
for (const state of trackState) {
|
|
722
|
+
const seconds = Math.max(0, state.tailEndSeconds - tailStart) * state.tailWorkFraction;
|
|
723
|
+
if (!(seconds > 0)) continue;
|
|
724
|
+
for (const sourceId of new Set(state.track.sourceIds ?? [])) {
|
|
725
|
+
sourceDurations.set(sourceId, Math.max(sourceDurations.get(sourceId) ?? 0, seconds));
|
|
795
726
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
let sourceArrivalAt = 0;
|
|
730
|
+
for (const [sourceId, seconds] of sourceDurations) {
|
|
731
|
+
const source = sourceState.get(sourceId);
|
|
732
|
+
if (!source) return { reason: "source-measurement-unavailable" };
|
|
733
|
+
if (source.source.complete === true) continue;
|
|
734
|
+
const bytes = estimateUnmappedSourceBytes(source.source, seconds, duration,
|
|
735
|
+
sourceByteCoverage.get(sourceId));
|
|
736
|
+
if (bytes === null) return { reason: "source-measurement-unavailable",
|
|
737
|
+
unavailableSource: { sourceId, phase: "tail" } };
|
|
738
|
+
if (!(bytes > 0)) continue;
|
|
739
|
+
const { service } = source;
|
|
740
|
+
if (!service.rate || !(service.rate.rateAt(0) > 0)) {
|
|
741
|
+
return { reason: service.rate ? "service-not-advancing" : "download-rate-unavailable",
|
|
742
|
+
unavailableSource: { sourceId, estimatedBytes: bytes, phase: "tail" } };
|
|
804
743
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
const requiresSource = sourceArrivalAt > 0;
|
|
813
|
-
const sourcePlanned = requiresSource && plan.finish >= sourceArrivalAt;
|
|
814
|
-
constrain(completedAt - ahead, "delivery", trackIndex, segment.index, completedAt, ahead, false, sourceArrivalAt,
|
|
815
|
-
sourcePlanned);
|
|
816
|
-
const covered = Math.max(0, ahead - Math.min(reserve, Math.max(0, state.tailEndSeconds - startsAt)) *
|
|
817
|
-
Number(requiresSource));
|
|
818
|
-
constrain(producedAt - covered, "production", trackIndex, segment.index, producedAt, covered, requiresSource,
|
|
819
|
-
sourceArrivalAt, sourcePlanned);
|
|
744
|
+
service.finish = service.rate.finish(bytes, service.finish);
|
|
745
|
+
sourceArrivalAt = Math.max(sourceArrivalAt, service.finish);
|
|
746
|
+
const state = trackState.find(candidate => candidate.track.sourceIds?.includes(sourceId));
|
|
747
|
+
const index = state?.segments.at(-1)?.index ?? 0;
|
|
748
|
+
const ahead = Math.max(0, (state?.tailEndSeconds ?? tailStart) - position);
|
|
749
|
+
constrain(service.finish - Math.max(0, ahead - reserve), "source", trackState.indexOf(state), index,
|
|
750
|
+
service.finish, ahead, true, service.finish, false);
|
|
820
751
|
}
|
|
821
|
-
}
|
|
822
752
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
753
|
+
let outputBits = 0;
|
|
754
|
+
let latestTrackFinish = 0;
|
|
755
|
+
for (let trackIndex = 0; trackIndex < trackState.length; trackIndex += 1) {
|
|
756
|
+
const state = trackState[trackIndex];
|
|
757
|
+
const end = state.tailEndSeconds;
|
|
758
|
+
const seconds = Math.max(0, end - tailStart);
|
|
759
|
+
if (!(seconds > 0)) continue;
|
|
760
|
+
const track = state.track;
|
|
761
|
+
const segment = state.segments.at(-1) ?? { index: 0 };
|
|
762
|
+
const remaining = seconds * state.tailWorkFraction;
|
|
763
|
+
const work = Math.max(0, remaining - numericCoverage((track.processingRanges ?? [])
|
|
764
|
+
.filter(range => Number.isFinite(range?.start) && Number.isFinite(range?.end))
|
|
765
|
+
.map(range => ({ start: Math.max(tailStart, range.start), end: Math.min(end, range.end) }))
|
|
766
|
+
.filter(range => range.end > range.start)).reduce((sum, range) => sum + range.end - range.start, 0));
|
|
767
|
+
const processing = state.curve;
|
|
768
|
+
if (work > 0 && !processing) return { reason: "encode-rate-unavailable" };
|
|
769
|
+
const run = runStarts[trackIndex];
|
|
770
|
+
const localStart = tailStart - state.clockSeconds;
|
|
771
|
+
const inRun = run.spans.some(span => localStart >= span.start && localStart < span.end);
|
|
772
|
+
const open = work > 0 && !inRun ? run.openSeconds : 0;
|
|
773
|
+
const finishedAt = work > 0 ? processing.finish(work, trackFinish[trackIndex] + open) : trackFinish[trackIndex];
|
|
774
|
+
latestTrackFinish = Math.max(latestTrackFinish, finishedAt);
|
|
775
|
+
const deliveryStart = Math.max(tailStart, position + buffered);
|
|
776
|
+
outputBits += Math.max(0, state.bitsPerMediaSecond) * Math.max(0, end - deliveryStart);
|
|
777
|
+
const ahead = Math.max(0, end - position);
|
|
778
|
+
constrain(finishedAt - Math.max(0, ahead - reserve), "production", trackIndex, segment.index,
|
|
779
|
+
finishedAt, ahead, sourceArrivalAt > 0, sourceArrivalAt, false);
|
|
837
780
|
}
|
|
838
|
-
const inside = low < coverage.length && coverage[low].start < byte ? byte - coverage[low].start : 0;
|
|
839
|
-
return byte - heldBefore[low] - inside;
|
|
840
|
-
}
|
|
841
781
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
782
|
+
if (outputBits > 0) {
|
|
783
|
+
const end = Math.max(...trackState.map(state => state.tailEndSeconds));
|
|
784
|
+
const completedAt = link.finish(outputBits, Math.max(transferFinish, latestTrackFinish));
|
|
785
|
+
const ahead = Math.max(0, end - position);
|
|
786
|
+
constrain(completedAt - Math.max(0, ahead - reserve), "delivery", 0,
|
|
787
|
+
trackState[0]?.segments.at(-1)?.index ?? 0, completedAt, ahead, false, sourceArrivalAt, false);
|
|
788
|
+
}
|
|
789
|
+
return null;
|
|
847
790
|
}
|
|
848
791
|
|
|
849
792
|
/**
|
|
@@ -1125,6 +1068,25 @@ function numericCoverage(ranges) {
|
|
|
1125
1068
|
return joined;
|
|
1126
1069
|
}
|
|
1127
1070
|
|
|
1071
|
+
/**
|
|
1072
|
+
* Estimate download work without claiming an address or held byte. The whole
|
|
1073
|
+
* file's measured density, reduced by the held fraction, prices unresolved
|
|
1074
|
+
* intervals and the aggregate suffix.
|
|
1075
|
+
*/
|
|
1076
|
+
function estimateUnmappedSourceBytes(source, mediaSeconds, durationSeconds, coverage = null) {
|
|
1077
|
+
const fileLength = Number(source.fileLength);
|
|
1078
|
+
const density = Number(source.bytesPerMediaSecond) > 0 ? Number(source.bytesPerMediaSecond)
|
|
1079
|
+
: fileLength > 0 && durationSeconds > 0 ? fileLength / durationSeconds : 0;
|
|
1080
|
+
if (!(density > 0) || !(mediaSeconds > 0)) return null;
|
|
1081
|
+
const heldRanges = coverage ?? source.residence;
|
|
1082
|
+
if (!(fileLength > 0) || !Array.isArray(heldRanges)) return density * mediaSeconds;
|
|
1083
|
+
const held = numericCoverage(heldRanges.filter(range =>
|
|
1084
|
+
range?.location !== "missing" && Number.isFinite(range?.start) && Number.isFinite(range?.end) &&
|
|
1085
|
+
range.end > range.start));
|
|
1086
|
+
const heldBytes = held.reduce((sum, range) => sum + range.end - range.start, 0);
|
|
1087
|
+
return density * mediaSeconds * Math.max(0, Math.min(1, (fileLength - heldBytes) / fileLength));
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1128
1090
|
/** Already held bytes have no arrival delay. Index future arrivals once. */
|
|
1129
1091
|
function indexedArrivals(ranges) {
|
|
1130
1092
|
let maximumEnd = 0;
|