@torrent-tv/proxy 2.94.0 → 2.94.2

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.94.0",
3
+ "version": "2.94.2",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -102,7 +102,7 @@ export class AviContainer extends Container {
102
102
  if (!(error instanceof OutsideReadableEdges)) throw error;
103
103
  tags.outsideEdges = true;
104
104
  }
105
- tags.trackTitles = textList((await this.readTracks()).map((track) => track.name));
105
+ tags.trackTitles = await this.trackTitlesForWork();
106
106
  if (!tags.outsideEdges) this.#workTags = tags;
107
107
  return tags;
108
108
  }
@@ -12,7 +12,8 @@
12
12
  * - AVI RIFF §: LIST hdrl, idx1
13
13
  */
14
14
 
15
- import { strictReader } from "./unavailable.js";
15
+ import { isUnavailable, strictReader } from "./unavailable.js";
16
+ import { textList } from "./work-tags.js";
16
17
 
17
18
  /**
18
19
  * What one file declares about itself. Every field is either a value the
@@ -514,6 +515,23 @@ export class Container {
514
515
  return null;
515
516
  }
516
517
 
518
+ /**
519
+ * The titles of the file's tracks, for what it states about its work. A
520
+ * track table this program refuses to read costs only the titles: the
521
+ * statements about the work are read from other elements. Bytes that have
522
+ * not arrived are still "not here yet".
523
+ *
524
+ * @returns {Promise<string[]>}
525
+ */
526
+ async trackTitlesForWork() {
527
+ try {
528
+ return textList((await this.readTracks()).map((track) => track.name));
529
+ } catch (error) {
530
+ if (isUnavailable(error)) throw error;
531
+ return [];
532
+ }
533
+ }
534
+
517
535
  /**
518
536
  * The cover image the file carries, checked to be an image by its bytes.
519
537
  * `null` where it carries none.
@@ -437,7 +437,7 @@ export class MatroskaContainer extends Container {
437
437
  const cover = await this.#coverAddress(reader, layout);
438
438
  if (cover && cover.size > 0 && cover.size <= MAX_COVER_BYTES) tags.cover = { type: cover.mediaType, size: cover.size };
439
439
  } catch (error) { leftOut(error); }
440
- tags.trackTitles = textList((await this.readTracks()).map((track) => track.name));
440
+ tags.trackTitles = await this.trackTitlesForWork();
441
441
  if (!tags.outsideEdges) this.#workTags = tags;
442
442
  return tags;
443
443
  }
@@ -28,7 +28,7 @@ import { hevcConfiguration } from "./hevc-configuration.js";
28
28
  import { mpegAudioFrame } from "./mpeg-audio-frame.js";
29
29
  import { readMp4Fragments } from "./mp4-fragments.js";
30
30
  import { workFromMoov } from "./mp4-work-tags.js";
31
- import { COVER_TYPES, MAX_COVER_BYTES, emptyWorkTags, imageTypeOf, textList } from "./work-tags.js";
31
+ import { COVER_TYPES, MAX_COVER_BYTES, emptyWorkTags, imageTypeOf } from "./work-tags.js";
32
32
 
33
33
  /** A box header is eight bytes, or sixteen when the size field says 1 (§4.2). */
34
34
  const HEADER_BYTES = 8;
@@ -532,7 +532,7 @@ export class Mp4Container extends Container {
532
532
  Object.assign(tags, work);
533
533
  if (cover && cover.size > 0 && cover.size <= MAX_COVER_BYTES) tags.cover = { type: cover.type, size: cover.size };
534
534
  }
535
- tags.trackTitles = textList((await this.readTracks()).map((track) => track.name));
535
+ tags.trackTitles = await this.trackTitlesForWork();
536
536
  this.workTags = tags;
537
537
  return tags;
538
538
  }
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { logger } from "../../utils/logger.js";
11
11
  import { isOutputName } from "../encode/output/index.js";
12
+ import { withSubtitleReadiness } from "./subtitle-readiness.js";
12
13
 
