@torrent-tv/proxy 2.95.22 → 2.95.23

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.95.22",
3
+ "version": "2.95.23",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -1,7 +1,7 @@
1
1
  import { parseRange } from "../../utils/parse-range.js";
2
2
 
3
3
  /** FFmpeg reads only original byte ranges already owned by its live run. */
4
- export function handleEncodeInputGet(req, reply, { inputOf }) {
4
+ export function handleEncodeInputGet(req, reply, { inputOf, refused = () => {} }) {
5
5
  const address = req.raw?.socket?.remoteAddress;
6
6
  if (!["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(address)) return reply.code(403).send();
7
7
  const token = Number(req.params.token), fileIndex = Number(req.params.fileIndex);
@@ -26,7 +26,12 @@ export function handleEncodeInputGet(req, reply, { inputOf }) {
26
26
  // end of a content range). A cap of 1 MiB here ended every original-source
27
27
  // segment longer than that on 6.1 (torrent-tv/meta#151).
28
28
  const held = input.read(fileIndex, range.start, range.end, true);
29
- if (!held) return reply.code(503).send({ error: "Original input bytes are outside the admitted ranges." });
29
+ if (!held) {
30
+ // FFmpeg asked for a byte the run was not given: the container named too
31
+ // little. Said, because FFmpeg itself reports only a failed seek or read.
32
+ refused({ token, fileIndex, start: range.start, runTag: input.runTag ?? null });
33
+ return reply.code(503).send({ error: "Original input bytes are outside the admitted ranges." });
34
+ }
30
35
  return reply.code(206).header("Content-Length", String(held.bytes.length))
31
36
  .header("Content-Range", `bytes ${range.start}-${held.end}/${length}`).send(held.bytes);
32
37
  }
package/server.js CHANGED
@@ -1147,7 +1147,8 @@ export async function startProxyServer({
1147
1147
  })
1148
1148
  );
1149
1149
  app.get("/encode-input/:token/:fileIndex", (req, reply) =>
1150
- handleEncodeInputGet(req, reply, { inputOf: token => outputParts.encodeRuns.originalInputOf(token) })
1150
+ handleEncodeInputGet(req, reply, { inputOf: token => outputParts.encodeRuns.originalInputOf(token),
1151
+ refused: ({ fileIndex, start, runTag }) => logger.warn(`encode input: run ${runTag ?? "?"} asked for byte ${start} of file ${fileIndex}, which it was not given`) })
1151
1152
  );
1152
1153
  app.get("/stream", async (req, reply) =>
1153
1154
  handleStreamGet(req, reply, {
@@ -478,7 +478,9 @@ export class EncodeRun {
478
478
  if (line.length > 0) {
479
479
  this.lastError = line;
480
480
  const errorText = this.#stderrReadTail + String(chunk);
481
- this.#inputTruncated ||= /Stream ends prematurely|Input\/output error|Error during demuxing/i.test(errorText);
481
+ // A failed seek leaves the demuxer wherever it stood, so what follows is
482
+ // not the stretch asked for and is never published (torrent-tv/meta#151).
483
+ this.#inputTruncated ||= /Stream ends prematurely|Input\/output error|Error during demuxing|Seek failed/i.test(errorText);
482
484
  this.#stderrReadTail = errorText.slice(-512);
483
485
  this.logger.warn(`ffmpeg #${this.from}..#${this.to} of ${this.address}: ${line}`);
484
486
  }
@@ -142,8 +142,9 @@ export class AviContainer extends Container {
142
142
  const record = { dispose: () => this.#index?.dispose?.() };
143
143
  const allocation = this.packetMemory?.forRecord?.(record, "index") ?? this.packetMemory;
144
144
  const index = await readAviIndex({ read: this.readRange, fileSize: this.fileSize, movi, idx1, allocation,
145
- streams: tracks.map(track => ({ type: track.type, timeBase: track.timeBase, sampleSize: track.sampleSize,
146
- startTimeSeconds: track.startTimeSeconds, indexChunks: streams[track.trackNumber].indexChunks })) });
145
+ streams: tracks.map(track => ({ type: track.type, codecId: track.codecId, timeBase: track.timeBase,
146
+ sampleSize: track.sampleSize, blockAlign: track.blockAlign ?? 0, startUnits: track.startUnits ?? 0,
147
+ indexChunks: streams[track.trackNumber].indexChunks })) });
147
148
  this.#index = index;
148
149
  return index;
149
150
  }
@@ -173,8 +174,12 @@ export class AviContainer extends Container {
173
174
  if (!index) return { kind: "needs-index", reason: "source-has-no-avi-index" };
174
175
  const tracks = await this.readTracks();
175
176
  const { movi } = await this.#topLevel();
176
- const requested = tracks.filter(track => ["video", "audio"].includes(track.type) &&
177
- (!interval.trackIds?.length || interval.trackIds.includes(track.trackNumber)));
177
+ // Every audio and video stream, whichever the run outputs: FFmpeg's AVI
178
+ // demuxer seeks to the earliest position any stream's index gives for the
179
+ // keyframe time (`pos_min`, avi_read_seek) and reads every stream's packets
180
+ // in file order. Naming only the run's own tracks left a picture-only run
181
+ // asking for sound it was not given, and its seek failed (torrent-tv/meta#151).
182
+ const requested = tracks.filter(track => ["video", "audio"].includes(track.type));
178
183
  const picture = ContainerTrack.firstUsable(tracks, "video");
179
184
  const padded = ([start, end]) => [Math.max(0, start), Math.min(this.fileSize - 1, end + FFMPEG_INPUT_BUFFER_BYTES)];
180
185
  const ranges = [[0, movi.start + 3], ...index.indexRanges, ...index.firstPackets(),
@@ -230,6 +235,7 @@ export class AviContainer extends Container {
230
235
  track = new AudioTrack({ ...params, codecId: waveCodec(tag),
231
236
  channels: strf.readUInt16LE(2), samplingFrequency: strf.readUInt32LE(4),
232
237
  codecPrivateB64: strf.subarray(18).toString("base64") });
238
+ track.blockAlign = strf.readUInt16LE(12);
233
239
  track.matroskaCodecId = "A_MS/ACM";
234
240
  track.matroskaCodecPrivateB64 = (strf.length === 16
235
241
  ? Buffer.concat([strf, Buffer.alloc(2)]) : strf).toString("base64");
@@ -238,6 +244,7 @@ export class AviContainer extends Container {
238
244
  track.startTimeSeconds = track.timeBase === null ? null : strh.readUInt32LE(28) * track.timeBase;
239
245
  track.durationSeconds = track.timeBase === null ? null : strh.readUInt32LE(32) * track.timeBase;
240
246
  track.sampleSize = strh.readUInt32LE(44);
247
+ track.startUnits = strh.readUInt32LE(28);
241
248
  if (["h264", "hevc"].includes(track.codecId)) track.presentationCadenceSeconds = track.timeBase;
242
249
  return track;
243
250
  });
@@ -74,8 +74,9 @@ class StreamPackets {
74
74
  * @param {number} params.fileSize
75
75
  * @param {{ start: number, end: number }} params.movi - The first `movi` list (`start` at its type).
76
76
  * @param {{ start: number, end: number } | null} params.idx1 - The `idx1` chunk's payload, if any.
77
- * @param {Array<{ type: string, timeBase: number | null, sampleSize: number, startTimeSeconds: number | null,
78
- * indexChunks: Array<{ start: number, end: number }> }>} params.streams - In stream-number order.
77
+ * @param {Array<{ type: string, codecId: string, timeBase: number | null, sampleSize: number, blockAlign: number,
78
+ * startUnits: number, indexChunks: Array<{ start: number, end: number }> }>} params.streams - In stream-number order,
79
+ * as the stream headers declare them (`strh` sample size and start, `WAVEFORMATEX` block alignment).
79
80
  * @param {{ reserve?: (bytes: number) => boolean, release?: (bytes: number) => void }} params.allocation
80
81
  * @returns {Promise<AviIndex | null>} Null when the file states no index.
81
82
  */
@@ -104,8 +105,38 @@ function reserve(allocation, count) {
104
105
  return bytes;
105
106
  }
106
107
 
107
- function durationOf(stream, length) {
108
- return stream.timeBase * (stream.sampleSize > 0 ? length / stream.sampleSize : 1);
108
+ /**
109
+ * A stream's clock as FFmpeg's AVI demuxer keeps it (avidec.c n8.1). The clock
110
+ * counts units: bytes where the stream has a sample size, otherwise blocks of
111
+ * the audio's `nBlockAlign` rounded up per packet (`get_duration`), otherwise
112
+ * one per packet, an empty one included. A packet's time is its units, divided
113
+ * by the sample size where there is one, times the time base. FFmpeg's header
114
+ * rules decide the sample size and the block alignment it uses; they are taken
115
+ * over exactly, since a time that disagrees with FFmpeg's moves the bytes a
116
+ * seek asks for out of the ranges named (field 2026-10-08: a two-hour MP3 AVI
117
+ * failed to seek at 384 s).
118
+ */
119
+ function clockOf(stream) {
120
+ // The header's sample size is a signed 32-bit field to FFmpeg; the start is
121
+ // multiplied by it as read, before any correction, and a start beyond an hour
122
+ // is discarded.
123
+ const declared = (stream.sampleSize ?? 0) | 0;
124
+ const startUnits = (stream.startUnits ?? 0) * stream.timeBase > 3600 ? 0 : stream.startUnits ?? 0;
125
+ let sampleSize = stream.type === "video" || declared < 0 ? 0 : declared;
126
+ let blockAlign = 0;
127
+ if (stream.type === "audio") {
128
+ blockAlign = stream.blockAlign > 0 ? stream.blockAlign : 0;
129
+ if (sampleSize && blockAlign && sampleSize !== blockAlign) sampleSize = blockAlign;
130
+ if (["aac", "flac", "mp2"].includes(stream.codecId) && blockAlign <= 4) blockAlign = 0;
131
+ if ((stream.codecId === "aac" && (blockAlign === 1024 || blockAlign === 4096) && sampleSize === blockAlign) ||
132
+ (stream.codecId === "mp3" && blockAlign === 1152 && sampleSize === 1152)) sampleSize = 0;
133
+ }
134
+ return {
135
+ sampleSize,
136
+ units: startUnits * Math.max(1, declared),
137
+ advance(length) { this.units += sampleSize ? length : blockAlign ? Math.ceil(length / blockAlign) : 1; },
138
+ seconds() { return stream.timeBase * (sampleSize ? Math.floor(this.units / sampleSize) : this.units); }
139
+ };
109
140
  }
110
141
 
111
142
  async function readIdx1({ read, fileSize, movi, idx1, streams, allocation }) {
@@ -139,7 +170,7 @@ async function readIdx1({ read, fileSize, movi, idx1, streams, allocation }) {
139
170
  const held = reserve(allocation, counts.reduce((sum, count) => sum + count, 0));
140
171
  try {
141
172
  const packets = counts.map(count => new StreamPackets(count));
142
- const clocks = streams.map(stream => stream.startTimeSeconds ?? 0);
173
+ const clocks = streams.map(clockOf);
143
174
  for await (const { bytes } of blocks(read, idx1.start, idx1.end, IDX1_ENTRY_BYTES)) {
144
175
  for (let offset = 0; offset < bytes.length; offset += IDX1_ENTRY_BYTES) {
145
176
  const chunkId = bytes.toString("ascii", offset, offset + 4);
@@ -151,8 +182,8 @@ async function readIdx1({ read, fileSize, movi, idx1, streams, allocation }) {
151
182
  // FFmpeg takes the index flag alone, and every entry as a keyframe
152
183
  // when no entry carries it (avi_read_idx1).
153
184
  const keyframe = !anyKeyframe || !!(bytes.readUInt32LE(offset + 4) & AVIIF_KEYFRAME);
154
- packets[id].push(start, length, clocks[id], keyframe);
155
- clocks[id] += durationOf(streams[id], length);
185
+ packets[id].push(start, length, clocks[id].seconds(), keyframe);
186
+ clocks[id].advance(length);
156
187
  }
157
188
  }
158
189
  return new AviIndex({ packets, indexRanges: [[idx1.start - 8, fileSize - 1]], held, allocation });
@@ -202,9 +233,8 @@ async function readOpenDml({ read, fileSize, streams, allocation }) {
202
233
  const held = reserve(allocation, counts.reduce((sum, count) => sum + count, 0));
203
234
  try {
204
235
  const packets = counts.map(count => new StreamPackets(count));
205
- const clocks = streams.map(stream => stream.startTimeSeconds ?? 0);
236
+ const clocks = streams.map(clockOf);
206
237
  for (const standard of parsed) {
207
- const stream = streams[standard.id];
208
238
  const tableStart = standard.start + 32;
209
239
  const tableEnd = tableStart + standard.count * standard.entryBytes;
210
240
  for await (const { bytes } of blocks(read, tableStart, tableEnd, standard.entryBytes)) {
@@ -212,8 +242,8 @@ async function readOpenDml({ read, fileSize, streams, allocation }) {
212
242
  const start = standard.base + bytes.readUInt32LE(offset);
213
243
  const encoded = bytes.readUInt32LE(offset + 4), length = encoded & 0x7fffffff;
214
244
  if (length && (start < 8 || start + length > fileSize)) throw new Error("AVI OpenDML packet exceeds the file.");
215
- packets[standard.id].push(start, length, clocks[standard.id], !(encoded & 0x80000000));
216
- clocks[standard.id] += durationOf(stream, length);
245
+ packets[standard.id].push(start, length, clocks[standard.id].seconds(), !(encoded & 0x80000000));
246
+ clocks[standard.id].advance(length);
217
247
  }
218
248
  }
219
249
  }