@torrent-tv/proxy 2.9.103 → 2.9.105
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
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 2.9.105
|
|
2
|
+
|
|
3
|
+
- **Fix**: A reader's claim on pieces is put back when WebTorrent drops it, which is what stopped a download dead for eleven minutes. A reader declares the window it needs as a selection and withdraws it when it ends; that claim turns out not to be durable — the library deletes a selection the moment every piece in it is present (`remove fully downloaded selection`). While the reader keeps moving this is invisible, because the next window is claimed at once. It is fatal when the reader STOPS: the encoder gets held back by the look-ahead cap, ffmpeg stops reading, the reader parks on a window that is fully downloaded, the selection disappears, and no code of ours can notice because the reader is parked inside a write. Measured 2026-08-05: the encoder was suspended at 22:44:51, the download hit zero at 22:45:05 and stayed there for eleven minutes with 150 peer connections open and the new diagnostic reading `0 selection(s) covering 0 piece(s), 0 being asked, 0 blocks in flight`; when the encoder was let go there was nothing ahead of it. Live reader windows are now re-asserted from the pool's own timer, using the set the piece store already keeps, and only where something is actually missing — re-claiming a satisfied window would only be deleted again on the next pass.
|
|
4
|
+
|
|
5
|
+
## 2.9.104
|
|
6
|
+
|
|
7
|
+
- **Fix**: An encoder run that stops because its input ran dry is no longer reported as a finished file. ffmpeg exits 0 both when it reaches the end of the source and when the source simply stops delivering, and over HTTP it cannot tell the two apart — so when a torrent's download died mid-session (field 2026-08-05), a run that had produced 188 segments of 624 logged `encode-run complete`, the player consumed what was already on disk and then froze for 60 s on the first segment nobody was making. The claim is now checked against the playlist that was published: a run that stopped short is a failure, which the session can restart, rather than a completed file.
|
|
8
|
+
- **New**: A download that stalls says so, and says which of the two possible reasons it is. The same session spent five minutes at **1 KB/s** with 186 peer connections open and trackers reporting ~300 seeders, on a torrent that was not finished, and produced no log line at all — the collapse had to be reconstructed afterwards from three unrelated counters. A torrent with an active reader that drops below 32 KB/s for ten seconds now reports how many pieces are selected and still missing, how many are marked critical, how many peers hold what we want, how many are choking us, how many are being asked and how many blocks are in flight. That separates "the swarm was never told what we need" from "it was told and will not deliver", which the previous evidence could not.
|
|
9
|
+
|
|
1
10
|
## 2.9.103
|
|
2
11
|
|
|
3
12
|
- **Fix**: Playback worked in neither 2.9.101 nor 2.9.102. Both cold-start estimates keep a window of recent samples, and the constant naming that window was used twice and declared nowhere. The session-create one runs on every new session, so `POST /api/transcode-sessions` answered 500 to every viewer and the browser then reported the first segment missing. Field session 2026-08-05: the plan succeeded in 5858 ms, the session request failed 47 ms later, the data channel closed 16 ms after that.
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* immediately when all registered consumers release them.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { createReadStream } from "node:fs";
|
|
10
|
+
import { createReadStream, readdirSync } from "node:fs";
|
|
11
11
|
import { access, mkdir, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
12
12
|
import { Readable } from "node:stream";
|
|
13
13
|
import os from "node:os";
|
|
@@ -2394,6 +2394,29 @@ export class HlsSessionManager {
|
|
|
2394
2394
|
return;
|
|
2395
2395
|
}
|
|
2396
2396
|
if (code === 0) {
|
|
2397
|
+
// ffmpeg exits 0 both when it reaches the end of the file and when its
|
|
2398
|
+
// input simply stops producing bytes — over HTTP the two look identical
|
|
2399
|
+
// to it. Field 2026-08-05: the torrent's download died, the read ended,
|
|
2400
|
+
// and a run that had made 188 segments of 624 reported itself complete;
|
|
2401
|
+
// the player then consumed what was on disk and froze on the first
|
|
2402
|
+
// segment nobody was making. So the claim is checked against the
|
|
2403
|
+
// playlist we published, and a run that stopped short is a FAILURE that
|
|
2404
|
+
// can be restarted, not a finished file.
|
|
2405
|
+
const producedThrough = this.#latestProducedSegment(session);
|
|
2406
|
+
const expectedLast = session.segmentCount > 0 ? session.segmentCount - 1 : null;
|
|
2407
|
+
if (expectedLast !== null && producedThrough !== null && producedThrough < expectedLast) {
|
|
2408
|
+
session.state = "failed";
|
|
2409
|
+
session.progress.state = "failed";
|
|
2410
|
+
session.progress.updatedAt = Date.now();
|
|
2411
|
+
session.lastError =
|
|
2412
|
+
`input ended after segment #${producedThrough} of ${expectedLast} — ` +
|
|
2413
|
+
"the source stopped delivering data";
|
|
2414
|
+
logger.error(
|
|
2415
|
+
`transcode ${session.id} ${session.runLabel ?? "run#?"} encode-run ended early: ` +
|
|
2416
|
+
`${session.lastError} "${session.fileName}"`
|
|
2417
|
+
);
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2397
2420
|
session.state = "ready";
|
|
2398
2421
|
session.progress.state = "ready";
|
|
2399
2422
|
session.progress.updatedAt = Date.now();
|
|
@@ -2823,6 +2846,27 @@ export class HlsSessionManager {
|
|
|
2823
2846
|
return sorted[Math.floor(sorted.length / 2)];
|
|
2824
2847
|
}
|
|
2825
2848
|
|
|
2849
|
+
/**
|
|
2850
|
+
* The highest segment index this session has on disk, or null when it has
|
|
2851
|
+
* none. Used to tell "the file ended" from "the data ran out".
|
|
2852
|
+
*
|
|
2853
|
+
* @param {HlsSession} session
|
|
2854
|
+
* @returns {number | null}
|
|
2855
|
+
*/
|
|
2856
|
+
#latestProducedSegment(session) {
|
|
2857
|
+
let highest = null;
|
|
2858
|
+
for (const name of readdirSync(session.dirPath, { withFileTypes: false })) {
|
|
2859
|
+
if (!this.segmentFormat.isSegmentFileName(name)) {
|
|
2860
|
+
continue;
|
|
2861
|
+
}
|
|
2862
|
+
const index = this.segmentFormat.segmentIndexFromName(name);
|
|
2863
|
+
if (index >= 0 && (highest === null || index > highest)) {
|
|
2864
|
+
highest = index;
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
return highest;
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2826
2870
|
/**
|
|
2827
2871
|
* How many times the viewer has moved since this session started.
|
|
2828
2872
|
*
|
|
@@ -200,6 +200,18 @@ export class PieceLru {
|
|
|
200
200
|
* @param {string|number} readerId
|
|
201
201
|
* @returns {void}
|
|
202
202
|
*/
|
|
203
|
+
/**
|
|
204
|
+
* Every live reader's declared window. Read by the pool to check that the
|
|
205
|
+
* torrent has actually been asked for those pieces — WebTorrent deletes a
|
|
206
|
+
* selection of its own accord once it is fully downloaded, so a claim made
|
|
207
|
+
* once does not stay made.
|
|
208
|
+
*
|
|
209
|
+
* @returns {Array<{ from: number, to: number }>}
|
|
210
|
+
*/
|
|
211
|
+
protectedRanges() {
|
|
212
|
+
return [...this.#protected.values()].map((range) => ({ from: range.from, to: range.to }));
|
|
213
|
+
}
|
|
214
|
+
|
|
203
215
|
unprotect(readerId) {
|
|
204
216
|
this.#protected.delete(readerId);
|
|
205
217
|
}
|