@torrent-tv/proxy 2.105.1 → 2.105.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.105.1",
3
+ "version": "2.105.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": {
@@ -18,6 +18,7 @@ import { readAviIndex } from "./avi-index.js";
18
18
  import { isUnavailable } from "./unavailable.js";
19
19
  import { Mpeg4PictureTiming } from "./mpeg4-picture-timing.js";
20
20
  import { MpegElementaryIndex } from "./mpeg-elementary-index.js";
21
+ import { ac3Frame } from "./ac3-frame.js";
21
22
  import { RetainedReads } from "./RetainedReads.js";
22
23
  import { OutsideReadableEdges, edgeReader, emptyWorkTags, text, textList, yearOf } from "./work-tags.js";
23
24
 
@@ -208,8 +209,14 @@ export class AviContainer extends Container {
208
209
  * ones, and the swarm was asked for the picture's next piece only after it
209
210
  * (torrent-tv/meta#178).
210
211
  */
211
- packetTableWouldWait() {
212
+ packetTableWouldWait(interval = null) {
212
213
  if (!this.#index || this.#packets || this.#scan) return false;
214
+ if (interval?.trackIds?.length) {
215
+ const tracks = this.#tracks ?? [];
216
+ const selected = tracks.filter(track => interval.trackIds.includes(track.trackNumber));
217
+ if (selected.length === interval.trackIds.length && selected.length === 1 && selected[0].type === "audio" &&
218
+ selected[0].codecId === "ac3") return false;
219
+ }
213
220
  return this.#soundPacketsRefused === null || this.packetMemory?.fits?.(this.#soundPacketsRefused) !== false;
214
221
  }
215
222
 
@@ -414,7 +421,16 @@ export class AviContainer extends Container {
414
421
  }
415
422
 
416
423
  async readPacketIndex(interval) {
417
- if (this.#packets) return this.#packets;
424
+ if (this.#packets) {
425
+ if (this.#index && interval?.trackIds?.length) {
426
+ const tracks = await this.readTracks();
427
+ const selected = tracks.filter(track => interval.trackIds.includes(track.trackNumber));
428
+ if (selected.length === 1 && selected[0].type === "audio" && selected[0].codecId === "ac3") {
429
+ return this.#indexedAc3PacketsOf(this.#index, selected[0], interval);
430
+ }
431
+ }
432
+ return this.#packets;
433
+ }
418
434
  if (this.#scan) return this.#readUnindexed(interval);
419
435
  // A picture of a file with an index is read by FFmpeg from the original
420
436
  // bytes (torrent-tv/meta#151). Its SOUND is not: the index states every
@@ -424,6 +440,12 @@ export class AviContainer extends Container {
424
440
  // copied 2.3 GB for 0.19 GB of its own (torrent-tv/meta#166).
425
441
  const indexed = await this.#readIndex();
426
442
  if (indexed) {
443
+ const tracks = await this.readTracks();
444
+ const selected = interval?.trackIds?.length
445
+ ? tracks.filter(track => interval.trackIds.includes(track.trackNumber)) : [];
446
+ if (selected.length === 1 && selected[0].type === "audio" && selected[0].codecId === "ac3") {
447
+ return this.#indexedAc3PacketsOf(indexed, selected[0], interval);
448
+ }
427
449
  // A refusal of memory for the table is kept for as long as the memory's
428
450
  // owner says the same reservation still would not fit. Not kept, the
429
451
  // next reader walked the index, counted the packets and was refused
@@ -463,6 +485,68 @@ export class AviContainer extends Container {
463
485
  return this.#readUnindexed(interval);
464
486
  }
465
487
 
488
+ /** Parse only the indexed AC-3 blocks needed for one audio interval. */
489
+ async #indexedAc3PacketsOf(index, track, interval) {
490
+ const stream = index.packets[track.trackNumber];
491
+ if (!stream?.count) throw new Error("Indexed AVI AC-3 track has no packets.");
492
+ const read = new Map();
493
+ const bytesAt = async at => {
494
+ if (read.has(at)) return read.get(at);
495
+ const length = stream.lengths[at];
496
+ const bytes = length ? await this.readRange(stream.starts[at], stream.starts[at] + length - 1) : Buffer.alloc(0);
497
+ read.set(at, bytes);
498
+ return bytes;
499
+ };
500
+ const chainAt = async start => {
501
+ let bytes = Buffer.alloc(0), cursor = 0, at = start, frames = 0;
502
+ while (frames < 3 && at < stream.count && bytes.length < 32 * 1024) {
503
+ bytes = Buffer.concat([bytes, await bytesAt(at++)]);
504
+ while (bytes.length - cursor >= 7 && frames < 3) {
505
+ if (bytes[cursor] !== 0x0b || bytes[cursor + 1] !== 0x77) return false;
506
+ let frame;
507
+ try { frame = ac3Frame(bytes.subarray(cursor)); }
508
+ catch { return false; }
509
+ if (bytes.length - cursor < frame.size) break;
510
+ cursor += frame.size;
511
+ frames++;
512
+ }
513
+ }
514
+ return frames === 3;
515
+ };
516
+
517
+ const anchor = stream.atOrBefore(interval.from);
518
+ let first = -1, scannedBytes = 0;
519
+ for (let at = anchor; at >= 0 && scannedBytes < 128 * 1024; at--) {
520
+ const bytes = await bytesAt(at);
521
+ scannedBytes += bytes.length;
522
+ if (bytes.length >= 7 && bytes[0] === 0x0b && bytes[1] === 0x77 && await chainAt(at)) {
523
+ first = at;
524
+ break;
525
+ }
526
+ }
527
+ if (first < 0) throw new Error("Indexed AVI AC-3 has no complete frame chain near the requested interval.");
528
+
529
+ const elementary = new MpegElementaryIndex([track], { packetMemory: this.packetMemory });
530
+ try {
531
+ for (let at = first; at < stream.count; at++) {
532
+ const bytes = await bytesAt(at);
533
+ if (bytes.length) elementary.push(track.trackNumber, bytes, stream.starts[at], at === first ? { pts: stream.times[first] } : {});
534
+ const input = elementary.index.inputFor({ trackId: track.trackNumber, from: interval.from, to: interval.to,
535
+ mode: interval.modes?.[track.trackNumber] ?? "transcode" });
536
+ if (input.kind === "result") {
537
+ elementary.index.disposeAfterUse = true;
538
+ return elementary.index;
539
+ }
540
+ }
541
+ elementary.complete();
542
+ elementary.index.disposeAfterUse = true;
543
+ return elementary.index;
544
+ } catch (error) {
545
+ elementary.index.dispose();
546
+ throw error;
547
+ }
548
+ }
549
+
466
550
  /**
467
551
  * The sound packets an AVI index states, on FFmpeg's clock: the time the
468
552
  * index gives each packet is the one FFmpeg's demuxer gives it, and a packet
@@ -33,11 +33,15 @@ export async function directSegmentInput(container, { interval, tracks, mode },
33
33
  const walked = await container.packetWalkRanges?.(interval);
34
34
  if (walked) return walked;
35
35
  // The table is begun, or joined, and not waited for.
36
- if (container.packetTableWouldWait?.() === true) {
36
+ if (container.packetTableWouldWait?.(interval) === true) {
37
37
  whenBuilt(container.readPacketIndex(interval).then(() => undefined, () => undefined));
38
38
  return { kind: "pending" };
39
39
  }
40
40
  const index = await container.readPacketIndex(interval);
41
- await index.prepareAudioDependencies?.(interval, container.readRange);
42
- return new SegmentInputs({ index, tracks }).forInterval({ ...interval, mode });
41
+ try {
42
+ await index.prepareAudioDependencies?.(interval, container.readRange);
43
+ return new SegmentInputs({ index, tracks }).forInterval({ ...interval, mode });
44
+ } finally {
45
+ if (index.disposeAfterUse === true) index.dispose();
46
+ }
43
47
  }
@@ -575,6 +575,7 @@ export class ViewerRequests {
575
575
  // still answered by the output it was made against — see the two deadlines
576
576
  // in `Assignments.js`.
577
577
  viewer.assignments.statedGeneration(generation);
578
+ this.#beginSeekWait(named, consumerId, generation);
578
579
  this.#host.outputs.touch(named);
579
580
  // EVERY REQUEST THIS VIEWER HAS OUTSTANDING WAS MADE FOR WHERE THEY WERE,
580
581
  // and hls.js keeps one fragment load outstanding per track: a request held
@@ -601,6 +602,40 @@ export class ViewerRequests {
601
602
  return true;
602
603
  }
603
604
 
605
+ #beginSeekWait(output, consumerId, generation) {
606
+ const file = output?.file;
607
+ if (!Number.isSafeInteger(generation) || !file || typeof file.sourceKey !== "string" ||
608
+ !Number.isSafeInteger(file.fileIndex) || file.fileIndex < 0) return;
609
+ const waitId = `${file.sourceKey}:${file.fileIndex}:${generation}`;
610
+ const key = `${consumerId}:${waitId}`;
611
+ const fileName = file.relativePath ?? file.name ?? "";
612
+ const extension = typeof fileName === "string" ? fileName.split(".").at(-1)?.toLowerCase() ?? "" : "";
613
+ const viewer = this.#host.viewers.get(consumerId);
614
+ const link = linkMeasurementOf(viewer);
615
+ const features = {
616
+ extension,
617
+ fileLength: Number.isFinite(file.length) && file.length > 0 ? file.length : null,
618
+ downloadSpeed: null,
619
+ linkMbps: link.mbps,
620
+ linkSampleAgeMs: link.ageMs,
621
+ preparedOutputBytes: this.#preparedOutputBytes(file.sourceKey, file.fileIndex),
622
+ fileAvailable: null,
623
+ headerRemaining: null,
624
+ peers: null,
625
+ videoOperation: null,
626
+ audioOperation: null,
627
+ videoSpeedX: null,
628
+ audioSpeedX: null
629
+ };
630
+ // A seek is a new waiting action even when a caller repeats the same
631
+ // generation. Start its clock at the explicit seek request, before the
632
+ // next readiness poll can return the first current-work estimate.
633
+ this.#earlyWaits.delete(key);
634
+ const context = this.#setEarlyWait({ consumerId, generation, sourceKey: file.sourceKey,
635
+ fileIndex: file.fileIndex, waitId, features, revision: 0, current: null, waitElapsedMs: 0 });
636
+ this.#recordEarlyWaitSnapshot(context, features);
637
+ }
638
+
604
639
  /**
605
640
  * Whether this viewer still takes a request made in the generation it states.
606
641
  *