@torrent-tv/proxy 2.80.5 → 2.80.6

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.
@@ -44,165 +44,319 @@
44
44
  */
45
45
 
46
46
  /**
47
- * How urgently a stretch of film is wanted. Higher is sooner.
47
+ * THE MAP ITSELF: one number per second of film.
48
+ *
49
+ * Literally an array as long as the film, where the value at second `x` says how
50
+ * urgently that second is wanted. Stated by the user 2026-09-06, and it replaced
51
+ * a list of stretches each carrying two numbers.
52
+ *
53
+ * Two arrays rather than one, because two different questions are asked of the
54
+ * map and only one of them can be answered by a rank:
55
+ *
56
+ * - `priority` orders the work. Only the ORDER between values is meaningful;
57
+ * the scale below says what the numbers separate;
58
+ * - `secondsUntilPlayed` is a quantity, and it is what makes lateness
59
+ * computable at all: whoever fills the map compares "when is this needed"
60
+ * against "when would it arrive", and only the second half is theirs to work
61
+ * out. `Infinity` where nobody is on their way.
62
+ *
63
+ * @typedef {object} PriorityMap
64
+ * @property {number} durationSeconds - The film's length, in whole seconds.
65
+ * @property {Uint8Array} priority - One entry per second. Zero means nothing is
66
+ * wanted there, which happens only where nobody is watching at all.
67
+ * @property {Float64Array} secondsUntilPlayed - One entry per second.
68
+ * @property {Uint8Array} behind - One entry per second, 1 where the second lies
69
+ * behind EVERY viewer. Stated rather than worked out from the other two,
70
+ * because working it out means knowing this file's scale, and the consumers
71
+ * are in other layers. Inferred from the absence of a time, as it was, it was
72
+ * wrong for a stopped viewer: their whole film carries no time, so the film in
73
+ * front of them counted as behind them and the encoders wandered to the start
74
+ * of the file.
75
+ */
76
+
77
+ /**
78
+ * A stretch of the map with one value throughout — the same facts said as a
79
+ * range instead of as a run of equal entries.
48
80
  *
49
81
  * @typedef {object} DemandZone
50
- * @property {number} from - First second of film, inclusive.
51
- * @property {number} to - Last second of film, inclusive.
52
- * @property {number} priority - Higher is more urgent. Only the ORDER between
53
- * zones is meaningful; the numbers themselves are not a scale.
82
+ * @property {number} from - First second, inclusive.
83
+ * @property {number} to - Last second, exclusive.
84
+ * @property {number} priority
85
+ * @property {number} withinSeconds
86
+ * @property {boolean} behind - Whether this lies behind every viewer.
54
87
  */
55
88
 
56
89
  /**
57
- * The number a stretch gets when nobody is heading towards it.
90
+ * THE SCALE. Three bands, and the order between them is the whole of what the
91
+ * numbers claim.
92
+ *
93
+ * 1. IN FRONT OF SOMEBODY WHO IS WATCHING — the top value at the second they
94
+ * are about to see, falling as the film gets further from them;
95
+ * 2. IN FRONT OF SOMEBODY WHO HAS STOPPED — the same shape, and every value of
96
+ * it below every value of band 1. A stopped viewer will watch this film when
97
+ * they press play, so it is still in front of them; nobody is waiting for it
98
+ * now, so it yields to anybody who is;
99
+ * 3. BEHIND EVERYBODY — reachable only by a seek back, and nothing measures how
100
+ * likely that is, so it is one value rather than a shape.
58
101
  *
59
- * Behind a viewer, and everywhere for a viewer who has stopped the picture: the
60
- * film is still wanted a seek back must be cheap but nobody is on their way
61
- * there, so it yields to everything anybody is approaching.
102
+ * IN FRONT ALWAYS OUTRANKS BEHIND, at any distance and whoever is stopped. The
103
+ * bands make that true by construction rather than by a comparison somebody has
104
+ * to remember to write.
105
+ *
106
+ * The values inside a band are AN ENCODING OF AN ORDER, not a measurement:
107
+ * every consumer asks only which of two seconds comes first, so any strictly
108
+ * decreasing shape does the same work, and none of these numbers claims
109
+ * anything about the world.
62
110
  */
