@torrent-tv/proxy 2.105.2 → 2.105.4

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.2",
3
+ "version": "2.105.4",
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
  }
@@ -147,7 +147,8 @@ export class ViewerRequests {
147
147
  const seconds = earlyWaitRemainingSeconds(totalSeconds, elapsedMs);
148
148
  if (!Number.isFinite(seconds) || seconds < 0) return null;
149
149
  this.#logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, historical,
150
- firstRequiredSegmentsSeconds: context.activeFirstRequiredSegmentsSeconds });
150
+ firstRequiredSegmentsSeconds: context.activeFirstRequiredSegmentsSeconds,
151
+ firstRequiredStages: context.activeFirstRequiredStages });
151
152
  return { version: 1, waitId, revision: context.revision, remainingSeconds: seconds, ageMs: 0 };
152
153
  }
153
154
 
@@ -192,13 +193,19 @@ export class ViewerRequests {
192
193
  const elapsedMs = waitElapsedSince(context, waitElapsedMs);
193
194
  const measuredWorkSeconds = processing.firstRequiredSegmentsSeconds;
194
195
  const hasActiveEstimate = Number.isFinite(measuredWorkSeconds) && measuredWorkSeconds >= 0;
196
+ const stagesChanged = Array.isArray(processing.firstRequiredStages) &&
197
+ JSON.stringify(processing.firstRequiredStages) !== JSON.stringify(context.activeFirstRequiredStages);
195
198
  const activeTotalSeconds = hasActiveEstimate
196
199
  ? elapsedMs / 1000 + measuredWorkSeconds
197
200
  : context.activeTotalSeconds;
198
- const activeEstimateChanged = hasActiveEstimate && activeTotalSeconds !== context.activeTotalSeconds;
201
+ const activeEstimateChanged = hasActiveEstimate &&
202
+ (activeTotalSeconds !== context.activeTotalSeconds || stagesChanged);
199
203
  if (hasActiveEstimate) {
200
204
  context.activeTotalSeconds = activeTotalSeconds;
201
205
  context.activeFirstRequiredSegmentsSeconds = measuredWorkSeconds;
206
+ if (Array.isArray(processing.firstRequiredStages)) {
207
+ context.activeFirstRequiredStages = processing.firstRequiredStages;
208
+ }
202
209
  }
203
210
  const changed = featuresChanged || activeEstimateChanged;
204
211
  if (sameWait && changed) context.revision += 1;
@@ -213,7 +220,8 @@ export class ViewerRequests {
213
220
  const seconds = earlyWaitRemainingSeconds(totalSeconds, elapsedMs);
214
221
  if (!Number.isFinite(seconds) || seconds < 0) return null;
215
222
  this.#logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, historical,
216
- firstRequiredSegmentsSeconds: hasActiveEstimate ? measuredWorkSeconds : context.activeFirstRequiredSegmentsSeconds });
223
+ firstRequiredSegmentsSeconds: hasActiveEstimate ? measuredWorkSeconds : context.activeFirstRequiredSegmentsSeconds,
224
+ firstRequiredStages: context.activeFirstRequiredStages });
217
225
  return { version: 1, waitId, revision: context.revision, remainingSeconds: seconds, ageMs: 0 };
218
226
  }
219
227
 
@@ -222,11 +230,12 @@ export class ViewerRequests {
222
230
  return Number.isFinite(estimate) ? { seconds: estimate, stagesMs: null, observations: null } : estimate;
223
231
  }
224
232
 
