@torrent-tv/proxy 2.107.19 → 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,15 +315,12 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
301
315
|
return result(false, null, buffered, reserve, null, "link-rate-unavailable", 0);
|
|
302
316
|
}
|
|
303
317
|
|
|
304
|
-
let preparedSegments = trackState.reduce((sum, { ready
|
|
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) {
|
|
310
322
|
const state = trackState[trackIndex];
|
|
311
323
|
const { track, segments } = state;
|
|
312
|
-
const safeThrough = position + finiteNonNegative(input.bufferedAheadSeconds) + reserve;
|
|
313
324
|
const processingCoverage = numericCoverage((Array.isArray(track.processingRanges) ? track.processingRanges : [])
|
|
314
325
|
.filter(range => Number.isFinite(range?.start) && Number.isFinite(range?.end) && range.end > range.start));
|
|
315
326
|
const measuredBitsPerMediaSecond = state.bitsPerMediaSecond;
|
|
@@ -345,7 +356,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
345
356
|
const encodeWork = produced ? 0 : remainingProcessing.reduce((sum, part) => sum + part.end - part.start, 0);
|
|
346
357
|
|
|
347
358
|
let sourceByteIntervals = NO_SOURCE_BYTES;
|
|
348
|
-
|
|
359
|
+
const estimatedSourceBytes = new Map();
|
|
349
360
|
if (!produced && encodeWork > 0) {
|
|
350
361
|
// Each source the piece reads, in order and once: the track's own, then
|
|
351
362
|
// those its inputs name. Gathered with loops and no copies, because
|
|
@@ -363,20 +374,18 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
363
374
|
unknownReason ??= "source-measurement-unavailable";
|
|
364
375
|
} else if (source.source.complete !== true) {
|
|
365
376
|
const ranges = exactInputs ? inputRangesOf(exactInputs, sourceId) : null;
|
|
366
|
-
if (ranges === null) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
state.tail = [];
|
|
373
|
-
deferredAtBoundary = true;
|
|
374
|
-
break;
|
|
375
|
-
}
|
|
376
|
-
if (unknownReason === null) {
|
|
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) {
|
|
377
383
|
unknownReason = "source-input-ranges-unavailable";
|
|
378
384
|
unavailableSource = { sourceId, trackId: track.id, segmentIndex: segment.index, phase: "near" };
|
|
379
385
|
}
|
|
386
|
+
} else if (ranges === null) {
|
|
387
|
+
unknownReason ??= "source-input-ranges-unavailable";
|
|
388
|
+
unavailableSource ??= { sourceId, trackId: track.id, segmentIndex: segment.index, phase: "near" };
|
|
380
389
|
} else {
|
|
381
390
|
if (sourceByteIntervals === NO_SOURCE_BYTES) sourceByteIntervals = new Map();
|
|
382
391
|
sourceByteIntervals.set(sourceId, ranges);
|
|
@@ -384,7 +393,6 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
384
393
|
}
|
|
385
394
|
}
|
|
386
395
|
}
|
|
387
|
-
if (deferredAtBoundary) break;
|
|
388
396
|
if (!produced && encodeWork > 0 && !state.curve) {
|
|
389
397
|
unknownReason ??= "encode-rate-unavailable";
|
|
390
398
|
}
|
|
@@ -396,14 +404,13 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
396
404
|
sizeBits,
|
|
397
405
|
encodeWork,
|
|
398
406
|
sourceByteIntervals,
|
|
407
|
+
estimatedSourceBytes,
|
|
399
408
|
produced
|
|
400
409
|
});
|
|
401
410
|
}
|
|
402
411
|
}
|
|
403
412
|
|
|
404
|
-
preparedSegments = trackState.reduce((sum, { ready
|
|
405
|
-
sum + segments.filter((segment) => ready.has(segment.index)).length +
|
|
406
|
-
tail.filter((segment) => ready.has(segment.index)).length, 0);
|
|
413
|
+
preparedSegments = trackState.reduce((sum, { ready }) => sum + ready.size, 0);
|
|
407
414
|
|
|
408
415
|
if (unknownReason) {
|
|
409
416
|
return { ...result(false, null, buffered, reserve, null, unknownReason, preparedSegments),
|
|
@@ -466,7 +473,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
466
473
|
const completions = [];
|
|
467
474
|
const productions = [];
|
|
468
475
|
for (const job of jobs) {
|
|
469
|
-
const { track, trackIndex, segment, sizeBits, encodeWork, sourceByteIntervals } = job;
|
|
476
|
+
const { track, trackIndex, segment, sizeBits, encodeWork, sourceByteIntervals, estimatedSourceBytes } = job;
|
|
470
477
|
let sourceArrivalAt = 0;
|
|
471
478
|
const plan = { finish: 0 };
|
|
472
479
|
for (const [sourceId, ranges] of sourceByteIntervals) {
|
|
@@ -514,6 +521,21 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
514
521
|
sourceService.finish = finish;
|
|
515
522
|
}
|
|
516
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
|
+
}
|
|
517
539
|
// Production accepts a complete held segment input. It cannot overlap its
|
|
518
540
|
// own source download; reused input retains its original arrival time.
|
|
519
541
|
const processing = trackState[trackIndex].curve;
|
|
@@ -608,9 +630,9 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
608
630
|
production.requiresSource, production.sourceArrivalAt, production.sourcePlanned);
|
|
609
631
|
}
|
|
610
632
|
}
|
|
611
|
-
if (trackState.some(({
|
|
612
|
-
const stop = scheduleTail({ trackState, sourceState, sourceByteCoverage,
|
|
613
|
-
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 });
|
|
614
636
|
if (stop) {
|
|
615
637
|
return { ...result(false, null, buffered, reserve, null, stop.reason, preparedSegments),
|
|
616
638
|
...(stop.unavailableSource ? { unavailableSource: stop.unavailableSource } : {}) };
|
|
@@ -681,185 +703,90 @@ function isPiece(segment) {
|
|
|
681
703
|
}
|
|
682
704
|
|
|
683
705
|
/**
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
* Where a byte stands in that order is the bytes still missing before it,
|
|
692
|
-
* read off what is held, planned and brought by the near window
|
|
693
|
-
* ({@link missingBefore}); a piece is placed by the last range of it that
|
|
694
|
-
* misses bytes, so a range it reads that is already held — an AVI's index at
|
|
695
|
-
* the end of the file — does not place it at the end of the file. Planned and
|
|
696
|
-
* brought bytes come when they were to come. No exact time, list of arrivals
|
|
697
|
-
* or per-piece object is built: this runs over the rest of the film on every
|
|
698
|
-
* 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.
|
|
699
713
|
*
|
|
700
714
|
* @returns {{ reason: string, unavailableSource?: object } | null} Why there is no answer, or null.
|
|
701
715
|
*/
|
|
702
|
-
function scheduleTail({ trackState, sourceState, sourceByteCoverage,
|
|
703
|
-
position, reserve, transferFinish, constrain }) {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
state = { coverage, heldBefore, asked: null, orders: [], finishes: [] };
|
|
713
|
-
supply.set(sourceId, state);
|
|
714
|
-
}
|
|
715
|
-
return state;
|
|
716
|
-
};
|
|
717
|
-
const next = trackState.map(() => 0);
|
|
718
|
-
const plan = { finish: 0 };
|
|
719
|
-
let delivered = transferFinish;
|
|
720
|
-
for (;;) {
|
|
721
|
-
// The next piece in media order across the tracks, as the near window orders them.
|
|
722
|
-
let trackIndex = -1;
|
|
723
|
-
for (let index = 0; index < trackState.length; index += 1) {
|
|
724
|
-
const candidate = trackState[index].tail[next[index]];
|
|
725
|
-
if (!candidate) continue;
|
|
726
|
-
const best = trackIndex < 0 ? null : trackState[trackIndex].tail[next[trackIndex]];
|
|
727
|
-
if (!best || candidate.startSeconds < best.startSeconds ||
|
|
728
|
-
(candidate.startSeconds === best.startSeconds && candidate.endSeconds < best.endSeconds)) trackIndex = index;
|
|
729
|
-
}
|
|
730
|
-
if (trackIndex < 0) return null;
|
|
731
|
-
const state = trackState[trackIndex];
|
|
732
|
-
const { track } = state;
|
|
733
|
-
const segment = state.tail[next[trackIndex]++];
|
|
734
|
-
const startsAt = segment.startSeconds + state.clockSeconds;
|
|
735
|
-
const endsAt = segment.endSeconds + state.clockSeconds;
|
|
736
|
-
// Media the page already holds is not transferred again.
|
|
737
|
-
if (Array.isArray(track.clientRanges) && track.clientRanges.some((range) =>
|
|
738
|
-
range?.start <= Math.max(startsAt, position) && range?.end >= endsAt)) continue;
|
|
739
|
-
const exactBytes = sizeOf(track.segmentSizesBytes, segment.index);
|
|
740
|
-
const sizeBits = exactBytes > 0 ? exactBytes * 8 : state.bitsPerMediaSecond * (segment.endSeconds - segment.startSeconds);
|
|
741
|
-
if (!(sizeBits > 0)) return { reason: "segment-size-unavailable" };
|
|
742
|
-
const produced = state.ready.has(segment.index);
|
|
743
|
-
let work = 0;
|
|
744
|
-
if (!produced) {
|
|
745
|
-
work = segment.endSeconds - segment.startSeconds;
|
|
746
|
-
for (const range of Array.isArray(track.processingRanges) ? track.processingRanges : []) {
|
|
747
|
-
if (Number.isFinite(range?.start) && Number.isFinite(range?.end)) {
|
|
748
|
-
work -= Math.max(0, Math.min(range.end, segment.endSeconds) - Math.max(range.start, segment.startSeconds));
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
work = Math.max(0, work);
|
|
752
|
-
}
|
|
753
|
-
let sourceArrivalAt = 0;
|
|
754
|
-
plan.finish = 0;
|
|
755
|
-
if (work > 0) {
|
|
756
|
-
if (!state.curve) return { reason: "encode-rate-unavailable" };
|
|
757
|
-
const trackSources = Array.isArray(track.sourceIds) ? track.sourceIds : [];
|
|
758
|
-
const exactInputs = Array.isArray(segment.sourceInputs) ? segment.sourceInputs : null;
|
|
759
|
-
const count = trackSources.length + (exactInputs?.length ?? 0);
|
|
760
|
-
for (let place = 0; place < count; place += 1) {
|
|
761
|
-
const sourceId = place < trackSources.length ? trackSources[place]
|
|
762
|
-
: exactInputs[place - trackSources.length]?.sourceId;
|
|
763
|
-
if (namedBefore(sourceId, place, trackSources, exactInputs)) continue;
|
|
764
|
-
const source = sourceState.get(sourceId);
|
|
765
|
-
if (!source) return { reason: "source-measurement-unavailable" };
|
|
766
|
-
if (source.source.complete === true) continue;
|
|
767
|
-
const ranges = exactInputs ? inputRangesOf(exactInputs, sourceId) : null;
|
|
768
|
-
if (ranges === null) {
|
|
769
|
-
// The tail extends beyond the ranges the playback map has asked to
|
|
770
|
-
// resolve. Stop this projection at that boundary: the near window
|
|
771
|
-
// remains exact, and later segments enter the map as the viewer
|
|
772
|
-
// advances. Waiting for the whole tail here prevents a cold start.
|
|
773
|
-
return null;
|
|
774
|
-
}
|
|
775
|
-
const bytes = supplyOf(sourceId);
|
|
776
|
-
let order = 0;
|
|
777
|
-
for (const range of ranges) {
|
|
778
|
-
// Bytes the torrent thread planned, or the near window brought, come when they were to come.
|
|
779
|
-
sourceArrivalAt = Math.max(sourceArrivalAt, latestArrival(range, sourceArrivals.get(sourceId), plan));
|
|
780
|
-
const after = missingBefore(bytes, range.end);
|
|
781
|
-
if (after > missingBefore(bytes, range.start)) order = Math.max(order, after);
|
|
782
|
-
}
|
|
783
|
-
if (order === 0) continue;
|
|
784
|
-
const { service } = source;
|
|
785
|
-
if (!service.rate || !(service.rate.rateAt(0) > 0)) {
|
|
786
|
-
return { reason: service.rate ? "service-not-advancing" : "download-rate-unavailable",
|
|
787
|
-
unavailableSource: { sourceId, segmentIndex: segment.index, missingBytes: order - (bytes.asked ?? 0) } };
|
|
788
|
-
}
|
|
789
|
-
// The tail's first asked byte is where this source's tail begins:
|
|
790
|
-
// bytes missing before it are not this film's to wait for.
|
|
791
|
-
bytes.asked ??= Math.max(0, order - missingWithin(bytes, ranges));
|
|
792
|
-
let arrival;
|
|
793
|
-
if (order > bytes.asked) {
|
|
794
|
-
service.finish = service.rate.finish(order - bytes.asked, service.finish);
|
|
795
|
-
bytes.asked = order;
|
|
796
|
-
bytes.orders.push(order);
|
|
797
|
-
bytes.finishes.push(service.finish);
|
|
798
|
-
arrival = service.finish;
|
|
799
|
-
} else {
|
|
800
|
-
// Brought by an earlier piece's ask: when that ask reached it.
|
|
801
|
-
let low = 0, high = bytes.orders.length - 1;
|
|
802
|
-
while (low < high) {
|
|
803
|
-
const middle = (low + high) >>> 1;
|
|
804
|
-
if (bytes.orders[middle] < order) low = middle + 1;
|
|
805
|
-
else high = middle;
|
|
806
|
-
}
|
|
807
|
-
arrival = bytes.finishes[low] ?? service.finish;
|
|
808
|
-
}
|
|
809
|
-
sourceArrivalAt = Math.max(sourceArrivalAt, arrival);
|
|
810
|
-
}
|
|
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));
|
|
811
726
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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" } };
|
|
820
743
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const requiresSource = sourceArrivalAt > 0;
|
|
829
|
-
const sourcePlanned = requiresSource && plan.finish >= sourceArrivalAt;
|
|
830
|
-
constrain(completedAt - ahead, "delivery", trackIndex, segment.index, completedAt, ahead, false, sourceArrivalAt,
|
|
831
|
-
sourcePlanned);
|
|
832
|
-
const covered = Math.max(0, ahead - Math.min(reserve, Math.max(0, state.tailEndSeconds - startsAt)) *
|
|
833
|
-
Number(requiresSource));
|
|
834
|
-
constrain(producedAt - covered, "production", trackIndex, segment.index, producedAt, covered, requiresSource,
|
|
835
|
-
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);
|
|
836
751
|
}
|
|
837
|
-
}
|
|
838
752
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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);
|
|
853
780
|
}
|
|
854
|
-
const inside = low < coverage.length && coverage[low].start < byte ? byte - coverage[low].start : 0;
|
|
855
|
-
return byte - heldBefore[low] - inside;
|
|
856
|
-
}
|
|
857
781
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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;
|
|
863
790
|
}
|
|
864
791
|
|
|
865
792
|
/**
|
|
@@ -1141,6 +1068,25 @@ function numericCoverage(ranges) {
|
|
|
1141
1068
|
return joined;
|
|
1142
1069
|
}
|
|
1143
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
|
+
|
|
1144
1090
|
/** Already held bytes have no arrival delay. Index future arrivals once. */
|
|
1145
1091
|
function indexedArrivals(ranges) {
|
|
1146
1092
|
let maximumEnd = 0;
|