@torrent-tv/proxy 2.95.3 → 2.95.5
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/package.json +1 -1
- package/routes/encode-input/get.js +29 -0
- package/server.js +53 -10
- package/services/encode/EncodeInputs.js +8 -2
- package/services/encode/EncodeRuns.js +21 -3
- package/services/encode/OriginalInput.js +59 -0
- package/services/encode/admitted-command.js +8 -2
- package/services/encode/output/Timeline.js +4 -0
- package/services/encode/piece-completeness.js +6 -2
- package/services/encode/run-command.js +5 -5
- package/services/encode/segment-formats/fmp4.js +1 -0
- package/services/encode/segment-formats/mp4-boxes.js +26 -2
- package/services/encode/source-command.js +261 -0
- package/services/media/ContainerOrchestrator.js +7 -1
- package/services/media/container/MatroskaContainer.js +79 -0
- package/services/media/container/ffprobe-record.js +2 -0
- package/services/server/SegmentServing.js +3 -1
- package/services/server/wire-outputs.js +2 -1
- package/services/storage/piece-store/shared-piece-store.js +8 -1
- package/services/torrent/download/FutureDownload.js +98 -150
- package/services/torrent/download/SwarmSelection.js +45 -33
- package/services/torrent/download/bands.js +5 -0
- package/services/torrent/download/registry.js +5 -11
- package/services/viewer/DownloadMaps.js +1 -1
- package/services/viewer/NativeSourceMap.js +25 -0
- package/services/viewer/PriorityMap.js +1 -0
- package/services/viewer/playback-readiness.js +61 -19
- package/services/viewer/segment-demands.js +6 -2
- package/services/torrent/download/DeadlineScheduler.js +0 -195
|
@@ -1,166 +1,114 @@
|
|
|
1
1
|
import { setImmediate as nextTurn } from "node:timers/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { piecesOf } from "../demand/pieces.js";
|
|
3
|
+
import { pieceStoreOf } from "../piece-store-of.js";
|
|
4
|
+
import { compareBands } from "./bands.js";
|
|
3
5
|
|
|
4
6
|
const BLOCK_BYTES = 16 * 1024;
|
|
5
7
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
+
/** Estimate completion from observed queues and the public selection bands.
|
|
9
|
+
* The native picker decides the order within a band, so every unqueued piece
|
|
10
|
+
* uses that band's completion time rather than an invented request order.
|
|
11
|
+
* Pieces without a measured supplier remain unknown. No requests are changed.
|
|
12
|
+
* Times are conditional on the measured rate and successful verification.
|
|
13
|
+
*/
|
|
14
|
+
export async function futureDownload(entries, { now = Date.now(), findStore = pieceStoreOf, isCurrent = () => true } = {}) {
|
|
8
15
|
if (!isCurrent()) return new Map();
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
let
|
|
12
|
-
for (const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (index < 0) return;
|
|
31
|
-
wire.requests.splice(index, 1);
|
|
32
|
-
const state = copy.states.get(piece);
|
|
33
|
-
for (let block = offset / BLOCK_BYTES; state && block * BLOCK_BYTES < offset + length; block++) {
|
|
34
|
-
state.reserved.delete(block);
|
|
35
|
-
state.cursor = Math.min(state.cursor, block);
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
copy.wires.push(wire);
|
|
16
|
+
const forecasts = new Map();
|
|
17
|
+
const pending = [];
|
|
18
|
+
let steps = 0;
|
|
19
|
+
for (const { torrent, register } of entries) {
|
|
20
|
+
if (torrent.destroyed || torrent.ready === false) continue;
|
|
21
|
+
const wanted = new Map();
|
|
22
|
+
for (const window of register.windows()) {
|
|
23
|
+
const file = torrent.files?.[window.fileIndex];
|
|
24
|
+
if (!file) continue;
|
|
25
|
+
const range = piecesOf({ fileOffset: file.offset, byteStart: window.byteStart,
|
|
26
|
+
byteEnd: Math.min(window.byteEnd, file.length - 1), pieceLength: torrent.pieceLength });
|
|
27
|
+
if (!range) continue;
|
|
28
|
+
for (let piece = range.from; piece <= range.to; piece++) {
|
|
29
|
+
const previous = wanted.get(piece);
|
|
30
|
+
if (!previous || compareBands(window, previous) < 0) {
|
|
31
|
+
wanted.set(piece, { urgency: window.urgency, priority: window.priority, deadlineAt: window.deadlineAt });
|
|
32
|
+
}
|
|
33
|
+
if (++steps % 256 === 0) {
|
|
34
|
+
await nextTurn();
|
|
35
|
+
if (!isCurrent()) return new Map();
|
|
36
|
+
}
|
|
39
37
|
}
|
|
40
|
-
copy._request = (wire, piece) => {
|
|
41
|
-
const state = copy.states.get(piece);
|
|
42
|
-
if (!state || state.complete) return false;
|
|
43
|
-
let block = state.cursor;
|
|
44
|
-
while (block < state.count && (state.present.has(block) || state.reserved.has(block))) block++;
|
|
45
|
-
if (block === state.count) return false;
|
|
46
|
-
const offset = block * BLOCK_BYTES;
|
|
47
|
-
const length = wire.type === "webSeed" ? state.length - offset : Math.min(BLOCK_BYTES, state.length - offset);
|
|
48
|
-
for (let index = block; index * BLOCK_BYTES < offset + length; index++) state.reserved.add(index);
|
|
49
|
-
state.cursor = Math.ceil((offset + length) / BLOCK_BYTES);
|
|
50
|
-
wire.requests.push({ piece, offset, length, remaining: length });
|
|
51
|
-
return true;
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
const piece = source.pieces?.[candidate.piece];
|
|
55
|
-
if (!piece || !Number.isSafeInteger(piece.length) || piece.length <= 0 ||
|
|
56
|
-
(piece.missing !== piece.length && !Array.isArray(piece._buffer))) {
|
|
57
|
-
copy.arrivals.set(candidate.piece, null);
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
const count = Math.ceil(piece.length / BLOCK_BYTES);
|
|
61
|
-
const present = new Set();
|
|
62
|
-
for (let block = 0; block < count; block++) if (piece._buffer?.[block]) present.add(block);
|
|
63
|
-
const reserved = new Set();
|
|
64
|
-
for (const wire of copy.wires) for (const request of wire.requests) {
|
|
65
|
-
if (request.piece !== candidate.piece) continue;
|
|
66
|
-
for (let block = request.offset / BLOCK_BYTES; block * BLOCK_BYTES < request.offset + request.length; block++) reserved.add(block);
|
|
67
|
-
}
|
|
68
|
-
copy.states.set(candidate.piece, { length: piece.length, count, present, reserved, cursor: 0, complete: false });
|
|
69
|
-
}
|
|
70
|
-
const ordered = candidates.filter(candidate => copies.get(candidate.torrent).states.has(candidate.piece))
|
|
71
|
-
.map(candidate => ({ ...candidate, torrent: copies.get(candidate.torrent) }));
|
|
72
|
-
dispatchDownloadCandidates(ordered, clock);
|
|
73
|
-
const solved = new Set();
|
|
74
|
-
if (!replayOnly) for (const copy of copies.values()) {
|
|
75
|
-
if (copy.wires.length === 1 && !copy.wires[0].destroyed) {
|
|
76
|
-
finishSinglePeer(copy, ordered.filter(candidate => candidate.torrent === copy), now);
|
|
77
|
-
solved.add(copy);
|
|
78
38
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
39
|
+
const queued = new Map();
|
|
40
|
+
for (const wire of torrent.wires ?? []) {
|
|
41
|
+
if (wire.destroyed) continue;
|
|
42
|
+
const speed = Number(wire.downloadSpeed?.());
|
|
43
|
+
if (!Number.isFinite(speed) || speed <= 0) continue;
|
|
44
|
+
let bytes = 0;
|
|
45
|
+
for (const request of wire.requests ?? []) {
|
|
46
|
+
if (!Number.isSafeInteger(request.length) || request.length <= 0) continue;
|
|
47
|
+
bytes += request.length;
|
|
48
|
+
const piece = torrent.pieces?.[request.piece];
|
|
49
|
+
if (!wanted.has(request.piece) || !piece || !Number.isSafeInteger(request.offset) ||
|
|
50
|
+
request.offset < 0 || request.offset % BLOCK_BYTES !== 0 ||
|
|
51
|
+
request.offset + request.length > piece.length ||
|
|
52
|
+
((request.offset + request.length) % BLOCK_BYTES !== 0 && request.offset + request.length !== piece.length)) continue;
|
|
53
|
+
let blocks = queued.get(request.piece);
|
|
54
|
+
if (!blocks) queued.set(request.piece, blocks = new Map());
|
|
55
|
+
const at = now + bytes / speed * 1000;
|
|
56
|
+
for (let block = request.offset / BLOCK_BYTES; block * BLOCK_BYTES < request.offset + request.length; block++) {
|
|
57
|
+
blocks.set(block, Math.min(blocks.get(block) ?? Infinity, at));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
98
60
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
for (const
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (request.remaining > 0) continue;
|
|
107
|
-
wire.requests.shift();
|
|
108
|
-
const state = copy.states.get(request.piece);
|
|
109
|
-
if (!state || state.complete) continue;
|
|
110
|
-
for (let block = request.offset / BLOCK_BYTES; block * BLOCK_BYTES < request.offset + request.length; block++) {
|
|
111
|
-
state.present.add(block);
|
|
112
|
-
state.reserved.delete(block);
|
|
61
|
+
const store = findStore(torrent);
|
|
62
|
+
const arrivals = new Map();
|
|
63
|
+
for (const [index, band] of wanted) {
|
|
64
|
+
const present = store?.locationOf ? store.locationOf(index) !== "missing" : torrent.bitfield?.get(index);
|
|
65
|
+
if (present) {
|
|
66
|
+
arrivals.set(index, now);
|
|
67
|
+
continue;
|
|
113
68
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
69
|
+
const piece = torrent.pieces?.[index];
|
|
70
|
+
let at = now;
|
|
71
|
+
if (!piece || !Number.isSafeInteger(piece.length) || piece.length <= 0 ||
|
|
72
|
+
(piece.missing !== piece.length && !Array.isArray(piece._buffer))) at = null;
|
|
73
|
+
else for (let block = 0; block * BLOCK_BYTES < piece.length; block++) {
|
|
74
|
+
const length = Math.min(BLOCK_BYTES, piece.length - block * BLOCK_BYTES);
|
|
75
|
+
if (piece._buffer?.[block]?.length === length) continue;
|
|
76
|
+
const arrival = queued.get(index)?.get(block);
|
|
77
|
+
if (!Number.isFinite(arrival)) { at = null; break; }
|
|
78
|
+
at = Math.max(at, arrival);
|
|
79
|
+
}
|
|
80
|
+
arrivals.set(index, at);
|
|
81
|
+
const remaining = piece?.missing;
|
|
82
|
+
const speed = (torrent.wires ?? []).reduce((sum, wire) => {
|
|
83
|
+
if (wire.destroyed || !wire.peerPieces?.get(index)) return sum;
|
|
84
|
+
const rate = Number(wire.downloadSpeed?.());
|
|
85
|
+
return Number.isFinite(rate) && rate > 0 ? sum + rate : sum;
|
|
86
|
+
}, 0);
|
|
87
|
+
pending.push({ arrivals, index, ...band, seconds:
|
|
88
|
+
Number.isSafeInteger(remaining) && remaining > 0 && remaining <= piece.length && speed > 0
|
|
89
|
+
? remaining / speed : null });
|
|
90
|
+
if (++steps % 256 === 0) {
|
|
91
|
+
await nextTurn();
|
|
92
|
+
if (!isCurrent()) return new Map();
|
|
120
93
|
}
|
|
121
94
|
}
|
|
122
|
-
|
|
123
|
-
await nextTurn();
|
|
124
|
-
if (!isCurrent()) return new Map();
|
|
125
|
-
}
|
|
95
|
+
forecasts.set(torrent, { measuredAt: now, arrivals });
|
|
126
96
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
function finishSinglePeer(copy, candidates, now) {
|
|
136
|
-
const wire = copy.wires[0];
|
|
137
|
-
const speed = wire.downloadSpeed();
|
|
138
|
-
if (!(speed > 0) || !(peerRequestCapacity(copy, wire) > 0)) return;
|
|
139
|
-
let at = now;
|
|
140
|
-
for (const request of wire.requests) {
|
|
141
|
-
at += request.length / speed * 1000;
|
|
142
|
-
const state = copy.states.get(request.piece);
|
|
143
|
-
if (!state || state.complete) continue;
|
|
144
|
-
for (let block = request.offset / BLOCK_BYTES; block * BLOCK_BYTES < request.offset + request.length; block++) {
|
|
145
|
-
state.present.add(block);
|
|
146
|
-
state.reserved.delete(block);
|
|
147
|
-
}
|
|
148
|
-
if (state.present.size === state.count) {
|
|
149
|
-
state.complete = true;
|
|
150
|
-
state.present.clear();
|
|
151
|
-
state.reserved.clear();
|
|
152
|
-
copy.arrivals.set(request.piece, at);
|
|
97
|
+
pending.sort(compareBands);
|
|
98
|
+
let finish = now;
|
|
99
|
+
for (let start = 0; start < pending.length;) {
|
|
100
|
+
let end = start;
|
|
101
|
+
const first = pending[start];
|
|
102
|
+
while (end < pending.length && compareBands(pending[end], first) === 0) {
|
|
103
|
+
const seconds = pending[end++].seconds;
|
|
104
|
+
finish = finish === null || seconds === null ? null : finish + seconds * 1000;
|
|
153
105
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
let missing = 0;
|
|
159
|
-
for (let block = 0; block < state.count; block++) {
|
|
160
|
-
if (!state.present.has(block)) missing += Math.min(BLOCK_BYTES, state.length - block * BLOCK_BYTES);
|
|
106
|
+
for (let i = start; i < end; i++) {
|
|
107
|
+
const { arrivals, index } = pending[i];
|
|
108
|
+
const queuedAt = arrivals.get(index);
|
|
109
|
+
if (finish !== null) arrivals.set(index, queuedAt === null ? finish : Math.min(queuedAt, finish));
|
|
161
110
|
}
|
|
162
|
-
|
|
163
|
-
state.complete = true;
|
|
164
|
-
copy.arrivals.set(piece, at);
|
|
111
|
+
start = end;
|
|
165
112
|
}
|
|
113
|
+
return isCurrent() ? forecasts : new Map();
|
|
166
114
|
}
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
urgencyName
|
|
30
30
|
} from "../demand/index.js";
|
|
31
31
|
import { pieceStoreOf } from "../piece-store-of.js";
|
|
32
|
+
import { compareBands } from "./bands.js";
|
|
32
33
|
|
|
33
34
|
export class SwarmSelection {
|
|
34
35
|
#torrent;
|
|
@@ -69,23 +70,37 @@ export class SwarmSelection {
|
|
|
69
70
|
* shared and the question is not a per-torrent one.
|
|
70
71
|
* @returns {{ stated: number, withdrawn: number }}
|
|
71
72
|
*/
|
|
72
|
-
reconcile() {
|
|
73
|
-
const
|
|
73
|
+
reconcile({ speculativeAllowed = true, maximumUrgency = Infinity, minimumPriority = 1, latestDeadlineAt = Infinity } = {}) {
|
|
74
|
+
const windows=this.#register.windows();
|
|
75
|
+
const missing=windows.filter(window=>!this.#isSatisfied(window));
|
|
76
|
+
const urgency=missing.length?Math.min(...missing.map(window=>window.urgency)):null;
|
|
77
|
+
const eligible=missing.filter(window=>window.urgency===urgency);
|
|
78
|
+
const priority=eligible.length?Math.max(...eligible.map(window=>window.priority)):null;
|
|
79
|
+
const ranked=eligible.filter(window=>window.priority===priority);
|
|
80
|
+
const deadlineAt=ranked.length?Math.min(...ranked.map(window=>window.deadlineAt)):Infinity;
|
|
81
|
+
const active=urgency===null||urgency>maximumUrgency||priority<minimumPriority||deadlineAt>latestDeadlineAt||(!speculativeAllowed&&urgency>=Urgency.TAIL)?[]:
|
|
82
|
+
ranked.filter(window=>window.deadlineAt===deadlineAt);
|
|
74
83
|
/** @type {Map<string, { from: number, to: number, priority: number }>} */
|
|
75
84
|
const wanted = new Map();
|
|
76
|
-
for (const
|
|
77
|
-
for (const window of this.#register.at(urgency)) {
|
|
85
|
+
for (const window of active) {
|
|
78
86
|
const range = this.#piecesFor(window);
|
|
79
87
|
if (!range) {
|
|
80
88
|
continue;
|
|
81
89
|
}
|
|
82
90
|
// Merged by range and priority, not by claimant: two readers wanting
|
|
83
91
|
// the same pieces are one instruction to the swarm.
|
|
84
|
-
const key = `${range.from}-${range.to}-${selectionPriority(urgency)}`;
|
|
85
|
-
wanted.set(key, { from: range.from, to: range.to, priority: selectionPriority(urgency) });
|
|
86
|
-
}
|
|
92
|
+
const key = `${range.from}-${range.to}-${selectionPriority(window.urgency)}`;
|
|
93
|
+
wanted.set(key, { from: range.from, to: range.to, priority: selectionPriority(window.urgency) });
|
|
87
94
|
}
|
|
88
95
|
|
|
96
|
+
// Public selections merge adjacent ranges. Publish their union so an
|
|
97
|
+
// unchanged map does not repeatedly replace the library's merged range.
|
|
98
|
+
const ordered=[...wanted.values()].sort((a,b)=>a.from-b.from);
|
|
99
|
+
wanted.clear();
|
|
100
|
+
const union=[];
|
|
101
|
+
for(const range of ordered){const previous=union.at(-1);if(previous&&range.from<=previous.to+1)previous.to=Math.max(previous.to,range.to);else union.push({...range});}
|
|
102
|
+
for(const range of union)wanted.set(`${range.from}-${range.to}-${range.priority}`,range);
|
|
103
|
+
|
|
89
104
|
let withdrawn = 0;
|
|
90
105
|
for (const [key, range] of [...this.#stated]) {
|
|
91
106
|
if (wanted.has(key)) {
|
|
@@ -111,6 +126,14 @@ export class SwarmSelection {
|
|
|
111
126
|
}
|
|
112
127
|
|
|
113
128
|
this.#projectIntoMemory();
|
|
129
|
+
for (const window of this.#register.at(Urgency.BLOCKED)) {
|
|
130
|
+
if (this.#isSatisfied(window)) continue;
|
|
131
|
+
const range = this.#piecesFor(window);
|
|
132
|
+
if (range) {
|
|
133
|
+
try { this.#torrent.critical?.(range.from, range.to); }
|
|
134
|
+
catch { /* A closing torrent cannot accept a critical selection. */ }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
114
137
|
return { stated, withdrawn };
|
|
115
138
|
}
|
|
116
139
|
|
|
@@ -177,16 +200,15 @@ export class SwarmSelection {
|
|
|
177
200
|
return false;
|
|
178
201
|
}
|
|
179
202
|
|
|
180
|
-
/**
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
*/
|
|
203
|
+
/** The highest missing map band, without choosing protocol blocks. */
|
|
204
|
+
missingBand() {
|
|
205
|
+
let band=null;
|
|
206
|
+
for(const window of this.#register.windows()) {
|
|
207
|
+
if(this.#isSatisfied(window))continue;
|
|
208
|
+
if(!band||compareBands(window,band)<0)band={urgency:window.urgency,priority:window.priority,deadlineAt:window.deadlineAt};
|
|
209
|
+
}
|
|
210
|
+
return band;
|
|
211
|
+
}
|
|
190
212
|
|
|
191
213
|
/**
|
|
192
214
|
* Tell the piece store which bytes will be read soon, from the same stated
|
|
@@ -281,8 +303,10 @@ export class SwarmSelection {
|
|
|
281
303
|
if (!range) {
|
|
282
304
|
return true;
|
|
283
305
|
}
|
|
306
|
+
const store = this.#findStore(this.#torrent);
|
|
284
307
|
for (let index = range.from; index <= range.to; index += 1) {
|
|
285
|
-
|
|
308
|
+
const present = store?.locationOf ? store.locationOf(index) !== "missing" : this.#torrent.bitfield?.get(index);
|
|
309
|
+
if (!present) {
|
|
286
310
|
return false;
|
|
287
311
|
}
|
|
288
312
|
}
|
|
@@ -311,15 +335,7 @@ export class SwarmSelection {
|
|
|
311
335
|
*/
|
|
312
336
|
#select({ from, to, priority }) {
|
|
313
337
|
try {
|
|
314
|
-
|
|
315
|
-
// claimants can hold at the same bounds; the public one merges and
|
|
316
|
-
// subtracts intervals and cannot express "one of several wants this". The
|
|
317
|
-
// public call is the fallback if a future version drops the private one.
|
|
318
|
-
if (typeof this.#torrent._select === "function") {
|
|
319
|
-
this.#torrent._select(from, to, priority, null, true);
|
|
320
|
-
} else if (typeof this.#torrent.select === "function") {
|
|
321
|
-
this.#torrent.select(from, to, priority);
|
|
322
|
-
}
|
|
338
|
+
this.#torrent.select?.(from, to, priority);
|
|
323
339
|
} catch {
|
|
324
340
|
// silent-ok: never fail a read because the download set refused.
|
|
325
341
|
}
|
|
@@ -331,11 +347,7 @@ export class SwarmSelection {
|
|
|
331
347
|
*/
|
|
332
348
|
#deselect({ from, to }) {
|
|
333
349
|
try {
|
|
334
|
-
|
|
335
|
-
this.#torrent._deselect(from, to, true);
|
|
336
|
-
} else if (typeof this.#torrent.deselect === "function") {
|
|
337
|
-
this.#torrent.deselect(from, to);
|
|
338
|
-
}
|
|
350
|
+
this.#torrent.deselect?.(from, to);
|
|
339
351
|
} catch {
|
|
340
352
|
// silent-ok.
|
|
341
353
|
}
|
|
@@ -353,7 +365,7 @@ export class SwarmSelection {
|
|
|
353
365
|
return (
|
|
354
366
|
`download: ${this.#stated.size} instruction(s) to the swarm from ` +
|
|
355
367
|
`${this.#register.size} stated need(s) [${needs.join(" ")}]` +
|
|
356
|
-
"; peer requests
|
|
368
|
+
"; peer requests issued by WebTorrent"
|
|
357
369
|
);
|
|
358
370
|
}
|
|
359
371
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Public selection groups retain both the map's rank and its required time. */
|
|
2
|
+
export function compareBands(left, right) {
|
|
3
|
+
return left.urgency - right.urgency || right.priority - left.priority ||
|
|
4
|
+
(left.deadlineAt === right.deadlineAt ? 0 : left.deadlineAt - right.deadlineAt);
|
|
5
|
+
}
|
|
@@ -17,14 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
import { DemandRegister } from "../demand/DemandRegister.js";
|
|
19
19
|
import { SwarmSelection } from "./SwarmSelection.js";
|
|
20
|
-
import { DeadlineScheduler } from "./DeadlineScheduler.js";
|
|
21
20
|
import { futureDownload } from "./FutureDownload.js";
|
|
22
21
|
|
|
23
22
|
/** @type {WeakMap<object, { register: DemandRegister, selection: SwarmSelection }>} */
|
|
24
23
|
const byTorrent = new WeakMap();
|
|
25
24
|
/** @type {Set<{ register: DemandRegister, selection: SwarmSelection }>} */
|
|
26
25
|
const live = new Set();
|
|
27
|
-
const scheduler = new DeadlineScheduler({ entries: () => live });
|
|
28
26
|
let futurePending = null;
|
|
29
27
|
let publishingSelections = false;
|
|
30
28
|
|
|
@@ -56,12 +54,6 @@ export function demandFor(torrent) {
|
|
|
56
54
|
}
|
|
57
55
|
const register = new DemandRegister();
|
|
58
56
|
const entry = { torrent, register, selection: new SwarmSelection({ torrent, register }), withdrawalRevision: 0 };
|
|
59
|
-
if (typeof torrent._request === "function" && typeof torrent._updateWire === "function") {
|
|
60
|
-
entry.previousWireUpdate = torrent._updateWire;
|
|
61
|
-
torrent._updateWire = wire => {
|
|
62
|
-
if (!publishingSelections) scheduler.reconcile(Date.now(), { torrent, wire });
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
57
|
byTorrent.set(torrent, entry);
|
|
66
58
|
live.add(entry);
|
|
67
59
|
entry.onBytesChanged = () => reconcileAll();
|
|
@@ -106,7 +98,6 @@ export function forgetTorrent(torrent) {
|
|
|
106
98
|
held.register.clear();
|
|
107
99
|
torrent.removeListener?.("verified", held.onBytesChanged);
|
|
108
100
|
torrent.removeListener?.("piece-withdrawn", held.onWithdrawn);
|
|
109
|
-
if (held.previousWireUpdate) torrent._updateWire = held.previousWireUpdate;
|
|
110
101
|
byTorrent.delete(torrent);
|
|
111
102
|
live.delete(held);
|
|
112
103
|
}
|
|
@@ -125,18 +116,21 @@ export function reconcileAll() {
|
|
|
125
116
|
if (publishingSelections) return { torrents: live.size, speculativeAllowed: false, stated: 0, withdrawn: 0 };
|
|
126
117
|
const entries = [...live];
|
|
127
118
|
const speculativeAllowed = !entries.some((entry) => entry.selection.hasUrgentMissing());
|
|
119
|
+
const bands=entries.map(entry=>entry.selection.missingBand()).filter(Boolean);
|
|
120
|
+
const maximumUrgency=bands.length?Math.min(...bands.map(band=>band.urgency)):Infinity;
|
|
121
|
+
const minimumPriority=bands.length?Math.max(...bands.filter(band=>band.urgency===maximumUrgency).map(band=>band.priority)):1;
|
|
122
|
+
const latestDeadlineAt=bands.length?Math.min(...bands.filter(band=>band.urgency===maximumUrgency&&band.priority===minimumPriority).map(band=>band.deadlineAt)):Infinity;
|
|
128
123
|
let stated = 0;
|
|
129
124
|
let withdrawn = 0;
|
|
130
125
|
publishingSelections = true;
|
|
131
126
|
try {
|
|
132
127
|
for (const entry of entries) {
|
|
133
|
-
const result = entry.selection.reconcile({ speculativeAllowed,
|
|
128
|
+
const result = entry.selection.reconcile({ speculativeAllowed, maximumUrgency, minimumPriority, latestDeadlineAt });
|
|
134
129
|
stated += result.stated;
|
|
135
130
|
withdrawn += result.withdrawn;
|
|
136
131
|
}
|
|
137
132
|
} finally {
|
|
138
133
|
publishingSelections = false;
|
|
139
134
|
}
|
|
140
|
-
scheduler.reconcile();
|
|
141
135
|
return { torrents: entries.length, speculativeAllowed, stated, withdrawn };
|
|
142
136
|
}
|
|
@@ -23,7 +23,7 @@ export class DownloadMaps {
|
|
|
23
23
|
const interval = zone.downloadInterval ?? zone;
|
|
24
24
|
const current = file.sourceZones.find(candidate => candidate.from === interval.from && candidate.to === interval.to);
|
|
25
25
|
const deadlineAt = current && Number.isFinite(current.deadlineAt)
|
|
26
|
-
? current.deadlineAt - (zone.leadSeconds ?? 0) * 1000 : current?.deadlineAt;
|
|
26
|
+
? current.deadlineAt + (Math.max(0, zone.from - current.from) - (zone.leadSeconds ?? 0)) * 1000 : current?.deadlineAt;
|
|
27
27
|
if (!current || Object.keys(current).some(key => key !== "from" && key !== "to" && !Object.is(zone[key], key === "deadlineAt" ? deadlineAt : current[key]))) changed = true;
|
|
28
28
|
return current ? [{ ...zone, ...current, from: zone.from, to: zone.to, deadlineAt }] : [];
|
|
29
29
|
});
|
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import { mapForViewer, mergeMaps, runsOf } from "./PriorityMap.js";
|
|
2
2
|
|
|
3
|
+
/** Price original-file cluster ranges without requiring a packet index. */
|
|
4
|
+
export async function nativeOriginalSourceMap({ container, viewers, durationSeconds, startTimeSeconds = 0,
|
|
5
|
+
allowanceSeconds, urgentReadyFor, now = Date.now() }) {
|
|
6
|
+
if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return [];
|
|
7
|
+
const maps = viewers.map(viewer => mapForViewer({
|
|
8
|
+
atSeconds: viewer.positionSeconds(now) ?? 0, durationSeconds, allowanceSeconds,
|
|
9
|
+
playing: viewer.playing || viewer.waiting, viewerCount: viewers.length,
|
|
10
|
+
pauseSeconds: viewer.pausedAt === null ? 0 : (now - viewer.pausedAt) / 1000,
|
|
11
|
+
urgentReady: urgentReadyFor(viewer)
|
|
12
|
+
}));
|
|
13
|
+
const zones = [];
|
|
14
|
+
for (const zone of runsOf(mergeMaps(maps))) {
|
|
15
|
+
const input = await container.readMappedSourceRanges({ from: zone.from + startTimeSeconds,
|
|
16
|
+
to: Math.min(durationSeconds, zone.to) + startTimeSeconds });
|
|
17
|
+
if (input.kind !== "result") continue;
|
|
18
|
+
const links = viewers.filter((_viewer, index) => maps[index].priority[zone.from] === zone.priority)
|
|
19
|
+
.map(viewer => viewer.linkReading()?.linkMbps).filter(rate => Number.isFinite(rate) && rate > 0);
|
|
20
|
+
const bytes = input.ranges.reduce((sum, [start, end]) => sum + end - start + 1, 0);
|
|
21
|
+
const deliverySeconds = links.length ? bytes * 8 / (Math.min(...links) * 1_000_000) : 0;
|
|
22
|
+
for (const [byteStart, byteEnd] of input.ranges) zones.push({ ...zone, byteStart, byteEnd,
|
|
23
|
+
deadlineAt: Number.isFinite(zone.withinSeconds) ? now + (zone.withinSeconds - deliverySeconds) * 1000 : Infinity });
|
|
24
|
+
}
|
|
25
|
+
return zones;
|
|
26
|
+
}
|
|
27
|
+
|
|
3
28
|
/** Price native source packets with the same viewer map as encoded outputs. */
|
|
4
29
|
export function nativeSourceMap({ index, tracks, viewers, durationSeconds, startTimeSeconds = 0,
|
|
5
30
|
allowanceSeconds, urgentReadyFor, now = Date.now() }) {
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
* @property {number} to - Last second, exclusive.
|
|
84
84
|
* @property {number} priority
|
|
85
85
|
* @property {number} withinSeconds
|
|
86
|
+
* @property {boolean} urgent - Required preparation before playback can continue.
|
|
86
87
|
* @property {boolean} behind - Whether this lies behind every viewer.
|
|
87
88
|
*/
|
|
88
89
|
|