@torrent-tv/proxy 2.100.1 → 2.100.3

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.1",
3
+ "version": "2.100.3",
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
@@ -892,6 +892,7 @@ export async function startProxyServer({
892
892
  const params = await containerOver({ sourceKey: output.file.sourceKey, fileIndex });
893
893
  if (!params) return { kind: "needs-source" };
894
894
  if (timing) {
895
+ params.walkAccount = timing.walk ??= { lockMs: 0, reads: 0, bytes: 0 };
895
896
  params.onTimed = (_statement, waitedMs, readMs) => {
896
897
  timing.reads += 1;
897
898
  timing.queuedMs += waitedMs;
@@ -971,10 +972,16 @@ export async function startProxyServer({
971
972
  const answer = input.read(source.fileIndex, start, end);
972
973
  return answer && typeof answer.then !== "function" && answer.end === end ? answer.bytes : null;
973
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
+ };
974
979
  const startedAt = Date.now();
975
- 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 =>
976
981
  logger.info(`encode input #${from}..#${to} of ${output.outputKey}: soundtrack packets of track(s) ${[...sounds].join(",")} ` +
977
- `over ${source.input.from.toFixed(3)}-${source.input.to.toFixed(3)}s ${learned ? "walked" : "not all walked"} from its copy in ${Date.now() - startedAt}ms`),
982
+ `over ${source.input.from.toFixed(3)}-${source.input.to.toFixed(3)}s ` +
983
+ (learned === "walked" ? "walked" : `not all walked (${learned === "memory" ? "memory refused" : "the copy does not hold the rest"})`) +
984
+ ` from its copy in ${Date.now() - startedAt}ms`),
978
985
  error => logger.warn(`encode input #${from}..#${to} of ${output.outputKey}: walking the soundtrack from its copy failed: ${error?.message ?? error}`));
979
986
  }
980
987
  },
@@ -648,6 +648,9 @@ function describeTiming(timing, now) {
648
648
  `${timing.resolutions} resolution(s) ${timing.resolveMs}ms ` +
649
649
  `(${timing.reads} file read(s): queued ${timing.queuedMs}ms, reading ${timing.readMs}ms), ` +
650
650
  `held ranges ${timing.heldMs}ms, budget ${timing.budgetMs}ms` +
651
+ // Where a packet input's walk spent its read: waiting for the same
652
+ // track's earlier walks, or reading the file (r62, torrent-tv/meta#178).
653
+ (timing.walk ? `; packet walk waited ${timing.walk.lockMs}ms for its track and read ${timing.walk.reads} time(s), ${timing.walk.bytes} bytes of the file` : "") +
651
654
  (timing.openApproximate ? "; sized by the host's startup open, this output has not opened yet" : "");
652
655
  }
653
656
 
@@ -215,7 +215,7 @@ export class ContainerOrchestrator {
215
215
  value = await container.readSourceRanges(params.packetInterval);
216
216
  if (value.kind !== "result") return { ...value, requestId };
217
217
  } else if (statement === "packets") {
218
- value = await container.readPacketIndex(params.packetInterval, { isCurrent: params.isCurrent });
218
+ value = await container.readPacketIndex(params.packetInterval, { isCurrent: params.isCurrent, account: params.walkAccount });
219
219
  if (value === null) return { kind: "terminal", reason: "request-obsolete", requestId };
220
220
  await value?.prepareAudioDependencies?.(params.packetInterval, container.readRange);
221
221
  } else {
@@ -789,8 +789,10 @@ export class MatroskaContainer extends Container {
789
789
  * A walk whose request is no longer current stops between clusters and
790
790
  * answers null. `readRange`, when given, is where this one walk reads
791
791
  * instead of the file's own reader; what it does not hold is unavailable.
792
+ * `account`, when given, is told how long each track's walk waited for
793
+ * that track's earlier walks and what it read (`lockMs`, `reads`, `bytes`).
792
794
  */
793
- async readPacketIndex(interval, { isCurrent, readRange } = {}) {
795
+ async readPacketIndex(interval, { isCurrent, readRange, holdsUntil, account } = {}) {
794
796
  if (this.#packets) return this.#packets;
795
797
  const layout = await this.#segmentLayout();
796
798
  if (!layout) throw new Error("Matroska Segment is absent.");
@@ -810,7 +812,7 @@ export class MatroskaContainer extends Container {
810
812
  let whole = true;
811
813
  for (const track of requested) {
812
814
  const chain = await this.#readTrackPackets(layout, tracks, track, start, interval, isCurrent,
813
- readRange ? strictReader(readRange, this.fileSize) : this.readRange);
815
+ readRange ? strictReader(readRange, this.fileSize) : this.readRange, account, readRange ? holdsUntil : null);
814
816
  if (!chain) return null;
815
817
  streams.set(track.trackNumber, chain.records);
816
818
  if (chain.reachesEnd) complete.add(track.trackNumber);
@@ -835,15 +837,18 @@ export class MatroskaContainer extends Container {
835
837
  *
836
838
  * @param {{ from: number, to: number, trackIds: number[] }} interval
837
839
  * @param {(start: number, end: number) => Buffer | null} readRange - Null where it does not hold the bytes.
838
- * @returns {Promise<boolean>} Whether the interval's packets are recorded.
840
+ * @param {(start: number) => number | null} [holdsUntil] - One past the last byte held from `start` on, if known.
841
+ * @returns {Promise<"walked" | "missing" | "memory">} Whether the
842
+ * interval's packets are recorded, or what stopped the walk: bytes the
843
+ * copy does not hold, or memory refused.
839
844
  */
840
- async learnPackets(interval, readRange) {
845
+ async learnPackets(interval, readRange, holdsUntil = null) {
841
846
  try {
842
- return await this.readPacketIndex(interval, { readRange }) !== null;
847
+ return await this.readPacketIndex(interval, { readRange, holdsUntil }) !== null ? "walked" : "missing";
843
848
  } catch (error) {
844
- // What the copy does not hold, or memory refused: the sound's own walk
845
- // reads it later, from where this one stopped.
846
- if (isUnavailable(error) || error instanceof IndexMemoryUnavailable) return false;
849
+ // The sound's own walk reads the rest later, from where this one stopped.
850
+ if (isUnavailable(error)) return "missing";
851
+ if (error instanceof IndexMemoryUnavailable) return "memory";
847
852
  throw error;
848
853
  }
849
854
  }
@@ -861,13 +866,21 @@ export class MatroskaContainer extends Container {
861
866
  *
862
867
  * @returns {Promise<{ records: PacketRecordsChain, startAt: number, reachesEnd: boolean }>}
863
868
  */
864
- async #readTrackPackets(layout, tracks, track, start, interval, isCurrent, read) {
869
+ async #readTrackPackets(layout, tracks, track, start, interval, isCurrent, reader, account = null, holdsUntil = null) {
865
870
  const id = track.trackNumber;
866
871
  const previous = this.#packetWalks.get(id) ?? Promise.resolve();
867
872
  let finished;
868
873
  const walk = previous.then(() => new Promise(resolve => { finished = resolve; }));
869
874
  this.#packetWalks.set(id, walk);
875
+ const waitedFrom = Date.now();
870
876
  await previous;
877
+ const read = account ? async (start, end) => {
878
+ const bytes = await reader(start, end);
879
+ account.reads += 1;
880
+ account.bytes += bytes.length;
881
+ return bytes;
882
+ } : reader;
883
+ if (account) account.lockMs += Date.now() - waitedFrom;
871
884
  try {
872
885
  const readings = this.#packetReadings.get(id) ?? [];
873
886
  this.#packetReadings.set(id, readings);
@@ -918,7 +931,7 @@ export class MatroskaContainer extends Container {
918
931
  const following = readings.find(reading => reading.startAt > current.startAt);
919
932
  const outcome = await walkMatroskaPackets({ readRange: read, fileSize: this.fileSize,
920
933
  portionBytes: this.portionBytes, layout, tracks, trackIds: [id], interval, packetMemory: this.packetMemory,
921
- state: current, until: following?.startAt ?? null, isCovered: covered, readsThrough, isCurrent });
934
+ state: current, until: following?.startAt ?? null, isCovered: covered, readsThrough, isCurrent, holdsUntil });
922
935
  if (outcome === "obsolete") return null;
923
936
  if (outcome !== "until") break;
924
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)]));