@torrent-tv/proxy 2.107.54 → 2.107.55
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
|
@@ -867,6 +867,7 @@ export class ViewerRequests {
|
|
|
867
867
|
const inventory = this.#host.segmentStore.sizesOf(output.outputKey);
|
|
868
868
|
const segments = this.#host.outputForecastGeometry.segmentsOf(output, sessionInit, inventory,
|
|
869
869
|
Number.isFinite(reportedPosition) ? reportedPosition : this.#host.viewerSecondsOn(session, consumerId, now));
|
|
870
|
+
const forecastGeometry = this.#host.outputForecastGeometry.get(output);
|
|
870
871
|
const sourceIndexes = [...output.spec.sourceFileIndexes];
|
|
871
872
|
if (sourceIndexes.length === 0) {
|
|
872
873
|
sourceIndexes.push(output.file.fileIndex);
|
|
@@ -997,7 +998,8 @@ export class ViewerRequests {
|
|
|
997
998
|
readings: encoderCaptureValid ? trackRates : [],
|
|
998
999
|
capture: encoderCapture,
|
|
999
1000
|
segments,
|
|
1000
|
-
|
|
1001
|
+
tailSegments: forecastGeometry?.tailSegments ?? [],
|
|
1002
|
+
geometry: forecastGeometry?.geometry,
|
|
1001
1003
|
readySegmentIndices: [...inventory.keys()],
|
|
1002
1004
|
segmentSizesBytes: inventory
|
|
1003
1005
|
});
|
|
@@ -27,10 +27,11 @@ export class OutputForecastGeometry {
|
|
|
27
27
|
const count = timeline?.segmentCount ?? 0;
|
|
28
28
|
let forecast = this.#byOutput.get(output);
|
|
29
29
|
if (!forecast) {
|
|
30
|
-
forecast = { pieces: [], geometry: {} };
|
|
30
|
+
forecast = { pieces: [], tailPieces: [], tailSegments: [], geometry: {} };
|
|
31
31
|
this.#byOutput.set(output, forecast);
|
|
32
32
|
}
|
|
33
33
|
const kept = forecast.pieces;
|
|
34
|
+
const keptTail = forecast.tailPieces;
|
|
34
35
|
const segments = [];
|
|
35
36
|
const lookahead = Number(this.#host.lookaheadSeconds);
|
|
36
37
|
const start = Number.isFinite(positionSeconds) ? Math.max(0, positionSeconds) : 0;
|
|
@@ -50,6 +51,23 @@ export class OutputForecastGeometry {
|
|
|
50
51
|
else high = middle;
|
|
51
52
|
}
|
|
52
53
|
const last = low;
|
|
54
|
+
const tailSegments = [];
|
|
55
|
+
for (let index = last; index < count; index += 1) {
|
|
56
|
+
const startSeconds = timeline.publishedStartOf(index);
|
|
57
|
+
const endSeconds = timeline.publishedStartOf(index + 1);
|
|
58
|
+
const sourceInputs = this.#host.sourceInputsFor?.(output, index) ?? undefined;
|
|
59
|
+
const previous = keptTail[index];
|
|
60
|
+
if (previous && previous.startSeconds === startSeconds && previous.endSeconds === endSeconds &&
|
|
61
|
+
sameInputs(previous.sourceInputs, sourceInputs)) {
|
|
62
|
+
tailSegments.push(previous);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const segment = { index, startSeconds, endSeconds, sourceInputs };
|
|
66
|
+
keptTail[index] = segment;
|
|
67
|
+
tailSegments.push(segment);
|
|
68
|
+
}
|
|
69
|
+
if (keptTail.length > count) keptTail.length = count;
|
|
70
|
+
forecast.tailSegments = tailSegments;
|
|
53
71
|
const indexes = new Set([0]);
|
|
54
72
|
for (let index = first; index < last; index++) indexes.add(index);
|
|
55
73
|
for (const index of indexes) {
|
|
@@ -255,7 +255,10 @@ function mergeCaptureSummary(previous, capture) {
|
|
|
255
255
|
* clockOffsetSeconds?: number, clientRanges?: Array<{ start: number, end: number }>,
|
|
256
256
|
* appendService?: object, fragmentService?: object,
|
|
257
257
|
* segments: Array<{ index: number, startSeconds: number, endSeconds: number,
|
|
258
|
+
* sourceInputs?: Array<{ sourceId: string, ranges: Array<{ start: number, end: number }> }>,
|
|
258
259
|
* mediaRanges?: { timescale: bigint | null, ranges: Array<{ start: bigint, end: bigint, frame: bigint }> } }>,
|
|
260
|
+
* tailSegments?: Array<{ index: number, startSeconds: number, endSeconds: number,
|
|
261
|
+
* sourceInputs?: Array<{ sourceId: string, ranges: Array<{ start: number, end: number }> }> }>,
|
|
259
262
|
* readySegmentIndices: number[], segmentSizesBytes: Map<number, number> | object }>} input.tracks
|
|
260
263
|
* `mediaRanges` are a read piece's ranges on its served timeline, in ticks;
|
|
261
264
|
* `clockOffsetSeconds` is the `timestampOffset` the page's player applied to
|
|
@@ -277,13 +280,23 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
277
280
|
// THE NEAR WINDOW AND THE SUFFIX. The look-ahead is scheduled with exact
|
|
278
281
|
// segment times and source ranges when known. Missing range metadata is priced
|
|
279
282
|
// from aggregate file density and measured download service without claiming
|
|
280
|
-
// an address or byte. Beyond the look-ahead, {@link scheduleTail}
|
|
281
|
-
//
|
|
283
|
+
// an address or byte. Beyond the look-ahead, {@link scheduleTail} checks
|
|
284
|
+
// mapped source-piece deadlines and estimates unmapped source, encoder and
|
|
285
|
+
// delivery work from aggregate service.
|
|
282
286
|
const lookahead = finiteNonNegative(input.lookaheadSeconds);
|
|
283
287
|
const horizonEnd = lookahead > 0 ? finiteNonNegative(input.positionSeconds) + lookahead : Number.POSITIVE_INFINITY;
|
|
284
|
-
const tracks = (Array.isArray(input.tracks) ? input.tracks : []).map((track) =>
|
|
285
|
-
Array.isArray(track.segments)
|
|
286
|
-
|
|
288
|
+
const tracks = (Array.isArray(input.tracks) ? input.tracks : []).map((track) => {
|
|
289
|
+
if (!Array.isArray(track.segments)) return track;
|
|
290
|
+
const tailProfileProvided = Array.isArray(track.tailSegments);
|
|
291
|
+
return { ...track, segmentCount: track.segmentCount ?? track.segments.length,
|
|
292
|
+
tailProfileProvided,
|
|
293
|
+
// Keep exact future source maps separately from the bounded near-window
|
|
294
|
+
// schedule. Tail source work can then be checked at the segment
|
|
295
|
+
// deadlines without treating the whole suffix as one final deadline.
|
|
296
|
+
tailSegments: (Array.isArray(track.tailSegments) ? track.tailSegments : track.segments)
|
|
297
|
+
.filter(segment => isPiece(segment) && segment.startSeconds >= horizonEnd),
|
|
298
|
+
segments: piecesThatMatter(track, finiteNonNegative(input.positionSeconds), horizonEnd) };
|
|
299
|
+
});
|
|
287
300
|
const duration = finiteNonNegative(input.durationSeconds);
|
|
288
301
|
// Each track's served ticks are placed on the clock the page reports by the
|
|
289
302
|
// offset its player applied (`timestampOffset`).
|
|
@@ -765,13 +778,19 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
765
778
|
}
|
|
766
779
|
}
|
|
767
780
|
const tailStock = [];
|
|
781
|
+
const tailCushionStock = [];
|
|
782
|
+
let tailUnmappedSources = new Set();
|
|
783
|
+
let tailUnmappedTracks = new Set();
|
|
768
784
|
if (trackState.some(({ tailEndSeconds }) => Number.isFinite(tailEndSeconds))) {
|
|
769
|
-
const stop = scheduleTail({ trackState, sourceState, runStarts, trackFinish,
|
|
770
|
-
link, position, buffered, reserve, transferFinish, constrain, duration, stock: tailStock
|
|
771
|
-
|
|
785
|
+
const stop = scheduleTail({ trackState, sourceState, sourceByteCoverage, sourceArrivals, runStarts, trackFinish,
|
|
786
|
+
link, position, buffered, reserve, transferFinish, constrain, duration, stock: tailStock,
|
|
787
|
+
cushionStock: tailCushionStock });
|
|
788
|
+
if (stop?.reason) {
|
|
772
789
|
return { ...result(false, null, buffered, reserve, null, stop.reason, preparedSegments),
|
|
773
790
|
...(stop.unavailableSource ? { unavailableSource: stop.unavailableSource } : {}) };
|
|
774
791
|
}
|
|
792
|
+
tailUnmappedSources = stop?.unmappedSources ?? tailUnmappedSources;
|
|
793
|
+
tailUnmappedTracks = stop?.unmappedTracks ?? tailUnmappedTracks;
|
|
775
794
|
}
|
|
776
795
|
const firstRequiredStages = trackState.slice(0, requiredTracks).map((state, trackIndex) => {
|
|
777
796
|
const first = state.segments.find(segment => segment.endSeconds > position);
|
|
@@ -860,6 +879,15 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
860
879
|
const firstRequiredSegmentsSeconds = firstRequiredTrackSeconds.every(Number.isFinite)
|
|
861
880
|
? Math.max(0, ...firstRequiredTrackSeconds) : null;
|
|
862
881
|
const delay = Math.max(0, ...constraints);
|
|
882
|
+
if (tailUnmappedSources.size > 0 || tailUnmappedTracks.size > 0) {
|
|
883
|
+
return { ...result(false, null, buffered, reserve, null, "tail-source-addresses-unavailable", preparedSegments),
|
|
884
|
+
diagnosticDelaySeconds: Number.isFinite(delay) ? delay : null,
|
|
885
|
+
unavailableTailSources: [...tailUnmappedSources].sort(),
|
|
886
|
+
unavailableTailTracks: [...tailUnmappedTracks].sort(),
|
|
887
|
+
tail: tailStock,
|
|
888
|
+
firstRequiredSegmentsSeconds,
|
|
889
|
+
firstRequiredStages };
|
|
890
|
+
}
|
|
863
891
|
// THE STOCK THE START ASKS FOR HAS TO FIT. Starting without a stop while the
|
|
864
892
|
// source comes slower than the film plays means holding, at the start, what
|
|
865
893
|
// is already here and everything that arrives before it. Where that is more
|
|
@@ -870,10 +898,11 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
870
898
|
// share (Home Assistant 2026-10-26, torrent-tv/meta#239). Said as such, and
|
|
871
899
|
// the viewer keeps waiting rather than starting into stops.
|
|
872
900
|
const storageBytes = Number(input.sourceStorageBytes);
|
|
873
|
-
const tailParts =
|
|
901
|
+
const tailParts = tailCushionStock.filter(part => !part.skipped);
|
|
874
902
|
const cushionBytes = Number.isFinite(delay) && tailParts.length > 0
|
|
875
903
|
? tailParts.reduce((sum, part) => sum + Math.max(0, part.totalBytes - part.missingBytes) +
|
|
876
|
-
Math.max(0, part.missingBytes - part.
|
|
904
|
+
Math.max(0, part.missingBytes - (part.planned ? (part.finish > delay ? part.missingBytes : 0)
|
|
905
|
+
: part.bytesPerSecond * Math.max(0, part.finish - delay))), 0)
|
|
877
906
|
: null;
|
|
878
907
|
if (Number.isFinite(delay) && Number.isFinite(storageBytes) && storageBytes >= 0 && tailParts.length > 0) {
|
|
879
908
|
if (cushionBytes > storageBytes) {
|
|
@@ -963,20 +992,182 @@ function scheduleTransfers(jobs, link) {
|
|
|
963
992
|
}
|
|
964
993
|
|
|
965
994
|
/**
|
|
966
|
-
*
|
|
967
|
-
*
|
|
968
|
-
*
|
|
969
|
-
*
|
|
970
|
-
*
|
|
971
|
-
*
|
|
972
|
-
|
|
995
|
+
* Schedule exact tail source ranges at the deadlines of the output pieces that
|
|
996
|
+
* need them. A source piece used by multiple tracks is assigned to its earliest
|
|
997
|
+
* deadline once. When a tail range is absent, the aggregate density estimate
|
|
998
|
+
* remains in force and is labelled as an unmapped estimate in `stock`.
|
|
999
|
+
*
|
|
1000
|
+
* @returns {{ reason: string, unavailableSource?: object } | null}
|
|
1001
|
+
*/
|
|
1002
|
+
function scheduleTailSourceRanges({ trackState, sourceState, sourceByteCoverage, sourceArrivals,
|
|
1003
|
+
position, reserve, constrain, stock, cushionStock }) {
|
|
1004
|
+
const pieceRequests = new Map();
|
|
1005
|
+
const unmappedSources = new Set();
|
|
1006
|
+
const unmappedTracks = new Set();
|
|
1007
|
+
const referencedSources = new Set();
|
|
1008
|
+
const completeSources = new Set();
|
|
1009
|
+
|
|
1010
|
+
for (let trackIndex = 0; trackIndex < trackState.length; trackIndex += 1) {
|
|
1011
|
+
const state = trackState[trackIndex];
|
|
1012
|
+
if (!(state.tailSegmentCount > 0)) continue;
|
|
1013
|
+
const tailSegments = (Array.isArray(state.track.tailSegments) ? state.track.tailSegments : [])
|
|
1014
|
+
.filter(segment => segment.startSeconds < state.tailEndSeconds);
|
|
1015
|
+
const tailSources = new Set(state.track.sourceIds ?? []);
|
|
1016
|
+
for (const segment of tailSegments) {
|
|
1017
|
+
for (const input of segment.sourceInputs ?? []) {
|
|
1018
|
+
if (typeof input?.sourceId === "string") tailSources.add(input.sourceId);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
if (state.track.tailProfileProvided === true) {
|
|
1022
|
+
for (const sourceId of tailSources) referencedSources.add(sourceId);
|
|
1023
|
+
}
|
|
1024
|
+
const readyTailCount = [...state.ready].reduce((count, index) => count + Number(index >= state.segmentCount - state.tailSegmentCount), 0);
|
|
1025
|
+
const unfinishedTailCount = Math.max(0, state.tailSegmentCount - readyTailCount);
|
|
1026
|
+
if (state.track.tailProfileProvided !== true) continue;
|
|
1027
|
+
if (unfinishedTailCount > 0 && tailSources.size === 0) {
|
|
1028
|
+
unmappedTracks.add(state.track.id ?? `track-${trackIndex}`);
|
|
1029
|
+
}
|
|
1030
|
+
if (tailSegments.filter(segment => !state.ready.has(segment.index)).length < unfinishedTailCount && tailSources.size > 0) {
|
|
1031
|
+
for (const sourceId of tailSources) {
|
|
1032
|
+
if (sourceState.get(sourceId)?.source.complete !== true) unmappedSources.add(sourceId);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
for (const segment of tailSegments) {
|
|
1037
|
+
if (state.ready.has(segment.index)) continue;
|
|
1038
|
+
const exactInputs = Array.isArray(segment.sourceInputs) ? segment.sourceInputs : null;
|
|
1039
|
+
for (const sourceId of tailSources) {
|
|
1040
|
+
const source = sourceState.get(sourceId);
|
|
1041
|
+
if (!source) return { reason: "source-measurement-unavailable",
|
|
1042
|
+
unavailableSource: { sourceId, trackId: state.track.id, segmentIndex: segment.index, phase: "tail" } };
|
|
1043
|
+
if (source.source.complete === true) {
|
|
1044
|
+
if (!completeSources.has(sourceId)) stock?.push({ sourceId, skipped: "complete" });
|
|
1045
|
+
completeSources.add(sourceId);
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
const ranges = exactInputs === null ? null : inputRangesOf(exactInputs, sourceId);
|
|
1049
|
+
if (ranges === null || ranges.some(range => range.end > source.source.fileLength) ||
|
|
1050
|
+
!Number.isSafeInteger(source.source.pieceLength) || source.source.pieceLength <= 0 ||
|
|
1051
|
+
!Number.isSafeInteger(source.source.fileOffset) || !Number.isSafeInteger(source.source.fileLength) ||
|
|
1052
|
+
source.source.fileLength <= 0) {
|
|
1053
|
+
unmappedSources.add(sourceId);
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
let requests = pieceRequests.get(sourceId);
|
|
1057
|
+
if (!requests) {
|
|
1058
|
+
requests = new Map();
|
|
1059
|
+
pieceRequests.set(sourceId, requests);
|
|
1060
|
+
}
|
|
1061
|
+
for (const range of ranges) {
|
|
1062
|
+
for (const piece of sourcePiecesFor([range], source.source)) {
|
|
1063
|
+
const first = Math.floor((source.source.fileOffset + piece.start) / source.source.pieceLength);
|
|
1064
|
+
const last = Math.ceil((source.source.fileOffset + piece.end) / source.source.pieceLength) - 1;
|
|
1065
|
+
for (let pieceIndex = first; pieceIndex <= last; pieceIndex += 1) {
|
|
1066
|
+
const start = Math.max(0, pieceIndex * source.source.pieceLength - source.source.fileOffset);
|
|
1067
|
+
const end = Math.min(source.source.fileLength, (pieceIndex + 1) * source.source.pieceLength - source.source.fileOffset);
|
|
1068
|
+
if (!(end > start)) continue;
|
|
1069
|
+
const current = requests.get(pieceIndex);
|
|
1070
|
+
const deadlineSeconds = segment.endSeconds;
|
|
1071
|
+
if (!current || deadlineSeconds < current.deadlineSeconds) {
|
|
1072
|
+
requests.set(pieceIndex, { sourceId, pieceIndex, start, end, deadlineSeconds,
|
|
1073
|
+
trackIndex, segmentIndex: segment.index, addressed: true });
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// A partially mapped source falls back as a whole to density estimation so
|
|
1083
|
+
// exact pieces and guessed bytes cannot both charge the same suffix work.
|
|
1084
|
+
for (const sourceId of unmappedSources) pieceRequests.delete(sourceId);
|
|
1085
|
+
const requests = [...pieceRequests.values()].flatMap(byPiece => [...byPiece.values()])
|
|
1086
|
+
.sort((left, right) => left.deadlineSeconds - right.deadlineSeconds ||
|
|
1087
|
+
left.sourceId.localeCompare(right.sourceId) || left.pieceIndex - right.pieceIndex);
|
|
1088
|
+
let tailLatestArrival = 0;
|
|
1089
|
+
const stockBySource = new Map();
|
|
1090
|
+
for (const request of requests) {
|
|
1091
|
+
const source = sourceState.get(request.sourceId);
|
|
1092
|
+
const { service } = source;
|
|
1093
|
+
const piece = { start: request.start, end: request.end };
|
|
1094
|
+
const coverage = sourceByteCoverage.get(request.sourceId) ?? [];
|
|
1095
|
+
const missing = uncoveredIntervals(piece, coverage);
|
|
1096
|
+
const heldRanges = numericCoverage((Array.isArray(source.source.residence) ? source.source.residence : [])
|
|
1097
|
+
.filter(range => range?.location !== "missing" &&
|
|
1098
|
+
Number.isFinite(range?.start) && Number.isFinite(range?.end) && range.end > range.start));
|
|
1099
|
+
const heldBytes = heldRanges.reduce((sum, range) => sum + Math.max(0,
|
|
1100
|
+
Math.min(piece.end, range.end) - Math.max(piece.start, range.start)), 0);
|
|
1101
|
+
const plan = { finish: 0 };
|
|
1102
|
+
let arrivalAt = latestArrival(piece, sourceArrivals.get(request.sourceId), plan);
|
|
1103
|
+
let sourcePlanned = arrivalAt > 0 && plan.finish >= arrivalAt;
|
|
1104
|
+
const rate = service.tailRate ?? service.rate;
|
|
1105
|
+
if (missing.length > 0) {
|
|
1106
|
+
if (!rate || !(rate.rateAt(0) > 0)) return { reason: rate ? "service-not-advancing" : "download-rate-unavailable",
|
|
1107
|
+
unavailableSource: { sourceId: request.sourceId, pieceIndex: request.pieceIndex,
|
|
1108
|
+
estimatedBytes: missing.reduce((sum, range) => sum + range.end - range.start, 0), phase: "tail" } };
|
|
1109
|
+
const missingBytes = missing.reduce((sum, range) => sum + range.end - range.start, 0);
|
|
1110
|
+
arrivalAt = rate.finish(missingBytes, Math.max(service.finish, arrivalAt));
|
|
1111
|
+
sourcePlanned = arrivalAt > 0 && plan.finish >= arrivalAt;
|
|
1112
|
+
service.finish = arrivalAt;
|
|
1113
|
+
const arrival = { start: piece.start, end: piece.end, begin: arrivalAt, finish: arrivalAt };
|
|
1114
|
+
sourceByteCoverage.set(request.sourceId, withCoverage(coverage, arrival));
|
|
1115
|
+
const arrivals = sourceArrivals.get(request.sourceId) ?? [];
|
|
1116
|
+
sourceArrivals.set(request.sourceId, withArrival(arrivals, arrival, [...arrivals, arrival]));
|
|
1117
|
+
} else {
|
|
1118
|
+
service.finish = Math.max(service.finish, arrivalAt);
|
|
1119
|
+
}
|
|
1120
|
+
tailLatestArrival = Math.max(tailLatestArrival, arrivalAt);
|
|
1121
|
+
const ahead = Math.max(0, request.deadlineSeconds - position);
|
|
1122
|
+
constrain(arrivalAt - Math.max(0, ahead - reserve), "source", request.trackIndex, request.segmentIndex,
|
|
1123
|
+
arrivalAt, ahead, true, arrivalAt, sourcePlanned);
|
|
1124
|
+
const pieceStock = { sourceId: request.sourceId, pieceIndex: request.pieceIndex,
|
|
1125
|
+
start: request.start, end: request.end, deadlineSeconds: request.deadlineSeconds,
|
|
1126
|
+
addressed: true, totalBytes: request.end - request.start,
|
|
1127
|
+
missingBytes: Math.max(0, request.end - request.start - heldBytes),
|
|
1128
|
+
planned: missing.length === 0 && sourcePlanned,
|
|
1129
|
+
bytesPerSecond: rate?.rateAt(0) ?? 0, finish: arrivalAt };
|
|
1130
|
+
cushionStock?.push(pieceStock);
|
|
1131
|
+
let sourceStock = stockBySource.get(request.sourceId);
|
|
1132
|
+
if (!sourceStock) {
|
|
1133
|
+
sourceStock = { sourceId: request.sourceId, addressed: true, uniquePieces: 0,
|
|
1134
|
+
start: request.start, end: request.end, earliestDeadlineSeconds: request.deadlineSeconds,
|
|
1135
|
+
latestDeadlineSeconds: request.deadlineSeconds, totalBytes: 0, missingBytes: 0,
|
|
1136
|
+
bytesPerSecond: pieceStock.bytesPerSecond, finish: 0 };
|
|
1137
|
+
stockBySource.set(request.sourceId, sourceStock);
|
|
1138
|
+
}
|
|
1139
|
+
sourceStock.uniquePieces += 1;
|
|
1140
|
+
sourceStock.start = Math.min(sourceStock.start, request.start);
|
|
1141
|
+
sourceStock.end = Math.max(sourceStock.end, request.end);
|
|
1142
|
+
sourceStock.earliestDeadlineSeconds = Math.min(sourceStock.earliestDeadlineSeconds, request.deadlineSeconds);
|
|
1143
|
+
sourceStock.latestDeadlineSeconds = Math.max(sourceStock.latestDeadlineSeconds, request.deadlineSeconds);
|
|
1144
|
+
sourceStock.totalBytes += pieceStock.totalBytes;
|
|
1145
|
+
sourceStock.missingBytes += pieceStock.missingBytes;
|
|
1146
|
+
sourceStock.finish = Math.max(sourceStock.finish, arrivalAt);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
stock?.push(...stockBySource.values());
|
|
1150
|
+
|
|
1151
|
+
return { latestArrival: tailLatestArrival, referencedSources, unmappedSources, unmappedTracks };
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Estimate the suffix after the exact near window. Mapped tail source ranges
|
|
1156
|
+
* receive individual piece deadlines; missing ranges retain the aggregate
|
|
1157
|
+
* density estimate, which is explicitly unaddressed. Processing and delivery
|
|
1158
|
+
* remain duration-level estimates. The schedule is conditional on measured
|
|
1159
|
+
* services continuing and does not claim exact frame timing.
|
|
973
1160
|
*
|
|
974
1161
|
* @returns {{ reason: string, unavailableSource?: object } | null} Why there is no answer, or null.
|
|
975
1162
|
*/
|
|
976
|
-
function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
|
|
977
|
-
position, buffered, reserve, transferFinish, constrain, duration, stock = null }) {
|
|
1163
|
+
function scheduleTail({ trackState, sourceState, sourceByteCoverage, sourceArrivals, runStarts, trackFinish, link,
|
|
1164
|
+
position, buffered, reserve, transferFinish, constrain, duration, stock = null, cushionStock = null }) {
|
|
978
1165
|
const tailStart = Math.max(position, ...trackState.map(state => state.tailStartSeconds));
|
|
979
1166
|
if (!Number.isFinite(tailStart)) return null;
|
|
1167
|
+
const exactSources = scheduleTailSourceRanges({ trackState, sourceState, sourceByteCoverage, sourceArrivals,
|
|
1168
|
+
position, reserve, constrain, stock, cushionStock });
|
|
1169
|
+
if (exactSources?.reason) return exactSources;
|
|
1170
|
+
let sourceArrivalAt = exactSources?.latestArrival ?? 0;
|
|
980
1171
|
const sourceDurations = new Map();
|
|
981
1172
|
for (const state of trackState) {
|
|
982
1173
|
const seconds = Math.max(0, state.tailEndSeconds - tailStart) * state.tailWorkFraction;
|
|
@@ -985,12 +1176,15 @@ function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
|
|
|
985
1176
|
workFraction: state.tailWorkFraction, tailSegments: state.tailSegmentCount });
|
|
986
1177
|
continue;
|
|
987
1178
|
}
|
|
988
|
-
|
|
1179
|
+
const sourceIds = new Set([...(state.track.sourceIds ?? []), ...(state.track.tailSegments ?? [])
|
|
1180
|
+
.filter(segment => segment.startSeconds < state.tailEndSeconds)
|
|
1181
|
+
.flatMap(segment => (segment.sourceInputs ?? []).map(item => item.sourceId))]);
|
|
1182
|
+
for (const sourceId of sourceIds) {
|
|
1183
|
+
if (exactSources?.referencedSources.has(sourceId) && !exactSources.unmappedSources.has(sourceId)) continue;
|
|
989
1184
|
sourceDurations.set(sourceId, Math.max(sourceDurations.get(sourceId) ?? 0, seconds));
|
|
990
1185
|
}
|
|
991
1186
|
}
|
|
992
1187
|
|
|
993
|
-
let sourceArrivalAt = 0;
|
|
994
1188
|
for (const [sourceId, seconds] of sourceDurations) {
|
|
995
1189
|
const source = sourceState.get(sourceId);
|
|
996
1190
|
if (!source) return { reason: "source-measurement-unavailable" };
|
|
@@ -1027,8 +1221,11 @@ function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
|
|
|
1027
1221
|
sourceArrivalAt = Math.max(sourceArrivalAt, service.finish);
|
|
1028
1222
|
// What this stretch weighs and what it still lacks: the stock the start
|
|
1029
1223
|
// asks to hold is read from these once the start's delay is known.
|
|
1030
|
-
|
|
1031
|
-
|
|
1224
|
+
const sourceStock = { sourceId, seconds, windowEnd, addressed: false, estimatedAddress: "file-density",
|
|
1225
|
+
totalBytes: estimateUnmappedSourceBytes(source.source, seconds, duration, []) ?? bytes,
|
|
1226
|
+
missingBytes: bytes, bytesPerSecond: rate.rateAt(0), finish: service.finish };
|
|
1227
|
+
stock?.push(sourceStock);
|
|
1228
|
+
cushionStock?.push(sourceStock);
|
|
1032
1229
|
const state = trackState.find(candidate => candidate.track.sourceIds?.includes(sourceId));
|
|
1033
1230
|
const index = state?.segments.at(-1)?.index ?? 0;
|
|
1034
1231
|
const ahead = Math.max(0, (state?.tailEndSeconds ?? tailStart) - position);
|
|
@@ -1081,7 +1278,8 @@ function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
|
|
|
1081
1278
|
constrain(completedAt - Math.max(0, ahead - reserve), "delivery", 0,
|
|
1082
1279
|
trackState[0]?.segments.at(-1)?.index ?? 0, completedAt, ahead, false, sourceArrivalAt, false);
|
|
1083
1280
|
}
|
|
1084
|
-
return
|
|
1281
|
+
return { unmappedSources: exactSources?.unmappedSources ?? new Set(),
|
|
1282
|
+
unmappedTracks: exactSources?.unmappedTracks ?? new Set() };
|
|
1085
1283
|
}
|
|
1086
1284
|
|
|
1087
1285
|
/**
|
|
@@ -1209,11 +1407,12 @@ function withSeekStartTolerance(ranges, position, unit) {
|
|
|
1209
1407
|
* before `horizonEnd`, the first piece, every piece that has been read and
|
|
1210
1408
|
* every piece that is made. A piece ending just before a seek target is kept
|
|
1211
1409
|
* as a prerequisite because its media timestamps can extend past its nominal
|
|
1212
|
-
* playlist boundary. A piece
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1410
|
+
* playlist boundary. A piece that is none of these changes no near-window
|
|
1411
|
+
* answer: its source map may still contribute an intermediate tail deadline.
|
|
1412
|
+
* Its media ranges are not placed in the exact browser-time schedule, it is
|
|
1413
|
+
* not where the track starts, no unread near-window piece is placed by it
|
|
1414
|
+
* (only read pieces are), it lends no frame to what the page holds, and it has
|
|
1415
|
+
* no size to measure the output's rate by.
|
|
1217
1416
|
*
|
|
1218
1417
|
* @param {{ segments: object[], readySegmentIndices?: number[] }} track
|
|
1219
1418
|
* @param {number} position - Seconds.
|