@torrent-tv/proxy 2.107.53 → 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.
@@ -149,14 +149,16 @@ export class SessionRate {
149
149
  #inverseSum = 0;
150
150
  #zero = false;
151
151
  #weighted = 0;
152
+ #capture = null;
152
153
 
153
- add(at, value) {
154
+ add(at, value, capture = null) {
154
155
  if (!Number.isFinite(at) || !Number.isFinite(value) || value < 0 || at <= (this.#last?.at ?? Number.NEGATIVE_INFINITY)) {
155
156
  return false;
156
157
  }
157
158
  if (this.#last) this.#weighted += this.#last.value * (at - this.#last.at);
158
159
  this.#first ??= { at, value };
159
160
  this.#last = { at, value };
161
+ this.#capture = mergeCaptureSummary(this.#capture, capture);
160
162
  this.#count += 1;
161
163
  this.#sum += value;
162
164
  if (value > 0) this.#inverseSum += 1 / value;
@@ -177,6 +179,10 @@ export class SessionRate {
177
179
  spanSeconds: spanMs / 1000
178
180
  };
179
181
  }
182
+
183
+ captureSnapshot() {
184
+ return this.#capture ? { ...this.#capture } : null;
185
+ }
180
186
  }
181
187
 
182
188
  /**
@@ -192,12 +198,12 @@ export class RateTrend {
192
198
  this.#horizonMs = Math.max(0, horizonSeconds) * 1000;
193
199
  }
194
200
 
195
- add(at, value) {
201
+ add(at, value, capture = null) {
196
202
  if (!Number.isFinite(at) || !Number.isFinite(value) || value < 0 ||
197
203
  at <= (this.#readings.at(-1)?.at ?? Number.NEGATIVE_INFINITY)) {
198
204
  return false;
199
205
  }
200
- this.#readings.push({ at, value });
206
+ this.#readings.push({ at, value, capture: capture ? { ...capture } : null });
201
207
  const cutoff = at - this.#horizonMs;
202
208
  while (this.#readings.length > 1 && this.#readings[1].at <= cutoff) {
203
209
  this.#readings.shift();
@@ -211,6 +217,22 @@ export class RateTrend {
211
217
  }
212
218
  return summarizeRate(this.#readings);
213
219
  }
220
+
221
+ captureSnapshot() {
222
+ return this.#readings.map(reading => reading.capture ? { ...reading.capture } : null);
223
+ }
224
+ }
225
+
226
+ function mergeCaptureSummary(previous, capture) {
227
+ if (!capture || capture.validity !== "valid" || !Number.isFinite(capture.clockEpoch) ||
228
+ !Number.isFinite(capture.startedAt) || !Number.isFinite(capture.completedAt) ||
229
+ capture.completedAt < capture.startedAt) return { validity: "unknown", unknownReason: "missing-rate-capture" };
230
+ if (!previous) return { ...capture };
231
+ if (previous.validity !== "valid" || previous.clockEpoch !== capture.clockEpoch) {
232
+ return { validity: "invalid", unknownReason: "rate-capture-epoch-mismatch" };
233
+ }
234
+ return { ...previous, startedAt: Math.min(previous.startedAt, capture.startedAt),
235
+ completedAt: Math.max(previous.completedAt, capture.completedAt), revision: capture.revision };
214
236
  }
215
237
 
216
238
  /**
@@ -233,7 +255,10 @@ export class RateTrend {
233
255
  * clockOffsetSeconds?: number, clientRanges?: Array<{ start: number, end: number }>,
234
256
  * appendService?: object, fragmentService?: object,
235
257
  * segments: Array<{ index: number, startSeconds: number, endSeconds: number,
258
+ * sourceInputs?: Array<{ sourceId: string, ranges: Array<{ start: number, end: number }> }>,
236
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 }> }> }>,
237
262
  * readySegmentIndices: number[], segmentSizesBytes: Map<number, number> | object }>} input.tracks
238
263
  * `mediaRanges` are a read piece's ranges on its served timeline, in ticks;
239
264
  * `clockOffsetSeconds` is the `timestampOffset` the page's player applied to
@@ -255,13 +280,23 @@ export function predictPlaybackReadiness(input = {}) {
255
280
  // THE NEAR WINDOW AND THE SUFFIX. The look-ahead is scheduled with exact
256
281
  // segment times and source ranges when known. Missing range metadata is priced
257
282
  // from aggregate file density and measured download service without claiming
258
- // an address or byte. Beyond the look-ahead, {@link scheduleTail} estimates
259
- // remaining source, encoder and delivery work without visiting every segment.
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.
260
286
  const lookahead = finiteNonNegative(input.lookaheadSeconds);
261
287
  const horizonEnd = lookahead > 0 ? finiteNonNegative(input.positionSeconds) + lookahead : Number.POSITIVE_INFINITY;
262
- const tracks = (Array.isArray(input.tracks) ? input.tracks : []).map((track) =>
263
- Array.isArray(track.segments) ? { ...track, segmentCount: track.segmentCount ?? track.segments.length,
264
- segments: piecesThatMatter(track, finiteNonNegative(input.positionSeconds), horizonEnd) } : track);
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
+ });
265
300
  const duration = finiteNonNegative(input.durationSeconds);
266
301
  // Each track's served ticks are placed on the clock the page reports by the
267
302
  // offset its player applied (`timestampOffset`).
@@ -743,13 +778,19 @@ export function predictPlaybackReadiness(input = {}) {
743
778
  }
744
779
  }
745
780
  const tailStock = [];
781
+ const tailCushionStock = [];
782
+ let tailUnmappedSources = new Set();
783
+ let tailUnmappedTracks = new Set();
746
784
  if (trackState.some(({ tailEndSeconds }) => Number.isFinite(tailEndSeconds))) {
747
- const stop = scheduleTail({ trackState, sourceState, runStarts, trackFinish,
748
- link, position, buffered, reserve, transferFinish, constrain, duration, stock: tailStock });
749
- if (stop) {
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) {
750
789
  return { ...result(false, null, buffered, reserve, null, stop.reason, preparedSegments),
751
790
  ...(stop.unavailableSource ? { unavailableSource: stop.unavailableSource } : {}) };
752
791
  }
792
+ tailUnmappedSources = stop?.unmappedSources ?? tailUnmappedSources;
793
+ tailUnmappedTracks = stop?.unmappedTracks ?? tailUnmappedTracks;
753
794
  }
754
795
  const firstRequiredStages = trackState.slice(0, requiredTracks).map((state, trackIndex) => {
755
796
  const first = state.segments.find(segment => segment.endSeconds > position);
@@ -838,6 +879,15 @@ export function predictPlaybackReadiness(input = {}) {
838
879
  const firstRequiredSegmentsSeconds = firstRequiredTrackSeconds.every(Number.isFinite)
839
880
  ? Math.max(0, ...firstRequiredTrackSeconds) : null;
840
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
+ }
841
891
  // THE STOCK THE START ASKS FOR HAS TO FIT. Starting without a stop while the
842
892
  // source comes slower than the film plays means holding, at the start, what
843
893
  // is already here and everything that arrives before it. Where that is more
@@ -848,10 +898,11 @@ export function predictPlaybackReadiness(input = {}) {
848
898
  // share (Home Assistant 2026-10-26, torrent-tv/meta#239). Said as such, and
849
899
  // the viewer keeps waiting rather than starting into stops.
850
900
  const storageBytes = Number(input.sourceStorageBytes);
851
- const tailParts = tailStock.filter(part => !part.skipped);
901
+ const tailParts = tailCushionStock.filter(part => !part.skipped);
852
902
  const cushionBytes = Number.isFinite(delay) && tailParts.length > 0
853
903
  ? tailParts.reduce((sum, part) => sum + Math.max(0, part.totalBytes - part.missingBytes) +
854
- Math.max(0, part.missingBytes - part.bytesPerSecond * Math.max(0, part.finish - delay)), 0)
904
+ Math.max(0, part.missingBytes - (part.planned ? (part.finish > delay ? part.missingBytes : 0)
905
+ : part.bytesPerSecond * Math.max(0, part.finish - delay))), 0)
855
906
  : null;
856
907
  if (Number.isFinite(delay) && Number.isFinite(storageBytes) && storageBytes >= 0 && tailParts.length > 0) {
857
908
  if (cushionBytes > storageBytes) {
@@ -941,20 +992,182 @@ function scheduleTransfers(jobs, link) {
941
992
  }
942
993
 
943
994
  /**
944
- * A duration-level estimate of the suffix after the exact near window. Source
945
- * work uses each file's aggregate byte density and measured download service;
946
- * processing uses each track's measured rate; delivery uses output bitrates and
947
- * measured link service. Held source bytes and made segments reduce the
948
- * estimate. This conditional forecast does not assert exact source ranges or
949
- * individual frame timing. Its work is bounded by sources and tracks, not by
950
- * the film's segment count.
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.
951
1160
  *
952
1161
  * @returns {{ reason: string, unavailableSource?: object } | null} Why there is no answer, or null.
953
1162
  */
954
- function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
955
- 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 }) {
956
1165
  const tailStart = Math.max(position, ...trackState.map(state => state.tailStartSeconds));
957
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;
958
1171
  const sourceDurations = new Map();
959
1172
  for (const state of trackState) {
960
1173
  const seconds = Math.max(0, state.tailEndSeconds - tailStart) * state.tailWorkFraction;
@@ -963,12 +1176,15 @@ function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
963
1176
  workFraction: state.tailWorkFraction, tailSegments: state.tailSegmentCount });
964
1177
  continue;
965
1178
  }