13
14
  function isWarmupTimeoutError(error) {
14
15
  if (!(error instanceof Error)) {
@@ -696,7 +697,13 @@ export class ViewerRequests {
696
697
  tracks,
697
698
  linkReadings: measurement.link.snapshot()
698
699
  };
699
- const forecast = this.#host.playbackReadiness.predict(input);
700
+ // A subtitle track chosen inside THIS file is the last input the forecast
701
+ // waits for. A subtitle FILE beside it is named by its own index and is
702
+ // fetched whole by the page before it is chosen, so it never matches here.
703
+ const subtitle = viewer?.subtitle?.sourceKey === session.file.sourceKey &&
704
+ viewer.subtitle.fileIndex === session.file.fileIndex ? viewer.subtitle : null;
705
+ const forecast = withSubtitleReadiness(this.#host.playbackReadiness.predict(input), subtitle,
706
+ subtitle ? this.#host.subtitleReadyFor?.(viewer) !== false : true);
700
707
  forecast.operations = outputs.flatMap((output, index) => {
701
708
  const common = { speed: this.#host.playbackReadiness.forecastRate(tracks[index].readings), processedSeconds: tracks[index].processedSeconds };
702
709
  return [
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @file The subtitle a viewer has chosen, as a term of their playback readiness.
3
+ *
4
+ * A file that says which subtitle to show — `FlagDefault` — is saying the film
5
+ * is to be watched with it, the opening included. Starting the picture before
6
+ * that track's cues have been read where the viewer stands showed the opening
7
+ * without them (torrent-tv/meta#8). So the chosen track is one more input the
8
+ * forecast waits for, beside the picture and the separately published sound.
9
+ *
10
+ * **What bounds the wait.** The read it waits for is one segment of that track
11
+ * at the viewer's position (`SourcePreparation`, the `subtitle-embedded`
12
+ * work), demanded at the picture's own urgency. It ends with the cues or with
13
+ * the proxy's refusal to read the track, and `subtitleReadyFor` counts both.
14
+ * No time limit is added: there is no measured figure to set one from, and the
15
+ * bytes it needs are the ones beside the picture's own at the same position.
16
+ *
17
+ * The answer always carries `subtitles`, `null` included, so the page can tell
18
+ * a proxy that weighed the chosen track from one that never heard of it.
19
+ */
20
+
21
+ /**
22
+ * @param {object} forecast - What `predictPlaybackReadiness` answered.
23
+ * @param {{ fileIndex: number, trackIndex: number | null } | null} selection -
24
+ * The subtitle this viewer chose, for the file being opened; null for none.
25
+ * @param {boolean} ready - Whether that subtitle has been read where they stand.
26
+ * @returns {object} The forecast, holding for the subtitle where it must.
27
+ */
28
+ export function withSubtitleReadiness(forecast, selection, ready) {
29
+ const subtitles = selection
30
+ ? { fileIndex: selection.fileIndex, trackIndex: selection.trackIndex, ready: ready === true }
31
+ : null;
32
+ if (forecast?.ready !== true || !subtitles || subtitles.ready) {
33
+ return { ...forecast, subtitles };
34
+ }
35
+ // No delay: the forecast's figure is the picture's, which is already met, and
36
+ // no rate predicts when a read of a few cues finishes.
37
+ return { ...forecast, ready: false, delaySeconds: null, reason: "subtitles-pending", subtitles };
38
+ }
@@ -572,6 +572,9 @@ export function wireOutputs({
572
572
  // When this viewer's playback can start and run to the end: the viewer
573
573
  // component's forecast, which models the viewer's browser.
574
574
  playbackReadiness: { predict: predictPlaybackReadiness, forecastRate, RateTrend },
575
+ // Whether the subtitle a viewer chose has been read where they stand — the
576
+ // source preparation's answer, set on these parts by the assembly.
577
+ subtitleReadyFor: (viewer) => parts.subtitleReadyFor?.(viewer) ?? true,
575
578
  sourceInputsFor: (output, index) => parts.sourceInputsFor?.(output, index),
576
579
  encodeSpeedReadingOf: (output) => parts.encodeCost.latestSpeedReadingOf(output),
577
580
  projectedEncodeSpeedOf: (output) => parts.encodeCost.projectedSpeedOf(output),
@@ -104,14 +104,26 @@ export class SourcePreparation {
104
104
  work.positions === this.#positionsFor(work.sourceKey, work.fileIndex, viewer.id));
105
105
  }
106
106
 
107
+ /**
108
+ * Whether the subtitle this viewer chose has been read where they stand.
109
+ *
110
+ * Playback readiness waits on this (`subtitle-readiness.js`), so every way
111
+ * the read can END must answer it, and a refusal is one of them. The cues
112
+ * are the last of four reads — the track table, the media info, the packet
113
+ * index, then the cues — and a refusal at an earlier step means the later
114
+ * ones are never asked. Counting only the cues' own answer left a track the
115
+ * proxy cannot read waiting for ever.
116
+ */
107
117
  subtitleReadyFor(viewer) {
108
118
  const selection = viewer.subtitle;
109
119
  if (!selection) return true;
120
+ const positions = () => this.#positionsFor(selection.sourceKey, selection.fileIndex, viewer.id, true);
110
121
  return [...this.#work.values()].some(work => work.sourceKey === selection.sourceKey &&
111
122
  work.fileIndex === selection.fileIndex && ["result", "terminal"].includes(work.result) &&
112
123
  (selection.trackIndex === null ? work.statement === "subtitle-file" && work.ownerFileIndex === viewer.source.selectedFileIndex
113
- : work.statement === "subtitle-cues" && work.ownerId === viewer.id && work.trackIndex === selection.trackIndex &&
114
- work.positions === this.#positionsFor(work.sourceKey, work.fileIndex, viewer.id, true)));
124
+ : work.role === "subtitle-embedded" && work.ownerId === viewer.id && work.trackIndex === selection.trackIndex &&
125
+ (work.positions === undefined || work.positions === positions()) &&
126
+ (work.statement === "subtitle-cues" || work.result === "terminal")));
115
127
  }
116
128
 
117
129
  async bytesChanged(sourceKey, fileIndex) {