@torrent-tv/proxy 2.96.6 → 2.96.8

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.96.6",
3
+ "version": "2.96.8",
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
@@ -495,9 +495,17 @@ export async function startProxyServer({
495
495
  const known = segmentSourceRanges.get(rangesKey);
496
496
  if (known) { convert(known); continue; }
497
497
  if (direct) {
498
+ // Asked per zone too: a soundtrack of an AVI is read from its own
499
+ // packets, which the same index states (torrent-tv/meta#166).
498
500
  let value = null;
499
- try { value = await direct.readSourceRanges(interval); }
500
- catch { value = null; }
501
+ try {
502
+ if (await direct.supportsOriginalSourceRanges(interval) === true) value = await direct.readSourceRanges(interval);
503
+ else {
504
+ const index = await direct.readPacketIndex(interval);
505
+ await index.prepareAudioDependencies?.(interval, direct.readRange);
506
+ value = new SegmentInputs({ index, tracks: wanted }).forInterval({ ...interval, mode: track => modes.get(track) });
507
+ }
508
+ } catch { value = null; }
501
509
  if (!map.isCurrent()) return [];
502
510
  if (value?.kind === "result") {
503
511
  segmentSourceRanges.set(rangesKey, value.ranges);
@@ -198,7 +198,7 @@ export class ContainerOrchestrator {
198
198
  value = await (statement === "cover" ? container.readCover(edgesOf(params)) : container.readWorkTags(edgesOf(params)));
199
199
  if (value === null) return { kind: "terminal", reason: statement === "cover" ? "no-cover" : "format-states-nothing", requestId };
200
200
  } else if (statement === "source-navigation") {
201
- value = await container.supportsOriginalSourceRanges?.() === true;
201
+ value = await container.supportsOriginalSourceRanges?.(params.packetInterval ?? null) === true;
202
202
  } else if (statement === "source-ranges") {
203
203
  if (typeof container.readSourceRanges !== "function") return { kind: "terminal", reason: "source-range-index-not-supported", requestId };
204
204
  value = await container.readSourceRanges(params.packetInterval);
@@ -32,6 +32,8 @@ export class AviContainer extends Container {
32
32
  #headers = null;
33
33
  #tracks = null;
34
34
  #packets = null;
35
+ /** The sound packets of an indexed file while they are being stated, so two asks build them once. */
36
+ #soundPacketsReading = null;
35
37
  #scan = null;
36
38
  #declarations = null;
37
39
  #workTags = null;
@@ -150,10 +152,18 @@ export class AviContainer extends Container {
150
152
  return index;
151
153
  }
152
154
 
153
- async supportsOriginalSourceRanges() {
155
+ /**
156
+ * Whether `interval` is read from the original file. Only a picture needs
157
+ * it: an AVI states no presentation time, so FFmpeg's own reading is what
158
+ * keeps its frames in order. Sound alone is read from its own packets, whose
159
+ * time the index states (torrent-tv/meta#166); asked of the file as a whole,
160
+ * the answer is the picture's.
161
+ */
162
+ async supportsOriginalSourceRanges(interval = null) {
154
163
  const index = await this.#readIndex();
155
164
  if (!index) return false;
156
165
  const picture = ContainerTrack.firstUsable(await this.readTracks(), "video");
166
+ if (picture && interval?.trackIds?.length && !interval.trackIds.includes(picture.trackNumber)) return false;
157
167
  return !picture || index.keyframeTimes(picture.trackNumber).length > 0;
158
168
  }
159
169
 
@@ -312,9 +322,19 @@ export class AviContainer extends Container {
312
322
  async readPacketIndex(interval) {
313
323
  if (this.#packets) return this.#packets;
314
324
  if (this.#scan) return this.#readUnindexed(interval);
315
- // A file with an index is read by FFmpeg from the original bytes; packets
316
- // are reassembled only where no index states them (torrent-tv/meta#151).
317
- if (await this.#readIndex()) throw new Error("AVI with an index is read from the original file, not from reassembled packets.");
325
+ // A picture of a file with an index is read by FFmpeg from the original
326
+ // bytes (torrent-tv/meta#151). Its SOUND is not: the index states every
327
+ // sound packet's place and FFmpeg's time for it, and a soundtrack read from
328
+ // the original file has to carry the picture's bytes too, because sound and
329
+ // picture are interleaved — field 2026-10-10, a two-hour film's soundtrack
330
+ // copied 2.3 GB for 0.19 GB of its own (torrent-tv/meta#166).
331
+ const indexed = await this.#readIndex();
332
+ if (indexed) {
333
+ this.#soundPacketsReading ??= this.#soundPacketsOf(indexed).then(
334
+ packets => (this.#packets = packets),
335
+ error => { this.#soundPacketsReading = null; throw error; });
336
+ return this.#soundPacketsReading;
337
+ }
318
338
  const tracks = await this.readTracks();
319
339
  const { streams } = await this.#readHeaders();
320
340
  const index = new PacketIndex({ packetMemory: this.packetMemory, deferMemory: true });
@@ -333,6 +353,53 @@ export class AviContainer extends Container {
333
353
  return this.#readUnindexed(interval);
334
354
  }
335
355
 
356
+ /**
357
+ * The sound packets an AVI index states, on FFmpeg's clock: the time the
358
+ * index gives each packet is the one FFmpeg's demuxer gives it, and a packet
359
+ * lasts until the next one of its stream, the last until the stream's clock
360
+ * after it. Only sound is declared; a picture of an indexed AVI states no
361
+ * presentation time and is read from the original file.
362
+ *
363
+ * @param {{ packets: Array<{ count: number, starts: Float64Array, lengths: Uint32Array, times: Float64Array, end: number | null }> }} index
364
+ * @returns {Promise<PacketIndex>}
365
+ */
366
+ async #soundPacketsOf(index) {
367
+ const tracks = await this.readTracks();
368
+ const { streams } = await this.#readHeaders();
369
+ const packets = new PacketIndex({ packetMemory: this.packetMemory });
370
+ try {
371
+ for (const track of tracks.filter(one => one.type === "audio")) {
372
+ const stream = index.packets[track.trackNumber];
373
+ packets.declareTrack(track.trackNumber, { type: "audio", codecId: track.codecId,
374
+ codecRanges: streams[track.trackNumber].codecRanges });
375
+ // The index says how many packets there are, so the memory they take is
376
+ // asked for at once. Asked for block by block, a refusal named one
377
+ // block, the table was discarded and built again for the next one:
378
+ // field 2026-10-10, 3485 refusals of 64 KB for a two-hour film's
379
+ // soundtrack, and no soundtrack encoder in three minutes.
380
+ let count = 0;
381
+ for (let at = 0; at < stream.count; at++) if (stream.lengths[at]) count++;
382
+ packets.reservePackets(track.trackNumber, count);
383
+ let previous = -1;
384
+ const append = (end) => {
385
+ packets.append(track.trackNumber, { pts: stream.times[previous], duration: Math.max(0, end - stream.times[previous]),
386
+ keyframe: true, ranges: [[stream.starts[previous], stream.starts[previous] + stream.lengths[previous] - 1]] });
387
+ };
388
+ for (let at = 0; at < stream.count; at++) {
389
+ if (!stream.lengths[at]) continue;
390
+ if (previous >= 0) append(stream.times[at]);
391
+ previous = at;
392
+ }
393
+ if (previous >= 0) append(stream.end ?? stream.times[previous]);
394
+ packets.complete(track.trackNumber);
395
+ }
396
+ } catch (error) {
397
+ packets.dispose();
398
+ throw error;
399
+ }
400
+ return packets;
401
+ }
402
+
336
403
  async #readUnindexed(interval) {
337
404
  const state = this.#scan;
338
405
  state.index.flushPending();
@@ -44,6 +44,8 @@ class StreamPackets {
44
44
  this.lengths = new Uint32Array(count);
45
45
  this.times = new Float64Array(count);
46
46
  this.keyframes = new Uint8Array(count);
47
+ /** Where the stream's clock stands after its last packet: the last packet's end. */
48
+ this.end = null;
47
49
  }
48
50
 
49
51
  push(start, length, time, keyframe) {
@@ -186,6 +188,7 @@ async function readIdx1({ read, fileSize, movi, idx1, streams, allocation }) {
186
188
  clocks[id].advance(length);
187
189
  }
188
190
  }
191
+ packets.forEach((stream, id) => { stream.end = clocks[id].seconds(); });
189
192
  return new AviIndex({ packets, indexRanges: [[idx1.start - 8, fileSize - 1]], held, allocation });
190
193
  } catch (error) {
191
194
  allocation?.release?.(held);
@@ -247,6 +250,7 @@ async function readOpenDml({ read, fileSize, streams, allocation }) {
247
250
  }
248
251
  }
249
252
  }
253
+ packets.forEach((stream, id) => { stream.end = clocks[id].seconds(); });
250
254
  return new AviIndex({ packets, indexRanges: parsed.map(standard => [standard.start, standard.end - 1]), held, allocation });
251
255
  } catch (error) {
252
256
  allocation?.release?.(held);