@torrent-tv/proxy 2.107.40 → 2.107.42

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.40",
3
+ "version": "2.107.42",
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,16 +687,8 @@ export function predictPlaybackReadiness(input = {}) {
687
687
  }
688
688
  const tailStock = [];
689
689
  if (trackState.some(({ tailEndSeconds }) => Number.isFinite(tailEndSeconds))) {
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
690
  const stop = scheduleTail({ trackState, sourceState, runStarts, trackFinish,
699
- link, position, buffered, reserve, transferFinish, constrain, duration, stock: tailStock, windowEnds });
691
+ link, position, buffered, reserve, transferFinish, constrain, duration, stock: tailStock });
700
692
  if (stop) {
701
693
  return { ...result(false, null, buffered, reserve, null, stop.reason, preparedSegments),
702
694
  ...(stop.unavailableSource ? { unavailableSource: stop.unavailableSource } : {}) };
@@ -740,12 +732,13 @@ export function predictPlaybackReadiness(input = {}) {
740
732
  // share (Home Assistant 2026-10-26, torrent-tv/meta#239). Said as such, and
741
733
  // the viewer keeps waiting rather than starting into stops.
742
734
  const storageBytes = Number(input.sourceStorageBytes);
743
- if (Number.isFinite(delay) && Number.isFinite(storageBytes) && storageBytes >= 0 && tailStock.length > 0) {
744
- const cushionBytes = tailStock.reduce((sum, part) => sum + Math.max(0, part.totalBytes - part.missingBytes) +
735
+ const tailParts = tailStock.filter(part => !part.skipped);
736
+ if (Number.isFinite(delay) && Number.isFinite(storageBytes) && storageBytes >= 0 && tailParts.length > 0) {
737
+ const cushionBytes = tailParts.reduce((sum, part) => sum + Math.max(0, part.totalBytes - part.missingBytes) +
745
738
  Math.max(0, part.missingBytes - part.bytesPerSecond * Math.max(0, part.finish - delay)), 0);
746
739
  if (cushionBytes > storageBytes) {
747
740
  return { ...result(false, null, buffered, reserve, null, "cushion-exceeds-storage", preparedSegments),
748
- cushionBytes, storageBytes };
741
+ cushionBytes, storageBytes, tail: tailStock };
749
742
  }
750
743
  }
751
744
  const originReady = trackState.every(({ clientRanges }) => compare(contiguousEnd(clientRanges, at), at) > 0);
@@ -756,6 +749,9 @@ export function predictPlaybackReadiness(input = {}) {
756
749
  return { ...forecast, measuredAt: now, predictedStartAt: Number.isFinite(delay) ? now + delay * 1000 : null,
757
750
  firstRequiredSegmentsSeconds,
758
751
  firstRequiredStages,
752
+ // What the rest of the film weighs per source, at what rate it comes and
753
+ // when, or why a source has none: said in the readiness line.
754
+ tail: tailStock,
759
755
  // The piece whose `kind` — its production, or its delivery to the page —
760
756
  // comes `atSeconds` from now while the media before it lasts
761
757
  // `aheadSeconds`: the difference is the delay.
@@ -795,13 +791,17 @@ function isPiece(segment) {
795
791
  * @returns {{ reason: string, unavailableSource?: object } | null} Why there is no answer, or null.
796
792
  */
797
793
  function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
798
- position, buffered, reserve, transferFinish, constrain, duration, stock = null, windowEnds = null }) {
794
+ position, buffered, reserve, transferFinish, constrain, duration, stock = null }) {
799
795
  const tailStart = Math.max(position, ...trackState.map(state => state.tailStartSeconds));
800
796
  if (!Number.isFinite(tailStart)) return null;
801
797
  const sourceDurations = new Map();
802
798
  for (const state of trackState) {
803
799
  const seconds = Math.max(0, state.tailEndSeconds - tailStart) * state.tailWorkFraction;
804
- if (!(seconds > 0)) continue;
800
+ if (!(seconds > 0)) {
801
+ stock?.push({ track: state.track.id, skipped: "no-work", tailStart, tailEnd: state.tailEndSeconds,
802
+ workFraction: state.tailWorkFraction, tailSegments: state.tailSegmentCount });
803
+ continue;
804
+ }
805
805
  for (const sourceId of new Set(state.track.sourceIds ?? [])) {
806
806
  sourceDurations.set(sourceId, Math.max(sourceDurations.get(sourceId) ?? 0, seconds));
807
807
  }
@@ -811,17 +811,29 @@ function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
811
811
  for (const [sourceId, seconds] of sourceDurations) {
812
812
  const source = sourceState.get(sourceId);
813
813
  if (!source) return { reason: "source-measurement-unavailable" };
814
- if (source.source.complete === true) continue;
815
- // Held means on this proxy now, past the last byte the near window's
816
- // pieces read: those are counted above, and the window's planned arrivals
817
- // counted again as held took 117 s of film off a 90-minute tail. The
818
- // torrent thread's plan is not that end: it can reach the end of the file.
819
- const windowEnd = windowEnds?.get(sourceId) ?? 0;
814
+ if (source.source.complete === true) {
815
+ stock?.push({ sourceId, skipped: "complete" });
816
+ continue;
817
+ }
818
+ // Held means on this proxy now, in the bytes of the film after the near
819
+ // window: the window's own bytes are counted above, and its planned
820
+ // arrivals counted again as held took 117 s of film off a 90-minute tail.
821
+ // Where those bytes begin is placed by the density the tail is priced at.
822
+ // Neither the torrent thread's plan nor the bytes the window reads are
823
+ // that place: both can reach the end of the file — a copied Matroska file
824
+ // reads its index there — and the whole of Despicable Me 3 then counted
825
+ // as held (Home Assistant 2026-10-26, torrent-tv/meta#239).
826
+ const fileLength = Number(source.source.fileLength);
827
+ const windowEnd = fileLength > 0 && duration > 0
828
+ ? Math.min(fileLength, Math.max(0, fileLength * tailStart / duration)) : 0;
820
829
  const bytes = estimateUnmappedSourceBytes(source.source, seconds, duration,
821
830
  heldBeyond(source.source.residence, windowEnd), windowEnd);
822
831
  if (bytes === null) return { reason: "source-measurement-unavailable",
823
832
  unavailableSource: { sourceId, phase: "tail" } };
824
- if (!(bytes > 0)) continue;
833
+ if (!(bytes > 0)) {
834
+ stock?.push({ sourceId, skipped: "held", seconds, windowEnd });
835
+ continue;
836
+ }
825
837
  const { service } = source;
826
838
  const rate = service.tailRate ?? service.rate;
827
839
  if (!rate || !(rate.rateAt(0) > 0)) {
@@ -832,7 +844,7 @@ function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
832
844
  sourceArrivalAt = Math.max(sourceArrivalAt, service.finish);
833
845
  // What this stretch weighs and what it still lacks: the stock the start
834
846
  // asks to hold is read from these once the start's delay is known.
835
- stock?.push({ totalBytes: estimateUnmappedSourceBytes(source.source, seconds, duration, []) ?? bytes,
847
+ stock?.push({ sourceId, seconds, windowEnd, totalBytes:estimateUnmappedSourceBytes(source.source, seconds, duration, []) ?? bytes,
836
848
  missingBytes: bytes, bytesPerSecond: rate.rateAt(0), finish: service.finish });
837
849
  const state = trackState.find(candidate => candidate.track.sourceIds?.includes(sourceId));
838
850
  const index = state?.segments.at(-1)?.index ?? 0;