@torrent-tv/proxy 2.107.39 → 2.107.41

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.107.39",
3
+ "version": "2.107.41",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -629,7 +629,7 @@ export function wireOutputs({
629
629
  viewerSecondsOn: (output, consumerId, now) => viewerSecondsOn(parts.viewers, output, consumerId, now),
630
630
  minimumBufferSecondsFor: (output) => minimumBufferSecondsOf(output),
631
631
  // What this proxy may keep of downloaded pieces on disk.
632
- sourceStorageBytes: () => parts.machineBudget?.allowedFor?.("spilled pieces") ?? null,
632
+ sourceStorageBytes: () => parts.machineBudget?.allowedFor("spilled pieces") ?? null,
633
633
  getSourceStats: (...args) => parts.getSourceStats?.(...args) ?? null,
634
634
  disposeSession: (...args) => parts.lifecycle.disposeSession(...args),
635
635
  planEncodersSoon: (...args) => parts.encodeRuns.planEncodersSoon(...args),
@@ -194,6 +194,8 @@ export function wireMachineBudget({
194
194
 
195
195
  return {
196
196
  capacityOf: name => budget.capacityOf(name),
197
+ /** What one claimant was last allowed, or null before a measured division. */
198
+ allowedFor: name => budget.allowedFor(name),
197
199
  /**
198
200
  * @param {string | null} [resource] - One resource to revise; all when absent.
199
201
  * @param {string | null} [because] - Who asked, for the revision's line.
@@ -687,8 +687,16 @@ export function predictPlaybackReadiness(input = {}) {
687
687
  }
688
688
  const tailStock = [];
689
689
  if (trackState.some(({ tailEndSeconds }) => Number.isFinite(tailEndSeconds))) {
690
- const stop = scheduleTail({ trackState, sourceState, sourceByteCoverage, runStarts, trackFinish,
691
- link, position, buffered, reserve, transferFinish, constrain, duration, stock: tailStock });
690
+ // Where each source's bytes stop being the near window's: the last byte
691
+ // its pieces read, in the file's own offsets.
692
+ const windowEnds = new Map();
693
+ for (const { sourceByteIntervals } of jobs) {
694
+ for (const [sourceId, ranges] of sourceByteIntervals) {
695
+ for (const range of ranges) windowEnds.set(sourceId, Math.max(windowEnds.get(sourceId) ?? 0, range.end));
696
+ }
697
+ }
698
+ const stop = scheduleTail({ trackState, sourceState, runStarts, trackFinish,
699
+ link, position, buffered, reserve, transferFinish, constrain, duration, stock: tailStock, windowEnds });
692
700
  if (stop) {
693
701
  return { ...result(false, null, buffered, reserve, null, stop.reason, preparedSegments),
694
702
  ...(stop.unavailableSource ? { unavailableSource: stop.unavailableSource } : {}) };
@@ -732,12 +740,13 @@ export function predictPlaybackReadiness(input = {}) {
732
740
  // share (Home Assistant 2026-10-26, torrent-tv/meta#239). Said as such, and
733
741
  // the viewer keeps waiting rather than starting into stops.
734
742
  const storageBytes = Number(input.sourceStorageBytes);
735
- if (Number.isFinite(delay) && Number.isFinite(storageBytes) && storageBytes >= 0 && tailStock.length > 0) {
736
- const cushionBytes = tailStock.reduce((sum, part) => sum + Math.max(0, part.totalBytes - part.missingBytes) +
743
+ const tailParts = tailStock.filter(part => !part.skipped);
744
+ if (Number.isFinite(delay) && Number.isFinite(storageBytes) && storageBytes >= 0 && tailParts.length > 0) {
745
+ const cushionBytes = tailParts.reduce((sum, part) => sum + Math.max(0, part.totalBytes - part.missingBytes) +
737
746
  Math.max(0, part.missingBytes - part.bytesPerSecond * Math.max(0, part.finish - delay)), 0);
738
747
  if (cushionBytes > storageBytes) {
739
748
  return { ...result(false, null, buffered, reserve, null, "cushion-exceeds-storage", preparedSegments),
740
- cushionBytes, storageBytes };
749
+ cushionBytes, storageBytes, tail: tailStock };
741
750
  }
742
751
  }
743
752
  const originReady = trackState.every(({ clientRanges }) => compare(contiguousEnd(clientRanges, at), at) > 0);
@@ -748,6 +757,9 @@ export function predictPlaybackReadiness(input = {}) {
748
757
  return { ...forecast, measuredAt: now, predictedStartAt: Number.isFinite(delay) ? now + delay * 1000 : null,
749
758
  firstRequiredSegmentsSeconds,
750
759
  firstRequiredStages,
760
+ // What the rest of the film weighs per source, at what rate it comes and
761
+ // when, or why a source has none: said in the readiness line.
762
+ tail: tailStock,
751
763
  // The piece whose `kind` — its production, or its delivery to the page —
752
764
  // comes `atSeconds` from now while the media before it lasts
753
765
  // `aheadSeconds`: the difference is the delay.
@@ -786,14 +798,18 @@ function isPiece(segment) {
786
798
  *
787
799
  * @returns {{ reason: string, unavailableSource?: object } | null} Why there is no answer, or null.
788
800
  */
789
- function scheduleTail({ trackState, sourceState, sourceByteCoverage, runStarts, trackFinish, link,
790
- position, buffered, reserve, transferFinish, constrain, duration, stock = null }) {
801
+ function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
802
+ position, buffered, reserve, transferFinish, constrain, duration, stock = null, windowEnds = null }) {
791
803
  const tailStart = Math.max(position, ...trackState.map(state => state.tailStartSeconds));
792
804
  if (!Number.isFinite(tailStart)) return null;
793
805
  const sourceDurations = new Map();
794
806
  for (const state of trackState) {
795
807
  const seconds = Math.max(0, state.tailEndSeconds - tailStart) * state.tailWorkFraction;
796
- if (!(seconds > 0)) continue;
808
+ if (!(seconds > 0)) {
809
+ stock?.push({ track: state.track.id, skipped: "no-work", tailStart, tailEnd: state.tailEndSeconds,
810
+ workFraction: state.tailWorkFraction, tailSegments: state.tailSegmentCount });
811
+ continue;
812
+ }
797
813
  for (const sourceId of new Set(state.track.sourceIds ?? [])) {
798
814
  sourceDurations.set(sourceId, Math.max(sourceDurations.get(sourceId) ?? 0, seconds));
799
815
  }
@@ -803,16 +819,23 @@ function scheduleTail({ trackState, sourceState, sourceByteCoverage, runStarts,
803
819
  for (const [sourceId, seconds] of sourceDurations) {
804
820
  const source = sourceState.get(sourceId);
805
821
  if (!source) return { reason: "source-measurement-unavailable" };
806
- if (source.source.complete === true) continue;
807
- // Held means on this proxy now, past the bytes the near window already
808
- // scheduled: those are counted above, and the window's planned arrivals
809
- // counted again as held took 117 s of film off a 90-minute tail.
810
- const windowEnd = Math.max(0, ...(sourceByteCoverage.get(sourceId) ?? []).map(range => range.end));
822
+ if (source.source.complete === true) {
823
+ stock?.push({ sourceId, skipped: "complete" });
824
+ continue;
825
+ }
826
+ // Held means on this proxy now, past the last byte the near window's
827
+ // pieces read: those are counted above, and the window's planned arrivals
828
+ // counted again as held took 117 s of film off a 90-minute tail. The
829
+ // torrent thread's plan is not that end: it can reach the end of the file.
830
+ const windowEnd = windowEnds?.get(sourceId) ?? 0;
811
831
  const bytes = estimateUnmappedSourceBytes(source.source, seconds, duration,
812
832
  heldBeyond(source.source.residence, windowEnd), windowEnd);
813
833
  if (bytes === null) return { reason: "source-measurement-unavailable",
814
834
  unavailableSource: { sourceId, phase: "tail" } };
815
- if (!(bytes > 0)) continue;
835
+ if (!(bytes > 0)) {
836
+ stock?.push({ sourceId, skipped: "held", seconds, windowEnd });
837
+ continue;
838
+ }
816
839
  const { service } = source;
817
840
  const rate = service.tailRate ?? service.rate;
818
841
  if (!rate || !(rate.rateAt(0) > 0)) {
@@ -823,7 +846,7 @@ function scheduleTail({ trackState, sourceState, sourceByteCoverage, runStarts,
823
846
  sourceArrivalAt = Math.max(sourceArrivalAt, service.finish);
824
847
  // What this stretch weighs and what it still lacks: the stock the start
825
848
  // asks to hold is read from these once the start's delay is known.
826
- stock?.push({ totalBytes: estimateUnmappedSourceBytes(source.source, seconds, duration, []) ?? bytes,
849
+ stock?.push({ sourceId, seconds, windowEnd, totalBytes:estimateUnmappedSourceBytes(source.source, seconds, duration, []) ?? bytes,
827
850
  missingBytes: bytes, bytesPerSecond: rate.rateAt(0), finish: service.finish });
828
851
  const state = trackState.find(candidate => candidate.track.sourceIds?.includes(sourceId));
829
852
  const index = state?.segments.at(-1)?.index ?? 0;