225
- #logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, historical, firstRequiredSegmentsSeconds }) {
233
+ #logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, historical, firstRequiredSegmentsSeconds,
234
+ firstRequiredStages }) {
226
235
  if (context.loggedForecastRevision === context.revision) return;
227
236
  const basis = Number.isFinite(firstRequiredSegmentsSeconds) ? "first-required" : "historical-stages";
228
237
  const estimate = basis === "first-required"
229
- ? { firstRequiredSegmentsSeconds }
238
+ ? { firstRequiredSegmentsSeconds, ...(Array.isArray(firstRequiredStages) ? { firstRequiredStages } : {}) }
230
239
  : { stagesMs: historical?.stagesMs ?? null, observations: historical?.observations ?? null };
231
240
  logger.info(`early wait forecast ${JSON.stringify({ waitId: context.waitId, consumer: context.consumerId,
232
241
  generation: context.generation, features: context.features, elapsedMs, revision: context.revision,
@@ -1062,7 +1071,8 @@ export class ViewerRequests {
1062
1071
  audioOperation: audioOperation?.operation,
1063
1072
  videoSpeedX: videoOperation?.speed,
1064
1073
  audioSpeedX: audioOperation?.speed,
1065
- firstRequiredSegmentsSeconds: forecast.firstRequiredSegmentsSeconds
1074
+ firstRequiredSegmentsSeconds: forecast.firstRequiredSegmentsSeconds,
1075
+ firstRequiredStages: forecast.firstRequiredStages
1066
1076
  } });
1067
1077
  if (waitEstimate) forecast.waitEstimate = waitEstimate;
1068
1078
  }
@@ -1105,6 +1115,7 @@ export class ViewerRequests {
1105
1115
  const holding = forecast.holdingBy;
1106
1116
  this.#host.noteViewerWaiting?.(consumerId || session.id,
1107
1117
  holding?.kind === "production" ? { address: holding.track, index: holding.segment } : null);
1118
+ delete forecast.firstRequiredStages;
1108
1119
  return forecast;
1109
1120
  }
1110
1121
 
@@ -463,23 +463,28 @@ export function predictPlaybackReadiness(input = {}) {
463
463
  // Production accepts a complete held segment input. It cannot overlap its
464
464
  // own source download; reused input retains its original arrival time.
465
465
  const processing = trackState[trackIndex].curve;
466
- let admittedAt = Math.max(trackFinish[trackIndex], sourceArrivalAt);
466
+ let runAdmissionAt = null;
467
+ let runOpenSeconds = 0;
468
+ let productionStartAt = Math.max(trackFinish[trackIndex], sourceArrivalAt);
467
469
  if (!job.produced && encodeWork > 0) {
468
470
  const run = runStarts[trackIndex];
469
471
  const live = run.spans.some(span => segment.startSeconds >= span.start && segment.startSeconds < span.end);
470
472
  if (!live && (run.admittedAt === null || sourceArrivalAt > run.admittedAt)) {
471
- run.admittedAt = admittedAt;
472
- admittedAt += run.openSeconds;
473
+ runAdmissionAt = productionStartAt;
474
+ run.admittedAt = runAdmissionAt;
475
+ runOpenSeconds = run.openSeconds;
476
+ productionStartAt += runOpenSeconds;
473
477
  }
474
478
  }
475
- const producedAt = job.produced || encodeWork === 0 ? 0 : processing.finish(encodeWork, admittedAt);
479
+ const producedAt = job.produced || encodeWork === 0 ? 0 : processing.finish(encodeWork, productionStartAt);
476
480
  trackFinish[trackIndex] = Math.max(trackFinish[trackIndex], producedAt);
477
481
  const sourcePlanned = sourceArrivalAt > 0 && plan.finish >= sourceArrivalAt;
478
482
  productions.push({ at: producedAt, trackIndex, segment, requiresSource: sourceArrivalAt > 0, sourceArrivalAt,
479
- sourcePlanned });
483
+ sourcePlanned, runAdmissionAt, runOpenSeconds, productionStartAt, encodeWork });
480
484
  const deliveryAt = link.finish(sizeBits, Math.max(transferFinish, producedAt));
481
485
  transferFinish = deliveryAt;
482
- completions.push({ at: deliveryAt + finiteNonNegative(track.appendSeconds), trackIndex, segment, sourceArrivalAt,
486
+ const appendSeconds = finiteNonNegative(track.appendSeconds);
487
+ completions.push({ at: deliveryAt + appendSeconds, deliveryAt, appendSeconds, trackIndex, segment, sourceArrivalAt,
483
488
  sourcePlanned });
484
489
  }
485
490
 
@@ -547,13 +552,32 @@ export function predictPlaybackReadiness(input = {}) {
547
552
  ...(stop.unavailableSource ? { unavailableSource: stop.unavailableSource } : {}) };
548
553
  }
549
554
  }
550
- const firstRequiredTrackSeconds = trackState.slice(0, requiredTracks).map((state, trackIndex) => {
555
+ const firstRequiredStages = trackState.slice(0, requiredTracks).map((state, trackIndex) => {
551
556
  const first = state.segments.find(segment => segment.endSeconds > position);
552
- if (!first) return 0;
553
- if (first.ranges.every(range => contains(state.clientRanges, range.start, range.end))) return 0;
557
+ if (!first) return { track: state.track.id ?? trackIndex, segment: null, alreadyBuffered: true,
558
+ sourceArrivalSeconds: 0, runAdmissionSeconds: null, runOpenSeconds: 0, productionStartSeconds: 0,
559
+ productionCompleteSeconds: 0, deliveryCompleteSeconds: 0, appendSeconds: 0, completionSeconds: 0 };
560
+ if (first.ranges.every(range => contains(state.clientRanges, range.start, range.end))) {
561
+ return { track: state.track.id ?? trackIndex, segment: first.index, alreadyBuffered: true,
562
+ sourceArrivalSeconds: 0, runAdmissionSeconds: null, runOpenSeconds: 0, productionStartSeconds: 0,
563
+ productionCompleteSeconds: 0, deliveryCompleteSeconds: 0, appendSeconds: 0, completionSeconds: 0 };
564
+ }
554
565
  const completion = completions.find(item => item.trackIndex === trackIndex && item.segment.index === first.index);
555
- return completion?.at ?? null;
566
+ if (!completion) return { track: state.track.id ?? trackIndex, segment: first.index, alreadyBuffered: false,
567
+ sourceArrivalSeconds: null, runAdmissionSeconds: null, runOpenSeconds: null, productionStartSeconds: null,
568
+ productionCompleteSeconds: null, deliveryCompleteSeconds: null, appendSeconds: null, completionSeconds: null };
569
+ const production = productions.find(item => item.trackIndex === trackIndex && item.segment.index === first.index);
570
+ return { track: state.track.id ?? trackIndex, segment: first.index, alreadyBuffered: false,
571
+ sourceArrivalSeconds: production?.sourceArrivalAt ?? 0,
572
+ runAdmissionSeconds: production?.runAdmissionAt ?? null,
573
+ runOpenSeconds: production?.runOpenSeconds ?? 0,
574
+ productionStartSeconds: production?.productionStartAt ?? null,
575
+ productionCompleteSeconds: production?.at ?? null,
576
+ deliveryCompleteSeconds: completion.deliveryAt,
577
+ appendSeconds: completion.appendSeconds,
578
+ completionSeconds: completion.at };
556
579
  });
580
+ const firstRequiredTrackSeconds = firstRequiredStages.map(stage => stage.completionSeconds);
557
581
  const firstRequiredSegmentsSeconds = firstRequiredTrackSeconds.every(Number.isFinite)
558
582
  ? Math.max(0, ...firstRequiredTrackSeconds) : null;
559
583
  const delay = Math.max(0, ...constraints);
@@ -564,6 +588,7 @@ export function predictPlaybackReadiness(input = {}) {
564
588
  Number.isFinite(delay) ? 'minimum-safe-delay' : 'service-not-advancing', preparedSegments);
565
589
  return { ...forecast, measuredAt: now, predictedStartAt: Number.isFinite(delay) ? now + delay * 1000 : null,
566
590
  firstRequiredSegmentsSeconds,
591
+ firstRequiredStages,
567
592
  // The piece whose `kind` — its production, or its delivery to the page —
568
593
  // comes `atSeconds` from now while the media before it lasts
569
594
  // `aheadSeconds`: the difference is the delay.