111
+ export const AT_A_WATCHING_VIEWER = 100;
112
+ const WATCHING_FLOOR = 70;
113
+ const AT_A_STOPPED_VIEWER = 69;
114
+ const STOPPED_FLOOR = 39;
115
+
116
+ /** Behind everybody. */
63
117
  export const NOBODY_IS_COMING = 1;
64
118
 
119
+ /** Nothing is wanted here at all. */
120
+ const NOTHING = 0;
121
+
122
+ /**
123
+ * Does this value mean "behind every viewer"?
124
+ *
125
+ * A read of the scale, so that nobody has to infer the side from the absence of
126
+ * a deadline. Inferred that way it was wrong for a stopped viewer, whose whole
127
+ * film carries no deadline: the film in front of them then counted as behind
128
+ * them, and the encoders were free to wander to the start of the file.
129
+ *
130
+ * @param {number} priority
131
+ * @returns {boolean}
132
+ */
133
+ export function isBehindEverybody(priority) {
134
+ return priority <= NOBODY_IS_COMING;
135
+ }
136
+
137
+ /**
138
+ * Is this second the one a viewer is about to watch?
139
+ *
140
+ * The top step of the watching band: the film under their feet and the measured
141
+ * allowance in front of it, which is the depth below which an interruption
142
+ * reaches them.
143
+ *
144
+ * @param {number} priority
145
+ * @returns {boolean}
146
+ */
147
+ export function isAtAWatchingViewer(priority) {
148
+ return priority >= AT_A_WATCHING_VIEWER;
149
+ }
150
+
151
+ /**
152
+ * Is this second in front of somebody who has STOPPED, and of nobody who is
153
+ * watching?
154
+ *
155
+ * Wanted, and wanted after everything anybody is walking towards: they will see
156
+ * it when they press play, and nothing here knows when that is.
157
+ *
158
+ * @param {number} priority
159
+ * @returns {boolean}
160
+ */
161
+ export function isNobodyComingNow(priority) {
162
+ return priority > NOBODY_IS_COMING && priority <= AT_A_STOPPED_VIEWER;
163
+ }
164
+
165
+ /**
166
+ * How far into its band a second sits, given its distance from the viewer.
167
+ *
168
+ * The step doubles: near the viewer the difference between now and ten seconds
169
+ * away decides what is made first, while twenty minutes out the difference
170
+ * between twenty and twenty-one minutes changes nothing. The first step is the
171
+ * MEASURED allowance for this file on this swarm — the depth below which an
172
+ * interruption reaches the viewer — so how finely the map divides comes from a
173
+ * measurement rather than from a number chosen here.
174
+ *
175
+ * @param {number} distanceSeconds
176
+ * @param {number} allowanceSeconds
177
+ * @returns {number} Zero at the viewer, growing with distance.
178
+ */
179
+ function stepsAway(distanceSeconds, allowanceSeconds) {
180
+ if (!(distanceSeconds > 0)) {
181
+ return 0;
182
+ }
183
+ return Math.floor(Math.log2(1 + distanceSeconds / allowanceSeconds));
184
+ }
185
+
65
186
  /**
66
- * The number the second a viewer is about to watch gets. Everything else ahead
67
- * of them counts down from here.
187
+ * An empty map of the right length: nothing wanted anywhere.
68
188
  *
69
- * One scale for every viewer, because the maps are merged by taking the highest
70
- * number per second: two viewers must be comparable, and they are, because the
71
- * number depends only on how far each of them still has to travel.
189
+ * @param {number} durationSeconds
190
+ * @returns {PriorityMap}
72
191
  */
73
- export const AT_THE_VIEWER = 32;
192
+ export function emptyMap(durationSeconds) {
193
+ const seconds = Number.isFinite(durationSeconds) && durationSeconds > 0
194
+ ? Math.ceil(durationSeconds)
195
+ : 0;
196
+ return {
197
+ durationSeconds: seconds,
198
+ priority: new Uint8Array(seconds),
199
+ secondsUntilPlayed: new Float64Array(seconds).fill(Number.POSITIVE_INFINITY),
200
+ behind: new Uint8Array(seconds).fill(1)
201
+ };
202
+ }
74
203
 
