@torrent-tv/proxy 2.75.0 → 2.76.1

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.
Files changed (113) hide show
  1. package/CHANGELOG.md +1509 -1461
  2. package/CLAUDE.md +11 -1
  3. package/biome.json +182 -1
  4. package/docs/container-architecture.md +2 -1
  5. package/docs/encode-run-state.md +1 -1
  6. package/knip.json +14 -0
  7. package/package.json +1 -1
  8. package/routes/api/sources/warm/post.js +1 -1
  9. package/routes/api/transcode-sessions/post.js +185 -185
  10. package/routes/api/transcode-sessions/progress/get.js +5 -1
  11. package/routes/transcode/audio-file/get.js +11 -1
  12. package/routes/transcode/audio-warm/get.js +11 -1
  13. package/routes/transcode/session-file/get.js +1 -1
  14. package/routes/transcode/variant-file/get.js +10 -1
  15. package/scripts/render-run-graph.js +2 -2
  16. package/server.js +25 -0
  17. package/services/audio-inventory.js +206 -201
  18. package/services/container/AviContainer.js +1 -1
  19. package/services/container/Container.js +33 -1
  20. package/services/container/MatroskaContainer.js +1 -1
  21. package/services/container/Mp4Container.js +1 -1
  22. package/services/container/SubtitleFileContainer.js +0 -1
  23. package/services/controllers/SubtitleController.js +128 -128
  24. package/services/demand/index.js +7 -10
  25. package/services/download/registry.js +0 -14
  26. package/services/encode/CoverageMap.js +281 -0
  27. package/services/encode/EncodePlan.js +255 -0
  28. package/services/encode/EncodeRun.js +587 -0
  29. package/services/encode/Encoder.js +84 -0
  30. package/services/encode/NvencEncoder.js +45 -0
  31. package/services/encode/QsvEncoder.js +47 -0
  32. package/services/encode/SegmentDemand.js +0 -0
  33. package/services/encode/SegmentStore.js +529 -0
  34. package/services/encode/SoftwareEncoder.js +111 -0
  35. package/services/encode/V4l2m2mEncoder.js +53 -0
  36. package/services/encode/VaapiEncoder.js +53 -0
  37. package/services/encode/args.js +200 -0
  38. package/services/{encode-exit.js → encode/encode-exit.js} +17 -0
  39. package/services/encode/index.js +9 -0
  40. package/services/encode/run-command.js +647 -0
  41. package/services/hls-session-manager.js +11100 -10711
  42. package/services/hwaccel.js +1688 -1992
  43. package/services/orchestrators/EncodeOrchestrator.js +359 -0
  44. package/services/output/LiveOutputs.js +213 -0
  45. package/services/output/Output.js +94 -0
  46. package/services/output/OutputSpec.js +195 -0
  47. package/services/output/Timeline.js +220 -0
  48. package/services/output/index.js +1 -0
  49. package/services/output/ladder.js +26 -0
  50. package/services/playback-planner.js +806 -775
  51. package/services/produced-index.js +222 -300
  52. package/services/source/SourceFile.js +346 -0
  53. package/services/{sidecar-files.js → torrent/files.js} +107 -11
  54. package/services/torrent/naming.js +619 -0
  55. package/services/torrent-worker/client.js +10 -0
  56. package/services/torrent-worker/container-tracks.js +71 -43
  57. package/services/torrent-worker/pool-adapter.js +18 -0
  58. package/services/torrent-worker/protocol.js +7 -0
  59. package/services/torrent-worker/subtitle-cues.js +549 -549
  60. package/services/torrent-worker/worker.js +18 -0
  61. package/services/tracks/TextSubtitleTrack.js +287 -287
  62. package/services/tracks/index.js +15 -14
  63. package/services/viewer/Viewer.js +145 -0
  64. package/services/viewer/Viewers.js +124 -0
  65. package/test/auto-quality-step.test.js +508 -506
  66. package/test/behind-head-repair.test.js +17 -7
  67. package/test/coverage-map.test.js +153 -0
  68. package/test/cut-times-timeline.test.js +6 -5
  69. package/test/cuts-follow-published-grid.test.js +4 -4
  70. package/test/decode-cost.test.js +31 -12
  71. package/test/encode-exit.test.js +1 -1
  72. package/test/encode-orchestrator.test.js +196 -0
  73. package/test/encode-plan.test.js +245 -0
  74. package/test/encode-run-state.test.js +2 -2
  75. package/test/encode-run.test.js +168 -0
  76. package/test/encoder-kinds.test.js +122 -0
  77. package/test/held-request-width.test.js +9 -3
  78. package/test/helpers/encode-run.js +128 -0
  79. package/test/keyframe-index-accuracy.test.js +19 -12
  80. package/test/keyframes-belong-to-the-file.test.js +132 -0
  81. package/test/orchestrator-wired.test.js +164 -0
  82. package/test/output-shape.test.js +68 -0
  83. package/test/output-spec.test.js +157 -0
  84. package/test/produced-copy-choice.test.js +58 -92
  85. package/test/produced-index.test.js +142 -188
  86. package/test/quality-variants.test.js +1079 -1075
  87. package/test/run-graph-drift.test.js +1 -1
  88. package/test/run-intervals.test.js +329 -0
  89. package/test/run-position-follows-published-grid.test.js +4 -4
  90. package/test/seek-landing.test.js +8 -8
  91. package/test/seek-target-not-superseded.test.js +21 -9
  92. package/test/segment-demand.test.js +82 -0
  93. package/test/segment-serve-wiring.test.js +47 -52
  94. package/test/segment-store.test.js +187 -0
  95. package/test/segments-are-shared.test.js +175 -0
  96. package/test/sidecar-naming.test.js +142 -0
  97. package/test/source-file.test.js +133 -0
  98. package/test/stale-request-after-seek.test.js +18 -12
  99. package/test/subtitle-language.test.js +252 -252
  100. package/test/timeline.test.js +95 -0
  101. package/test/{sidecar-files.test.js → torrent-files.test.js} +44 -1
  102. package/test/torrent-naming.test.js +255 -0
  103. package/test/tracks-begin-together.test.js +44 -32
  104. package/test/two-viewers-one-picture.test.js +347 -0
  105. package/test/viewer-outputs.test.js +273 -0
  106. package/test/viewer.test.js +91 -0
  107. package/utils/perf.js +1 -63
  108. package/services/container/index.js +0 -6
  109. package/services/controllers/index.js +0 -2
  110. package/services/download/index.js +0 -8
  111. package/services/orchestrators/index.js +0 -2
  112. /package/services/{encode-run-state.js → encode/encode-run-state.js} +0 -0
  113. /package/services/{language-detect.js → tracks/language-detect.js} +0 -0
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @file One object per person watching, and one deletion to forget them.
3
+ *
4
+ * It was six maps hung on the session, each keyed by consumer id, and the
5
+ * forgetting was already wrong: releasing a consumer emptied none of them, so a
6
+ * viewer who had left went on counting as wanting their soundtrack until their
7
+ * head expired, and their entries stayed for the life of the session.
8
+ */
9
+
10
+ import test from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { SourceFile } from "../services/source/SourceFile.js";
13
+ import { mkdtemp, rm } from "node:fs/promises";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+ import { HlsSessionManager } from "../services/hls-session-manager.js";
17
+ import { Viewer, viewerOf, viewersOf } from "../services/viewer/Viewer.js";
18
+
19
+ const SESSION_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
20
+
21
+ test("a viewer nobody has heard from is not watching", () => {
22
+ const viewer = new Viewer("someone");
23
+ const now = 1_000_000;
24
+
25
+ assert.equal(viewer.isLive(now, 60_000), false, "no head at all: they have never asked");
26
+ viewer.head = { segment: 4, seconds: 16, at: now - 10_000 };
27
+ assert.equal(viewer.isLive(now, 60_000), true);
28
+ viewer.head = { segment: 4, seconds: 16, at: now - 120_000 };
29
+ assert.equal(viewer.isLive(now, 60_000), false, "the tab is gone and nothing released it");
30
+ });
31
+
32
+ test("a stated position outranks the segment they last asked for", () => {
33
+ const viewer = new Viewer("someone");
34
+ assert.equal(viewer.positionSeconds(), null);
35
+
36
+ viewer.head = { segment: 10, seconds: 40, at: 1 };
37
+ assert.equal(viewer.positionSeconds(), 40, "where their player is reading");
38
+
39
+ viewer.head = { segment: 10, seconds: 40, at: 1, seeked: 900 };
40
+ assert.equal(viewer.positionSeconds(), 900, "a seek is the viewer saying where they are");
41
+ });
42
+
43
+ test("asking for a viewer twice is the same viewer", () => {
44
+ const session = {};
45
+ const first = viewerOf(session, "one");
46
+ first.audio = { trackIndex: 3, transcode: false };
47
+
48
+ assert.equal(viewerOf(session, "one"), first);
49
+ assert.equal(viewerOf(session, "one").audio.trackIndex, 3);
50
+ assert.notEqual(viewerOf(session, "two"), first);
51
+ assert.equal(viewersOf(session).size, 2);
52
+ });
53
+
54
+ test("releasing a consumer forgets everything that was true of them alone", async (t) => {
55
+ const dirPath = await mkdtemp(path.join(os.tmpdir(), "viewer-"));
56
+ t.after(async () => {
57
+ await rm(dirPath, { recursive: true, force: true });
58
+ });
59
+ const manager = new HlsSessionManager({
60
+ enabled: true,
61
+ ffmpegBin: "ffmpeg",
62
+ localBindHost: "127.0.0.1",
63
+ localPort: 9090
64
+ });
65
+ const session = {
66
+ id: SESSION_ID,
67
+ dirPath,
68
+ state: "live",
69
+ file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" }),
70
+ // An ordinary session reads its own file, and its sound is inside it. The
71
+ // three differ only for a soundtrack shipped as a file of its own.
72
+ get inputFile() { return this.file; },
73
+ get audioFile() { return this.file; },
74
+ consumers: new Set(["staying", "leaving"]),
75
+ viewers: new Map(),
76
+ lastAccessedAt: Date.now()
77
+ };
78
+ manager.sessionsById.set(SESSION_ID, session);
79
+
80
+ const leaving = viewerOf(session, "leaving");
81
+ leaving.audio = { trackIndex: 2, transcode: true };
82
+ leaving.activeVariantId = "some-variant";
83
+ leaving.head = { segment: 40, seconds: 160, at: Date.now() };
84
+ viewerOf(session, "staying").audio = { trackIndex: 0, transcode: false };
85
+
86
+ await manager.releaseSessionConsumer(SESSION_ID, "leaving", "the tab was closed");
87
+
88
+ assert.equal(session.viewers.has("leaving"), false, "one deletion, not six");
89
+ assert.equal(session.viewers.has("staying"), true, "and it takes nobody else with it");
90
+ assert.equal(session.consumers.size, 1);
91
+ });
package/utils/perf.js CHANGED
@@ -21,7 +21,7 @@
21
21
  * somewhere specific.
