@torrent-tv/proxy 2.80.0 → 2.80.2
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.
- package/CHANGELOG.md +1576 -1558
- package/package.json +1 -1
- package/server.js +16 -0
- package/services/demand/DemandRegister.js +20 -0
- package/services/download/SwarmSelection.js +4 -1
- package/services/encode/EncodePlan.js +159 -31
- package/services/encode/SegmentStore.js +29 -0
- package/services/encode/run-budget.js +8 -0
- package/services/hls-session-manager.js +25 -8
- package/services/orchestrators/EncodeOrchestrator.js +27 -5
- package/services/piece-store/piece-lru.js +91 -31
- package/services/piece-store/shared-piece-store.js +1531 -1512
- package/services/priority/PriorityMap.js +57 -101
- package/services/priority/PriorityOrchestrator.js +149 -0
- package/services/torrent-pool.js +104 -0
- package/services/torrent-worker/client.js +20 -0
- package/services/torrent-worker/piece-reader.js +57 -217
- package/services/torrent-worker/pool-adapter.js +17 -0
- package/services/torrent-worker/protocol.js +10 -0
- package/services/torrent-worker/worker.js +11 -0
- package/test/demand-register.test.js +11 -0
- package/test/encode-plan.test.js +65 -0
- package/test/piece-lru.test.js +61 -0
- package/test/piece-store-eviction.test.js +30 -0
- package/test/priority-map-download.test.js +156 -0
- package/test/priority-map.test.js +50 -77
- package/test/read-window.test.js +46 -29
- package/test/read-bands.test.js +0 -140
|
@@ -53,133 +53,89 @@
|
|
|
53
53
|
* zones is meaningful; the numbers themselves are not a scale.
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
|
-
/** Where the viewer stands. Nothing outranks it. */
|
|
57
|
-
const AT_THE_VIEWER = 33;
|
|
58
|
-
|
|
59
|
-
/** In front of them, within reach while they watch what is already made. */
|
|
60
|
-
const IN_FRONT = 32;
|
|
61
|
-
|
|
62
|
-
/** The rest of the track: wanted, because the file is encoded whole. */
|
|
63
|
-
const THE_REST = 1;
|
|
64
|
-
|
|
65
56
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* not a policy about how many encoders may run (the machine's budget answers
|
|
72
|
-
* that, and it is far smaller); it stops a speed measured at almost zero from
|
|
73
|
-
* turning a film into thousands of slivers before anything reads the map.
|
|
57
|
+
* The number a stretch gets when nobody is heading towards it.
|
|
58
|
+
*
|
|
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.
|
|
74
62
|
*/
|
|
75
|
-
const
|
|
63
|
+
export const NOBODY_IS_COMING = 1;
|
|
76
64
|
|
|
77
65
|
/**
|
|
78
|
-
*
|
|
66
|
+
* The number the second a viewer is about to watch gets. Everything else ahead
|
|
67
|
+
* of them counts down from here.
|
|
79
68
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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.
|
|
72
|
+
*/
|
|
73
|
+
export const AT_THE_VIEWER = 32;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* One viewer's map: seconds of film against a number, and nothing else.
|
|
77
|
+
*
|
|
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.
|
|
83
|
+
*
|
|
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.
|
|
90
|
+
*
|
|
91
|
+
* **What must NOT be here**, and the boundary is the point: how fast this
|
|
92
|
+
* 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.
|
|
95
96
|
*
|
|
96
97
|
* @param {object} params
|
|
97
98
|
* @param {number} params.atSeconds - Where they are watching from.
|
|
98
99
|
* @param {number} params.durationSeconds - How long the film is.
|
|
99
100
|
* @param {number} params.allowanceSeconds - The measured depth below which an
|
|
100
|
-
* interruption reaches this viewer
|
|
101
|
-
* @param {
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* @returns {DemandZone[]} Ascending, without gaps or overlaps
|
|
105
|
-
* everything from where they are to the end of the film.
|
|
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.
|
|
106
106
|
*/
|
|
107
|
-
export function mapForViewer({ atSeconds, durationSeconds, allowanceSeconds,
|
|
107
|
+
export function mapForViewer({ atSeconds, durationSeconds, allowanceSeconds, playing = true }) {
|
|
108
108
|
const from = Number.isFinite(atSeconds) && atSeconds > 0 ? atSeconds : 0;
|
|
109
109
|
const end = Number.isFinite(durationSeconds) ? durationSeconds : 0;
|
|
110
|
-
if (!(end >
|
|
110
|
+
if (!(end > 0)) {
|
|
111
111
|
return [];
|
|
112
112
|
}
|
|
113
113
|
const allowance = Number.isFinite(allowanceSeconds) && allowanceSeconds > 0 ? allowanceSeconds : 0;
|
|
114
|
-
|
|
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
|
+
}
|
|
115
119
|
|
|
116
120
|
/** @type {DemandZone[]} */
|
|
117
121
|
const zones = [];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
// From the condition below with `s >= 1`: the encoder gains on the viewer at
|
|
122
|
-
// every point, so there is no distance at which they catch it. All that has
|
|
123
|
-
// to exist in front of them is the allowance this file's own interruptions
|
|
124
|
-
// have shown to be necessary.
|
|
125
|
-
if (speed === 0 || speed >= 1) {
|
|
126
|
-
const readyBy = Math.min(end, from + allowance);
|
|
127
|
-
zones.push({ from, to: readyBy, priority: AT_THE_VIEWER });
|
|
128
|
-
if (readyBy < end && speed > 0) {
|
|
129
|
-
// While they watch what the first zone holds, the encoder makes `speed`
|
|
130
|
-
// times as much again. Beyond that nobody is waiting yet.
|
|
131
|
-
const reach = Math.min(end, readyBy + (readyBy - from) * speed);
|
|
132
|
-
if (reach > readyBy) {
|
|
133
|
-
zones.push({ from: readyBy, to: reach, priority: IN_FRONT });
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const covered = zones[zones.length - 1].to;
|
|
137
|
-
if (covered < end) {
|
|
138
|
-
zones.push({ from: covered, to: end, priority: THE_REST });
|
|
139
|
-
}
|
|
140
|
-
return zones;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// BELOW REALTIME THE ZONES GROW, AND EACH IS ONE ENCODER'S SHARE.
|
|
144
|
-
//
|
|
145
|
-
// An encoder starting at `q` produces the point `q + y` after `y / s`, and the
|
|
146
|
-
// viewer reaches it after `q + y - p`. It stays ahead while
|
|
147
|
-
//
|
|
148
|
-
// y <= (q - p) * s / (1 - s)
|
|
149
|
-
//
|
|
150
|
-
// so the length one encoder can hold GROWS with its distance from the viewer:
|
|
151
|
-
// the further off it starts, the later the viewer arrives. Equal shares are
|
|
152
|
-
// therefore the wrong division, and by a wide margin — on the addon host with
|
|
153
|
-
// 2400 s of film left, 120 s held and 0.5x, equal shares need twenty encoders
|
|
154
|
-
// and growing ones need five (120, 240, 480, 960, 1920).
|
|
155
|
-
//
|
|
156
|
-
// Each zone is exactly as long as its bound allows, which makes the count the
|
|
157
|
-
// smallest that can hold this viewer: any zone longer stalls them, and any
|
|
158
|
-
// shorter leaves the next one starting nearer, where its own bound is tighter.
|
|
159
|
-
// With nothing held, no partition holds this viewer: the first encoder can
|
|
160
|
-
// stay ahead for `b * s / (1 - s)`, and that is zero. Saying so plainly beats
|
|
161
|
-
// slicing the film into equal slivers that pretend otherwise — the whole of
|
|
162
|
-
// what is left is urgent, and it will still not be enough.
|
|
163
|
-
if (allowance <= 0) {
|
|
164
|
-
return [{ from, to: end, priority: AT_THE_VIEWER }];
|
|
122
|
+
if (from > 0) {
|
|
123
|
+
zones.push({ from: 0, to: from, priority: NOBODY_IS_COMING });
|
|
165
124
|
}
|
|
166
|
-
const growth = speed / (1 - speed);
|
|
167
125
|
let at = from;
|
|
168
|
-
let
|
|
126
|
+
let width = allowance;
|
|
169
127
|
let priority = AT_THE_VIEWER;
|
|
170
|
-
while (at < end &&
|
|
171
|
-
const
|
|
172
|
-
const to = Math.min(end, at + share);
|
|
128
|
+
while (at < end && priority > NOBODY_IS_COMING + 1) {
|
|
129
|
+
const to = Math.min(end, at + width);
|
|
173
130
|
zones.push({ from: at, to, priority });
|
|
174
|
-
held += to - at;
|
|
175
131
|
at = to;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// numbers that mean different things.
|
|
179
|
-
priority = Math.max(THE_REST + 1, priority - 1);
|
|
132
|
+
width *= 2;
|
|
133
|
+
priority -= 1;
|
|
180
134
|
}
|
|
181
135
|
if (at < end) {
|
|
182
|
-
|
|
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 });
|
|
183
139
|
}
|
|
184
140
|
return zones;
|
|
185
141
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The priority map, built once and handed to everybody who acts on it.
|
|
3
|
+
*
|
|
4
|
+
* One map per film, in seconds of film against a number. It is built from where
|
|
5
|
+
* the viewers are and nothing else, and both of the things that do work — the
|
|
6
|
+
* encoding and the downloading — read it and decide for themselves. They do not
|
|
7
|
+
* talk to each other, and neither of them tells this class anything.
|
|
8
|
+
*
|
|
9
|
+
* **Why it has to be published rather than asked for.** The downloading lives
|
|
10
|
+
* in another thread. Until now it took its orders from the reads themselves:
|
|
11
|
+
* every read declared a window around its own head, so fifteen reads declared
|
|
12
|
+
* fifteen windows on a piece store that holds sixteen pieces. Half of all
|
|
13
|
+
* evictions then took a piece a reader had said it wanted, two thirds of reads
|
|
14
|
+
* came back from disk, and what `/stream` handed out stopped being the file's
|
|
15
|
+
* bytes — twenty-two source-parse errors, a segment the player could not
|
|
16
|
+
* append, and an empty picture for six minutes (field 2026-09-05).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { mapForViewer, mergeMaps } from "./PriorityMap.js";
|
|
20
|
+
|
|
21
|
+
export class PriorityOrchestrator {
|
|
22
|
+
/** Where the map goes once it is built. @type {(published: object) => void} */
|
|
23
|
+
#publish;
|
|
24
|
+
|
|
25
|
+
/** The last map published per film and file, so an unchanged one is not resent. */
|
|
26
|
+
#last = new Map();
|
|
27
|
+
|
|
28
|
+
/** Who is watching one session. @type {(session: object) => Map<string, object>} */
|
|
29
|
+
#viewersOf;
|
|
30
|
+
|
|
31
|
+
/** How wide the first band of one session's file is. @type {(session: object) => number} */
|
|
32
|
+
#allowanceFor;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* This layer states facts and imports nothing above itself, so what it needs
|
|
36
|
+
* of a session — who is watching it, and how wide an interruption this file
|
|
37
|
+
* has shown on this swarm — is passed in.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} params
|
|
40
|
+
* @param {(published: { sourceKey: string, fileIndex: number, durationSeconds: number,
|
|
41
|
+
* zones: { from: number, to: number, priority: number }[] }) => void} params.publish
|
|
42
|
+
* @param {(session: object) => Map<string, object>} [params.viewersOf]
|
|
43
|
+
* @param {(session: object) => number} [params.allowanceFor]
|
|
44
|
+
*/
|
|
45
|
+
constructor({ publish, viewersOf, allowanceFor }) {
|
|
46
|
+
this.#publish = typeof publish === "function" ? publish : () => {};
|
|
47
|
+
this.#viewersOf = typeof viewersOf === "function" ? viewersOf : () => new Map();
|
|
48
|
+
this.#allowanceFor = typeof allowanceFor === "function" ? allowanceFor : () => 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The map for one film, from everyone watching it.
|
|
53
|
+
*
|
|
54
|
+
* @param {object} params
|
|
55
|
+
* @param {string} params.sourceKey
|
|
56
|
+
* @param {number} params.fileIndex
|
|
57
|
+
* @param {number} params.durationSeconds
|
|
58
|
+
* @param {number} params.allowanceSeconds - The measured depth below which an
|
|
59
|
+
* interruption reaches a viewer of this file.
|
|
60
|
+
* @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.
|
|
63
|
+
*/
|
|
64
|
+
build({ sourceKey, fileIndex, durationSeconds, allowanceSeconds, viewers }) {
|
|
65
|
+
const zones = mergeMaps(
|
|
66
|
+
(viewers ?? []).map((viewer) =>
|
|
67
|
+
mapForViewer({
|
|
68
|
+
atSeconds: viewer.atSeconds,
|
|
69
|
+
durationSeconds,
|
|
70
|
+
allowanceSeconds,
|
|
71
|
+
playing: viewer.playing !== false
|
|
72
|
+
})
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
const key = `${sourceKey}:${fileIndex}`;
|
|
76
|
+
// Unchanged maps are not republished: the downloading rebuilds what it asks
|
|
77
|
+
// the swarm for on every one, and a viewer sitting still would otherwise
|
|
78
|
+
// make it do that several times a second.
|
|
79
|
+
const shape = JSON.stringify(zones);
|
|
80
|
+
if (this.#last.get(key) !== shape) {
|
|
81
|
+
this.#last.set(key, shape);
|
|
82
|
+
this.#publish({ sourceKey, fileIndex, durationSeconds, zones });
|
|
83
|
+
}
|
|
84
|
+
return zones;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Build and publish the map for every file anybody is watching.
|
|
89
|
+
*
|
|
90
|
+
* One map per FILE, not per output: the picture, a quality step and a
|
|
91
|
+
* soundtrack of one film are three outputs reading the same bytes, and the
|
|
92
|
+
* swarm is asked for bytes. Viewers of all of them merge into one map.
|
|
93
|
+
*
|
|
94
|
+
* @param {object} params
|
|
95
|
+
* @param {Iterable<object[]>} params.sessionGroups - The live sessions, in
|
|
96
|
+
* whatever grouping the caller holds them; they are regrouped by file here.
|
|
97
|
+
* @param {number} params.staleAfterMs - How long a viewer may be silent and
|
|
98
|
+
* still count as watching.
|
|
99
|
+
* @param {number} [params.now]
|
|
100
|
+
* @returns {void}
|
|
101
|
+
*/
|
|
102
|
+
publishFor({ sessionGroups, staleAfterMs, now = Date.now() }) {
|
|
103
|
+
/** @type {Map<string, { sourceKey: string, fileIndex: number, durationSeconds: number, allowanceSeconds: number, viewers: object[] }>} */
|
|
104
|
+
const byFile = new Map();
|
|
105
|
+
for (const sessions of sessionGroups) {
|
|
106
|
+
for (const session of sessions) {
|
|
107
|
+
const key = `${session.sourceKey}:${session.fileIndex}`;
|
|
108
|
+
let held = byFile.get(key);
|
|
109
|
+
if (!held) {
|
|
110
|
+
held = {
|
|
111
|
+
sourceKey: session.sourceKey,
|
|
112
|
+
fileIndex: session.fileIndex,
|
|
113
|
+
durationSeconds: Number(session.file?.durationSeconds) || 0,
|
|
114
|
+
// The first band is as wide as an interruption this file has
|
|
115
|
+
// actually shown on this swarm, never a chosen number.
|
|
116
|
+
allowanceSeconds: this.#allowanceFor(session),
|
|
117
|
+
viewers: []
|
|
118
|
+
};
|
|
119
|
+
byFile.set(key, held);
|
|
120
|
+
}
|
|
121
|
+
for (const viewer of this.#viewersOf(session).values()) {
|
|
122
|
+
if (viewer.isPresent(now, staleAfterMs)) {
|
|
123
|
+
held.viewers.push({
|
|
124
|
+
atSeconds: viewer.positionSeconds() ?? 0,
|
|
125
|
+
playing: viewer.playing !== false
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const one of byFile.values()) {
|
|
132
|
+
// A file of unknown length cannot be divided into zones, and a file
|
|
133
|
+
// nobody is watching has nothing to be urgent about.
|
|
134
|
+
if (one.durationSeconds > 0 && one.viewers.length > 0) {
|
|
135
|
+
this.build(one);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Nobody is watching this file any more.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} sourceKey
|
|
144
|
+
* @param {number} fileIndex
|
|
145
|
+
*/
|
|
146
|
+
forget(sourceKey, fileIndex) {
|
|
147
|
+
this.#last.delete(`${sourceKey}:${fileIndex}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
package/services/torrent-pool.js
CHANGED
|
@@ -17,8 +17,12 @@ import { logger } from "../utils/logger.js";
|
|
|
17
17
|
import { SharedPieceStore, findSharedStore } from "./piece-store/shared-piece-store.js";
|
|
18
18
|
import { Urgency } from "./demand/index.js";
|
|
19
19
|
import { demandFor, forgetTorrent, reconcileAll } from "./download/registry.js";
|
|
20
|
+
import { AT_THE_VIEWER, NOBODY_IS_COMING } from "./priority/PriorityMap.js";
|
|
20
21
|
import { deriveSourceKey } from "./torrent-source-key.js";
|
|
21
22
|
|
|
23
|
+
/** How a window stated from the priority map names itself. */
|
|
24
|
+
const MAP_CLAIMANT = "priority-map";
|
|
25
|
+
|
|
22
26
|
// The DHT's entry points. Two of the three the library ships answer nothing —
|
|
23
27
|
// measured 2026-08-21 from the addon host: `router.bittorrent.com` and
|
|
24
28
|
// `router.utorrent.com` did not reply to a hand-written `ping` at all, while a
|
|
@@ -992,6 +996,106 @@ export class TorrentPool {
|
|
|
992
996
|
}
|
|
993
997
|
}
|
|
994
998
|
|
|
999
|
+
/**
|
|
1000
|
+
* Take the priority map for one file and state what to fetch from it.
|
|
1001
|
+
*
|
|
1002
|
+
* THE MAP IS THE ONLY SOURCE. What used to state demand here was the reads
|
|
1003
|
+
* themselves: every read declared a window around its own head, so fifteen
|
|
1004
|
+
* reads declared fifteen windows on a piece store that holds sixteen pieces —
|
|
1005
|
+
* half of all evictions then took a piece a reader had said it wanted, two
|
|
1006
|
+
* thirds of reads came back from disk, and the bytes handed out stopped being
|
|
1007
|
+
* the file's (field 2026-09-05).
|
|
1008
|
+
*
|
|
1009
|
+
* Seconds become bytes here, because this is the side that knows how the file
|
|
1010
|
+
* is laid out. The map never learns about bytes and the viewer never appears.
|
|
1011
|
+
*
|
|
1012
|
+
* @param {object} torrent
|
|
1013
|
+
* @param {number} fileIndex
|
|
1014
|
+
* @param {{ from: number, to: number, priority: number }[]} zones - Seconds of
|
|
1015
|
+
* film against a number, highest first in urgency.
|
|
1016
|
+
* @param {number} durationSeconds
|
|
1017
|
+
*/
|
|
1018
|
+
applyPriorityMap(torrent, fileIndex, zones, durationSeconds) {
|
|
1019
|
+
const file = Array.isArray(torrent?.files) ? torrent.files[fileIndex] : null;
|
|
1020
|
+
const length = Number(file?.length);
|
|
1021
|
+
const duration = Number(durationSeconds);
|
|
1022
|
+
if (!file || !(length > 0) || !(duration > 0) || !Array.isArray(zones)) {
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const { register } = demandFor(torrent);
|
|
1026
|
+
// Highest number first, so the stretch a viewer reaches soonest is stated
|
|
1027
|
+
// first and the rest fall in behind it.
|
|
1028
|
+
const ordered = [...zones]
|
|
1029
|
+
.filter((zone) => Number.isFinite(zone?.from) && Number.isFinite(zone?.to) && zone.to > zone.from)
|
|
1030
|
+
.sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
|
|
1031
|
+
/**
|
|
1032
|
+
* Which level of urgency one zone is stated at, from the map's own number
|
|
1033
|
+
* and nothing else. The map's numbers are a scale as long as the film needs
|
|
1034
|
+
* — ten bands on a fifty-minute film — while the register has five levels,
|
|
1035
|
+
* and the fit is by meaning:
|
|
1036
|
+
*
|
|
1037
|
+
* - the top of the scale is where a viewer is standing, so it is the
|
|
1038
|
+
* cushion being built: {@link Urgency.NEAR}. Never BLOCKED — that level
|
|
1039
|
+
* means a reader is stopped on those bytes right now, which only a read
|
|
1040
|
+
* can say;
|
|
1041
|
+
* - the bottom is what nobody is approaching: behind a viewer moving
|
|
1042
|
+
* forward, and the whole film of a viewer who has stopped the picture.
|
|
1043
|
+
* Wanted only if somebody seeks back, which is {@link Urgency.BEHIND};
|
|
1044
|
+
* - one above the bottom is the far tail — wanted for certain if the
|
|
1045
|
+
* viewer watches on, and not before: {@link Urgency.TAIL};
|
|
1046
|
+
* - everything between is the lead being built: {@link Urgency.AHEAD}.
|
|
1047
|
+
*
|
|
1048
|
+
* Read from the scale's own ends rather than from the highest and lowest
|
|
1049
|
+
* number in THIS file's map. The two speculative levels are withheld across
|
|
1050
|
+
* every torrent at once while anything urgent is missing anywhere, so a
|
|
1051
|
+
* paused viewer's film has to compare as wanted-last against another film
|
|
1052
|
+
* somebody is watching — and relative to itself alone it would compare as
|
|
1053
|
+
* the most urgent thing there is.
|
|
1054
|
+
*
|
|
1055
|
+
* @param {{ priority?: number }} zone
|
|
1056
|
+
* @returns {number}
|
|
1057
|
+
*/
|
|
1058
|
+
const levelOf = (zone) => {
|
|
1059
|
+
const priority = zone.priority ?? 0;
|
|
1060
|
+
if (priority <= NOBODY_IS_COMING) {
|
|
1061
|
+
return Urgency.BEHIND;
|
|
1062
|
+
}
|
|
1063
|
+
if (priority <= NOBODY_IS_COMING + 1) {
|
|
1064
|
+
return Urgency.TAIL;
|
|
1065
|
+
}
|
|
1066
|
+
return priority >= AT_THE_VIEWER ? Urgency.NEAR : Urgency.AHEAD;
|
|
1067
|
+
};
|
|
1068
|
+
ordered.forEach((zone, index) => {
|
|
1069
|
+
// A second of film sits at that fraction of the file. Constant bitrate is
|
|
1070
|
+
// an approximation, and it is the only one available without an index of
|
|
1071
|
+
// the container — near enough to say which stretch matters more, which is
|
|
1072
|
+
// all this decides.
|
|
1073
|
+
const byteStart = Math.max(0, Math.floor((zone.from / duration) * length));
|
|
1074
|
+
const byteEnd = Math.min(length - 1, Math.ceil((zone.to / duration) * length) - 1);
|
|
1075
|
+
if (byteEnd < byteStart) {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
register.state({
|
|
1079
|
+
claimant: `${MAP_CLAIMANT}:${fileIndex}:${index}`,
|
|
1080
|
+
fileIndex,
|
|
1081
|
+
byteStart,
|
|
1082
|
+
byteEnd,
|
|
1083
|
+
urgency: levelOf(zone)
|
|
1084
|
+
});
|
|
1085
|
+
});
|
|
1086
|
+
// Bands the map no longer has: a viewer moved on, and what they had wanted
|
|
1087
|
+
// is not wanted by anybody now.
|
|
1088
|
+
for (const window of register.windows()) {
|
|
1089
|
+
if (!String(window.claimant).startsWith(`${MAP_CLAIMANT}:${fileIndex}:`)) {
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
const index = Number(String(window.claimant).split(":").pop());
|
|
1093
|
+
if (!(index < ordered.length)) {
|
|
1094
|
+
register.withdraw(window.claimant);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
995
1099
|
#reportStalledDownloads() {
|
|
996
1100
|
const now = Date.now();
|
|
997
1101
|
for (const torrent of this.torrents.values()) {
|
|
@@ -432,6 +432,26 @@ export class TorrentWorkerClient {
|
|
|
432
432
|
});
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Hand the download the priority map for one file.
|
|
437
|
+
*
|
|
438
|
+
* Seconds of film against a number, and nothing else: what is wanted and in
|
|
439
|
+
* what order. Turning that into bytes, into pieces, and into what the swarm
|
|
440
|
+
* is asked for is the download's own business, and so is what it keeps in
|
|
441
|
+
* memory.
|
|
442
|
+
*
|
|
443
|
+
* @param {{ sourceKey: string, fileIndex: number, durationSeconds: number, zones: { from: number, to: number, priority: number }[] }} params
|
|
444
|
+
* @returns {Promise<void>}
|
|
445
|
+
*/
|
|
446
|
+
async setPriorityMap({ sourceKey, fileIndex, zones, durationSeconds }) {
|
|
447
|
+
await this.#caller.call(Command.PRIORITY_MAP, {
|
|
448
|
+
sourceKey,
|
|
449
|
+
fileIndex,
|
|
450
|
+
zones,
|
|
451
|
+
durationSeconds
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
435
455
|
/**
|
|
436
456
|
* Pre-fetch the head and tail the codec probe needs.
|
|
437
457
|
*
|