@torrent-tv/proxy 2.37.1 → 2.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/services/container-index/mp4.js +119 -7
- package/services/torrent-worker/fastest-wires.js +71 -0
- package/services/torrent-worker/piece-reader.js +26 -1
- package/test/mp4-composition-times.test.js +0 -0
- package/test/piece-tail.test.js +97 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 2.38.1
|
|
2
|
+
|
|
3
|
+
- **New**: When a blocked piece cannot be steered anywhere, the wait line says what is holding it. The steering added in 2.29.0 often places nothing — `steered onto 0 of 9 asks (8 peers held it)`, measured 2026-08-18 while eight peers had the piece — because every block is already reserved and WebTorrent will not hand out a second request for the same block (`Piece.reserve()` answers -1; the only mention of an endgame in the library is a commented-out line). Duplicating those blocks is the standard remedy and costs a block's traffic each time, so this measures the tail before anything is built on it: `tail 2/512 blocks missing, held by 1@12KB/s 1@900KB/s`, slowest wire first, and `held by nobody` when the piece has not been asked for at all. Sampled at the instant an attempt placed nothing rather than once at the start, so the numbers and the reason they are printed describe the same moment. If the missing blocks turn out to sit on one slow wire, duplication is aimed at the right thing; if they are spread across fast ones, the wait has another cause and that work should not be done.
|
|
4
|
+
|
|
5
|
+
## 2.38.0
|
|
6
|
+
|
|
7
|
+
- **Fix**: An MP4's keyframe times are read as composition times, on the track the handler names. Two faults, both measured on real releases over the swarm (`research/mp4-composition-times-2026-08-19.md`). (1) The reader took sample times from `stts`, which is DECODE order, and used neither `ctts` nor `elst`: ISO/IEC 14496-12 says `CT(n) = DT(n) + CTTS(n)` (§8.6.1.3) and the edit list then shifts that (§8.6.6.3). Every LostFilm MP4 measured carries a composition offset AND an edit list cancelling it exactly, which is why decode times had been right on them; `Firefly.S01E03.720p.mp4` carries the same 2002-tick offset with NO edit list, and its times were **62.1 ms early on all 34 keyframes** compared against ffmpeg's own `pts_time` — a constant that closes to four decimals as offset (0.08342 s) minus the container start (0.02133 s). After the fix that file matches ffmpeg to the container start, which `computeSegmentBoundaries` already subtracts, and `Superman.720p` — where the terms cancel — is unchanged and exact to 0.0000 s. Version 1 offsets are read as SIGNED, which is what that version exists for; an empty edit (`media_time = -1`) is skipped rather than treated as a shift. (2) The video track was "the first one carrying sync samples", and the handler was never read. That worked only because all seven measured files put video first; the standard identifies a track by `hdlr`, and a file whose audio track carries sync samples, or one leading with a cover-art video track, would have been read from the wrong place — the same defect fixed in the Matroska reader the day before, arrived at from the other side.
|
|
8
|
+
|
|
1
9
|
## 2.37.1
|
|
2
10
|
|
|
3
11
|
- **Fix**: The cost of a seek no longer counts against the quality offer. `requiredSpeed` — the speed a step must sustain to survive a swarm — is built from the reader's interruptions, and the wait on the first piece after a JUMP is not one of them: those pieces have not been asked for yet and the encoder is restarting, so it measures the move, not the supply. Measured 2026-08-18: `proxy now offers 720p` landed 131 ms after a seek, collapsing a five-rung menu to one while the player was already hunting for a fragment, and another session churned `640p` → `640p 540p` → `640p 240p`. The wait is still reported, saying plainly that it belongs to the jump and is not counted, so a gap in the history cannot be mistaken for a swarm that never made the reader wait.
|
package/package.json
CHANGED
|
@@ -149,7 +149,7 @@ function findAllBoxes(buffer, start, end, type) {
|
|
|
149
149
|
* @param {Set<number>} wanted - Sample numbers (1-based).
|
|
150
150
|
* @returns {number[]} Seconds, ascending.
|
|
151
151
|
*/
|
|
152
|
-
function resolveSampleTimes(buffer, stts, timescale, wanted) {
|
|
152
|
+
function resolveSampleTimes(buffer, stts, timescale, wanted, offsets = null) {
|
|
153
153
|
const entryCount = buffer.readUInt32BE(stts.dataOffset + 4);
|
|
154
154
|
const times = [];
|
|
155
155
|
let sampleNumber = 1;
|
|
@@ -160,7 +160,10 @@ function resolveSampleTimes(buffer, stts, timescale, wanted) {
|
|
|
160
160
|
const delta = buffer.readUInt32BE(cursor + 4);
|
|
161
161
|
for (let index = 0; index < count; index += 1) {
|
|
162
162
|
if (wanted.has(sampleNumber)) {
|
|
163
|
-
|
|
163
|
+
// `CT(n) = DT(n) + CTTS(n)` — ISO/IEC 14496-12 §8.6.1.3. The offset is
|
|
164
|
+
// what turns decode order into the order frames are shown in, and it is
|
|
165
|
+
// the timeline ffmpeg cuts on.
|
|
166
|
+
times.push((ticks + (offsets?.get(sampleNumber) ?? 0)) / timescale);
|
|
164
167
|
}
|
|
165
168
|
ticks += delta;
|
|
166
169
|
sampleNumber += 1;
|
|
@@ -170,6 +173,100 @@ function resolveSampleTimes(buffer, stts, timescale, wanted) {
|
|
|
170
173
|
return times;
|
|
171
174
|
}
|
|
172
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Composition offsets for the sample numbers asked for.
|
|
178
|
+
*
|
|
179
|
+
* `ctts` is run-length encoded like `stts`, and version 1 carries SIGNED
|
|
180
|
+
* offsets — which is what the version exists for: a frame may be shown before
|
|
181
|
+
* it is decoded. Reading them as unsigned turns a small negative offset into
|
|
182
|
+
* roughly four billion ticks.
|
|
183
|
+
*
|
|
184
|
+
* @param {Buffer} buffer
|
|
185
|
+
* @param {{ dataOffset: number, end: number }} ctts
|
|
186
|
+
* @param {Set<number>} wanted - Sample numbers (1-based).
|
|
187
|
+
* @returns {Map<number, number>} Sample number to offset in media ticks.
|
|
188
|
+
*/
|
|
189
|
+
function readCompositionOffsets(buffer, ctts, wanted) {
|
|
190
|
+
const version = buffer[ctts.dataOffset];
|
|
191
|
+
const entryCount = buffer.readUInt32BE(ctts.dataOffset + 4);
|
|
192
|
+
const offsets = new Map();
|
|
193
|
+
let sampleNumber = 1;
|
|
194
|
+
let cursor = ctts.dataOffset + 8;
|
|
195
|
+
for (let entry = 0; entry < entryCount && cursor + 8 <= ctts.end; entry += 1) {
|
|
196
|
+
const count = buffer.readUInt32BE(cursor);
|
|
197
|
+
const offset = version === 1 ? buffer.readInt32BE(cursor + 4) : buffer.readUInt32BE(cursor + 4);
|
|
198
|
+
for (let index = 0; index < count; index += 1) {
|
|
199
|
+
if (wanted.has(sampleNumber)) {
|
|
200
|
+
offsets.set(sampleNumber, offset);
|
|
201
|
+
}
|
|
202
|
+
sampleNumber += 1;
|
|
203
|
+
}
|
|
204
|
+
cursor += 8;
|
|
205
|
+
}
|
|
206
|
+
return offsets;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* How far the edit list shifts this track's composition timeline, in media
|
|
211
|
+
* ticks.
|
|
212
|
+
*
|
|
213
|
+
* ISO/IEC 14496-12 §8.6.6.3: `media_time` is the start of the edit within the
|
|
214
|
+
* media, in the MEDIA timescale and in composition time, while
|
|
215
|
+
* `segment_duration` is in the MOVIE timescale — two different units in one
|
|
216
|
+
* structure, which is why only the first is read here. `media_time = -1` is an
|
|
217
|
+
* empty edit: it inserts blank presentation time and starts no media, so the
|
|
218
|
+
* first real edit is the one that matters.
|
|
219
|
+
*
|
|
220
|
+
* Measured 2026-08-19: every LostFilm MP4 that carries a composition offset
|
|
221
|
+
* also carries an edit list cancelling it exactly, which is why decode times
|
|
222
|
+
* have been right on those files. `Firefly.S01E03` has the offset and NO edit
|
|
223
|
+
* list, and its times were 62.1 ms early on all 34 keyframes checked.
|
|
224
|
+
*
|
|
225
|
+
* @param {Buffer} buffer
|
|
226
|
+
* @param {{ dataOffset: number, end: number }} elst
|
|
227
|
+
* @returns {number} Ticks to subtract; zero when nothing is shifted.
|
|
228
|
+
*/
|
|
229
|
+
function readEditShift(buffer, elst) {
|
|
230
|
+
const version = buffer[elst.dataOffset];
|
|
231
|
+
const entryCount = buffer.readUInt32BE(elst.dataOffset + 4);
|
|
232
|
+
const wide = version === 1;
|
|
233
|
+
const entryBytes = wide ? 20 : 12;
|
|
234
|
+
let cursor = elst.dataOffset + 8;
|
|
235
|
+
for (let entry = 0; entry < entryCount && cursor + entryBytes <= elst.end; entry += 1) {
|
|
236
|
+
const mediaTime = wide
|
|
237
|
+
? Number(buffer.readBigInt64BE(cursor + 8))
|
|
238
|
+
: buffer.readInt32BE(cursor + 4);
|
|
239
|
+
if (mediaTime >= 0) {
|
|
240
|
+
return mediaTime;
|
|
241
|
+
}
|
|
242
|
+
cursor += entryBytes;
|
|
243
|
+
}
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Whether this track's handler says it carries video.
|
|
249
|
+
*
|
|
250
|
+
* The standard identifies a track by its `hdlr`, and nothing else does. Picking
|
|
251
|
+
* "the first track that happens to carry sync samples" worked only because the
|
|
252
|
+
* seven releases measured all put video first; a file whose audio track carries
|
|
253
|
+
* them, or one that leads with a cover-art video track, would be read from the
|
|
254
|
+
* wrong place. That is the same defect that was fixed in the Matroska reader on
|
|
255
|
+
* 2026-08-18, arrived at from the other side.
|
|
256
|
+
*
|
|
257
|
+
* @param {Buffer} buffer
|
|
258
|
+
* @param {{ dataOffset: number, end: number }} mdia
|
|
259
|
+
* @returns {boolean}
|
|
260
|
+
*/
|
|
261
|
+
function isVideoTrack(buffer, mdia) {
|
|
262
|
+
const hdlr = findBox(buffer, mdia.dataOffset, mdia.end, "hdlr");
|
|
263
|
+
if (!hdlr || hdlr.dataOffset + 12 > hdlr.end) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
// FullBox header (4) then a reserved pre_defined (4), then the handler type.
|
|
267
|
+
return buffer.toString("latin1", hdlr.dataOffset + 8, hdlr.dataOffset + 12) === "vide";
|
|
268
|
+
}
|
|
269
|
+
|
|
173
270
|
/**
|
|
174
271
|
* Read the keyframe times of an MP4/MOV file.
|
|
175
272
|
*
|
|
@@ -189,12 +286,12 @@ export async function readMp4KeyframeTimes(readRange, fileSize) {
|
|
|
189
286
|
return null;
|
|
190
287
|
}
|
|
191
288
|
|
|
192
|
-
// Examine every track
|
|
289
|
+
// Examine every track, and take the one whose HANDLER says it is video. A
|
|
193
290
|
// track with no `stss` has every sample a keyframe, so it constrains nothing
|
|
194
|
-
// and is skipped.
|
|
291
|
+
// and is skipped even when it is the video one.
|
|
195
292
|
for (const trak of findAllBoxes(moov, moovBox.headerBytes, moov.length, "trak")) {
|
|
196
293
|
const mdia = findBox(moov, trak.dataOffset, trak.end, "mdia");
|
|
197
|
-
if (!mdia) {
|
|
294
|
+
if (!mdia || !isVideoTrack(moov, mdia)) {
|
|
198
295
|
continue;
|
|
199
296
|
}
|
|
200
297
|
const mdhd = findBox(moov, mdia.dataOffset, mdia.end, "mdhd");
|
|
@@ -237,9 +334,24 @@ export async function readMp4KeyframeTimes(readRange, fileSize) {
|
|
|
237
334
|
continue;
|
|
238
335
|
}
|
|
239
336
|
|
|
240
|
-
|
|
337
|
+
// The two terms that turn decode times into the timeline ffmpeg cuts on.
|
|
338
|
+
// Both are optional: a file without them is one whose decode and
|
|
339
|
+
// composition orders already agree, and then nothing is added or taken.
|
|
340
|
+
const ctts = findBox(moov, stbl.dataOffset, stbl.end, "ctts");
|
|
341
|
+
const offsets = ctts ? readCompositionOffsets(moov, ctts, wanted) : null;
|
|
342
|
+
const edts = findBox(moov, trak.dataOffset, trak.end, "edts");
|
|
343
|
+
const elst = edts && findBox(moov, edts.dataOffset, edts.end, "elst");
|
|
344
|
+
const editShift = elst ? readEditShift(moov, elst) : 0;
|
|
345
|
+
|
|
346
|
+
const times = resolveSampleTimes(moov, stts, timescale, wanted, offsets);
|
|
241
347
|
if (times.length > 0) {
|
|
242
|
-
|
|
348
|
+
// A shift applied after the division would be in the wrong units: the
|
|
349
|
+
// edit's `media_time` is in MEDIA ticks, like everything else here.
|
|
350
|
+
const shifted = editShift === 0 ? times : times.map((time) => time - editShift / timescale);
|
|
351
|
+
// A negative time is not a position in the file. It happens when an edit
|
|
352
|
+
// starts later than a keyframe the table lists, and those frames are not
|
|
353
|
+
// presented at all.
|
|
354
|
+
return shifted.filter((time) => time >= 0);
|
|
243
355
|
}
|
|
244
356
|
}
|
|
245
357
|
return null;
|
|
@@ -126,3 +126,74 @@ export function askFastestWiresFor(torrent, pieceIndex, limit = 3) {
|
|
|
126
126
|
fastestBytesPerSecond: candidates.length > 0 ? speedOf(candidates[0]) : 0
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* What is actually holding up a piece the reader is blocked on.
|
|
132
|
+
*
|
|
133
|
+
* The steering above can only move a block that the library will hand over, and
|
|
134
|
+
* the field says it often hands over nothing: `steered onto 0 of 9 asks (8
|
|
135
|
+
* peers held it)`, recorded 2026-08-18 while eight peers had the piece. When
|
|
136
|
+
* every block of a piece is already reserved, `Piece.reserve()` answers -1 and
|
|
137
|
+
* there is no request left to place — the read then ends when the SLOWEST
|
|
138
|
+
* holder delivers its block, however fast the rest of the swarm is.
|
|
139
|
+
*
|
|
140
|
+
* Duplicating those last blocks onto faster wires is the standard remedy, and
|
|
141
|
+
* it is not free: every duplicate is a block's worth of traffic paid twice. So
|
|
142
|
+
* this describes the tail before anything is built with it — how many blocks
|
|
143
|
+
* are still missing, and on which wires they sit, with each wire's speed. If
|
|
144
|
+
* the missing blocks turn out to sit on one slow wire, duplication is aimed at
|
|
145
|
+
* exactly the right thing; if they are spread across fast ones, the wait has
|
|
146
|
+
* another cause and this work should not be done at all.
|
|
147
|
+
*
|
|
148
|
+
* Reads only: nothing here changes a reservation or places a request.
|
|
149
|
+
*
|
|
150
|
+
* @param {import("webtorrent").Torrent} torrent
|
|
151
|
+
* @param {number} pieceIndex
|
|
152
|
+
* @returns {{ chunks: number, missing: number,
|
|
153
|
+
* outstanding: Array<{ blocks: number, bytesPerSecond: number, choking: boolean }> } | null}
|
|
154
|
+
* Null only when the piece object is gone — it completed and was cleared.
|
|
155
|
+
* A piece nobody has reserved a block of yet is NOT null: `torrent-piece`
|
|
156
|
+
* creates its buffer lazily on the first reserve, so a piece the picker has
|
|
157
|
+
* not reached reads as every block missing and nothing outstanding, which is
|
|
158
|
+
* the most informative answer this can give — the wait is not on a slow
|
|
159
|
+
* holder, it is on nobody having been asked.
|
|
160
|
+
*/
|
|
161
|
+
export function describePieceTail(torrent, pieceIndex) {
|
|
162
|
+
const piece = torrent?.pieces?.[pieceIndex];
|
|
163
|
+
if (!piece) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const buffer = Array.isArray(piece._buffer) ? piece._buffer : null;
|
|
167
|
+
const chunks = Number.isFinite(piece._chunks)
|
|
168
|
+
? piece._chunks
|
|
169
|
+
: (buffer ? buffer.length : 0);
|
|
170
|
+
// No buffer means no block of this piece has been reserved yet, so all of it
|
|
171
|
+
// is missing. Reading that as "no tail" hid the case worth seeing most.
|
|
172
|
+
let missing = chunks;
|
|
173
|
+
if (buffer) {
|
|
174
|
+
missing = 0;
|
|
175
|
+
for (let index = 0; index < chunks; index += 1) {
|
|
176
|
+
if (!buffer[index]) {
|
|
177
|
+
missing += 1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const wires = Array.isArray(torrent?.wires) ? torrent.wires : [];
|
|
183
|
+
const outstanding = [];
|
|
184
|
+
for (const wire of wires) {
|
|
185
|
+
const requests = Array.isArray(wire?.requests) ? wire.requests : [];
|
|
186
|
+
const blocks = requests.filter((request) => request?.piece === pieceIndex).length;
|
|
187
|
+
if (blocks > 0) {
|
|
188
|
+
outstanding.push({
|
|
189
|
+
blocks,
|
|
190
|
+
bytesPerSecond: speedOf(wire),
|
|
191
|
+
choking: wire?.peerChoking === true
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Slowest first: that is the wire the read is waiting on, and the one a
|
|
196
|
+
// duplicate would be aimed past.
|
|
197
|
+
outstanding.sort((left, right) => left.bytesPerSecond - right.bytesPerSecond);
|
|
198
|
+
return { chunks, missing, outstanding };
|
|
199
|
+
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { findSharedStore } from "../piece-store/shared-piece-store.js";
|
|
24
24
|
import { logger } from "../../utils/logger.js";
|
|
25
|
-
import { askFastestWiresFor, canPlaceRequests } from "./fastest-wires.js";
|
|
25
|
+
import { askFastestWiresFor, canPlaceRequests, describePieceTail } from "./fastest-wires.js";
|
|
26
26
|
import { minimumBufferFrom, requiredSpeedFrom } from "../supply-margin.js";
|
|
27
27
|
|
|
28
28
|
/** Only waits at least this long are reported; sequential reading stays silent. */
|
|
@@ -566,9 +566,18 @@ export async function* readFragments({
|
|
|
566
566
|
// of bandwidth and the reader still waited 1.0-4.5 s, 47 times in two
|
|
567
567
|
// minutes, on pieces five peers already had.
|
|
568
568
|
let pushed = { asked: 0, attempted: 0, considered: 0, fastestBytesPerSecond: 0 };
|
|
569
|
+
// The tail as it stood at an attempt that placed NOTHING — the state the
|
|
570
|
+
// duplication work has to answer, and the only one worth a line. Sampled
|
|
571
|
+
// at that instant rather than once up front, because the steering runs
|
|
572
|
+
// again every half second and the piece changes under it; the last such
|
|
573
|
+
// reading is kept, so the line describes the most recent failure.
|
|
574
|
+
let tailWhenNothingPlaced = null;
|
|
569
575
|
const pushToFastest = () => {
|
|
570
576
|
try {
|
|
571
577
|
const result = askFastestWiresFor(torrent, pieceIndex);
|
|
578
|
+
if (result.asked === 0) {
|
|
579
|
+
tailWhenNothingPlaced = describePieceTail(torrent, pieceIndex);
|
|
580
|
+
}
|
|
572
581
|
pushed = {
|
|
573
582
|
asked: pushed.asked + result.asked,
|
|
574
583
|
// Summed like the successes, so the line compares two totals over
|
|
@@ -586,6 +595,9 @@ export async function* readFragments({
|
|
|
586
595
|
if (canPlaceRequests(torrent)) {
|
|
587
596
|
pushToFastest();
|
|
588
597
|
} else {
|
|
598
|
+
// Nothing can be placed at all on this build, so the tail is the whole
|
|
599
|
+
// of the answer.
|
|
600
|
+
tailWhenNothingPlaced = describePieceTail(torrent, pieceIndex);
|
|
589
601
|
logger.warn(
|
|
590
602
|
"piece-reader: this webtorrent build offers no way to place a request; " +
|
|
591
603
|
"the blocked piece cannot be steered onto a faster peer"
|
|
@@ -657,6 +669,19 @@ export async function* readFragments({
|
|
|
657
669
|
`; steered onto ${pushed.asked} of ${pushed.attempted} asks (${pushed.considered} peers held it)` +
|
|
658
670
|
(pushed.fastestBytesPerSecond > 0
|
|
659
671
|
? `, fastest ${Math.round(pushed.fastestBytesPerSecond / 1024)}KB/s`
|
|
672
|
+
: "") +
|
|
673
|
+
// Only when the steering placed nothing, which is the case that
|
|
674
|
+
// decides whether duplicating the tail is worth building: it says
|
|
675
|
+
// how much of the piece is still missing and which wires are
|
|
676
|
+
// holding it, slowest first.
|
|
677
|
+
(tailWhenNothingPlaced
|
|
678
|
+
? `; tail ${tailWhenNothingPlaced.missing}/${tailWhenNothingPlaced.chunks} blocks missing, held by ` +
|
|
679
|
+
(tailWhenNothingPlaced.outstanding.length > 0
|
|
680
|
+
? tailWhenNothingPlaced.outstanding
|
|
681
|
+
.map((wire) => `${wire.blocks}@${Math.round(wire.bytesPerSecond / 1024)}KB/s` +
|
|
682
|
+
(wire.choking ? " (choking)" : ""))
|
|
683
|
+
.join(" ")
|
|
684
|
+
: "nobody")
|
|
660
685
|
: "")
|
|
661
686
|
);
|
|
662
687
|
}
|
|
Binary file
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What a blocked piece's tail looks like, before anything is built on it.
|
|
3
|
+
*
|
|
4
|
+
* The steering shipped in 2.29.0 often places nothing — `steered onto 0 of 9
|
|
5
|
+
* asks (8 peers held it)`, measured 2026-08-18 — because every block of the
|
|
6
|
+
* piece is already reserved and the library will not hand out a second request
|
|
7
|
+
* for the same block. Duplicating those blocks is the standard remedy and costs
|
|
8
|
+
* traffic, so the tail is described first: how much is missing, and on which
|
|
9
|
+
* wires it sits.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { describePieceTail } from "../services/torrent-worker/fastest-wires.js";
|
|
15
|
+
|
|
16
|
+
/** A torrent whose piece has some blocks in hand and some in flight. */
|
|
17
|
+
function torrentWith({ buffer, wires }) {
|
|
18
|
+
return {
|
|
19
|
+
pieces: [{ _buffer: buffer, _chunks: buffer.length }],
|
|
20
|
+
wires
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function wire({ has = true, blocks = 0, speed = 0, choking = false }) {
|
|
25
|
+
return {
|
|
26
|
+
peerPieces: { get: () => has },
|
|
27
|
+
requests: Array.from({ length: blocks }, () => ({ piece: 0 })),
|
|
28
|
+
downloadSpeed: () => speed,
|
|
29
|
+
peerChoking: choking
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test("the tail names how much is missing and who is holding it", () => {
|
|
34
|
+
const tail = describePieceTail(
|
|
35
|
+
torrentWith({
|
|
36
|
+
// Four blocks, two already in hand.
|
|
37
|
+
buffer: [new Uint8Array(1), new Uint8Array(1), null, null],
|
|
38
|
+
wires: [
|
|
39
|
+
wire({ blocks: 1, speed: 900_000 }),
|
|
40
|
+
wire({ blocks: 1, speed: 12_000 }),
|
|
41
|
+
wire({ has: true, blocks: 0, speed: 5_000_000 })
|
|
42
|
+
]
|
|
43
|
+
}),
|
|
44
|
+
0
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
assert.equal(tail.missing, 2);
|
|
48
|
+
assert.equal(tail.chunks, 4);
|
|
49
|
+
assert.equal(tail.outstanding.length, 2, "only the wires actually holding a block of it");
|
|
50
|
+
assert.equal(
|
|
51
|
+
tail.outstanding[0].bytesPerSecond,
|
|
52
|
+
12_000,
|
|
53
|
+
"slowest first — that is the wire the read is waiting on"
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("a piece nobody is fetching reads as entirely missing, held by nobody", () => {
|
|
58
|
+
const tail = describePieceTail(
|
|
59
|
+
torrentWith({ buffer: [null, null], wires: [wire({ has: false, blocks: 0 })] }),
|
|
60
|
+
0
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
assert.equal(tail.outstanding.length, 0);
|
|
64
|
+
assert.equal(tail.missing, 2, "and the missing count still says the piece is untouched");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("a piece no block of which has been reserved is described, not skipped", () => {
|
|
68
|
+
// `torrent-piece` builds its buffer lazily on the first reserve, so a piece
|
|
69
|
+
// the picker has not reached has none. Reading that as "no tail" hid the
|
|
70
|
+
// clearest answer there is: the wait is on nobody having been asked.
|
|
71
|
+
const tail = describePieceTail(
|
|
72
|
+
{ pieces: [{ _buffer: null, _chunks: 512 }], wires: [wire({ has: true, blocks: 0, speed: 900_000 })] },
|
|
73
|
+
0
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
assert.equal(tail.missing, 512, "every block of it is missing");
|
|
77
|
+
assert.equal(tail.chunks, 512);
|
|
78
|
+
assert.equal(tail.outstanding.length, 0, "and not one of them has been asked for");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("a completed piece is not described at all", () => {
|
|
82
|
+
// WebTorrent nulls `pieces[index]` once the piece is verified and stored.
|
|
83
|
+
assert.equal(describePieceTail({ pieces: [null], wires: [] }, 0), null);
|
|
84
|
+
assert.equal(describePieceTail({ pieces: [], wires: [] }, 0), null);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("a wire that is choking us is named as such", () => {
|
|
88
|
+
const tail = describePieceTail(
|
|
89
|
+
torrentWith({
|
|
90
|
+
buffer: [null],
|
|
91
|
+
wires: [wire({ blocks: 1, speed: 100, choking: true })]
|
|
92
|
+
}),
|
|
93
|
+
0
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
assert.equal(tail.outstanding[0].choking, true, "a block reserved by a choking wire is going nowhere");
|
|
97
|
+
});
|