22
22
  */
23
23
 
24
- import { monitorEventLoopDelay, performance } from "node:perf_hooks";
24
+ import { monitorEventLoopDelay } from "node:perf_hooks";
25
25
 
26
26
  const NANOSECONDS_PER_MILLISECOND = 1e6;
27
27
  // Sampling interval for the loop-delay histogram. 20 ms is fine enough to catch
@@ -57,65 +57,3 @@ export function eventLoopDelay() {
57
57
  export function resetEventLoopDelay() {
58
58
  loopDelay.reset();
59
59
  }
60
-
61
- /**
62
- * Accumulates how long the parts of one operation took.
63
- *
64
- * Usage: `mark()` after each step; `summary()` renders `step=12.3ms` pairs in
65
- * the order they were marked.
66
- */
67
- export class OperationTimer {
68
- #startedAt;
69
- #lastMarkAt;
70
- #marks;
71
-
72
- constructor() {
73
- this.#startedAt = performance.now();
74
- this.#lastMarkAt = this.#startedAt;
75
- this.#marks = [];
76
- }
77
-
78
- /**
79
- * Record the time since the previous mark under `name`.
80
- *
81
- * @param {string} name
82
- * @returns {number} Milliseconds since the previous mark.
83
- */
84
- mark(name) {
85
- const now = performance.now();
86
- const elapsed = now - this.#lastMarkAt;
87
- this.#lastMarkAt = now;
88
- this.#marks.push([name, elapsed]);
89
- return elapsed;
90
- }
91
-
92
- /**
93
- * Add a figure measured elsewhere (a running total, a count) so it appears in
94
- * the same line as the timings.
95
- *
96
- * @param {string} name
97
- * @param {number} value
98
- * @returns {void}
99
- */
100
- add(name, value) {
101
- this.#marks.push([name, value]);
102
- }
103
-
104
- /**
105
- * Total elapsed time since construction, in milliseconds.
106
- *
107
- * @returns {number}
108
- */
109
- totalMs() {
110
- return performance.now() - this.#startedAt;
111
- }
112
-
113
- /**
114
- * All marks as `name=12.3ms` pairs, in order.
115
- *
116
- * @returns {string}
117
- */
118
- summary() {
119
- return this.#marks.map(([name, value]) => `${name}=${value.toFixed(1)}ms`).join(" ");
120
- }
121
- }
@@ -1,6 +0,0 @@
1
- export { Container } from "./Container.js";
2
- export { MatroskaContainer } from "./MatroskaContainer.js";
3
- export { Mp4Container } from "./Mp4Container.js";
4
- export { AviContainer } from "./AviContainer.js";
5
- export { SubtitleFileContainer, SUBTITLE_FILE_EXTENSIONS } from "./SubtitleFileContainer.js";
6
- export { ContainerFactory } from "./ContainerFactory.js";
@@ -1,2 +0,0 @@
1
- export { PlaybackController } from "./PlaybackController.js";
2
- export { SubtitleController } from "./SubtitleController.js";
@@ -1,8 +0,0 @@
1
- export { SwarmSelection } from "./SwarmSelection.js";
2
- export {
3
- anythingUrgentIsMissing,
4
- demandFor,
5
- forgetTorrent,
6
- liveDemand,
7
- reconcileAll
8
- } from "./registry.js";
@@ -1,2 +0,0 @@
1
- export { ContainerOrchestrator, containerOrchestrator } from "./ContainerOrchestrator.js";
2
- export { SubtitleOrchestrator, subtitleOrchestrator } from "./SubtitleOrchestrator.js";