75
204
  /**
76
- * One viewer's map: seconds of film against a number, and nothing else.
205
+ * One viewer's map.
77
206
  *
78
- * **The number is a reading of how soon they will be there.** A viewer moving
79
- * forward reaches the second `x` after `x - p` seconds of film. That distance —
80
- * not a clock time, not a deadline — is what the number is derived from, and it
81
- * is why two viewers can be compared at all: the nearer one wins the second
82
- * they both want.
207
+ * **The number is a reading of how soon they will be there.** A viewer watching
208
+ * forward reaches second `x` after `x - p` seconds of film. That distance — not
209
+ * a clock time — is what the value is derived from, and it is why two viewers
210
+ * can be compared at all: the nearer one wins the second they both want.
83
211
  *
84
- * **The bands widen as they go.** Near the viewer the difference between now
85
- * and ten seconds away decides what is made first; twenty minutes out, the
86
- * difference between twenty and twenty-one changes nothing. So the first band
87
- * is the measured allowance the depth below which an interruption reaches
88
- * this viewer — and each next band is twice the last. A film of any length is
89
- * then described by a handful of bands, fine where it matters.
212
+ * **A pause removes the time, not the direction.** A stopped viewer is still
213
+ * standing somewhere, and the film in front of them is still the film they will
214
+ * watch, so their map keeps its shape and moves into the band below. Collapsed
215
+ * to one flat value over the whole film, as it was, their position disappeared
216
+ * entirely — and with it the rule that what is in front is made first.
90
217
  *
91
218
  * **What must NOT be here**, and the boundary is the point: how fast this
92
219
  * machine encodes, how many encoders that takes, what a second weighs in bytes,
93
- * where a piece boundary falls. Those are answers the encoding and the
94
- * downloading work out for themselves, from this map and from what each knows
95
- * about itself.
220
+ * where a piece boundary falls, and what has already been made. Those are
221
+ * answers the encoding and the downloading work out for themselves, from this
222
+ * map and from what each knows about itself.
96
223
  *
97
224
  * @param {object} params
98
225
  * @param {number} params.atSeconds - Where they are watching from.
99
226
  * @param {number} params.durationSeconds - How long the film is.
100
227
  * @param {number} params.allowanceSeconds - The measured depth below which an
101
- * interruption reaches this viewer. The width of the first band.
102
- * @param {boolean} [params.playing] - Whether the picture is moving. A viewer
103
- * who has stopped it is going nowhere, so nothing is nearer to them than
104
- * anything else.
105
- * @returns {DemandZone[]} Ascending by position, without gaps or overlaps.
228
+ * interruption reaches this viewer. The width of the first step.
229
+ * @param {boolean} [params.playing] - Whether the picture is moving.
230
+ * @returns {PriorityMap}
106
231
  */
