@torrent-tv/proxy 2.72.1 → 2.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/docs/container-architecture.md +66 -0
- package/package.json +1 -1
- package/routes/api/sources/warm/post.js +19 -0
- package/routes/stream/get.js +18 -1
- package/server.js +25 -1
- package/services/container/AviContainer.js +36 -0
- package/services/container/Container.js +41 -0
- package/services/container/MatroskaContainer.js +186 -1
- package/services/container/Mp4Container.js +158 -29
- package/services/container-index/ebml-reader.js +30 -0
- package/services/hls-session-manager.js +167 -37
- package/services/orchestrators/ContainerOrchestrator.js +22 -0
- package/services/piece-store/shared-piece-store.js +1512 -1502
- package/services/torrent-pool.js +46 -0
- package/services/torrent-worker/client.js +21 -0
- package/services/torrent-worker/container-tracks.js +134 -0
- package/services/torrent-worker/fastest-wires.js +29 -6
- package/services/torrent-worker/piece-reader.js +11 -4
- package/services/torrent-worker/pool-adapter.js +35 -0
- package/services/torrent-worker/protocol.js +12 -0
- package/services/torrent-worker/worker.js +45 -1
- package/test/container-media-info.test.js +228 -0
- package/test/piece-store-reservations.test.js +31 -8
- package/test/resume-warm.test.js +39 -0
- package/test/tail-duplication.test.js +48 -4
|
@@ -156,7 +156,12 @@ test("a spill that fails gives back the slot the eviction claimed", async () =>
|
|
|
156
156
|
}
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
// A bound, because the failure this catches is a claim that never ends: without
|
|
160
|
+
// it the check does not fail, it HANGS, and `node --test` then cannot finish at
|
|
161
|
+
// all — which is what happened between 2026-08-31 and 2026-09-03 (roadmap item
|
|
162
|
+
// 54). The store gives up after PINNED_WAIT_MS, so 30 s is ample for the answer
|
|
163
|
+
// and short enough to be a failure rather than a stoppage.
|
|
164
|
+
test("a claim that cannot be met ends in an error, not in waiting for ever", { timeout: 30_000 }, async () => {
|
|
160
165
|
const disk = makeDisk({ holdWrites: true });
|
|
161
166
|
const { store } = makeStore({ pieces: 2, disk });
|
|
162
167
|
try {
|
|
@@ -183,7 +188,7 @@ test("a claim that cannot be met ends in an error, not in waiting for ever", asy
|
|
|
183
188
|
}
|
|
184
189
|
});
|
|
185
190
|
|
|
186
|
-
test("closing the store fails whoever is waiting for room", async () => {
|
|
191
|
+
test("closing the store fails whoever is waiting for room", { timeout: 30_000 }, async () => {
|
|
187
192
|
const disk = makeDisk({ holdWrites: true });
|
|
188
193
|
const { store } = makeStore({ pieces: 2, disk });
|
|
189
194
|
try {
|
|
@@ -194,7 +199,15 @@ test("closing the store fails whoever is waiting for room", async () => {
|
|
|
194
199
|
store.pin(1);
|
|
195
200
|
|
|
196
201
|
const waiting = put(store, 3);
|
|
197
|
-
|
|
202
|
+
// Either counter: the claim waits for the disk first — a block is in flight
|
|
203
|
+
// and the store is full, so evicting another piece would only raise the
|
|
204
|
+
// memory in use — and reaches the pinned wait five seconds later. Asking for
|
|
205
|
+
// `waitedForPins` alone named one of the two ways of waiting and timed out
|
|
206
|
+
// while the store was demonstrably doing the other.
|
|
207
|
+
await until(
|
|
208
|
+
() => store.stats().waitedForPins + store.stats().waitedForDisk > 0,
|
|
209
|
+
"the claim is waiting"
|
|
210
|
+
);
|
|
198
211
|
|
|
199
212
|
await new Promise((resolve) => store.close(resolve));
|
|
200
213
|
await assert.rejects(() => waiting, /closed/);
|
|
@@ -207,20 +220,30 @@ test("closing the store fails whoever is waiting for room", async () => {
|
|
|
207
220
|
}
|
|
208
221
|
});
|
|
209
222
|
|
|
210
|
-
test("a piece written back to memory is not resurrected on disk by its own spill", async () => {
|
|
223
|
+
test("a piece written back to memory is not resurrected on disk by its own spill", { timeout: 30_000 }, async () => {
|
|
211
224
|
const disk = makeDisk({ holdWrites: true });
|
|
212
|
-
const { store } = makeStore({ pieces:
|
|
225
|
+
const { store } = makeStore({ pieces: 4, disk });
|
|
213
226
|
try {
|
|
214
227
|
await put(store, 0);
|
|
215
228
|
await put(store, 1);
|
|
216
229
|
await put(store, 2);
|
|
217
230
|
|
|
218
231
|
// Piece 0 leaves memory because the machine's allowance fell; its write is
|
|
219
|
-
// still in flight. The allowance then recovers
|
|
220
|
-
//
|
|
232
|
+
// still in flight. The allowance then recovers to MORE than it was, so the
|
|
233
|
+
// piece coming back needs no eviction and no wait for that write.
|
|
234
|
+
//
|
|
235
|
+
// Four blocks, not three, and the reason is what this check is about. A
|
|
236
|
+
// block being written out is still memory in use, so with a ceiling of three
|
|
237
|
+
// and one block in flight the store is full, and the claim correctly waits
|
|
238
|
+
// for the disk — which the earlier setup did not allow for, so the check
|
|
239
|
+
// timed out on an admission policy it never meant to measure. Whether a
|
|
240
|
+
// piece arriving while its OWN spill is in flight should be admitted without
|
|
241
|
+
// taking a second block is a real question about the accounting and is
|
|
242
|
+
// roadmap item 9, not this check's subject: the subject is that the spill,
|
|
243
|
+
// when it completes, must not put the stale copy back on disk.
|
|
221
244
|
store.reviseGrowthCeiling(CHUNK * 2);
|
|
222
245
|
await until(() => disk.heldCount > 0, "the spill of piece 0 is in flight");
|
|
223
|
-
store.reviseGrowthCeiling(CHUNK *
|
|
246
|
+
store.reviseGrowthCeiling(CHUNK * 4);
|
|
224
247
|
|
|
225
248
|
// The swarm hands piece 0 back while that write is still going. The store
|
|
226
249
|
// must drop the disk copy AFTER the write has recorded it, not before —
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Where a viewer's resume position falls in the file, in bytes.
|
|
3
|
+
*
|
|
4
|
+
* The warm-up fetches a file's two edges, because that is what the codec probe
|
|
5
|
+
* reads. The region the VIEWER will resume at was asked for by nobody until the
|
|
6
|
+
* encoder opened its input — field 2026-09-03, 53 s after the Retry button on a
|
|
7
|
+
* cold torrent, and the piece it then needed took another 46.3 s to arrive.
|
|
8
|
+
* Turning the position into an offset is the only arithmetic in that path, so it
|
|
9
|
+
* is the only part with anything to get wrong.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
|
|
15
|
+
import { resumeByteOffset } from "../services/torrent-worker/container-tracks.js";
|
|
16
|
+
|
|
17
|
+
test("a position halfway through a film is halfway through its file", () => {
|
|
18
|
+
assert.equal(resumeByteOffset(1000, 100, 50), 500);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("the field case lands where the encoder went looking", () => {
|
|
22
|
+
// The measured session: 171.338 s into a 23:41 episode of 541 MB, and the
|
|
23
|
+
// read that blocked was at 63 MB.
|
|
24
|
+
const at = resumeByteOffset(541 * 1024 * 1024, 23 * 60 + 41, 171.338);
|
|
25
|
+
const megabytes = at / (1024 * 1024);
|
|
26
|
+
assert.ok(megabytes > 60 && megabytes < 68, `landed at ${megabytes.toFixed(1)}MB`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("a position past the end reads the end, rather than past it", () => {
|
|
30
|
+
const length = 1000;
|
|
31
|
+
const at = resumeByteOffset(length, 100, 10_000);
|
|
32
|
+
assert.equal(at, length - 1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("nothing is known, nothing is guessed", () => {
|
|
36
|
+
assert.equal(resumeByteOffset(0, 100, 50), 0, "no file length");
|
|
37
|
+
assert.equal(resumeByteOffset(1000, 0, 50), 0, "no duration — the container did not declare one");
|
|
38
|
+
assert.equal(resumeByteOffset(1000, 100, 0), 0, "the viewer is at the beginning, where the edges already are");
|
|
39
|
+
});
|
|
@@ -60,7 +60,7 @@ test("the missing blocks are freed and asked of a second wire", () => {
|
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
test("at most one duplicate per wire, however long the tail", () => {
|
|
63
|
-
const target = piece(512,
|
|
63
|
+
const target = piece(512, 16);
|
|
64
64
|
const torrent = {
|
|
65
65
|
pieces: [target],
|
|
66
66
|
wires: [wire(), wire()],
|
|
@@ -73,7 +73,29 @@ test("at most one duplicate per wire, however long the tail", () => {
|
|
|
73
73
|
assert.equal(target.cancelled.length, 2, "and no reservation is freed that nobody was asked for");
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
test("a
|
|
76
|
+
test("a piece still arriving normally is not duplicated at all", () => {
|
|
77
|
+
// Forty blocks outstanding is not a tail, it is a piece in transit. Asking
|
|
78
|
+
// for a second copy of it would spend the shared link on bytes that are
|
|
79
|
+
// already on their way — which is the whole difference between this and the
|
|
80
|
+
// 2-14 blocks a blocked reader was measured waiting on.
|
|
81
|
+
const target = piece(512, 40);
|
|
82
|
+
const torrent = {
|
|
83
|
+
pieces: [target],
|
|
84
|
+
wires: [wire(), wire()],
|
|
85
|
+
_request: () => true
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const result = duplicateTailFor(torrent, 0);
|
|
89
|
+
|
|
90
|
+
assert.equal(result.duplicated, 0);
|
|
91
|
+
assert.equal(target.cancelled.length, 0, "and nothing is freed either");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("a wire whose pipeline is full does not end the attempt", () => {
|
|
95
|
+
// Pipelines are per wire, so one wire being full says nothing about the next.
|
|
96
|
+
// This used to stop the whole pass on the first refusal, on the stated
|
|
97
|
+
// reasoning that the remaining wires were "no emptier" — an assumption about
|
|
98
|
+
// other peers' queues that nothing measures.
|
|
77
99
|
const target = piece(512, 5);
|
|
78
100
|
const torrent = {
|
|
79
101
|
pieces: [target],
|
|
@@ -88,11 +110,33 @@ test("a wire whose pipeline is full ends the attempt", () => {
|
|
|
88
110
|
assert.equal(result.duplicated, 0);
|
|
89
111
|
assert.equal(
|
|
90
112
|
target.cancelled.length,
|
|
91
|
-
|
|
92
|
-
"
|
|
113
|
+
3,
|
|
114
|
+
"every wire was offered a block; a freed reservation nobody took is handed to whoever asks next"
|
|
93
115
|
);
|
|
94
116
|
});
|
|
95
117
|
|
|
118
|
+
test("a refusal by one wire does not cost the block a faster wire would have taken", () => {
|
|
119
|
+
const target = piece(512, 3);
|
|
120
|
+
const placed = [];
|
|
121
|
+
const torrent = {
|
|
122
|
+
pieces: [target],
|
|
123
|
+
wires: [wire({ speed: 900_000 }), wire({ speed: 800_000 })],
|
|
124
|
+
_request: (wireAsked, index) => {
|
|
125
|
+
// The first wire is full; the second is not.
|
|
126
|
+
if (wireAsked === torrent.wires[0]) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
placed.push(index);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const result = duplicateTailFor(torrent, 0);
|
|
135
|
+
|
|
136
|
+
assert.equal(result.duplicated, 1);
|
|
137
|
+
assert.deepEqual(placed, [0], "the second wire was still asked");
|
|
138
|
+
});
|
|
139
|
+
|
|
96
140
|
test("a piece with nothing missing is left alone", () => {
|
|
97
141
|
const target = piece(4, 0);
|
|
98
142
|
const torrent = { pieces: [target], wires: [wire()], _request: () => true };
|