@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,145 @@
1
+ /**
2
+ * @file One person watching, and everything that is true of them alone.
3
+ *
4
+ * A viewer is not a property of the material. What they are listening to, which
5
+ * quality step they have on screen, what is being prepared for them, where they
6
+ * are and what their link can carry — none of it changes a byte of what any
7
+ * encoder produces, and none of it belongs to a session, which is a description
8
+ * of an OUTPUT.
9
+ *
10
+ * It was six parallel maps hung on the session, each keyed by consumer id:
11
+ * `audioChoiceByConsumer`, `activeVariantByConsumer`,
12
+ * `warmingVariantByConsumer`, `warmingAudioByConsumer`, `consumerHeads`,
13
+ * `netReports`. Six places to remember to update and six to remember to forget,
14
+ * and the forgetting was already wrong — releasing a consumer emptied none of
15
+ * them, so a viewer who had left went on counting as wanting their soundtrack
16
+ * until their head expired, and their entries stayed for the life of the
17
+ * session.
18
+ *
19
+ * One object, one map, one thing to remove.
20
+ *
21
+ * **Nothing here knows about ffmpeg, the disk or the torrent.** A viewer states
22
+ * what they want and where they are; what to make of that is the orchestrator's
23
+ * question, and it reads a union of viewers rather than any one of them.
24
+ */
25
+
26
+ export class Viewer {
27
+ /**
28
+ * @param {string} id - The consumer id the browser sends with every request
29
+ * that means "this viewer".
30
+ */
31
+ constructor(id) {
32
+ this.id = String(id ?? "");
33
+ /**
34
+ * Which soundtrack this viewer is listening to and whether their browser
35
+ * needs it re-encoded. Theirs alone: two viewers of one picture may have
36
+ * chosen different languages, and one browser may decode a track another
37
+ * cannot.
38
+ * @type {{ trackIndex: number, transcode: boolean }}
39
+ */
40
+ this.audio = { trackIndex: 0, transcode: false };
41
+ /** The quality step on their screen. Null means the base session. @type {string | null} */
42
+ this.activeVariantId = null;
43
+ /** A step being prepared for a switch they have not made yet. @type {string | null} */
44
+ this.warmingVariantId = null;
45
+ /** A soundtrack being prepared for the same reason. @type {string | null} */
46
+ this.warmingAudioId = null;
47
+ /**
48
+ * Where they are: the segment they last asked for, or the position they
49
+ * stated by seeking. `seeked` holds the stated position for as long as they
50
+ * stay there, which is the distinction a cold open's soundtrack placement
51
+ * turns on.
52
+ * @type {{ segment: number, seconds: number, at: number, seeked?: number } | null}
53
+ */
54
+ this.head = null;
55
+ /** What their link was last measured to carry. @type {object | null} */
56
+ this.netReport = null;
57
+ /**
58
+ * Every output this viewer is watching, by session id: the picture, the
59
+ * quality step on their screen, the soundtrack they chose.
60
+ *
61
+ * WHY THERE ARE TWO SETS AND NOT ONE. There is one relation — this person
62
+ * watches this output — and it is asked from both ends. An output asks "has
63
+ * anybody left?", to decide whether to go on producing. A viewer who leaves
64
+ * asks "what was I watching?", so that each of those outputs can be told.
65
+ * Neither question can be answered from the other side without walking every
66
+ * session in the process, so the relation is indexed both ways. It is
67
+ * written in exactly one place — `Viewers.of` and `Viewers.leaves` write
68
+ * both directions together — which is what keeps two indexes of one relation
69
+ * from becoming two different answers.
70
+ *
71
+ * This is what replaced a film object. There is no "film" anywhere in this
72
+ * proxy — its parts are born at different times, die at different times and
73
+ * are addressed separately — and the three link fields that stood in for one
74
+ * could not say how many people were listening to a soundtrack.
75
+ *
76
+ * @type {Set<string>}
77
+ */
78
+ this.outputs = new Set();
79
+ }
80
+
81
+ /**
82
+ * Whether this viewer has been heard from recently enough to still be
83
+ * watching.
84
+ *
85
+ * Nothing releases a session when a channel closes, so without this a viewer
86
+ * whose tab is gone would hold a soundtrack or a quality step for the whole
87
+ * life of the session.
88
+ *
89
+ * @param {number} now
90
+ * @param {number} staleAfterMs
91
+ * @returns {boolean}
92
+ */
93
+ isLive(now, staleAfterMs) {
94
+ return this.head !== null && now - this.head.at <= staleAfterMs;
95
+ }
96
+
97
+ /**
98
+ * Where this viewer is, in seconds, or null when they have never said.
99
+ *
100
+ * @returns {number | null}
101
+ */
102
+ positionSeconds() {
103
+ if (this.head === null) {
104
+ return null;
105
+ }
106
+ return Number.isFinite(this.head.seeked) ? this.head.seeked : this.head.seconds;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * The viewers of one session, made on first use.
112
+ *
113
+ * @param {object} session
114
+ * @returns {Map<string, Viewer>}
115
+ */
116
+ export function viewersOf(session) {
117
+ if (!(session.viewers instanceof Map)) {
118
+ session.viewers = new Map();
119
+ }
120
+ return session.viewers;
121
+ }
122
+
123
+ /**
124
+ * The viewer with this id, made if this session has not met them before.
125
+ *
126
+ * Use `Viewers.of` instead wherever a registry is at hand: this makes ONE
127
+ * viewer per session, so the same person watching a picture, a quality step and
128
+ * a soundtrack is three objects, and `outputs` — a fact about the person — is
129
+ * then three sets that nothing keeps in step. It is kept for a session assembled
130
+ * by hand in a test that has no registry.
131
+ *
132
+ * @param {object} session
133
+ * @param {string} consumerId
134
+ * @returns {Viewer}
135
+ */
136
+ export function viewerOf(session, consumerId) {
137
+ const viewers = viewersOf(session);
138
+ let viewer = viewers.get(consumerId);
139
+ if (!viewer) {
140
+ viewer = new Viewer(consumerId);
141
+ viewers.set(consumerId, viewer);
142
+ }
143
+ viewer.outputs.add(session.id);
144
+ return viewer;
145
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @file Every viewer this proxy has met, one object per person.
3
+ *
4
+ * A viewer used to be made per SESSION: the same person watching a picture, a
5
+ * quality step and a soundtrack was three objects, each with its own copy of
6
+ * what that person had chosen and where they were. Two of those copies were
7
+ * always wrong, and the field that says which outputs a person is watching was
8
+ * worse than wrong — being per session, each copy could only ever hold the id
9
+ * of the session that owned it, so it carried no information at all and the one
10
+ * place that read it could learn nothing from it.
11
+ *
12
+ * One person is one object here, keyed by the consumer id the browser sends.
13
+ * That id is minted once per film opened in the page, so one id is one person
14
+ * watching one film, and sharing the object cannot conflate two films.
15
+ *
16
+ * The relation "this person watches this output" is indexed both ways — the
17
+ * output holds its viewers, the viewer holds its outputs — because it is asked
18
+ * from both ends, and both indexes are written here and nowhere else.
19
+ */
20
+
21
+ import { Viewer, viewersOf } from "./Viewer.js";
22
+
23
+ export class Viewers {
24
+ /**
25
+ * One object per named viewer. An unnamed one is not in here: a viewer that
26
+ * cannot say who it is is not the same viewer as another that cannot, so it
27
+ * belongs to the session that met it and to no one else.
28
+ *
29
+ * @type {Map<string, Viewer>}
30
+ */
31
+ #byId = new Map();
32
+
33
+ /**
34
+ * This viewer, watching this output.
35
+ *
36
+ * Both directions of the relation are written here. Asking for a viewer of an
37
+ * output IS the statement that they are watching it: every caller either
38
+ * records where they are, what they chose, or what is being prepared for
39
+ * them, and each of those is only true of somebody watching.
40
+ *
41
+ * @param {object} output - A session.
42
+ * @param {string} consumerId
43
+ * @returns {Viewer}
44
+ */
45
+ of(output, consumerId) {
46
+ const viewers = viewersOf(output);
47
+ const known = viewers.get(consumerId);
48
+ if (known) {
49
+ known.outputs.add(output.id);
50
+ return known;
51
+ }
52
+ const viewer = consumerId
53
+ ? this.#byId.get(consumerId) ?? new Viewer(consumerId)
54
+ : new Viewer("");
55
+ if (consumerId) {
56
+ this.#byId.set(consumerId, viewer);
57
+ }
58
+ viewers.set(consumerId, viewer);
59
+ viewer.outputs.add(output.id);
60
+ return viewer;
61
+ }
62
+
63
+ /**
64
+ * The viewer with this id, or null when nobody by that name is watching
65
+ * anything. Never makes one.
66
+ *
67
+ * @param {string} consumerId
68
+ * @returns {Viewer | null}
69
+ */
70
+ get(consumerId) {
71
+ return this.#byId.get(consumerId) ?? null;
72
+ }
73
+
74
+ /**
75
+ * The outputs this viewer is watching, as ids, copied so that leaving them
76
+ * can be walked without mutating what is being walked.
77
+ *
78
+ * @param {object} anyOutput - A session they are known to, for an unnamed
79
+ * viewer whose record lives on that session alone.
80
+ * @param {string} consumerId
81
+ * @returns {string[]}
82
+ */
83
+ watching(anyOutput, consumerId) {
84
+ const viewer = consumerId
85
+ ? this.#byId.get(consumerId)
86
+ : viewersOf(anyOutput).get("");
87
+ return viewer ? [...viewer.outputs] : [];
88
+ }
89
+
90
+ /**
91
+ * This viewer is no longer watching this output.
92
+ *
93
+ * Both directions again, and the viewer itself is forgotten once it is
94
+ * watching nothing — otherwise the registry would be a map that only grows,
95
+ * which is the shape of half the memory faults recorded in this repository.
96
+ *
97
+ * @param {object} output - A session.
98
+ * @param {string} consumerId
99
+ * @returns {boolean} Whether they were watching it.
100
+ */
101
+ leaves(output, consumerId) {
102
+ const viewers = viewersOf(output);
103
+ const viewer = viewers.get(consumerId);
104
+ if (!viewer) {
105
+ return false;
106
+ }
107
+ viewers.delete(consumerId);
108
+ viewer.outputs.delete(output.id);
109
+ if (consumerId && viewer.outputs.size === 0) {
110
+ this.#byId.delete(consumerId);
111
+ }
112
+ return true;
113
+ }
114
+
115
+ /**
116
+ * How many named viewers are watching anything. For the log line and for a
117
+ * check that the registry does not grow.
118
+ *
119
+ * @returns {number}
120
+ */
121
+ get size() {
122
+ return this.#byId.size;
123
+ }
124
+ }