107
232
  export function mapForViewer({ atSeconds, durationSeconds, allowanceSeconds, playing = true }) {
108
- const from = Number.isFinite(atSeconds) && atSeconds > 0 ? atSeconds : 0;
109
- const end = Number.isFinite(durationSeconds) ? durationSeconds : 0;
110
- if (!(end > 0)) {
111
- return [];
112
- }
113
- const allowance = Number.isFinite(allowanceSeconds) && allowanceSeconds > 0 ? allowanceSeconds : 0;
114
- if (!playing || allowance <= 0) {
115
- // Nobody is on their way anywhere: the film is wanted and nothing in it is
116
- // wanted sooner than the rest.
117
- return [{ from: 0, to: end, priority: NOBODY_IS_COMING }];
118
- }
119
-
120
- /** @type {DemandZone[]} */
121
- const zones = [];
122
- if (from > 0) {
123
- zones.push({ from: 0, to: from, priority: NOBODY_IS_COMING });
124
- }
125
- let at = from;
126
- let width = allowance;
127
- let priority = AT_THE_VIEWER;
128
- while (at < end && priority > NOBODY_IS_COMING + 1) {
129
- const to = Math.min(end, at + width);
130
- zones.push({ from: at, to, priority });
131
- at = to;
132
- width *= 2;
133
- priority -= 1;
233
+ const map = emptyMap(durationSeconds);
234
+ if (map.durationSeconds === 0) {
235
+ return map;
134
236
  }
135
- if (at < end) {
136
- // Everything left is equally far off: at this distance one more band would
137
- // not change any decision.
138
- zones.push({ from: at, to: end, priority: NOBODY_IS_COMING + 1 });
237
+ const at = Number.isFinite(atSeconds) && atSeconds > 0 ? Math.floor(atSeconds) : 0;
238
+ const allowance = Number.isFinite(allowanceSeconds) && allowanceSeconds > 0
239
+ ? allowanceSeconds
240
+ : 1;
241
+ const watching = playing !== false;
242
+ const top = watching ? AT_A_WATCHING_VIEWER : AT_A_STOPPED_VIEWER;
243
+ const floor = watching ? WATCHING_FLOOR : STOPPED_FLOOR;
244
+ for (let second = 0; second < map.durationSeconds; second += 1) {
245
+ if (second < at) {
246
+ map.priority[second] = NOBODY_IS_COMING;
247
+ continue;
248
+ }
249
+ map.behind[second] = 0;
250
+ const distance = second - at;
251
+ map.priority[second] = Math.max(floor, top - stepsAway(distance, allowance));
252
+ // A viewer who has stopped is on their way nowhere, so there is no second by
253
+ // which any of this must exist. The direction survives the pause, in the
254
+ // priority above; the time does not, because there is none to state.
255
+ map.secondsUntilPlayed[second] = watching ? distance : Number.POSITIVE_INFINITY;
139
256
  }
140
- return zones;
257
+ return map;
141
258
  }
142
259
 
143
260
  /**
144
261
  * Every viewer's map as one.
145
262
  *
146
- * The highest priority per second wins: film two people want is as urgent as
147
- * the more urgent of them, and making it once serves both. What comes back has
148
- * no overlaps, so the filling can walk it without asking about any individual
149
- * viewer which is the rule this layer exists to keep, that which viewer asked
150
- * never reaches the encoders.
263
+ * The highest priority per second wins, and the soonest time: film two people
264
+ * want is as urgent as the more urgent of them, and making it once serves both.
265
+ * The two are taken separately on purpose the priority is coarse, so two
266
+ * viewers can tie on it while one is genuinely nearer, and whoever schedules
267
+ * against the time must be given the nearer one.
151
268
  *
152
- * @param {DemandZone[][]} maps
153
- * @returns {DemandZone[]} Ascending by position.
269
+ * In front of ANYBODY is in front, and the third array says so directly.
270
+ *
271
+ * @param {PriorityMap[]} maps
272
+ * @returns {PriorityMap}
154
273
  */
155
274
  export function mergeMaps(maps) {
156
- /** @type {DemandZone[]} */
157
- const all = [];
158
- for (const map of maps ?? []) {
159
- for (const zone of map ?? []) {
160
- if (Number.isFinite(zone?.from) && Number.isFinite(zone?.to) && zone.to > zone.from) {
161
- all.push(zone);
162
- }
163
- }
164
- }
275
+ const all = (maps ?? []).filter((map) => map && map.durationSeconds > 0);
165
276
  if (all.length === 0) {
166
- return [];
277
+ return emptyMap(0);
167
278
  }
168
- // Walked by BOUNDARIES rather than by second: a film is thousands of them and
169
- // this is asked again on every change.
170
- const points = [...new Set(all.flatMap((zone) => [zone.from, zone.to]))].sort(
171
- (left, right) => left - right
172
- );
173
- /** @type {DemandZone[]} */
174
- const merged = [];
175
- for (let index = 0; index < points.length - 1; index += 1) {
176
- const from = points[index];
177
- const to = points[index + 1];
178
- let priority = 0;
179
- for (const zone of all) {
180
- if (zone.from <= from && to <= zone.to && zone.priority > priority) {
181
- priority = zone.priority;
279
+ const merged = emptyMap(Math.max(...all.map((map) => map.durationSeconds)));
280
+ for (const map of all) {
281
+ for (let second = 0; second < map.durationSeconds; second += 1) {
282
+ if (map.priority[second] > merged.priority[second]) {
283
+ merged.priority[second] = map.priority[second];
284
+ }
285
+ if (map.secondsUntilPlayed[second] < merged.secondsUntilPlayed[second]) {
286
+ merged.secondsUntilPlayed[second] = map.secondsUntilPlayed[second];
287
+ }
288
+ // In front of ANYBODY is in front: a stretch one viewer has passed is
289
+ // still the film another is walking towards.
290
+ if (map.behind[second] === 0) {
291
+ merged.behind[second] = 0;
182
292
  }
183
293
  }
184
- if (priority <= 0) {
294
+ }
295
+ return merged;
296
+ }
297
+
298
+ /**
299
+ * The map as stretches: runs of seconds that agree on both numbers.
300
+ *
301
+ * A VIEW of the map, never a second copy of it. Consumers that work in ranges —
302
+ * the swarm is asked for byte ranges, an encoder is given a stretch of pieces —
303
+ * would otherwise each write this walk for themselves.
304
+ *
305
+ * @param {PriorityMap} map
306
+ * @returns {DemandZone[]} Ascending, without gaps or overlaps. Seconds nobody
307
+ * wants are left out.
308
+ */
309
+ export function runsOf(map) {
310
+ /** @type {DemandZone[]} */
311
+ const runs = [];
312
+ if (!map || !(map.durationSeconds > 0)) {
313
+ return runs;
314
+ }
315
+ for (let second = 0; second < map.durationSeconds; second += 1) {
316
+ const priority = map.priority[second];
317
+ if (priority === NOTHING) {
185
318
  continue;
186
319
  }
187
- const previous = merged[merged.length - 1];
188
- if (previous && previous.priority === priority && previous.to === from) {
189
- previous.to = to;
320
+ const behind = map.behind[second] === 1;
321
+ const previous = runs[runs.length - 1];
322
+ if (
323
+ previous
324
+ && previous.to === second
325
+ && previous.priority === priority
326
+ && previous.behind === behind
327
+ ) {
328
+ previous.to = second + 1;
190
329
  continue;
191
330
  }
192
- merged.push({ from, to, priority });
331
+ // THE TIME OF A STRETCH IS THE TIME OF ITS NEAR EDGE. A stretch is met at
332
+ // its beginning, so its beginning is when it must exist; whoever needs the
333
+ // time of a second inside it walks forward from there, at a second of film
334
+ // per second, which is the same arithmetic that put the number here.
335
+ //
336
+ // Merging on the exact time as well would merge nothing at all: the time is
337
+ // the distance, so it differs at every second, and the map came back as one
338
+ // stretch per second of film.
339
+ const withinSeconds = map.secondsUntilPlayed[second];
340
+ runs.push({
341
+ from: second,
342
+ to: second + 1,
343
+ priority,
344
+ withinSeconds,
345
+ behind
346
+ });
193
347
  }
194
- return merged;
348
+ return runs;
195
349
  }
196
350
 
197
351
  /**
198
- * The merged map in the order the work is taken: most urgent first, and within
199
- * one priority the earliest film first — that is where somebody is stopped.
352
+ * The map in the order the work is taken: most urgent first, and within one
353
+ * priority the earliest film first — that is where somebody is stopped.
200
354
  *
201
- * @param {DemandZone[]} merged
355
+ * @param {DemandZone[]} runs
202
356
  * @returns {DemandZone[]}
203
357
  */
204
- export function inWorkingOrder(merged) {
205
- return [...(merged ?? [])].sort(
358
+ export function inWorkingOrder(runs) {
359
+ return [...(runs ?? [])].sort(
206
360
  (left, right) => right.priority - left.priority || left.from - right.from
207
361
  );
208
362
  }
@@ -16,7 +16,7 @@
16
16
  * append, and an empty picture for six minutes (field 2026-09-05).
17
17
  */
18
18
 
19
- import { mapForViewer, mergeMaps } from "./PriorityMap.js";
19
+ import { emptyMap, mapForViewer, mergeMaps, runsOf } from "./PriorityMap.js";
20
20
 
21
21
  export class PriorityOrchestrator {
22
22
  /** Where the map goes once it is built. @type {(published: object) => void} */
@@ -25,6 +25,10 @@ export class PriorityOrchestrator {
25
25
  /** The last map published per film and file, so an unchanged one is not resent. */
26
26
  #last = new Map();
27
27
 
28
+ /** The last map BUILT per film and file, for whoever reads instead of being
29
+ * handed it. @type {Map<string, import("./PriorityMap.js").PriorityMap>} */
30
+ #maps = new Map();
31
+
28
32
  /** Who is watching one session. @type {(session: object) => Map<string, object>} */
29
33
  #viewersOf;
30
34
 
@@ -58,11 +62,11 @@ export class PriorityOrchestrator {
58
62
  * @param {number} params.allowanceSeconds - The measured depth below which an
59
63
  * interruption reaches a viewer of this file.
60
64
  * @param {{ atSeconds: number, playing: boolean }[]} params.viewers
61
- * @returns {{ from: number, to: number, priority: number }[]} Seconds of film
62
- * against a number, merged over everyone.
65
+ * @returns {import("./PriorityMap.js").PriorityMap} One number per second of
66
+ * film, merged over everyone watching it.
63
67
  */
64
68
  build({ sourceKey, fileIndex, durationSeconds, allowanceSeconds, viewers }) {
65
- const zones = mergeMaps(
69
+ const map = mergeMaps(
66
70
  (viewers ?? []).map((viewer) =>
67
71
  mapForViewer({
68
72
  atSeconds: viewer.atSeconds,
@@ -73,15 +77,18 @@ export class PriorityOrchestrator {
73
77
  )
74
78
  );
75
79
  const key = `${sourceKey}:${fileIndex}`;
80
+ this.#maps.set(key, map);
76
81
  // Unchanged maps are not republished: the downloading rebuilds what it asks
77
82
  // the swarm for on every one, and a viewer sitting still would otherwise
78
- // make it do that several times a second.
83
+ // make it do that several times a second. Compared as stretches rather than
84
+ // second by second, which is the same comparison over far fewer values.
85
+ const zones = runsOf(map);
79
86
  const shape = JSON.stringify(zones);
80
87
  if (this.#last.get(key) !== shape) {
81
88
  this.#last.set(key, shape);
82
89
  this.#publish({ sourceKey, fileIndex, durationSeconds, zones });
83
90
  }
84
- return zones;
91
+ return map;
85
92
  }
86
93
 
87
94
  /**
@@ -143,7 +150,25 @@ export class PriorityOrchestrator {
143
150
  * @param {string} sourceKey
144
151
  * @param {number} fileIndex
145
152
  */
153
+ /**
154
+ * The map this class last built for one file.
155
+ *
156
+ * Read by whoever acts on it and cannot be handed it at the moment it is
157
+ * made — the encoding decides per output, and one file has several. It is the
158
+ * SAME map: built once here, from where the viewers are, and neither read
159
+ * changes it.
160
+ *
161
+ * @param {string} sourceKey
162
+ * @param {number} fileIndex
163
+ * @returns {import("./PriorityMap.js").PriorityMap} A map of no length where
164
+ * none was built, which says the same as a map with nothing in it.
165
+ */
166
+ mapFor(sourceKey, fileIndex) {
167
+ return this.#maps.get(`${sourceKey}:${fileIndex}`) ?? emptyMap(0);
168
+ }
169
+
146
170
  forget(sourceKey, fileIndex) {
147
171
  this.#last.delete(`${sourceKey}:${fileIndex}`);
172
+ this.#maps.delete(`${sourceKey}:${fileIndex}`);
148
173
  }
149
174
  }