@torrent-tv/proxy 2.83.8 → 2.85.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.
@@ -1,195 +1,202 @@
1
- /**
2
- * @file What a session PRODUCES, stated once and used as its identity.
3
- *
4
- * Three domain axes already have a home in this package: what a file states
5
- * about itself (`container/`), what a track states about itself (`tracks/`),
6
- * and what anybody wants off the swarm (`demand/`). This is the fourth — what
7
- * we make out of them — and it had no home at all: the identity of a session
8
- * was a `[...].join(":")` inside a 300-line function, with no name, no test and
9
- * nothing printing it.
10
- *
11
- * The rule it exists to express, stated by the user 2026-09-03:
12
- *
13
- * > A session belongs to the tracks its output actually carries. Its key holds
14
- * > those tracks' parameters and nothing else. Two sessions whose parameters
15
- * > agree ARE the same session, and the encoded result is reused by definition.
16
- *
17
- * So reuse between viewers is not a feature built on top of this — it is what a
18
- * correctly built key already means. A field of the REQUEST that does not
19
- * change one byte of the output must not appear here; measured 2026-09-03, two
20
- * viewers of one copied picture got two sessions whose output was identical
21
- * byte for byte (`research/two-viewers-one-file-2026-09-03.md`).
22
- *
23
- * What deliberately does NOT appear:
24
- *
25
- * 1. the viewer, in any form. Not the consumer id, not where they started, not
26
- * their viewport. A viewer is not a property of the material;
27
- * 2. the bitrate ceiling the viewer's measured link puts on a re-encode. It is
28
- * a runtime parameter of a SHARED session — the worst link among the live
29
- * consumers decides — so two viewers on one encode is the design, not a
30
- * collision. Rate control appears in neither the SPS nor the PPS, which is
31
- * why it can move under a player that has already cached the init;
32
- * 3. the video track number. Only `0:v:0` is ever mapped, so the file names the
33
- * picture. Add it here the day a second video track can be chosen.
34
- *
35
- * A limit worth stating: two specs that agree name interchangeable output
36
- * within ONE proxy. A copied picture's bytes depend only on the source, but a
37
- * re-encoded one's depend on this host's encoder, its preset and its rate cap —
38
- * so this is not enough to reuse segments BETWEEN proxies (roadmap item 41).
39
- */
40
-
41
- /**
42
- * The picture an output carries.
43
- */
44
- export class VideoOutput {
45
- /**
46
- * @param {object} params
47
- * @param {number} params.fileIndex - The file the picture is read from.
48
- * @param {{ width: number, height: number, manual: boolean } | null} params.encode
49
- * Null when the picture is copied — then the output is the source's own
50
- * size and nothing about a target box can change it. The box when it is
51
- * re-encoded, with `manual` saying the viewer forced it and the realtime
52
- * budget must not move it.
53
- */
54
- constructor({ fileIndex, encode = null }) {
55
- this.fileIndex = Number.isInteger(fileIndex) && fileIndex >= 0 ? fileIndex : 0;
56
- this.encode = encode
57
- ? {
58
- width: Number.isInteger(encode.width) && encode.width > 0 ? encode.width : 0,
59
- height: Number.isInteger(encode.height) && encode.height > 0 ? encode.height : 0,
60
- manual: encode.manual === true
61
- }
62
- : null;
63
- }
64
-
65
- /**
66
- * @returns {string}
67
- */
68
- toKey() {
69
- if (!this.encode) {
70
- return `v=${this.fileIndex}/copy`;
71
- }
72
- const box = `${this.encode.width}x${this.encode.height}`;
73
- return `v=${this.fileIndex}/enc:${box}:${this.encode.manual ? "manual" : "auto"}`;
74
- }
75
- }
76
-
77
- /**
78
- * The soundtrack an output carries.
79
- */
80
- export class AudioOutput {
81
- /**
82
- * @param {object} params
83
- * @param {number} params.fileIndex - The file the TRACK lives in, which for a
84
- * dub shipped beside the picture is not the picture's file.
85
- * @param {number} params.trackIndex - `0:a:N` inside that file. The flat
86
- * number the browser sends spans the picture's own tracks and the files
87
- * beside it, and two flat numbers of two different pictures can name one
88
- * track; this is the number that cannot.
89
- * @param {boolean} params.transcode - Re-encoded to AAC, or copied.
90
- */
91
- constructor({ fileIndex, trackIndex, transcode }) {
92
- this.fileIndex = Number.isInteger(fileIndex) && fileIndex >= 0 ? fileIndex : 0;
93
- this.trackIndex = Number.isInteger(trackIndex) && trackIndex >= 0 ? trackIndex : 0;
94
- this.transcode = transcode === true;
95
- }
96
-
97
- /**
98
- * @returns {string}
99
- */
100
- toKey() {
101
- return `a=${this.fileIndex}/${this.trackIndex}/${this.transcode ? "aac" : "copy"}`;
102
- }
103
- }
104
-
105
- /**
106
- * Where an output is cut.
107
- *
108
- * Both forms belong to a FILE and not to the session: the keyframe grid is that
109
- * file's own keyframe times, and the uniform grid is derived from that file's
110
- * duration. A soundtrack takes the grid of the picture it accompanies, so its
111
- * grid names the picture's file — which is what makes two soundtrack sessions
112
- * cut for two different pictures tell themselves apart.
113
- */
114
- export class CutGrid {
115
- /**
116
- * @param {object} params
117
- * @param {"keyframe" | "uniform"} params.kind
118
- * @param {number} params.fileIndex - Whose keyframes, or whose duration.
119
- */
120
- constructor({ kind, fileIndex }) {
121
- this.kind = kind === "keyframe" ? "keyframe" : "uniform";
122
- this.fileIndex = Number.isInteger(fileIndex) && fileIndex >= 0 ? fileIndex : 0;
123
- }
124
-
125
- /**
126
- * @returns {string}
127
- */
128
- toKey() {
129
- return `grid=${this.kind === "keyframe" ? "kf" : "even"}@${this.fileIndex}`;
130
- }
131
- }
132
-
133
- /**
134
- * One encode of one torrent's material: which tracks, in what form, cut how,
135
- * packaged how.
136
- */
137
- export class OutputSpec {
138
- /**
139
- * @param {object} params
140
- * @param {string} params.sourceKey - `torrent:<infohash>`, the canonical
141
- * identity of the torrent itself: a magnet and a `.torrent` file for the
142
- * same content produce the same one (`torrent-source-key.js`). Nothing
143
- * further is needed to say WHICH film this is.
144
- * @param {string} params.segmentFormatId - fMP4 or MPEG-TS. Two viewers
145
- * asking for different containers cannot share one ffmpeg.
146
- * @param {CutGrid} params.grid
147
- * @param {VideoOutput | null} params.video
148
- * @param {AudioOutput | null} params.audio
149
- */
150
- constructor({ sourceKey, segmentFormatId, grid, video = null, audio = null }) {
151
- this.sourceKey = String(sourceKey ?? "");
152
- this.segmentFormatId = String(segmentFormatId ?? "");
153
- this.grid = grid;
154
- this.video = video;
155
- this.audio = audio;
156
- }
157
-
158
- /**
159
- * What this output carries, in the vocabulary the rest of the class uses.
160
- *
161
- * `muxed` is the one case where a session legitimately holds the parameters
162
- * of two tracks: a browser that does not understand rendition groups must be
163
- * sent its sound inside the picture's own stream.
164
- *
165
- * @returns {"video-only" | "audio-only" | "muxed" | "empty"}
166
- */
167
- get carries() {
168
- if (this.video && this.audio) {
169
- return "muxed";
170
- }
171
- if (this.video) {
172
- return "video-only";
173
- }
174
- if (this.audio) {
175
- return "audio-only";
176
- }
177
- return "empty";
178
- }
179
-
180
- /**
181
- * The identity. Two outputs with the same one are the same output.
182
- *
183
- * @returns {string}
184
- */
185
- toKey() {
186
- const parts = [this.sourceKey, `fmt=${this.segmentFormatId}`, this.grid.toKey(), this.carries];
187
- if (this.video) {
188
- parts.push(this.video.toKey());
189
- }
190
- if (this.audio) {
191
- parts.push(this.audio.toKey());
192
- }
193
- return parts.join(":");
194
- }
195
- }
1
+ /**
2
+ * @file What a session PRODUCES, stated once and used as its identity.
3
+ *
4
+ * Three domain axes already have a home in this package: what a file states
5
+ * about itself (`container/`), what a track states about itself (`tracks/`),
6
+ * and what anybody wants off the swarm (`demand/`). This is the fourth — what
7
+ * we make out of them — and it had no home at all: the identity of a session
8
+ * was a `[...].join(":")` inside a 300-line function, with no name, no test and
9
+ * nothing printing it.
10
+ *
11
+ * The rule it exists to express, stated by the user 2026-09-03:
12
+ *
13
+ * > A session belongs to the tracks its output actually carries. Its key holds
14
+ * > those tracks' parameters and nothing else. Two sessions whose parameters
15
+ * > agree ARE the same session, and the encoded result is reused by definition.
16
+ *
17
+ * So reuse between viewers is not a feature built on top of this — it is what a
18
+ * correctly built key already means. A field of the REQUEST that does not
19
+ * change one byte of the output must not appear here; measured 2026-09-03, two
20
+ * viewers of one copied picture got two sessions whose output was identical
21
+ * byte for byte (`research/two-viewers-one-file-2026-09-03.md`).
22
+ *
23
+ * What deliberately does NOT appear:
24
+ *
25
+ * 1. the viewer, in any form. Not the consumer id, not where they started, not
26
+ * their viewport. A viewer is not a property of the material;
27
+ * 2. the bitrate ceiling the viewer's measured link puts on a re-encode. It is
28
+ * a runtime parameter of a SHARED session — the worst link among the live
29
+ * consumers decides — so two viewers on one encode is the design, not a
30
+ * collision. Rate control appears in neither the SPS nor the PPS, which is
31
+ * why it can move under a player that has already cached the init;
32
+ * 3. the video track number. Only `0:v:0` is ever mapped, so the file names the
33
+ * picture. Add it here the day a second video track can be chosen.
34
+ *
35
+ * A limit worth stating: two specs that agree name interchangeable output
36
+ * within ONE proxy. A copied picture's bytes depend only on the source, but a
37
+ * re-encoded one's depend on this host's encoder, its preset and its rate cap —
38
+ * so this is not enough to reuse segments BETWEEN proxies (roadmap item 41).
39
+ */
40
+
41
+ /**
42
+ * The picture an output carries.
43
+ */
44
+ export class VideoOutput {
45
+ /**
46
+ * @param {object} params
47
+ * @param {number} params.fileIndex - The file the picture is read from.
48
+ * @param {{ width: number, height: number, exactSize: boolean } | null} params.encode
49
+ * Null when the picture is copied — then the output is the source's own
50
+ * size and nothing about a target box can change it. The box when it is
51
+ * re-encoded, with `exactSize` saying that box is produced exactly as asked
52
+ * and the realtime budget must not move it.
53
+ *
54
+ * `exactSize` says NOTHING ABOUT WHO ASKED. Every rung of a master playlist
55
+ * sets it, whether the viewer picked that rung or the player moved itself,
56
+ * because two rungs allowed to drift would land on one height and the
57
+ * choice between them would mean nothing. It was called `manual` until
58
+ * 2026-09-13, and that name was read as "the viewer did this" four times
59
+ * over a few weeks — in field logs where the viewer had touched nothing.
60
+ */
61
+ constructor({ fileIndex, encode = null }) {
62
+ this.fileIndex = Number.isInteger(fileIndex) && fileIndex >= 0 ? fileIndex : 0;
63
+ this.encode = encode
64
+ ? {
65
+ width: Number.isInteger(encode.width) && encode.width > 0 ? encode.width : 0,
66
+ height: Number.isInteger(encode.height) && encode.height > 0 ? encode.height : 0,
67
+ exactSize: encode.exactSize === true
68
+ }
69
+ : null;
70
+ }
71
+
72
+ /**
73
+ * @returns {string}
74
+ */
75
+ toKey() {
76
+ if (!this.encode) {
77
+ return `v=${this.fileIndex}/copy`;
78
+ }
79
+ const box = `${this.encode.width}x${this.encode.height}`;
80
+ return `v=${this.fileIndex}/enc:${box}:${this.encode.exactSize ? "exact" : "budget"}`;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * The soundtrack an output carries.
86
+ */
87
+ export class AudioOutput {
88
+ /**
89
+ * @param {object} params
90
+ * @param {number} params.fileIndex - The file the TRACK lives in, which for a
91
+ * dub shipped beside the picture is not the picture's file.
92
+ * @param {number} params.trackIndex - `0:a:N` inside that file. The flat
93
+ * number the browser sends spans the picture's own tracks and the files
94
+ * beside it, and two flat numbers of two different pictures can name one
95
+ * track; this is the number that cannot.
96
+ * @param {boolean} params.transcode - Re-encoded to AAC, or copied.
97
+ */
98
+ constructor({ fileIndex, trackIndex, transcode }) {
99
+ this.fileIndex = Number.isInteger(fileIndex) && fileIndex >= 0 ? fileIndex : 0;
100
+ this.trackIndex = Number.isInteger(trackIndex) && trackIndex >= 0 ? trackIndex : 0;
101
+ this.transcode = transcode === true;
102
+ }
103
+
104
+ /**
105
+ * @returns {string}
106
+ */
107
+ toKey() {
108
+ return `a=${this.fileIndex}/${this.trackIndex}/${this.transcode ? "aac" : "copy"}`;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Where an output is cut.
114
+ *
115
+ * Both forms belong to a FILE and not to the session: the keyframe grid is that
116
+ * file's own keyframe times, and the uniform grid is derived from that file's
117
+ * duration. A soundtrack takes the grid of the picture it accompanies, so its
118
+ * grid names the picture's file — which is what makes two soundtrack sessions
119
+ * cut for two different pictures tell themselves apart.
120
+ */
121
+ export class CutGrid {
122
+ /**
123
+ * @param {object} params
124
+ * @param {"keyframe" | "uniform"} params.kind
125
+ * @param {number} params.fileIndex - Whose keyframes, or whose duration.
126
+ */
127
+ constructor({ kind, fileIndex }) {
128
+ this.kind = kind === "keyframe" ? "keyframe" : "uniform";
129
+ this.fileIndex = Number.isInteger(fileIndex) && fileIndex >= 0 ? fileIndex : 0;
130
+ }
131
+
132
+ /**
133
+ * @returns {string}
134
+ */
135
+ toKey() {
136
+ return `grid=${this.kind === "keyframe" ? "kf" : "even"}@${this.fileIndex}`;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * One encode of one torrent's material: which tracks, in what form, cut how,
142
+ * packaged how.
143
+ */
144
+ export class OutputSpec {
145
+ /**
146
+ * @param {object} params
147
+ * @param {string} params.sourceKey - `torrent:<infohash>`, the canonical
148
+ * identity of the torrent itself: a magnet and a `.torrent` file for the
149
+ * same content produce the same one (`torrent-source-key.js`). Nothing
150
+ * further is needed to say WHICH film this is.
151
+ * @param {string} params.segmentFormatId - fMP4 or MPEG-TS. Two viewers
152
+ * asking for different containers cannot share one ffmpeg.
153
+ * @param {CutGrid} params.grid
154
+ * @param {VideoOutput | null} params.video
155
+ * @param {AudioOutput | null} params.audio
156
+ */
157
+ constructor({ sourceKey, segmentFormatId, grid, video = null, audio = null }) {
158
+ this.sourceKey = String(sourceKey ?? "");
159
+ this.segmentFormatId = String(segmentFormatId ?? "");
160
+ this.grid = grid;
161
+ this.video = video;
162
+ this.audio = audio;
163
+ }
164
+
165
+ /**
166
+ * What this output carries, in the vocabulary the rest of the class uses.
167
+ *
168
+ * `muxed` is the one case where a session legitimately holds the parameters
169
+ * of two tracks: a browser that does not understand rendition groups must be
170
+ * sent its sound inside the picture's own stream.
171
+ *
172
+ * @returns {"video-only" | "audio-only" | "muxed" | "empty"}
173
+ */
174
+ get carries() {
175
+ if (this.video && this.audio) {
176
+ return "muxed";
177
+ }
178
+ if (this.video) {
179
+ return "video-only";
180
+ }
181
+ if (this.audio) {
182
+ return "audio-only";
183
+ }
184
+ return "empty";
185
+ }
186
+
187
+ /**
188
+ * The identity. Two outputs with the same one are the same output.
189
+ *
190
+ * @returns {string}
191
+ */
192
+ toKey() {
193
+ const parts = [this.sourceKey, `fmt=${this.segmentFormatId}`, this.grid.toKey(), this.carries];
194
+ if (this.video) {
195
+ parts.push(this.video.toKey());
196
+ }
197
+ if (this.audio) {
198
+ parts.push(this.audio.toKey());
199
+ }
200
+ return parts.join(":");
201
+ }
202
+ }