@torrent-tv/proxy 2.95.28 → 2.95.29

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.95.28",
3
+ "version": "2.95.29",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -614,6 +614,10 @@ export class ViewerRequests {
614
614
  id: sourceId,
615
615
  complete: stats?.fileAvailable === true,
616
616
  residence: stats?.residence,
617
+ fileOffset: stats?.fileOffset,
618
+ fileLength: stats?.fileLength,
619
+ pieceLength: stats?.pieceLength,
620
+ downloadRateReadings: this.#sourceRateReadings(measurement, sourceId, stats, now),
617
621
  downloadForecast,
618
622
  });
619
623
  }
@@ -718,10 +722,11 @@ export class ViewerRequests {
718
722
  bufferLimitSeconds: input.bufferLimitSeconds,
719
723
  lookaheadSeconds: input.lookaheadSeconds,
720
724
  link: input.linkReadings,
721
- sources: input.sources.map(({ id, complete, residence, downloadForecast }) => ({
725
+ sources: input.sources.map(({ id, complete, residence, downloadForecast, downloadRateReadings }) => ({
722
726
  id, complete, residenceRanges: residence?.length ?? 0,
723
727
  forecastAt: downloadForecast?.measuredAt ?? null, plannedPieces: downloadForecast?.ranges.length ?? 0,
724
- unknownPieces: downloadForecast?.ranges.filter(range => range.availableAt === null).length ?? 0
728
+ unknownPieces: downloadForecast?.ranges.filter(range => range.availableAt === null).length ?? 0,
729
+ downloadRateBytesPerSecond: downloadRateReadings?.lastValue ?? null
725
730
  })),
726
731
  tracks: tracks.map(({ id, processedSeconds, readings, segments, readySegmentIndices }) => ({
727
732
  id, processedSeconds, readings, segments: segments.length, prepared: readySegmentIndices.length
@@ -740,7 +745,7 @@ export class ViewerRequests {
740
745
  const id = typeof consumerId === "string" ? consumerId : "";
741
746
  let state = byViewer.get(id);
742
747
  if (!state) {
743
- state = { link: new this.#host.playbackReadiness.RateTrend(this.#host.lookaheadSeconds), tracks: new Map() };
748
+ state = { link: new this.#host.playbackReadiness.RateTrend(this.#host.lookaheadSeconds), tracks: new Map(), sources: new Map() };
744
749
  byViewer.set(id, state);
745
750
  }
746
751
  return state;
@@ -764,4 +769,16 @@ export class ViewerRequests {
764
769
  }
765
770
  return trend.snapshot();
766
771
  }
772
+
773
+ #sourceRateReadings(state, id, stats, now) {
774
+ let trend = state.sources.get(id);
775
+ if (!trend) {
776
+ trend = new this.#host.playbackReadiness.RateTrend(this.#host.lookaheadSeconds);
777
+ state.sources.set(id, trend);
778
+ }
779
+ if (Number.isFinite(stats?.downloadSpeed) && stats.downloadSpeed >= 0) {
780
+ trend.add(now, stats.downloadSpeed);
781
+ }
782
+ return trend.snapshot();
783
+ }
767
784
  }
@@ -2118,6 +2118,7 @@ export class TorrentPool {
2118
2118
  fileDownloaded,
2119
2119
  fileLength,
2120
2120
  fileOffset: file.offset,
2121
+ pieceLength: torrent.pieceLength,
2121
2122
  residence,
2122
2123
  fileAvailable: fileLength > 0 && residence.length > 0 && residence.every(range => range.location !== "missing"),
2123
2124
  // Compatibility fields report the same current urgent map demand.
@@ -193,7 +193,7 @@ export function predictPlaybackReadiness(input = {}) {
193
193
  }
194
194
  const sourceState = new Map();
195
195
  for (const [id, source] of sources) {
196
- sourceState.set(id, { source });
196
+ sourceState.set(id, { source, rate: rateCurve(source.downloadRateReadings, now), serviceFinish: 0 });
197
197
  }
198
198
  const link = rateCurve(input.linkReadings, now);
199
199
  let unknownReason = null;
@@ -318,11 +318,43 @@ export function predictPlaybackReadiness(input = {}) {
318
318
  const { track, trackIndex, segment, sizeBits, encodeWork, sourceByteIntervals } = job;
319
319
  let sourceArrivalAt = 0;
320
320
  for (const [sourceId, ranges] of sourceByteIntervals) {
321
+ const source = sourceState.get(sourceId);
322
+ if (!source) {
323
+ return result(false, null, buffered, reserve, null, "source-measurement-unavailable", preparedSegments);
324
+ }
321
325
  for (const interval of ranges) {
322
326
  const missing = uncoveredIntervals(interval, sourceByteCoverage.get(sourceId) ?? []);
323
- if (missing.length) return { ...result(false, null, buffered, reserve, null, "download-schedule-unavailable", preparedSegments),
327
+ if (missing.length === 0) {
328
+ sourceArrivalAt = Math.max(sourceArrivalAt, latestArrival(interval, sourceArrivals.get(sourceId)));
329
+ continue;
330
+ }
331
+
332
+ // Explicit piece arrivals remain authoritative. For still-unmapped
333
+ // pieces, project only the media-order demand from the torrent's latest
334
+ // measured download service; no supplier or positive rate means no
335
+ // estimated arrival.
336
+ const { source: facts, rate } = source;
337
+ if (!rate || !(rate.rateAt(0) > 0) || !Number.isSafeInteger(facts.pieceLength) ||
338
+ facts.pieceLength <= 0 || !Number.isSafeInteger(facts.fileOffset) ||
339
+ !Number.isSafeInteger(facts.fileLength) || facts.fileLength <= 0) {
340
+ return { ...result(false, null, buffered, reserve, null,
341
+ rate ? "service-not-advancing" : "download-rate-unavailable", preparedSegments),
324
342
  unavailableSource: { sourceId, segmentIndex: segment.index, range: interval, missing } };
325
- sourceArrivalAt = Math.max(sourceArrivalAt, latestArrival(interval, sourceArrivals.get(sourceId)));
343
+ }
344
+ const estimatedPieces = sourcePiecesFor(missing, facts);
345
+ let finish = Math.max(source.serviceFinish, sourceArrivalAt);
346
+ const service = sourceByteService.get(sourceId) ?? [];
347
+ for (const pieceRange of estimatedPieces) {
348
+ const stillMissing = uncoveredIntervals(pieceRange, sourceByteCoverage.get(sourceId) ?? []);
349
+ for (const part of stillMissing) {
350
+ finish = rate.finish(part.end - part.start, finish);
351
+ service.push({ start: part.start, end: part.end, begin: finish, finish });
352
+ sourceArrivalAt = Math.max(sourceArrivalAt, finish);
353
+ }
354
+ sourceByteCoverage.set(sourceId, numericCoverage(service));
355
+ sourceArrivals.set(sourceId, indexedArrivals(service));
356
+ }
357
+ source.serviceFinish = finish;
326
358
  }
327
359
  }
328
360
  // Production accepts a complete held segment input. It cannot overlap its
@@ -536,6 +568,22 @@ function uncoveredIntervals(interval, served) {
536
568
  return missing;
537
569
  }
538
570
 
571
+ /** Expand missing file bytes to the whole torrent pieces the reader must hold. */
572
+ function sourcePiecesFor(ranges, source) {
573
+ const pieces = [];
574
+ for (const range of ranges) {
575
+ if (!(range.end > range.start)) continue;
576
+ const first = Math.floor((source.fileOffset + range.start) / source.pieceLength);
577
+ const last = Math.ceil((source.fileOffset + range.end) / source.pieceLength) - 1;
578
+ for (let index = first; index <= last; index += 1) {
579
+ const start = Math.max(0, index * source.pieceLength - source.fileOffset);
580
+ const end = Math.min(source.fileLength, (index + 1) * source.pieceLength - source.fileOffset);
581
+ if (end > start) pieces.push({ start, end });
582
+ }
583
+ }
584
+ return numericCoverage(pieces);
585
+ }
586
+
539
587
  /**
540
588
  * Ranges for pieces not read yet, placed in the clock of the pieces that were.
541
589
  * A cut time of the playlist is mapped linearly between the nearest measured