@torrent-tv/proxy 2.90.0 → 2.92.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/bin/cli.js +5 -2
- package/package.json +18 -2
- package/routes/api/playback-plan/post.js +21 -4
- package/routes/api/sources/files/get.js +20 -28
- package/routes/api/sources/post.js +4 -2
- package/routes/api/sources/stats/get.js +4 -11
- package/routes/api/sources/viewer/post.js +16 -0
- package/routes/api/sources/warm/post.js +6 -215
- package/routes/api/subtitles/get.js +22 -125
- package/routes/api/subtitles/post.js +26 -0
- package/routes/api/transcode-sessions/post.js +24 -2
- package/routes/media/get.js +80 -0
- package/routes/stream/get.js +26 -94
- package/routes/transcode/audio-warm/get.js +9 -21
- package/routes/transcode/variant-warm/get.js +8 -14
- package/server.js +410 -46
- package/services/encode/AdmittedInput.js +136 -0
- package/services/encode/CushionReport.js +1 -76
- package/services/encode/EncodeInputs.js +142 -0
- package/services/encode/EncodeOrchestrator.js +4 -1
- package/services/encode/EncodeRun.js +13 -4
- package/services/encode/EncodeRuns.js +132 -52
- package/services/encode/InputFailures.js +37 -0
- package/services/encode/MatroskaInput.js +158 -0
- package/services/encode/OutputOpening.js +16 -64
- package/services/encode/Renditions.js +1 -17
- package/services/encode/admitted-command.js +60 -0
- package/services/encode/annex-b-input.js +37 -0
- package/services/encode/bit-payload.js +29 -0
- package/services/encode/output/OutputCatalog.js +1 -1
- package/services/encode/output/Timeline.js +11 -0
- package/services/encode/pcm-normalization.js +30 -0
- package/services/encode/piece-completeness.js +54 -1
- package/services/encode/quality/HostLoad.js +19 -73
- package/services/encode/quality/QualityOffer.js +1 -1
- package/services/encode/run-command.js +1 -589
- package/services/encode/segment-formats/mp4-boxes.js +3 -1
- package/services/encode/segment-formats/presentation-segment.js +114 -0
- package/services/media/ContainerOrchestrator.js +118 -48
- package/services/media/KeyframeTables.js +16 -115
- package/services/media/MediaReadRequests.js +120 -0
- package/services/media/ProbeRequests.js +135 -0
- package/services/media/SegmentInputs.js +48 -0
- package/services/media/SourceFile.js +6 -0
- package/services/media/SubtitleOrchestrator.js +190 -4
- package/services/media/await-plan.js +47 -0
- package/services/media/container/AsfContainer.js +217 -0
- package/services/media/container/AviContainer.js +343 -34
- package/services/media/container/CadencedPictureOrder.js +45 -0
- package/services/media/container/Container.js +7 -3
- package/services/media/container/ContainerFactory.js +10 -3
- package/services/media/container/FfprobeContainer.js +222 -0
- package/services/media/container/MatroskaContainer.js +72 -46
- package/services/media/container/Mp4Container.js +472 -139
- package/services/media/container/MpegPsContainer.js +281 -0
- package/services/media/container/MpegTsContainer.js +277 -0
- package/services/media/container/PacketIndex.js +270 -0
- package/services/media/container/PacketRecords.js +237 -0
- package/services/media/container/RetainedBytes.js +39 -0
- package/services/media/container/RetainedReads.js +51 -0
- package/services/media/container/ac3-frame.js +46 -0
- package/services/media/container/asf-packets.js +153 -0
- package/services/media/container/avc-picture-order.js +50 -0
- package/services/media/container/avi-open-dml.js +47 -0
- package/services/media/container/dts-frame.js +51 -0
- package/services/media/container/ebml-stream.js +12 -1
- package/services/media/container/ffprobe-record.js +45 -0
- package/services/media/container/flv-packet-ranges.js +27 -0
- package/services/media/container/following-packet-time.js +37 -0
- package/services/media/container/h264-configuration.js +183 -0
- package/services/media/container/h264-elementary-index.js +245 -0
- package/services/media/container/hevc-configuration.js +165 -0
- package/services/media/container/hevc-elementary-index.js +132 -0
- package/services/media/container/hevc-picture-order.js +66 -0
- package/services/media/container/latm-frame.js +99 -0
- package/services/media/container/matroska-packets.js +207 -0
- package/services/media/container/memory-unavailable.js +8 -0
- package/services/media/container/mp3-packet-dependencies.js +41 -0
- package/services/media/container/mp4-fragments.js +148 -0
- package/services/media/container/mpeg-audio-frame.js +19 -0
- package/services/media/container/mpeg-audio-reservoir.js +27 -0
- package/services/media/container/mpeg-elementary-index.js +196 -0
- package/services/media/container/mpeg-sequence-reader.js +69 -0
- package/services/media/container/mpeg-ts-packets.js +93 -0
- package/services/media/container/mpeg-video-configuration.js +50 -0
- package/services/media/container/mpeg4-picture-timing.js +108 -0
- package/services/media/container/pes-header.js +34 -0
- package/services/media/playback-declarations.js +57 -0
- package/services/media/playback-planner.js +72 -230
- package/services/media/probe-packets.js +61 -0
- package/services/media/read-playback-declarations.js +18 -0
- package/services/media/tracks/AudioTrack.js +9 -0
- package/services/media/tracks/TextSubtitleTrack.js +4 -1
- package/services/server/OutputLifecycle.js +2 -0
- package/services/server/SegmentServing.js +40 -33
- package/services/server/ViewerRequests.js +22 -41
- package/services/server/controllers/PlaybackController.js +4 -0
- package/services/server/controllers/SubtitleController.js +8 -29
- package/services/server/transcode-session-files.js +58 -99
- package/services/server/wire-outputs.js +58 -15
- package/services/storage/IndexMemory.js +85 -0
- package/services/storage/MachineBudget.js +27 -3
- package/services/storage/allowance.js +8 -1
- package/services/storage/piece-store/piece-disk-store.js +18 -3
- package/services/storage/piece-store/piece-lru.js +8 -4
- package/services/storage/piece-store/shared-piece-store.js +26 -11
- package/services/storage/segment-store/SegmentStore.js +49 -18
- package/services/storage/wire.js +7 -0
- package/services/torrent/Contents.js +52 -20
- package/services/torrent/demand/DemandRegister.js +13 -4
- package/services/torrent/demand/Window.js +13 -2
- package/services/torrent/demand-progress.js +25 -0
- package/services/torrent/download/DeadlineScheduler.js +167 -0
- package/services/torrent/download/FutureDownload.js +166 -0
- package/services/torrent/download/SwarmSelection.js +30 -59
- package/services/torrent/download/registry.js +45 -5
- package/services/torrent/download/withdraw-claim.js +2 -0
- package/services/torrent/torrent-pool.js +39 -600
- package/services/torrent/worker/channel.js +8 -1
- package/services/torrent/worker/client.js +74 -72
- package/services/torrent/worker/files-in-use.js +9 -0
- package/services/torrent/worker/handle-state.js +12 -0
- package/services/torrent/worker/held-bytes.js +82 -99
- package/services/torrent/worker/held-file-bytes.js +17 -0
- package/services/torrent/worker/piece-reader.js +67 -683
- package/services/torrent/worker/pool-adapter.js +37 -123
- package/services/torrent/worker/protocol.js +6 -17
- package/services/torrent/worker/whole-file-stats.js +2 -0
- package/services/torrent/worker/worker.js +86 -156
- package/services/transport/data-channel-handler.js +56 -13
- package/services/transport/tunnel-client.js +111 -20
- package/services/viewer/DownloadMaps.js +168 -0
- package/services/viewer/NativeSourceMap.js +33 -0
- package/services/viewer/PriorityMap.js +49 -40
- package/services/viewer/PriorityOrchestrator.js +42 -13
- package/services/viewer/SourcePreparation.js +278 -0
- package/services/viewer/Viewer.js +11 -0
- package/services/viewer/Viewers.js +109 -2
- package/services/viewer/playback-readiness.js +50 -63
- package/services/viewer/segment-demands.js +37 -0
- package/services/viewer/urgent-output-ready.js +26 -0
- package/services/encode/run-inputs.js +0 -53
- package/services/media/keyframe-probe.js +0 -157
- package/services/media/media-info-probe.js +0 -93
- package/services/torrent/worker/background-fill.js +0 -213
- package/services/torrent/worker/resume-warm.js +0 -107
|
@@ -16,6 +16,7 @@ import { rmSync } from "node:fs";
|
|
|
16
16
|
import WebTorrent from "webtorrent";
|
|
17
17
|
import { logger } from "../../utils/logger.js";
|
|
18
18
|
import { pieceStoreOf } from "./piece-store-of.js";
|
|
19
|
+
import { demandProgress } from "./demand-progress.js";
|
|
19
20
|
import { Urgency, urgencyName } from "./demand/index.js";
|
|
20
21
|
import { demandFor, forgetTorrent, reconcileAll, hasUnmetDemand } from "./download/registry.js";
|
|
21
22
|
import { withdrawClaim } from "./download/withdraw-claim.js";
|
|
@@ -27,13 +28,10 @@ import { noteTorrentDestroyed } from "./destroyed-torrents.js";
|
|
|
27
28
|
const MAP_CLAIMANT = "priority-map";
|
|
28
29
|
|
|
29
30
|
/** Files of a selected torrent requested for background completion. */
|
|
30
|
-
const torrentFillEnabled = new WeakSet();
|
|
31
31
|
|
|
32
32
|
/** Claimant prefix for conditional whole-torrent fill demand. */
|
|
33
|
-
const TORRENT_FILL_CLAIMANT = "torrent-fill:";
|
|
34
33
|
|
|
35
34
|
/** How the two ends of a file name themselves. */
|
|
36
|
-
const EDGES_CLAIMANT = "file-edges";
|
|
37
35
|
|
|
38
36
|
// The DHT's entry points. Two of the three the library ships answer nothing —
|
|
39
37
|
// measured 2026-08-21 from the addon host: `router.bittorrent.com` and
|
|
@@ -81,21 +79,6 @@ const WEBTORRENT_STORE_ROOT = path.join(os.tmpdir(), "webtorrent");
|
|
|
81
79
|
// had gone.
|
|
82
80
|
const TORRENT_IDLE_TTL_MS = IDLE_KEEP_MS;
|
|
83
81
|
|
|
84
|
-
// Bytes ahead of a read position to mark CRITICAL on each range request. In
|
|
85
|
-
// WebTorrent, `critical` does NOT reorder the sequential piece scan — it enables
|
|
86
|
-
// HOTSWAP (re-request a block from a faster peer when a slower one already
|
|
87
|
-
// reserved it), so this is the near read-ahead cushion where stealing from slow
|
|
88
|
-
// peers pays off. The actual "download the seek target first" effect comes from
|
|
89
|
-
// deselecting the gap BEHIND the playhead (see prioritizeByteRange). Kept a
|
|
90
|
-
// moving window (reset each call) so criticality never accumulates over the
|
|
91
|
-
// whole file across seeks, which would make hotswap thrash.
|
|
92
|
-
const PRIORITY_WINDOW_BYTES = 16 * 1024 * 1024;
|
|
93
|
-
|
|
94
|
-
// The file's header/index region the codec probe needs (phase 1). Must match
|
|
95
|
-
// the ranges prefetchFileEdges fetches: leading bytes + trailing bytes.
|
|
96
|
-
const HEADER_HEAD_BYTES = 256 * 1024;
|
|
97
|
-
const HEADER_TAIL_BYTES = 2 * 1024 * 1024;
|
|
98
|
-
|
|
99
82
|
// Adaptive upload. Seeding to the BitTorrent swarm does not help our viewer (we
|
|
100
83
|
// deliver over our own WebRTC/HTTPS channel) — it is pure uplink cost and the
|
|
101
84
|
// riskiest legal act (active distribution). So the default is minimal: a
|
|
@@ -814,81 +797,6 @@ function withdrawPriorityMap(torrent, fileIndex, keepBelow = 0) {
|
|
|
814
797
|
return withdrawn;
|
|
815
798
|
}
|
|
816
799
|
|
|
817
|
-
/**
|
|
818
|
-
* WHERE A FILE SAYS WHAT IT IS: the piece at each of its ends.
|
|
819
|
-
*
|
|
820
|
-
* A container keeps its directory at one end or the other — `ftyp` and an EBML
|
|
821
|
-
* header at the front, and for an MP4 that was not written for streaming the
|
|
822
|
-
* `moov` at the very back. Nothing can be read of such a file until those have
|
|
823
|
-
* arrived, and they stay wanted for as long as it is open: a player asks for
|
|
824
|
-
* the file's shape again at every seek.
|
|
825
|
-
*
|
|
826
|
-
* **The size is not chosen, and it cannot be.** One byte is claimed at each
|
|
827
|
-
* end, and the piece is what the swarm delivers — so one byte at each end IS
|
|
828
|
-
* one piece at each end, with nothing rounded up by a number anybody picked.
|
|
829
|
-
* The 256 KB and 2 MB the prefetch reads are its own affair; where a directory
|
|
830
|
-
* is bigger than the piece it starts in, the read that needs it says so itself,
|
|
831
|
-
* at the level of a reader that is stopped.
|
|
832
|
-
*
|
|
833
|
-
* **Stated by the FILE, not by the read that happens to want it.** A read
|
|
834
|
-
* withdraws what it stated the moment it finishes, so until now the two ends of
|
|
835
|
-
* an open film were held by nothing at all once the codec probe was done — and
|
|
836
|
-
* they are exactly the pieces a seek needs and eviction is free to take.
|
|
837
|
-
*
|
|
838
|
-
* A function of a torrent rather than of the pool, like `leaveSwarm` beside it,
|
|
839
|
-
* so both can be exercised without building one.
|
|
840
|
-
*
|
|
841
|
-
* @param {object} torrent
|
|
842
|
-
* @param {number} fileIndex
|
|
843
|
-
* @param {number} urgency - NEAR while somebody is waiting for them, TAIL to
|
|
844
|
-
* keep them afterwards.
|
|
845
|
-
* @returns {boolean} Whether anything was stated.
|
|
846
|
-
*/
|
|
847
|
-
export function stateFileEdges(torrent, fileIndex, urgency, { lower = false } = {}) {
|
|
848
|
-
const file = Array.isArray(torrent?.files) ? torrent.files[fileIndex] : null;
|
|
849
|
-
const length = Number(file?.length);
|
|
850
|
-
if (!file || !(length > 0)) {
|
|
851
|
-
return false;
|
|
852
|
-
}
|
|
853
|
-
const { register } = demandFor(torrent);
|
|
854
|
-
const ends = [
|
|
855
|
-
{ claimant: `${EDGES_CLAIMANT}:${fileIndex}:head`, byteStart: 0, byteEnd: 0 },
|
|
856
|
-
{ claimant: `${EDGES_CLAIMANT}:${fileIndex}:tail`, byteStart: length - 1, byteEnd: length - 1 }
|
|
857
|
-
];
|
|
858
|
-
for (const end of ends) {
|
|
859
|
-
// RAISED, NEVER LOWERED, unless the caller is the one putting them back
|
|
860
|
-
// down. Two things ask for the same ends — a warm-up that nobody is waiting
|
|
861
|
-
// for and a playback plan that somebody is — and the warm-up's sidecars are
|
|
862
|
-
// fired off without being awaited, so it can arrive second. Taking the
|
|
863
|
-
// plan's urgency away there would leave a person watching a loading screen
|
|
864
|
-
// behind a film somebody else is watching.
|
|
865
|
-
const stated = register.windows().find((window) => String(window.claimant) === end.claimant);
|
|
866
|
-
const level = !lower && stated ? Math.min(stated.urgency, urgency) : urgency;
|
|
867
|
-
register.state({ ...end, fileIndex, urgency: level });
|
|
868
|
-
}
|
|
869
|
-
return true;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
/**
|
|
873
|
-
* Give up the ends of a file nobody has any use for.
|
|
874
|
-
*
|
|
875
|
-
* @param {object} torrent
|
|
876
|
-
* @param {number} fileIndex
|
|
877
|
-
* @returns {number} How many were withdrawn.
|
|
878
|
-
*/
|
|
879
|
-
export function withdrawFileEdges(torrent, fileIndex) {
|
|
880
|
-
const { register } = demandFor(torrent);
|
|
881
|
-
let withdrawn = 0;
|
|
882
|
-
for (const end of ["head", "tail"]) {
|
|
883
|
-
const claimant = `${EDGES_CLAIMANT}:${fileIndex}:${end}`;
|
|
884
|
-
if (register.windows().some((window) => String(window.claimant) === claimant)) {
|
|
885
|
-
register.withdraw(claimant);
|
|
886
|
-
withdrawn += 1;
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
return withdrawn;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
800
|
/**
|
|
893
801
|
* IS ANYTHING WANTED OF THIS TORRENT — the one question asked about a torrent's
|
|
894
802
|
* life, and it is answered by what has been stated, never by counting who is
|
|
@@ -966,17 +874,6 @@ export class TorrentPool {
|
|
|
966
874
|
*/
|
|
967
875
|
#lastAccess = new Map();
|
|
968
876
|
|
|
969
|
-
/**
|
|
970
|
-
* Last byte offset each active reader is streaming from, keyed by torrent then
|
|
971
|
-
* fileIndex. Set by prioritizeByteRange on every /stream range request; read by
|
|
972
|
-
* getFileStats to report how much of the window ahead of the read head is still
|
|
973
|
-
* to download — the "amount left to resume" shown while buffering.
|
|
974
|
-
*
|
|
975
|
-
* @type {Map<import("webtorrent").Torrent, Map<number, number>>}
|
|
976
|
-
*/
|
|
977
|
-
#readPositionByTorrent = new Map();
|
|
978
|
-
|
|
979
|
-
|
|
980
877
|
/** When each torrent's download first fell below the stall threshold. */
|
|
981
878
|
#stallSince = new Map();
|
|
982
879
|
/** When each torrent's stall was last reported, so it is not repeated hotly. */
|
|
@@ -988,7 +885,6 @@ export class TorrentPool {
|
|
|
988
885
|
*
|
|
989
886
|
* @type {Map<string, Promise<void>>}
|
|
990
887
|
*/
|
|
991
|
-
#edgePrefetches = new Map();
|
|
992
888
|
|
|
993
889
|
/** Memory budget per torrent for resident pieces; undefined = store default. */
|
|
994
890
|
#memoryBytes;
|
|
@@ -1312,77 +1208,6 @@ export class TorrentPool {
|
|
|
1312
1208
|
return { ranges, missing };
|
|
1313
1209
|
}
|
|
1314
1210
|
|
|
1315
|
-
/**
|
|
1316
|
-
* State what is worth fetching once nothing urgent is missing: the rest of
|
|
1317
|
-
* each file being read, from the furthest window in THAT file to its end.
|
|
1318
|
-
*
|
|
1319
|
-
* Per file, and that is a fix rather than a detail. It used to take the
|
|
1320
|
-
* furthest window across ALL files and the last piece across ALL files and
|
|
1321
|
-
* claim everything between: with two viewers on two episodes of one release —
|
|
1322
|
-
* the ordinary case for a season pack — that claimed every episode lying
|
|
1323
|
-
* between them, none of which anybody had asked for.
|
|
1324
|
-
*
|
|
1325
|
-
* Whether it is stated at all is not decided here. It is a level of urgency
|
|
1326
|
-
* like any other, and the swarm layer withholds it while anything urgent, on
|
|
1327
|
-
* ANY torrent, is still missing.
|
|
1328
|
-
*
|
|
1329
|
-
* @param {import("webtorrent").Torrent} torrent
|
|
1330
|
-
* @returns {void}
|
|
1331
|
-
*/
|
|
1332
|
-
#stateBackgroundFill(torrent) {
|
|
1333
|
-
const { register } = demandFor(torrent);
|
|
1334
|
-
const pieceLength = Number(torrent.pieceLength);
|
|
1335
|
-
if (!Number.isFinite(pieceLength) || pieceLength <= 0) {
|
|
1336
|
-
return;
|
|
1337
|
-
}
|
|
1338
|
-
if (torrentFillEnabled.has(torrent)) {
|
|
1339
|
-
for (const [fileIndex, file] of (torrent.files ?? []).entries()) {
|
|
1340
|
-
const claimant = `${TORRENT_FILL_CLAIMANT}${fileIndex}`;
|
|
1341
|
-
const length = Number(file?.length);
|
|
1342
|
-
if (!(length > 0) || file.done === true) {
|
|
1343
|
-
register.withdraw(claimant);
|
|
1344
|
-
continue;
|
|
1345
|
-
}
|
|
1346
|
-
register.state({
|
|
1347
|
-
claimant,
|
|
1348
|
-
fileIndex,
|
|
1349
|
-
byteStart: 0,
|
|
1350
|
-
byteEnd: length - 1,
|
|
1351
|
-
urgency: Urgency.TAIL
|
|
1352
|
-
});
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
// The files anything is stated for — which is the same list the reader
|
|
1356
|
-
// counts used to give and is one fact rather than two.
|
|
1357
|
-
for (const fileIndex of register.files()) {
|
|
1358
|
-
const file = torrent.files?.[fileIndex] ?? null;
|
|
1359
|
-
const claimant = `background-fill:${fileIndex}`;
|
|
1360
|
-
if (!file) {
|
|
1361
|
-
register.withdraw(claimant);
|
|
1362
|
-
continue;
|
|
1363
|
-
}
|
|
1364
|
-
const wanted = register
|
|
1365
|
-
.windows()
|
|
1366
|
-
.filter((window) => window.fileIndex === fileIndex && window.claimant !== claimant);
|
|
1367
|
-
const furthest = wanted.length === 0
|
|
1368
|
-
? -1
|
|
1369
|
-
: Math.max(...wanted.map((window) => window.byteEnd));
|
|
1370
|
-
const byteStart = furthest + 1;
|
|
1371
|
-
const byteEnd = Number(file.length) - 1;
|
|
1372
|
-
if (wanted.length === 0 || byteStart > byteEnd) {
|
|
1373
|
-
register.withdraw(claimant);
|
|
1374
|
-
continue;
|
|
1375
|
-
}
|
|
1376
|
-
register.state({
|
|
1377
|
-
claimant,
|
|
1378
|
-
fileIndex,
|
|
1379
|
-
byteStart,
|
|
1380
|
-
byteEnd,
|
|
1381
|
-
urgency: Urgency.TAIL
|
|
1382
|
-
});
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
1211
|
/**
|
|
1387
1212
|
* Take the priority map for one file and state what to fetch from it.
|
|
1388
1213
|
*
|
|
@@ -1412,7 +1237,6 @@ export class TorrentPool {
|
|
|
1412
1237
|
withdrawPriorityMap(torrent, fileIndex);
|
|
1413
1238
|
// The ends of the file go with it. They are kept for as long as the file
|
|
1414
1239
|
// is open, and this is what says it is not.
|
|
1415
|
-
withdrawFileEdges(torrent, fileIndex);
|
|
1416
1240
|
// And this may have been the last thing anybody wanted of this torrent.
|
|
1417
1241
|
// Optional because these two are functions of a torrent and are exercised
|
|
1418
1242
|
// as such; called on the pool, the pool also acts on what they said.
|
|
@@ -1422,14 +1246,15 @@ export class TorrentPool {
|
|
|
1422
1246
|
const file = Array.isArray(torrent?.files) ? torrent.files[fileIndex] : null;
|
|
1423
1247
|
const length = Number(file?.length);
|
|
1424
1248
|
const duration = Number(durationSeconds);
|
|
1425
|
-
if (!file || !(length > 0) || !
|
|
1249
|
+
if (!file || !(length > 0) || !Array.isArray(zones)) {
|
|
1426
1250
|
return;
|
|
1427
1251
|
}
|
|
1428
1252
|
const { register } = demandFor(torrent);
|
|
1429
1253
|
// Highest number first, so the stretch a viewer reaches soonest is stated
|
|
1430
1254
|
// first and the rest fall in behind it.
|
|
1431
1255
|
const ordered = [...zones]
|
|
1432
|
-
.filter((zone) => Number.
|
|
1256
|
+
.filter((zone) => Number.isSafeInteger(zone?.byteStart) && Number.isSafeInteger(zone?.byteEnd) &&
|
|
1257
|
+
zone.byteStart >= 0 && zone.byteEnd >= zone.byteStart && zone.byteStart < length)
|
|
1433
1258
|
.sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
|
|
1434
1259
|
/**
|
|
1435
1260
|
* Which level of urgency one zone is stated at, from the map's own number
|
|
@@ -1463,23 +1288,20 @@ export class TorrentPool {
|
|
|
1463
1288
|
// The scale is that layer's, and the numbers inside a band mean nothing
|
|
1464
1289
|
// but their order.
|
|
1465
1290
|
const priority = zone.priority ?? 0;
|
|
1466
|
-
if (isBehindEverybody(priority)) {
|
|
1291
|
+
if (zone.behind === true || (zone.behind === undefined && isBehindEverybody(priority))) {
|
|
1467
1292
|
return Urgency.BEHIND;
|
|
1468
1293
|
}
|
|
1469
|
-
if (isNobodyComingNow(priority)) {
|
|
1294
|
+
if (zone.deferred === true || (zone.deferred === undefined && isNobodyComingNow(priority))) {
|
|
1470
1295
|
// In front of somebody who has stopped the picture, and of nobody who is
|
|
1471
1296
|
// watching. Wanted, and wanted after everyone who is on their way.
|
|
1472
1297
|
return Urgency.TAIL;
|
|
1473
1298
|
}
|
|
1474
|
-
return isAtAWatchingViewer(priority) ? Urgency.NEAR : Urgency.AHEAD;
|
|
1299
|
+
return (zone.urgent === true || (zone.urgent === undefined && isAtAWatchingViewer(priority))) ? Urgency.NEAR : Urgency.AHEAD;
|
|
1475
1300
|
};
|
|
1476
1301
|
ordered.forEach((zone, index) => {
|
|
1477
|
-
//
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
// all this decides.
|
|
1481
|
-
const byteStart = Math.max(0, Math.floor((zone.from / duration) * length));
|
|
1482
|
-
const byteEnd = Math.min(length - 1, Math.ceil((zone.to / duration) * length) - 1);
|
|
1302
|
+
// Only ranges declared by the container index or metadata read are wanted.
|
|
1303
|
+
const byteStart = zone.byteStart;
|
|
1304
|
+
const byteEnd = Math.min(length - 1, zone.byteEnd);
|
|
1483
1305
|
if (byteEnd < byteStart) {
|
|
1484
1306
|
return;
|
|
1485
1307
|
}
|
|
@@ -1488,7 +1310,12 @@ export class TorrentPool {
|
|
|
1488
1310
|
fileIndex,
|
|
1489
1311
|
byteStart,
|
|
1490
1312
|
byteEnd,
|
|
1491
|
-
urgency: levelOf(zone)
|
|
1313
|
+
urgency: levelOf(zone),
|
|
1314
|
+
priority: Number.isFinite(zone.priority) ? Math.max(1, Math.min(100, zone.priority)) : 1,
|
|
1315
|
+
order: Number.isSafeInteger(zone.order) && zone.order >= 0 ? zone.order : 0,
|
|
1316
|
+
deadlineAt: Number.isFinite(zone.deadlineAt) ? zone.deadlineAt :
|
|
1317
|
+
Number.isFinite(zone.withinSeconds) ? Date.now() + Math.max(0, zone.withinSeconds) * 1000 : Number.POSITIVE_INFINITY,
|
|
1318
|
+
requestId: zone.requestId ?? `${torrent.infoHash}:${fileIndex}:map`
|
|
1492
1319
|
});
|
|
1493
1320
|
});
|
|
1494
1321
|
// Bands the map no longer has: a viewer moved on, and what they had wanted
|
|
@@ -1507,10 +1334,10 @@ export class TorrentPool {
|
|
|
1507
1334
|
const byLevel = new Map();
|
|
1508
1335
|
for (const zone of ordered) {
|
|
1509
1336
|
const level = levelOf(zone);
|
|
1510
|
-
const
|
|
1337
|
+
const byteCount = Math.max(0, Math.min(length - 1, zone.byteEnd) - zone.byteStart + 1);
|
|
1511
1338
|
const held = byLevel.get(level) ?? { zones: 0, megabytes: 0 };
|
|
1512
1339
|
held.zones += 1;
|
|
1513
|
-
held.megabytes +=
|
|
1340
|
+
held.megabytes += byteCount / 1048576;
|
|
1514
1341
|
byLevel.set(level, held);
|
|
1515
1342
|
}
|
|
1516
1343
|
const shape = [...byLevel.entries()]
|
|
@@ -1530,23 +1357,6 @@ export class TorrentPool {
|
|
|
1530
1357
|
}
|
|
1531
1358
|
}
|
|
1532
1359
|
|
|
1533
|
-
/**
|
|
1534
|
-
* Enable conditional whole-torrent download for a selected source.
|
|
1535
|
-
*
|
|
1536
|
-
* @param {import("webtorrent").Torrent} torrent
|
|
1537
|
-
* @returns {boolean}
|
|
1538
|
-
*/
|
|
1539
|
-
fillTorrentAsCapacityAllows(torrent) {
|
|
1540
|
-
if (!torrent || !Array.isArray(torrent.files) || torrent.destroyed) {
|
|
1541
|
-
return false;
|
|
1542
|
-
}
|
|
1543
|
-
torrentFillEnabled.add(torrent);
|
|
1544
|
-
this.#stateBackgroundFill(torrent);
|
|
1545
|
-
this.followTheDemand(torrent);
|
|
1546
|
-
reconcileAll();
|
|
1547
|
-
return true;
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
1360
|
#reportStalledDownloads() {
|
|
1551
1361
|
const now = Date.now();
|
|
1552
1362
|
for (const torrent of this.torrents.values()) {
|
|
@@ -1600,12 +1410,6 @@ export class TorrentPool {
|
|
|
1600
1410
|
if (!isWanted(torrent) || torrent?.done === true) {
|
|
1601
1411
|
continue;
|
|
1602
1412
|
}
|
|
1603
|
-
this.#stateBackgroundFill(torrent);
|
|
1604
|
-
// And act on what that just said. The fill withdraws itself once nothing
|
|
1605
|
-
// else wants the file, so a torrent everybody has left can be held by the
|
|
1606
|
-
// fill's own claim until this pass — and without this line the moment it
|
|
1607
|
-
// goes would be noticed by nobody, leaving the torrent in its swarm with
|
|
1608
|
-
// no idle clock running.
|
|
1609
1413
|
this.followTheDemand(torrent);
|
|
1610
1414
|
}
|
|
1611
1415
|
this.#reportStalledDownloads();
|
|
@@ -1660,6 +1464,16 @@ export class TorrentPool {
|
|
|
1660
1464
|
const addedAt = Date.now();
|
|
1661
1465
|
this.#swarmTimingByTorrent.set(torrent, { addedAt, firstPeerAt: null });
|
|
1662
1466
|
|
|
1467
|
+
torrent.on("download-requested", ({ piece, deadlineAt, requestId, peer, queued }) => {
|
|
1468
|
+
logger.info(`download request=${requestId} piece=${piece} deadline=${deadlineAt} peer=${peer} outstanding=${queued}`);
|
|
1469
|
+
});
|
|
1470
|
+
torrent.on("download-request-cancelled", ({ piece, offset, length, reason, requestId }) => {
|
|
1471
|
+
logger.info(`download request=${requestId ?? `${torrent.infoHash ?? "unknown"}:${piece}:${offset}`} cancel bytes=${length} reason=${reason}`);
|
|
1472
|
+
});
|
|
1473
|
+
torrent.on("download-request-cancel-failed", ({ piece, offset, length, error }) => {
|
|
1474
|
+
logger.warn(`download request=${torrent.infoHash ?? "unknown"}:${piece}:${offset} cancel bytes=${length} failed: ${error}`);
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1663
1477
|
// The first connected peer, said once, because the wait for it can BE the
|
|
1664
1478
|
// whole cold start and nothing else measures it.
|
|
1665
1479
|
torrent.on("wire", () => {
|
|
@@ -2125,7 +1939,6 @@ export class TorrentPool {
|
|
|
2125
1939
|
#forgetBookkeeping(torrent) {
|
|
2126
1940
|
this.#cancelIdleRemoval(torrent);
|
|
2127
1941
|
this.#lastAccess.delete(torrent);
|
|
2128
|
-
this.#readPositionByTorrent.delete(torrent);
|
|
2129
1942
|
this.#stallSince.delete(torrent);
|
|
2130
1943
|
this.#stallReportedAt.delete(torrent);
|
|
2131
1944
|
}
|
|
@@ -2166,16 +1979,6 @@ export class TorrentPool {
|
|
|
2166
1979
|
*
|
|
2167
1980
|
* @param {import("webtorrent").Torrent} torrent
|
|
2168
1981
|
* @param {number | null} [fileIndex] - Zero-based file index, or null for torrent-level only.
|
|
2169
|
-
* @param {{ resumeAnchorByteStart?: number | null }} [options] - `resumeAnchorByteStart`
|
|
2170
|
-
* pins the resume window to a FIXED byte offset instead of the live (moving)
|
|
2171
|
-
* read position. Without it, the window is anchored to wherever the file is
|
|
2172
|
-
* CURRENTLY being read from — which slides forward as playback/encoding
|
|
2173
|
-
* advances, so "bytes still needed" can jump up mid-poll even though nothing
|
|
2174
|
-
* regressed (the window just moved past already-downloaded pieces into
|
|
2175
|
-
* fresh ones). The caller should capture the returned `resumeAnchorByteStart`
|
|
2176
|
-
* on the FIRST poll of a buffering episode and pass it back on subsequent
|
|
2177
|
-
* polls of that SAME episode, so "bytes needed" counts down monotonically
|
|
2178
|
-
* against a fixed target instead of chasing a moving one.
|
|
2179
1982
|
* @returns {{
|
|
2180
1983
|
* numPeers: number,
|
|
2181
1984
|
* downloadSpeed: number,
|
|
@@ -2193,7 +1996,7 @@ export class TorrentPool {
|
|
|
2193
1996
|
* secondsWaitingForFirstPeer: number | null
|
|
2194
1997
|
* }}
|
|
2195
1998
|
*/
|
|
2196
|
-
getFileStats(torrent, fileIndex = null
|
|
1999
|
+
getFileStats(torrent, fileIndex = null) {
|
|
2197
2000
|
const numPeers = typeof torrent?.numPeers === "number" ? torrent.numPeers : 0;
|
|
2198
2001
|
const downloadSpeed = typeof torrent?.downloadSpeed === "number" ? torrent.downloadSpeed : 0;
|
|
2199
2002
|
const uploadSpeed = typeof torrent?.uploadSpeed === "number" ? torrent.uploadSpeed : 0;
|
|
@@ -2237,19 +2040,10 @@ export class TorrentPool {
|
|
|
2237
2040
|
return { ...base, fileProgress: null, fileDownloaded: null, fileLength: null };
|
|
2238
2041
|
}
|
|
2239
2042
|
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
// the caller-supplied FROZEN offset when given (see JSDoc above), otherwise
|
|
2245
|
-
// the live (moving) read position tracked from /stream range requests.
|
|
2246
|
-
const readPositions = this.#readPositionByTorrent.get(torrent);
|
|
2247
|
-
const liveReadByteStart = readPositions ? readPositions.get(fileIndex) : undefined;
|
|
2248
|
-
const requestedAnchor = options?.resumeAnchorByteStart;
|
|
2249
|
-
const readByteStart = Number.isFinite(requestedAnchor) ? requestedAnchor : liveReadByteStart;
|
|
2250
|
-
const resume = typeof readByteStart === "number"
|
|
2251
|
-
? this.#getResumeWindowProgress(torrent, file, readByteStart)
|
|
2252
|
-
: null;
|
|
2043
|
+
// Current map demand is the only source of preparation progress.
|
|
2044
|
+
const resume = demandProgress({ windows: demandFor(torrent).register.windows()
|
|
2045
|
+
.filter(window => window.fileIndex === fileIndex && window.urgency <= Urgency.NEAR),
|
|
2046
|
+
file, pieceLength: torrent.pieceLength, locationOf: pieceStoreOf(torrent)?.locationOf?.bind(pieceStoreOf(torrent)) });
|
|
2253
2047
|
|
|
2254
2048
|
// Null-safe downloaded/progress (webtorrent's own getters throw on a
|
|
2255
2049
|
// deselected null piece — see fileDownloadedBytes).
|
|
@@ -2273,373 +2067,18 @@ export class TorrentPool {
|
|
|
2273
2067
|
fileProgress: fileLength > 0 ? Math.max(0, Math.min(1, fileDownloaded / fileLength)) : 0,
|
|
2274
2068
|
fileDownloaded,
|
|
2275
2069
|
fileLength,
|
|
2070
|
+
fileOffset: file.offset,
|
|
2276
2071
|
residence,
|
|
2277
|
-
|
|
2278
|
-
//
|
|
2072
|
+
fileAvailable: fileLength > 0 && residence.length > 0 && residence.every(range => range.location !== "missing"),
|
|
2073
|
+
// Compatibility fields report the same current urgent map demand.
|
|
2279
2074
|
resumeNeededBytes: resume ? resume.totalBytes : null,
|
|
2280
2075
|
resumeDownloadedBytes: resume ? resume.downloadedBytes : null,
|
|
2281
|
-
resumeAnchorByteStart:
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
// by whole pieces from the torrent bitfield, so it advances coarsely
|
|
2285
|
-
// (piece granularity). Null when the bitfield/piece info is unavailable.
|
|
2286
|
-
headerBytes: header ? header.totalBytes : null,
|
|
2287
|
-
headerDownloadedBytes: header ? header.downloadedBytes : null
|
|
2076
|
+
resumeAnchorByteStart: null,
|
|
2077
|
+
headerBytes: resume ? resume.totalBytes : null,
|
|
2078
|
+
headerDownloadedBytes: resume ? resume.downloadedBytes : null
|
|
2288
2079
|
};
|
|
2289
2080
|
}
|
|
2290
2081
|
|
|
2291
|
-
/**
|
|
2292
|
-
* Count, by whole torrent pieces, how many bytes of a file's header/index
|
|
2293
|
-
* region (leading {@link HEADER_HEAD_BYTES} + trailing {@link HEADER_TAIL_BYTES})
|
|
2294
|
-
* are downloaded. Used to show progress toward the codec-probe phase.
|
|
2295
|
-
*
|
|
2296
|
-
* @param {import("webtorrent").Torrent} torrent
|
|
2297
|
-
* @param {import("webtorrent").TorrentFile} file
|
|
2298
|
-
* @returns {{ totalBytes: number, downloadedBytes: number } | null}
|
|
2299
|
-
*/
|
|
2300
|
-
#getHeaderRangeProgress(torrent, file) {
|
|
2301
|
-
const pieceLength = Number(torrent?.pieceLength);
|
|
2302
|
-
const bitfield = torrent?.bitfield;
|
|
2303
|
-
const fileLength = Number(file?.length);
|
|
2304
|
-
if (
|
|
2305
|
-
!Number.isFinite(pieceLength) || pieceLength <= 0 ||
|
|
2306
|
-
!bitfield || typeof bitfield.get !== "function" ||
|
|
2307
|
-
!Number.isFinite(fileLength) || fileLength <= 0
|
|
2308
|
-
) {
|
|
2309
|
-
return null;
|
|
2310
|
-
}
|
|
2311
|
-
const fileOffset = Number.isFinite(file.offset) ? file.offset : 0;
|
|
2312
|
-
const headEnd = Math.min(HEADER_HEAD_BYTES, fileLength) - 1;
|
|
2313
|
-
const ranges = [[0, headEnd]];
|
|
2314
|
-
const tailStart = Math.max(headEnd + 1, fileLength - HEADER_TAIL_BYTES);
|
|
2315
|
-
if (tailStart <= fileLength - 1) {
|
|
2316
|
-
ranges.push([tailStart, fileLength - 1]);
|
|
2317
|
-
}
|
|
2318
|
-
const pieces = new Set();
|
|
2319
|
-
for (const [start, end] of ranges) {
|
|
2320
|
-
const first = Math.floor((fileOffset + start) / pieceLength);
|
|
2321
|
-
const last = Math.floor((fileOffset + end) / pieceLength);
|
|
2322
|
-
for (let piece = first; piece <= last; piece += 1) {
|
|
2323
|
-
pieces.add(piece);
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
let totalBytes = 0;
|
|
2327
|
-
let downloadedBytes = 0;
|
|
2328
|
-
for (const piece of pieces) {
|
|
2329
|
-
totalBytes += pieceLength;
|
|
2330
|
-
if (bitfield.get(piece)) {
|
|
2331
|
-
downloadedBytes += pieceLength;
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
return { totalBytes, downloadedBytes };
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
/**
|
|
2338
|
-
* Count, by whole torrent pieces, how many bytes of the window AHEAD of the
|
|
2339
|
-
* current read position are downloaded — i.e. how much is still to download
|
|
2340
|
-
* before playback can continue from that point. Mirrors
|
|
2341
|
-
* {@link #getHeaderRangeProgress}, for the moving read head.
|
|
2342
|
-
*
|
|
2343
|
-
* @param {import("webtorrent").Torrent} torrent
|
|
2344
|
-
* @param {import("webtorrent").TorrentFile} file
|
|
2345
|
-
* @param {number} readByteStart - Byte offset within the file the reader is at.
|
|
2346
|
-
* @returns {{ totalBytes: number, downloadedBytes: number } | null}
|
|
2347
|
-
*/
|
|
2348
|
-
#getResumeWindowProgress(torrent, file, readByteStart) {
|
|
2349
|
-
const pieceLength = Number(torrent?.pieceLength);
|
|
2350
|
-
const bitfield = torrent?.bitfield;
|
|
2351
|
-
const fileLength = Number(file?.length);
|
|
2352
|
-
if (
|
|
2353
|
-
!Number.isFinite(pieceLength) || pieceLength <= 0 ||
|
|
2354
|
-
!bitfield || typeof bitfield.get !== "function" ||
|
|
2355
|
-
!Number.isFinite(fileLength) || fileLength <= 0
|
|
2356
|
-
) {
|
|
2357
|
-
return null;
|
|
2358
|
-
}
|
|
2359
|
-
const fileOffset = Number.isFinite(file.offset) ? file.offset : 0;
|
|
2360
|
-
const windowStart = Math.max(0, Math.min(Number(readByteStart) || 0, fileLength - 1));
|
|
2361
|
-
const windowEnd = Math.min(fileLength - 1, windowStart + PRIORITY_WINDOW_BYTES - 1);
|
|
2362
|
-
const firstPiece = Math.floor((fileOffset + windowStart) / pieceLength);
|
|
2363
|
-
const lastPiece = Math.floor((fileOffset + windowEnd) / pieceLength);
|
|
2364
|
-
// Byte-accurate: count the PARTIAL progress of in-progress pieces (not whole
|
|
2365
|
-
// pieces), so "amount left" moves smoothly instead of jumping by a whole
|
|
2366
|
-
// piece (8 MB here) at a time.
|
|
2367
|
-
let totalBytes = 0;
|
|
2368
|
-
let downloadedBytes = 0;
|
|
2369
|
-
for (let piece = firstPiece; piece <= lastPiece; piece += 1) {
|
|
2370
|
-
totalBytes += pieceLength;
|
|
2371
|
-
downloadedBytes += pieceDownloadedBytes(torrent, piece);
|
|
2372
|
-
}
|
|
2373
|
-
return { totalBytes, downloadedBytes };
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
|
-
/**
|
|
2377
|
-
* Pre-fetch the leading and trailing bytes of a torrent file so that
|
|
2378
|
-
* WebTorrent prioritises the pieces that contain file headers and footers.
|
|
2379
|
-
*
|
|
2380
|
-
* For MP4 files the MOOV atom is often placed at the very end of the file
|
|
2381
|
-
* (non-faststart encoding). Fetching the tail ensures that ffprobe can
|
|
2382
|
-
* identify codecs and duration even for freshly-added torrents without
|
|
2383
|
-
* waiting for the rest of the content to download.
|
|
2384
|
-
*
|
|
2385
|
-
* Resolves once both regions have been fully downloaded, or when the
|
|
2386
|
-
* timeout elapses — whichever comes first. Never rejects.
|
|
2387
|
-
*
|
|
2388
|
-
* @param {import("webtorrent").Torrent} torrent
|
|
2389
|
-
* @param {number} fileIndex - Zero-based index into `torrent.files`.
|
|
2390
|
-
* @param {object} [options]
|
|
2391
|
-
* @param {number} [options.headBytes=262144] - Leading bytes to fetch (default 256 KB).
|
|
2392
|
-
* @param {number} [options.tailBytes=2097152] - Trailing bytes to fetch (default 2 MB).
|
|
2393
|
-
* @param {number} [options.timeoutMs=300000] - Maximum wait time in milliseconds (default 5 min).
|
|
2394
|
-
* @param {boolean} [options.awaited=false] - Whether somebody is waiting for
|
|
2395
|
-
* these bytes right now. The playback plan is: it cannot answer until the
|
|
2396
|
-
* file has said what is in it, and a person is watching a loading screen
|
|
2397
|
-
* meanwhile. The warm-up is NOT, by its whole purpose — it happens while
|
|
2398
|
-
* the viewer is still choosing, so it must not outrank another film that
|
|
2399
|
-
* somebody is watching on this proxy this minute.
|
|
2400
|
-
* @returns {Promise<void>}
|
|
2401
|
-
*/
|
|
2402
|
-
async prefetchFileEdges(
|
|
2403
|
-
torrent,
|
|
2404
|
-
fileIndex,
|
|
2405
|
-
{ headBytes = 256 * 1024, tailBytes = 2 * 1024 * 1024, timeoutMs = 300_000, awaited = false } = {}
|
|
2406
|
-
) {
|
|
2407
|
-
if (!torrent || !Array.isArray(torrent.files)) {
|
|
2408
|
-
return;
|
|
2409
|
-
}
|
|
2410
|
-
// THE ENDS OF THIS FILE ARE WANTED, and they go on being wanted after this
|
|
2411
|
-
// read is over: the file's own directory lives there, and a seek asks for
|
|
2412
|
-
// it again. Stated before the read rather than by it, and kept afterwards
|
|
2413
|
-
// at the level of something nobody is waiting for.
|
|
2414
|
-
stateFileEdges(torrent, fileIndex, awaited ? Urgency.NEAR : Urgency.TAIL);
|
|
2415
|
-
// The first thing ever stated about a torrent being opened, and therefore
|
|
2416
|
-
// what keeps it in its swarm through the seconds when nothing else can say
|
|
2417
|
-
// anything about it.
|
|
2418
|
-
this.followTheDemand(torrent);
|
|
2419
|
-
// Two callers can ask for the same edges at once: the warm-up that starts
|
|
2420
|
-
// when a torrent is picked, and the playback plan a moment later. Reading
|
|
2421
|
-
// the same two pieces twice costs nothing in bandwidth — the torrent
|
|
2422
|
-
// fetches each piece once — but it does open a second pair of readers, each
|
|
2423
|
-
// claiming a window and holding pieces. One is enough.
|
|
2424
|
-
const inFlightKey = `${torrent.infoHash}:${fileIndex}`;
|
|
2425
|
-
const running = this.#edgePrefetches.get(inFlightKey);
|
|
2426
|
-
if (running) {
|
|
2427
|
-
return running;
|
|
2428
|
-
}
|
|
2429
|
-
const prefetch = this.#prefetchFileEdgesOnce(torrent, fileIndex, { headBytes, tailBytes, timeoutMs });
|
|
2430
|
-
this.#edgePrefetches.set(inFlightKey, prefetch);
|
|
2431
|
-
try {
|
|
2432
|
-
return await prefetch;
|
|
2433
|
-
} finally {
|
|
2434
|
-
this.#edgePrefetches.delete(inFlightKey);
|
|
2435
|
-
// Nobody is waiting for them now. Said with `lower`, because this is the
|
|
2436
|
-
// one caller entitled to put them back down: the read it belongs to is
|
|
2437
|
-
// over, and there is no other — a file's edges are fetched once at a time.
|
|
2438
|
-
stateFileEdges(torrent, fileIndex, Urgency.TAIL, { lower: true });
|
|
2439
|
-
}
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
|
-
/**
|
|
2443
|
-
* Fetch a bounded region in the MIDDLE of a file.
|
|
2444
|
-
*
|
|
2445
|
-
* The warm-up fetches a file's two edges because the codec probe reads them.
|
|
2446
|
-
* A viewer resuming a film needs neither: they need the region under their own
|
|
2447
|
-
* position, and until now nothing asked for it before the encoder did. Field
|
|
2448
|
-
* 2026-09-03 — a retry after a crash reached the encoder 53 s after the button
|
|
2449
|
-
* was pressed, and only THEN was the piece under the viewer's position first
|
|
2450
|
-
* requested; it took another 46 s, and the browser gave up 0.4 s before it
|
|
2451
|
-
* landed.
|
|
2452
|
-
*
|
|
2453
|
-
* Read as an ordinary bounded read, never as a selection: claiming a whole
|
|
2454
|
-
* region alongside the readers' own windows is the mistake `#syncSelections`
|
|
2455
|
-
* was written against.
|
|
2456
|
-
*
|
|
2457
|
-
* @param {import("webtorrent").Torrent} torrent
|
|
2458
|
-
* @param {number} fileIndex
|
|
2459
|
-
* @param {number} startByte
|
|
2460
|
-
* @param {number} bytes
|
|
2461
|
-
* @param {{ timeoutMs?: number }} [options]
|
|
2462
|
-
* @returns {Promise<void>}
|
|
2463
|
-
*/
|
|
2464
|
-
async prefetchFileRegion(torrent, fileIndex, startByte, bytes, { timeoutMs = 300_000 } = {}) {
|
|
2465
|
-
const file = torrent?.files?.[fileIndex];
|
|
2466
|
-
if (!file || typeof file.createReadStream !== "function") {
|
|
2467
|
-
return;
|
|
2468
|
-
}
|
|
2469
|
-
const fileSize = file.length;
|
|
2470
|
-
if (!Number.isFinite(fileSize) || fileSize <= 0 || !(bytes > 0)) {
|
|
2471
|
-
return;
|
|
2472
|
-
}
|
|
2473
|
-
const start = Math.max(0, Math.min(Math.floor(startByte), fileSize - 1));
|
|
2474
|
-
const end = Math.min(fileSize - 1, start + Math.floor(bytes) - 1);
|
|
2475
|
-
if (end <= start) {
|
|
2476
|
-
return;
|
|
2477
|
-
}
|
|
2478
|
-
const drained = new Promise((resolve) => {
|
|
2479
|
-
const stream = file.createReadStream({ start, end });
|
|
2480
|
-
stream.on("data", () => undefined);
|
|
2481
|
-
stream.once("end", resolve);
|
|
2482
|
-
stream.once("error", resolve);
|
|
2483
|
-
stream.once("close", resolve);
|
|
2484
|
-
});
|
|
2485
|
-
await Promise.race([drained, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
/**
|
|
2489
|
-
* The body of {@link prefetchFileEdges}, without the de-duplication.
|
|
2490
|
-
*
|
|
2491
|
-
* @param {import("webtorrent").Torrent} torrent
|
|
2492
|
-
* @param {number} fileIndex
|
|
2493
|
-
* @param {{ headBytes: number, tailBytes: number, timeoutMs: number }} options
|
|
2494
|
-
* @returns {Promise<void>}
|
|
2495
|
-
*/
|
|
2496
|
-
async #prefetchFileEdgesOnce(torrent, fileIndex, { headBytes, tailBytes, timeoutMs }) {
|
|
2497
|
-
if (!torrent || !Array.isArray(torrent.files)) {
|
|
2498
|
-
return;
|
|
2499
|
-
}
|
|
2500
|
-
const file = torrent.files[fileIndex];
|
|
2501
|
-
if (!file || typeof file.createReadStream !== "function") {
|
|
2502
|
-
return;
|
|
2503
|
-
}
|
|
2504
|
-
const fileSize = file.length;
|
|
2505
|
-
if (!Number.isFinite(fileSize) || fileSize <= 0) {
|
|
2506
|
-
return;
|
|
2507
|
-
}
|
|
2508
|
-
|
|
2509
|
-
const safeHeadEnd = Math.min(headBytes, fileSize) - 1;
|
|
2510
|
-
const safeTailStart = Math.max(0, fileSize - tailBytes);
|
|
2511
|
-
|
|
2512
|
-
/** Drain a readable stream, resolving on end/error/close. */
|
|
2513
|
-
const drainStream = (stream) =>
|
|
2514
|
-
new Promise((resolve) => {
|
|
2515
|
-
stream.on("data", () => undefined);
|
|
2516
|
-
stream.once("end", resolve);
|
|
2517
|
-
stream.once("error", resolve);
|
|
2518
|
-
stream.once("close", resolve);
|
|
2519
|
-
});
|
|
2520
|
-
|
|
2521
|
-
try {
|
|
2522
|
-
const tasks = [
|
|
2523
|
-
// Head: FTYP/MOOV (faststart MP4), EBML header (MKV), etc.
|
|
2524
|
-
drainStream(file.createReadStream({ start: 0, end: safeHeadEnd }))
|
|
2525
|
-
];
|
|
2526
|
-
|
|
2527
|
-
// Tail: MOOV atom for non-faststart MP4. Skip when it overlaps the head.
|
|
2528
|
-
if (safeTailStart > safeHeadEnd + 1) {
|
|
2529
|
-
tasks.push(drainStream(file.createReadStream({ start: safeTailStart, end: fileSize - 1 })));
|
|
2530
|
-
}
|
|
2531
|
-
|
|
2532
|
-
await Promise.race([
|
|
2533
|
-
Promise.all(tasks),
|
|
2534
|
-
new Promise((resolve) => setTimeout(resolve, timeoutMs))
|
|
2535
|
-
]);
|
|
2536
|
-
} catch (_error) {
|
|
2537
|
-
// Best-effort — a prefetch failure must never prevent playback.
|
|
2538
|
-
}
|
|
2539
|
-
}
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
/**
|
|
2544
|
-
* Record where a file is being read from.
|
|
2545
|
-
*
|
|
2546
|
-
* This used to also decide what the torrent should download, and that was the
|
|
2547
|
-
* mistake: it was one of THREE places claiming pieces for the same file — the
|
|
2548
|
-
* whole-file selection, a window around the read head, and the reader itself
|
|
2549
|
-
* — and they overwrote each other on every request. Every need is now stated
|
|
2550
|
-
* in one register (`services/demand/`) and one class turns the register into
|
|
2551
|
-
* requests to the swarm (`services/torrent/download/SwarmSelection.js`); several
|
|
2552
|
-
* readers on one file therefore produce the union of their windows instead of
|
|
2553
|
-
* the last caller's opinion.
|
|
2554
|
-
*
|
|
2555
|
-
* What is left here is bookkeeping the readers cannot do: `getFileStats`
|
|
2556
|
-
* reports how much of the window ahead of the read head is still missing, so
|
|
2557
|
-
* the viewer can be shown how long a resume will take, and a jump in the read
|
|
2558
|
-
* position is logged because a seek that never reaches the torrent is
|
|
2559
|
-
* invisible otherwise.
|
|
2560
|
-
*
|
|
2561
|
-
* @param {import("webtorrent").Torrent} torrent
|
|
2562
|
-
* @param {number} fileIndex
|
|
2563
|
-
* @param {number} byteStart - Start offset within the file.
|
|
2564
|
-
* @param {number} [windowBytes] - Unused; kept so callers need not change.
|
|
2565
|
-
* @param {{ wholeFileRead?: boolean, isPlaybackRead?: boolean }} [options] -
|
|
2566
|
-
* `wholeFileRead` marks a request that carried no byte range, i.e. one that
|
|
2567
|
-
* merely opens the file at 0 rather than asking to read from there.
|
|
2568
|
-
* `isPlaybackRead` marks the encoder's input read, the only one that
|
|
2569
|
-
* follows the viewer. See the guards below.
|
|
2570
|
-
* @returns {void}
|
|
2571
|
-
*/
|
|
2572
|
-
prioritizeByteRange(
|
|
2573
|
-
torrent,
|
|
2574
|
-
fileIndex,
|
|
2575
|
-
byteStart,
|
|
2576
|
-
windowBytes = PRIORITY_WINDOW_BYTES,
|
|
2577
|
-
options = {}
|
|
2578
|
-
) {
|
|
2579
|
-
if (!torrent || !Array.isArray(torrent.files)) {
|
|
2580
|
-
return;
|
|
2581
|
-
}
|
|
2582
|
-
const file = torrent.files[fileIndex];
|
|
2583
|
-
if (!file) {
|
|
2584
|
-
return;
|
|
2585
|
-
}
|
|
2586
|
-
const fileLength = Number(file.length);
|
|
2587
|
-
if (!Number.isFinite(fileLength) || fileLength <= 0) {
|
|
2588
|
-
return;
|
|
2589
|
-
}
|
|
2590
|
-
|
|
2591
|
-
const safeStart = Math.max(0, Number(byteStart) || 0);
|
|
2592
|
-
|
|
2593
|
-
// Remember where this file is being read from, so getFileStats can report the
|
|
2594
|
-
// download progress of the window ahead of the read head (resume amount).
|
|
2595
|
-
let readPositions = this.#readPositionByTorrent.get(torrent);
|
|
2596
|
-
if (!readPositions) {
|
|
2597
|
-
readPositions = new Map();
|
|
2598
|
-
this.#readPositionByTorrent.set(torrent, readPositions);
|
|
2599
|
-
}
|
|
2600
|
-
const previousStart = readPositions.get(fileIndex);
|
|
2601
|
-
|
|
2602
|
-
// A request with no byte range says nothing about where the viewer is. ffmpeg
|
|
2603
|
-
// opens its input with a plain GET and abandons it the moment it seeks, and
|
|
2604
|
-
// the keyframe index and the codec probe do the same — four such reads around
|
|
2605
|
-
// every encoder restart, each one arriving here as "position 0". Acting on
|
|
2606
|
-
// them undoes the seek that just happened: the whole file is re-selected from
|
|
2607
|
-
// piece 0, the picker skips the pieces already on disk and walks the swarm
|
|
2608
|
-
// forward from the first hole. Measured on a 4.7 GB film: a seek to 89.1%
|
|
2609
|
-
// downloaded 2.47 GB over 93 s before the segment could be served. So a
|
|
2610
|
-
// whole-file read only sets the position when nothing else has.
|
|
2611
|
-
if (options.wholeFileRead && previousStart !== undefined) {
|
|
2612
|
-
return;
|
|
2613
|
-
}
|
|
2614
|
-
|
|
2615
|
-
readPositions.set(fileIndex, safeStart);
|
|
2616
|
-
|
|
2617
|
-
// Log jumps only. Sequential reading calls this on every range request and
|
|
2618
|
-
// would drown the log; a jump is a seek, and a seek that never reaches the
|
|
2619
|
-
// torrent is exactly the failure this line exists to make visible — after a
|
|
2620
|
-
// seek the encoder waits on pieces nobody has been told to fetch.
|
|
2621
|
-
const isJump =
|
|
2622
|
-
previousStart === undefined || Math.abs(safeStart - previousStart) > PRIORITY_WINDOW_BYTES;
|
|
2623
|
-
if (isJump) {
|
|
2624
|
-
// A jump in the read that follows the viewer is a seek. Whatever the
|
|
2625
|
-
// swarm was giving us was for somewhere else, and the pieces at the new
|
|
2626
|
-
// position have to be earned from peers that are choking us — the same
|
|
2627
|
-
// standing start as a fresh torrent. A jump in any OTHER read is the
|
|
2628
|
-
// codec probe or the keyframe index visiting the ends of the file, and
|
|
2629
|
-
// nobody is waiting on those the way a viewer waits on a seek.
|
|
2630
|
-
if (options.isPlaybackRead) {
|
|
2631
|
-
this.#markHurry(torrent, "the viewer moved");
|
|
2632
|
-
}
|
|
2633
|
-
const percent = ((safeStart / fileLength) * 100).toFixed(1);
|
|
2634
|
-
logger.info(
|
|
2635
|
-
`torrent-pool: [${String(torrent.infoHash).slice(0, 8)}] read position -> ` +
|
|
2636
|
-
`${(safeStart / 1024 / 1024).toFixed(0)}MB (${percent}% of file ${fileIndex})` +
|
|
2637
|
-
(previousStart === undefined ? " (first)" : ` (was ${(previousStart / 1024 / 1024).toFixed(0)}MB)`)
|
|
2638
|
-
);
|
|
2639
|
-
}
|
|
2640
|
-
|
|
2641
|
-
}
|
|
2642
|
-
|
|
2643
2082
|
/**
|
|
2644
2083
|
* Destroy every torrent together with its on-disk store, then tear down the
|
|
2645
2084
|
* WebTorrent client. Called from the proxy's graceful-shutdown `onClose`
|