966
- for (const sourceId of new Set(state.track.sourceIds ?? [])) {
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;
967
1184
  sourceDurations.set(sourceId, Math.max(sourceDurations.get(sourceId) ?? 0, seconds));
968
1185
  }
969
1186
  }
970
1187
 
971
- let sourceArrivalAt = 0;
972
1188
  for (const [sourceId, seconds] of sourceDurations) {
973
1189
  const source = sourceState.get(sourceId);
974
1190
  if (!source) return { reason: "source-measurement-unavailable" };
@@ -1005,8 +1221,11 @@ function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
1005
1221
  sourceArrivalAt = Math.max(sourceArrivalAt, service.finish);
1006
1222
  // What this stretch weighs and what it still lacks: the stock the start
1007
1223
  // asks to hold is read from these once the start's delay is known.
1008
- stock?.push({ sourceId, seconds, windowEnd, totalBytes:estimateUnmappedSourceBytes(source.source, seconds, duration, []) ?? bytes,
1009
- missingBytes: bytes, bytesPerSecond: rate.rateAt(0), finish: service.finish });
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);
1010
1229
  const state = trackState.find(candidate => candidate.track.sourceIds?.includes(sourceId));
1011
1230
  const index = state?.segments.at(-1)?.index ?? 0;
1012
1231
  const ahead = Math.max(0, (state?.tailEndSeconds ?? tailStart) - position);
