@torrent-tv/proxy 2.22.0 → 2.24.0

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.
@@ -15,7 +15,7 @@ import assert from "node:assert/strict";
15
15
  import { mkdtemp, rm } from "node:fs/promises";
16
16
  import os from "node:os";
17
17
  import path from "node:path";
18
- import { computeSegmentBoundaries, HlsSessionManager } from "../services/hls-session-manager.js";
18
+ import { computeSegmentBoundaries, costKindForSession, HlsSessionManager } from "../services/hls-session-manager.js";
19
19
  import { fmp4Format } from "../services/segment-formats/fmp4.js";
20
20
 
21
21
  const BASE_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
@@ -736,3 +736,69 @@ test("an audio track is prepared at the position the switch will land on", async
736
736
  "and the track is pointed at the switch position, one back for the preceding keyframe"
737
737
  );
738
738
  });
739
+
740
+ test("a reading from a soundtrack is priced as one", () => {
741
+ // The fault this pins: an audio rendition reached no learner at all. One
742
+ // guard refused renditions a reading, and the only call that would have
743
+ // priced one sat behind a second guard its caller had already made — so a
744
+ // soundtrack ran for the whole film and was charged at nothing, which is the
745
+ // half of roadmap item 6 that was left owing.
746
+ assert.equal(costKindForSession({ audioOnly: true, transcodeVideo: false }), "audio");
747
+ assert.equal(
748
+ costKindForSession({ audioOnly: true, transcodeVideo: true }),
749
+ "audio",
750
+ "a rendition carries no picture, whatever the flag it inherited says"
751
+ );
752
+ assert.equal(costKindForSession({ transcodeVideo: true }), "decode");
753
+ assert.equal(costKindForSession({ transcodeVideo: false }), "copy");
754
+ });
755
+
756
+ test("a quality step being warmed is not refused by its own cost", async (t) => {
757
+ const dirPath = await mkdtemp(path.join(os.tmpdir(), "warm-cost-"));
758
+ // The addon host's own figures, so the arithmetic below is the field's and
759
+ // not an invention. Without a benchmark the check returns every height
760
+ // untouched and a test over it would pass while proving nothing.
761
+ const manager = new HlsSessionManager({
762
+ enabled: true,
763
+ ffmpegBin: "ffmpeg",
764
+ localBindHost: "127.0.0.1",
765
+ localPort: 9090,
766
+ softwarePresetBenchmark: [
767
+ { preset: "fast", pixelsPerSec: 17.0e6 },
768
+ { preset: "ultrafast", pixelsPerSec: 67.5e6 }
769
+ ],
770
+ decodeCostModel: { pixelTerm: 0.007742, bitrateTerm: 0, constantTerm: 0 }
771
+ });
772
+ t.after(async () => {
773
+ await manager.disposeAll();
774
+ await rm(dirPath, { recursive: true, force: true });
775
+ });
776
+ // A copied 1080p picture, and a 240p step warmed beside it for a switch —
777
+ // which is what every quality change does, two encoders on purpose.
778
+ const base = fakeSession({ id: BASE_ID, encodeHeight: 0, dirPath, transcodeVideo: false });
779
+ base.variantHeight = 1080;
780
+ // What this source costs to decode — 1080p24 at 8 Mbit/s, the field file.
781
+ // Without it nothing can be priced and every height comes back offerable for
782
+ // want of a measurement, which would make the assertion hold for the wrong
783
+ // reason.
784
+ base.sourceDecode = { megapixelsPerSecond: (1920 * 1080 * 24) / 1e6, megabitsPerSecond: 8 };
785
+ const warming = fakeSession({ id: VARIANT_ID, encodeHeight: 240, dirPath });
786
+ warming.variantHeight = 240;
787
+ warming.sourceDecode = base.sourceDecode;
788
+ base.variants = new Map([[240, VARIANT_ID]]);
789
+ warming.variantBases = new Set([BASE_ID]);
790
+ // Running, and running well: it says of itself that it holds twice realtime,
791
+ // i.e. half a second of work per second of video.
792
+ warming.ffmpeg = fakeEncoder();
793
+ warming.lastAloneSpeed = 2;
794
+ manager.sessionsById.set(BASE_ID, base);
795
+ manager.sessionsById.set(VARIANT_ID, warming);
796
+
797
+ const offered = manager.offeredHeights(base);
798
+
799
+ assert.ok(
800
+ offered.includes(240),
801
+ "charged its own cost while being judged, the step the viewer just asked for is dropped from the " +
802
+ "offer by the act of warming it — and its next segment 404s on a stream that is playing"
803
+ );
804
+ });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @file The drawing cannot disagree with the table.
3
+ *
4
+ * `docs/encode-run-state.md` is generated from `services/encode-run-state.js`.
5
+ * A picture kept beside the code drifts — this repository has the receipts —
6
+ * so the check is mechanical: regenerate it and compare. A behavioural change
7
+ * to the table that forgets `npm run graph` fails here, in the same commit.
8
+ */
9
+
10
+ import assert from "node:assert/strict";
11
+ import { readFileSync } from "node:fs";
12
+ import test from "node:test";
13
+
14
+ import { GRAPH_DOC_PATH, renderRunGraphMarkdown } from "../scripts/render-run-graph.js";
15
+
16
+ /**
17
+ * Line endings are not part of the comparison. Git is configured with
18
+ * `autocrlf=true` on the machine this is developed on, so the same file is LF
19
+ * in the index and CRLF in the working tree — and a test failing on that would
20
+ * point at the generator, whose output is always LF, and be unfixable by the
21
+ * remedy it suggests.
22
+ *
23
+ * @param {string} text
24
+ * @returns {string}
25
+ */
26
+ function withoutLineEndings(text) {
27
+ return text.replace(/\r\n/g, "\n");
28
+ }
29
+
30
+ test("the committed picture is the one the table produces", () => {
31
+ const committed = readFileSync(GRAPH_DOC_PATH, "utf8");
32
+ assert.equal(
33
+ withoutLineEndings(committed),
34
+ withoutLineEndings(renderRunGraphMarkdown()),
35
+ "docs/encode-run-state.md is out of date — run `npm run graph` and commit the result"
36
+ );
37
+ });
@@ -21,6 +21,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
21
21
  import os from "node:os";
