@torrent-tv/proxy 2.100.2 → 2.100.4

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.100.2",
3
+ "version": "2.100.4",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
package/server.js CHANGED
@@ -972,8 +972,12 @@ export async function startProxyServer({
972
972
  const answer = input.read(source.fileIndex, start, end);
973
973
  return answer && typeof answer.then !== "function" && answer.end === end ? answer.bytes : null;
974
974
  };
975
+ const holdsUntil = start => {
976
+ const range = source.input.ranges.find(([first, last]) => first <= start && start <= last);
977
+ return range ? range[1] + 1 : null;
978
+ };
975
979
  const startedAt = Date.now();
976
- void container.learnPackets({ from: source.input.from, to: source.input.to, trackIds: [...sounds] }, read).then(learned =>
980
+ void container.learnPackets({ from: source.input.from, to: source.input.to, trackIds: [...sounds] }, read, holdsUntil).then(learned =>
977
981
  logger.info(`encode input #${from}..#${to} of ${output.outputKey}: soundtrack packets of track(s) ${[...sounds].join(",")} ` +
978
982
  `over ${source.input.from.toFixed(3)}-${source.input.to.toFixed(3)}s ` +
979
983
  (learned === "walked" ? "walked" : `not all walked (${learned === "memory" ? "memory refused" : "the copy does not hold the rest"})`) +
@@ -654,11 +654,17 @@ export class HostLoad {
654
654
  // alone would print something like `ffmpeg=-598%`. Only pids present in
655
655
  // BOTH readings are counted, so a process that came or went contributes
656
656
  // nothing rather than a lie.
657
- const runs = encoding.flatMap((session) => this.#host.liveRunsOf(session)).filter((run) => Number.isInteger(run.process?.pid));
657
+ // Each run's pid and start are read BEFORE the readings are awaited: a run
658
+ // that ends meanwhile drops its process, and read after the await it threw
659
+ // `Cannot read properties of null (reading 'pid')`, seven times in one
660
+ // field session, each losing that reading (r64, torrent-tv/meta#178).
661
+ const runs = encoding.flatMap((session) => this.#host.liveRunsOf(session))
662
+ .filter((run) => Number.isInteger(run.process?.pid))
663
+ .map((run) => ({ pid: run.process.pid, startedAt: run.startedAt }));
658
664
  const [system, endedChildren, ...cpuReadings] = await Promise.all([
659
665
  this.#host.readSystemCpu(),
660
666
  this.#host.readEndedChildrenCpuSeconds?.() ?? null,
661
- ...runs.map((run) => this.#host.readProcessCpuSeconds(run.process.pid))
667
+ ...runs.map((run) => this.#host.readProcessCpuSeconds(run.pid))
662
668
  ]);
663
669
  /** @type {Map<number, number>} */
664
670
  const byPid = new Map();
@@ -667,8 +673,8 @@ export class HostLoad {
667
673
  runs.forEach((run, index) => {
668
674
  const seconds = cpuReadings[index];
669
675
  if (seconds !== null) {
670
- byPid.set(run.process.pid, seconds);
671
- startedAt.set(run.process.pid, run.startedAt);
676
+ byPid.set(run.pid, seconds);
677
+ startedAt.set(run.pid, run.startedAt);
672
678
  }
673
679
  });
674
680
  const sample = {
@@ -792,7 +792,7 @@ export class MatroskaContainer extends Container {
792
792
  * `account`, when given, is told how long each track's walk waited for
793
793
  * that track's earlier walks and what it read (`lockMs`, `reads`, `bytes`).
794
794
  */
795
- async readPacketIndex(interval, { isCurrent, readRange, account } = {}) {
795
+ async readPacketIndex(interval, { isCurrent, readRange, holdsUntil, account } = {}) {
796
796
  if (this.#packets) return this.#packets;
797
797
  const layout = await this.#segmentLayout();
798
798
  if (!layout) throw new Error("Matroska Segment is absent.");
@@ -812,7 +812,7 @@ export class MatroskaContainer extends Container {
812
812
  let whole = true;
813
813
  for (const track of requested) {
814
814
  const chain = await this.#readTrackPackets(layout, tracks, track, start, interval, isCurrent,
815
- readRange ? strictReader(readRange, this.fileSize) : this.readRange, account);
815
+ readRange ? strictReader(readRange, this.fileSize) : this.readRange, account, readRange ? holdsUntil : null);
816
816
  if (!chain) return null;
817
817
  streams.set(track.trackNumber, chain.records);
818
818
  if (chain.reachesEnd) complete.add(track.trackNumber);
@@ -837,13 +837,14 @@ export class MatroskaContainer extends Container {
837
837
  *
838
838
  * @param {{ from: number, to: number, trackIds: number[] }} interval
839
839
  * @param {(start: number, end: number) => Buffer | null} readRange - Null where it does not hold the bytes.
840
+ * @param {(start: number) => number | null} [holdsUntil] - One past the last byte held from `start` on, if known.
840
841
  * @returns {Promise<"walked" | "missing" | "memory">} Whether the
841
842
  * interval's packets are recorded, or what stopped the walk: bytes the
842
843
  * copy does not hold, or memory refused.
843
844
  */
844
- async learnPackets(interval, readRange) {
845
+ async learnPackets(interval, readRange, holdsUntil = null) {
845
846
  try {
846
- return await this.readPacketIndex(interval, { readRange }) !== null ? "walked" : "missing";
847
+ return await this.readPacketIndex(interval, { readRange, holdsUntil }) !== null ? "walked" : "missing";
847
848
  } catch (error) {
848
849
  // The sound's own walk reads the rest later, from where this one stopped.
849
850
  if (isUnavailable(error)) return "missing";
@@ -865,7 +866,7 @@ export class MatroskaContainer extends Container {
865
866
  *
866
867
  * @returns {Promise<{ records: PacketRecordsChain, startAt: number, reachesEnd: boolean }>}
867
868
  */
868
- async #readTrackPackets(layout, tracks, track, start, interval, isCurrent, reader, account = null) {
869
+ async #readTrackPackets(layout, tracks, track, start, interval, isCurrent, reader, account = null, holdsUntil = null) {
869
870
  const id = track.trackNumber;
870
871
  const previous = this.#packetWalks.get(id) ?? Promise.resolve();
871
872
  let finished;
@@ -930,7 +931,7 @@ export class MatroskaContainer extends Container {
930
931
  const following = readings.find(reading => reading.startAt > current.startAt);
931
932
  const outcome = await walkMatroskaPackets({ readRange: read, fileSize: this.fileSize,
932
933
  portionBytes: this.portionBytes, layout, tracks, trackIds: [id], interval, packetMemory: this.packetMemory,
933
- state: current, until: following?.startAt ?? null, isCovered: covered, readsThrough, isCurrent });
934
+ state: current, until: following?.startAt ?? null, isCovered: covered, readsThrough, isCurrent, holdsUntil });
934
935
  if (outcome === "obsolete") return null;
935
936
  if (outcome !== "until") break;
936
937
  current.next = following;
@@ -49,7 +49,7 @@ function recordedTracks(tracks, trackIds) {
49
49
  *
50
50
  * @returns {Promise<"covered" | "until" | "end" | "obsolete">}
51
51
  */
52
- export async function walkMatroskaPackets({ readRange, fileSize, portionBytes, layout, tracks, trackIds, interval, packetMemory, state, until = null, isCovered = null, readsThrough = null, isCurrent = null }) {
52
+ export async function walkMatroskaPackets({ readRange, fileSize, portionBytes, layout, tracks, trackIds, interval, packetMemory, state, until = null, isCovered = null, readsThrough = null, isCurrent = null, holdsUntil = null }) {
53
53
  // INSIDE A CLUSTER EVERY BLOCK HEADER IS READ, so its bytes are read a
54
54
  // portion at a time up to the cluster's end, and never past it. Read a
55
55
  // header at a time, each a read of a few bytes from the torrent thread, the
@@ -61,8 +61,14 @@ export async function walkMatroskaPackets({ readRange, fileSize, portionBytes, l
61
61
  // (r54). The headers lie throughout the portion, so it is all needed.
62
62
  // Between clusters only their headers are read.
63
63
  let readableUntil = null;
64
+ // `holdsUntil` bounds a portion by what the reader holds, so a walk over a
65
+ // copy reads up to its end what it needs, not a portion past it (r63).
64
66
  const reader = new ElementReader({ read: readRange, fileSize, portionBytes, prefetch: true,
65
- readableUntil: start => Math.max(readableUntil ?? start, readsThrough?.(start) ?? start) });
67
+ readableUntil: start => {
68
+ const reach = Math.max(readableUntil ?? start, readsThrough?.(start) ?? start);
69
+ const held = holdsUntil?.(start);
70
+ return Number.isFinite(held) ? Math.min(reach, held) : reach;
71
+ } });
66
72
  const declared = recordedTracks(tracks, trackIds);
67
73
  if (!state.packets) {
68
74
  state.packets = new Map([...declared.keys()].map(id => [id, new PacketRecords(packetMemory)]));