@torrent-tv/proxy 2.94.1 → 2.94.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.94.1",
3
+ "version": "2.94.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": {
@@ -21,16 +21,7 @@ export function h264Configuration(avcc) {
21
21
 
22
22
  function readSps(nal, includeOrder = false) {
23
23
  if ((nal[0] & 31) !== 7 || nal[0] & 128) throw new Error("AVC configuration entry is not an SPS.");
24
- const bytes = [];
25
- for (let at = 1, zeroes = 0; at < nal.length; at++) {
26
- const byte = nal[at];
27
- if (zeroes >= 2 && byte === 3) {
28
- if (at + 1 >= nal.length || nal[at + 1] > 3) throw new Error("AVC emulation prevention byte is invalid.");
29
- zeroes = 0; continue;
30
- }
31
- bytes.push(byte); zeroes = byte === 0 ? zeroes + 1 : 0;
32
- }
33
- const bits = new Bits(Buffer.from(bytes));
24
+ const bits = new Bits(unescapeNal(nal));
34
25
  const profile = bits.uint(8), constraints = bits.uint(8), level = bits.uint(8);
35
26
  bits.ue();
36
27
  let chroma = 1, separate = 0, depth = 8;
@@ -131,14 +122,16 @@ export function avcOrderParameters(sps, pps) {
131
122
  return { ...facts.pictureOrder, bottomFieldOrderPresent: bits.uint(1) === 1 };
132
123
  }
133
124
 
125
+ /**
126
+ * Remove every emulation prevention byte: a 0x03 after two zero bytes, whatever follows it. H.264 §7.4.1
127
+ * forbids 0x000003 before a byte above 0x03, but encoders write it (LostFilm's LostCoder) and decoders drop
128
+ * it (ffmpeg's ff_h2645_extract_rbsp), so refusing it refuses files every player plays.
129
+ */
134
130
  export function unescapeNal(nal) {
135
131
  const bytes = [];
136
132
  for (let at = 1, zeroes = 0; at < nal.length; at++) {
137
133
  const byte = nal[at];
138
- if (zeroes >= 2 && byte === 3) {
139
- if (at + 1 >= nal.length || nal[at + 1] > 3) throw new Error("AVC escape byte is invalid.");
140
- zeroes = 0; continue;
141
- }
134
+ if (zeroes >= 2 && byte === 3) { zeroes = 0; continue; }
142
135
  bytes.push(byte);
143
136
  zeroes = byte === 0 ? zeroes + 1 : 0;
144
137
  }
@@ -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) {