22
22
  import path from "node:path";
23
23
  import { HlsSessionManager } from "../services/hls-session-manager.js";
24
+ import { ENCODE_RUN_STATE } from "../services/encode-run-state.js";
24
25
  import { fmp4Format } from "../services/segment-formats/fmp4.js";
25
26
 
26
27
  const MOVIE_TIMESCALE = 1000;
@@ -276,3 +277,48 @@ test("a segment is found in the run directory that produced it, newest run first
276
277
  "the newest run's output must win — the older file here is the 8-byte stub a killed run leaves"
277
278
  );
278
279
  });
280
+
281
+ test("serving a run's own segment moves the run out of STARTING", async (t) => {
282
+ const { manager, session, dirPath } = await managerWithReadySegment();
283
+ t.after(async () => {
284
+ await manager.disposeAll();
285
+ await rm(dirPath, { recursive: true, force: true });
286
+ });
287
+ // The table lives in `encode-run-state.js` and is tested there as a graph.
288
+ // What this pins is that a real serve REACHES it: a state nothing writes
289
+ // describes every run as still starting, for ever, and the log built on it
290
+ // would say so too.
291
+ session.runState = ENCODE_RUN_STATE.STARTING;
292
+ // Where the run in force is writing. The fixture's segments live directly in
293
+ // the session directory, which is exactly what a single run's directory is
294
+ // here.
295
+ session.runDirPath = dirPath;
296
+
297
+ await manager.getFileStream(SESSION_ID, "segment-00000.mp4", { requestSeq: 1 });
298
+
299
+ assert.equal(session.runState, ENCODE_RUN_STATE.PRODUCING);
300
+ });
301
+
302
+ test("a segment left by an earlier run does not claim the new run has produced", async (t) => {
303
+ const { manager, session, dirPath } = await managerWithReadySegment();
304
+ t.after(async () => {
305
+ await manager.disposeAll();
306
+ await rm(dirPath, { recursive: true, force: true });
307
+ });
308
+ // A seek places the new run in a directory of its own; the previous run's
309
+ // segments stay servable and are served from theirs. They say nothing about
310
+ // what the run now starting has done — and a run believed to be producing is
311
+ // one the look-ahead may suspend and the seek path may wave through as
312
+ // "already covered by the running encode".
313
+ //
314
+ // Deliberately a segment ABOVE the new run's start index, because that is the
315
+ // case an index comparison gets wrong: after a backward seek the old run's
316
+ // output sits ahead of the new run's beginning.
317
+ session.runState = ENCODE_RUN_STATE.STARTING;
318
+ session.encodeStartIndex = 0;
319
+ session.runDirPath = path.join(dirPath, "run-7");
320
+
321
+ await manager.getFileStream(SESSION_ID, "segment-00000.mp4", { requestSeq: 1 });
322
+
323
+ assert.equal(session.runState, ENCODE_RUN_STATE.STARTING);
324
+ });