@torrent-tv/proxy 2.80.9 → 2.80.10

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.
@@ -1,261 +0,0 @@
1
- /**
2
- * @file A segment request below the running encode must not be held for ever.
3
- *
4
- * The encoder only moves forward from where its run began, so a request BELOW
5
- * that point cannot be answered by anything the run does. Every other far
6
- * request is a claim the running encode may yet reach; this one is a hole.
7
- *
8
- * Measured 2026-08-11: a quality switch placed a run at segment #770 while the
9
- * player needed #757, and the request was held for two minutes forty-one while
10
- * the encoder produced 409 s of video nobody had asked for at 2.48x. The
11
- * placement that caused it is fixed; this is the guard that stops the SHAPE
12
- * from hanging a session again, whatever puts it there.
13
- */
14
-
15
- import test from "node:test";
16
- import assert from "node:assert/strict";
17
- import { SourceFile } from "../services/source/SourceFile.js";
18
- import { Timeline } from "../services/output/Timeline.js";
19
- import { mkdtemp, rm } from "node:fs/promises";
20
- import os from "node:os";
21
- import path from "node:path";
22
- import { HlsSessionManager } from "../services/hls-session-manager.js";
23
- import { startRunOn } from "./helpers/encode-run.js";
24
- import { fmp4Format } from "../services/segment-formats/fmp4.js";
25
-
26
- const SESSION_ID = "bbbbbbbb-cccc-dddd-eeee-ffffffffffff";
27
- const SEGMENT_SECONDS = 4;
28
- const RUN_STARTS_AT = 770;
29
- const WANTED = 757;
30
-
31
- /**
32
- * A session whose run began well past the segment being asked for.
33
- *
34
- * @returns {Promise<{ manager: HlsSessionManager, session: object, dirPath: string, restarts: number[] }>}
35
- */
36
- async function managerWithRunAhead() {
37
- const dirPath = await mkdtemp(path.join(os.tmpdir(), "behind-head-"));
38
- const manager = new HlsSessionManager({
39
- enabled: true,
40
- ffmpegBin: "ffmpeg",
41
- localBindHost: "127.0.0.1",
42
- localPort: 9090
43
- });
44
- const session = {
45
- id: SESSION_ID,
46
- dirPath,
47
- // Where this file is cut, held by the file. A fixture that stated it
48
- // on the session was describing what production no longer does.
49
- timeline: new Timeline({
50
- boundaries: Array.from({ length: 1937 }, (_, index) => index * SEGMENT_SECONDS),
51
- cutGrid: "uniform"
52
- }),
53
- state: "ready",
54
- file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.avi" }),
55
- // An ordinary session reads its own file, and its sound is inside it. The
56
- // three differ only for a soundtrack shipped as a file of its own.
57
- get inputFile() { return this.file; },
58
- get audioFile() { return this.file; },
59
- startedAt: Date.now(),
60
- createEntryMs: Date.now(),
61
- lastAccessedAt: Date.now(),
62
- lastError: "",
63
- consumers: new Set(),
64
- segmentFormat: fmp4Format,
65
- transcodeVideo: true,
66
- useSyntheticPlaylist: true,
67
- playlistText: "#EXTM3U\n",
68
- lastRestartAt: 0,
69
- failedStartAt: -1,
70
- failedStartCount: 0,
71
- seekSettleTimer: null,
72
- seekTarget: null,
73
- waitEpoch: 0,
74
- firstWantedAt: new Map(),
75
- runs: new Set(),
76
- progress: { state: "running", processedSeconds: RUN_STARTS_AT * SEGMENT_SECONDS + 400, startPositionSeconds: RUN_STARTS_AT * SEGMENT_SECONDS }
77
- };
78
- manager.sessionsById.set(SESSION_ID, session);
79
- startRunOn(session, { from: RUN_STARTS_AT });
80
- const restarts = [];
81
- return { manager, session, dirPath, restarts };
82
- }
83
-
84
- test("a request behind the run is repaired once the player asks again", async (t) => {
85
- const { manager, session, dirPath } = await managerWithRunAhead();
86
- t.after(async () => {
87
- await manager.disposeAll();
88
- await rm(dirPath, { recursive: true, force: true });
89
- });
90
- // Asked for a moment ago and still unanswerable. What decides is that the
91
- // player comes BACK for the same segment: it is the only one it wants, and no
92
- // amount of waiting could say that as clearly.
93
- session.firstWantedAt.set(WANTED, Date.now() - 1000);
94
- const name = fmp4Format.segmentFileName(WANTED);
95
-
96
- await manager.getFileStream(SESSION_ID, name, { requestSeq: 1 });
97
-
98
- assert.equal(session.seekTarget, null, "one poll is not yet evidence");
99
-
100
- await manager.getFileStream(SESSION_ID, name, { requestSeq: 2 });
101
-
102
- assert.equal(
103
- session.seekTarget,
104
- WANTED - 1,
105
- "the encoder must be moved back to it — one segment early, for the preceding keyframe"
106
- );
107
- });
108
-
109
- test("a burst of different segments behind the run is a scan, and moves nothing", async (t) => {
110
- const { manager, session, dirPath } = await managerWithRunAhead();
111
- t.after(async () => {
112
- await manager.disposeAll();
113
- await rm(dirPath, { recursive: true, force: true });
114
- });
115
- // What hls.js does on a seek: dozens of indices within half a second, each
116
- // abandoned. Field log 2026-08-02: #178, #681, #725, #807, #74, #245, #387.
117
- const scanned = [WANTED, WANTED - 40, WANTED - 120, WANTED - 200, WANTED - 300];
118
- for (const index of scanned) {
119
- session.firstWantedAt.set(index, Date.now() - 1000);
120
- }
121
- // Asked ONCE each, which is what a scan is: the player opens them together
122
- // and abandons them together. Field log 2026-08-02 — #178, #681, #725, #807,
123
- // #74, #245, #387 within half a second, none of them repeated.
124
- for (const index of scanned) {
125
- await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(index), { requestSeq: 1 });
126
- }
127
-
128
- assert.equal(
129
- session.seekTarget,
130
- null,
131
- "moving the encoder to one of these would be moving it to a number the player picked at random"
132
- );
133
- });
134
-
135
- test("a request behind the run is left alone at first", async (t) => {
136
- const { manager, session, dirPath } = await managerWithRunAhead();
137
- t.after(async () => {
138
- await manager.disposeAll();
139
- await rm(dirPath, { recursive: true, force: true });
140
- });
141
-
142
- await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(WANTED), { requestSeq: 1 });
143
-
144
- assert.equal(
145
- session.seekTarget,
146
- null,
147
- "a burst around a reported seek settles by itself, and the seek is what should move the encoder"
148
- );
149
- });
150
-
151
- test("a seek already settling is not overridden", async (t) => {
152
- const { manager, session, dirPath } = await managerWithRunAhead();
153
- t.after(async () => {
154
- await manager.disposeAll();
155
- await rm(dirPath, { recursive: true, force: true });
156
- });
157
- session.firstWantedAt.set(WANTED, Date.now() - 4000);
158
- // The viewer has stated where they are and it is about to be acted on.
159
- session.seekTarget = 1200;
160
- session.seekSettleTimer = setTimeout(() => {}, 60_000);
161
- t.after(() => clearTimeout(session.seekSettleTimer));
162
-
163
- await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(WANTED), { requestSeq: 1 });
164
-
165
- assert.equal(
166
- session.seekTarget,
167
- 1200,
168
- "a statement from the viewer outranks anything inferred from what the player is fetching"
169
- );
170
- });
171
-
172
- test("a playlist scan far below the run is left where it belongs", async (t) => {
173
- const { manager, session, dirPath } = await managerWithRunAhead();
174
- t.after(async () => {
175
- await manager.disposeAll();
176
- await rm(dirPath, { recursive: true, force: true });
177
- });
178
- // A player that cannot get what it wants scans the playlist: field log
179
- // 2026-08-02, probes at #178, #681, #725, #807, #74, #245, #387 within half a
180
- // second. Steering on the lowest of those put the encoder at the start of the
181
- // film and left the viewer's own requests unreachable ahead of it — the exact
182
- // reason request-steering was removed from this proxy.
183
- const probe = 74;
184
- session.firstWantedAt.set(probe, Date.now() - 30_000);
185
-
186
- await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(probe), { requestSeq: 1 });
187
-
188
- assert.equal(
189
- session.seekTarget,
190
- null,
191
- "a misplaced run is out by a buffer; a scan probe is out by anything, and the two must not be confused"
192
- );
193
- });
194
-
195
- test("a rung whose encoder was stopped is not brought back by a held request", async (t) => {
196
- const { manager, session, dirPath } = await managerWithRunAhead();
197
- t.after(async () => {
198
- await manager.disposeAll();
199
- await rm(dirPath, { recursive: true, force: true });
200
- });
201
- // What a quality switch leaves behind: the rung nobody is watching, parked.
202
- session.runs = new Set();
203
- session.firstWantedAt.set(WANTED, Date.now() - 4000);
204
-
205
- await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(WANTED), { requestSeq: 1 });
206
-
207
- assert.equal(
208
- session.seekTarget,
209
- null,
210
- "restarting it would put a second encoder on a host sized for one, for a rung nobody is watching"
211
- );
212
- });
213
-
214
- test("a request ahead of the run is not touched", async (t) => {
215
- const { manager, session, dirPath } = await managerWithRunAhead();
216
- t.after(async () => {
217
- await manager.disposeAll();
218
- await rm(dirPath, { recursive: true, force: true });
219
- });
220
- const ahead = RUN_STARTS_AT + 400;
221
- session.firstWantedAt.set(ahead, Date.now() - 30_000);
222
-
223
- await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(ahead), { requestSeq: 1 });
224
-
225
- assert.equal(
226
- session.seekTarget,
227
- null,
228
- "the running encode may yet reach it; restarting on a far request is what produced nine restarts in a minute"
229
- );
230
- });
231
-
232
- test("a request far beyond the repair's reach is answered at once, not held", async (t) => {
233
- const { manager, session, dirPath } = await managerWithRunAhead();
234
- t.after(async () => {
235
- await manager.disposeAll();
236
- await rm(dirPath, { recursive: true, force: true });
237
- });
238
- // What a track change does: hls.js asks the new stream for segment #0 while
239
- // the run is hundreds of segments in. The repair cannot reach it, no seek is
240
- // coming, and the run only moves forward — so it can never be produced.
241
- // Measured 2026-08-15: held for the full minute, and the viewer watched a
242
- // spinner for 63 s after a track that had been made ready in 7.
243
- const answer = await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(0), { requestSeq: 1 });
244
-
245
- assert.equal(answer.kind, "not-found", "answered, so the player can move on to what it can have");
246
- assert.equal(session.seekTarget, null, "and the encoder was not sent to the start of the film for it");
247
- });
248
-
249
- test("a request just behind the run is still held, because the repair will fetch it", async (t) => {
250
- const { manager, session, dirPath } = await managerWithRunAhead();
251
- t.after(async () => {
252
- await manager.disposeAll();
253
- await rm(dirPath, { recursive: true, force: true });
254
- });
255
- const name = fmp4Format.segmentFileName(WANTED);
256
- session.firstWantedAt.set(WANTED, Date.now() - 1000);
257
-
258
- const answer = await manager.getFileStream(SESSION_ID, name, { requestSeq: 1 });
259
-
260
- assert.equal(answer.kind, "warming-up", "within reach: the encoder is about to be moved there");
261
- });