@torrent-tv/proxy 2.99.13 → 2.99.15

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.99.13",
3
+ "version": "2.99.15",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -682,6 +682,18 @@ export class MatroskaContainer extends Container {
682
682
 
683
683
  async readMappedSourceRanges(interval) { return this.readSourceRanges(interval); }
684
684
 
685
+ /**
686
+ * The bytes a packet table of `interval` is read from: the clusters the
687
+ * Cues name for it, as for the original file. Every block header in them is
688
+ * read, and the headers lie throughout. Null without Cues, where the walk
689
+ * itself finds what it needs.
690
+ */
691
+ async packetWalkRanges(interval) {
692
+ if (!await this.supportsOriginalSourceRanges()) return null;
693
+ const ranges = await this.readSourceRanges(interval);
694
+ return ranges.kind === "result" ? ranges : null;
695
+ }
696
+
685
697
  async readSourceRanges(interval) {
686
698
  if (!Number.isFinite(interval?.from) || !Number.isFinite(interval?.to) || interval.to <= interval.from) {
687
699
  throw new TypeError("Source ranges require a finite increasing interval.");
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import { readVint } from "./ebml-reader.js";
22
+ import { isUnavailable } from "./unavailable.js";
22
23
 
23
24
  /** An element id is at most four bytes and a size at most eight (RFC 8794 §4). */
24
25
  export const MAX_HEADER_BYTES = 12;
@@ -64,6 +65,10 @@ export class ElementReader {
64
65
  /** @type {number} */
65
66
  #windowAt = 0;
66
67
 
68
+ /** Below this position a portion was found missing: read exactly there. */
69
+ #exactBefore = -1;
70
+ #narrow;
71
+
67
72
  /**
68
73
  * @param {object} params
69
74
  * @param {(start: number, end: number) => Promise<Buffer>} params.read - Answers
@@ -79,8 +84,13 @@ export class ElementReader {
79
84
  * last byte that can be read from `start` in one go. A read of what is
80
85
  * downloaded must not run a prefetch past the end of the downloaded run,
81
86
  * or a cluster that is whole would fail for the bytes after it.
87
+ * @param {boolean} [params.narrowWhenMissing] - When a prefetched portion is
88
+ * not all downloaded, read only the bytes asked for, and read exactly up to
89
+ * the end of that portion: what is not downloaded yet is then only what is
90
+ * needed, and the bytes before it are read.
82
91
  */
83
- constructor({ read, fileSize, portionBytes = Number.POSITIVE_INFINITY, prefetch = false, readableUntil }) {
92
+ constructor({ read, fileSize, portionBytes = Number.POSITIVE_INFINITY, prefetch = false, readableUntil, narrowWhenMissing = false }) {
93
+ this.#narrow = narrowWhenMissing;
84
94
  this.#read = read;
85
95
  this.#fileSize = fileSize;
86
96
  this.#readableUntil = typeof readableUntil === "function" ? readableUntil : () => fileSize;
@@ -110,8 +120,15 @@ export class ElementReader {
110
120
  return window.subarray(start - this.#windowAt, end - this.#windowAt);
111
121
  }
112
122
  const reach = Math.min(this.#fileSize, Math.max(end, this.#readableUntil(start)));
113
- const span = this.#prefetch ? Math.max(end - start, Math.min(this.#portion, reach - start)) : end - start;
114
- const got = await this.#read(start, start + span - 1);
123
+ const span = this.#prefetch && start >= this.#exactBefore ? Math.max(end - start, Math.min(this.#portion, reach - start)) : end - start;
124
+ let got;
125
+ try {
126
+ got = await this.#read(start, start + span - 1);
127
+ } catch (error) {
128
+ if (!this.#narrow || span === end - start || !isUnavailable(error)) throw error;
129
+ this.#exactBefore = start + span;
130
+ got = await this.#read(start, end - 1);
131
+ }
115
132
  this.#window = got;
116
133
  this.#windowAt = start;
117
134
  return got.subarray(0, end - start);
@@ -43,7 +43,16 @@ function recordedTracks(tracks, trackIds) {
43
43
  * @returns {Promise<"covered" | "until" | "end">}
44
44
  */
45
45
  export async function walkMatroskaPackets({ readRange, fileSize, portionBytes, layout, tracks, trackIds, interval, packetMemory, state, until = null, isCovered = null }) {
46
- const reader = new ElementReader({ read: readRange, fileSize, portionBytes });
46
+ // INSIDE A CLUSTER EVERY BLOCK HEADER IS READ, so its bytes are read a
47
+ // portion at a time up to the cluster's end, and never past it. Read a
48
+ // header at a time, each a read of a few bytes from the torrent thread, the
49
+ // soundtrack's second piece of a copied 1080p film took 29.6 s on Home
50
+ // Assistant while that thread was downloading (r52, torrent-tv/meta#178).
51
+ // Between clusters only their headers are read; where a portion is not all
52
+ // downloaded, only the bytes a header needs are read and asked for.
53
+ let readableUntil = null;
54
+ const reader = new ElementReader({ read: readRange, fileSize, portionBytes, prefetch: true, narrowWhenMissing: true,
55
+ readableUntil: start => readableUntil ?? start });
47
56
  const declared = recordedTracks(tracks, trackIds);
48
57
  if (!state.packets) {
49
58
  state.packets = new Map([...declared.keys()].map(id => [id, new PacketRecords(packetMemory)]));
@@ -64,6 +73,7 @@ export async function walkMatroskaPackets({ readRange, fileSize, portionBytes, l
64
73
  }
65
74
  const limit = element.end ?? layout.segmentEnd;
66
75
  if (!state.cluster) state.cluster = { element, cursor: element.dataOffset, clusterTicks: null };
76
+ readableUntil = limit;
67
77
  let { cursor, clusterTicks } = state.cluster;
68
78
  while (cursor < limit) {
69
79
  const child = await validHeader(reader, cursor, limit);
@@ -120,6 +130,7 @@ export async function walkMatroskaPackets({ readRange, fileSize, portionBytes, l
120
130
  at = element.end ?? cursor;
121
131
  state.at = at;
122
132
  state.cluster = null;
133
+ readableUntil = null;
123
134
  }
124
135
  if (at < layout.segmentEnd) return "until";
125
136
  state.reachedEnd = true;
@@ -14,7 +14,8 @@
14
14
  import { SegmentInputs } from "./SegmentInputs.js";
15
15
 
16
16
  /**
17
- * @param {object} container - Has `supportsOriginalSourceRanges`, `readSourceRanges` and `readPacketIndex`.
17
+ * @param {object} container - Has `supportsOriginalSourceRanges`, `readSourceRanges` and `readPacketIndex`;
18
+ * `packetWalkRanges` where a packet table is read from bytes the container can name in advance.
18
19
  * @param {{ interval: object, tracks: object[], mode: (track: object) => string }} segment
19
20
  * @param {(built: Promise<void>) => void} whenBuilt - Given a promise that settles when a table
20
21
  * still being built is built or refused, when the answer is "pending".
@@ -23,6 +24,14 @@ import { SegmentInputs } from "./SegmentInputs.js";
23
24
  */
24
25
  export async function directSegmentInput(container, { interval, tracks, mode }, whenBuilt) {
25
26
  if (await container.supportsOriginalSourceRanges(interval) === true) return container.readSourceRanges(interval);
27
+ // A soundtrack read from its own packets is prepared by walking the
28
+ // clusters that hold them, so those are the bytes it waits for: the map and
29
+ // the start forecast are told them, as for the picture. Told only the
30
+ // packets once the walk had found them, a far piece's bytes were unknown
31
+ // until downloaded, the forecast never stood, and a copied 1080p film did not
32
+ // start in 275 s (r53, torrent-tv/meta#178).
33
+ const walked = await container.packetWalkRanges?.(interval);
34
+ if (walked) return walked;
26
35
  // The table is begun, or joined, and not waited for.
27
36
  if (container.packetTableWouldWait?.() === true) {
28
37
  whenBuilt(container.readPacketIndex(interval).then(() => undefined, () => undefined));