@@ -1059,7 +1278,8 @@ function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
1059
1278
  constrain(completedAt - Math.max(0, ahead - reserve), "delivery", 0,
1060
1279
  trackState[0]?.segments.at(-1)?.index ?? 0, completedAt, ahead, false, sourceArrivalAt, false);
1061
1280
  }
1062
- return null;
1281
+ return { unmappedSources: exactSources?.unmappedSources ?? new Set(),
1282
+ unmappedTracks: exactSources?.unmappedTracks ?? new Set() };
1063
1283
  }
1064
1284
 
1065
1285
  /**
@@ -1187,11 +1407,12 @@ function withSeekStartTolerance(ranges, position, unit) {
1187
1407
  * before `horizonEnd`, the first piece, every piece that has been read and
1188
1408
  * every piece that is made. A piece ending just before a seek target is kept
1189
1409
  * as a prerequisite because its media timestamps can extend past its nominal
1190
- * playlist boundary. A piece
1191
- * that is none of these changes no exact answer: after the near window it is
1192
- * scheduled in seconds, it is not where the track starts, no unread piece is
1193
- * placed by it (only read pieces are), it lends no frame to what the page
1194
- * holds, and it has no size to measure the output's rate by.
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.
1195
1416
  *
1196
1417
  * @param {{ segments: object[], readySegmentIndices?: number[] }} track
1197
1418
  * @param {number} position - Seconds.